feat:播放列表拖拽排序;播放界面ui优化;插件页滚动问题;歌曲加载状提示;接入window系统的 AMTC 控制

This commit is contained in:
sqj
2025-09-15 20:43:44 +08:00
parent 65e876a2e9
commit 6692751c62
74 changed files with 1240 additions and 745 deletions

13
.eslintrc.backup.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": [
"@electron-toolkit/eslint-config-ts",
"@electron-toolkit/eslint-config-prettier"
],
"rules": {
"vue/require-default-prop": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"no-console": "warn"
}
}

View File

@@ -2134,7 +2134,6 @@ function warn$1(msg, ...args) {
const trace = getComponentTrace()
if (appWarnHandler) {
callWithErrorHandling(appWarnHandler, instance, 11, [
// eslint-disable-next-line no-restricted-syntax
msg +
args
.map((a) => {
@@ -2700,7 +2699,6 @@ function setDevtoolsHook$1(hook, target) {
// (#4815)
typeof window !== 'undefined' && // some envs mock window but not fully
window.HTMLElement && // also exclude jsdom
// eslint-disable-next-line no-restricted-syntax
!((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null
? void 0
: _b.includes('jsdom'))
@@ -6521,10 +6519,10 @@ function normalizePropsOptions(comp, appContext, asMixin = false) {
shouldCast = isFunction(propType) && propType.name === 'Boolean'
}
prop[0] =
/* shouldCast */
/* shouldCast */
shouldCast
prop[1] =
/* shouldCastTrue */
/* shouldCastTrue */
shouldCastTrue
if (shouldCast || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
@@ -9613,7 +9611,7 @@ function hydrateSuspense(
parentSuspense,
parentComponent,
node.parentNode,
// eslint-disable-next-line no-restricted-globals
document.createElement('div'),
null,
namespace,

View File

@@ -3985,11 +3985,11 @@ var walker = (
const result = isEmptyObject(innerAnnotations)
? {
transformedValue,
annotations: !!transformationResult ? [transformationResult.type] : void 0
annotations: transformationResult ? [transformationResult.type] : void 0
}
: {
transformedValue,
annotations: !!transformationResult
annotations: transformationResult
? [transformationResult.type, innerAnnotations]
: innerAnnotations
}

235
eslint.config.js Normal file
View File

@@ -0,0 +1,235 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import vue from 'eslint-plugin-vue'
import prettier from '@electron-toolkit/eslint-config-prettier'
export default [
// 基础 JavaScript 推荐配置
js.configs.recommended,
// TypeScript 推荐配置
...tseslint.configs.recommended,
// Vue 3 推荐配置
...vue.configs['flat/recommended'],
// 忽略的文件和目录
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/out/**',
'**/build/**',
'**/.vitepress/**',
'**/docs/**',
'**/website/**',
'**/coverage/**',
'**/*.min.js',
'**/auto-imports.d.ts',
'**/components.d.ts',
'src/preload/index.d.ts', // 忽略类型定义文件
'src/renderer/src/assets/icon_font/**', // 忽略第三方图标字体文件
'src/main/utils/musicSdk/**', // 忽略第三方音乐 SDK
'src/main/utils/request.js', // 忽略第三方请求库
'scripts/**', // 忽略脚本文件
'src/common/utils/lyricUtils/**' // 忽略第三方歌词工具
]
},
// 全局配置
{
files: ['**/*.{js,ts,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
// 代码质量 (放宽规则)
'no-unused-vars': 'off', // 由 TypeScript 处理
'no-undef': 'off', // 由 TypeScript 处理
'prefer-const': 'warn', // 降级为警告
'no-var': 'warn', // 降级为警告
'no-duplicate-imports': 'off', // 允许重复导入
'no-useless-return': 'off',
'no-useless-concat': 'off',
'no-useless-escape': 'off',
'no-unreachable': 'warn',
'no-debugger': 'off',
// 代码风格 (大幅放宽)
eqeqeq: 'off', // 允许 == 和 ===
curly: 'off', // 允许不使用大括号
'brace-style': 'off',
'comma-dangle': 'off',
quotes: 'off',
semi: 'off',
indent: 'off',
'object-curly-spacing': 'off',
'array-bracket-spacing': 'off',
'space-before-function-paren': 'off',
// 最佳实践 (放宽)
'no-eval': 'warn',
'no-implied-eval': 'warn',
'no-new-func': 'warn',
'no-alert': 'off',
'no-empty': 'off', // 允许空块
'no-extra-boolean-cast': 'off',
'no-extra-semi': 'off',
'no-irregular-whitespace': 'off',
'no-multiple-empty-lines': 'off',
'no-trailing-spaces': 'off',
'eol-last': 'off',
'no-fallthrough': 'off', // 允许 switch case 穿透
'no-case-declarations': 'off', // 允许 case 中声明变量
'no-empty-pattern': 'off', // 允许空对象模式
'no-prototype-builtins': 'off', // 允许直接调用 hasOwnProperty
'no-self-assign': 'off', // 允许自赋值
'no-async-promise-executor': 'off' // 允许异步 Promise 执行器
}
},
// 主进程 TypeScript 配置
{
files: ['src/main/**/*.ts', 'src/preload/**/*.ts', 'src/common/**/*.ts', 'src/types/**/*.ts'],
languageOptions: {
parserOptions: {
project: './tsconfig.node.json',
tsconfigRootDir: process.cwd()
}
},
rules: {
// TypeScript 特定规则 (大幅放宽)
'@typescript-eslint/no-unused-vars': 'off', // 完全关闭未使用变量检查
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off', // 允许 require
'@typescript-eslint/ban-ts-comment': 'off', // 允许 @ts-ignore
'@typescript-eslint/no-empty-function': 'off', // 允许空函数
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-unused-expressions': 'off', // 允许未使用的表达式
'@typescript-eslint/no-require-imports': 'off', // 允许 require 导入
'@typescript-eslint/no-unsafe-function-type': 'off', // 允许 Function 类型
'@typescript-eslint/prefer-as-const': 'off' // 允许字面量类型
}
},
// 渲染进程 TypeScript 配置
{
files: ['src/renderer/**/*.ts'],
languageOptions: {
parserOptions: {
project: './tsconfig.web.json',
tsconfigRootDir: process.cwd()
}
},
rules: {
// TypeScript 特定规则 (大幅放宽)
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/prefer-as-const': 'off'
}
},
// Vue 特定配置
{
files: ['src/renderer/**/*.vue'],
languageOptions: {
parserOptions: {
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.vue']
}
},
rules: {
// Vue 特定规则 (大幅放宽)
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off', // 允许 v-html
'vue/require-default-prop': 'off',
'vue/require-explicit-emits': 'off', // 不强制显式 emits
'vue/component-definition-name-casing': 'off',
'vue/component-name-in-template-casing': 'off',
'vue/custom-event-name-casing': 'off', // 允许任意事件命名
'vue/define-macros-order': 'off',
'vue/html-self-closing': 'off',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/no-side-effects-in-computed-properties': 'off', // 允许计算属性中的副作用
'vue/no-required-prop-with-default': 'off', // 允许带默认值的必需属性
// TypeScript 在 Vue 中的规则 (放宽)
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off'
}
},
// 主进程文件配置 (Node.js 环境)
{
files: [
'src/main/**/*.{ts,js}',
'src/preload/**/*.{ts,js}',
'electron.vite.config.*',
'scripts/**/*.{js,ts}'
],
languageOptions: {
globals: {
__dirname: 'readonly',
__filename: 'readonly',
Buffer: 'readonly',
process: 'readonly',
global: 'readonly'
}
},
rules: {
// Node.js 特定规则 (放宽)
'no-console': 'off',
'no-process-exit': 'off' // 允许 process.exit()
}
},
// 渲染进程文件配置 (浏览器环境)
{
files: ['src/renderer/**/*.{ts,js,vue}'],
languageOptions: {
globals: {
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
console: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly'
}
},
rules: {
// 浏览器环境特定规则
'no-console': 'off'
}
},
// 配置文件特殊规则
{
files: ['*.config.{js,ts}', 'vite.config.*', 'electron.vite.config.*'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-var-requires': 'off'
}
},
// Prettier 配置 (必须放在最后)
prettier
]

View File

@@ -1,102 +0,0 @@
const baseRule = {
'no-new': 'off',
camelcase: 'off',
'no-return-assign': 'off',
'space-before-function-paren': ['error', 'never'],
'no-var': 'error',
'no-fallthrough': 'off',
eqeqeq: 'off',
'require-atomic-updates': ['error', { allowProperties: true }],
'no-multiple-empty-lines': [1, { max: 2 }],
'comma-dangle': [2, 'always-multiline'],
'standard/no-callback-literal': 'off',
'prefer-const': 'off',
'no-labels': 'off',
'node/no-callback-literal': 'off',
'multiline-ternary': 'off'
}
const typescriptRule = {
...baseRule,
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/restrict-template-expressions': [
1,
{
allowBoolean: true,
allowAny: true
}
],
'@typescript-eslint/restrict-plus-operands': [
1,
{
allowBoolean: true,
allowAny: true
}
],
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: {
arguments: false,
attributes: false
}
}
],
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/comma-dangle': 'off',
'@typescript-eslint/no-unsafe-argument': 'off'
}
const vueRule = {
...typescriptRule,
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/use-v-on-exact': 'off'
}
export const base = {
extends: ['standard'],
rules: baseRule,
parser: '@babel/eslint-parser'
}
export const html = {
files: ['*.html'],
plugins: ['html']
}
export const typescript = {
files: ['*.ts'],
rules: typescriptRule,
parser: '@typescript-eslint/parser',
extends: ['standard-with-typescript']
}
export const vue = {
files: ['*.vue'],
rules: vueRule,
parser: 'vue-eslint-parser',
extends: [
// 'plugin:vue/vue3-essential',
'plugin:vue/base',
'plugin:vue/vue3-recommended',
'plugin:vue-pug/vue3-recommended',
// "plugin:vue/strongly-recommended"
'standard-with-typescript'
],
parserOptions: {
sourceType: 'module',
parser: {
// Script parser for `<script>`
js: '@typescript-eslint/parser',
// Script parser for `<script lang="ts">`
ts: '@typescript-eslint/parser'
},
extraFileExtensions: ['.vue']
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "ceru-music",
"version": "1.2.8",
"version": "1.3.0",
"description": "一款简洁优雅的音乐播放器",
"main": "./out/main/index.js",
"author": "sqj,wldss,star",

View File

@@ -26,7 +26,7 @@ export function compareVer(currentVer: string, targetVer: string): -1 | 0 | 1 {
.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)
const 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]

View File

@@ -27,10 +27,10 @@ export const toDateObj = (date: any): Date | '' => {
switch (typeof date) {
case 'string':
if (!date.includes('T')) date = date.split('.')[0].replace(/-/g, '/')
// eslint-disable-next-line no-fallthrough
case 'number':
date = new Date(date)
// eslint-disable-next-line no-fallthrough
case 'object':
break
default:

View File

@@ -24,13 +24,13 @@ const headExp = /^.*\[id:\$\w+\]\n/
const parseLyric = (str) => {
str = str.replace(/\r/g, '')
if (headExp.test(str)) str = str.replace(headExp, '')
let trans = str.match(/\[language:([\w=\\/+]+)\]/)
const trans = str.match(/\[language:([\w=\\/+]+)\]/)
let lyric
let rlyric
let tlyric
if (trans) {
str = str.replace(/\[language:[\w=\\/+]+\]\n/, '')
let json = JSON.parse(Buffer.from(trans[1], 'base64').toString())
const json = JSON.parse(Buffer.from(trans[1], 'base64').toString())
for (const item of json.content) {
switch (item.type) {
case 0:
@@ -44,23 +44,23 @@ const parseLyric = (str) => {
}
let i = 0
let crlyric = str.replace(/\[((\d+),\d+)\].*/g, (str) => {
let result = str.match(/\[((\d+),\d+)\].*/)
let lineStartTime = parseInt(result[2]) // 行开始时间
const result = str.match(/\[((\d+),\d+)\].*/)
const lineStartTime = parseInt(result[2]) // 行开始时间
let time = lineStartTime
let ms = time % 1000
const ms = time % 1000
time /= 1000
let m = parseInt(time / 60)
const m = parseInt(time / 60)
.toString()
.padStart(2, '0')
time %= 60
let s = parseInt(time).toString().padStart(2, '0')
const s = parseInt(time).toString().padStart(2, '0')
time = `${m}:${s}.${ms}`
if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}`
if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}`
i++
// 保持原始的 [start,duration] 格式,将相对时间戳转换为绝对时间戳
let processedStr = str.replace(/<(\d+),(\d+),(\d+)>/g, (match, start, duration, param) => {
const processedStr = str.replace(/<(\d+),(\d+),(\d+)>/g, (match, start, duration, param) => {
const absoluteStart = lineStartTime + parseInt(start)
return `(${absoluteStart},${duration},${param})`
})

View File

@@ -38,7 +38,7 @@ const handleScrollY = (
// @ts-expect-error
const start = element.scrollTop ?? element.scrollY ?? 0
if (to > start) {
let maxScrollTop = element.scrollHeight - element.clientHeight
const maxScrollTop = element.scrollHeight - element.clientHeight
if (to > maxScrollTop) to = maxScrollTop
} else if (to < start) {
if (to < 0) to = 0
@@ -55,7 +55,7 @@ const handleScrollY = (
let currentTime = 0
let val: number
let key = Math.random()
const key = Math.random()
const animateScroll = () => {
element.lx_scrollTimeout = undefined
@@ -156,7 +156,7 @@ const handleScrollX = (
// @ts-expect-error
const start = element.scrollLeft || element.scrollX || 0
if (to > start) {
let maxScrollLeft = element.scrollWidth - element.clientWidth
const maxScrollLeft = element.scrollWidth - element.clientWidth
if (to > maxScrollLeft) to = maxScrollLeft
} else if (to < start) {
if (to < 0) to = 0
@@ -173,7 +173,7 @@ const handleScrollX = (
let currentTime = 0
let val: number
let key = Math.random()
const key = Math.random()
const animateScroll = () => {
element.lx_scrollTimeout = undefined
@@ -272,7 +272,7 @@ const handleScrollXR = (
// @ts-expect-error
const start = element.scrollLeft || (element.scrollX as number) || 0
if (to < start) {
let maxScrollLeft = -element.scrollWidth + element.clientWidth
const maxScrollLeft = -element.scrollWidth + element.clientWidth
if (to < maxScrollLeft) to = maxScrollLeft
} else if (to > start) {
if (to > 0) to = 0
@@ -290,7 +290,7 @@ const handleScrollXR = (
let currentTime = 0
let val: number
let key = Math.random()
const key = Math.random()
const animateScroll = () => {
element.lx_scrollTimeout = undefined
@@ -371,7 +371,7 @@ export const scrollXRTo = (
/**
* 设置标题
*/
let dom_title = document.getElementsByTagName('title')[0]
const dom_title = document.getElementsByTagName('title')[0]
export const setTitle = (title: string | null) => {
title ||= 'LX Music'
dom_title.innerText = title

View File

@@ -51,14 +51,14 @@ export default {
...sources,
init() {
const tasks = []
for (let source of sources.sources) {
let sm = sources[source.id]
for (const source of sources.sources) {
const sm = sources[source.id]
sm && sm.init && tasks.push(sm.init())
}
return Promise.all(tasks)
},
async searchMusic({ name, singer, source: s, limit = 25 }) {
const trimStr = (str) => (typeof str == 'string' ? str.trim() : str)
const trimStr = (str) => (typeof str === 'string' ? str.trim() : str)
const musicName = trimStr(name)
const tasks = []
const excludeSource = ['xm']
@@ -106,7 +106,7 @@ export default {
const getIntv = (interval) => {
if (!interval) return 0
// if (musicInfo._interval) return musicInfo._interval
let intvArr = interval.split(':')
const intvArr = interval.split(':')
let intv = 0
let unit = 1
while (intvArr.length) {
@@ -115,9 +115,9 @@ export default {
}
return intv
}
const trimStr = (str) => (typeof str == 'string' ? str.trim() : str || '')
const trimStr = (str) => (typeof str === 'string' ? str.trim() : str || '')
const filterStr = (str) =>
typeof str == 'string'
typeof str === 'string'
? str.replace(/\s|'|\.|,||&|"|、|\(|\)|||`|~|-|<|>|\||\/|\]|\[|!|/g, '')
: String(str || '')
const fMusicName = filterStr(name).toLowerCase()

View File

@@ -47,7 +47,7 @@ export default {
)
if (!albumList.info) return Promise.reject(new Error('Get album list failed.'))
let result = await getMusicInfosByList(albumList.info)
const result = await getMusicInfosByList(albumList.info)
const info = await this.getAlbumInfo(id)

View File

@@ -12,7 +12,7 @@ export default {
// const res_id = (await getMusicInfoRaw(hash)).classification?.[0]?.res_id
// if (!res_id) throw new Error('获取评论失败')
let timestamp = Date.now()
const timestamp = Date.now()
const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0`
// const params = `appid=1005&clienttime=${timestamp}&clienttoken=0&clientver=11409&code=fc4be23b4e972707f36b8a828a93ba8a&dfid=0&extdata=${hash}&kugouid=0&mid=16249512204336365674023395779019&mixsongid=${res_id}&p=${page}&pagesize=${limit}&uuid=0&ver=10`
const _requestObj = httpFetch(
@@ -40,7 +40,7 @@ export default {
async getHotComment({ hash }, page = 1, limit = 20) {
// console.log(songmid)
if (this._requestObj2) this._requestObj2.cancelHttp()
let timestamp = Date.now()
const timestamp = Date.now()
const params = `dfid=0&mid=16249512204336365674023395779019&clienttime=${timestamp}&uuid=0&extdata=${hash}&appid=1005&code=fc4be23b4e972707f36b8a828a93ba8a&schash=${hash}&clientver=11409&p=${page}&clienttoken=&pagesize=${limit}&ver=10&kugouid=0`
// https://github.com/GitHub-ZC/wp_MusicApi/blob/bf9307dd138dc8ac6c4f7de29361209d4f5b665f/routes/v1/kugou/comment.js#L53
const _requestObj2 = httpFetch(
@@ -94,7 +94,7 @@ export default {
},
filterComment(rawList) {
return rawList.map((item) => {
let data = {
const data = {
id: item.id,
text: decodeName(
(item.atlist ? this.replaceAt(item.content, item.atlist) : item.content) || ''

View File

@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate } from '../index'
import { formatSingerName } from '../utils'
let boardList = [
const boardList = [
{ id: 'kg__8888', name: 'TOP500', bangid: '8888' },
{ id: 'kg__6666', name: '飙升榜', bangid: '6666' },
{ id: 'kg__59703', name: '蜂鸟流行音乐榜', bangid: '59703' },
@@ -137,7 +137,7 @@ export default {
return requestDataObj.promise
},
getSinger(singers) {
let arr = []
const arr = []
singers.forEach((singer) => {
arr.push(singer.author_name)
})
@@ -149,7 +149,7 @@ export default {
const types = []
const _types = {}
if (item.filesize !== 0) {
let size = sizeFormate(item.filesize)
const size = sizeFormate(item.filesize)
types.push({ type: '128k', size, hash: item.hash })
_types['128k'] = {
size,
@@ -157,7 +157,7 @@ export default {
}
}
if (item['320filesize'] !== 0) {
let size = sizeFormate(item['320filesize'])
const size = sizeFormate(item['320filesize'])
types.push({ type: '320k', size, hash: item['320hash'] })
_types['320k'] = {
size,
@@ -165,7 +165,7 @@ export default {
}
}
if (item.sqfilesize !== 0) {
let size = sizeFormate(item.sqfilesize)
const size = sizeFormate(item.sqfilesize)
types.push({ type: 'flac', size, hash: item.sqhash })
_types.flac = {
size,
@@ -173,7 +173,7 @@ export default {
}
}
if (item.filesize_high !== 0) {
let size = sizeFormate(item.filesize_high)
const size = sizeFormate(item.filesize_high)
types.push({ type: 'flac24bit', size, hash: item.hash_high })
_types.flac24bit = {
size,
@@ -201,7 +201,7 @@ export default {
filterBoardsData(rawList) {
// console.log(rawList)
let list = []
const list = []
for (const board of rawList) {
if (board.isvol != 1) continue
list.push({
@@ -243,9 +243,9 @@ export default {
if (body.errcode != 0) return this.getList(bangid, page, retryNum)
// console.log(body)
let total = body.data.total
let limit = 100
let listData = this.filterData(body.data.info)
const total = body.data.total
const limit = 100
const listData = this.filterData(body.data.info)
// console.log(listData)
return {
total,
@@ -256,7 +256,7 @@ export default {
}
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('kg__', '')
if (typeof id === 'string') id = id.replace('kg__', '')
return `https://www.kugou.com/yy/rank/home/1-${id}.html`
}
}

View File

@@ -4,7 +4,7 @@ import { decodeKrc } from '../../../../common/utils/lyricUtils/kg'
export default {
getIntv(interval) {
if (!interval) return 0
let intvArr = interval.split(':')
const intvArr = interval.split(':')
let intv = 0
let unit = 1
while (intvArr.length) {
@@ -36,7 +36,7 @@ export default {
// return requestObj
// },
searchLyric(name, hash, time, tryNum = 0) {
let requestObj = httpFetch(
const requestObj = httpFetch(
`http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=${encodeURIComponent(name)}&hash=${hash}&timelength=${time}&lrctxt=1`,
{
headers: {
@@ -49,12 +49,12 @@ export default {
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
if (statusCode !== 200) {
if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))
let tryRequestObj = this.searchLyric(name, hash, time, ++tryNum)
const tryRequestObj = this.searchLyric(name, hash, time, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
if (body.candidates.length) {
let info = body.candidates[0]
const info = body.candidates[0]
return {
id: info.id,
accessKey: info.accesskey,
@@ -66,7 +66,7 @@ export default {
return requestObj
},
getLyricDownload(id, accessKey, fmt, tryNum = 0) {
let requestObj = httpFetch(
const requestObj = httpFetch(
`http://lyrics.kugou.com/download?ver=1&client=pc&id=${id}&accesskey=${accessKey}&fmt=${fmt}&charset=utf8`,
{
headers: {
@@ -79,7 +79,7 @@ export default {
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
if (statusCode !== 200) {
if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))
let tryRequestObj = this.getLyric(id, accessKey, fmt, ++tryNum)
const tryRequestObj = this.getLyric(id, accessKey, fmt, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
@@ -102,7 +102,7 @@ export default {
return requestObj
},
getLyric(songInfo, tryNum = 0) {
let requestObj = this.searchLyric(
const requestObj = this.searchLyric(
songInfo.name,
songInfo.hash,
songInfo._interval || this.getIntv(songInfo.interval)
@@ -111,7 +111,7 @@ export default {
requestObj.promise = requestObj.promise.then((result) => {
if (!result) return Promise.reject(new Error('Get lyric failed'))
let requestObj2 = this.getLyricDownload(result.id, result.accessKey, result.fmt)
const requestObj2 = this.getLyricDownload(result.id, result.accessKey, result.fmt)
requestObj.cancelHttp = requestObj2.cancelHttp.bind(requestObj2)

View File

@@ -2,7 +2,7 @@ import { decodeName, formatPlayTime, sizeFormate } from '../../index'
import { createHttpFetch } from './util'
const createGetMusicInfosTask = (hashs) => {
let data = {
const data = {
area_code: '1',
show_privilege: 1,
show_album_info: '1',
@@ -16,13 +16,13 @@ const createGetMusicInfosTask = (hashs) => {
fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname,classification'
}
let list = hashs
let tasks = []
const tasks = []
while (list.length) {
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
if (list.length < 100) break
list = list.slice(100)
}
let url = 'http://gateway.kugou.com/v3/album_audio/audio'
const url = 'http://gateway.kugou.com/v3/album_audio/audio'
return tasks.map((task) =>
createHttpFetch(url, {
method: 'POST',
@@ -41,8 +41,8 @@ const createGetMusicInfosTask = (hashs) => {
export const filterMusicInfoList = (rawList) => {
// console.log(rawList)
let ids = new Set()
let list = []
const ids = new Set()
const list = []
rawList.forEach((item) => {
if (!item) return
if (ids.has(item.audio_info.audio_id)) return
@@ -50,7 +50,7 @@ export const filterMusicInfoList = (rawList) => {
const types = []
const _types = {}
if (item.audio_info.filesize !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize))
const size = sizeFormate(parseInt(item.audio_info.filesize))
types.push({ type: '128k', size, hash: item.audio_info.hash })
_types['128k'] = {
size,
@@ -58,7 +58,7 @@ export const filterMusicInfoList = (rawList) => {
}
}
if (item.audio_info.filesize_320 !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_320))
const size = sizeFormate(parseInt(item.audio_info.filesize_320))
types.push({ type: '320k', size, hash: item.audio_info.hash_320 })
_types['320k'] = {
size,
@@ -66,7 +66,7 @@ export const filterMusicInfoList = (rawList) => {
}
}
if (item.audio_info.filesize_flac !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_flac))
const size = sizeFormate(parseInt(item.audio_info.filesize_flac))
types.push({ type: 'flac', size, hash: item.audio_info.hash_flac })
_types.flac = {
size,
@@ -74,7 +74,7 @@ export const filterMusicInfoList = (rawList) => {
}
}
if (item.audio_info.filesize_high !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_high))
const size = sizeFormate(parseInt(item.audio_info.filesize_high))
types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high })
_types.flac24bit = {
size,

View File

@@ -17,7 +17,7 @@ export default {
const types = []
const _types = {}
if (rawData.FileSize !== 0) {
let size = sizeFormate(rawData.FileSize)
const size = sizeFormate(rawData.FileSize)
types.push({ type: '128k', size, hash: rawData.FileHash })
_types['128k'] = {
size,
@@ -25,7 +25,7 @@ export default {
}
}
if (rawData.HQFileSize !== 0) {
let size = sizeFormate(rawData.HQFileSize)
const size = sizeFormate(rawData.HQFileSize)
types.push({ type: '320k', size, hash: rawData.HQFileHash })
_types['320k'] = {
size,
@@ -33,7 +33,7 @@ export default {
}
}
if (rawData.SQFileSize !== 0) {
let size = sizeFormate(rawData.SQFileSize)
const size = sizeFormate(rawData.SQFileSize)
types.push({ type: 'flac', size, hash: rawData.SQFileHash })
_types.flac = {
size,
@@ -41,7 +41,7 @@ export default {
}
}
if (rawData.ResFileSize !== 0) {
let size = sizeFormate(rawData.ResFileSize)
const size = sizeFormate(rawData.ResFileSize)
types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash })
_types.flac24bit = {
size,
@@ -67,7 +67,7 @@ export default {
}
},
handleResult(rawData) {
let ids = new Set()
const ids = new Set()
const list = []
rawData.forEach((item) => {
const key = item.Audioid + item.FileHash
@@ -89,7 +89,7 @@ export default {
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page, limit).then((result) => {
if (!result || result.error_code !== 0) return this.search(str, page, limit, retryNum)
let list = this.handleResult(result.data.lists)
const list = this.handleResult(result.data.lists)
if (list == null) return this.search(str, page, limit, retryNum)

View File

@@ -36,7 +36,7 @@ export default {
})
return requestObj.promise.then(({ body }) => {
if (body.error_code !== 0) return Promise.reject(new Error('图片获取失败'))
let info = body.data[0].info
const info = body.data[0].info
const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image
if (!img) return Promise.reject(new Error('Pic get failed'))
return img

View File

@@ -71,10 +71,10 @@ export default {
if (tryNum > 2) throw new Error('try max num')
const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise
let listData = body.match(this.regExps.listData)
let listInfo = body.match(this.regExps.listInfo)
const listData = body.match(this.regExps.listData)
const listInfo = body.match(this.regExps.listInfo)
if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)
let list = await this.getMusicInfos(JSON.parse(listData[1]))
const list = await this.getMusicInfos(JSON.parse(listData[1]))
// listData = this.filterData(JSON.parse(listData[1]))
let name
let pic
@@ -82,7 +82,7 @@ export default {
name = listInfo[1]
pic = listInfo[2]
}
let desc = this.parseHtmlDesc(body)
const desc = this.parseHtmlDesc(body)
return {
list,
@@ -116,7 +116,7 @@ export default {
const result = []
if (rawData.status !== 1) return result
for (const key of Object.keys(rawData.data)) {
let tag = rawData.data[key]
const tag = rawData.data[key]
result.push({
id: tag.special_id,
name: tag.special_name,
@@ -219,7 +219,7 @@ export default {
},
createTask(hashs) {
let data = {
const data = {
area_code: '1',
show_privilege: 1,
show_album_info: '1',
@@ -233,13 +233,13 @@ export default {
fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname'
}
let list = hashs
let tasks = []
const tasks = []
while (list.length) {
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
if (list.length < 100) break
list = list.slice(100)
}
let url = 'http://gateway.kugou.com/v2/album_audio/audio'
const url = 'http://gateway.kugou.com/v2/album_audio/audio'
return tasks.map((task) =>
this.createHttp(url, {
method: 'POST',
@@ -283,7 +283,7 @@ export default {
// console.log(songInfo)
// type 1单曲2歌单3电台4酷狗码5别人的播放队列
let songList
let info = songInfo.info
const info = songInfo.info
switch (info.type) {
case 2:
if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id)
@@ -319,7 +319,7 @@ export default {
})
// console.log(songList)
}
let list = await this.getMusicInfos(songList || songInfo.list)
const list = await this.getMusicInfos(songList || songInfo.list)
return {
list,
page: 1,
@@ -354,7 +354,7 @@ export default {
this.getUserListDetail5(chain)
)
}
let list = await this.getMusicInfos(songInfo.list)
const list = await this.getMusicInfos(songInfo.list)
// console.log(info, songInfo)
return {
list,
@@ -373,7 +373,7 @@ export default {
},
deDuplication(datas) {
let ids = new Set()
const ids = new Set()
return datas.filter(({ hash }) => {
if (ids.has(hash)) return false
ids.add(hash)
@@ -408,9 +408,9 @@ export default {
},
async getUserListDetailByLink({ info }, link) {
let listInfo = info['0']
const listInfo = info['0']
let total = listInfo.count
let tasks = []
const tasks = []
let page = 0
while (total) {
const limit = total > 90 ? 90 : total
@@ -448,7 +448,7 @@ export default {
}
},
createGetListDetail2Task(id, total) {
let tasks = []
const tasks = []
let page = 0
while (total) {
const limit = total > 300 ? 300 : total
@@ -481,13 +481,13 @@ export default {
return Promise.all(tasks).then(([...datas]) => datas.flat())
},
async getUserListDetail2(global_collection_id) {
let id = global_collection_id
const id = global_collection_id
if (id.length > 1000) throw new Error('get list error')
const params =
'appid=1058&specialid=0&global_specialid=' +
id +
'&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'
let info = await this.createHttp(
const info = await this.createHttp(
`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`,
{
headers: {
@@ -501,7 +501,7 @@ export default {
}
)
const songInfo = await this.createGetListDetail2Task(id, info.songcount)
let list = await this.getMusicInfos(songInfo)
const list = await this.getMusicInfos(songInfo)
// console.log(info, songInfo, list)
return {
list,
@@ -534,7 +534,7 @@ export default {
},
async getUserListDetailByPcChain(chain) {
let key = `${chain}_pc_list`
const key = `${chain}_pc_list`
if (this.cache.has(key)) return this.cache.get(key)
const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {
headers: {
@@ -595,7 +595,7 @@ export default {
async getUserListDetailById(id, page, limit) {
const signature = await handleSignature(id, page, limit)
let info = await this.createHttp(
const info = await this.createHttp(
`https://pubsongscdn.kugou.com/v2/get_other_list_file?srcappid=2919&clientver=20000&appid=1058&type=0&module=playlist&page=${page}&pagesize=${limit}&specialid=${id}&signature=${signature}`,
{
headers: {
@@ -608,7 +608,7 @@ export default {
)
// console.log(info)
let result = await this.getMusicInfos(info.info)
const result = await this.getMusicInfos(info.info)
// console.log(info, songInfo)
return result
},
@@ -621,7 +621,7 @@ export default {
link.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
)
if (link.includes('gcid_')) {
let gcid = link.match(/gcid_\w+/)?.[0]
const gcid = link.match(/gcid_\w+/)?.[0]
if (gcid) {
const global_collection_id = await this.decodeGcid(gcid)
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
@@ -667,7 +667,7 @@ export default {
location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1')
)
if (location.includes('gcid_')) {
let gcid = link.match(/gcid_\w+/)?.[0]
const gcid = link.match(/gcid_\w+/)?.[0]
if (gcid) {
const global_collection_id = await this.decodeGcid(gcid)
if (global_collection_id) return this.getUserListDetail2(global_collection_id)
@@ -698,7 +698,7 @@ export default {
// console.log('location', location)
return this.getUserListDetail(location, page, ++retryNum)
}
if (typeof body == 'string') {
if (typeof body === 'string') {
let global_collection_id = body.match(/"global_collection_id":"(\w+)"/)?.[1]
if (!global_collection_id) {
let gcid = body.match(/"encode_gic":"(\w+)"/)?.[1]
@@ -735,7 +735,7 @@ export default {
const types = []
const _types = {}
if (item.filesize !== 0) {
let size = sizeFormate(item.filesize)
const size = sizeFormate(item.filesize)
types.push({ type: '128k', size, hash: item.hash })
_types['128k'] = {
size,
@@ -743,7 +743,7 @@ export default {
}
}
if (item.filesize_320 !== 0) {
let size = sizeFormate(item.filesize_320)
const size = sizeFormate(item.filesize_320)
types.push({ type: '320k', size, hash: item.hash_320 })
_types['320k'] = {
size,
@@ -751,7 +751,7 @@ export default {
}
}
if (item.filesize_ape !== 0) {
let size = sizeFormate(item.filesize_ape)
const size = sizeFormate(item.filesize_ape)
types.push({ type: 'ape', size, hash: item.hash_ape })
_types.ape = {
size,
@@ -759,7 +759,7 @@ export default {
}
}
if (item.filesize_flac !== 0) {
let size = sizeFormate(item.filesize_flac)
const size = sizeFormate(item.filesize_flac)
types.push({ type: 'flac', size, hash: item.hash_flac })
_types.flac = {
size,
@@ -849,8 +849,8 @@ export default {
// hash list filter
filterData2(rawList) {
// console.log(rawList)
let ids = new Set()
let list = []
const ids = new Set()
const list = []
rawList.forEach((item) => {
if (!item) return
if (ids.has(item.audio_info.audio_id)) return
@@ -858,7 +858,7 @@ export default {
const types = []
const _types = {}
if (item.audio_info.filesize !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize))
const size = sizeFormate(parseInt(item.audio_info.filesize))
types.push({ type: '128k', size, hash: item.audio_info.hash })
_types['128k'] = {
size,
@@ -866,7 +866,7 @@ export default {
}
}
if (item.audio_info.filesize_320 !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_320))
const size = sizeFormate(parseInt(item.audio_info.filesize_320))
types.push({ type: '320k', size, hash: item.audio_info.hash_320 })
_types['320k'] = {
size,
@@ -874,7 +874,7 @@ export default {
}
}
if (item.audio_info.filesize_flac !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_flac))
const size = sizeFormate(parseInt(item.audio_info.filesize_flac))
types.push({ type: 'flac', size, hash: item.audio_info.hash_flac })
_types.flac = {
size,
@@ -882,7 +882,7 @@ export default {
}
}
if (item.audio_info.filesize_high !== '0') {
let size = sizeFormate(parseInt(item.audio_info.filesize_high))
const size = sizeFormate(parseInt(item.audio_info.filesize_high))
types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high })
_types.flac24bit = {
size,
@@ -927,7 +927,7 @@ export default {
// 获取列表数据
getList(sortId, tagId, page) {
let tasks = [this.getSongList(sortId, tagId, page)]
const tasks = [this.getSongList(sortId, tagId, page)]
tasks.push(
this.currentTagInfo.id === tagId
? Promise.resolve(this.currentTagInfo.info)
@@ -964,7 +964,7 @@ export default {
},
getDetailPageUrl(id) {
if (typeof id == 'string') {
if (typeof id === 'string') {
if (/^https?:\/\//.test(id)) return id
id = id.replace('id_', '')
}

View File

@@ -13,9 +13,9 @@ import { httpFetch } from '../../request'
export const signatureParams = (params, platform = 'android', body = '') => {
let keyparam = 'OIlwieks28dk2k092lksi2UIkp'
if (platform === 'web') keyparam = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'
let param_list = params.split('&')
const param_list = params.split('&')
param_list.sort()
let sign_params = `${keyparam}${param_list.join('')}${body}${keyparam}`
const sign_params = `${keyparam}${param_list.join('')}${body}${keyparam}`
return toMD5(sign_params)
}

View File

@@ -10,9 +10,9 @@ export default {
// console.log(rawList)
// console.log(rawList.length, rawList2.length)
return rawList.map((item, inedx) => {
let formats = item.formats.split('|')
let types = []
let _types = {}
const formats = item.formats.split('|')
const types = []
const _types = {}
if (formats.includes('MP3128')) {
types.push({ type: '128k', size: null })
_types['128k'] = {

View File

@@ -68,13 +68,13 @@ const kw = {
},
getMusicUrls(musicInfo, cb) {
let tasks = []
let songId = musicInfo.songmid
const tasks = []
const songId = musicInfo.songmid
musicInfo.types.forEach((type) => {
tasks.push(kw.getMusicUrl(songId, type.type).promise)
})
Promise.all(tasks).then((urlInfo) => {
let typeUrl = {}
const typeUrl = {}
urlInfo.forEach((info) => {
typeUrl[info.type] = info.url
})

View File

@@ -206,7 +206,7 @@ export default {
filterBoardsData(rawList) {
// console.log(rawList)
let list = []
const list = []
for (const board of rawList) {
if (board.source != '1') continue
list.push({

View File

@@ -148,8 +148,8 @@ export default {
}, */
sortLrcArr(arr) {
const lrcSet = new Set()
let lrc = []
let lrcT = []
const lrc = []
const lrcT = []
let isLyricx = false
for (const item of arr) {
@@ -192,11 +192,11 @@ export default {
},
parseLrc(lrc) {
const lines = lrc.split(/\r\n|\r|\n/)
let tags = []
let lrcArr = []
const tags = []
const lrcArr = []
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
let result = timeExp.exec(line)
const result = timeExp.exec(line)
if (result) {
const text = line.replace(timeExp, '').trim()
let time = RegExp.$1

View File

@@ -32,7 +32,7 @@ export default {
// console.log(rawData)
for (let i = 0; i < rawData.length; i++) {
const info = rawData[i]
let songId = info.MUSICRID.replace('MUSIC_', '')
const songId = info.MUSICRID.replace('MUSIC_', '')
// const format = (info.FORMATS || info.formats).split('|')
if (!info.N_MINFO) {
@@ -43,7 +43,7 @@ export default {
const types = []
const _types = {}
let infoArr = info.N_MINFO.split(';')
const infoArr = info.N_MINFO.split(';')
for (let info of infoArr) {
info = info.match(this.regExps.mInfo)
if (info) {
@@ -77,7 +77,7 @@ export default {
}
types.reverse()
let interval = parseInt(info.DURATION)
const interval = parseInt(info.DURATION)
result.push({
name: decodeName(info.SONGNAME),
@@ -109,7 +109,7 @@ export default {
// console.log(result)
if (!result || (result.TOTAL !== '0' && result.SHOW === '0'))
return this.search(str, page, limit, ++retryNum)
let list = this.handleResult(result.abslist)
const list = this.handleResult(result.abslist)
if (list == null) return this.search(str, page, limit, ++retryNum)

View File

@@ -95,7 +95,7 @@ export default {
let id
let type
if (tagId) {
let arr = tagId.split('-')
const arr = tagId.split('-')
id = arr[0]
type = arr[1]
} else {
@@ -235,9 +235,9 @@ export default {
filterBDListDetail(rawList) {
return rawList.map((item) => {
let types = []
let _types = {}
for (let info of item.audios) {
const types = []
const _types = {}
for (const info of item.audios) {
info.size = info.size?.toLocaleUpperCase()
switch (info.bitrate) {
case '4000':
@@ -415,9 +415,9 @@ export default {
filterListDetail(rawData) {
// console.log(rawData)
return rawData.map((item) => {
let infoArr = item.N_MINFO.split(';')
let types = []
let _types = {}
const infoArr = item.N_MINFO.split(';')
const types = []
const _types = {}
for (let info of infoArr) {
info = info.match(this.regExps.mInfo)
if (info) {
@@ -478,7 +478,7 @@ export default {
getDetailPageUrl(id) {
if (/[?&:/]/.test(id)) id = id.replace(this.regExps.listDetailLink, '$1')
else if (/^digest-/.test(id)) {
let result = id.split('__')
const result = id.split('__')
id = result[1]
}
return `http://www.kuwo.cn/playlist_detail/${id}`

View File

@@ -111,8 +111,8 @@ export const lrcTools = {
// 使用原始的酷我音乐时间计算逻辑,但输出绝对时间戳
const offset = parseInt(str)
const offset2 = parseInt(str2)
let startTime = Math.abs((offset + offset2) / (this.offset * 2))
let duration = Math.abs((offset - offset2) / (this.offset2 * 2))
const startTime = Math.abs((offset + offset2) / (this.offset * 2))
const duration = Math.abs((offset - offset2) / (this.offset2 * 2))
// 转换为基于行开始时间的绝对时间戳
const absoluteStartTime = lineStartTime + startTime

View File

@@ -9,7 +9,7 @@ export default {
async getComment(musicInfo, page = 1, limit = 10) {
if (this._requestObj) this._requestObj.cancelHttp()
if (!musicInfo.songId) {
let id = await getSongId(musicInfo)
const id = await getSongId(musicInfo)
if (!id) throw new Error('获取评论失败')
musicInfo.songId = id
}
@@ -40,7 +40,7 @@ export default {
if (this._requestObj2) this._requestObj2.cancelHttp()
if (!musicInfo.songId) {
let id = await getSongId(musicInfo)
const id = await getSongId(musicInfo)
if (!id) throw new Error('获取评论失败')
musicInfo.songId = id
}

View File

@@ -102,7 +102,7 @@ export default {
},
filterBoardsData(rawList) {
// console.log(rawList)
let list = []
const list = []
for (const board of rawList) {
if (board.template != 'group1') continue
for (const item of board.itemList) {
@@ -112,7 +112,7 @@ export default {
)
continue
let data = item.displayLogId.param
const data = item.displayLogId.param
list.push({
id: 'mg__' + data.rankId,
name: data.rankName,
@@ -164,7 +164,7 @@ export default {
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('mg__', '')
if (typeof id === 'string') id = id.replace('mg__', '')
for (const item of boardList) {
if (item.bangid == id) {
return `https://music.migu.cn/v3/music/top/${item.webId}`

View File

@@ -16,21 +16,21 @@ const mrcTools = {
for (const line of lines) {
if (line.length < 6) continue
let result = this.rxps.lineTime.exec(line)
const result = this.rxps.lineTime.exec(line)
if (!result) continue
const startTime = parseInt(result[1])
let time = startTime
let ms = time % 1000
const ms = time % 1000
time /= 1000
let m = parseInt(time / 60)
const m = parseInt(time / 60)
.toString()
.padStart(2, '0')
time %= 60
let s = parseInt(time).toString().padStart(2, '0')
const s = parseInt(time).toString().padStart(2, '0')
time = `${m}:${s}.${ms}`
let words = line.replace(this.rxps.lineTime, '')
const words = line.replace(this.rxps.lineTime, '')
lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`)
@@ -100,11 +100,11 @@ export default {
getLyricWeb(songInfo, tryNum = 0) {
// console.log(songInfo.copyrightId)
if (songInfo.lrcUrl) {
let requestObj = httpFetch(songInfo.lrcUrl)
const requestObj = httpFetch(songInfo.lrcUrl)
requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {
if (statusCode !== 200) {
if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))
let tryRequestObj = this.getLyricWeb(songInfo, ++tryNum)
const tryRequestObj = this.getLyricWeb(songInfo, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
@@ -115,7 +115,7 @@ export default {
})
return requestObj
} else {
let requestObj = httpFetch(
const requestObj = httpFetch(
`https://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=${songInfo.copyrightId}`,
{
headers: {
@@ -126,7 +126,7 @@ export default {
requestObj.promise = requestObj.promise.then(({ body }) => {
if (body.returnCode !== '000000' || !body.lyric) {
if (tryNum > 5) return Promise.reject(new Error('Get lyric failed'))
let tryRequestObj = this.getLyricWeb(songInfo, ++tryNum)
const tryRequestObj = this.getLyricWeb(songInfo, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}
@@ -140,9 +140,9 @@ export default {
},
getLyric(songInfo) {
let requestObj = mrcTools.getLyric(songInfo)
const requestObj = mrcTools.getLyric(songInfo)
requestObj.promise = requestObj.promise.catch(() => {
let webRequestObj = this.getLyricWeb(songInfo)
const webRequestObj = this.getLyricWeb(songInfo)
requestObj.cancelHttp = webRequestObj.cancelHttp.bind(webRequestObj)
return webRequestObj.promise
})

View File

@@ -4,13 +4,13 @@ import { formatSingerName } from '../utils'
const createGetMusicInfosTask = (ids) => {
let list = ids
let tasks = []
const tasks = []
while (list.length) {
tasks.push(list.slice(0, 100))
if (list.length < 100) break
list = list.slice(100)
}
let url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2'
const url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2'
return Promise.all(
tasks.map((task) =>
createHttpFetch(url, {
@@ -25,7 +25,7 @@ const createGetMusicInfosTask = (ids) => {
export const filterMusicInfoList = (rawList) => {
// console.log(rawList)
let ids = new Set()
const ids = new Set()
const list = []
rawList.forEach((item) => {
if (!item.songId || ids.has(item.songId)) return

View File

@@ -212,7 +212,7 @@ export default {
return Promise.reject(new Error(result ? result.info : '搜索失败'))
const songResultData = result.songResultData || { resultList: [], totalCount: 0 }
let list = this.filterData(songResultData.resultList)
const list = this.filterData(songResultData.resultList)
if (list == null) return this.search(str, page, limit, retryNum)
this.total = parseInt(songResultData.totalCount)

View File

@@ -3,7 +3,7 @@ import getSongId from './songId'
export default {
async getPicUrl(songId, tryNum = 0) {
let requestObj = httpFetch(
const requestObj = httpFetch(
`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songId}`,
{
headers: {
@@ -14,7 +14,7 @@ export default {
requestObj.promise.then(({ body }) => {
if (body.returnCode !== '000000') {
if (tryNum > 5) return Promise.reject(new Error('图片获取失败'))
let tryRequestObj = this.getPic(songId, ++tryNum)
const tryRequestObj = this.getPic(songId, ++tryNum)
requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)
return tryRequestObj.promise
}

View File

@@ -22,15 +22,15 @@ const teaDecrypt = (data, key) => {
let j2 = data[0]
let j3 = toLong((6n + 52n / lengthBitint) * DELTA)
while (true) {
let j4 = j3
const j4 = j3
if (j4 == 0n) break
let j5 = toLong(3n & toLong(j4 >> 2n))
const j5 = toLong(3n & toLong(j4 >> 2n))
let j6 = lengthBitint
while (true) {
j6--
if (j6 > 0n) {
let j7 = data[j6 - 1n]
let i = j6
const j7 = data[j6 - 1n]
const i = j6
j2 = toLong(
data[i] -
(toLong(toLong(j2 ^ j4) + toLong(j7 ^ key[toLong(toLong(3n & j6) ^ j5)])) ^
@@ -42,7 +42,7 @@ const teaDecrypt = (data, key) => {
data[i] = j2
} else break
}
let j8 = data[lengthBitint - 1n]
const j8 = data[lengthBitint - 1n]
j2 = toLong(
data[0n] -
toLong(
@@ -89,7 +89,7 @@ const toBigintArray = (data) => {
const MAX = 9223372036854775807n
const MIN = -9223372036854775808n
const toLong = (str) => {
const num = typeof str == 'string' ? BigInt('0x' + str) : str
const num = typeof str === 'string' ? BigInt('0x' + str) : str
if (num > MAX) return toLong(num - (1n << 64n))
else if (num < MIN) return toLong(num + (1n << 64n))
return num

View File

@@ -197,10 +197,10 @@ export default {
},
filterNewComment(rawList) {
return rawList.map((item) => {
let time = this.formatTime(item.time)
let timeStr = time ? dateFormat2(time) : null
const time = this.formatTime(item.time)
const timeStr = time ? dateFormat2(time) : null
if (item.middlecommentcontent) {
let firstItem = item.middlecommentcontent[0]
const firstItem = item.middlecommentcontent[0]
firstItem.avatarurl = item.avatarurl
firstItem.praisenum = item.praisenum
item.avatarurl = null
@@ -270,12 +270,12 @@ export default {
})
},
replaceEmoji(msg) {
let rxp = /^\[em\](e\d+)\[\/em\]$/
const rxp = /^\[em\](e\d+)\[\/em\]$/
let result = msg.match(/\[em\]e\d+\[\/em\]/g)
if (!result) return msg
result = Array.from(new Set(result))
for (let item of result) {
let code = item.replace(rxp, '$1')
for (const item of result) {
const code = item.replace(rxp, '$1')
msg = msg.replace(
new RegExp(item.replace('[em]', '\\[em\\]').replace('[/em]', '\\[\\/em\\]'), 'g'),
emojis[code] || ''

View File

@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
import { formatPlayTime, sizeFormate } from '../index'
import { formatSingerName } from '../utils'
let boardList = [
const boardList = [
{ id: 'tx__4', name: '流行指数榜', bangid: '4' },
{ id: 'tx__26', name: '热歌榜', bangid: '26' },
{ id: 'tx__27', name: '新歌榜', bangid: '27' },
@@ -137,31 +137,31 @@ export default {
filterData(rawList) {
// console.log(rawList)
return rawList.map((item) => {
let types = []
let _types = {}
const types = []
const _types = {}
if (item.file.size_128mp3 !== 0) {
let size = sizeFormate(item.file.size_128mp3)
const size = sizeFormate(item.file.size_128mp3)
types.push({ type: '128k', size })
_types['128k'] = {
size
}
}
if (item.file.size_320mp3 !== 0) {
let size = sizeFormate(item.file.size_320mp3)
const size = sizeFormate(item.file.size_320mp3)
types.push({ type: '320k', size })
_types['320k'] = {
size
}
}
if (item.file.size_flac !== 0) {
let size = sizeFormate(item.file.size_flac)
const size = sizeFormate(item.file.size_flac)
types.push({ type: 'flac', size })
_types.flac = {
size
}
}
if (item.file.size_hires !== 0) {
let size = sizeFormate(item.file.size_hires)
const size = sizeFormate(item.file.size_hires)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size
@@ -195,10 +195,10 @@ export default {
},
getPeriods(bangid) {
return this.getData(this.periodUrl).then(({ body: html }) => {
let result = html.match(this.regExps.periodList)
const result = html.match(this.regExps.periodList)
if (!result) return Promise.reject(new Error('get data failed'))
result.forEach((item) => {
let result = item.match(this.regExps.period)
const result = item.match(this.regExps.period)
if (!result) return
this.periods[result[2]] = {
name: result[1],
@@ -212,7 +212,7 @@ export default {
},
filterBoardsData(rawList) {
// console.log(rawList)
let list = []
const list = []
for (const board of rawList) {
// 排除 MV榜
if (board.id == 201) continue
@@ -256,8 +256,8 @@ export default {
getList(bangid, page, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
bangid = parseInt(bangid)
let info = this.periods[bangid]
let p = info ? Promise.resolve(info.period) : this.getPeriods(bangid)
const info = this.periods[bangid]
const p = info ? Promise.resolve(info.period) : this.getPeriods(bangid)
return p.then((period) => {
return this.listDetailRequest(bangid, period, this.limit).then((resp) => {
if (resp.body.code !== 0) return this.getList(bangid, page, retryNum)
@@ -273,7 +273,7 @@ export default {
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('tx__', '')
if (typeof id === 'string') id = id.replace('tx__', '')
return `https://y.qq.com/n/ryqq/toplist/${id}`
}
}

View File

@@ -2,7 +2,7 @@ import { httpFetch } from '../../request'
import { formatPlayTime, sizeFormate } from '../index'
const getSinger = (singers) => {
let arr = []
const arr = []
singers.forEach((singer) => {
arr.push(singer.name)
})
@@ -37,32 +37,32 @@ export default (songmid) => {
const item = body.req.data.track_info
if (!item.file?.media_mid) return null
let types = []
let _types = {}
const types = []
const _types = {}
const file = item.file
if (file.size_128mp3 != 0) {
let size = sizeFormate(file.size_128mp3)
const size = sizeFormate(file.size_128mp3)
types.push({ type: '128k', size })
_types['128k'] = {
size
}
}
if (file.size_320mp3 !== 0) {
let size = sizeFormate(file.size_320mp3)
const size = sizeFormate(file.size_320mp3)
types.push({ type: '320k', size })
_types['320k'] = {
size
}
}
if (file.size_flac !== 0) {
let size = sizeFormate(file.size_flac)
const size = sizeFormate(file.size_flac)
types.push({ type: 'flac', size })
_types.flac = {
size
}
}
if (file.size_hires !== 0) {
let size = sizeFormate(file.size_hires)
const size = sizeFormate(file.size_hires)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size

View File

@@ -56,32 +56,32 @@ export default {
rawList.forEach((item) => {
if (!item.file?.media_mid) return
let types = []
let _types = {}
const types = []
const _types = {}
const file = item.file
if (file.size_128mp3 != 0) {
let size = sizeFormate(file.size_128mp3)
const size = sizeFormate(file.size_128mp3)
types.push({ type: '128k', size })
_types['128k'] = {
size
}
}
if (file.size_320mp3 !== 0) {
let size = sizeFormate(file.size_320mp3)
const size = sizeFormate(file.size_320mp3)
types.push({ type: '320k', size })
_types['320k'] = {
size
}
}
if (file.size_flac !== 0) {
let size = sizeFormate(file.size_flac)
const size = sizeFormate(file.size_flac)
types.push({ type: 'flac', size })
_types.flac = {
size
}
}
if (file.size_hires !== 0) {
let size = sizeFormate(file.size_hires)
const size = sizeFormate(file.size_hires)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size
@@ -123,7 +123,7 @@ export default {
if (limit == null) limit = this.limit
// http://newlyric.kuwo.cn/newlyric.lrc?62355680
return this.musicSearch(str, page, limit).then(({ body, meta }) => {
let list = this.handleResult(body.item_song)
const list = this.handleResult(body.item_song)
this.total = meta.estimate_sum
this.page = page

View File

@@ -7,28 +7,28 @@ export const filterMusicInfoItem = (item) => {
const types = []
const _types = {}
if (item.file.size_128mp3 != 0) {
let size = sizeFormate(item.file.size_128mp3)
const size = sizeFormate(item.file.size_128mp3)
types.push({ type: '128k', size })
_types['128k'] = {
size
}
}
if (item.file.size_320mp3 !== 0) {
let size = sizeFormate(item.file.size_320mp3)
const size = sizeFormate(item.file.size_320mp3)
types.push({ type: '320k', size })
_types['320k'] = {
size
}
}
if (item.file.size_flac !== 0) {
let size = sizeFormate(item.file.size_flac)
const size = sizeFormate(item.file.size_flac)
types.push({ type: 'flac', size })
_types.flac = {
size
}
}
if (item.file.size_hires !== 0) {
let size = sizeFormate(item.file.size_hires)
const size = sizeFormate(item.file.size_hires)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size

View File

@@ -95,12 +95,12 @@ export default {
})
},
filterInfoHotTag(html) {
let hotTag = html.match(this.regExps.hotTagHtml)
const hotTag = html.match(this.regExps.hotTagHtml)
const hotTags = []
if (!hotTag) return hotTags
hotTag.forEach((tagHtml) => {
let result = tagHtml.match(this.regExps.hotTag)
const result = tagHtml.match(this.regExps.hotTag)
if (!result) return
hotTags.push({
id: parseInt(result[1]),
@@ -240,31 +240,31 @@ export default {
filterListDetail(rawList) {
// console.log(rawList)
return rawList.map((item) => {
let types = []
let _types = {}
const types = []
const _types = {}
if (item.file.size_128mp3 !== 0) {
let size = sizeFormate(item.file.size_128mp3)
const size = sizeFormate(item.file.size_128mp3)
types.push({ type: '128k', size })
_types['128k'] = {
size
}
}
if (item.file.size_320mp3 !== 0) {
let size = sizeFormate(item.file.size_320mp3)
const size = sizeFormate(item.file.size_320mp3)
types.push({ type: '320k', size })
_types['320k'] = {
size
}
}
if (item.file.size_flac !== 0) {
let size = sizeFormate(item.file.size_flac)
const size = sizeFormate(item.file.size_flac)
types.push({ type: 'flac', size })
_types.flac = {
size
}
}
if (item.file.size_hires !== 0) {
let size = sizeFormate(item.file.size_hires)
const size = sizeFormate(item.file.size_hires)
types.push({ type: 'flac24bit', size })
_types.flac24bit = {
size

View File

@@ -42,7 +42,7 @@ export const formatSingerName = (singers, nameKey = 'name', join = '、') => {
if (Array.isArray(singers)) {
const singer = []
singers.forEach((item) => {
let name = item[nameKey]
const name = item[nameKey]
if (!name) return
singer.push(name)
})

View File

@@ -70,7 +70,7 @@ const applyEmoji = (text) => {
return text
}
let cursorTools = {
const cursorTools = {
cache: {},
getCursor(id, page, limit) {
let cacheData = this.cache[id]
@@ -190,7 +190,7 @@ export default {
},
filterComment(rawList) {
return rawList.map((item) => {
let data = {
const data = {
id: item.commentId,
text: item.content ? applyEmoji(item.content) : '',
time: item.time ? item.time : '',
@@ -203,7 +203,7 @@ export default {
reply: []
}
let replyData = item.beReplied && item.beReplied[0]
const replyData = item.beReplied && item.beReplied[0]
return replyData
? {
id: item.commentId,

View File

@@ -134,7 +134,7 @@ export default {
filterBoardsData(rawList) {
// console.log(rawList)
let list = []
const list = []
for (const board of rawList) {
// 排除 MV榜
// if (board.id == 201) continue
@@ -210,7 +210,7 @@ export default {
},
getDetailPageUrl(id) {
if (typeof id == 'string') id = id.replace('wy__', '')
if (typeof id === 'string') id = id.replace('wy__', '')
return `https://music.163.com/#/discover/toplist?id=${id}`
}
}

View File

@@ -64,13 +64,13 @@ const parseTools = {
},
msFormat(timeMs) {
if (Number.isNaN(timeMs)) return ''
let ms = timeMs % 1000
const ms = timeMs % 1000
timeMs /= 1000
let m = parseInt(timeMs / 60)
const m = parseInt(timeMs / 60)
.toString()
.padStart(2, '0')
timeMs %= 60
let s = parseInt(timeMs).toString().padStart(2, '0')
const s = parseInt(timeMs).toString().padStart(2, '0')
return `[${m}:${s}.${ms}]`
},
parseLyric(lines) {
@@ -79,7 +79,7 @@ const parseTools = {
for (let line of lines) {
line = line.trim()
let result = this.rxps.lineTime.exec(line)
const result = this.rxps.lineTime.exec(line)
if (!result) {
if (line.startsWith('[offset')) {
lxlrcLines.push(line)
@@ -92,7 +92,7 @@ const parseTools = {
const startTimeStr = this.msFormat(startMsTime)
if (!startTimeStr) continue
let words = line.replace(this.rxps.lineTime, '')
const words = line.replace(this.rxps.lineTime, '')
lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`)
@@ -124,7 +124,7 @@ const parseTools = {
getIntv(interval) {
if (!interval) return 0
if (!interval.includes('.')) interval += '.0'
let arr = interval.split(/:|\./)
const arr = interval.split(/:|\./)
while (arr.length < 3) arr.unshift('0')
const [m, s, ms] = arr
return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)
@@ -134,7 +134,7 @@ const parseTools = {
const targetlrcLines = targetlrc.split('\n')
const timeRxp = /^\[([\d:.]+)\]/
let temp = []
let newLrc = []
const newLrc = []
targetlrcLines.forEach((line) => {
const result = timeRxp.exec(line)
if (!result) return
@@ -168,7 +168,7 @@ const parseTools = {
crlyric: ''
}
if (ylrc) {
let lines = this.parseHeaderInfo(ylrc)
const lines = this.parseHeaderInfo(ylrc)
if (lines) {
const result = this.parseLyric(lines)
if (ytlrc) {
@@ -245,8 +245,8 @@ const parseTools = {
// https://github.com/lyswhut/lx-music-mobile/issues/370
const fixTimeLabel = (lrc, tlrc, romalrc) => {
if (lrc) {
let newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]')
let newTlrc = tlrc?.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') ?? tlrc
const newLrc = lrc.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]')
const newTlrc = tlrc?.replace(/\[(\d{2}:\d{2}):(\d{2})]/g, '[$1.$2]') ?? tlrc
if (newLrc != lrc || newTlrc != tlrc) {
lrc = newLrc
tlrc = newTlrc

View File

@@ -5,7 +5,7 @@ import { formatPlayTime, sizeFormate } from '../index'
export default {
getSinger(singers) {
let arr = []
const arr = []
singers?.forEach((singer) => {
arr.push(singer.name)
})

View File

@@ -20,7 +20,7 @@ export default {
return searchRequest.promise.then(({ body }) => body)
},
getSinger(singers) {
let arr = []
const arr = []
singers.forEach((singer) => {
arr.push(singer.name)
})
@@ -87,7 +87,7 @@ export default {
return this.musicSearch(str, page, limit).then((result) => {
// console.log(result)
if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)
let list = this.handleResult(result.result.songs || [])
const list = this.handleResult(result.result.songs || [])
// console.log(list)
if (list == null) return this.search(str, page, limit, retryNum)

View File

@@ -90,8 +90,8 @@ export default {
const { statusCode, body } = await requestObj_listDetail.promise
if (statusCode !== 200 || body.code !== this.successCode)
return this.getListDetail(id, page, ++tryNum)
let limit = 1000
let rangeStart = (page - 1) * limit
const limit = 1000
const rangeStart = (page - 1) * limit
// console.log(body)
let list
if (body.playlist.trackIds.length == body.privileges.length) {

View File

@@ -21,7 +21,7 @@ const aesEncrypt = (buffer, mode, key, iv) => {
}
const aesDecrypt = function (cipherBuffer, mode, key, iv) {
let decipher = createDecipheriv(mode, key, iv)
const decipher = createDecipheriv(mode, key, iv)
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()])
}

View File

@@ -20,7 +20,7 @@ function getAppDirPath(
| 'logs'
| 'crashDumps'
) {
let dirPath: string = electron.app.getPath(name ?? 'userData')
const dirPath: string = electron.app.getPath(name ?? 'userData')
return dirPath
}

View File

@@ -85,7 +85,7 @@ const defaultHeaders = {
* @param {Object} options - 请求选项
*/
const buildHttpPromise = (url, options) => {
let obj = {
const obj = {
isCancelled: false,
cancelToken: axios.CancelToken.source(),
cancelHttp: () => {
@@ -190,12 +190,12 @@ const fetchData = async (url, method = 'get', options = {}) => {
let s = Buffer.from(bHh, 'hex').toString()
s = s.replace(s.substr(-1), '')
s = Buffer.from(s, 'base64').toString()
let v = process.versions.app
const v = process.versions.app
.split('-')[0]
.split('.')
.map((n) => (n.length < 3 ? n.padStart(3, '0') : n))
.join('')
let v2 = process.versions.app.split('-')[1] || ''
const v2 = process.versions.app.split('-')[1] || ''
requestHeaders[s] =
!s ||
`${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${path}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}`
@@ -385,7 +385,7 @@ export const http_jsonp = (url, options, callback) => {
options = {}
}
let jsonpCallback = 'jsonpCallback'
const jsonpCallback = 'jsonpCallback'
if (url.indexOf('?') < 0) url += '?'
url += `&${options.jsonpCallback}=${jsonpCallback}`

View File

@@ -1,54 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
MusicCache: typeof import('./src/components/Settings/MusicCache.vue')['default']
PlaylistActions: typeof import('./src/components/Play/PlaylistActions.vue')['default']
PlaylistDrawer: typeof import('./src/components/Play/PlaylistDrawer.vue')['default']
PlaylistSettings: typeof import('./src/components/Settings/PlaylistSettings.vue')['default']
PlayMusic: typeof import('./src/components/Play/PlayMusic.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchComponent: typeof import('./src/components/Search/SearchComponent.vue')['default']
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
TAlert: typeof import('tdesign-vue-next')['Alert']
TAside: typeof import('tdesign-vue-next')['Aside']
TBadge: typeof import('tdesign-vue-next')['Badge']
TButton: typeof import('tdesign-vue-next')['Button']
TCard: typeof import('tdesign-vue-next')['Card']
TContent: typeof import('tdesign-vue-next')['Content']
TDialog: typeof import('tdesign-vue-next')['Dialog']
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
TForm: typeof import('tdesign-vue-next')['Form']
TFormItem: typeof import('tdesign-vue-next')['FormItem']
ThemeSelector: typeof import('./src/components/ThemeSelector.vue')['default']
TIcon: typeof import('tdesign-vue-next')['Icon']
TImage: typeof import('tdesign-vue-next')['Image']
TInput: typeof import('tdesign-vue-next')['Input']
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
TLayout: typeof import('tdesign-vue-next')['Layout']
TLoading: typeof import('tdesign-vue-next')['Loading']
TRadioButton: typeof import('tdesign-vue-next')['RadioButton']
TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
TSlider: typeof import('tdesign-vue-next')['Slider']
TSwitch: typeof import('tdesign-vue-next')['Switch']
TTextarea: typeof import('tdesign-vue-next')['Textarea']
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
UpdateProgress: typeof import('./src/components/UpdateProgress.vue')['default']
UpdateSettings: typeof import('./src/components/Settings/UpdateSettings.vue')['default']
Versions: typeof import('./src/components/Versions.vue')['default']
}
}

View File

@@ -25,7 +25,7 @@
}
}
;((i = function () {
var a,
let a,
l = document.createElement('div')
;((l.innerHTML = c._iconfont_svg_string_4997692),
(l = l.getElementsByTagName('svg')[0]) &&

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -481,7 +481,7 @@ onBeforeUnmount(() => {
:class="message.type"
>
<div v-if="message.type === 'loading'" class="message-content loading-content">
<t-loading size="small" />
<TLoading size="small" />
<span class="loading-text">{{ message.content }}</span>
</div>
<div v-else class="message-content" v-html="message.html || message.content"></div>

View File

@@ -2,11 +2,11 @@
<div class="song-virtual-list">
<!-- 表头 -->
<div class="list-header">
<div class="col-index" v-if="showIndex"></div>
<div v-if="showIndex" class="col-index"></div>
<div class="col-title">标题</div>
<div class="col-album" v-if="showAlbum">专辑</div>
<div v-if="showAlbum" class="col-album">专辑</div>
<div class="col-like">喜欢</div>
<div class="col-duration" v-if="showDuration">时长</div>
<div v-if="showDuration" class="col-duration">时长</div>
</div>
<!-- 虚拟滚动容器 -->
@@ -21,13 +21,15 @@
@mouseleave="hoveredSong = null"
>
<!-- 序号或播放状态图标 -->
<div class="col-index" v-if="showIndex">
<span v-if="hoveredSong !== (song.id || song.songmid)" class="track-number">
{{ String(visibleStartIndex + index + 1).padStart(2, '0') }}
</span>
<button v-else class="play-btn" title="播放" @click.stop="handlePlay(song)">
<i class="icon-play"></i>
</button>
<div v-if="showIndex" class="col-index">
<Transition name="playSong" mode="out-in">
<span v-if="hoveredSong !== (song.id || song.songmid)" class="track-number">
{{ String(visibleStartIndex + index + 1).padStart(2, '0') }}
</span>
<button v-else class="play-btn" title="播放" @click.stop="handlePlay(song)">
<i class="icon-play"></i>
</button>
</Transition>
</div>
<!-- 歌曲信息 -->
@@ -47,7 +49,7 @@
</div>
<!-- 专辑信息 -->
<div class="col-album" v-if="showAlbum">
<div v-if="showAlbum" class="col-album">
<span class="album-name" :title="song.albumName">
{{ song.albumName || '-' }}
</span>
@@ -61,7 +63,7 @@
</div>
<!-- 时长 -->
<div class="col-duration" v-if="showDuration">
<div v-if="showDuration" class="col-duration">
<div class="duration-wrapper">
<span v-if="hoveredSong !== (song.id || song.songmid)" class="duration">
{{ formatDuration(song.interval) }}
@@ -247,6 +249,15 @@ onMounted(() => {
</script>
<style scoped>
.playSong-enter-active,
.playSong-leave-active {
transition: all 0.2s ease-in-out;
}
.playSong-enter-from,
.playSong-leave-to {
opacity: 0;
transform: scale(0.9);
}
.song-virtual-list {
height: 100%;
width: 100%;
@@ -373,6 +384,7 @@ onMounted(() => {
align-items: center;
justify-content: center;
font-style: none;
&:hover {
background: rgba(80, 125, 175, 0.1);
color: #3a5d8f;
@@ -568,14 +580,17 @@ onMounted(() => {
content: '▶';
font-style: normal;
}
.icon-pause::before {
content: '⏸';
font-style: normal;
}
.icon-download::before {
content: '⬇';
font-style: normal;
}
.icon-heart::before {
content: '♡';
font-style: normal;

View File

@@ -12,11 +12,6 @@ interface Props {
backgroundColor?: string
}
// 定义事件
const emit = defineEmits<{
lowFreqUpdate: [volume: number]
}>()
const props = withDefaults(defineProps<Props>(), {
show: true,
height: 80,
@@ -25,6 +20,11 @@ const props = withDefaults(defineProps<Props>(), {
backgroundColor: 'transparent'
})
// 定义事件
const emit = defineEmits<{
lowFreqUpdate: [volume: number]
}>()
const canvasRef = ref<HTMLCanvasElement>()
const animationId = ref<number>()
const analyser = ref<AnalyserNode>()

View File

@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<Props>(), {
show: false,
coverImage: '@assets/images/Default.jpg',
songId: '',
mainColor: '#fff'
mainColor: '#rgb(0,0,0)'
})
// 定义事件
const emit = defineEmits(['toggle-fullscreen'])
@@ -251,6 +251,27 @@ watch(
const handleLowFreqUpdate = (volume: number) => {
state.lowFreqVolume = volume
}
// 计算偏白的主题色
const lightMainColor = computed(() => {
const color = props.mainColor
// 解析rgb颜色值
const rgbMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*\d+\)/)
if (rgbMatch) {
let r = parseInt(rgbMatch[1])
let g = parseInt(rgbMatch[2])
let b = parseInt(rgbMatch[3])
// 适度向白色偏移,保持主题色特征
r = Math.min(255, r + (255 - r) * 0.8)
g = Math.min(255, g + (255 - g) * 0.8)
b = Math.min(255, b + (255 - b) * 0.8)
return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, 0.9)`
}
// 如果解析失败,返回默认的偏白色
return 'rgba(255, 255, 255, 0.9)'
})
</script>
<template>
@@ -291,6 +312,12 @@ const handleLowFreqUpdate = (volume: number) => {
</Transition>
<div class="playbox">
<div class="left" :style="state.lyricLines.length <= 0 && 'width:100vw'">
<img
class="pointer"
:class="{ playing: Audio.isPlay }"
src="@renderer/assets/pointer.png"
alt="pointer"
/>
<div
class="cd-container"
:class="{ playing: Audio.isPlay }"
@@ -331,7 +358,7 @@ const handleLowFreqUpdate = (volume: number) => {
</div>
</div>
<!-- 音频可视化组件 -->
<div class="audio-visualizer-container" v-if="props.show && coverImage">
<div v-if="props.show && coverImage" class="audio-visualizer-container">
<AudioVisualizer
:show="props.show && Audio.isPlay"
:height="70"
@@ -441,12 +468,13 @@ const handleLowFreqUpdate = (volume: number) => {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.256);
-webkit-drop-filter: blur(10px);
-webkit-drop-filter: blur(80px);
padding: 0 10vw;
-webkit-drop-filter: blur(10px);
-webkit-drop-filter: blur(80px);
overflow: hidden;
display: flex;
position: relative;
--cd-width-auto: max(200px, min(30vw, 700px, calc(100vh - var(--play-bottom-height) - 250px)));
.left {
width: 40%;
@@ -464,9 +492,24 @@ const handleLowFreqUpdate = (volume: number) => {
margin: 0 0 var(--play-bottom-height) 0;
perspective: 1000px;
.pointer {
position: absolute;
width: calc(var(--cd-width-auto) / 3.5);
left: calc(50% - 1.8vh);
top: calc(50% - var(--cd-width-auto) / 2 - calc(var(--cd-width-auto) / 3.5) - 1vh);
transform: rotate(-20deg);
transform-origin: 1.8vh 1.8vh;
z-index: 2;
transition: transform 0.3s;
&.playing {
transform: rotate(0deg);
}
}
.cd-container {
width: min(30vw, 700px);
height: min(30vw, 700px);
width: var(--cd-width-auto);
height: var(--cd-width-auto);
position: relative;
display: flex;
align-items: center;
@@ -620,14 +663,33 @@ const handleLowFreqUpdate = (volume: number) => {
}
.right {
mask: linear-gradient(
rgba(255, 255, 255, 0) 0px,
rgba(255, 255, 255, 0.6) 5%,
rgb(255, 255, 255) 10%,
rgb(255, 255, 255) 75%,
rgba(255, 255, 255, 0.6) 85%,
rgba(255, 255, 255, 0)
);
:deep(.lyric-player) {
--amll-lyric-view-color: v-bind(lightMainColor);
font-family: lyricfont;
--amll-lyric-player-font-size: min(2.6vw, 32px);
--amll-lyric-player-font-size: min(2.6vw, 39px);
// bottom: max(2vw, 29px);
height: 200%;
transform: translateY(-25%);
* [class^='lyricMainLine'] {
font-weight: 600 !important;
* {
font-weight: 600 !important;
}
}
& > div {
padding-bottom: 0;
overflow: hidden;
@@ -665,6 +727,7 @@ const handleLowFreqUpdate = (volume: number) => {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
@@ -675,10 +738,12 @@ const handleLowFreqUpdate = (volume: number) => {
opacity: 0.1;
transform: rotate(0deg) scale(1);
}
50% {
opacity: 0.2;
transform: rotate(180deg) scale(1.1);
}
100% {
opacity: 0.1;
transform: rotate(360deg) scale(1);
@@ -690,16 +755,20 @@ const handleLowFreqUpdate = (volume: number) => {
opacity: 0.05;
transform: rotate(0deg);
}
25% {
opacity: 0.15;
}
50% {
opacity: 0.1;
transform: rotate(180deg);
}
75% {
opacity: 0.15;
}
100% {
opacity: 0.05;
transform: rotate(360deg);

View File

@@ -121,6 +121,7 @@ onUnmounted(() => {
<template>
<div>
<audio
id="globaAudio"
ref="audioMeta"
preload="auto"
:src="audioStore.Audio.url"
@@ -131,7 +132,6 @@ onUnmounted(() => {
@loadeddata="handleLoadedData"
@ended="handleEnded"
@canplay="handleCanPlay"
id="globaAudio"
></audio>
</div>
</template>

View File

@@ -27,6 +27,7 @@ import {
destroyPlaylistEventListeners,
getSongRealUrl
} from '@renderer/utils/playlistManager'
import mediaSessionController from '@renderer/utils/useAmtc'
import defaultCoverImg from '/default-cover.png'
const controlAudio = ControlAudioStore()
@@ -41,7 +42,7 @@ const removeMusicCtrlListener = window.api.onMusicCtrl(() => {
togglePlayPause()
})
let timer: any = null
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
function throttle(callback: Function, delay: number) {
if (timer) return
timer = setTimeout(() => {
@@ -132,6 +133,9 @@ let isFull = false
// 播放指定歌曲
const playSong = async (song: SongList) => {
try {
// 设置加载状态
isLoadingSong.value = true
// 检查是否需要恢复播放位置(历史播放)
const isHistoryPlay =
song.songmid === userInfo.value.lastPlaySongId &&
@@ -163,6 +167,14 @@ const playSong = async (song: SongList) => {
...song
}
// 更新媒体会话元数据
mediaSessionController.updateMetadata({
title: song.name,
artist: song.singer,
album: song.albumName || '未知专辑',
artworkUrl: song.img || defaultCoverImg
})
// 确保主题色更新
await setColor()
@@ -198,8 +210,8 @@ const playSong = async (song: SongList) => {
// 等待音频准备就绪
await waitForAudioReady()
// 短暂延迟确保音频状态稳定
await new Promise((resolve) => setTimeout(resolve, 100))
// // 短暂延迟确保音频状态稳定
// await new Promise((resolve) => setTimeout(resolve, 100))
// 开始播放
try {
@@ -229,6 +241,9 @@ const playSong = async (song: SongList) => {
} catch (error: any) {
console.error('播放歌曲失败:', error)
MessagePlugin.error('播放失败,原因:' + error.message)
} finally {
// 无论成功还是失败,都清除加载状态
isLoadingSong.value = false
}
}
provide('PlaySong', playSong)
@@ -236,6 +251,9 @@ provide('PlaySong', playSong)
// const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
const playMode = ref(PlayMode.SEQUENCE)
// 歌曲加载状态
const isLoadingSong = ref(false)
// 更新播放模式
const updatePlayMode = () => {
const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE]
@@ -270,8 +288,6 @@ const showVolumeSlider = ref(false)
const volumeBarRef = ref<HTMLDivElement | null>(null)
const isDraggingVolume = ref(false)
const volumeValue = computed({
get: () => Audio.value.volume,
set: (val) => {
@@ -415,6 +431,26 @@ onMounted(async () => {
// 初始化播放列表事件监听器
initPlaylistEventListeners(localUserStore, playSong)
// 初始化媒体会话控制器
if (Audio.value.audio) {
mediaSessionController.init(Audio.value.audio, {
play: async () => {
// 专门的播放函数,只处理播放逻辑
if (!Audio.value.isPlay) {
await handlePlay()
}
},
pause: async () => {
// 专门的暂停函数,只处理暂停逻辑
if (Audio.value.isPlay) {
await handlePause()
}
},
playPrevious: () => playPrevious(),
playNext: () => playNext()
})
}
// 监听音频结束事件,根据播放模式播放下一首
unEnded = controlAudio.subscribe('ended', () => {
window.requestAnimationFrame(() => {
@@ -432,6 +468,14 @@ onMounted(async () => {
...lastPlayedSong
}
// 立即更新媒体会话元数据,让系统显示当前歌曲信息
mediaSessionController.updateMetadata({
title: lastPlayedSong.name,
artist: lastPlayedSong.singer,
album: lastPlayedSong.albumName || '未知专辑',
artworkUrl: lastPlayedSong.img || defaultCoverImg
})
// 如果有历史播放位置,设置为待恢复状态
if (!Audio.value.isPlay) {
if (userInfo.value.currentTime && userInfo.value.currentTime > 0) {
@@ -451,6 +495,9 @@ onMounted(async () => {
} catch (error) {
console.error('获取上次播放歌曲URL失败:', error)
}
} else {
// 如果当前正在播放,设置状态为播放中
mediaSessionController.updatePlaybackState('playing')
}
}
}
@@ -463,8 +510,6 @@ onMounted(async () => {
}, 1000) // 每1秒保存一次
})
// 组件卸载时清理
onUnmounted(() => {
destroyPlaylistEventListeners()
@@ -475,6 +520,8 @@ onUnmounted(() => {
if (removeMusicCtrlListener) {
removeMusicCtrlListener()
}
// 清理媒体会话控制器
mediaSessionController.cleanup()
unEnded()
})
@@ -559,50 +606,63 @@ const formatTime = (seconds: number) => {
const currentTimeFormatted = computed(() => formatTime(Audio.value.currentTime))
const durationFormatted = computed(() => formatTime(Audio.value.duration))
// 播放/暂停切换
const togglePlayPause = async () => {
if (Audio.value.url) {
if (Audio.value.isPlay) {
const stopResult = stop()
if (stopResult && typeof stopResult.then === 'function') {
await stopResult
}
} else {
try {
// 检查是否需要恢复历史播放位置
if (pendingRestorePosition > 0 && pendingRestoreSongId === userInfo.value.lastPlaySongId) {
console.log(`恢复播放位置: ${pendingRestorePosition}`)
// 等待音频准备就绪
await waitForAudioReady()
// 设置播放位置
setCurrentTime(pendingRestorePosition)
if (Audio.value.audio) {
Audio.value.audio.currentTime = pendingRestorePosition
}
// 清除待恢复的位置
pendingRestorePosition = 0
pendingRestoreSongId = null
}
const startResult = start()
if (startResult && typeof startResult.then === 'function') {
await startResult
}
} catch (error) {
console.error('播放失败:', error)
MessagePlugin.error('播放失败,请重试')
}
}
} else {
// 专门的播放函数
const handlePlay = async () => {
if (!Audio.value.url) {
// 如果没有URL但有播放列表尝试播放第一首歌
if (list.value.length > 0) {
await playSong(list.value[0])
} else {
MessagePlugin.warning('播放列表为空,请先添加歌曲')
}
return
}
try {
// 检查是否需要恢复历史播放位置
if (pendingRestorePosition > 0 && pendingRestoreSongId === userInfo.value.lastPlaySongId) {
console.log(`恢复播放位置: ${pendingRestorePosition}`)
// 等待音频准备就绪
await waitForAudioReady()
// 设置播放位置
setCurrentTime(pendingRestorePosition)
if (Audio.value.audio) {
Audio.value.audio.currentTime = pendingRestorePosition
}
// 清除待恢复的位置
pendingRestorePosition = 0
pendingRestoreSongId = null
}
const startResult = start()
if (startResult && typeof startResult.then === 'function') {
await startResult
}
} catch (error) {
console.error('播放失败:', error)
MessagePlugin.error('播放失败,请重试')
}
}
// 专门的暂停函数
const handlePause = async () => {
if (Audio.value.url && Audio.value.isPlay) {
const stopResult = stop()
if (stopResult && typeof stopResult.then === 'function') {
await stopResult
}
}
}
// 播放/暂停切换
const togglePlayPause = async () => {
if (Audio.value.isPlay) {
await handlePause()
} else {
await handlePlay()
}
}
@@ -754,8 +814,8 @@ watch(showFullPlay, (val) => {
<div class="player-content">
<!-- 左侧封面和歌曲信息 -->
<div class="left-section">
<div class="album-cover" v-if="songInfo.songmid">
<img :src="songInfo.img" alt="专辑封面" v-if="songInfo.img" />
<div v-if="songInfo.songmid" class="album-cover">
<img v-if="songInfo.img" :src="songInfo.img" alt="专辑封面" />
<img :src="defaultCoverImg" alt="默认封面" />
</div>
@@ -770,11 +830,18 @@ watch(showFullPlay, (val) => {
<t-button class="control-btn" variant="text" shape="circle" @click.stop="playPrevious">
<span class="iconfont icon-shangyishou"></span>
</t-button>
<button class="control-btn play-btn" @click.stop="togglePlayPause">
<transition name="fade" mode="out-in">
<span v-if="Audio.isPlay" key="play" class="iconfont icon-zanting"></span>
<span v-else key="pause" class="iconfont icon-bofang"></span>
</transition>
<button
class="control-btn play-btn"
:disabled="isLoadingSong"
@click.stop="() => !isLoadingSong && togglePlayPause()"
>
<Transition name="loadSong" mode="out-in">
<div v-if="isLoadingSong" key="loading" class="loading-spinner play-loading"></div>
<transition v-else name="fade" mode="out-in">
<span v-if="Audio.isPlay" key="play" class="iconfont icon-zanting"></span>
<span v-else key="pause" class="iconfont icon-bofang"></span>
</transition>
</Transition>
</button>
<t-button class="control-btn" shape="circle" variant="text" @click.stop="playNext">
<span class="iconfont icon-xiayishou"></span>
@@ -830,7 +897,7 @@ watch(showFullPlay, (val) => {
<!-- 播放列表按钮 -->
<t-tooltip content="播放列表">
<t-badge :count="list.length" :maxCount="99" color="#aaa">
<t-badge :count="list.length" :max-count="99" color="#aaa">
<t-button
class="control-btn"
shape="circle"
@@ -850,9 +917,9 @@ watch(showFullPlay, (val) => {
:song-id="songInfo.songmid ? songInfo.songmid.toString() : null"
:show="showFullPlay"
:cover-image="songInfo.img"
@toggle-fullscreen="toggleFullPlay"
:song-info="songInfo"
:main-color="maincolor"
@toggle-fullscreen="toggleFullPlay"
/>
</div>
@@ -886,6 +953,55 @@ watch(showFullPlay, (val) => {
transform: rotate(-180deg);
}
/* 加载动画 */
.loading-spinner {
border: 2px solid rgba(255, 255, 255, 0.3);
border-top: 2px solid v-bind(hoverColor);
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
width: 1em;
height: 1em;
}
/* 播放按钮中的加载动画 */
.play-loading {
width: 20px !important;
height: 20px !important;
margin: 4px;
border-width: 3px;
border-color: rgba(255, 255, 255, 0.3);
border-top-color: v-bind(hoverColor);
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 加载歌曲过渡动画 - 缩小透明效果 */
.loadSong-enter-active,
.loadSong-leave-active {
transition: all 0.2s ease-in-out;
}
.loadSong-enter-from,
.loadSong-leave-to {
opacity: 0;
transform: scale(0.8);
}
.loadSong-enter-to,
.loadSong-leave-from {
opacity: 1;
transform: scale(1);
}
.player-container {
box-shadow: 0px -2px 20px 0px #00000039;
position: fixed;
@@ -1211,8 +1327,6 @@ watch(showFullPlay, (val) => {
transform: translateY(10px) scale(0.95);
}
/* 响应式设计 */
@media (max-width: 768px) {
.right-section .time-display {

View File

@@ -76,8 +76,6 @@ const scrollToCurrentSong = () => {
})
}
// 关闭播放列表
const handleClose = () => {
emit('close')
@@ -87,12 +85,12 @@ const handleClose = () => {
const handleMouseEnter = (index: number) => {
// 如果正在拖拽,不显示提示
if (isDragSorting.value) return
// 清除之前的定时器
if (hoverTimer.value) {
clearTimeout(hoverTimer.value)
}
// 设置新的定时器
hoverTimer.value = window.setTimeout(() => {
hoverTipVisible.value = true
@@ -106,7 +104,7 @@ const handleMouseLeave = () => {
clearTimeout(hoverTimer.value)
hoverTimer.value = null
}
// 隐藏提示
hoverTipVisible.value = false
hoverTipIndex.value = -1
@@ -134,65 +132,75 @@ const handleTouchStart = (event: TouchEvent, index: number, song: any) => {
}
// 拖拽排序相关方法
const handlePointerStart = (event: MouseEvent | TouchEvent, index: number, song: any, isTouch: boolean) => {
const handlePointerStart = (
event: MouseEvent | TouchEvent,
index: number,
song: any,
isTouch: boolean
) => {
event.preventDefault()
event.stopPropagation()
// 重置标记
isDragStarted.value = false
wasLongPressed.value = false
currentOperatingSong.value = song
// 清除之前的定时器
if (longPressTimer.value) {
clearTimeout(longPressTimer.value)
}
// 记录初始位置
const clientY = isTouch ? (event as TouchEvent).touches[0].clientY : (event as MouseEvent).clientY
dragStartY.value = clientY
dragCurrentY.value = clientY
// 设置长按定时器
longPressTimer.value = window.setTimeout(() => {
wasLongPressed.value = true // 标记发生了长按
startDragSort(index, song)
isDragStarted.value = true // 标记已开始拖动
}, longPressDelay)
// 添加移动和结束事件监听
const handleMove = (e: MouseEvent | TouchEvent) => {
const currentY = 'touches' in e ? e.touches[0].clientY : e.clientY
const deltaY = Math.abs(currentY - dragStartY.value)
// 如果移动距离超过阈值,取消长按
if (deltaY > dragThreshold && longPressTimer.value) {
clearTimeout(longPressTimer.value)
longPressTimer.value = null
}
// 如果已经开始拖拽,更新位置
if (isDragSorting.value) {
dragCurrentY.value = currentY
updateDragOverIndex(currentY)
}
}
const handleEnd = () => {
const hadLongPressTimer = !!longPressTimer.value
const wasInDragMode = isDragSorting.value
if (longPressTimer.value) {
clearTimeout(longPressTimer.value)
longPressTimer.value = null
}
if (isDragSorting.value) {
endDragSort()
}
// 如果没有发生长按且没有在拖拽模式,说明是正常点击,触发播放
if (!wasLongPressed.value && !wasInDragMode && hadLongPressTimer && currentOperatingSong.value) {
if (
!wasLongPressed.value &&
!wasInDragMode &&
hadLongPressTimer &&
currentOperatingSong.value
) {
// 短暂延迟后播放,确保状态已经稳定
setTimeout(() => {
emit('playSong', currentOperatingSong.value)
@@ -208,14 +216,14 @@ const handlePointerStart = (event: MouseEvent | TouchEvent, index: number, song:
currentOperatingSong.value = null
}, 200)
}
// 移除事件监听
document.removeEventListener('mousemove', handleMove)
document.removeEventListener('mouseup', handleEnd)
document.removeEventListener('touchmove', handleMove)
document.removeEventListener('touchend', handleEnd)
}
// 添加事件监听
document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleEnd)
@@ -226,18 +234,18 @@ const handlePointerStart = (event: MouseEvent | TouchEvent, index: number, song:
const startDragSort = (index: number, song: any) => {
// 隐藏悬停提示
hideTip()
isDragSorting.value = true
draggedIndex.value = index
draggedSong.value = song
dragOverIndex.value = index
// 保存原始列表用于实时预览
originalList.value = [...list.value]
// 添加拖拽样式
document.body.style.userSelect = 'none'
// 触觉反馈(如果支持)
if ('vibrate' in navigator) {
navigator.vibrate(50)
@@ -247,19 +255,20 @@ const startDragSort = (index: number, song: any) => {
const updateDragOverIndex = (clientY: number) => {
const playlistContainer = document.querySelector('.playlist-content')
if (!playlistContainer) return
const containerRect = playlistContainer.getBoundingClientRect()
const scrollThreshold = 80 // 增加边缘滚动触发区域
const maxScrollSpeed = 15 // 增加最大滚动速度
// 检查是否需要自动滚动
const distanceFromTop = clientY - containerRect.top
const distanceFromBottom = containerRect.bottom - clientY
// 检查是否可以滚动
const canScrollUp = playlistContainer.scrollTop > 0
const canScrollDown = playlistContainer.scrollTop < (playlistContainer.scrollHeight - playlistContainer.clientHeight)
const canScrollDown =
playlistContainer.scrollTop < playlistContainer.scrollHeight - playlistContainer.clientHeight
if (distanceFromTop < scrollThreshold && distanceFromTop > 0 && canScrollUp) {
// 向上滚动
const intensity = (scrollThreshold - distanceFromTop) / scrollThreshold
@@ -274,18 +283,18 @@ const updateDragOverIndex = (clientY: number) => {
// 停止自动滚动
stopAutoScroll()
}
// 计算拖拽位置,考虑容器滚动偏移
const playlistSongs = document.querySelectorAll('.playlist-song')
let newOverIndex = draggedIndex.value
// 如果在容器范围内,计算最接近的插入位置
if (clientY >= containerRect.top && clientY <= containerRect.bottom) {
for (let i = 0; i < playlistSongs.length; i++) {
const songElement = playlistSongs[i] as HTMLElement
const rect = songElement.getBoundingClientRect()
const centerY = rect.top + rect.height / 2
if (clientY < centerY) {
newOverIndex = i
break
@@ -300,9 +309,13 @@ const updateDragOverIndex = (clientY: number) => {
// 在容器下方,插入到末尾
newOverIndex = playlistSongs.length
}
// 实时更新列表顺序进行预览
if (newOverIndex !== dragOverIndex.value && newOverIndex >= 0 && newOverIndex <= list.value.length) {
if (
newOverIndex !== dragOverIndex.value &&
newOverIndex >= 0 &&
newOverIndex <= list.value.length
) {
dragOverIndex.value = newOverIndex
updatePreviewList()
}
@@ -311,21 +324,21 @@ const updateDragOverIndex = (clientY: number) => {
// 实时预览列表更新
const updatePreviewList = () => {
if (draggedIndex.value === -1 || dragOverIndex.value === -1) return
const newList = [...list.value]
const draggedItem = newList.splice(draggedIndex.value, 1)[0]
// 计算实际插入位置
let insertIndex = dragOverIndex.value
if (dragOverIndex.value > draggedIndex.value) {
insertIndex = dragOverIndex.value - 1
}
newList.splice(insertIndex, 0, draggedItem)
// 更新预览列表不保存到localStorage
list.value = newList
// 更新拖拽索引
draggedIndex.value = insertIndex
}
@@ -333,12 +346,12 @@ const updatePreviewList = () => {
// 自动滚动相关方法
const startAutoScroll = () => {
if (autoScrollTimer.value) return
autoScrollTimer.value = window.setInterval(() => {
const playlistContainer = document.querySelector('.playlist-content')
if (playlistContainer && scrollSpeed.value !== 0) {
playlistContainer.scrollTop += scrollSpeed.value
// 在自动滚动过程中持续更新拖拽位置预览
if (isDragSorting.value) {
updateDragOverIndex(dragCurrentY.value)
@@ -358,10 +371,10 @@ const stopAutoScroll = () => {
const endDragSort = () => {
// 停止自动滚动
stopAutoScroll()
// 由于实时预览已经更新了列表,这里只需要确保保存
// list.value 的变化会被 watch 监听器自动保存到 localStorage
// 重置状态
isDragSorting.value = false
draggedIndex.value = -1
@@ -369,7 +382,7 @@ const endDragSort = () => {
draggedSong.value = null
isDragStarted.value = false
wasLongPressed.value = false
// 移除拖拽样式
document.body.style.userSelect = ''
}
@@ -380,12 +393,12 @@ onUnmounted(() => {
if (hoverTimer.value) {
clearTimeout(hoverTimer.value)
}
// 清理拖拽相关定时器
if (longPressTimer.value) {
clearTimeout(longPressTimer.value)
}
// 清理自动滚动定时器
stopAutoScroll()
})
@@ -418,19 +431,14 @@ defineExpose({
<p>播放列表为空</p>
<p>请添加歌曲到播放列表也可在设置中导入歌曲列表</p>
</div>
<TransitionGroup
v-else
:class="playlistSongsClass"
name="list-item"
tag="div"
>
<TransitionGroup v-else :class="playlistSongsClass" name="list-item" tag="div">
<div
v-for="(song, index) in list"
:key="song.songmid"
class="playlist-song"
:class="{
:class="{
active: song.songmid === currentSongId,
'dragging': isDragSorting && index === draggedIndex
dragging: isDragSorting && index === draggedIndex
}"
@mousedown="handleMouseDown($event, index, song)"
@touchstart="handleTouchStart($event, index, song)"
@@ -438,10 +446,10 @@ defineExpose({
@mouseleave="handleMouseLeave"
>
<!-- 拖拽手柄 -->
<div class="drag-handle" v-if="isDragSorting && index === draggedIndex">
<div v-if="isDragSorting && index === draggedIndex" class="drag-handle">
<span class="drag-dots"></span>
</div>
<div class="song-info">
<div class="song-name">{{ song.name }}</div>
<div class="song-artist">{{ song.singer }}</div>
@@ -456,14 +464,10 @@ defineExpose({
<button class="song-remove" @click.stop="localUserStore.removeSong(song.songmid)">
<span class="iconfont icon-xuanxiangshanchu"></span>
</button>
<!-- 悬停提示 -->
<transition name="hover-tip">
<div
v-if="hoverTipVisible && hoverTipIndex === index"
class="hover-tip"
@click.stop
>
<div v-if="hoverTipVisible && hoverTipIndex === index" class="hover-tip" @click.stop>
长按可拖动排序
</div>
</transition>
@@ -801,4 +805,4 @@ defineExpose({
border-radius: 8px 8px 0 0;
}
}
</style>
</style>

View File

@@ -12,9 +12,9 @@ const canvasRef = ref<HTMLCanvasElement | null>(null)
let gl: WebGLRenderingContext | null = null
let program: WebGLProgram | null = null
let animationFrameId: number | null = null
let startTime = Date.now()
let dominantColor = ref({ r: 0.3, g: 0.3, b: 0.5 })
let colorPalette = ref<Color[]>([
const startTime = Date.now()
const dominantColor = ref({ r: 0.3, g: 0.3, b: 0.5 })
const colorPalette = ref<Color[]>([
{ r: 76, g: 116, b: 206 }, // 蓝色
{ r: 120, g: 80, b: 180 }, // 紫色
{ r: 60, g: 160, b: 160 } // 青色

View File

@@ -4,9 +4,9 @@
<h3>自动更新</h3>
<div class="update-info">
<p>当前版本: {{ currentVersion }}</p>
<t-button theme="primary" :loading="isChecking" @click="handleCheckUpdate">
<TButton theme="primary" :loading="isChecking" @click="handleCheckUpdate">
{{ isChecking ? '检查中...' : '检查更新' }}
</t-button>
</TButton>
</div>
</div>
</div>

View File

@@ -4,6 +4,13 @@ import { useRouter } from 'vue-router'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { storeToRefs } from 'pinia'
const props = withDefaults(defineProps<Props>(), {
controlStyle: false,
showSettings: true,
showBack: false,
title: '',
color: 'black'
})
const Store = LocalUserDetailStore()
const { userInfo } = storeToRefs(Store)
@@ -18,14 +25,6 @@ interface Props {
color?: string
}
const props = withDefaults(defineProps<Props>(), {
controlStyle: false,
showSettings: true,
showBack: false,
title: '',
color: 'black'
})
// Mini 模式现在是直接隐藏到系统托盘,不需要状态跟踪
// 计算样式类名

View File

@@ -22,7 +22,7 @@
<span>已下载: {{ formatBytes(downloadState.progress.transferred) }}</span>
<span>总大小: {{ formatBytes(downloadState.progress.total) }}</span>
</div>
<div class="download-speed" v-if="downloadSpeed > 0">
<div v-if="downloadSpeed > 0" class="download-speed">
下载速度: {{ formatBytes(downloadSpeed) }}/s
</div>
</div>

View File

@@ -1,6 +1,6 @@
import { createWebHashHistory, createRouter, RouteRecordRaw, RouterOptions } from 'vue-router'
let routes: RouteRecordRaw[] = [
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'welcome',
@@ -55,7 +55,7 @@ let routes: RouteRecordRaw[] = [
]
function setAnimate(routerObj: RouteRecordRaw[]) {
for (let i = 0; i < routerObj.length; i++) {
let item = routerObj[i]
const item = routerObj[i]
if (item.children && item.children.length > 0) {
setAnimate(item.children)
} else {

View File

@@ -180,7 +180,6 @@ export const ControlAudioStore = defineStore('controlAudio', () => {
}
const start = async () => {
const volume = Audio.volume
console.log('开始播放音频111', volume)
if (Audio.audio) {
Audio.audio.volume = 0
try {

View File

@@ -1,7 +1,6 @@
import { NotifyPlugin } from 'tdesign-vue-next'
import { NotifyPlugin, MessagePlugin } from 'tdesign-vue-next'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { toRaw } from 'vue'
import { MessagePlugin } from 'tdesign-vue-next'
interface MusicItem {
singer: string

View File

@@ -0,0 +1,161 @@
interface MediaSessionCallbacks {
play: () => void
pause: () => void
playPrevious: () => void
playNext: () => void
}
interface TrackMetadata {
title: string
artist: string
album: string
artworkUrl: string
}
/**
* Media Session API 控制器
* 用于管理浏览器的媒体会话,支持系统级媒体控制
*/
class MediaSessionController {
private audioElement: HTMLAudioElement | null = null
private callbacks: MediaSessionCallbacks | null = null
private eventListeners: Array<{
element: HTMLAudioElement
event: string
handler: EventListener
}> = []
/**
* 检查浏览器是否支持 Media Session API
*/
private get isSupported(): boolean {
return 'mediaSession' in navigator
}
/**
* 更新媒体会话元数据
*/
updateMetadata(metadata: TrackMetadata): void {
if (!this.isSupported) return
try {
navigator.mediaSession.metadata = new MediaMetadata({
title: metadata.title,
artist: metadata.artist,
album: metadata.album,
artwork: this.generateArtworkSizes(metadata.artworkUrl)
})
} catch (error) {
console.warn('Failed to update media session metadata:', error)
}
}
/**
* 生成不同尺寸的封面图片配置
*/
private generateArtworkSizes(artworkUrl: string): MediaImage[] {
const sizes = ['96x96', '128x128', '192x192', '256x256', '384x384', '512x512']
return sizes.map((size) => ({
src: artworkUrl,
sizes: size,
type: 'image/png'
}))
}
/**
* 初始化媒体会话控制器
*/
init(audioElement: HTMLAudioElement, callbacks: MediaSessionCallbacks): void {
if (!this.isSupported) {
console.warn('Media Session API is not supported in this browser')
return
}
// 清理之前的监听器
this.cleanup()
this.audioElement = audioElement
this.callbacks = callbacks
// 只设置媒体会话动作处理器,不自动监听音频事件
// 让应用层手动控制播放状态更新,避免循环调用
this.setupMediaSessionActionHandlers()
}
/**
* 设置媒体会话动作处理器
*/
private setupMediaSessionActionHandlers(): void {
if (!this.callbacks) return
const actionHandlers: Array<[MediaSessionAction, () => void]> = [
['play', this.callbacks.play],
['pause', this.callbacks.pause],
['previoustrack', this.callbacks.playPrevious],
['nexttrack', this.callbacks.playNext]
]
actionHandlers.forEach(([action, handler]) => {
navigator.mediaSession.setActionHandler(action, handler)
})
// 设置 seekto 处理器
navigator.mediaSession.setActionHandler('seekto', (details) => {
if (!this.audioElement || !details.seekTime) return
try {
if (details.fastSeek && 'fastSeek' in this.audioElement) {
this.audioElement.fastSeek(details.seekTime)
} else {
this.audioElement.currentTime = details.seekTime
}
} catch (error) {
console.warn('Failed to seek audio:', error)
}
})
}
/**
* 更新播放状态
*/
updatePlaybackState(state: MediaSessionPlaybackState): void {
if (!this.isSupported) return
try {
navigator.mediaSession.playbackState = state
} catch (error) {
console.warn('Failed to update playback state:', error)
}
}
/**
* 清理事件监听器和媒体会话
*/
cleanup(): void {
// 移除音频事件监听器
this.eventListeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler)
})
this.eventListeners = []
// 清理媒体会话动作处理器
if (this.isSupported) {
const actions: MediaSessionAction[] = [
'play',
'pause',
'previoustrack',
'nexttrack',
'seekto'
]
actions.forEach((action) => {
navigator.mediaSession.setActionHandler(action, null)
})
}
this.audioElement = null
this.callbacks = null
}
}
// 导出单例实例
export default new MediaSessionController()

View File

@@ -142,7 +142,7 @@ onUnmounted(() => {
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<t-alert theme="error" :message="error" />
<t-button theme="primary" @click="fetchHotSonglist" style="margin-top: 1rem">
<t-button theme="primary" style="margin-top: 1rem" @click="fetchHotSonglist">
重新加载
</t-button>
</div>
@@ -161,24 +161,22 @@ onUnmounted(() => {
<div
class="playlist-info"
:style="{
'background-color': mainColors[index],
color: textColors[index]
'--hover-bg-color': mainColors[index],
'--hover-text-color': textColors[index]
}"
>
<h4 class="playlist-title" :style="{ color: textColors[index] }">
<h4 class="playlist-title">
{{ playlist.title }}
</h4>
<p class="playlist-desc" :style="{ color: textColors[index] }">
<p class="playlist-desc">
{{ playlist.description }}
</p>
<div class="playlist-meta">
<span class="play-count" :style="{ color: textColors[index] }">
<span class="play-count">
<i class="iconfont icon-bofang"></i>
{{ playlist.playCount }}
</span>
<span class="song-count" v-if="playlist.total" :style="{ color: textColors[index] }"
>{{ playlist.total }}</span
>
<span v-if="playlist.total" class="song-count">{{ playlist.total }}</span>
</div>
<!-- <div class="playlist-author">by {{ playlist.author }}</div> -->
</div>
@@ -312,6 +310,23 @@ onUnmounted(() => {
.playlist-info {
backdrop-filter: blur(8px);
background-color: var(--hover-bg-color);
color: #111827;
.playlist-title {
color: var(--hover-text-color);
}
.playlist-desc {
color: var(--hover-text-color);
}
.playlist-meta {
color: var(--hover-text-color);
* {
color: var(--hover-text-color);
}
}
.playlist-author {
color: var(--hover-text-color);
}
}
}
@@ -357,8 +372,9 @@ onUnmounted(() => {
padding: 1.25rem 1rem;
position: relative;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
transition: backdrop-filter 0.3s ease;
transition: all 0.3s ease;
.playlist-title {
font-size: 1rem;
@@ -384,6 +400,7 @@ onUnmounted(() => {
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 2.625rem; // 确保描述区域高度一致
transition: color 0.3s ease;
}
.playlist-meta {
@@ -394,6 +411,7 @@ onUnmounted(() => {
margin-top: auto; // 推到底部
padding-top: 0.5rem;
border-top: 1px solid rgba(229, 231, 235, 0.5);
transition: color 0.3s ease;
}
.play-count {
@@ -403,6 +421,7 @@ onUnmounted(() => {
align-items: center;
gap: 0.25rem;
font-weight: 500;
transition: color 0.3s ease;
.iconfont {
font-size: 0.875rem;
@@ -417,6 +436,7 @@ onUnmounted(() => {
background: rgba(156, 163, 175, 0.1);
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
transition: color 0.3s ease;
}
.playlist-author {
@@ -425,6 +445,7 @@ onUnmounted(() => {
font-style: italic;
margin-top: 0.25rem;
opacity: 0.8;
transition: color 0.3s ease;
}
}
}

View File

@@ -312,9 +312,9 @@ onMounted(() => {
<t-button
theme="primary"
size="medium"
@click="handlePlayPlaylist"
:disabled="songs.length === 0 || loading"
class="play-btn"
@click="handlePlayPlaylist"
>
<template #icon>
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor">
@@ -327,9 +327,9 @@ onMounted(() => {
<t-button
variant="outline"
size="medium"
@click="handleShufflePlaylist"
:disabled="songs.length === 0 || loading"
class="shuffle-btn"
@click="handleShufflePlaylist"
>
<template #icon>
<svg class="shuffle-icon" viewBox="0 0 24 24" fill="currentColor">

View File

@@ -684,12 +684,12 @@ onMounted(() => {
</div>
</div>
<div class="playlist-info">
<div class="playlist-name" @click="viewPlaylist(playlist)" :title="playlist.name">
<div class="playlist-name" :title="playlist.name" @click="viewPlaylist(playlist)">
{{ playlist.name }}
</div>
<div
class="playlist-description"
v-if="playlist.description"
class="playlist-description"
:title="playlist.description"
>
{{ playlist.description }}
@@ -860,8 +860,8 @@ onMounted(() => {
<!-- 创建歌单对话框 -->
<t-dialog
placement="center"
v-model:visible="showCreatePlaylistDialog"
placement="center"
header="创建新歌单"
width="500px"
:confirm-btn="{ content: '创建', theme: 'primary' }"
@@ -892,8 +892,8 @@ onMounted(() => {
<!-- 导入选择对话框 -->
<t-dialog
placement="center"
v-model:visible="showImportDialog"
placement="center"
header="选择导入方式"
width="400px"
:footer="false"
@@ -928,14 +928,14 @@ onMounted(() => {
</t-dialog>
<!-- 网络歌单导入对话框 -->
<t-dialog
placement="center"
v-model:visible="showNetworkImportDialog"
placement="center"
header="导入网易云音乐歌单"
:confirm-btn="{ content: '开始导入', theme: 'primary' }"
:cancel-btn="{ content: '取消', variant: 'outline' }"
width="500px"
@confirm="confirmNetworkImport"
@cancel="cancelNetworkImport"
width="500px"
>
<div class="network-import-content">
<p class="import-description">
@@ -967,14 +967,14 @@ onMounted(() => {
<!-- 编辑歌单对话框 -->
<t-dialog
placement="center"
v-model:visible="showEditPlaylistDialog"
placement="center"
header="编辑歌单信息"
:confirm-btn="{ content: '保存', theme: 'primary' }"
:cancel-btn="{ content: '取消', variant: 'outline' }"
width="500px"
@confirm="savePlaylistEdit"
@cancel="cancelPlaylistEdit"
width="500px"
>
<div class="edit-playlist-content">
<div class="form-item">

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import TitleBarControls from '@renderer/components/TitleBarControls.vue'
import PlaylistSettings from '@renderer/components/Settings/PlaylistSettings.vue'
import { ref } from 'vue'
import { ref, computed, watch } from 'vue'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { storeToRefs } from 'pinia'
import {
@@ -15,7 +15,6 @@ import {
} from 'tdesign-icons-vue-next'
import fonts from '@renderer/assets/icon_font/icons'
import { useRouter } from 'vue-router'
import { computed, watch } from 'vue'
import MusicCache from '@renderer/components/Settings/MusicCache.vue'
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
import ThemeSelector from '@renderer/components/ThemeSelector.vue'
@@ -26,7 +25,6 @@ const { userInfo } = storeToRefs(Store)
// 当前选择的设置分类
const activeCategory = ref<string>('appearance')
// 应用版本号
const appVersion = ref('1.0.0')
@@ -477,7 +475,7 @@ const openLink = (url: string) => {
<div class="source-name">{{ source.name }}</div>
<div class="source-type">{{ source.type || '音乐源' }}</div>
</div>
<div class="source-check" v-if="userInfo.selectSources === key">
<div v-if="userInfo.selectSources === key" class="source-check">
<i class="iconfont icon-check"></i>
</div>
</div>
@@ -494,8 +492,8 @@ const openLink = (url: string) => {
:step="1"
:marks="qualityMarks"
:label="qualityMarks[qualitySliderValue]"
@change="onQualityChange"
class="quality-slider"
@change="onQualityChange"
/>
</div>
<div class="quality-description">

View File

@@ -1,178 +1,180 @@
<template>
<TitleBarControls title="插件管理" :show-back="true" class="header"></TitleBarControls>
<div class="plugins-container">
<h2>插件管理</h2>
<div class="page">
<TitleBarControls title="插件管理" :show-back="true" class="header"></TitleBarControls>
<div class="plugins-container">
<h2>插件管理</h2>
<div class="plugin-actions">
<t-button theme="primary" @click="plugTypeDialog = true">
<template #icon><t-icon name="add" /></template> 添加插件
</t-button>
<t-dialog
:visible="plugTypeDialog"
:close-btn="true"
confirm-btn="确定"
cancel-btn="取消"
:on-confirm="addPlug"
:on-close="() => (plugTypeDialog = false)"
>
<template #header>请选择你的插件类别</template>
<template #body>
<t-radio-group v-model="type" variant="primary-filled" default-value="cr">
<t-radio-button value="cr">澜音插件</t-radio-button>
<t-radio-button value="lx">洛雪插件</t-radio-button>
</t-radio-group>
</template>
</t-dialog>
<t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 刷新
</t-button>
</div>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<div v-else-if="error" class="error-state">
<t-icon name="error-circle" style="font-size: 48px; color: #dc3545" />
<p>加载插件时出错</p>
<p class="error-message">{{ error }}</p>
<t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 重试
</t-button>
</div>
<div v-else-if="plugins.length === 0" class="empty-state">
<t-icon name="app" style="font-size: 48px" />
<p>暂无已安装的插件</p>
<p class="hint">点击"添加插件"按钮来安装新插件</p>
</div>
<div v-else class="plugin-list">
<div
v-for="plugin in plugins"
:key="plugin.pluginId"
class="plugin-item"
:class="{ selected: isPluginSelected(plugin.pluginId) }"
>
<div class="plugin-info">
<h3>
{{ plugin.pluginInfo.name }}
<span class="version">{{ plugin.pluginInfo.version }}</span>
<span v-if="isPluginSelected(plugin.pluginId)" class="current-tag">当前使用</span>
</h3>
<p class="author">作者: {{ plugin.pluginInfo.author }}</p>
<p class="description">{{ plugin.pluginInfo.description || '无描述' }}</p>
<div
v-if="plugin.supportedSources && Object.keys(plugin.supportedSources).length > 0"
class="plugin-sources"
>
<span class="source-label">支持的音源:</span>
<span v-for="source in plugin.supportedSources" :key="source.name" class="source-tag">
{{ source.name }}
</span>
</div>
</div>
<div class="plugin-actions">
<t-button
theme="default"
size="small"
@click.stop="viewPluginLogs(plugin.pluginId, plugin.pluginInfo.name)"
:disabled="loading"
>
<template #icon><t-icon name="view-list" /></template> 日志
</t-button>
<t-button
v-if="!isPluginSelected(plugin.pluginId)"
theme="primary"
size="small"
@click="selectPlugin(plugin)"
>
<template #icon><t-icon name="check" /></template> 使用
</t-button>
<t-button
theme="danger"
size="small"
@click="uninstallPlugin(plugin.pluginId, plugin.pluginInfo.name)"
>
<template #icon><t-icon name="delete" /></template> 卸载
</t-button>
</div>
<div class="plugin-actions">
<t-button theme="primary" @click="plugTypeDialog = true">
<template #icon><t-icon name="add" /></template> 添加插件
</t-button>
<t-dialog
:visible="plugTypeDialog"
:close-btn="true"
confirm-btn="确定"
cancel-btn="取消"
:on-confirm="addPlug"
:on-close="() => (plugTypeDialog = false)"
>
<template #header>请选择你的插件类别</template>
<template #body>
<t-radio-group v-model="type" variant="primary-filled" default-value="cr">
<t-radio-button value="cr">澜音插件</t-radio-button>
<t-radio-button value="lx">洛雪插件</t-radio-button>
</t-radio-group>
</template>
</t-dialog>
<t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 刷新
</t-button>
</div>
</div>
<!-- 插件日志弹窗 -->
<t-dialog
v-model:visible="logDialogVisible"
top="10vh"
:close-btn="false"
:footer="false"
width="80%"
:style="{ maxWidth: '900px', maxHeight: '80vh' }"
class="log-dialog"
>
<template #header>
<div class="log-dialog-header">
<div class="log-title">
<i class="iconfont icon-terminal"></i>
{{ currentLogPluginName }} - 插件日志
</div>
<div class="log-actions">
<t-button
size="small"
variant="outline"
theme="default"
ghost
:disabled="logsLoading"
@click.stop="refreshLogs"
<div v-if="loading" class="loading">
<div class="spinner"></div>
<span>加载中...</span>
</div>
<div v-else-if="error" class="error-state">
<t-icon name="error-circle" style="font-size: 48px; color: #dc3545" />
<p>加载插件时出错</p>
<p class="error-message">{{ error }}</p>
<t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 重试
</t-button>
</div>
<div v-else-if="plugins.length === 0" class="empty-state">
<t-icon name="app" style="font-size: 48px" />
<p>暂无已安装的插件</p>
<p class="hint">点击"添加插件"按钮来安装新插件</p>
</div>
<div v-else class="plugin-list">
<div
v-for="plugin in plugins"
:key="plugin.pluginId"
class="plugin-item"
:class="{ selected: isPluginSelected(plugin.pluginId) }"
>
<div class="plugin-info">
<h3>
{{ plugin.pluginInfo.name }}
<span class="version">{{ plugin.pluginInfo.version }}</span>
<span v-if="isPluginSelected(plugin.pluginId)" class="current-tag">当前使用</span>
</h3>
<p class="author">作者: {{ plugin.pluginInfo.author }}</p>
<p class="description">{{ plugin.pluginInfo.description || '无描述' }}</p>
<div
v-if="plugin.supportedSources && Object.keys(plugin.supportedSources).length > 0"
class="plugin-sources"
>
刷新
<span class="source-label">支持的音源:</span>
<span v-for="source in plugin.supportedSources" :key="source.name" class="source-tag">
{{ source.name }}
</span>
</div>
</div>
<div class="plugin-actions">
<t-button
theme="default"
size="small"
:disabled="loading"
@click.stop="viewPluginLogs(plugin.pluginId, plugin.pluginInfo.name)"
>
<template #icon><t-icon name="view-list" /></template> 日志
</t-button>
<t-button
v-if="!isPluginSelected(plugin.pluginId)"
theme="primary"
size="small"
@click="selectPlugin(plugin)"
>
<template #icon><t-icon name="check" /></template> 使用
</t-button>
<t-button
theme="danger"
size="small"
@click="uninstallPlugin(plugin.pluginId, plugin.pluginInfo.name)"
>
<template #icon><t-icon name="delete" /></template> 卸载
</t-button>
</div>
<div class="mac-controls">
<div class="mac-button close" @click="logDialogVisible = false"></div>
<div class="mac-button minimize"></div>
<div class="mac-button maximize"></div>
</div>
</div>
</template>
<template #body>
<div class="console-container">
<div class="console-header">
<div class="console-info">
<span class="console-prompt">$</span>
<span class="console-path">~/plugins/{{ currentLogPluginName }}</span>
<span class="console-time">{{ formatTime(new Date()) }}</span>
</div>
<!-- 插件日志弹窗 -->
<t-dialog
v-model:visible="logDialogVisible"
top="10vh"
:close-btn="false"
:footer="false"
width="80%"
:style="{ maxWidth: '900px', maxHeight: '80vh' }"
class="log-dialog"
>
<template #header>
<div class="log-dialog-header">
<div class="log-title">
<i class="iconfont icon-terminal"></i>
{{ currentLogPluginName }} - 插件日志
</div>
<div class="log-actions">
<t-button
size="small"
variant="outline"
theme="default"
ghost
:disabled="logsLoading"
@click.stop="refreshLogs"
>
刷新
</t-button>
</div>
<div class="mac-controls">
<div class="mac-button close" @click="logDialogVisible = false"></div>
<div class="mac-button minimize"></div>
<div class="mac-button maximize"></div>
</div>
</div>
<div ref="logContentRef" class="console-content" :class="{ loading: logsLoading }">
<div v-if="logsLoading" class="console-loading">
<div class="loading-spinner"></div>
<span>正在加载日志...</span>
</template>
<template #body>
<div class="console-container">
<div class="console-header">
<div class="console-info">
<span class="console-prompt">$</span>
<span class="console-path">~/plugins/{{ currentLogPluginName }}</span>
<span class="console-time">{{ formatTime(new Date()) }}</span>
</div>
</div>
<div v-else-if="logsError" class="console-error">
<span class="error-icon">❌</span>
<span>加载日志失败: {{ logsError }}</span>
</div>
<div v-else-if="logs.length === 0" class="console-empty">
<span class="empty-icon">📝</span>
<span>暂无日志记录</span>
</div>
<div v-else class="log-entries">
<div
v-for="(log, index) in logs"
:key="index"
class="log-entry"
:class="getLogLevel(log)"
>
<span class="log-timestamp">{{ formatLogTime(index) }}</span>
<span class="log-content">{{ log }}</span>
<div ref="logContentRef" class="console-content" :class="{ loading: logsLoading }">
<div v-if="logsLoading" class="console-loading">
<div class="loading-spinner"></div>
<span>正在加载日志...</span>
</div>
<div v-else-if="logsError" class="console-error">
<span class="error-icon">❌</span>
<span>加载日志失败: {{ logsError }}</span>
</div>
<div v-else-if="logs.length === 0" class="console-empty">
<span class="empty-icon">📝</span>
<span>暂无日志记录</span>
</div>
<div v-else class="log-entries">
<div
v-for="(log, index) in logs"
:key="index"
class="log-entry"
:class="getLogLevel(log)"
>
<span class="log-timestamp">{{ formatLogTime(index) }}</span>
<span class="log-content">{{ log }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
</t-dialog>
</template>
</t-dialog>
</div>
</div>
</template>
@@ -213,7 +215,7 @@ const plugins = ref<Plugin[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const plugTypeDialog = ref(false)
let type = ref<'lx' | 'cr'>('cr')
const type = ref<'lx' | 'cr'>('cr')
// 日志相关状态
const logDialogVisible = ref(false)
@@ -381,7 +383,7 @@ async function uninstallPlugin(pluginId: string, pluginName: string) {
// 卸载成功才刷新插件列表
await getPlugins()
// 显示成功消息
if (pluginId == localUserStore.userInfo.pluginId) {
if (pluginId === localUserStore.userInfo.pluginId) {
localUserStore.userInfo.pluginId = ''
localUserStore.userInfo.supportedSources = {}
localUserStore.userInfo.selectSources = ''
@@ -508,6 +510,11 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
.page {
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
-webkit-app-region: drag;
display: flex;
@@ -522,8 +529,13 @@ onMounted(async () => {
}
.plugins-container {
flex: 1;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
}
.plugin-actions {
@@ -590,6 +602,9 @@ onMounted(async () => {
display: flex;
flex-direction: column;
gap: 15px;
flex: 1;
overflow-y: auto;
min-height: 0;
}
.plugin-item {
@@ -676,6 +691,7 @@ onMounted(async () => {
/* 日志弹窗样式 */
:deep(.log-dialog) {
height: 80vh;
.t-dialog {
background: #1e1e1e;
border-radius: 12px;
@@ -710,6 +726,7 @@ onMounted(async () => {
background: linear-gradient(135deg, #2d2d2d 0%, #1e1e1e 100%);
min-height: 48px;
width: 100%;
.log-title {
display: flex;
align-items: center;
@@ -753,6 +770,7 @@ onMounted(async () => {
display: flex;
gap: 8px;
flex-direction: row-reverse;
.mac-button {
width: 12px;
height: 12px;
@@ -762,6 +780,7 @@ onMounted(async () => {
&.close {
background: #ff5f57;
&:hover {
background: #ff3b30;
}
@@ -769,6 +788,7 @@ onMounted(async () => {
&.minimize {
background: #ffbd2e;
&:hover {
background: #ff9500;
}
@@ -776,6 +796,7 @@ onMounted(async () => {
&.maximize {
background: #28ca42;
&:hover {
background: #30d158;
}
@@ -938,6 +959,7 @@ onMounted(async () => {
.log-content {
color: #ff6b6b;
}
.log-timestamp {
color: #ff6b6b;
}
@@ -947,6 +969,7 @@ onMounted(async () => {
.log-content {
color: #ffd93d;
}
.log-timestamp {
color: #ffd93d;
}
@@ -956,6 +979,7 @@ onMounted(async () => {
.log-content {
color: #74b9ff;
}
.log-timestamp {
color: #74b9ff;
}
@@ -965,6 +989,7 @@ onMounted(async () => {
.log-content {
color: #a29bfe;
}
.log-timestamp {
color: #a29bfe;
}
@@ -982,6 +1007,7 @@ onMounted(async () => {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}

View File

@@ -24,7 +24,7 @@
<!-- 特性标签 -->
<div class="feature-tags">
<span class="tag" v-for="(feature, index) in features" :key="index">
<span v-for="(feature, index) in features" :key="index" class="tag">
{{ feature }}
</span>
</div>