mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 03:15:07 +08:00
fix:优化项目结构
This commit is contained in:
268
README.md
268
README.md
@@ -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
|
||||
```
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 提供插件加载与管理功能,支持通过合规插件获取公开音乐信息
|
||||
|
||||
@@ -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
0
docs/guide/analyze.md
Normal file
76922
qodana.sarif.json
Normal file
76922
qodana.sarif.json
Normal file
File diff suppressed because one or more lines are too long
3
qodana.yaml
Normal file
3
qodana.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
version: "1.0"
|
||||
profile:
|
||||
name: qodana.starter
|
||||
55
scripts/genAst.js
Normal file
55
scripts/genAst.js
Normal 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);
|
||||
@@ -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, // 歌曲ID,local为文件路径
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
* 格式化播放数量
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
* 格式化播放数量
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -77,3 +77,6 @@ body {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 #f1f5f9;
|
||||
}
|
||||
.t-dialog__mask{
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 导入问题
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
@@ -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元素在各种主题下的表现。
|
||||
@@ -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'
|
||||
|
||||
|
||||
380
src/renderer/src/layout/index.vue
Normal file
380
src/renderer/src/layout/index.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
96
src/types/global.d.ts
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user