fix:优化项目结构

This commit is contained in:
sqj
2025-09-19 22:46:52 +08:00
parent bc53203bfa
commit 0c54a852ba
47 changed files with 77756 additions and 963 deletions

268
README.md
View File

@@ -21,6 +21,274 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
- **CeruPlugins**:音乐插件运行环境(仅提供框架,不包含默认插件)
- **AMLL**:音乐生态辅助模块(仅提供功能接口,不关联具体音乐数据源)
## 项目结构
```ast
CeruMuisc/
├── .github/
│ └── workflows/
│ ├── auto-sync-release.yml
│ ├── deploydocs.yml
│ ├── main.yml
│ ├── sync-releases-to-webdav.yml
│ └── uploadpan.yml
├── scripts/
│ ├── auth-test.js
│ ├── genAst.js
│ └── test-alist.js
├── src/
│ ├── common/
│ │ ├── types/
│ │ │ ├── playList.ts
│ │ │ └── songList.ts
│ │ ├── utils/
│ │ │ ├── lyricUtils/
│ │ │ │ ├── kg.js
│ │ │ │ └── util.ts
│ │ │ ├── common.ts
│ │ │ ├── nodejs.ts
│ │ │ └── renderer.ts
│ │ └── index.ts
│ ├── main/
│ │ ├── events/
│ │ │ ├── ai.ts
│ │ │ ├── autoUpdate.ts
│ │ │ ├── directorySettings.ts
│ │ │ ├── musicCache.ts
│ │ │ └── songList.ts
│ │ ├── services/
│ │ │ ├── music/
│ │ │ │ ├── index.ts
│ │ │ │ ├── net-ease-service.ts
│ │ │ │ └── service-base.ts
│ │ │ ├── musicCache/
│ │ │ │ └── index.ts
│ │ │ ├── musicSdk/
│ │ │ │ ├── index.ts
│ │ │ │ ├── service.ts
│ │ │ │ └── type.ts
│ │ │ ├── plugin/
│ │ │ │ ├── manager/
│ │ │ │ │ ├── CeruMusicPluginHost.ts
│ │ │ │ │ └── converter-event-driven.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── logger.ts
│ │ │ ├── songList/
│ │ │ │ ├── ManageSongList.ts
│ │ │ │ └── PlayListSongs.ts
│ │ │ └── ai-service.ts
│ │ ├── utils/
│ │ │ ├── musicSdk/
│ │ │ │ ├── kg/
│ │ │ │ │ ├── temp/
│ │ │ │ │ │ ├── musicSearch-new.js
│ │ │ │ │ │ └── songList-new.js
│ │ │ │ │ ├── vendors/
│ │ │ │ │ │ └── infSign.min.js
│ │ │ │ │ ├── album.js
│ │ │ │ │ ├── api-test.js
│ │ │ │ │ ├── comment.js
│ │ │ │ │ ├── hotSearch.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── leaderboard.js
│ │ │ │ │ ├── lyric.js
│ │ │ │ │ ├── musicInfo.js
│ │ │ │ │ ├── musicSearch.js
│ │ │ │ │ ├── pic.js
│ │ │ │ │ ├── singer.js
│ │ │ │ │ ├── songList.js
│ │ │ │ │ ├── tipSearch.js
│ │ │ │ │ └── util.js
│ │ │ │ ├── kw/
│ │ │ │ │ ├── album.js
│ │ │ │ │ ├── api-temp.js
│ │ │ │ │ ├── api-test.js
│ │ │ │ │ ├── comment.js
│ │ │ │ │ ├── hotSearch.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── kwdecode.ts
│ │ │ │ │ ├── leaderboard.js
│ │ │ │ │ ├── lyric.js
│ │ │ │ │ ├── musicSearch.js
│ │ │ │ │ ├── pic.js
│ │ │ │ │ ├── songList.js
│ │ │ │ │ ├── tipSearch.js
│ │ │ │ │ └── util.js
│ │ │ │ ├── mg/
│ │ │ │ │ ├── temp/
│ │ │ │ │ │ └── leaderboard-old.js
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── mrc.js
│ │ │ │ │ ├── album.js
│ │ │ │ │ ├── api-test.js
│ │ │ │ │ ├── comment.js
│ │ │ │ │ ├── hotSearch.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── leaderboard.js
│ │ │ │ │ ├── lyric.js
│ │ │ │ │ ├── musicInfo.js
│ │ │ │ │ ├── musicSearch.js
│ │ │ │ │ ├── pic.js
│ │ │ │ │ ├── songId.js
│ │ │ │ │ ├── songList.js
│ │ │ │ │ └── tipSearch.js
│ │ │ │ ├── tx/
│ │ │ │ │ ├── api-test.js
│ │ │ │ │ ├── comment.js
│ │ │ │ │ ├── hotSearch.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── leaderboard.js
│ │ │ │ │ ├── lyric.js
│ │ │ │ │ ├── musicInfo.js
│ │ │ │ │ ├── musicSearch.js
│ │ │ │ │ ├── singer.js
│ │ │ │ │ ├── songList.js
│ │ │ │ │ └── tipSearch.js
│ │ │ │ ├── wy/
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ ├── crypto.js
│ │ │ │ │ │ └── index.js
│ │ │ │ │ ├── api-test.js
│ │ │ │ │ ├── comment.js
│ │ │ │ │ ├── hotSearch.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── leaderboard.js
│ │ │ │ │ ├── lyric.js
│ │ │ │ │ ├── musicDetail.js
│ │ │ │ │ ├── musicInfo.js
│ │ │ │ │ ├── musicSearch.js
│ │ │ │ │ ├── singer.js
│ │ │ │ │ ├── songList.js
│ │ │ │ │ └── tipSearch.js
│ │ │ │ ├── api-source-info.ts
│ │ │ │ ├── index.js
│ │ │ │ ├── options.js
│ │ │ │ └── utils.js
│ │ │ ├── array.ts
│ │ │ ├── index.ts
│ │ │ ├── object.ts
│ │ │ ├── path.ts
│ │ │ ├── request.js
│ │ │ └── utils.ts
│ │ ├── autoUpdate.ts
│ │ └── index.ts
│ ├── preload/
│ │ ├── index.d.ts
│ │ └── index.ts
│ ├── renderer/
│ │ ├── public/
│ │ │ ├── default-cover.png
│ │ │ ├── head.jpg
│ │ │ ├── logo.svg
│ │ │ ├── star.png
│ │ │ └── wldss.png
│ │ ├── src/
│ │ │ ├── api/
│ │ │ │ └── songList.ts
│ │ │ ├── components/
│ │ │ │ ├── AI/
│ │ │ │ │ └── FloatBall.vue
│ │ │ │ ├── Music/
│ │ │ │ │ └── SongVirtualList.vue
│ │ │ │ ├── Play/
│ │ │ │ │ ├── AudioVisualizer.vue
│ │ │ │ │ ├── FullPlay.vue
│ │ │ │ │ ├── GlobalAudio.vue
│ │ │ │ │ ├── PlaylistActions.vue
│ │ │ │ │ ├── PlaylistDrawer.vue
│ │ │ │ │ ├── PlayMusic.vue
│ │ │ │ │ └── ShaderBackground.vue
│ │ │ │ ├── Search/
│ │ │ │ │ └── SearchComponent.vue
│ │ │ │ ├── Settings/
│ │ │ │ │ ├── AIFloatBallSettings.vue
│ │ │ │ │ ├── DirectorySettings.vue
│ │ │ │ │ ├── MusicCache.vue
│ │ │ │ │ ├── PlaylistSettings.vue
│ │ │ │ │ └── UpdateSettings.vue
│ │ │ │ ├── ThemeSelector.vue
│ │ │ │ ├── TitleBarControls.vue
│ │ │ │ ├── UpdateExample.vue
│ │ │ │ ├── UpdateProgress.vue
│ │ │ │ └── Versions.vue
│ │ │ ├── composables/
│ │ │ │ └── useAutoUpdate.ts
│ │ │ ├── layout/
│ │ │ │ └── index.vue
│ │ │ ├── router/
│ │ │ │ └── index.ts
│ │ │ ├── services/
│ │ │ │ ├── music/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── service-base.ts
│ │ │ │ └── autoUpdateService.ts
│ │ │ ├── store/
│ │ │ │ ├── ControlAudio.ts
│ │ │ │ ├── LocalUserDetail.ts
│ │ │ │ ├── search.ts
│ │ │ │ └── Settings.ts
│ │ │ ├── types/
│ │ │ │ ├── audio.ts
│ │ │ │ ├── Sources.ts
│ │ │ │ └── userInfo.ts
│ │ │ ├── utils/
│ │ │ │ ├── audio/
│ │ │ │ │ ├── audioManager.ts
│ │ │ │ │ ├── download.ts
│ │ │ │ │ ├── useSmtc.ts
│ │ │ │ │ └── volume.ts
│ │ │ │ ├── color/
│ │ │ │ │ ├── colorExtractor.ts
│ │ │ │ │ └── contrastColor.ts
│ │ │ │ └── playlist/
│ │ │ │ ├── playlistExportImport.ts
│ │ │ │ └── playlistManager.ts
│ │ │ ├── views/
│ │ │ │ ├── home/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── music/
│ │ │ │ │ ├── find.vue
│ │ │ │ │ ├── list.vue
│ │ │ │ │ ├── local.vue
│ │ │ │ │ ├── recent.vue
│ │ │ │ │ └── search.vue
│ │ │ │ ├── settings/
│ │ │ │ │ ├── index.vue
│ │ │ │ │ └── plugins.vue
│ │ │ │ └── welcome/
│ │ │ │ └── index.vue
│ │ │ ├── App.vue
│ │ │ ├── env.d.ts
│ │ │ └── main.ts
│ │ ├── auto-imports.d.ts
│ │ ├── components.d.ts
│ │ └── index.html
│ └── types/
│ ├── musicCache.ts
│ └── songList.ts
├── website/
│ ├── CeruUse.html
│ ├── design.html
│ ├── index.html
│ ├── pluginDev.html
│ ├── script.js
│ └── styles.css
├── electron-builder.yml
├── electron.vite.config.ts
├── eslint.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── qodana.sarif.json
├── qodana.yaml
├── README.md
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.web.json
└── yarn.lock
```
## 主要功能
- 提供插件加载与管理功能,支持通过合规插件获取公开音乐信息

View File

@@ -284,7 +284,4 @@ html .vp-doc div[class*='language-'] pre {
}
.VPDoc.has-aside .content-container {
max-width: none !important;
}
.vp-doc{
// padding: min(3vw, 64px) !important;
}

0
docs/guide/analyze.md Normal file
View File

76922
qodana.sarif.json Normal file

File diff suppressed because one or more lines are too long

3
qodana.yaml Normal file
View File

@@ -0,0 +1,3 @@
version: "1.0"
profile:
name: qodana.starter

55
scripts/genAst.js Normal file
View File

@@ -0,0 +1,55 @@
const fs = require('fs');
const path = require('path');
function generateTree(dir, prefix = '', isLast = true, excludeDirs = ['node_modules', 'dist', 'out', '.git','.kiro','.idea','.codebuddy','.vscode','.workflow','assets','resources','docs']) {
const basename = path.basename(dir);
// 跳过排除的目录和隐藏文件
if (basename.startsWith('.') && basename !== '.' && basename !== '..' && !['.github', '.workflow'].includes(basename)) {
return;
}
if (excludeDirs.includes(basename)) {
return;
}
// 当前项目显示
if (prefix === '') {
console.log(`${basename}/`);
} else {
const connector = isLast ? '└── ' : '├── ';
const displayName = fs.statSync(dir).isDirectory() ? `${basename}/` : basename;
console.log(prefix + connector + displayName);
}
if (!fs.statSync(dir).isDirectory()) {
return;
}
try {
const items = fs.readdirSync(dir)
.filter(item => !item.startsWith('.') || ['.github', '.workflow'].includes(item))
.filter(item => !excludeDirs.includes(item))
.sort((a, b) => {
// 目录排在前面,文件排在后面
const aIsDir = fs.statSync(path.join(dir, a)).isDirectory();
const bIsDir = fs.statSync(path.join(dir, b)).isDirectory();
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
return a.localeCompare(b);
});
const newPrefix = prefix + (isLast ? ' ' : '│ ');
items.forEach((item, index) => {
const isLastItem = index === items.length - 1;
generateTree(path.join(dir, item), newPrefix, isLastItem, excludeDirs);
});
} catch (error) {
console.error(`Error reading directory: ${dir}`, error.message);
}
}
// 使用示例
const targetDir = process.argv[2] || '.';
console.log('项目文件结构:');
generateTree(targetDir);

View File

@@ -1,150 +0,0 @@
// 业务工具方法
import { LX } from '../../types/global'
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {
const meta: Record<string, any> = {
songId: oldMusicInfo.songmid, // 歌曲IDlocal为文件路径
albumName: oldMusicInfo.albumName, // 歌曲专辑名称
picUrl: oldMusicInfo.img // 歌曲图片链接
}
const newInfo = {
id: `${oldMusicInfo.source}_${oldMusicInfo.songmid}`,
name: oldMusicInfo.name,
singer: oldMusicInfo.singer,
source: oldMusicInfo.source,
interval: oldMusicInfo.interval,
meta: meta as LX.Music.MusicInfoOnline['meta']
}
if (oldMusicInfo.source == 'local') {
meta.filePath = oldMusicInfo.filePath ?? oldMusicInfo.songmid ?? ''
meta.ext = oldMusicInfo.ext ?? /\.(\w+)$/.exec(meta.filePath)?.[1] ?? ''
} else {
meta.qualitys = oldMusicInfo.types
meta._qualitys = oldMusicInfo._types
meta.albumId = oldMusicInfo.albumId
if (meta._qualitys.flac32bit && !meta._qualitys.flac24bit) {
meta._qualitys.flac24bit = meta._qualitys.flac32bit
delete meta._qualitys.flac32bit
meta.qualitys = (meta.qualitys as any[]).map((quality) => {
if (quality.type == 'flac32bit') quality.type = 'flac24bit'
return quality
})
}
switch (oldMusicInfo.source) {
case 'kg':
meta.hash = oldMusicInfo.hash
newInfo.id = oldMusicInfo.songmid + '_' + oldMusicInfo.hash
break
case 'tx':
meta.strMediaMid = oldMusicInfo.strMediaMid
meta.id = oldMusicInfo.songId
meta.albumMid = oldMusicInfo.albumMid
break
case 'mg':
meta.copyrightId = oldMusicInfo.copyrightId
meta.lrcUrl = oldMusicInfo.lrcUrl
meta.mrcUrl = oldMusicInfo.mrcUrl
meta.trcUrl = oldMusicInfo.trcUrl
break
}
}
return newInfo
}
export const toOldMusicInfo = (minfo: LX.Music.MusicInfo) => {
const oInfo: Record<string, any> = {
name: minfo.name,
singer: minfo.singer,
source: minfo.source,
songmid: minfo.meta.songId,
interval: minfo.interval,
albumName: minfo.meta.albumName,
img: minfo.meta.picUrl ?? '',
typeUrl: {}
}
if (minfo.source == 'local') {
oInfo.filePath = minfo.meta.filePath
oInfo.ext = minfo.meta.ext
oInfo.albumId = ''
oInfo.types = []
oInfo._types = {}
} else {
oInfo.albumId = minfo.meta.albumId
oInfo.types = minfo.meta.qualitys
oInfo._types = minfo.meta._qualitys
switch (minfo.source) {
case 'kg':
oInfo.hash = minfo.meta.hash
break
case 'tx':
oInfo.strMediaMid = minfo.meta.strMediaMid
oInfo.albumMid = minfo.meta.albumMid
oInfo.songId = minfo.meta.id
break
case 'mg':
oInfo.copyrightId = minfo.meta.copyrightId
oInfo.lrcUrl = minfo.meta.lrcUrl
oInfo.mrcUrl = minfo.meta.mrcUrl
oInfo.trcUrl = minfo.meta.trcUrl
break
}
}
return oInfo
}
/**
* 修复2.0.0-dev.8之前的新列表数据音质
* @param musicInfo
*/
export const fixNewMusicInfoQuality = (musicInfo: LX.Music.MusicInfo) => {
if (musicInfo.source == 'local') return musicInfo
// @ts-expect-error
if (musicInfo.meta._qualitys.flac32bit && !musicInfo.meta._qualitys.flac24bit) {
// @ts-expect-error
musicInfo.meta._qualitys.flac24bit = musicInfo.meta._qualitys.flac32bit
// @ts-expect-error
delete musicInfo.meta._qualitys.flac32bit
musicInfo.meta.qualitys = musicInfo.meta.qualitys.map((quality) => {
// @ts-expect-error
if (quality.type == 'flac32bit') quality.type = 'flac24bit'
return quality
})
}
return musicInfo
}
export const filterMusicList = <T extends LX.Music.MusicInfo>(list: T[]): T[] => {
const ids = new Set<string>()
return list.filter((s) => {
if (!s.id || ids.has(s.id) || !s.name) return false
if (s.singer == null) s.singer = ''
ids.add(s.id)
return true
})
}
const MAX_NAME_LENGTH = 80
const MAX_FILE_NAME_LENGTH = 150
export const clipNameLength = (name: string) => {
if (name.length <= MAX_NAME_LENGTH || !name.includes('、')) return name
const names = name.split('、')
let newName = names.shift()!
for (const name of names) {
if (newName.length + name.length > MAX_NAME_LENGTH) break
newName = newName + '、' + name
}
return newName
}
export const clipFileNameLength = (name: string) => {
return name.length > MAX_FILE_NAME_LENGTH ? name.substring(0, MAX_FILE_NAME_LENGTH) : name
}

View File

@@ -22,7 +22,7 @@ const UPDATE_API_URL = `${UPDATE_SERVER}/update/${process.platform}/${app.getVer
// Alist API 配置
const ALIST_BASE_URL = 'https://alist.shiqianjiang.cn' // 请替换为实际的 alist 域名
const ALIST_USERNAME = 'ceruupdate'
const ALIST_PASSWORD = '123456'
const ALIST_PASSWORD = '123456' //登录公开的账号密码
// Alist 认证 token
let alistToken: string | null = null

View File

@@ -1,3 +1,14 @@
/*
* Copyright (c) 2025. 时迁酱 Inc. All rights reserved.
*
* This software is the confidential and proprietary information of 时迁酱.
* Unauthorized copying of this file, via any medium is strictly prohibited.
*
* @author 时迁酱无聊的霜霜Star
* @since 2025-9-19
* @version 1.0
*/
import * as vm from 'vm'
import fetch from 'node-fetch'
import * as fs from 'fs'

View File

@@ -1,10 +1,9 @@
// 导入通用工具函数
import { dateFormat } from '../../common/utils/common'
import { dateFormat } from '@common/utils/common'
// 导出通用工具函数
export * from '../../common/utils/nodejs'
export * from '../../common/utils/common'
export * from '../../common/utils/tools'
/**
* 格式化播放数量

View File

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

View File

@@ -4,7 +4,6 @@ import { dateFormat } from '../../common/utils/common'
// 导出通用工具函数
export * from '../../common/utils/nodejs'
export * from '../../common/utils/common'
export * from '../../common/utils/tools'
/**
* 格式化播放数量

View File

@@ -22,16 +22,18 @@ const api = {
},
toggleFullscreen: () => ipcRenderer.send('window-toggle-fullscreen'),
onMusicCtrl: (callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
// 音乐控制
const handler = (event: Electron.IpcRendererEvent) => callback(event)
ipcRenderer.on('music-control', handler)
return () => ipcRenderer.removeListener('music-control', handler)
},
// 音乐相关方法
music: {
request: (api: string, args: any) => ipcRenderer.invoke('service-music-request', api, args),
requestSdk: (api: string, args: any) =>
ipcRenderer.invoke('service-music-sdk-request', api, args)
},
//音源插件
plugins: {
selectAndAddPlugin: (type: 'lx' | 'cr') =>
ipcRenderer.invoke('service-plugin-selectAndAddPlugin', type),
@@ -43,7 +45,7 @@ const api = {
ipcRenderer.invoke('service-plugin-uninstallPlugin', pluginId),
getPluginLog: (pluginId: string) => ipcRenderer.invoke('service-plugin-getPluginLog', pluginId)
},
// ai助手
ai: {
ask: (prompt: string) => ipcRenderer.invoke('ai-ask', prompt),
askStream: (prompt: string, streamId: string) =>
@@ -63,7 +65,7 @@ const api = {
ipcRenderer.removeAllListeners('ai-stream-error')
}
},
// 音频缓存管理
musicCache: {
getInfo: () => ipcRenderer.invoke('music-cache:get-info'),
clear: () => ipcRenderer.invoke('music-cache:clear'),

View File

@@ -3,11 +3,6 @@
<head lang="zh-CN">
<meta charset="UTF-8" />
<title>澜音 Music</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' *.gtimg.com; style-src 'self' 'unsafe-inline'; img-src *; connect-src 'self' https://music.163.com https://*.music.163.com *.bikonoo.com/ ; media-src 'self' data: https://* http://*;"
/> -->
</head>
<body>

View File

@@ -1,3 +1,14 @@
<!--
- Copyright (c) 2025. 时迁酱 Inc. All rights reserved.
-
- This software is the confidential and proprietary information of 时迁酱.
- Unauthorized copying of this file, via any medium is strictly prohibited.
-
- @author 时迁酱无聊的霜霜Star
- @since 2025-9-19
- @version 1.0
-->
<script setup lang="ts">
import { onMounted } from 'vue'
import GlobalAudio from './components/Play/GlobalAudio.vue'

View File

@@ -77,3 +77,6 @@ body {
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
}
.t-dialog__mask{
backdrop-filter: blur(5px);
}

View File

@@ -133,7 +133,7 @@
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-active: rgba(255, 255, 255, 0.6);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}
@@ -244,7 +244,7 @@
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-active: rgba(0, 0, 0, 0.1);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
}

View File

@@ -132,7 +132,7 @@
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-active: rgba(0, 0, 0, 0.1);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}
@@ -243,7 +243,7 @@
--td-bg-color-specialcomponent: transparent;
--td-border-level-1-color: var(--td-gray-color-11);
--td-border-level-2-color: var(--td-gray-color-9);
--td-mask-active: rgba(0, 0, 0, 0.4);
--td-mask-active: rgba(0, 0, 0, 0.1);
--td-mask-disabled: rgba(0, 0, 0, 0.6);
}

View File

@@ -132,7 +132,7 @@
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-active: rgba(0, 0, 0, 0.1);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}

View File

@@ -132,7 +132,7 @@
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-active: rgba(0, 0, 0, 0.1);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}

View File

@@ -132,7 +132,7 @@
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
--td-mask-active: rgba(0, 0, 0, 0.6);
--td-mask-active: rgba(0, 0, 0, 0.1);
--td-mask-disabled: rgba(255, 255, 255, 0.6);
}

View File

@@ -2,7 +2,7 @@
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { ControlAudioStore } from '@renderer/store/ControlAudio'
import { storeToRefs } from 'pinia'
import audioManager from '@renderer/utils/audioManager'
import audioManager from '@renderer/utils/audio/audioManager'
interface Props {
show?: boolean

View File

@@ -12,7 +12,7 @@ import {
import type { SongList } from '@renderer/types/audio'
import type { LyricLine } from '@applemusic-like-lyrics/core'
import { ref, computed, onMounted, watch, reactive, onBeforeUnmount, toRaw } from 'vue'
import { shouldUseBlackText } from '@renderer/utils/contrastColor'
import { shouldUseBlackText } from '@renderer/utils/color/contrastColor'
import { ControlAudioStore } from '@renderer/store/ControlAudio'
import { Fullscreen1Icon, FullscreenExit1Icon, ChevronDownIcon } from 'tdesign-icons-vue-next'
// 直接从包路径导入,避免 WebAssembly 导入问题

View File

@@ -18,16 +18,16 @@ const { liebiao, shengyin } = icons
import { storeToRefs } from 'pinia'
import FullPlay from './FullPlay.vue'
import PlaylistDrawer from './PlaylistDrawer.vue'
import { extractDominantColor } from '@renderer/utils/colorExtractor'
import { getBestContrastTextColorWithOpacity } from '@renderer/utils/contrastColor'
import { extractDominantColor } from '@renderer/utils/color/colorExtractor'
import { getBestContrastTextColorWithOpacity } from '@renderer/utils/color/contrastColor'
import { PlayMode, type SongList } from '@renderer/types/audio'
import { MessagePlugin } from 'tdesign-vue-next'
import {
initPlaylistEventListeners,
destroyPlaylistEventListeners,
getSongRealUrl
} from '@renderer/utils/playlistManager'
import mediaSessionController from '@renderer/utils/useSmtc'
} from '@renderer/utils/playlist/playlistManager'
import mediaSessionController from '@renderer/utils/audio/useSmtc'
import defaultCoverImg from '/default-cover.png'
const controlAudio = ControlAudioStore()

View File

@@ -8,7 +8,7 @@ import {
importPlaylistFromFile,
importPlaylistFromClipboard,
validateImportedPlaylist
} from '@renderer/utils/playlistExportImport'
} from '@renderer/utils/playlist/playlistExportImport'
import { CloudDownloadIcon } from 'tdesign-icons-vue-next'
import type { SongList } from '@renderer/types/audio'
import { storeToRefs } from 'pinia'

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, watch, computed } from 'vue'
import { extractColors, Color } from '@renderer/utils/colorExtractor'
import { extractColors, Color } from '@renderer/utils/color/colorExtractor'
import DefaultCover from '@renderer/assets/images/Default.jpg'
import CoverImage from '@renderer/assets/images/cover.png'
@@ -40,7 +40,7 @@ const actualCoverImage = computed(() => {
const vertexShaderSource = `
attribute vec2 a_position;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_position * 0.5 + 0.5;
@@ -53,81 +53,81 @@ const fragmentShaderSource = `
varying vec2 v_texCoord;
uniform float u_time;
uniform vec3 u_color;
// 改进的随机函数 - 更平滑
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}
// 改进的噪声函数 - 使用三次Hermite插值更平滑
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
// 四个角的随机值
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
// 使用三次Hermite插值更加平滑
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) +
(c - a) * u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
// 改进的分数布朗运动 - 降低频率,减少网格感
float fbm(vec2 st) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 0.6; // 降低初始频率
// 减少迭代次数,使用更平滑的混合
for (int i = 0; i < 4; i++) { // 减少迭代次数
value += amplitude * noise(st * frequency);
frequency *= 1.8; // 降低频率增长率
amplitude *= 0.6; // 提高振幅衰减率
}
// 额外的平滑处理
return smoothstep(0.2, 0.8, value);
}
// HSV转RGB颜色
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
// 使用时间和位置创建动态效果
vec2 st = v_texCoord;
float time = u_time * 0.20; // 降低时间速率,使动画更平滑
// 创建更平滑的移动噪声场
vec2 q = vec2(
fbm(st + vec2(0.0, time * 0.3)),
fbm(st + vec2(time * 0.2, 0.0))
);
// 使用q创建第二层噪声降低强度
vec2 r = vec2(
fbm(st + 2.0 * q + vec2(1.7, 9.2) + time * 0.1),
fbm(st + 2.0 * q + vec2(8.3, 2.8) + time * 0.08)
);
// 最终的噪声值 - 额外平滑处理
float f = fbm(st + r * 0.7);
// 从主色调提取HSV
vec3 baseColor = u_color;
float maxComp = max(max(baseColor.r, baseColor.g), baseColor.b);
float minComp = min(min(baseColor.r, baseColor.g), baseColor.b);
float delta = maxComp - minComp;
// 估算色相
float hue = 0.0;
if (delta > 0.0) {
@@ -140,78 +140,78 @@ const fragmentShaderSource = `
}
hue /= 6.0;
}
// 估算饱和度和明度
float saturation = maxComp == 0.0 ? 0.0 : delta / maxComp;
float value = maxComp;
// 提高基础亮度和饱和度,使颜色更加明亮清新
saturation = min(saturation * 1.0, 1.0); // 增加饱和度
value = min(value * 1.3, 1.0); // 增加亮度
// 创建多个颜色变体 - 更明亮的变体
vec3 color1 = hsv2rgb(vec3(hue, saturation * 0.9, min(value * 1.1, 1.0)));
vec3 color2 = hsv2rgb(vec3(mod(hue + 0.05, 1.0), min(saturation * 1.3, 1.0), min(value * 1.2, 1.0)));
vec3 color3 = hsv2rgb(vec3(mod(hue + 0.1, 1.0), min(saturation * 1.1, 1.0), min(value * 1.15, 1.0)));
vec3 color4 = hsv2rgb(vec3(mod(hue - 0.05, 1.0), min(saturation * 1.2, 1.0), min(value * 1.25, 1.0)));
// 使用噪声值混合多个颜色 - 更平滑的混合,使用更多主色调
float t1 = smoothstep(0.0, 1.0, f);
float t2 = sin(f * 3.14) * 0.5 + 0.5;
float t3 = cos(f * 2.0 + time * 0.5) * 0.5 + 0.5;
float t4 = sin(f * 4.0 + time * 0.3) * 0.5 + 0.5; // 额外的混合因子
// 创建两个额外的颜色变体,增加色彩丰富度
vec3 color5 = hsv2rgb(vec3(mod(hue + 0.15, 1.0), min(saturation * 1.4, 1.0), min(value * 1.3, 1.0)));
vec3 color6 = hsv2rgb(vec3(mod(hue - 0.15, 1.0), min(saturation * 1.3, 1.0), min(value * 1.2, 1.0)));
// 混合所有颜色
vec3 colorMix1 = mix(color1, color2, t1);
vec3 colorMix2 = mix(color3, color4, t2);
vec3 colorMix3 = mix(color5, color6, t4);
vec3 color = mix(
mix(colorMix1, colorMix2, t3),
colorMix3,
sin(f * 2.5 + time * 0.4) * 0.5 + 0.5
);
// 添加更多的动态亮点和波纹
color += 0.15 * sin(f * 8.0 + time) * vec3(1.0);
// 增加波纹效果
float ripple1 = sin(st.x * 12.0 + time * 0.8) * sin(st.y * 12.0 + time * 0.7) * 0.06;
float ripple2 = sin(st.x * 8.0 - time * 0.6) * sin(st.y * 8.0 - time * 0.5) * 0.05;
float ripple3 = sin(st.x * 15.0 + time * 0.4) * sin(st.y * 15.0 + time * 0.3) * 0.04;
// 混合多层波纹
color += vec3(ripple1 + ripple2 + ripple3);
// 添加更大范围、更柔和的光晕效果
float glow = smoothstep(0.3, 0.7, f);
color = mix(color, vec3(1.0), glow * 0.12);
// 添加柔和的渐变效果,进一步减少网格感
float vignette = smoothstep(0.0, 0.7, 0.5 - length(st - 0.5));
color = mix(color, color * 1.2, vignette * 0.3);
// 应用高斯模糊效果,减少锐利的网格边缘
vec2 pixel = vec2(1.0) / vec2(800.0, 600.0); // 假设的分辨率
float blur = 0.0;
// 简化的高斯模糊 - 只采样几个点以保持性能
blur += f * 0.5;
blur += fbm(st + pixel * vec2(1.0, 0.0)) * 0.125;
blur += fbm(st + pixel * vec2(-1.0, 0.0)) * 0.125;
blur += fbm(st + pixel * vec2(0.0, 1.0)) * 0.125;
blur += fbm(st + pixel * vec2(0.0, -1.0)) * 0.125;
// 使用模糊值平滑颜色过渡
color = mix(color, mix(color1, color4, 0.5), (blur - f) * 0.2);
// 确保颜色在有效范围内
color = clamp(color, 0.0, 1.0);
gl_FragColor = vec4(color, 1.0);
}
`

View File

@@ -1,162 +0,0 @@
# 主题切换组件使用说明
## 概述
ThemeSelector 是一个现代化的主题切换组件,支持在多个预设主题色之间切换。组件与现有的 TDesign 主题系统完全兼容。
## 功能特性
- ✅ 支持多种预设主题色(默认、粉色、蓝色、青色、橙色)
- ✅ 使用 `theme-mode` 属性实现主题切换
- ✅ 自动保存用户选择到本地存储
- ✅ 现代化的下拉选择界面
- ✅ 平滑的过渡动画效果
- ✅ 响应式设计,支持移动端
- ✅ 与 TDesign 主题系统完全兼容
## 使用方法
### 1. 基本使用
```vue
<template>
<div>
<!-- 在任何需要的地方使用主题切换器 -->
<ThemeSelector />
</div>
</template>
<script setup>
import ThemeSelector from '@/components/ThemeSelector.vue'
</script>
```
### 2. 在导航栏中使用
```vue
<template>
<header class="app-header">
<h1>应用标题</h1>
<div class="header-actions">
<ThemeSelector />
</div>
</header>
</template>
```
### 3. 在设置页面中使用
```vue
<template>
<div class="settings-page">
<div class="setting-item">
<label>主题色</label>
<ThemeSelector />
</div>
</div>
</template>
```
## 主题切换原理
组件通过以下方式实现主题切换:
1. **默认主题**: 移除 `theme-mode` 属性
```javascript
document.documentElement.removeAttribute('theme-mode')
```
2. **其他主题**: 设置对应的 `theme-mode` 属性
```javascript
document.documentElement.setAttribute('theme-mode', 'pink')
```
## 支持的主题
| 主题名称 | 属性值 | 主色调 |
| -------- | --------- | ------- |
| 默认 | `default` | #57b4ff |
| 粉色 | `pink` | #fc5e7e |
| 蓝色 | `blue` | #57b4ff |
| 青色 | `cyan` | #3ac2b8 |
| 橙色 | `orange` | #fb9458 |
## 自定义配置
如果需要添加新的主题,请按以下步骤操作:
### 1. 创建主题CSS文件
在 `src/renderer/src/assets/theme/` 目录下创建新的主题文件,例如 `green.css`
```css
:root[theme-mode='green'] {
--td-brand-color: #10b981;
--td-brand-color-hover: #059669;
/* 其他主题变量... */
}
```
### 2. 更新组件配置
在 `ThemeSelector.vue` 中添加新主题:
```javascript
const themes = [
// 现有主题...
{ name: 'green', label: '绿色', color: '#10b981' }
]
```
## 样式自定义
组件使用 TDesign 的 CSS 变量,可以通过覆盖这些变量来自定义样式:
```css
.theme-selector {
/* 自定义触发器样式 */
--td-radius-medium: 8px;
}
.theme-dropdown {
/* 自定义下拉菜单样式 */
--td-shadow-2: 0 8px 25px rgba(0, 0, 0, 0.15);
}
```
## 事件和回调
组件会自动处理主题切换和本地存储,无需额外配置。如果需要监听主题变化,可以监听 `localStorage` 的变化:
```javascript
// 监听主题变化
window.addEventListener('storage', (e) => {
if (e.key === 'selected-theme') {
console.log('主题已切换到:', e.newValue)
}
})
```
## 注意事项
1. 确保项目中已引入对应的主题CSS文件
2. 组件会自动加载用户上次选择的主题
3. 主题切换是全局的,会影响整个应用
4. 建议在应用的主入口处使用,避免重复渲染
## 演示组件
项目还包含一个 `ThemeDemo.vue` 组件,展示了主题切换的效果:
```vue
<template>
<ThemeDemo />
</template>
<script setup>
import ThemeDemo from '@/components/ThemeDemo.vue'
</script>
```
这个演示组件展示了不同UI元素在各种主题下的表现。

View File

@@ -9,7 +9,7 @@ import {
importPlaylistFromFile,
importPlaylistFromClipboard,
validateImportedPlaylist
} from '@renderer/utils/playlistExportImport'
} from '@renderer/utils/playlist/playlistExportImport'
import type { SongList } from '@renderer/types/audio'
import { CloudDownloadIcon, DeleteIcon, CloudUploadIcon } from 'tdesign-icons-vue-next'

View File

@@ -0,0 +1,380 @@
<script setup lang="ts">
import TitleBarControls from '@renderer/components/TitleBarControls.vue'
import { SearchIcon } from 'tdesign-icons-vue-next'
import { onMounted, ref, watchEffect } from 'vue'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { useRouter } from 'vue-router'
import { searchValue } from '@renderer/store/search'
onMounted(() => {
const LocalUserDetail = LocalUserDetailStore()
watchEffect(() => {
source.value = sourceicon[LocalUserDetail.userSource.source || 'wy']
})
})
const sourceicon = {
kg: 'kugouyinle',
wy: 'wangyiyun',
mg: 'mg',
tx: 'tx',
kw: 'kw'
}
const source = ref('kugouyinle')
interface MenuItem {
name: string
icon: string
path: string
}
const menuList: MenuItem[] = [
{
name: '发现',
icon: 'icon-faxian',
path: '/home/find'
},
{
name: '本地',
icon: 'icon-music',
path: '/home/local'
},
{
name: '最近',
icon: 'icon-shijian',
path: '/home/recent'
}
]
const menuActive = ref(0)
const router = useRouter()
const handleClick = (index: number): void => {
menuActive.value = index
router.push(menuList[index].path)
}
// 导航历史前进后退功能
const goBack = (): void => {
router.go(-1)
}
const goForward = (): void => {
router.go(1)
}
// 搜索相关
const keyword = ref('')
// 搜索类型1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单
// const searchType = ref(1)
// 处理搜索事件
const handleSearch = async () => {
if (!keyword.value.trim()) return
const useSearch = searchValue()
// 重新设置搜索关键字
try {
// 跳转到搜索结果页面,并传递搜索结果和关键词
useSearch.setValue(keyword.value.trim()) // 设置搜索关键字
router.push({
path: '/home/search'
})
} catch (error) {
console.error('搜索失败:', error)
}
}
// 处理按键事件,按下回车键时触发搜索
const handleKeyDown = () => {
handleSearch()
}
</script>
<template>
<t-layout class="home-container">
<!-- sidebar -->
<t-aside class="sidebar">
<div class="sidebar-content">
<div class="logo-section">
<div class="logo-icon">
<i class="iconfont icon-music"></i>
</div>
<p class="app-title">
<span style="color: #000; font-weight: 800">Ceru Music</span>
</p>
</div>
<nav class="nav-section">
<t-button
v-for="(item, index) in menuList"
:key="index"
:variant="menuActive == index ? 'base' : 'text'"
:class="menuActive == index ? 'nav-button active' : 'nav-button'"
block
@click="handleClick(index)"
>
<i :class="`iconfont ${item.icon} nav-icon`"></i>
{{ item.name }}
</t-button>
</nav>
</div>
</t-aside>
<t-layout style="flex: 1">
<t-content>
<div class="content">
<!-- Header -->
<div class="header">
<t-button shape="circle" theme="default" class="nav-btn" @click="goBack">
<i class="iconfont icon-xiangzuo"></i>
</t-button>
<t-button shape="circle" theme="default" class="nav-btn" @click="goForward">
<i class="iconfont icon-xiangyou"></i>
</t-button>
<div class="search-container">
<div class="search-input">
<svg class="icon" aria-hidden="true">
<use :xlink:href="`#icon-${source}`"></use>
</svg>
<t-input
v-model="keyword"
placeholder="搜索音乐、歌手"
style="width: 100%"
@enter="handleKeyDown"
>
<template #suffix>
<t-button
theme="primary"
variant="text"
shape="circle"
style="display: flex; align-items: center; justify-content: center"
@click="handleSearch"
>
<SearchIcon style="font-size: 16px; color: #000" />
</t-button>
</template>
</t-input>
</div>
<TitleBarControls :color="'#000'"></TitleBarControls>
</div>
</div>
<div class="mainContent">
<slot name="body"></slot>
</div>
</div>
</t-content>
</t-layout>
</t-layout>
</template>
<style scoped lang="scss">
:deep(.animate__animated) {
position: absolute;
width: 100%;
}
.home-container {
height: calc(100vh - var(--play-bottom-height));
overflow-y: hidden;
position: relative;
}
.icon {
width: 1.5rem;
height: 1.5rem;
}
.sidebar {
width: 15rem;
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -140vh, #ffffff 30vh);
border-right: 0.0625rem solid #e5e7eb;
flex-shrink: 0;
.sidebar-content {
padding: 1rem;
.logo-section {
-webkit-app-region: drag;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
.logo-icon {
width: 2rem;
height: 2rem;
background-color: var(--td-brand-color-4);
border-radius: 0.625rem;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 1.25rem;
color: white;
}
}
.app-title {
font-weight: 500;
font-size: 1.125rem;
color: #111827;
span {
font-weight: 500;
color: #b8f0cc;
}
}
}
.nav-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
.nav-button {
justify-content: flex-start;
height: 2.4rem;
text-align: left;
padding: 0.7rem 1rem;
border-radius: 0.5rem;
border: none;
.nav-icon {
margin-right: 0.75rem;
font-size: 1rem;
}
&.active {
background-color: var(--td-brand-color-4);
color: rgb(255, 255, 255);
&:hover {
background-color: var(--td-brand-color-5);
}
}
&:not(.active) {
color: #6b7280;
&:hover {
color: #111827;
background-color: #f3f4f6;
}
}
}
}
}
}
:deep(.t-layout__content) {
height: 100%;
display: flex;
}
.content {
padding: 0;
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -110vh, #ffffff 15vh);
display: flex;
flex: 1;
flex-direction: column;
.header {
-webkit-app-region: drag;
display: flex;
align-items: center;
padding: 1.5rem;
.nav-btn {
-webkit-app-region: no-drag;
margin-right: 0.5rem;
&:last-of-type {
margin-right: 0.5rem;
}
.iconfont {
font-size: 1rem;
color: #3d4043;
}
&:hover .iconfont {
color: #111827;
}
}
.search-container {
display: flex;
flex: 1;
position: relative;
justify-content: space-between;
.search-input {
-webkit-app-region: no-drag;
display: flex;
align-items: center;
transition: width 0.3s;
padding: 0 0.5rem;
width: min(18.75rem, 400px);
margin-right: 0.5rem;
border-radius: 1.25rem !important;
background-color: #fff;
overflow: hidden;
&:has(input:focus) {
width: max(18.75rem, 400px);
}
}
:deep(.t-input) {
border-radius: 0rem !important;
border: none;
box-shadow: none;
}
.settings-btn {
.iconfont {
font-size: 1rem;
color: #6b7280;
}
&:hover .iconfont {
color: #111827;
}
}
}
}
.mainContent {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
height: 0;
/* 确保flex子元素能够正确计算高度 */
&::-webkit-scrollbar {
width: 0.375rem;
}
&::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 0.1875rem;
}
&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 0.1875rem;
transition: background-color 0.2s ease;
&:hover {
background: #94a3b8;
}
}
/* Firefox 滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
}
}
</style>

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { transitionVolume } from '@renderer/utils/volume'
import { transitionVolume } from '@renderer/utils/audio/volume'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import type {

View File

@@ -1,36 +0,0 @@
// 测试网易云音乐服务的IPC通信
export async function testNeteaseService() {
try {
console.log('开始测试网易云音乐服务...')
// 测试搜索功能
const searchResult = await window.api.music.request('search', {
type: 1,
keyword: '周杰伦',
limit: 10,
offset: 0
})
console.log('搜索结果:', searchResult)
// 如果搜索成功且有结果,测试获取歌曲详情
if (searchResult && searchResult.songs && searchResult.songs.length > 0) {
const songId = searchResult.songs[0].id
const songDetail = await window.api.music.request('getSongDetail', {
ids: [songId.toString()]
})
console.log('歌曲详情:', songDetail)
// 测试获取歌词
const lyric = await window.api.music.request('getLyric', {
id: songId.toString()
})
console.log('歌词:', lyric)
}
console.log('网易云音乐服务测试完成!')
return true
} catch (error) {
console.error('网易云音乐服务测试失败:', error)
return false
}
}

View File

@@ -1,3 +1,14 @@
/*
* Copyright (c) 2025. Inc. All rights reserved.
*
* This software is the confidential and proprietary information of .
* Unauthorized copying of this file, via any medium is strictly prohibited.
*
* @author Star
* @since 2025-9-19
* @version 1.0
*/
interface MediaSessionCallbacks {
play: () => void
pause: () => void
@@ -51,8 +62,7 @@ class MediaSessionController {
// 强制更新播放状态确保SMTC正确识别
if (this.audioElement) {
const currentState = this.audioElement.paused ? 'paused' : 'playing'
navigator.mediaSession.playbackState = currentState
navigator.mediaSession.playbackState = this.audioElement.paused ? 'paused' : 'playing'
}
} catch (error) {
console.warn('Failed to update media session metadata:', error)

View File

@@ -1,50 +0,0 @@
<template>
<div class="settings-page">
<div class="settings-container">
<h2>应用设置</h2>
<!-- 其他设置项 -->
<div class="settings-section">
<h3>常规设置</h3>
<!-- 这里可以添加其他设置项 -->
</div>
<!-- 自动更新设置 -->
<UpdateSettings />
</div>
</div>
</template>
<script setup lang="ts">
import UpdateSettings from '../components/Settings/UpdateSettings.vue'
</script>
<style scoped>
.settings-page {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.settings-container h2 {
margin-bottom: 24px;
font-size: 24px;
font-weight: 600;
color: var(--td-text-color-primary);
}
.settings-section {
margin-bottom: 32px;
padding: 20px;
background: var(--td-bg-color-container);
border-radius: 8px;
border: 1px solid var(--td-border-level-1-color);
}
.settings-section h3 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
color: var(--td-text-color-primary);
}
</style>

View File

@@ -1,182 +1,25 @@
<script setup lang="ts">
import PlayMusic from '@renderer/components/Play/PlayMusic.vue'
import TitleBarControls from '@renderer/components/TitleBarControls.vue'
import { onMounted, ref, watchEffect } from 'vue'
import { SearchIcon } from 'tdesign-icons-vue-next'
import { useRouter } from 'vue-router'
import { searchValue } from '@renderer/store/search'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
onMounted(() => {
const LocalUserDetail = LocalUserDetailStore()
watchEffect(() => {
source.value = sourceicon[LocalUserDetail.userSource.source || 'wy']
})
})
const sourceicon = {
kg: 'kugouyinle',
wy: 'wangyiyun',
mg: 'mg',
tx: 'tx',
kw: 'kw'
}
const source = ref('kugouyinle')
interface MenuItem {
name: string
icon: string
path: string
}
const menuList: MenuItem[] = [
{
name: '发现',
icon: 'icon-faxian',
path: '/home/find'
},
{
name: '本地',
icon: 'icon-music',
path: '/home/local'
},
{
name: '最近',
icon: 'icon-shijian',
path: '/home/recent'
}
]
const menuActive = ref(0)
const router = useRouter()
const handleClick = (index: number): void => {
menuActive.value = index
router.push(menuList[index].path)
}
// 导航历史前进后退功能
const goBack = (): void => {
router.go(-1)
}
const goForward = (): void => {
router.go(1)
}
// 搜索相关
const keyword = ref('')
// 搜索类型1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单
// const searchType = ref(1)
// 处理搜索事件
const handleSearch = async () => {
if (!keyword.value.trim()) return
const useSearch = searchValue()
// 重新设置搜索关键字
try {
// 跳转到搜索结果页面,并传递搜索结果和关键词
useSearch.setValue(keyword.value.trim()) // 设置搜索关键字
router.push({
path: '/home/search'
})
} catch (error) {
console.error('搜索失败:', error)
}
}
// 处理按键事件,按下回车键时触发搜索
const handleKeyDown = () => {
handleSearch()
}
import HomeLayout from '@renderer//layout/index.vue'
</script>
<template>
<div class="home">
<t-layout class="home-container">
<!-- sidebar -->
<t-aside class="sidebar">
<div class="sidebar-content">
<div class="logo-section">
<div class="logo-icon">
<i class="iconfont icon-music"></i>
</div>
<p class="app-title">
<span style="color: #000; font-weight: 800">Ceru Music</span>
</p>
</div>
<nav class="nav-section">
<t-button
v-for="(item, index) in menuList"
:key="index"
:variant="menuActive == index ? 'base' : 'text'"
:class="menuActive == index ? 'nav-button active' : 'nav-button'"
block
@click="handleClick(index)"
>
<i :class="`iconfont ${item.icon} nav-icon`"></i>
{{ item.name }}
</t-button>
</nav>
</div>
</t-aside>
<t-layout style="flex: 1">
<t-content>
<div class="content">
<!-- Header -->
<div class="header">
<t-button shape="circle" theme="default" class="nav-btn" @click="goBack">
<i class="iconfont icon-xiangzuo"></i>
</t-button>
<t-button shape="circle" theme="default" class="nav-btn" @click="goForward">
<i class="iconfont icon-xiangyou"></i>
</t-button>
<div class="search-container">
<div class="search-input">
<svg class="icon" aria-hidden="true">
<use :xlink:href="`#icon-${source}`"></use>
</svg>
<t-input
v-model="keyword"
placeholder="搜索音乐、歌手"
style="width: 100%"
@enter="handleKeyDown"
>
<template #suffix>
<t-button
theme="primary"
variant="text"
shape="circle"
style="display: flex; align-items: center; justify-content: center"
@click="handleSearch"
>
<SearchIcon style="font-size: 16px; color: #000" />
</t-button>
</template>
</t-input>
</div>
<TitleBarControls :color="'#000'"></TitleBarControls>
</div>
</div>
<div class="mainContent">
<router-view v-slot="{ Component, route }">
<Transition
name="page"
:enter-active-class="`animate__animated ${route.meta.transitionIn} animate__fast`"
:leave-active-class="`animate__animated ${route.meta.transitionOut} animate__fast`"
>
<KeepAlive exclude="list">
<component :is="Component" />
</KeepAlive>
</Transition>
</router-view>
</div>
</div>
</t-content>
</t-layout>
</t-layout>
<HomeLayout>
<template #body>
<router-view v-slot="{ Component, route }">
<Transition
name="page"
:enter-active-class="`animate__animated ${route.meta.transitionIn} animate__fast`"
:leave-active-class="`animate__animated ${route.meta.transitionOut} animate__fast`"
>
<KeepAlive exclude="list">
<component :is="Component" />
</KeepAlive>
</Transition>
</router-view>
</template>
</HomeLayout>
<PlayMusic />
</div>
</template>
@@ -186,209 +29,4 @@ const handleKeyDown = () => {
position: absolute;
width: 100%;
}
.home-container {
height: calc(100vh - var(--play-bottom-height));
overflow-y: hidden;
position: relative;
}
.icon {
width: 1.5rem;
height: 1.5rem;
}
.sidebar {
width: 15rem;
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -140vh, #ffffff 30vh);
border-right: 0.0625rem solid #e5e7eb;
flex-shrink: 0;
.sidebar-content {
padding: 1rem;
.logo-section {
-webkit-app-region: drag;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
.logo-icon {
width: 2rem;
height: 2rem;
background-color: var(--td-brand-color-4);
border-radius: 0.625rem;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 1.25rem;
color: white;
}
}
.app-title {
font-weight: 500;
font-size: 1.125rem;
color: #111827;
span {
font-weight: 500;
color: #b8f0cc;
}
}
}
.nav-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
.nav-button {
justify-content: flex-start;
height: 2.4rem;
text-align: left;
padding: 0.7rem 1rem;
border-radius: 0.5rem;
border: none;
.nav-icon {
margin-right: 0.75rem;
font-size: 1rem;
}
&.active {
background-color: var(--td-brand-color-4);
color: rgb(255, 255, 255);
&:hover {
background-color: var(--td-brand-color-5);
}
}
&:not(.active) {
color: #6b7280;
&:hover {
color: #111827;
background-color: #f3f4f6;
}
}
}
}
}
}
:deep(.t-layout__content) {
height: 100%;
display: flex;
}
.content {
padding: 0;
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -110vh, #ffffff 15vh);
display: flex;
flex: 1;
flex-direction: column;
.header {
-webkit-app-region: drag;
display: flex;
align-items: center;
padding: 1.5rem;
.nav-btn {
-webkit-app-region: no-drag;
margin-right: 0.5rem;
&:last-of-type {
margin-right: 0.5rem;
}
.iconfont {
font-size: 1rem;
color: #3d4043;
}
&:hover .iconfont {
color: #111827;
}
}
.search-container {
display: flex;
flex: 1;
position: relative;
justify-content: space-between;
.search-input {
-webkit-app-region: no-drag;
display: flex;
align-items: center;
transition: width 0.3s;
padding: 0 0.5rem;
width: min(18.75rem, 400px);
margin-right: 0.5rem;
border-radius: 1.25rem !important;
background-color: #fff;
overflow: hidden;
&:has(input:focus) {
width: max(18.75rem, 400px);
}
}
:deep(.t-input) {
border-radius: 0rem !important;
border: none;
box-shadow: none;
}
.settings-btn {
.iconfont {
font-size: 1rem;
color: #6b7280;
}
&:hover .iconfont {
color: #111827;
}
}
}
}
.mainContent {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
height: 0;
/* 确保flex子元素能够正确计算高度 */
&::-webkit-scrollbar {
width: 0.375rem;
}
&::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 0.1875rem;
}
&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 0.1875rem;
transition: background-color 0.2s ease;
&:hover {
background: #94a3b8;
}
}
/* Firefox 滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #cbd5e1 #f1f5f9;
}
}
</style>

View File

@@ -2,7 +2,7 @@
import { ref, onMounted, watch, WatchHandle, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { extractDominantColor } from '../../utils/colorExtractor'
import { extractDominantColor } from '../../utils/color/colorExtractor'
// 路由实例
const router = useRouter()

View File

@@ -3,7 +3,7 @@ import { ref, onMounted, toRaw } from 'vue'
import { useRoute } from 'vue-router'
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { downloadSingleSong } from '@renderer/utils/download'
import { downloadSingleSong } from '@renderer/utils/audio/download'
import SongVirtualList from '@renderer/components/Music/SongVirtualList.vue'
interface MusicItem {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, computed, watch, toRaw } from 'vue'
import { searchValue } from '@renderer/store/search'
import { downloadSingleSong } from '@renderer/utils/download'
import { downloadSingleSong } from '@renderer/utils/audio/download'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
import { MessagePlugin } from 'tdesign-vue-next'
import SongVirtualList from '@renderer/components/Music/SongVirtualList.vue'

View File

@@ -829,7 +829,8 @@ onMounted(async () => {
align-items: center;
gap: 12px;
font-size: 12px;
user-select: text;
.console-prompt {
color: var(--td-brand-color-5);
font-weight: bold;
@@ -1024,6 +1025,7 @@ onMounted(async () => {
}
.console-container {
height: 50vh;
max-height: 400px;
min-height: 250px;

View File

@@ -14,14 +14,6 @@
<h1 class="brand-title">Cerulean Music</h1>
<p class="brand-subtitle">澜音-纯净音乐极致音乐体验</p>
<!-- 加载状态 -->
<!-- <div class="loading-area">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
<p class="loading-text">{{ loadingText }}</p>
</div> -->
<!-- 特性标签 -->
<div class="feature-tags">
<span v-for="(feature, index) in features" :key="index" class="tag">
@@ -58,12 +50,13 @@ onMounted(async () => {
}
setTimeout(() => {
router.push('/home')
router.replace('/home')
}, 2000)
})
</script>
<style scoped>
.welcome-container {
width: 100vw;
height: 100vh;
@@ -228,28 +221,28 @@ onMounted(async () => {
flex-direction: column;
text-align: center;
}
.logo-section {
flex: none;
padding: 2rem 2rem 1rem 2rem;
}
.content-section {
flex: none;
justify-content: center;
padding: 1rem 2rem 2rem 2rem;
}
.brand-title {
font-size: 2.8rem;
}
.image-bg {
width: 150px;
height: 150px;
filter: blur(40px);
}
.logo-image {
width: 100px;
height: 100px;
@@ -260,21 +253,21 @@ onMounted(async () => {
.brand-title {
font-size: 2.2rem;
}
.brand-subtitle {
font-size: 1rem;
}
.content-section {
padding: 1rem;
}
.image-bg {
width: 120px;
height: 120px;
filter: blur(30px);
}
.logo-image {
width: 80px;
height: 80px;

96
src/types/global.d.ts vendored
View File

@@ -1,96 +0,0 @@
import { CacheInfo, CacheOperationResult } from './musicCache'
// 全局类型定义
declare global {
interface Window {
electronAPI: {
// 音乐缓存相关
musicCache: {
getInfo: () => Promise<CacheInfo>
clear: () => Promise<CacheOperationResult>
getSize: () => Promise<number>
}
}
api: {
// 自动更新相关
autoUpdater: {
checkForUpdates: () => Promise<void>
quitAndInstall: () => Promise<void>
onMessage: (callback: (data: { type: string; data?: any }) => void) => void
removeMessageListener: () => void
}
}
electron: {
ipcRenderer: {
invoke: (channel: string, ...args: any[]) => Promise<any>
}
}
}
}
declare namespace LX {
namespace Music {
// 音质类型
type QualityType = 'flac24bit' | 'flac' | '320k' | '192k' | '128k' | 'ape' | 'wav'
// 音质信息
interface Quality {
type: QualityType
size?: string
}
// 基础音乐信息
interface MusicInfoBase {
id: string
name: string
singer: string
source: string
interval: string | number
}
// 本地音乐元数据
interface MusicInfoLocalMeta {
songId: string
albumName?: string
picUrl?: string
filePath: string
ext: string
}
// 在线音乐元数据
interface MusicInfoOnlineMeta {
songId: string
albumName?: string
picUrl?: string
albumId?: string
qualitys: Quality[]
_qualitys: Record<QualityType, boolean>
// 酷狗特有
hash?: string
// 腾讯音乐特有
strMediaMid?: string
id?: string
albumMid?: string
// 咪咕特有
copyrightId?: string
lrcUrl?: string
mrcUrl?: string
trcUrl?: string
}
// 本地音乐信息
interface MusicInfoLocal extends MusicInfoBase {
source: 'local'
meta: MusicInfoLocalMeta
}
// 在线音乐信息
interface MusicInfoOnline extends MusicInfoBase {
source: 'kg' | 'tx' | 'mg' | 'kw' | 'wy'
meta: MusicInfoOnlineMeta
}
// 统一音乐信息类型
type MusicInfo = MusicInfoLocal | MusicInfoOnline
}
}