Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b07cc2359a | ||
|
|
46756a8b09 | ||
|
|
deb73fa789 | ||
|
|
910ab1ff10 | ||
|
|
0cfc31de31 | ||
|
|
2c0c8be2bf | ||
|
|
489e920b69 | ||
|
|
fdd548972c | ||
|
|
f81b46b1b4 | ||
|
|
e1e2d88c67 | ||
|
|
3c0be7a20f | ||
|
|
79e05c884d | ||
|
|
970baf081b | ||
|
|
9341c57278 | ||
|
|
15a76a5313 | ||
|
|
6ee7e6dc99 | ||
|
|
630ec056db | ||
|
|
d739e60930 | ||
|
|
86ea8b6797 | ||
|
|
3d87fa145f | ||
|
|
6fa4e21d40 | ||
|
|
4c11c19139 | ||
|
|
f82d4271a7 | ||
|
|
42dcf52d59 | ||
|
|
d9d2bdab93 | ||
|
|
6060aa2ef4 | ||
|
|
0b2e8eef64 | ||
|
|
57de7b49e8 | ||
|
|
42e17e83e7 | ||
|
|
380c273329 | ||
|
|
6f56f5e240 | ||
|
|
7af7779e5c | ||
|
|
669a348218 | ||
|
|
f02264c80c | ||
|
|
d0d5f918bd | ||
|
|
761d265d18 | ||
|
|
204df64535 | ||
|
|
cc814eddbd | ||
|
|
51df14a9e9 | ||
|
|
2473b36928 | ||
|
|
dbba7a3d26 | ||
|
|
a817865bd8 | ||
|
|
c4a4d26bd8 | ||
|
|
dfa36d872e | ||
|
|
995859e661 |
66
.github/workflows/deploydocs.yml
vendored
@@ -1,66 +0,0 @@
|
|||||||
# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
|
|
||||||
#
|
|
||||||
name: Deploy VitePress site to Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
# 在针对 `main` 分支的推送上运行。如果你
|
|
||||||
# 使用 `master` 分支作为默认分支,请将其更改为 `master`
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
# 允许你从 Actions 选项卡手动运行此工作流程
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
|
|
||||||
# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
|
|
||||||
concurrency:
|
|
||||||
group: pages
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# 构建工作
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
|
|
||||||
- uses: pnpm/action-setup@v3 # 如果使用 pnpm,请取消此区域注释
|
|
||||||
with:
|
|
||||||
version: 9
|
|
||||||
# - uses: oven-sh/setup-bun@v1 # 如果使用 Bun,请取消注释
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
cache: pnpm # 或 pnpm / yarn
|
|
||||||
- name: Setup Pages
|
|
||||||
uses: actions/configure-pages@v4
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm add -D vitepress@next # 或 pnpm install / yarn install / bun install
|
|
||||||
- name: Build with VitePress
|
|
||||||
run: pnpm run docs:build # 或 pnpm docs:build / yarn docs:build / bun run docs:build
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: docs/.vitepress/dist
|
|
||||||
|
|
||||||
# 部署工作
|
|
||||||
deploy:
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Deploy
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
146
.github/workflows/main.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Build Electron App for macos
|
- name: Build Electron App for macos
|
||||||
if: matrix.os == 'macos-latest' # 只在macOS上运行
|
if: matrix.os == 'macos-latest' # 只在macOS上运行
|
||||||
run: |
|
run: |
|
||||||
yarn run build:mac
|
yarn run build:mac:universal
|
||||||
|
|
||||||
- name: Build Electron App for linux
|
- name: Build Electron App for linux
|
||||||
if: matrix.os == 'ubuntu-latest' # 只在Linux上运行
|
if: matrix.os == 'ubuntu-latest' # 只在Linux上运行
|
||||||
@@ -70,146 +70,4 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
files: 'dist/**' # 将dist目录下所有文件添加到release
|
files: 'dist/**' # 将dist目录下所有文件添加到release
|
||||||
|
body: ${{ github.event.head_commit.message }} # 自动将commit message写入release描述
|
||||||
# 新增:自动同步到 WebDAV
|
|
||||||
sync-to-webdav:
|
|
||||||
name: Sync to WebDAV
|
|
||||||
needs: release # 等待 release 任务完成
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v') # 只在标签推送时执行
|
|
||||||
steps:
|
|
||||||
- name: Wait for release to be ready
|
|
||||||
run: |
|
|
||||||
echo "等待 Release 准备就绪..."
|
|
||||||
sleep 30 # 等待30秒确保 release 完全创建
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y curl jq
|
|
||||||
|
|
||||||
- name: Get latest release info
|
|
||||||
id: get-release
|
|
||||||
run: |
|
|
||||||
# 获取当前标签对应的 release 信息
|
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
|
||||||
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# 获取 release 详细信息
|
|
||||||
response=$(curl -s \
|
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
"https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG_NAME")
|
|
||||||
|
|
||||||
release_id=$(echo "$response" | jq -r '.id')
|
|
||||||
echo "release_id=$release_id" >> $GITHUB_OUTPUT
|
|
||||||
echo "找到 Release ID: $release_id"
|
|
||||||
|
|
||||||
- name: Sync release to WebDAV
|
|
||||||
run: |
|
|
||||||
TAG_NAME="${{ steps.get-release.outputs.tag_name }}"
|
|
||||||
RELEASE_ID="${{ steps.get-release.outputs.release_id }}"
|
|
||||||
|
|
||||||
echo "🚀 开始同步版本 $TAG_NAME 到 WebDAV..."
|
|
||||||
echo "Release ID: $RELEASE_ID"
|
|
||||||
echo "WebDAV 根路径: ${{ secrets.WEBDAV_BASE_URL }}/yd/ceru"
|
|
||||||
|
|
||||||
# 获取该release的所有资源文件
|
|
||||||
assets_json=$(curl -s \
|
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets")
|
|
||||||
|
|
||||||
assets_count=$(echo "$assets_json" | jq '. | length')
|
|
||||||
echo "找到 $assets_count 个资源文件"
|
|
||||||
|
|
||||||
if [ "$assets_count" -eq 0 ]; then
|
|
||||||
echo "⚠️ 该版本没有资源文件,跳过同步"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 先创建版本目录
|
|
||||||
dir_path="/yd/ceru/$TAG_NAME"
|
|
||||||
dir_url="${{ secrets.WEBDAV_BASE_URL }}$dir_path"
|
|
||||||
|
|
||||||
echo "创建版本目录: $dir_path"
|
|
||||||
curl -s -X MKCOL \
|
|
||||||
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
|
||||||
"$dir_url" || echo "目录可能已存在"
|
|
||||||
|
|
||||||
# 处理每个asset
|
|
||||||
success_count=0
|
|
||||||
failed_count=0
|
|
||||||
|
|
||||||
for i in $(seq 0 $(($assets_count - 1))); do
|
|
||||||
asset=$(echo "$assets_json" | jq -c ".[$i]")
|
|
||||||
asset_name=$(echo "$asset" | jq -r '.name')
|
|
||||||
asset_url=$(echo "$asset" | jq -r '.url')
|
|
||||||
asset_size=$(echo "$asset" | jq -r '.size')
|
|
||||||
|
|
||||||
echo "📦 处理资源: $asset_name (大小: $asset_size bytes)"
|
|
||||||
|
|
||||||
# 下载资源文件
|
|
||||||
safe_filename="./temp_${TAG_NAME}_$(date +%s)_$i"
|
|
||||||
|
|
||||||
if ! curl -sL -o "$safe_filename" \
|
|
||||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
||||||
-H "Accept: application/octet-stream" \
|
|
||||||
"$asset_url"; then
|
|
||||||
echo "❌ 下载失败: $asset_name"
|
|
||||||
failed_count=$((failed_count + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$safe_filename" ]; then
|
|
||||||
actual_size=$(wc -c < "$safe_filename")
|
|
||||||
if [ "$actual_size" -ne "$asset_size" ]; then
|
|
||||||
echo "❌ 文件大小不匹配: $asset_name (期望: $asset_size, 实际: $actual_size)"
|
|
||||||
rm -f "$safe_filename"
|
|
||||||
failed_count=$((failed_count + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "⬆️ 上传到 WebDAV: $asset_name"
|
|
||||||
|
|
||||||
# 构建远程路径
|
|
||||||
remote_path="/yd/ceru/$TAG_NAME/$asset_name"
|
|
||||||
full_url="${{ secrets.WEBDAV_BASE_URL }}$remote_path"
|
|
||||||
|
|
||||||
# 使用 WebDAV PUT 方法上传文件
|
|
||||||
if curl -s -f -X PUT \
|
|
||||||
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
|
||||||
-T "$safe_filename" \
|
|
||||||
"$full_url"; then
|
|
||||||
|
|
||||||
echo "✅ 上传成功: $asset_name"
|
|
||||||
success_count=$((success_count + 1))
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "❌ 上传失败: $asset_name"
|
|
||||||
failed_count=$((failed_count + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 清理临时文件
|
|
||||||
rm -f "$safe_filename"
|
|
||||||
echo "----------------------------------------"
|
|
||||||
else
|
|
||||||
echo "❌ 临时文件不存在: $safe_filename"
|
|
||||||
failed_count=$((failed_count + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "========================================"
|
|
||||||
echo "🎉 同步完成!"
|
|
||||||
echo "成功: $success_count 个文件"
|
|
||||||
echo "失败: $failed_count 个文件"
|
|
||||||
echo "总计: $assets_count 个文件"
|
|
||||||
|
|
||||||
- name: Notify completion
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ "${{ job.status }}" == "success" ]; then
|
|
||||||
echo "✅ 版本 ${{ steps.get-release.outputs.tag_name }} 已成功同步到 alist"
|
|
||||||
else
|
|
||||||
echo "❌ 版本 ${{ steps.get-release.outputs.tag_name }} 同步失败"
|
|
||||||
fi
|
|
||||||
|
|||||||
275
.vitepress/cache/deps/@theme_index.js
vendored
@@ -1,275 +0,0 @@
|
|||||||
import {
|
|
||||||
useMediaQuery
|
|
||||||
} from "./chunk-B6YPYVPP.js";
|
|
||||||
import {
|
|
||||||
computed,
|
|
||||||
ref,
|
|
||||||
shallowRef,
|
|
||||||
watch
|
|
||||||
} from "./chunk-I4O5PVBA.js";
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/index.js
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/fonts.css";
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/vars.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/base.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/icons.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/utils.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css";
|
|
||||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css";
|
|
||||||
import VPBadge from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
|
||||||
import Layout from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/Layout.vue";
|
|
||||||
import { default as default2 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
|
||||||
import { default as default3 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue";
|
|
||||||
import { default as default4 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue";
|
|
||||||
import { default as default5 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue";
|
|
||||||
import { default as default6 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue";
|
|
||||||
import { default as default7 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue";
|
|
||||||
import { default as default8 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue";
|
|
||||||
import { default as default9 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue";
|
|
||||||
import { default as default10 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue";
|
|
||||||
import { default as default11 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue";
|
|
||||||
import { default as default12 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue";
|
|
||||||
import { default as default13 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue";
|
|
||||||
import { default as default14 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue";
|
|
||||||
import { default as default15 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue";
|
|
||||||
import { default as default16 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue";
|
|
||||||
import { default as default17 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue";
|
|
||||||
import { default as default18 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue";
|
|
||||||
import { default as default19 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue";
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
|
||||||
import { onContentUpdated } from "vitepress";
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
|
|
||||||
import { getScrollOffset } from "vitepress";
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/support/utils.js
|
|
||||||
import { withBase } from "vitepress";
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/composables/data.js
|
|
||||||
import { useData as useData$ } from "vitepress";
|
|
||||||
var useData = useData$;
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/support/utils.js
|
|
||||||
function ensureStartingSlash(path) {
|
|
||||||
return path.startsWith("/") ? path : `/${path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/support/sidebar.js
|
|
||||||
function getSidebar(_sidebar, path) {
|
|
||||||
if (Array.isArray(_sidebar))
|
|
||||||
return addBase(_sidebar);
|
|
||||||
if (_sidebar == null)
|
|
||||||
return [];
|
|
||||||
path = ensureStartingSlash(path);
|
|
||||||
const dir = Object.keys(_sidebar).sort((a, b) => {
|
|
||||||
return b.split("/").length - a.split("/").length;
|
|
||||||
}).find((dir2) => {
|
|
||||||
return path.startsWith(ensureStartingSlash(dir2));
|
|
||||||
});
|
|
||||||
const sidebar = dir ? _sidebar[dir] : [];
|
|
||||||
return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base);
|
|
||||||
}
|
|
||||||
function getSidebarGroups(sidebar) {
|
|
||||||
const groups = [];
|
|
||||||
let lastGroupIndex = 0;
|
|
||||||
for (const index in sidebar) {
|
|
||||||
const item = sidebar[index];
|
|
||||||
if (item.items) {
|
|
||||||
lastGroupIndex = groups.push(item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!groups[lastGroupIndex]) {
|
|
||||||
groups.push({ items: [] });
|
|
||||||
}
|
|
||||||
groups[lastGroupIndex].items.push(item);
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
function addBase(items, _base) {
|
|
||||||
return [...items].map((_item) => {
|
|
||||||
const item = { ..._item };
|
|
||||||
const base = item.base || _base;
|
|
||||||
if (base && item.link)
|
|
||||||
item.link = base + item.link;
|
|
||||||
if (item.items)
|
|
||||||
item.items = addBase(item.items, base);
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
|
|
||||||
function useSidebar() {
|
|
||||||
const { frontmatter, page, theme: theme2 } = useData();
|
|
||||||
const is960 = useMediaQuery("(min-width: 960px)");
|
|
||||||
const isOpen = ref(false);
|
|
||||||
const _sidebar = computed(() => {
|
|
||||||
const sidebarConfig = theme2.value.sidebar;
|
|
||||||
const relativePath = page.value.relativePath;
|
|
||||||
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
|
|
||||||
});
|
|
||||||
const sidebar = ref(_sidebar.value);
|
|
||||||
watch(_sidebar, (next, prev) => {
|
|
||||||
if (JSON.stringify(next) !== JSON.stringify(prev))
|
|
||||||
sidebar.value = _sidebar.value;
|
|
||||||
});
|
|
||||||
const hasSidebar = computed(() => {
|
|
||||||
return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home";
|
|
||||||
});
|
|
||||||
const leftAside = computed(() => {
|
|
||||||
if (hasAside)
|
|
||||||
return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left";
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const hasAside = computed(() => {
|
|
||||||
if (frontmatter.value.layout === "home")
|
|
||||||
return false;
|
|
||||||
if (frontmatter.value.aside != null)
|
|
||||||
return !!frontmatter.value.aside;
|
|
||||||
return theme2.value.aside !== false;
|
|
||||||
});
|
|
||||||
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
|
|
||||||
const sidebarGroups = computed(() => {
|
|
||||||
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
|
|
||||||
});
|
|
||||||
function open() {
|
|
||||||
isOpen.value = true;
|
|
||||||
}
|
|
||||||
function close() {
|
|
||||||
isOpen.value = false;
|
|
||||||
}
|
|
||||||
function toggle() {
|
|
||||||
isOpen.value ? close() : open();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isOpen,
|
|
||||||
sidebar,
|
|
||||||
sidebarGroups,
|
|
||||||
hasSidebar,
|
|
||||||
hasAside,
|
|
||||||
leftAside,
|
|
||||||
isSidebarEnabled,
|
|
||||||
open,
|
|
||||||
close,
|
|
||||||
toggle
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
|
|
||||||
var ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/;
|
|
||||||
var resolvedHeaders = [];
|
|
||||||
function getHeaders(range) {
|
|
||||||
const headers = [
|
|
||||||
...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
|
|
||||||
].filter((el) => el.id && el.hasChildNodes()).map((el) => {
|
|
||||||
const level = Number(el.tagName[1]);
|
|
||||||
return {
|
|
||||||
element: el,
|
|
||||||
title: serializeHeader(el),
|
|
||||||
link: "#" + el.id,
|
|
||||||
level
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return resolveHeaders(headers, range);
|
|
||||||
}
|
|
||||||
function serializeHeader(h) {
|
|
||||||
let ret = "";
|
|
||||||
for (const node of h.childNodes) {
|
|
||||||
if (node.nodeType === 1) {
|
|
||||||
if (ignoreRE.test(node.className))
|
|
||||||
continue;
|
|
||||||
ret += node.textContent;
|
|
||||||
} else if (node.nodeType === 3) {
|
|
||||||
ret += node.textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret.trim();
|
|
||||||
}
|
|
||||||
function resolveHeaders(headers, range) {
|
|
||||||
if (range === false) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
|
|
||||||
const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
|
|
||||||
return buildTree(headers, high, low);
|
|
||||||
}
|
|
||||||
function buildTree(data, min, max) {
|
|
||||||
resolvedHeaders.length = 0;
|
|
||||||
const result = [];
|
|
||||||
const stack = [];
|
|
||||||
data.forEach((item) => {
|
|
||||||
const node = { ...item, children: [] };
|
|
||||||
let parent = stack[stack.length - 1];
|
|
||||||
while (parent && parent.level >= node.level) {
|
|
||||||
stack.pop();
|
|
||||||
parent = stack[stack.length - 1];
|
|
||||||
}
|
|
||||||
if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) {
|
|
||||||
stack.push({ level: node.level, shouldIgnore: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.level > max || node.level < min)
|
|
||||||
return;
|
|
||||||
resolvedHeaders.push({ element: node.element, link: node.link });
|
|
||||||
if (parent)
|
|
||||||
parent.children.push(node);
|
|
||||||
else
|
|
||||||
result.push(node);
|
|
||||||
stack.push(node);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
|
||||||
function useLocalNav() {
|
|
||||||
const { theme: theme2, frontmatter } = useData();
|
|
||||||
const headers = shallowRef([]);
|
|
||||||
const hasLocalNav = computed(() => {
|
|
||||||
return headers.value.length > 0;
|
|
||||||
});
|
|
||||||
onContentUpdated(() => {
|
|
||||||
headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline);
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
headers,
|
|
||||||
hasLocalNav
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
|
||||||
var theme = {
|
|
||||||
Layout,
|
|
||||||
enhanceApp: ({ app }) => {
|
|
||||||
app.component("Badge", VPBadge);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var without_fonts_default = theme;
|
|
||||||
export {
|
|
||||||
default2 as VPBadge,
|
|
||||||
default3 as VPButton,
|
|
||||||
default4 as VPDocAsideSponsors,
|
|
||||||
default5 as VPFeatures,
|
|
||||||
default6 as VPHomeContent,
|
|
||||||
default7 as VPHomeFeatures,
|
|
||||||
default8 as VPHomeHero,
|
|
||||||
default9 as VPHomeSponsors,
|
|
||||||
default10 as VPImage,
|
|
||||||
default11 as VPLink,
|
|
||||||
default12 as VPNavBarSearch,
|
|
||||||
default13 as VPSocialLink,
|
|
||||||
default14 as VPSocialLinks,
|
|
||||||
default15 as VPSponsors,
|
|
||||||
default16 as VPTeamMembers,
|
|
||||||
default17 as VPTeamPage,
|
|
||||||
default18 as VPTeamPageSection,
|
|
||||||
default19 as VPTeamPageTitle,
|
|
||||||
without_fonts_default as default,
|
|
||||||
useLocalNav,
|
|
||||||
useSidebar
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=@theme_index.js.map
|
|
||||||
7
.vitepress/cache/deps/@theme_index.js.map
vendored
40
.vitepress/cache/deps/_metadata.json
vendored
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"hash": "99cf66da",
|
|
||||||
"configHash": "acc3a95b",
|
|
||||||
"lockfileHash": "6f0f9736",
|
|
||||||
"browserHash": "6e863def",
|
|
||||||
"optimized": {
|
|
||||||
"vue": {
|
|
||||||
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
|
||||||
"file": "vue.js",
|
|
||||||
"fileHash": "4f939392",
|
|
||||||
"needsInterop": false
|
|
||||||
},
|
|
||||||
"vitepress > @vue/devtools-api": {
|
|
||||||
"src": "../../../node_modules/@vue/devtools-api/dist/index.js",
|
|
||||||
"file": "vitepress___@vue_devtools-api.js",
|
|
||||||
"fileHash": "fcdf6679",
|
|
||||||
"needsInterop": false
|
|
||||||
},
|
|
||||||
"vitepress > @vueuse/core": {
|
|
||||||
"src": "../../../node_modules/@vueuse/core/index.mjs",
|
|
||||||
"file": "vitepress___@vueuse_core.js",
|
|
||||||
"fileHash": "f6cccf57",
|
|
||||||
"needsInterop": false
|
|
||||||
},
|
|
||||||
"@theme/index": {
|
|
||||||
"src": "../../../node_modules/vitepress/dist/client/theme-default/index.js",
|
|
||||||
"file": "@theme_index.js",
|
|
||||||
"fileHash": "1995bc33",
|
|
||||||
"needsInterop": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chunks": {
|
|
||||||
"chunk-B6YPYVPP": {
|
|
||||||
"file": "chunk-B6YPYVPP.js"
|
|
||||||
},
|
|
||||||
"chunk-I4O5PVBA": {
|
|
||||||
"file": "chunk-I4O5PVBA.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9719
.vitepress/cache/deps/chunk-B6YPYVPP.js
vendored
7
.vitepress/cache/deps/chunk-B6YPYVPP.js.map
vendored
12683
.vitepress/cache/deps/chunk-I4O5PVBA.js
vendored
7
.vitepress/cache/deps/chunk-I4O5PVBA.js.map
vendored
3
.vitepress/cache/deps/package.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
||||||
4505
.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
583
.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
@@ -1,583 +0,0 @@
|
|||||||
import {
|
|
||||||
DefaultMagicKeysAliasMap,
|
|
||||||
StorageSerializers,
|
|
||||||
TransitionPresets,
|
|
||||||
assert,
|
|
||||||
breakpointsAntDesign,
|
|
||||||
breakpointsBootstrapV5,
|
|
||||||
breakpointsElement,
|
|
||||||
breakpointsMasterCss,
|
|
||||||
breakpointsPrimeFlex,
|
|
||||||
breakpointsQuasar,
|
|
||||||
breakpointsSematic,
|
|
||||||
breakpointsTailwind,
|
|
||||||
breakpointsVuetify,
|
|
||||||
breakpointsVuetifyV2,
|
|
||||||
breakpointsVuetifyV3,
|
|
||||||
bypassFilter,
|
|
||||||
camelize,
|
|
||||||
clamp,
|
|
||||||
cloneFnJSON,
|
|
||||||
computedAsync,
|
|
||||||
computedEager,
|
|
||||||
computedInject,
|
|
||||||
computedWithControl,
|
|
||||||
containsProp,
|
|
||||||
controlledRef,
|
|
||||||
createEventHook,
|
|
||||||
createFetch,
|
|
||||||
createFilterWrapper,
|
|
||||||
createGlobalState,
|
|
||||||
createInjectionState,
|
|
||||||
createRef,
|
|
||||||
createReusableTemplate,
|
|
||||||
createSharedComposable,
|
|
||||||
createSingletonPromise,
|
|
||||||
createTemplatePromise,
|
|
||||||
createUnrefFn,
|
|
||||||
customStorageEventName,
|
|
||||||
debounceFilter,
|
|
||||||
defaultDocument,
|
|
||||||
defaultLocation,
|
|
||||||
defaultNavigator,
|
|
||||||
defaultWindow,
|
|
||||||
executeTransition,
|
|
||||||
extendRef,
|
|
||||||
formatDate,
|
|
||||||
formatTimeAgo,
|
|
||||||
get,
|
|
||||||
getLifeCycleTarget,
|
|
||||||
getSSRHandler,
|
|
||||||
hasOwn,
|
|
||||||
hyphenate,
|
|
||||||
identity,
|
|
||||||
increaseWithUnit,
|
|
||||||
injectLocal,
|
|
||||||
invoke,
|
|
||||||
isClient,
|
|
||||||
isDef,
|
|
||||||
isDefined,
|
|
||||||
isIOS,
|
|
||||||
isObject,
|
|
||||||
isWorker,
|
|
||||||
makeDestructurable,
|
|
||||||
mapGamepadToXbox360Controller,
|
|
||||||
noop,
|
|
||||||
normalizeDate,
|
|
||||||
notNullish,
|
|
||||||
now,
|
|
||||||
objectEntries,
|
|
||||||
objectOmit,
|
|
||||||
objectPick,
|
|
||||||
onClickOutside,
|
|
||||||
onElementRemoval,
|
|
||||||
onKeyDown,
|
|
||||||
onKeyPressed,
|
|
||||||
onKeyStroke,
|
|
||||||
onKeyUp,
|
|
||||||
onLongPress,
|
|
||||||
onStartTyping,
|
|
||||||
pausableFilter,
|
|
||||||
promiseTimeout,
|
|
||||||
provideLocal,
|
|
||||||
provideSSRWidth,
|
|
||||||
pxValue,
|
|
||||||
rand,
|
|
||||||
reactify,
|
|
||||||
reactifyObject,
|
|
||||||
reactiveComputed,
|
|
||||||
reactiveOmit,
|
|
||||||
reactivePick,
|
|
||||||
refAutoReset,
|
|
||||||
refDebounced,
|
|
||||||
refDefault,
|
|
||||||
refThrottled,
|
|
||||||
refWithControl,
|
|
||||||
resolveRef,
|
|
||||||
resolveUnref,
|
|
||||||
set,
|
|
||||||
setSSRHandler,
|
|
||||||
syncRef,
|
|
||||||
syncRefs,
|
|
||||||
templateRef,
|
|
||||||
throttleFilter,
|
|
||||||
timestamp,
|
|
||||||
toArray,
|
|
||||||
toReactive,
|
|
||||||
toRef,
|
|
||||||
toRefs,
|
|
||||||
toValue,
|
|
||||||
tryOnBeforeMount,
|
|
||||||
tryOnBeforeUnmount,
|
|
||||||
tryOnMounted,
|
|
||||||
tryOnScopeDispose,
|
|
||||||
tryOnUnmounted,
|
|
||||||
unrefElement,
|
|
||||||
until,
|
|
||||||
useActiveElement,
|
|
||||||
useAnimate,
|
|
||||||
useArrayDifference,
|
|
||||||
useArrayEvery,
|
|
||||||
useArrayFilter,
|
|
||||||
useArrayFind,
|
|
||||||
useArrayFindIndex,
|
|
||||||
useArrayFindLast,
|
|
||||||
useArrayIncludes,
|
|
||||||
useArrayJoin,
|
|
||||||
useArrayMap,
|
|
||||||
useArrayReduce,
|
|
||||||
useArraySome,
|
|
||||||
useArrayUnique,
|
|
||||||
useAsyncQueue,
|
|
||||||
useAsyncState,
|
|
||||||
useBase64,
|
|
||||||
useBattery,
|
|
||||||
useBluetooth,
|
|
||||||
useBreakpoints,
|
|
||||||
useBroadcastChannel,
|
|
||||||
useBrowserLocation,
|
|
||||||
useCached,
|
|
||||||
useClipboard,
|
|
||||||
useClipboardItems,
|
|
||||||
useCloned,
|
|
||||||
useColorMode,
|
|
||||||
useConfirmDialog,
|
|
||||||
useCountdown,
|
|
||||||
useCounter,
|
|
||||||
useCssVar,
|
|
||||||
useCurrentElement,
|
|
||||||
useCycleList,
|
|
||||||
useDark,
|
|
||||||
useDateFormat,
|
|
||||||
useDebounceFn,
|
|
||||||
useDebouncedRefHistory,
|
|
||||||
useDeviceMotion,
|
|
||||||
useDeviceOrientation,
|
|
||||||
useDevicePixelRatio,
|
|
||||||
useDevicesList,
|
|
||||||
useDisplayMedia,
|
|
||||||
useDocumentVisibility,
|
|
||||||
useDraggable,
|
|
||||||
useDropZone,
|
|
||||||
useElementBounding,
|
|
||||||
useElementByPoint,
|
|
||||||
useElementHover,
|
|
||||||
useElementSize,
|
|
||||||
useElementVisibility,
|
|
||||||
useEventBus,
|
|
||||||
useEventListener,
|
|
||||||
useEventSource,
|
|
||||||
useEyeDropper,
|
|
||||||
useFavicon,
|
|
||||||
useFetch,
|
|
||||||
useFileDialog,
|
|
||||||
useFileSystemAccess,
|
|
||||||
useFocus,
|
|
||||||
useFocusWithin,
|
|
||||||
useFps,
|
|
||||||
useFullscreen,
|
|
||||||
useGamepad,
|
|
||||||
useGeolocation,
|
|
||||||
useIdle,
|
|
||||||
useImage,
|
|
||||||
useInfiniteScroll,
|
|
||||||
useIntersectionObserver,
|
|
||||||
useInterval,
|
|
||||||
useIntervalFn,
|
|
||||||
useKeyModifier,
|
|
||||||
useLastChanged,
|
|
||||||
useLocalStorage,
|
|
||||||
useMagicKeys,
|
|
||||||
useManualRefHistory,
|
|
||||||
useMediaControls,
|
|
||||||
useMediaQuery,
|
|
||||||
useMemoize,
|
|
||||||
useMemory,
|
|
||||||
useMounted,
|
|
||||||
useMouse,
|
|
||||||
useMouseInElement,
|
|
||||||
useMousePressed,
|
|
||||||
useMutationObserver,
|
|
||||||
useNavigatorLanguage,
|
|
||||||
useNetwork,
|
|
||||||
useNow,
|
|
||||||
useObjectUrl,
|
|
||||||
useOffsetPagination,
|
|
||||||
useOnline,
|
|
||||||
usePageLeave,
|
|
||||||
useParallax,
|
|
||||||
useParentElement,
|
|
||||||
usePerformanceObserver,
|
|
||||||
usePermission,
|
|
||||||
usePointer,
|
|
||||||
usePointerLock,
|
|
||||||
usePointerSwipe,
|
|
||||||
usePreferredColorScheme,
|
|
||||||
usePreferredContrast,
|
|
||||||
usePreferredDark,
|
|
||||||
usePreferredLanguages,
|
|
||||||
usePreferredReducedMotion,
|
|
||||||
usePreferredReducedTransparency,
|
|
||||||
usePrevious,
|
|
||||||
useRafFn,
|
|
||||||
useRefHistory,
|
|
||||||
useResizeObserver,
|
|
||||||
useSSRWidth,
|
|
||||||
useScreenOrientation,
|
|
||||||
useScreenSafeArea,
|
|
||||||
useScriptTag,
|
|
||||||
useScroll,
|
|
||||||
useScrollLock,
|
|
||||||
useSessionStorage,
|
|
||||||
useShare,
|
|
||||||
useSorted,
|
|
||||||
useSpeechRecognition,
|
|
||||||
useSpeechSynthesis,
|
|
||||||
useStepper,
|
|
||||||
useStorage,
|
|
||||||
useStorageAsync,
|
|
||||||
useStyleTag,
|
|
||||||
useSupported,
|
|
||||||
useSwipe,
|
|
||||||
useTemplateRefsList,
|
|
||||||
useTextDirection,
|
|
||||||
useTextSelection,
|
|
||||||
useTextareaAutosize,
|
|
||||||
useThrottleFn,
|
|
||||||
useThrottledRefHistory,
|
|
||||||
useTimeAgo,
|
|
||||||
useTimeout,
|
|
||||||
useTimeoutFn,
|
|
||||||
useTimeoutPoll,
|
|
||||||
useTimestamp,
|
|
||||||
useTitle,
|
|
||||||
useToNumber,
|
|
||||||
useToString,
|
|
||||||
useToggle,
|
|
||||||
useTransition,
|
|
||||||
useUrlSearchParams,
|
|
||||||
useUserMedia,
|
|
||||||
useVModel,
|
|
||||||
useVModels,
|
|
||||||
useVibrate,
|
|
||||||
useVirtualList,
|
|
||||||
useWakeLock,
|
|
||||||
useWebNotification,
|
|
||||||
useWebSocket,
|
|
||||||
useWebWorker,
|
|
||||||
useWebWorkerFn,
|
|
||||||
useWindowFocus,
|
|
||||||
useWindowScroll,
|
|
||||||
useWindowSize,
|
|
||||||
watchArray,
|
|
||||||
watchAtMost,
|
|
||||||
watchDebounced,
|
|
||||||
watchDeep,
|
|
||||||
watchIgnorable,
|
|
||||||
watchImmediate,
|
|
||||||
watchOnce,
|
|
||||||
watchPausable,
|
|
||||||
watchThrottled,
|
|
||||||
watchTriggerable,
|
|
||||||
watchWithFilter,
|
|
||||||
whenever
|
|
||||||
} from "./chunk-B6YPYVPP.js";
|
|
||||||
import "./chunk-I4O5PVBA.js";
|
|
||||||
export {
|
|
||||||
DefaultMagicKeysAliasMap,
|
|
||||||
StorageSerializers,
|
|
||||||
TransitionPresets,
|
|
||||||
assert,
|
|
||||||
computedAsync as asyncComputed,
|
|
||||||
refAutoReset as autoResetRef,
|
|
||||||
breakpointsAntDesign,
|
|
||||||
breakpointsBootstrapV5,
|
|
||||||
breakpointsElement,
|
|
||||||
breakpointsMasterCss,
|
|
||||||
breakpointsPrimeFlex,
|
|
||||||
breakpointsQuasar,
|
|
||||||
breakpointsSematic,
|
|
||||||
breakpointsTailwind,
|
|
||||||
breakpointsVuetify,
|
|
||||||
breakpointsVuetifyV2,
|
|
||||||
breakpointsVuetifyV3,
|
|
||||||
bypassFilter,
|
|
||||||
camelize,
|
|
||||||
clamp,
|
|
||||||
cloneFnJSON,
|
|
||||||
computedAsync,
|
|
||||||
computedEager,
|
|
||||||
computedInject,
|
|
||||||
computedWithControl,
|
|
||||||
containsProp,
|
|
||||||
computedWithControl as controlledComputed,
|
|
||||||
controlledRef,
|
|
||||||
createEventHook,
|
|
||||||
createFetch,
|
|
||||||
createFilterWrapper,
|
|
||||||
createGlobalState,
|
|
||||||
createInjectionState,
|
|
||||||
reactify as createReactiveFn,
|
|
||||||
createRef,
|
|
||||||
createReusableTemplate,
|
|
||||||
createSharedComposable,
|
|
||||||
createSingletonPromise,
|
|
||||||
createTemplatePromise,
|
|
||||||
createUnrefFn,
|
|
||||||
customStorageEventName,
|
|
||||||
debounceFilter,
|
|
||||||
refDebounced as debouncedRef,
|
|
||||||
watchDebounced as debouncedWatch,
|
|
||||||
defaultDocument,
|
|
||||||
defaultLocation,
|
|
||||||
defaultNavigator,
|
|
||||||
defaultWindow,
|
|
||||||
computedEager as eagerComputed,
|
|
||||||
executeTransition,
|
|
||||||
extendRef,
|
|
||||||
formatDate,
|
|
||||||
formatTimeAgo,
|
|
||||||
get,
|
|
||||||
getLifeCycleTarget,
|
|
||||||
getSSRHandler,
|
|
||||||
hasOwn,
|
|
||||||
hyphenate,
|
|
||||||
identity,
|
|
||||||
watchIgnorable as ignorableWatch,
|
|
||||||
increaseWithUnit,
|
|
||||||
injectLocal,
|
|
||||||
invoke,
|
|
||||||
isClient,
|
|
||||||
isDef,
|
|
||||||
isDefined,
|
|
||||||
isIOS,
|
|
||||||
isObject,
|
|
||||||
isWorker,
|
|
||||||
makeDestructurable,
|
|
||||||
mapGamepadToXbox360Controller,
|
|
||||||
noop,
|
|
||||||
normalizeDate,
|
|
||||||
notNullish,
|
|
||||||
now,
|
|
||||||
objectEntries,
|
|
||||||
objectOmit,
|
|
||||||
objectPick,
|
|
||||||
onClickOutside,
|
|
||||||
onElementRemoval,
|
|
||||||
onKeyDown,
|
|
||||||
onKeyPressed,
|
|
||||||
onKeyStroke,
|
|
||||||
onKeyUp,
|
|
||||||
onLongPress,
|
|
||||||
onStartTyping,
|
|
||||||
pausableFilter,
|
|
||||||
watchPausable as pausableWatch,
|
|
||||||
promiseTimeout,
|
|
||||||
provideLocal,
|
|
||||||
provideSSRWidth,
|
|
||||||
pxValue,
|
|
||||||
rand,
|
|
||||||
reactify,
|
|
||||||
reactifyObject,
|
|
||||||
reactiveComputed,
|
|
||||||
reactiveOmit,
|
|
||||||
reactivePick,
|
|
||||||
refAutoReset,
|
|
||||||
refDebounced,
|
|
||||||
refDefault,
|
|
||||||
refThrottled,
|
|
||||||
refWithControl,
|
|
||||||
resolveRef,
|
|
||||||
resolveUnref,
|
|
||||||
set,
|
|
||||||
setSSRHandler,
|
|
||||||
syncRef,
|
|
||||||
syncRefs,
|
|
||||||
templateRef,
|
|
||||||
throttleFilter,
|
|
||||||
refThrottled as throttledRef,
|
|
||||||
watchThrottled as throttledWatch,
|
|
||||||
timestamp,
|
|
||||||
toArray,
|
|
||||||
toReactive,
|
|
||||||
toRef,
|
|
||||||
toRefs,
|
|
||||||
toValue,
|
|
||||||
tryOnBeforeMount,
|
|
||||||
tryOnBeforeUnmount,
|
|
||||||
tryOnMounted,
|
|
||||||
tryOnScopeDispose,
|
|
||||||
tryOnUnmounted,
|
|
||||||
unrefElement,
|
|
||||||
until,
|
|
||||||
useActiveElement,
|
|
||||||
useAnimate,
|
|
||||||
useArrayDifference,
|
|
||||||
useArrayEvery,
|
|
||||||
useArrayFilter,
|
|
||||||
useArrayFind,
|
|
||||||
useArrayFindIndex,
|
|
||||||
useArrayFindLast,
|
|
||||||
useArrayIncludes,
|
|
||||||
useArrayJoin,
|
|
||||||
useArrayMap,
|
|
||||||
useArrayReduce,
|
|
||||||
useArraySome,
|
|
||||||
useArrayUnique,
|
|
||||||
useAsyncQueue,
|
|
||||||
useAsyncState,
|
|
||||||
useBase64,
|
|
||||||
useBattery,
|
|
||||||
useBluetooth,
|
|
||||||
useBreakpoints,
|
|
||||||
useBroadcastChannel,
|
|
||||||
useBrowserLocation,
|
|
||||||
useCached,
|
|
||||||
useClipboard,
|
|
||||||
useClipboardItems,
|
|
||||||
useCloned,
|
|
||||||
useColorMode,
|
|
||||||
useConfirmDialog,
|
|
||||||
useCountdown,
|
|
||||||
useCounter,
|
|
||||||
useCssVar,
|
|
||||||
useCurrentElement,
|
|
||||||
useCycleList,
|
|
||||||
useDark,
|
|
||||||
useDateFormat,
|
|
||||||
refDebounced as useDebounce,
|
|
||||||
useDebounceFn,
|
|
||||||
useDebouncedRefHistory,
|
|
||||||
useDeviceMotion,
|
|
||||||
useDeviceOrientation,
|
|
||||||
useDevicePixelRatio,
|
|
||||||
useDevicesList,
|
|
||||||
useDisplayMedia,
|
|
||||||
useDocumentVisibility,
|
|
||||||
useDraggable,
|
|
||||||
useDropZone,
|
|
||||||
useElementBounding,
|
|
||||||
useElementByPoint,
|
|
||||||
useElementHover,
|
|
||||||
useElementSize,
|
|
||||||
useElementVisibility,
|
|
||||||
useEventBus,
|
|
||||||
useEventListener,
|
|
||||||
useEventSource,
|
|
||||||
useEyeDropper,
|
|
||||||
useFavicon,
|
|
||||||
useFetch,
|
|
||||||
useFileDialog,
|
|
||||||
useFileSystemAccess,
|
|
||||||
useFocus,
|
|
||||||
useFocusWithin,
|
|
||||||
useFps,
|
|
||||||
useFullscreen,
|
|
||||||
useGamepad,
|
|
||||||
useGeolocation,
|
|
||||||
useIdle,
|
|
||||||
useImage,
|
|
||||||
useInfiniteScroll,
|
|
||||||
useIntersectionObserver,
|
|
||||||
useInterval,
|
|
||||||
useIntervalFn,
|
|
||||||
useKeyModifier,
|
|
||||||
useLastChanged,
|
|
||||||
useLocalStorage,
|
|
||||||
useMagicKeys,
|
|
||||||
useManualRefHistory,
|
|
||||||
useMediaControls,
|
|
||||||
useMediaQuery,
|
|
||||||
useMemoize,
|
|
||||||
useMemory,
|
|
||||||
useMounted,
|
|
||||||
useMouse,
|
|
||||||
useMouseInElement,
|
|
||||||
useMousePressed,
|
|
||||||
useMutationObserver,
|
|
||||||
useNavigatorLanguage,
|
|
||||||
useNetwork,
|
|
||||||
useNow,
|
|
||||||
useObjectUrl,
|
|
||||||
useOffsetPagination,
|
|
||||||
useOnline,
|
|
||||||
usePageLeave,
|
|
||||||
useParallax,
|
|
||||||
useParentElement,
|
|
||||||
usePerformanceObserver,
|
|
||||||
usePermission,
|
|
||||||
usePointer,
|
|
||||||
usePointerLock,
|
|
||||||
usePointerSwipe,
|
|
||||||
usePreferredColorScheme,
|
|
||||||
usePreferredContrast,
|
|
||||||
usePreferredDark,
|
|
||||||
usePreferredLanguages,
|
|
||||||
usePreferredReducedMotion,
|
|
||||||
usePreferredReducedTransparency,
|
|
||||||
usePrevious,
|
|
||||||
useRafFn,
|
|
||||||
useRefHistory,
|
|
||||||
useResizeObserver,
|
|
||||||
useSSRWidth,
|
|
||||||
useScreenOrientation,
|
|
||||||
useScreenSafeArea,
|
|
||||||
useScriptTag,
|
|
||||||
useScroll,
|
|
||||||
useScrollLock,
|
|
||||||
useSessionStorage,
|
|
||||||
useShare,
|
|
||||||
useSorted,
|
|
||||||
useSpeechRecognition,
|
|
||||||
useSpeechSynthesis,
|
|
||||||
useStepper,
|
|
||||||
useStorage,
|
|
||||||
useStorageAsync,
|
|
||||||
useStyleTag,
|
|
||||||
useSupported,
|
|
||||||
useSwipe,
|
|
||||||
useTemplateRefsList,
|
|
||||||
useTextDirection,
|
|
||||||
useTextSelection,
|
|
||||||
useTextareaAutosize,
|
|
||||||
refThrottled as useThrottle,
|
|
||||||
useThrottleFn,
|
|
||||||
useThrottledRefHistory,
|
|
||||||
useTimeAgo,
|
|
||||||
useTimeout,
|
|
||||||
useTimeoutFn,
|
|
||||||
useTimeoutPoll,
|
|
||||||
useTimestamp,
|
|
||||||
useTitle,
|
|
||||||
useToNumber,
|
|
||||||
useToString,
|
|
||||||
useToggle,
|
|
||||||
useTransition,
|
|
||||||
useUrlSearchParams,
|
|
||||||
useUserMedia,
|
|
||||||
useVModel,
|
|
||||||
useVModels,
|
|
||||||
useVibrate,
|
|
||||||
useVirtualList,
|
|
||||||
useWakeLock,
|
|
||||||
useWebNotification,
|
|
||||||
useWebSocket,
|
|
||||||
useWebWorker,
|
|
||||||
useWebWorkerFn,
|
|
||||||
useWindowFocus,
|
|
||||||
useWindowScroll,
|
|
||||||
useWindowSize,
|
|
||||||
watchArray,
|
|
||||||
watchAtMost,
|
|
||||||
watchDebounced,
|
|
||||||
watchDeep,
|
|
||||||
watchIgnorable,
|
|
||||||
watchImmediate,
|
|
||||||
watchOnce,
|
|
||||||
watchPausable,
|
|
||||||
watchThrottled,
|
|
||||||
watchTriggerable,
|
|
||||||
watchWithFilter,
|
|
||||||
whenever
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=vitepress___@vueuse_core.js.map
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"sources": [],
|
|
||||||
"sourcesContent": [],
|
|
||||||
"mappings": "",
|
|
||||||
"names": []
|
|
||||||
}
|
|
||||||
343
.vitepress/cache/deps/vue.js
vendored
@@ -1,343 +0,0 @@
|
|||||||
import {
|
|
||||||
BaseTransition,
|
|
||||||
BaseTransitionPropsValidators,
|
|
||||||
Comment,
|
|
||||||
DeprecationTypes,
|
|
||||||
EffectScope,
|
|
||||||
ErrorCodes,
|
|
||||||
ErrorTypeStrings,
|
|
||||||
Fragment,
|
|
||||||
KeepAlive,
|
|
||||||
ReactiveEffect,
|
|
||||||
Static,
|
|
||||||
Suspense,
|
|
||||||
Teleport,
|
|
||||||
Text,
|
|
||||||
TrackOpTypes,
|
|
||||||
Transition,
|
|
||||||
TransitionGroup,
|
|
||||||
TriggerOpTypes,
|
|
||||||
VueElement,
|
|
||||||
assertNumber,
|
|
||||||
callWithAsyncErrorHandling,
|
|
||||||
callWithErrorHandling,
|
|
||||||
camelize,
|
|
||||||
capitalize,
|
|
||||||
cloneVNode,
|
|
||||||
compatUtils,
|
|
||||||
compile,
|
|
||||||
computed,
|
|
||||||
createApp,
|
|
||||||
createBaseVNode,
|
|
||||||
createBlock,
|
|
||||||
createCommentVNode,
|
|
||||||
createElementBlock,
|
|
||||||
createHydrationRenderer,
|
|
||||||
createPropsRestProxy,
|
|
||||||
createRenderer,
|
|
||||||
createSSRApp,
|
|
||||||
createSlots,
|
|
||||||
createStaticVNode,
|
|
||||||
createTextVNode,
|
|
||||||
createVNode,
|
|
||||||
customRef,
|
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
defineCustomElement,
|
|
||||||
defineEmits,
|
|
||||||
defineExpose,
|
|
||||||
defineModel,
|
|
||||||
defineOptions,
|
|
||||||
defineProps,
|
|
||||||
defineSSRCustomElement,
|
|
||||||
defineSlots,
|
|
||||||
devtools,
|
|
||||||
effect,
|
|
||||||
effectScope,
|
|
||||||
getCurrentInstance,
|
|
||||||
getCurrentScope,
|
|
||||||
getCurrentWatcher,
|
|
||||||
getTransitionRawChildren,
|
|
||||||
guardReactiveProps,
|
|
||||||
h,
|
|
||||||
handleError,
|
|
||||||
hasInjectionContext,
|
|
||||||
hydrate,
|
|
||||||
hydrateOnIdle,
|
|
||||||
hydrateOnInteraction,
|
|
||||||
hydrateOnMediaQuery,
|
|
||||||
hydrateOnVisible,
|
|
||||||
initCustomFormatter,
|
|
||||||
initDirectivesForSSR,
|
|
||||||
inject,
|
|
||||||
isMemoSame,
|
|
||||||
isProxy,
|
|
||||||
isReactive,
|
|
||||||
isReadonly,
|
|
||||||
isRef,
|
|
||||||
isRuntimeOnly,
|
|
||||||
isShallow,
|
|
||||||
isVNode,
|
|
||||||
markRaw,
|
|
||||||
mergeDefaults,
|
|
||||||
mergeModels,
|
|
||||||
mergeProps,
|
|
||||||
nextTick,
|
|
||||||
normalizeClass,
|
|
||||||
normalizeProps,
|
|
||||||
normalizeStyle,
|
|
||||||
onActivated,
|
|
||||||
onBeforeMount,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onBeforeUpdate,
|
|
||||||
onDeactivated,
|
|
||||||
onErrorCaptured,
|
|
||||||
onMounted,
|
|
||||||
onRenderTracked,
|
|
||||||
onRenderTriggered,
|
|
||||||
onScopeDispose,
|
|
||||||
onServerPrefetch,
|
|
||||||
onUnmounted,
|
|
||||||
onUpdated,
|
|
||||||
onWatcherCleanup,
|
|
||||||
openBlock,
|
|
||||||
popScopeId,
|
|
||||||
provide,
|
|
||||||
proxyRefs,
|
|
||||||
pushScopeId,
|
|
||||||
queuePostFlushCb,
|
|
||||||
reactive,
|
|
||||||
readonly,
|
|
||||||
ref,
|
|
||||||
registerRuntimeCompiler,
|
|
||||||
render,
|
|
||||||
renderList,
|
|
||||||
renderSlot,
|
|
||||||
resolveComponent,
|
|
||||||
resolveDirective,
|
|
||||||
resolveDynamicComponent,
|
|
||||||
resolveFilter,
|
|
||||||
resolveTransitionHooks,
|
|
||||||
setBlockTracking,
|
|
||||||
setDevtoolsHook,
|
|
||||||
setTransitionHooks,
|
|
||||||
shallowReactive,
|
|
||||||
shallowReadonly,
|
|
||||||
shallowRef,
|
|
||||||
ssrContextKey,
|
|
||||||
ssrUtils,
|
|
||||||
stop,
|
|
||||||
toDisplayString,
|
|
||||||
toHandlerKey,
|
|
||||||
toHandlers,
|
|
||||||
toRaw,
|
|
||||||
toRef,
|
|
||||||
toRefs,
|
|
||||||
toValue,
|
|
||||||
transformVNodeArgs,
|
|
||||||
triggerRef,
|
|
||||||
unref,
|
|
||||||
useAttrs,
|
|
||||||
useCssModule,
|
|
||||||
useCssVars,
|
|
||||||
useHost,
|
|
||||||
useId,
|
|
||||||
useModel,
|
|
||||||
useSSRContext,
|
|
||||||
useShadowRoot,
|
|
||||||
useSlots,
|
|
||||||
useTemplateRef,
|
|
||||||
useTransitionState,
|
|
||||||
vModelCheckbox,
|
|
||||||
vModelDynamic,
|
|
||||||
vModelRadio,
|
|
||||||
vModelSelect,
|
|
||||||
vModelText,
|
|
||||||
vShow,
|
|
||||||
version,
|
|
||||||
warn,
|
|
||||||
watch,
|
|
||||||
watchEffect,
|
|
||||||
watchPostEffect,
|
|
||||||
watchSyncEffect,
|
|
||||||
withAsyncContext,
|
|
||||||
withCtx,
|
|
||||||
withDefaults,
|
|
||||||
withDirectives,
|
|
||||||
withKeys,
|
|
||||||
withMemo,
|
|
||||||
withModifiers,
|
|
||||||
withScopeId
|
|
||||||
} from "./chunk-I4O5PVBA.js";
|
|
||||||
export {
|
|
||||||
BaseTransition,
|
|
||||||
BaseTransitionPropsValidators,
|
|
||||||
Comment,
|
|
||||||
DeprecationTypes,
|
|
||||||
EffectScope,
|
|
||||||
ErrorCodes,
|
|
||||||
ErrorTypeStrings,
|
|
||||||
Fragment,
|
|
||||||
KeepAlive,
|
|
||||||
ReactiveEffect,
|
|
||||||
Static,
|
|
||||||
Suspense,
|
|
||||||
Teleport,
|
|
||||||
Text,
|
|
||||||
TrackOpTypes,
|
|
||||||
Transition,
|
|
||||||
TransitionGroup,
|
|
||||||
TriggerOpTypes,
|
|
||||||
VueElement,
|
|
||||||
assertNumber,
|
|
||||||
callWithAsyncErrorHandling,
|
|
||||||
callWithErrorHandling,
|
|
||||||
camelize,
|
|
||||||
capitalize,
|
|
||||||
cloneVNode,
|
|
||||||
compatUtils,
|
|
||||||
compile,
|
|
||||||
computed,
|
|
||||||
createApp,
|
|
||||||
createBlock,
|
|
||||||
createCommentVNode,
|
|
||||||
createElementBlock,
|
|
||||||
createBaseVNode as createElementVNode,
|
|
||||||
createHydrationRenderer,
|
|
||||||
createPropsRestProxy,
|
|
||||||
createRenderer,
|
|
||||||
createSSRApp,
|
|
||||||
createSlots,
|
|
||||||
createStaticVNode,
|
|
||||||
createTextVNode,
|
|
||||||
createVNode,
|
|
||||||
customRef,
|
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
defineCustomElement,
|
|
||||||
defineEmits,
|
|
||||||
defineExpose,
|
|
||||||
defineModel,
|
|
||||||
defineOptions,
|
|
||||||
defineProps,
|
|
||||||
defineSSRCustomElement,
|
|
||||||
defineSlots,
|
|
||||||
devtools,
|
|
||||||
effect,
|
|
||||||
effectScope,
|
|
||||||
getCurrentInstance,
|
|
||||||
getCurrentScope,
|
|
||||||
getCurrentWatcher,
|
|
||||||
getTransitionRawChildren,
|
|
||||||
guardReactiveProps,
|
|
||||||
h,
|
|
||||||
handleError,
|
|
||||||
hasInjectionContext,
|
|
||||||
hydrate,
|
|
||||||
hydrateOnIdle,
|
|
||||||
hydrateOnInteraction,
|
|
||||||
hydrateOnMediaQuery,
|
|
||||||
hydrateOnVisible,
|
|
||||||
initCustomFormatter,
|
|
||||||
initDirectivesForSSR,
|
|
||||||
inject,
|
|
||||||
isMemoSame,
|
|
||||||
isProxy,
|
|
||||||
isReactive,
|
|
||||||
isReadonly,
|
|
||||||
isRef,
|
|
||||||
isRuntimeOnly,
|
|
||||||
isShallow,
|
|
||||||
isVNode,
|
|
||||||
markRaw,
|
|
||||||
mergeDefaults,
|
|
||||||
mergeModels,
|
|
||||||
mergeProps,
|
|
||||||
nextTick,
|
|
||||||
normalizeClass,
|
|
||||||
normalizeProps,
|
|
||||||
normalizeStyle,
|
|
||||||
onActivated,
|
|
||||||
onBeforeMount,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onBeforeUpdate,
|
|
||||||
onDeactivated,
|
|
||||||
onErrorCaptured,
|
|
||||||
onMounted,
|
|
||||||
onRenderTracked,
|
|
||||||
onRenderTriggered,
|
|
||||||
onScopeDispose,
|
|
||||||
onServerPrefetch,
|
|
||||||
onUnmounted,
|
|
||||||
onUpdated,
|
|
||||||
onWatcherCleanup,
|
|
||||||
openBlock,
|
|
||||||
popScopeId,
|
|
||||||
provide,
|
|
||||||
proxyRefs,
|
|
||||||
pushScopeId,
|
|
||||||
queuePostFlushCb,
|
|
||||||
reactive,
|
|
||||||
readonly,
|
|
||||||
ref,
|
|
||||||
registerRuntimeCompiler,
|
|
||||||
render,
|
|
||||||
renderList,
|
|
||||||
renderSlot,
|
|
||||||
resolveComponent,
|
|
||||||
resolveDirective,
|
|
||||||
resolveDynamicComponent,
|
|
||||||
resolveFilter,
|
|
||||||
resolveTransitionHooks,
|
|
||||||
setBlockTracking,
|
|
||||||
setDevtoolsHook,
|
|
||||||
setTransitionHooks,
|
|
||||||
shallowReactive,
|
|
||||||
shallowReadonly,
|
|
||||||
shallowRef,
|
|
||||||
ssrContextKey,
|
|
||||||
ssrUtils,
|
|
||||||
stop,
|
|
||||||
toDisplayString,
|
|
||||||
toHandlerKey,
|
|
||||||
toHandlers,
|
|
||||||
toRaw,
|
|
||||||
toRef,
|
|
||||||
toRefs,
|
|
||||||
toValue,
|
|
||||||
transformVNodeArgs,
|
|
||||||
triggerRef,
|
|
||||||
unref,
|
|
||||||
useAttrs,
|
|
||||||
useCssModule,
|
|
||||||
useCssVars,
|
|
||||||
useHost,
|
|
||||||
useId,
|
|
||||||
useModel,
|
|
||||||
useSSRContext,
|
|
||||||
useShadowRoot,
|
|
||||||
useSlots,
|
|
||||||
useTemplateRef,
|
|
||||||
useTransitionState,
|
|
||||||
vModelCheckbox,
|
|
||||||
vModelDynamic,
|
|
||||||
vModelRadio,
|
|
||||||
vModelSelect,
|
|
||||||
vModelText,
|
|
||||||
vShow,
|
|
||||||
version,
|
|
||||||
warn,
|
|
||||||
watch,
|
|
||||||
watchEffect,
|
|
||||||
watchPostEffect,
|
|
||||||
watchSyncEffect,
|
|
||||||
withAsyncContext,
|
|
||||||
withCtx,
|
|
||||||
withDefaults,
|
|
||||||
withDirectives,
|
|
||||||
withKeys,
|
|
||||||
withMemo,
|
|
||||||
withModifiers,
|
|
||||||
withScopeId
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=vue.js.map
|
|
||||||
7
.vitepress/cache/deps/vue.js.map
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"sources": [],
|
|
||||||
"sourcesContent": [],
|
|
||||||
"mappings": "",
|
|
||||||
"names": []
|
|
||||||
}
|
|
||||||
215
.workflow/main copy.yml
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
name: AutoBuild # 工作流的名称
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # 给予写入仓库内容的权限
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*' # 当推送以v开头的标签时触发此工作流
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: build and release electron app # 任务名称
|
||||||
|
runs-on: ${{ matrix.os }} # 在matrix.os定义的操作系统上运行
|
||||||
|
strategy:
|
||||||
|
fail-fast: false # 如果一个任务失败,其他任务继续运行
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, macos-latest, ubuntu-latest] # 在Windows和macOS上运行任务
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out git repository
|
||||||
|
uses: actions/checkout@v4 # 检出代码仓库
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22 # 安装Node.js 22 (这里node环境是能够运行代码的环境)
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
npm i -g yarn
|
||||||
|
yarn install # 安装项目依赖
|
||||||
|
|
||||||
|
- name: Build Electron App for windows
|
||||||
|
if: matrix.os == 'windows-latest' # 只在Windows上运行
|
||||||
|
run: yarn run build:win # 构建Windows版应用
|
||||||
|
|
||||||
|
- name: Build Electron App for macos
|
||||||
|
if: matrix.os == 'macos-latest' # 只在macOS上运行
|
||||||
|
run: |
|
||||||
|
yarn run build:mac
|
||||||
|
|
||||||
|
- name: Build Electron App for linux
|
||||||
|
if: matrix.os == 'ubuntu-latest' # 只在Linux上运行
|
||||||
|
run: yarn run build:linux # 构建Linux版应用
|
||||||
|
|
||||||
|
- name: Cleanup Artifacts for Windows
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: |
|
||||||
|
npx del-cli "dist/*" "!dist/*.exe" "!dist/*.zip" "!dist/*.yml" # 清理Windows构建产物,只保留特定文件
|
||||||
|
|
||||||
|
- name: Cleanup Artifacts for MacOS
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
npx del-cli "dist/*" "!dist/(*.dmg|*.zip|latest*.yml)" # 清理macOS构建产物,只保留特定文件
|
||||||
|
|
||||||
|
- name: Cleanup Artifacts for Linux
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
npx del-cli "dist/*" "!dist/(*.AppImage|*.deb|*.snap|latest*.yml)" # 清理Linux构建产物,只保留特定文件
|
||||||
|
|
||||||
|
- name: upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }}
|
||||||
|
path: dist # 上传构建产物作为工作流artifact
|
||||||
|
|
||||||
|
- name: release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: 'dist/**' # 将dist目录下所有文件添加到release
|
||||||
|
|
||||||
|
# 新增:自动同步到 WebDAV
|
||||||
|
sync-to-webdav:
|
||||||
|
name: Sync to WebDAV
|
||||||
|
needs: release # 等待 release 任务完成
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') # 只在标签推送时执行
|
||||||
|
steps:
|
||||||
|
- name: Wait for release to be ready
|
||||||
|
run: |
|
||||||
|
echo "等待 Release 准备就绪..."
|
||||||
|
sleep 30 # 等待30秒确保 release 完全创建
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y curl jq
|
||||||
|
|
||||||
|
- name: Get latest release info
|
||||||
|
id: get-release
|
||||||
|
run: |
|
||||||
|
# 获取当前标签对应的 release 信息
|
||||||
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||||
|
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# 获取 release 详细信息
|
||||||
|
response=$(curl -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
"https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG_NAME")
|
||||||
|
|
||||||
|
release_id=$(echo "$response" | jq -r '.id')
|
||||||
|
echo "release_id=$release_id" >> $GITHUB_OUTPUT
|
||||||
|
echo "找到 Release ID: $release_id"
|
||||||
|
|
||||||
|
- name: Sync release to WebDAV
|
||||||
|
run: |
|
||||||
|
TAG_NAME="${{ steps.get-release.outputs.tag_name }}"
|
||||||
|
RELEASE_ID="${{ steps.get-release.outputs.release_id }}"
|
||||||
|
|
||||||
|
echo "🚀 开始同步版本 $TAG_NAME 到 WebDAV..."
|
||||||
|
echo "Release ID: $RELEASE_ID"
|
||||||
|
echo "WebDAV 根路径: ${{ secrets.WEBDAV_BASE_URL }}/yd/ceru"
|
||||||
|
|
||||||
|
# 获取该release的所有资源文件
|
||||||
|
assets_json=$(curl -s \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
-H "Accept: application/vnd.github.v3+json" \
|
||||||
|
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets")
|
||||||
|
|
||||||
|
assets_count=$(echo "$assets_json" | jq '. | length')
|
||||||
|
echo "找到 $assets_count 个资源文件"
|
||||||
|
|
||||||
|
if [ "$assets_count" -eq 0 ]; then
|
||||||
|
echo "⚠️ 该版本没有资源文件,跳过同步"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 先创建版本目录
|
||||||
|
dir_path="/yd/ceru/$TAG_NAME"
|
||||||
|
dir_url="${{ secrets.WEBDAV_BASE_URL }}$dir_path"
|
||||||
|
|
||||||
|
echo "创建版本目录: $dir_path"
|
||||||
|
curl -s -X MKCOL \
|
||||||
|
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||||
|
"$dir_url" || echo "目录可能已存在"
|
||||||
|
|
||||||
|
# 处理每个asset
|
||||||
|
success_count=0
|
||||||
|
failed_count=0
|
||||||
|
|
||||||
|
for i in $(seq 0 $(($assets_count - 1))); do
|
||||||
|
asset=$(echo "$assets_json" | jq -c ".[$i]")
|
||||||
|
asset_name=$(echo "$asset" | jq -r '.name')
|
||||||
|
asset_url=$(echo "$asset" | jq -r '.url')
|
||||||
|
asset_size=$(echo "$asset" | jq -r '.size')
|
||||||
|
|
||||||
|
echo "📦 处理资源: $asset_name (大小: $asset_size bytes)"
|
||||||
|
|
||||||
|
# 下载资源文件
|
||||||
|
safe_filename="./temp_${TAG_NAME}_$(date +%s)_$i"
|
||||||
|
|
||||||
|
if ! curl -sL -o "$safe_filename" \
|
||||||
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
-H "Accept: application/octet-stream" \
|
||||||
|
"$asset_url"; then
|
||||||
|
echo "❌ 下载失败: $asset_name"
|
||||||
|
failed_count=$((failed_count + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$safe_filename" ]; then
|
||||||
|
actual_size=$(wc -c < "$safe_filename")
|
||||||
|
if [ "$actual_size" -ne "$asset_size" ]; then
|
||||||
|
echo "❌ 文件大小不匹配: $asset_name (期望: $asset_size, 实际: $actual_size)"
|
||||||
|
rm -f "$safe_filename"
|
||||||
|
failed_count=$((failed_count + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "⬆️ 上传到 WebDAV: $asset_name"
|
||||||
|
|
||||||
|
# 构建远程路径
|
||||||
|
remote_path="/yd/ceru/$TAG_NAME/$asset_name"
|
||||||
|
full_url="${{ secrets.WEBDAV_BASE_URL }}$remote_path"
|
||||||
|
|
||||||
|
# 使用 WebDAV PUT 方法上传文件
|
||||||
|
if curl -s -f -X PUT \
|
||||||
|
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||||
|
-T "$safe_filename" \
|
||||||
|
"$full_url"; then
|
||||||
|
|
||||||
|
echo "✅ 上传成功: $asset_name"
|
||||||
|
success_count=$((success_count + 1))
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "❌ 上传失败: $asset_name"
|
||||||
|
failed_count=$((failed_count + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
rm -f "$safe_filename"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
else
|
||||||
|
echo "❌ 临时文件不存在: $safe_filename"
|
||||||
|
failed_count=$((failed_count + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "🎉 同步完成!"
|
||||||
|
echo "成功: $success_count 个文件"
|
||||||
|
echo "失败: $failed_count 个文件"
|
||||||
|
echo "总计: $assets_count 个文件"
|
||||||
|
|
||||||
|
- name: Notify completion
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ "${{ job.status }}" == "success" ]; then
|
||||||
|
echo "✅ 版本 ${{ steps.get-release.outputs.tag_name }} 已成功同步到 alist"
|
||||||
|
else
|
||||||
|
echo "❌ 版本 ${{ steps.get-release.outputs.tag_name }} 同步失败"
|
||||||
|
fi
|
||||||
142
README.md
@@ -6,11 +6,14 @@
|
|||||||
|
|
||||||
Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,**仅提供插件运行框架与播放功能**,不直接存储、提供任何音乐源文件。用户需通过自行选择、安装合规插件获取音乐相关数据,项目旨在为开发者提供桌面应用技术实践与学习案例,为用户提供合规的音乐播放工具框架。
|
Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,**仅提供插件运行框架与播放功能**,不直接存储、提供任何音乐源文件。用户需通过自行选择、安装合规插件获取音乐相关数据,项目旨在为开发者提供桌面应用技术实践与学习案例,为用户提供合规的音乐播放工具框架。
|
||||||
|
|
||||||
<img src="assets/image-20250827175023917.png" alt="image-20250827175023917" style="zoom: 33%;" /><img src="assets/image-20250827175109430.png" alt="image-20250827175109430" style="zoom:33%;" />
|
<img src="assets/image-20251003173109619.png" alt="image-20251003173109619" style="zoom:33%;" />
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://www.star-history.com/#timeshiftsauce/CeruMusic&Date)
|
[](https://www.star-history.com/#timeshiftsauce/CeruMusic&Date)
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- **Electron**:用于构建跨平台桌面应用
|
- **Electron**:用于构建跨平台桌面应用
|
||||||
@@ -19,23 +22,17 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
|
|||||||
- **Pinia**:状态管理工具
|
- **Pinia**:状态管理工具
|
||||||
- **Vite**:快速的前端构建工具
|
- **Vite**:快速的前端构建工具
|
||||||
- **CeruPlugins**:音乐插件运行环境(仅提供框架,不包含默认插件)
|
- **CeruPlugins**:音乐插件运行环境(仅提供框架,不包含默认插件)
|
||||||
- **AMLL**:音乐生态辅助模块(仅提供功能接口,不关联具体音乐数据源)
|
- **AMLL**:音乐生态(歌词渲染等)辅助模块(仅提供功能接口,不关联具体音乐数据源)
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>点击查看目录结构</summary>
|
||||||
|
|
||||||
```ast
|
```ast
|
||||||
CeruMuisc/
|
CeruMuisc/
|
||||||
├── .github/
|
├── .github/
|
||||||
│ └── workflows/
|
|
||||||
│ ├── auto-sync-release.yml
|
|
||||||
│ ├── deploydocs.yml
|
|
||||||
│ ├── main.yml
|
|
||||||
│ ├── sync-releases-to-webdav.yml
|
|
||||||
│ └── uploadpan.yml
|
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── auth-test.js
|
|
||||||
│ ├── genAst.js
|
|
||||||
│ └── test-alist.js
|
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── common/
|
│ ├── common/
|
||||||
│ │ ├── types/
|
│ │ ├── types/
|
||||||
@@ -55,6 +52,7 @@ CeruMuisc/
|
|||||||
│ │ │ ├── autoUpdate.ts
|
│ │ │ ├── autoUpdate.ts
|
||||||
│ │ │ ├── directorySettings.ts
|
│ │ │ ├── directorySettings.ts
|
||||||
│ │ │ ├── musicCache.ts
|
│ │ │ ├── musicCache.ts
|
||||||
|
│ │ │ ├── pluginNotice.ts
|
||||||
│ │ │ └── songList.ts
|
│ │ │ └── songList.ts
|
||||||
│ │ ├── services/
|
│ │ ├── services/
|
||||||
│ │ │ ├── music/
|
│ │ │ ├── music/
|
||||||
@@ -76,91 +74,10 @@ CeruMuisc/
|
|||||||
│ │ │ ├── songList/
|
│ │ │ ├── songList/
|
||||||
│ │ │ │ ├── ManageSongList.ts
|
│ │ │ │ ├── ManageSongList.ts
|
||||||
│ │ │ │ └── PlayListSongs.ts
|
│ │ │ │ └── PlayListSongs.ts
|
||||||
│ │ │ └── ai-service.ts
|
│ │ │ ├── ai-service.ts
|
||||||
|
│ │ │ └── ConfigManager.ts
|
||||||
│ │ ├── utils/
|
│ │ ├── utils/
|
||||||
│ │ │ ├── musicSdk/
|
│ │ │ ├── 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
|
│ │ │ │ ├── api-source-info.ts
|
||||||
│ │ │ │ ├── index.js
|
│ │ │ │ ├── index.js
|
||||||
│ │ │ │ ├── options.js
|
│ │ │ │ ├── options.js
|
||||||
@@ -189,6 +106,16 @@ CeruMuisc/
|
|||||||
│ │ │ ├── components/
|
│ │ │ ├── components/
|
||||||
│ │ │ │ ├── AI/
|
│ │ │ │ ├── AI/
|
||||||
│ │ │ │ │ └── FloatBall.vue
|
│ │ │ │ │ └── FloatBall.vue
|
||||||
|
│ │ │ │ ├── ContextMenu/
|
||||||
|
│ │ │ │ │ ├── composables.ts
|
||||||
|
│ │ │ │ │ ├── ContextMenu.vue
|
||||||
|
│ │ │ │ │ ├── demo.vue
|
||||||
|
│ │ │ │ │ ├── index.ts
|
||||||
|
│ │ │ │ │ ├── README.md
|
||||||
|
│ │ │ │ │ ├── types.ts
|
||||||
|
│ │ │ │ │ └── utils.ts
|
||||||
|
│ │ │ │ ├── layout/
|
||||||
|
│ │ │ │ │ └── HomeLayout.vue
|
||||||
│ │ │ │ ├── Music/
|
│ │ │ │ ├── Music/
|
||||||
│ │ │ │ │ └── SongVirtualList.vue
|
│ │ │ │ │ └── SongVirtualList.vue
|
||||||
│ │ │ │ ├── Play/
|
│ │ │ │ ├── Play/
|
||||||
@@ -199,14 +126,14 @@ CeruMuisc/
|
|||||||
│ │ │ │ │ ├── PlaylistDrawer.vue
|
│ │ │ │ │ ├── PlaylistDrawer.vue
|
||||||
│ │ │ │ │ ├── PlayMusic.vue
|
│ │ │ │ │ ├── PlayMusic.vue
|
||||||
│ │ │ │ │ └── ShaderBackground.vue
|
│ │ │ │ │ └── ShaderBackground.vue
|
||||||
│ │ │ │ ├── Search/
|
|
||||||
│ │ │ │ │ └── SearchComponent.vue
|
|
||||||
│ │ │ │ ├── Settings/
|
│ │ │ │ ├── Settings/
|
||||||
│ │ │ │ │ ├── AIFloatBallSettings.vue
|
│ │ │ │ │ ├── AIFloatBallSettings.vue
|
||||||
│ │ │ │ │ ├── DirectorySettings.vue
|
│ │ │ │ │ ├── DirectorySettings.vue
|
||||||
│ │ │ │ │ ├── MusicCache.vue
|
│ │ │ │ │ ├── MusicCache.vue
|
||||||
│ │ │ │ │ ├── PlaylistSettings.vue
|
│ │ │ │ │ ├── PlaylistSettings.vue
|
||||||
|
│ │ │ │ │ ├── plugins.vue
|
||||||
│ │ │ │ │ └── UpdateSettings.vue
|
│ │ │ │ │ └── UpdateSettings.vue
|
||||||
|
│ │ │ │ ├── PluginNoticeDialog.vue
|
||||||
│ │ │ │ ├── ThemeSelector.vue
|
│ │ │ │ ├── ThemeSelector.vue
|
||||||
│ │ │ │ ├── TitleBarControls.vue
|
│ │ │ │ ├── TitleBarControls.vue
|
||||||
│ │ │ │ ├── UpdateExample.vue
|
│ │ │ │ ├── UpdateExample.vue
|
||||||
@@ -214,8 +141,6 @@ CeruMuisc/
|
|||||||
│ │ │ │ └── Versions.vue
|
│ │ │ │ └── Versions.vue
|
||||||
│ │ │ ├── composables/
|
│ │ │ ├── composables/
|
||||||
│ │ │ │ └── useAutoUpdate.ts
|
│ │ │ │ └── useAutoUpdate.ts
|
||||||
│ │ │ ├── layout/
|
|
||||||
│ │ │ │ └── index.vue
|
|
||||||
│ │ │ ├── router/
|
│ │ │ ├── router/
|
||||||
│ │ │ │ └── index.ts
|
│ │ │ │ └── index.ts
|
||||||
│ │ │ ├── services/
|
│ │ │ ├── services/
|
||||||
@@ -254,10 +179,10 @@ CeruMuisc/
|
|||||||
│ │ │ │ │ ├── recent.vue
|
│ │ │ │ │ ├── recent.vue
|
||||||
│ │ │ │ │ └── search.vue
|
│ │ │ │ │ └── search.vue
|
||||||
│ │ │ │ ├── settings/
|
│ │ │ │ ├── settings/
|
||||||
│ │ │ │ │ ├── index.vue
|
│ │ │ │ │ └── index.vue
|
||||||
│ │ │ │ │ └── plugins.vue
|
│ │ │ │ ├── welcome/
|
||||||
│ │ │ │ └── welcome/
|
│ │ │ │ │ └── index.vue
|
||||||
│ │ │ │ └── index.vue
|
│ │ │ │ └── ThemeDemo.vue
|
||||||
│ │ │ ├── App.vue
|
│ │ │ ├── App.vue
|
||||||
│ │ │ ├── env.d.ts
|
│ │ │ ├── env.d.ts
|
||||||
│ │ │ └── main.ts
|
│ │ │ └── main.ts
|
||||||
@@ -289,6 +214,8 @@ CeruMuisc/
|
|||||||
└── yarn.lock
|
└── yarn.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 主要功能
|
## 主要功能
|
||||||
|
|
||||||
- 提供插件加载与管理功能,支持通过合规插件获取公开音乐信息
|
- 提供插件加载与管理功能,支持通过合规插件获取公开音乐信息
|
||||||
@@ -351,8 +278,8 @@ CeruMuisc/
|
|||||||
|
|
||||||
## 文档与资源
|
## 文档与资源
|
||||||
|
|
||||||
- [产品设计文档](https://www.doubao.com/thread/docs/design.md):涵盖项目架构、核心功能设计和开发规范(不含任何音乐数据源信息)。
|
- 产品设计文档:涵盖项目架构、核心功能设计和开发规范(不含任何音乐数据源信息)。
|
||||||
- [插件开发文档](https://www.doubao.com/thread/docs/CeruMusic插件开发文档.md):仅提供插件开发技术规范,**明确要求插件开发者需遵守数据来源平台的用户协议与版权法**,禁止开发、传播获取非公开数据的插件。
|
- [插件开发文档](https://ceru.docs.shiqianjiang.cn/guide/CeruMusicPluginDev.html):仅提供插件开发技术规范,**明确要求插件开发者需遵守数据来源平台的用户协议与版权法**,禁止开发、传播获取非公开数据的插件。
|
||||||
|
|
||||||
## 开源许可
|
## 开源许可
|
||||||
|
|
||||||
@@ -363,7 +290,7 @@ CeruMuisc/
|
|||||||
欢迎开发者贡献代码与反馈建议,贡献内容需符合以下要求:
|
欢迎开发者贡献代码与反馈建议,贡献内容需符合以下要求:
|
||||||
|
|
||||||
1. 仅涉及播放器框架功能优化、bug 修复、文档完善,不包含任何音乐数据源相关代码。
|
1. 仅涉及播放器框架功能优化、bug 修复、文档完善,不包含任何音乐数据源相关代码。
|
||||||
2. 遵循 [Git 提交规范](https://www.doubao.com/thread/docs/design.md#git提交规范) 并确保代码符合项目风格指南。
|
2. 遵循 [Git 提交规范](#) 并确保代码符合项目风格指南。
|
||||||
3. 贡献的代码需无第三方版权纠纷,且不违反开源许可协议。
|
3. 贡献的代码需无第三方版权纠纷,且不违反开源许可协议。
|
||||||
|
|
||||||
## 联系方式
|
## 联系方式
|
||||||
@@ -430,3 +357,8 @@ CeruMuisc/
|
|||||||
|
|
||||||
若您认可本项目的技术价值,欢迎通过以下方式支持开发者(仅用于项目技术维护与迭代):
|
若您认可本项目的技术价值,欢迎通过以下方式支持开发者(仅用于项目技术维护与迭代):
|
||||||
<img src="assets/image-20250827175356006.png" alt="赞助方式1" style="zoom:33%;" /><img src="assets/image-20250827175547444.png" alt="赞助方式2" style="zoom: 33%;" />
|
<img src="assets/image-20250827175356006.png" alt="赞助方式1" style="zoom:33%;" /><img src="assets/image-20250827175547444.png" alt="赞助方式2" style="zoom: 33%;" />
|
||||||
|
|
||||||
|
## 联系
|
||||||
|
|
||||||
|
关于项目问题也可联系
|
||||||
|
邮箱:sqj@shiqianjiang.cn
|
||||||
|
|||||||
BIN
assets/image-20251003173109619.png
Normal file
|
After Width: | Height: | Size: 958 KiB |
BIN
assets/image-20251003173141699.png
Normal file
|
After Width: | Height: | Size: 1020 KiB |
BIN
assets/image-20251003173654569.png
Normal file
|
After Width: | Height: | Size: 870 KiB |
@@ -5,10 +5,23 @@ export default defineConfig({
|
|||||||
lang: 'zh-CN',
|
lang: 'zh-CN',
|
||||||
title: 'Ceru Music',
|
title: 'Ceru Music',
|
||||||
base: '/',
|
base: '/',
|
||||||
|
head: [
|
||||||
|
['link', { rel: 'icon', href: '/logo.svg' }],
|
||||||
|
['meta', { name: 'author', href: '时迁酱,无聊的霜霜,star' }],
|
||||||
|
[
|
||||||
|
'meta',
|
||||||
|
{
|
||||||
|
name: 'keywords',
|
||||||
|
content:
|
||||||
|
'Ceru Music,音乐播放器,音乐播放器工具,音乐播放器软件,音乐播放器下载,音乐播放器下载地址,澜音播放器,免费的音乐播放器,cerumusic,时迁酱,周晨鹭,无聊的霜霜,star,洛雪音乐,洛雪'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
['meta', { name: 'baidu-site-verification', content: 'codeva-ocKFImCsOO' }]
|
||||||
|
],
|
||||||
description:
|
description:
|
||||||
'Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。',
|
'Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。',
|
||||||
markdown:{
|
markdown: {
|
||||||
config(md){
|
config(md) {
|
||||||
md.use(note)
|
md.use(note)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -28,13 +41,10 @@ export default defineConfig({
|
|||||||
{ text: '安装教程', link: '/guide/' },
|
{ text: '安装教程', link: '/guide/' },
|
||||||
{
|
{
|
||||||
text: '使用教程',
|
text: '使用教程',
|
||||||
items: [
|
items: [{ text: '音乐播放列表', link: '/guide/used/playList' }]
|
||||||
{ text: '音乐播放列表', link: '/guide/used/playList' },
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{ text: '软件设计文档', link: '/guide/design' },
|
|
||||||
{ text: '更新日志', link: '/guide/updateLog' },
|
{ text: '更新日志', link: '/guide/updateLog' },
|
||||||
{ text: '更新计划', link: '/guide/update'}
|
{ text: '更新计划', link: '/guide/update' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,6 +53,10 @@ export default defineConfig({
|
|||||||
{ text: '插件类使用', link: '/guide/CeruMusicPluginHost' },
|
{ text: '插件类使用', link: '/guide/CeruMusicPluginHost' },
|
||||||
{ text: '澜音插件开发文档(重点)', link: '/guide/CeruMusicPluginDev' }
|
{ text: '澜音插件开发文档(重点)', link: '/guide/CeruMusicPluginDev' }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '鸣谢名单',
|
||||||
|
link: '/guide/sponsorship'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -64,21 +78,19 @@ export default defineConfig({
|
|||||||
provider: 'local'
|
provider: 'local'
|
||||||
},
|
},
|
||||||
outline: {
|
outline: {
|
||||||
level: [2,4],
|
level: [2, 4],
|
||||||
label: '文章导航'
|
label: '文章导航'
|
||||||
},
|
},
|
||||||
docFooter: {
|
docFooter: {
|
||||||
next: '下一篇',
|
next: '下一篇',
|
||||||
prev: '上一篇'
|
prev: '上一篇'
|
||||||
},
|
},
|
||||||
lastUpdatedText: '上次更新',
|
lastUpdatedText: '上次更新'
|
||||||
|
|
||||||
},
|
},
|
||||||
sitemap: {
|
sitemap: {
|
||||||
hostname: 'https://ceru.docs.shiqianjiang.cn'
|
hostname: 'https://ceru.docs.shiqianjiang.cn'
|
||||||
},
|
},
|
||||||
lastUpdated: true,
|
lastUpdated: true
|
||||||
head: [['link', { rel: 'icon', href: '/logo.svg' }]],
|
|
||||||
})
|
})
|
||||||
console.log(process.env.BASE_URL_DOCS)
|
console.log(process.env.BASE_URL_DOCS)
|
||||||
// Smooth scrolling functions
|
// Smooth scrolling functions
|
||||||
|
|||||||
@@ -168,15 +168,15 @@ html.dark #app {
|
|||||||
// --autonum-h3: counter(h1) "." counter(h2) "." counter(h3) ". ";
|
// --autonum-h3: counter(h1) "." counter(h2) "." counter(h3) ". ";
|
||||||
// --autonum-h4: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
|
// --autonum-h4: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
|
||||||
// --autonum-h5: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". ";
|
// --autonum-h5: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". ";
|
||||||
// --autonum-h6: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". ";
|
// --autonum-h6: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". ";
|
||||||
|
|
||||||
/* 下面是文章内Toc目录自动编号,与上面一样即可 */
|
/* 下面是文章内Toc目录自动编号,与上面一样即可 */
|
||||||
// --autonum-h1toc: counter(h1toc) ". ";
|
// --autonum-h1toc: counter(h1toc) ". ";
|
||||||
// --autonum-h2toc: counter(h1toc) "." counter(h2toc) ". ";
|
// --autonum-h2toc: counter(h1toc) "." counter(h2toc) ". ";
|
||||||
// --autonum-h3toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) ". ";
|
// --autonum-h3toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) ". ";
|
||||||
// --autonum-h4toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) ". ";
|
// --autonum-h4toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) ". ";
|
||||||
// --autonum-h5toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) ". ";
|
// --autonum-h5toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) ". ";
|
||||||
// --autonum-h6toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) "." counter(h6toc) ". ";
|
// --autonum-h6toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) "." counter(h6toc) ". ";
|
||||||
|
|
||||||
/* 主题颜色 */
|
/* 主题颜色 */
|
||||||
|
|
||||||
@@ -284,4 +284,4 @@ html .vp-doc div[class*='language-'] pre {
|
|||||||
}
|
}
|
||||||
.VPDoc.has-aside .content-container {
|
.VPDoc.has-aside .content-container {
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
CeruMusic 支持两种类型的插件:
|
CeruMusic 支持两种类型的插件:
|
||||||
|
|
||||||
1. **CeruMusic 原生插件**:基于 CeruMusic API 的插件格式
|
1. **CeruMusic 原生插件**:基于 CeruMusic API 的插件格式
|
||||||
2. **LX 兼容插件**:兼容 LX Music 的事件驱动插件格式
|
2. **LX 兼容插件**:兼容 LX Music 的事件驱动插件格式
|
||||||
|
|
||||||
@@ -64,11 +65,11 @@ const pluginInfo = {
|
|||||||
const sources = {
|
const sources = {
|
||||||
kw:{
|
kw:{
|
||||||
name: "酷我音乐",
|
name: "酷我音乐",
|
||||||
qualities: ['128k', '320k', 'flac', 'flac24bit']
|
qualitys: ['128k', '320k', 'flac', 'flac24bit']
|
||||||
},
|
},
|
||||||
tx:{
|
tx:{
|
||||||
name: "QQ音乐",
|
name: "QQ音乐",
|
||||||
qualities: ['128k', '320k', 'flac']
|
qualitys: ['128k', '320k', 'flac']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: musicInfo.id,
|
id: musicInfo.id,
|
||||||
quality: quality
|
qualitys: quality
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,23 +133,20 @@ module.exports = {
|
|||||||
> #### PS:
|
> #### PS:
|
||||||
>
|
>
|
||||||
> - `sources key` 取值
|
> - `sources key` 取值
|
||||||
>
|
|
||||||
> - wy 网易云音乐 |
|
> - wy 网易云音乐 |
|
||||||
> - tx QQ音乐 |
|
> - tx QQ音乐 |
|
||||||
> - kg 酷狗音乐 |
|
> - kg 酷狗音乐 |
|
||||||
> - mg 咪咕音乐 |
|
> - mg 咪咕音乐 |
|
||||||
> - kw 酷我音乐
|
> - kw 酷我音乐
|
||||||
>
|
|
||||||
> - 导出
|
> - 导出
|
||||||
>
|
>
|
||||||
> ```javascript
|
> ```javascript
|
||||||
> module.exports = {
|
> module.exports = {
|
||||||
> sources, // 你的音源支持
|
> sources // 你的音源支持
|
||||||
> };
|
> }
|
||||||
> ```
|
> ```
|
||||||
>
|
>
|
||||||
> - 支持的音质 ` sources.qualities: ['128k', '320k', 'flac']`
|
> - 支持的音质 ` sources.qualitys: ['128k', '320k', 'flac']`
|
||||||
>
|
|
||||||
> - `128k`: 128kbps
|
> - `128k`: 128kbps
|
||||||
> - `320k`: 320kbps
|
> - `320k`: 320kbps
|
||||||
> - `flac`: FLAC 无损
|
> - `flac`: FLAC 无损
|
||||||
@@ -157,8 +155,6 @@ module.exports = {
|
|||||||
> - `atmos`: 杜比全景声
|
> - `atmos`: 杜比全景声
|
||||||
> - `master`: 母带音质
|
> - `master`: 母带音质
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### CeruMusic API 参考
|
### CeruMusic API 参考
|
||||||
|
|
||||||
#### cerumusic.request(url, options)
|
#### cerumusic.request(url, options)
|
||||||
@@ -166,6 +162,7 @@ module.exports = {
|
|||||||
HTTP 请求方法,返回 Promise。
|
HTTP 请求方法,返回 Promise。
|
||||||
|
|
||||||
**参数:**
|
**参数:**
|
||||||
|
|
||||||
- `url` (string): 请求地址
|
- `url` (string): 请求地址
|
||||||
- `options` (object): 请求选项
|
- `options` (object): 请求选项
|
||||||
- `method`: 请求方法 (GET, POST, PUT, DELETE 等)
|
- `method`: 请求方法 (GET, POST, PUT, DELETE 等)
|
||||||
@@ -174,6 +171,7 @@ HTTP 请求方法,返回 Promise。
|
|||||||
- `timeout`: 超时时间(毫秒)
|
- `timeout`: 超时时间(毫秒)
|
||||||
|
|
||||||
**返回值:**
|
**返回值:**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
{
|
{
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
@@ -206,16 +204,17 @@ cerumusic.utils.crypto.rsaEncrypt(data, key)
|
|||||||
cerumusic.NoticeCenter('info', {
|
cerumusic.NoticeCenter('info', {
|
||||||
title: '通知标题',
|
title: '通知标题',
|
||||||
content: '通知内容',
|
content: '通知内容',
|
||||||
url: 'https://example.com', // 可选 当通知为update 版本跟新可传
|
url: 'https://example.com', // 可选 当通知为update 版本跟新可传
|
||||||
version: '版本号', // 当通知为update 版本跟新可传
|
version: '版本号', // 当通知为update 版本跟新可传
|
||||||
pluginInfo: {
|
pluginInfo: {
|
||||||
name: '插件名称',
|
name: '插件名称',
|
||||||
type: 'cr', // 固定唯一标识
|
type: 'cr' // 固定唯一标识
|
||||||
}// 当通知为update 版本跟新可传
|
} // 当通知为update 版本跟新可传
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**通知类型:**
|
**通知类型:**
|
||||||
|
|
||||||
- `'info'`: 信息通知
|
- `'info'`: 信息通知
|
||||||
- `'success'`: 成功通知
|
- `'success'`: 成功通知
|
||||||
- `'warn'`: 警告通知
|
- `'warn'`: 警告通知
|
||||||
@@ -247,46 +246,47 @@ const qualitys = {
|
|||||||
'128k': '128',
|
'128k': '128',
|
||||||
'320k': '320',
|
'320k': '320',
|
||||||
flac: 'flac',
|
flac: 'flac',
|
||||||
flac24bit: 'flac24bit',
|
flac24bit: 'flac24bit'
|
||||||
},
|
},
|
||||||
local: {},
|
local: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP 请求封装
|
// HTTP 请求封装
|
||||||
const httpRequest = (url, options) => new Promise((resolve, reject) => {
|
const httpRequest = (url, options) =>
|
||||||
request(url, options, (err, resp) => {
|
new Promise((resolve, reject) => {
|
||||||
if (err) return reject(err)
|
request(url, options, (err, resp) => {
|
||||||
resolve(resp.body)
|
if (err) return reject(err)
|
||||||
|
resolve(resp.body)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// API 实现
|
// API 实现
|
||||||
const apis = {
|
const apis = {
|
||||||
kw: {
|
kw: {
|
||||||
musicUrl({ songmid }, quality) {
|
musicUrl({ songmid }, quality) {
|
||||||
return httpRequest('http://xxx').then(data => {
|
return httpRequest('http://xxx').then((data) => {
|
||||||
return data.url
|
return data.url
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
musicUrl(info) {
|
musicUrl(info) {
|
||||||
return httpRequest('http://xxx').then(data => {
|
return httpRequest('http://xxx').then((data) => {
|
||||||
return data.url
|
return data.url
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
pic(info) {
|
pic(info) {
|
||||||
return httpRequest('http://xxx').then(data => {
|
return httpRequest('http://xxx').then((data) => {
|
||||||
return data.url
|
return data.url
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
lyric(info) {
|
lyric(info) {
|
||||||
return httpRequest('http://xxx').then(data => {
|
return httpRequest('http://xxx').then((data) => {
|
||||||
return {
|
return {
|
||||||
lyric: '...', // 歌曲歌词
|
lyric: '...', // 歌曲歌词
|
||||||
tlyric: '...', // 翻译歌词,没有可为 null
|
tlyric: '...', // 翻译歌词,没有可为 null
|
||||||
rlyric: '...', // 罗马音歌词,没有可为 null
|
rlyric: '...', // 罗马音歌词,没有可为 null
|
||||||
lxlyric: '...', // lx 逐字歌词,没有可为 null
|
lxlyric: '...' // lx 逐字歌词,没有可为 null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -313,15 +313,15 @@ send(EVENT_NAMES.inited, {
|
|||||||
name: '酷我音乐',
|
name: '酷我音乐',
|
||||||
type: 'music',
|
type: 'music',
|
||||||
actions: ['musicUrl'],
|
actions: ['musicUrl'],
|
||||||
qualitys: ['128k', '320k', 'flac', 'flac24bit'],
|
qualitys: ['128k', '320k', 'flac', 'flac24bit']
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
name: '本地音乐',
|
name: '本地音乐',
|
||||||
type: 'music',
|
type: 'music',
|
||||||
actions: ['musicUrl', 'lyric', 'pic'],
|
actions: ['musicUrl', 'lyric', 'pic'],
|
||||||
qualitys: [],
|
qualitys: []
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -342,8 +342,8 @@ send(EVENT_NAMES.inited, {
|
|||||||
```javascript
|
```javascript
|
||||||
lx.on(lx.EVENT_NAMES.request, ({ source, action, info }) => {
|
lx.on(lx.EVENT_NAMES.request, ({ source, action, info }) => {
|
||||||
// 必须返回 Promise
|
// 必须返回 Promise
|
||||||
return Promise.resolve(result);
|
return Promise.resolve(result)
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
#### globalThis.lx.send(eventName, data)
|
#### globalThis.lx.send(eventName, data)
|
||||||
@@ -369,18 +369,22 @@ lx.send(lx.EVENT_NAMES.updateAlert, {
|
|||||||
HTTP 请求方法:
|
HTTP 请求方法:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
lx.request('https://api.example.com', {
|
lx.request(
|
||||||
method: 'POST',
|
'https://api.example.com',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
{
|
||||||
body: JSON.stringify(data),
|
method: 'POST',
|
||||||
timeout: 10000
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}, (err, resp) => {
|
body: JSON.stringify(data),
|
||||||
if (err) {
|
timeout: 10000
|
||||||
console.error('请求失败:', err);
|
},
|
||||||
return;
|
(err, resp) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('请求失败:', err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('响应:', resp.body)
|
||||||
}
|
}
|
||||||
console.log('响应:', resp.body);
|
)
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### globalThis.lx.utils
|
#### globalThis.lx.utils
|
||||||
@@ -433,28 +437,28 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
try {
|
try {
|
||||||
// 参数验证
|
// 参数验证
|
||||||
if (!musicInfo || !musicInfo.id) {
|
if (!musicInfo || !musicInfo.id) {
|
||||||
throw new Error('音乐信息不完整');
|
throw new Error('音乐信息不完整')
|
||||||
}
|
}
|
||||||
|
|
||||||
// API 调用
|
// API 调用
|
||||||
const result = await cerumusic.request(url, options);
|
const result = await cerumusic.request(url, options)
|
||||||
|
|
||||||
// 结果验证
|
// 结果验证
|
||||||
if (!result || result.statusCode !== 200) {
|
if (!result || result.statusCode !== 200) {
|
||||||
throw new Error(`API 请求失败: ${result?.statusCode || 'Unknown'}`);
|
throw new Error(`API 请求失败: ${result?.statusCode || 'Unknown'}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.body || !result.body.url) {
|
if (!result.body || !result.body.url) {
|
||||||
throw new Error('返回数据格式错误');
|
throw new Error('返回数据格式错误')
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.body.url;
|
return result.body.url
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 记录错误日志
|
// 记录错误日志
|
||||||
console.error(`[${source}] 获取音乐链接失败:`, error.message);
|
console.error(`[${source}] 获取音乐链接失败:`, error.message)
|
||||||
|
|
||||||
// 重新抛出错误供上层处理
|
// 重新抛出错误供上层处理
|
||||||
throw new Error(`获取 ${source} 音乐链接失败: ${error.message}`);
|
throw new Error(`获取 ${source} 音乐链接失败: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -473,9 +477,9 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
### 1. 使用 console.log
|
### 1. 使用 console.log
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
console.log('[插件名] 调试信息:', data);
|
console.log('[插件名] 调试信息:', data)
|
||||||
console.warn('[插件名] 警告信息:', warning);
|
console.warn('[插件名] 警告信息:', warning)
|
||||||
console.error('[插件名] 错误信息:', error);
|
console.error('[插件名] 错误信息:', error)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. LX 插件开发者工具
|
### 2. LX 插件开发者工具
|
||||||
@@ -491,8 +495,8 @@ send(EVENT_NAMES.inited, {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.error('未处理的 Promise 拒绝:', reason);
|
console.error('未处理的 Promise 拒绝:', reason)
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -502,17 +506,17 @@ process.on('unhandledRejection', (reason, promise) => {
|
|||||||
### 1. 请求缓存
|
### 1. 请求缓存
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const cache = new Map();
|
const cache = new Map()
|
||||||
|
|
||||||
async function getCachedData(key, fetcher, ttl = 300000) {
|
async function getCachedData(key, fetcher, ttl = 300000) {
|
||||||
const cached = cache.get(key);
|
const cached = cache.get(key)
|
||||||
if (cached && Date.now() - cached.timestamp < ttl) {
|
if (cached && Date.now() - cached.timestamp < ttl) {
|
||||||
return cached.data;
|
return cached.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetcher();
|
const data = await fetcher()
|
||||||
cache.set(key, { data, timestamp: Date.now() });
|
cache.set(key, { data, timestamp: Date.now() })
|
||||||
return data;
|
return data
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -521,21 +525,21 @@ async function getCachedData(key, fetcher, ttl = 300000) {
|
|||||||
```javascript
|
```javascript
|
||||||
const result = await cerumusic.request(url, {
|
const result = await cerumusic.request(url, {
|
||||||
timeout: 10000 // 10秒超时
|
timeout: 10000 // 10秒超时
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 并发控制
|
### 3. 并发控制
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 限制并发请求数量
|
// 限制并发请求数量
|
||||||
const semaphore = new Semaphore(3); // 最多3个并发请求
|
const semaphore = new Semaphore(3) // 最多3个并发请求
|
||||||
|
|
||||||
async function limitedRequest(url, options) {
|
async function limitedRequest(url, options) {
|
||||||
await semaphore.acquire();
|
await semaphore.acquire()
|
||||||
try {
|
try {
|
||||||
return await cerumusic.request(url, options);
|
return await cerumusic.request(url, options)
|
||||||
} finally {
|
} finally {
|
||||||
semaphore.release();
|
semaphore.release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -549,14 +553,14 @@ async function limitedRequest(url, options) {
|
|||||||
```javascript
|
```javascript
|
||||||
function validateMusicInfo(musicInfo) {
|
function validateMusicInfo(musicInfo) {
|
||||||
if (!musicInfo || typeof musicInfo !== 'object') {
|
if (!musicInfo || typeof musicInfo !== 'object') {
|
||||||
throw new Error('音乐信息格式错误');
|
throw new Error('音乐信息格式错误')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!musicInfo.id || typeof musicInfo.id !== 'string') {
|
if (!musicInfo.id || typeof musicInfo.id !== 'string') {
|
||||||
throw new Error('音乐 ID 无效');
|
throw new Error('音乐 ID 无效')
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -565,10 +569,10 @@ function validateMusicInfo(musicInfo) {
|
|||||||
```javascript
|
```javascript
|
||||||
function isValidUrl(url) {
|
function isValidUrl(url) {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url)
|
||||||
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
|
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -581,7 +585,7 @@ console.log('请求参数:', {
|
|||||||
...params,
|
...params,
|
||||||
token: '***', // 隐藏敏感信息
|
token: '***', // 隐藏敏感信息
|
||||||
password: '***'
|
password: '***'
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -605,13 +609,13 @@ async function testMusicUrl() {
|
|||||||
id: 'test123',
|
id: 'test123',
|
||||||
name: '测试歌曲',
|
name: '测试歌曲',
|
||||||
artist: '测试歌手'
|
artist: '测试歌手'
|
||||||
};
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = await musicUrl('kw', testMusicInfo, '320k');
|
const url = await musicUrl('kw', testMusicInfo, '320k')
|
||||||
console.log('测试通过:', url);
|
console.log('测试通过:', url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('测试失败:', error);
|
console.error('测试失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -619,6 +623,7 @@ async function testMusicUrl() {
|
|||||||
### 3. 版本管理
|
### 3. 版本管理
|
||||||
|
|
||||||
使用语义化版本号:
|
使用语义化版本号:
|
||||||
|
|
||||||
- `1.0.0`: 主版本.次版本.修订版本
|
- `1.0.0`: 主版本.次版本.修订版本
|
||||||
- 主版本:不兼容的 API 修改
|
- 主版本:不兼容的 API 修改
|
||||||
- 次版本:向下兼容的功能性新增
|
- 次版本:向下兼容的功能性新增
|
||||||
@@ -631,6 +636,7 @@ async function testMusicUrl() {
|
|||||||
### Q: 插件加载失败怎么办?
|
### Q: 插件加载失败怎么办?
|
||||||
|
|
||||||
A: 检查以下几点:
|
A: 检查以下几点:
|
||||||
|
|
||||||
1. 文件编码是否为 UTF-8
|
1. 文件编码是否为 UTF-8
|
||||||
2. 插件信息注释格式是否正确
|
2. 插件信息注释格式是否正确
|
||||||
3. JavaScript 语法是否有错误
|
3. JavaScript 语法是否有错误
|
||||||
@@ -645,20 +651,21 @@ A: CeruMusic 的请求方法不受浏览器跨域限制,可以直接请求任
|
|||||||
A: 使用 `cerumusic.NoticeCenter` 事件通知用户更新:
|
A: 使用 `cerumusic.NoticeCenter` 事件通知用户更新:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
cerumusic.NoticeCenter('update',{
|
cerumusic.NoticeCenter('update', {
|
||||||
title:'新版本更新',
|
title: '新版本更新',
|
||||||
content:'xxxx',
|
content: 'xxxx',
|
||||||
version: 'v1.0.3',
|
version: 'v1.0.3',
|
||||||
url:'https://shiqianjiang.cn',
|
url: 'https://shiqianjiang.cn',
|
||||||
pluginInfo:{
|
pluginInfo: {
|
||||||
type:'cr'
|
type: 'cr'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### Q: 如何调试插件?
|
### Q: 如何调试插件?
|
||||||
|
|
||||||
A:
|
A:
|
||||||
|
|
||||||
1. 使用 `console.log` 输出调试信息 可在设置—>插件管理—>日志 查看调试
|
1. 使用 `console.log` 输出调试信息 可在设置—>插件管理—>日志 查看调试
|
||||||
2. LX 插件可以设置 `openDevTools: true` 打开开发者工具
|
2. LX 插件可以设置 `openDevTools: true` 打开开发者工具
|
||||||
3. 查看 CeruMusic 的插件日志
|
3. 查看 CeruMusic 的插件日志
|
||||||
@@ -668,5 +675,6 @@ A:
|
|||||||
## 技术支持
|
## 技术支持
|
||||||
|
|
||||||
如有问题或建议,请通过以下方式联系:
|
如有问题或建议,请通过以下方式联系:
|
||||||
|
|
||||||
- GitHub Issues: [CeruMusic Issues](https://github.com/timeshiftsauce/CeruMusic/issues)
|
- GitHub Issues: [CeruMusic Issues](https://github.com/timeshiftsauce/CeruMusic/issues)
|
||||||
- Blog (最好登录,否则需要审核): [CeruMusic Blog](https://shiqianjiang.cn/blog/4966904626407280640)
|
- Blog (最好登录,否则需要审核): [CeruMusic Blog](https://shiqianjiang.cn/blog/4966904626407280640)
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
# 音乐API接口文档
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
这是一个基于 Meting 库的音乐API接口,支持多个音乐平台的数据获取,包括歌曲信息、专辑、歌词、播放链接等。
|
|
||||||
|
|
||||||
## 基础信息
|
|
||||||
|
|
||||||
- **请求方式**: GET
|
|
||||||
- **返回格式**: JSON
|
|
||||||
- **字符编码**: UTF-8
|
|
||||||
- **跨域支持**: 是
|
|
||||||
|
|
||||||
## 请求参数
|
|
||||||
|
|
||||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
|
||||||
| ------ | ------ | ---- | ------- | -------------- |
|
|
||||||
| server | string | 否 | netease | 音乐平台 |
|
|
||||||
| type | string | 否 | search | 请求类型 |
|
|
||||||
| id | string | 否 | hello | 查询ID或关键词 |
|
|
||||||
|
|
||||||
### 支持的音乐平台 (server)
|
|
||||||
|
|
||||||
| 平台代码 | 平台名称 |
|
|
||||||
| -------- | ---------- |
|
|
||||||
| netease | 网易云音乐 |
|
|
||||||
| tencent | QQ音乐 |
|
|
||||||
| baidu | 百度音乐 |
|
|
||||||
| xiami | 虾米音乐 |
|
|
||||||
| kugou | 酷狗音乐 |
|
|
||||||
| kuwo | 酷我音乐 |
|
|
||||||
|
|
||||||
### 支持的请求类型 (type)
|
|
||||||
|
|
||||||
| 类型 | 说明 | id参数说明 |
|
|
||||||
| -------- | ------------ | ---------------- |
|
|
||||||
| search | 搜索歌曲 | 搜索关键词 |
|
|
||||||
| song | 获取歌曲详情 | 歌曲ID |
|
|
||||||
| album | 获取专辑信息 | 专辑ID |
|
|
||||||
| artist | 获取歌手信息 | 歌手ID |
|
|
||||||
| playlist | 获取歌单信息 | 歌单ID |
|
|
||||||
| lrc | 获取歌词 | 歌曲ID |
|
|
||||||
| url | 获取播放链接 | 歌曲ID |
|
|
||||||
| pic | 获取封面图片 | 歌曲/专辑/歌手ID |
|
|
||||||
|
|
||||||
## 响应格式
|
|
||||||
|
|
||||||
### 成功响应
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": {
|
|
||||||
// 具体数据内容,根据请求类型不同而不同
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 错误响应
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"message": "错误信息"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 请求示例
|
|
||||||
|
|
||||||
### 1. 搜索歌曲
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=search&id=周杰伦
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应示例**:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": [
|
|
||||||
{
|
|
||||||
"id": "186016",
|
|
||||||
"name": "青花瓷",
|
|
||||||
"artist": ["周杰伦"],
|
|
||||||
"album": "我很忙",
|
|
||||||
"pic_id": "109951163240682406",
|
|
||||||
"url_id": "186016",
|
|
||||||
"lyric_id": "186016"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 获取歌曲详情
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=song&id=186016
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 获取歌词
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=lrc&id=186016
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应示例**:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": {
|
|
||||||
"lyric": "[00:00.00] 作词 : 方文山\n[00:01.00] 作曲 : 周杰伦\n[00:22.78]素胚勾勒出青花笔锋浓转淡\n..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 获取播放链接
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=url&id=186016
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应示例**:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"message": [
|
|
||||||
{
|
|
||||||
"id": "186016",
|
|
||||||
"url": "http://music.163.com/song/media/outer/url?id=186016.mp3",
|
|
||||||
"size": 4729252,
|
|
||||||
"br": 128
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 获取专辑信息
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=album&id=18905
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 获取歌手信息
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=artist&id=6452
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. 获取歌单信息
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=playlist&id=19723756
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. 获取封面图片
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /?server=netease&type=pic&id=186016
|
|
||||||
```
|
|
||||||
|
|
||||||
## 错误码说明
|
|
||||||
|
|
||||||
| 错误信息 | 说明 |
|
|
||||||
| ------------------- | ---------------- |
|
|
||||||
| require id. | 缺少必需的id参数 |
|
|
||||||
| unsupported server. | 不支持的音乐平台 |
|
|
||||||
| unsupported type. | 不支持的请求类型 |
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **代理支持**: 如果设置了环境变量 `METING_PROXY`,API会使用代理访问音乐平台
|
|
||||||
2. **Cookie支持**: API会自动传递请求中的Cookie到音乐平台
|
|
||||||
3. **跨域访问**: API已配置CORS,支持跨域请求
|
|
||||||
4. **请求频率**: 建议控制请求频率,避免被音乐平台限制
|
|
||||||
5. **数据时效性**: 音乐平台的数据可能会发生变化,建议适当缓存但不要过度依赖
|
|
||||||
|
|
||||||
## 使用建议
|
|
||||||
|
|
||||||
1. **错误处理**: 请务必检查响应中的 `success` 字段
|
|
||||||
2. **数据验证**: 返回的数据结构可能因平台而异,请做好数据验证
|
|
||||||
3. **备用方案**: 建议支持多个音乐平台作为备用数据源
|
|
||||||
4. **缓存策略**: 对于不经常变化的数据(如歌词、专辑信息)建议进行缓存
|
|
||||||
|
|
||||||
## 技术实现
|
|
||||||
|
|
||||||
本API基于以下技术栈:
|
|
||||||
|
|
||||||
- **PHP**: 后端语言
|
|
||||||
- **Meting**: 音乐数据获取库
|
|
||||||
- **Composer**: 依赖管理
|
|
||||||
|
|
||||||
## 更新日志
|
|
||||||
|
|
||||||
- **v1.0.0**: 初始版本,支持基础的音乐数据获取功能
|
|
||||||
BIN
docs/guide/assets/image-20250926225425123.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,600 +0,0 @@
|
|||||||
# Ceru Music 产品设计文档
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
|
|
||||||
Ceru Music 是一个基于 Electron + Vue 3 的跨平台桌面音乐播放器,支持多音乐平台数据源,提供流畅的音乐播放体验。
|
|
||||||
|
|
||||||
## 项目架构
|
|
||||||
|
|
||||||
### 技术栈
|
|
||||||
|
|
||||||
- **前端框架**: Vue 3 + TypeScript + Composition API
|
|
||||||
- **桌面框架**: Electron (v37.2.3)
|
|
||||||
- **UI组件库**: TDesign Vue Next (v1.15.2)
|
|
||||||
- 
|
|
||||||
- **状态管理**: Pinia (v3.0.3)
|
|
||||||
- **路由管理**: Vue Router (v4.5.1)
|
|
||||||
- **构建工具**: Vite + electron-vite
|
|
||||||
- **包管理器**: PNPM
|
|
||||||
- **Node pnpm 版本**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
PS D:\code\Ceru-Music> node -v
|
|
||||||
v22.17.0
|
|
||||||
PS D:\code\Ceru-Music> pnpm -v
|
|
||||||
10.14.0
|
|
||||||
```
|
|
||||||
|
|
||||||
-
|
|
||||||
|
|
||||||
### 架构设计
|
|
||||||
|
|
||||||
```asp
|
|
||||||
Ceru Music
|
|
||||||
├── 主进程 (Main Process)
|
|
||||||
│ ├── 应用生命周期管理
|
|
||||||
│ ├── 窗口管理
|
|
||||||
│ ├── 系统集成 (托盘、快捷键)
|
|
||||||
│ └── 文件系统操作
|
|
||||||
├── 渲染进程 (Renderer Process)
|
|
||||||
│ ├── Vue 3 应用
|
|
||||||
│ ├── 用户界面
|
|
||||||
│ ├── 音乐播放控制
|
|
||||||
│ └── 数据展示
|
|
||||||
└── 预加载脚本 (Preload Script)
|
|
||||||
└── 安全的 IPC 通信桥梁
|
|
||||||
```
|
|
||||||
|
|
||||||
### 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── main/ # 主进程代码
|
|
||||||
│ ├── index.ts # 主进程入口
|
|
||||||
│ ├── window.ts # 窗口管理
|
|
||||||
│ └── services/ # 主进程服务
|
|
||||||
├── preload/ # 预加载脚本
|
|
||||||
│ └── index.ts # IPC 通信接口
|
|
||||||
└── renderer/ # 渲染进程 (Vue 应用)
|
|
||||||
├── src/
|
|
||||||
│ ├── components/ # Vue 组件
|
|
||||||
│ ├── views/ # 页面视图
|
|
||||||
│ ├── stores/ # Pinia 状态管理
|
|
||||||
│ ├── services/ # API 服务
|
|
||||||
│ ├── utils/ # 工具函数
|
|
||||||
│ └── types/ # TypeScript 类型定义
|
|
||||||
└── index.html # 应用入口
|
|
||||||
```
|
|
||||||
|
|
||||||
## 项目开发使用方式
|
|
||||||
|
|
||||||
### 开发环境启动
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 安装依赖
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# 启动开发服务器
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# 代码检查
|
|
||||||
pnpm lint
|
|
||||||
|
|
||||||
# 类型检查
|
|
||||||
pnpm typecheck
|
|
||||||
```
|
|
||||||
|
|
||||||
### 构建打包
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 构建当前平台
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# 构建 Windows 版本
|
|
||||||
pnpm build:win
|
|
||||||
|
|
||||||
# 构建 macOS 版本
|
|
||||||
pnpm build:mac
|
|
||||||
|
|
||||||
# 构建 Linux 版本
|
|
||||||
pnpm build:linux
|
|
||||||
```
|
|
||||||
|
|
||||||
## 音乐数据源接口设计
|
|
||||||
|
|
||||||
### 接口1: 网易云音乐原生接口 (主要数据源)
|
|
||||||
|
|
||||||
#### 获取音乐信息
|
|
||||||
|
|
||||||
- **请求地址**: `https://music.163.com/api/song/detail`
|
|
||||||
- **请求参数**: `ids=[ID1,ID2,ID3,...]` 音乐ID列表
|
|
||||||
- **示例**: `https://music.163.com/api/song/detail?ids=[36270426]`
|
|
||||||
|
|
||||||
#### 获取音乐直链
|
|
||||||
|
|
||||||
- **请求地址**: `https://music.163.com/song/media/outer/url`
|
|
||||||
- **请求参数**: `id=123` 音乐ID
|
|
||||||
- **示例**: `https://music.163.com/song/media/outer/url?id=36270426.mp3`
|
|
||||||
|
|
||||||
#### 获取歌词
|
|
||||||
|
|
||||||
- **请求地址**: `https://music.163.com/api/song/lyric`
|
|
||||||
- **请求参数**:
|
|
||||||
- `id=123` 音乐ID
|
|
||||||
- `lv=-1` 获取歌词
|
|
||||||
- `yv=-1` 获取逐字歌词
|
|
||||||
- `tv=-1` 获取歌词翻译
|
|
||||||
- **示例**: `https://music.163.com/api/song/lyric?id=36270426&lv=-1&yv=-1&tv=-1`
|
|
||||||
|
|
||||||
#### 搜索歌曲
|
|
||||||
|
|
||||||
- **请求地址**: `https://music.163.com/api/search/get/web`
|
|
||||||
- **请求参数**:
|
|
||||||
- `s` 歌名
|
|
||||||
- `type=1` 搜索类型
|
|
||||||
- `offset=0` 偏移量
|
|
||||||
- `limit=10` 搜索结果数量
|
|
||||||
- **示例**: `https://music.163.com/api/search/get/web?s=来自天堂的魔鬼&type=1&offset=0&limit=10`
|
|
||||||
|
|
||||||
### 接口2: Meting API (备用数据源)
|
|
||||||
|
|
||||||
#### 参数说明
|
|
||||||
|
|
||||||
- **server**: 数据源
|
|
||||||
- `netease` 网易云音乐(默认)
|
|
||||||
- `tencent` QQ音乐
|
|
||||||
- **type**: 类型
|
|
||||||
- `name` 歌曲名
|
|
||||||
- `artist` 歌手
|
|
||||||
- `url` 链接
|
|
||||||
- `pic` 封面
|
|
||||||
- `lrc` 歌词
|
|
||||||
- `song` 单曲
|
|
||||||
- `playlist` 歌单
|
|
||||||
- **id**: 类型ID(封面ID/单曲ID/歌单ID)
|
|
||||||
|
|
||||||
#### 使用示例
|
|
||||||
|
|
||||||
```
|
|
||||||
https://api.qijieya.cn/meting/?type=url&id=1969519579
|
|
||||||
https://api.qijieya.cn/meting/?type=song&id=591321
|
|
||||||
https://api.qijieya.cn/meting/?type=playlist&id=2619366284
|
|
||||||
```
|
|
||||||
|
|
||||||
### 接口3: 备选接口
|
|
||||||
|
|
||||||
- **地址**: https://doc.vkeys.cn/api-doc/
|
|
||||||
- **说明**: 不建议使用,延迟较高
|
|
||||||
|
|
||||||
### 接口4: 自部署接口 (备用)
|
|
||||||
|
|
||||||
- **地址**: `https://music.shiqianjiang.cn?id=你是我的风景&server=netease`
|
|
||||||
- **说明**: 不支持分页,用于获取歌曲源、歌词源等
|
|
||||||
- **文档**: [API文档](./api.md)
|
|
||||||
|
|
||||||
## 核心功能设计
|
|
||||||
|
|
||||||
### 通用请求函数设计
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 音乐服务接口定义
|
|
||||||
interface MusicService {
|
|
||||||
search({keyword: string, page?: number, limit?: number}): Promise<SearchResult>
|
|
||||||
getSongDetail({id: string)}: Promise<SongDetail>
|
|
||||||
getSongUrl({id: string}): Promise<string>
|
|
||||||
getLyric({id: string}): Promise<LyricData>
|
|
||||||
getPlaylist({id: string}): Promise<PlaylistData>
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用请求函数
|
|
||||||
async function request(method: string, ...args: any{},isLoading=false): Promise<any> {
|
|
||||||
try {
|
|
||||||
switch (method) {
|
|
||||||
case 'search':
|
|
||||||
return await musicService.search(args)
|
|
||||||
case 'getSongDetail':
|
|
||||||
return await musicService.getSongDetail(args)
|
|
||||||
case 'getSongUrl':
|
|
||||||
return await musicService.getSongUrl(args)
|
|
||||||
case 'getLyric':
|
|
||||||
return await musicService.getLyric(args)
|
|
||||||
default:
|
|
||||||
throw new Error(`未知的方法: ${method}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`请求失败: ${method}`, error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用示例
|
|
||||||
request('search', '周杰伦', 1, 20).then((result) => {
|
|
||||||
console.log('搜索结果:', result)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 状态管理设计 (Pinia + LocalStorage)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// stores/music.ts
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useMusicStore = defineStore('music', {
|
|
||||||
state: () => ({
|
|
||||||
// 当前播放歌曲
|
|
||||||
currentSong: null as Song | null,
|
|
||||||
// 播放列表
|
|
||||||
playlist: [] as Song[],
|
|
||||||
// 播放状态
|
|
||||||
isPlaying: false,
|
|
||||||
// 播放模式 (顺序、随机、单曲循环)
|
|
||||||
playMode: 'order' as 'order' | 'random' | 'repeat',
|
|
||||||
// 音量
|
|
||||||
volume: 0.8,
|
|
||||||
// 播放进度
|
|
||||||
currentTime: 0,
|
|
||||||
duration: 0
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
// 播放歌曲
|
|
||||||
async playSong(song: Song) {
|
|
||||||
this.currentSong = song
|
|
||||||
this.isPlaying = true
|
|
||||||
this.saveToStorage()
|
|
||||||
},
|
|
||||||
|
|
||||||
// 添加到播放列表
|
|
||||||
addToPlaylist(songs: Song[]) {
|
|
||||||
this.playlist.push(...songs)
|
|
||||||
this.saveToStorage()
|
|
||||||
},
|
|
||||||
|
|
||||||
// 保存到本地存储
|
|
||||||
saveToStorage() {
|
|
||||||
localStorage.setItem(
|
|
||||||
'music-state',
|
|
||||||
JSON.stringify({
|
|
||||||
currentSong: this.currentSong,
|
|
||||||
playlist: this.playlist,
|
|
||||||
playMode: this.playMode,
|
|
||||||
volume: this.volume
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
// 从本地存储恢复
|
|
||||||
loadFromStorage() {
|
|
||||||
const saved = localStorage.getItem('music-state')
|
|
||||||
if (saved) {
|
|
||||||
const state = JSON.parse(saved)
|
|
||||||
Object.assign(this, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 虚拟滚动列表设计
|
|
||||||
|
|
||||||
使用 TDesign 的虚拟滚动组件展示大量歌曲数据:
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<t-virtual-scroll :data="songList" :height="600" :item-height="60" :buffer="10">
|
|
||||||
<template #default="{ data: song, index }">
|
|
||||||
<div class="song-item" @click="playSong(song)">
|
|
||||||
<div class="song-cover">
|
|
||||||
<img :src="song.pic" :alt="song.name" />
|
|
||||||
</div>
|
|
||||||
<div class="song-info">
|
|
||||||
<div class="song-name">{{ song.name }}</div>
|
|
||||||
<div class="song-artist">{{ song.artist }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="song-duration">{{ formatTime(song.duration) }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-virtual-scroll>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 本地数据存储设计
|
|
||||||
|
|
||||||
#### 播放列表存储
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 方案1: LocalStorage (简单方案)
|
|
||||||
class PlaylistStorage {
|
|
||||||
private key = 'ceru-playlists'
|
|
||||||
|
|
||||||
save(playlists: Playlist[]) {
|
|
||||||
localStorage.setItem(this.key, JSON.stringify(playlists))
|
|
||||||
}
|
|
||||||
|
|
||||||
load(): Playlist[] {
|
|
||||||
const data = localStorage.getItem(this.key)
|
|
||||||
return data ? JSON.parse(data) : []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 方案2: Node.js 文件存储 (最优方案,支持分享)
|
|
||||||
class FileStorage {
|
|
||||||
private filePath = path.join(app.getPath('userData'), 'playlists.json')
|
|
||||||
|
|
||||||
async save(playlists: Playlist[]) {
|
|
||||||
await fs.writeFile(this.filePath, JSON.stringify(playlists, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
async load(): Promise<Playlist[]> {
|
|
||||||
try {
|
|
||||||
const data = await fs.readFile(this.filePath, 'utf-8')
|
|
||||||
return JSON.parse(data)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出播放列表
|
|
||||||
async export(playlist: Playlist, exportPath: string) {
|
|
||||||
await fs.writeFile(exportPath, JSON.stringify(playlist, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入播放列表
|
|
||||||
async import(importPath: string): Promise<Playlist> {
|
|
||||||
const data = await fs.readFile(importPath, 'utf-8')
|
|
||||||
return JSON.parse(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 用户体验设计
|
|
||||||
|
|
||||||
### 首次启动流程
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// stores/app.ts
|
|
||||||
export const useAppStore = defineStore('app', {
|
|
||||||
state: () => ({
|
|
||||||
isFirstLaunch: true,
|
|
||||||
hasCompletedWelcome: false,
|
|
||||||
userPreferences: {
|
|
||||||
theme: 'auto' as 'light' | 'dark' | 'auto',
|
|
||||||
language: 'zh-CN',
|
|
||||||
defaultMusicSource: 'netease',
|
|
||||||
autoPlay: false
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
checkFirstLaunch() {
|
|
||||||
const hasLaunched = localStorage.getItem('has-launched')
|
|
||||||
this.isFirstLaunch = !hasLaunched
|
|
||||||
|
|
||||||
if (this.isFirstLaunch) {
|
|
||||||
// 跳转到欢迎页面
|
|
||||||
router.push('/welcome')
|
|
||||||
} else {
|
|
||||||
// 加载用户配置
|
|
||||||
this.loadUserPreferences()
|
|
||||||
router.push('/home')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
completeWelcome(preferences?: Partial<UserPreferences>) {
|
|
||||||
if (preferences) {
|
|
||||||
Object.assign(this.userPreferences, preferences)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasCompletedWelcome = true
|
|
||||||
localStorage.setItem('has-launched', 'true')
|
|
||||||
localStorage.setItem('user-preferences', JSON.stringify(this.userPreferences))
|
|
||||||
|
|
||||||
router.push('/home')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 欢迎页面设计
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div class="welcome-container">
|
|
||||||
<t-steps :current="currentStep" class="welcome-steps">
|
|
||||||
<t-step title="欢迎使用" content="欢迎使用 Ceru Music" />
|
|
||||||
<t-step title="基础设置" content="配置您的偏好设置" />
|
|
||||||
<t-step title="完成设置" content="开始您的音乐之旅" />
|
|
||||||
</t-steps>
|
|
||||||
|
|
||||||
<transition name="slide" mode="out-in">
|
|
||||||
<component :is="currentStepComponent" @next="nextStep" @skip="skipWelcome" />
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import WelcomeStep1 from './steps/WelcomeStep1.vue'
|
|
||||||
import WelcomeStep2 from './steps/WelcomeStep2.vue'
|
|
||||||
import WelcomeStep3 from './steps/WelcomeStep3.vue'
|
|
||||||
|
|
||||||
const currentStep = ref(0)
|
|
||||||
const steps = [WelcomeStep1, WelcomeStep2, WelcomeStep3]
|
|
||||||
|
|
||||||
const currentStepComponent = computed(() => steps[currentStep.value])
|
|
||||||
|
|
||||||
function nextStep() {
|
|
||||||
if (currentStep.value < steps.length - 1) {
|
|
||||||
currentStep.value++
|
|
||||||
} else {
|
|
||||||
completeWelcome()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function skipWelcome() {
|
|
||||||
appStore.completeWelcome()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.slide-enter-active,
|
|
||||||
.slide-leave-active {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.slide-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(30px);
|
|
||||||
}
|
|
||||||
.slide-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-30px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 界面UI参考
|
|
||||||
|
|
||||||
![..\assets\image-20250813180944752.png)
|
|
||||||
|
|
||||||
## 页面动画设计
|
|
||||||
|
|
||||||
### 路由过渡动画
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<router-view v-slot="{ Component, route }">
|
|
||||||
<transition :name="getTransitionName(route)" mode="out-in">
|
|
||||||
<component :is="Component" :key="route.path" />
|
|
||||||
</transition>
|
|
||||||
</router-view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
function getTransitionName(route: any) {
|
|
||||||
// 根据路由层级决定动画方向
|
|
||||||
const depth = route.path.split('/').length
|
|
||||||
return depth > 2 ? 'slide-left' : 'slide-right'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 滑动动画 */
|
|
||||||
.slide-left-enter-active,
|
|
||||||
.slide-left-leave-active,
|
|
||||||
.slide-right-enter-active,
|
|
||||||
.slide-right-leave-active {
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-left-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
.slide-left-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-right-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
.slide-right-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 淡入淡出动画 */
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心组件设计
|
|
||||||
|
|
||||||
### 音乐播放器组件
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div class="music-player">
|
|
||||||
<div class="player-info">
|
|
||||||
<img :src="currentSong?.pic" class="song-cover" />
|
|
||||||
<div class="song-details">
|
|
||||||
<div class="song-name">{{ currentSong?.name }}</div>
|
|
||||||
<div class="song-artist">{{ currentSong?.artist }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="player-controls">
|
|
||||||
<t-button variant="text" @click="previousSong">
|
|
||||||
<t-icon name="skip-previous" />
|
|
||||||
</t-button>
|
|
||||||
<t-button :variant="isPlaying ? 'filled' : 'outline'" @click="togglePlay">
|
|
||||||
<t-icon :name="isPlaying ? 'pause' : 'play'" />
|
|
||||||
</t-button>
|
|
||||||
<t-button variant="text" @click="nextSong">
|
|
||||||
<t-icon name="skip-next" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="player-progress">
|
|
||||||
<span class="time-current">{{ formatTime(currentTime) }}</span>
|
|
||||||
<t-slider v-model="progress" :max="duration" @change="seekTo" class="progress-slider" />
|
|
||||||
<span class="time-duration">{{ formatTime(duration) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 开发规范
|
|
||||||
|
|
||||||
### 代码规范
|
|
||||||
|
|
||||||
- 使用 TypeScript 进行类型检查
|
|
||||||
- 遵循 ESLint 配置的代码规范
|
|
||||||
- 使用 Prettier 进行代码格式化
|
|
||||||
- 组件命名使用 PascalCase
|
|
||||||
- 文件命名使用 kebab-case
|
|
||||||
|
|
||||||
### Git 提交规范
|
|
||||||
|
|
||||||
```
|
|
||||||
feat: 新功能
|
|
||||||
fix: 修复bug
|
|
||||||
docs: 文档更新
|
|
||||||
style: 代码格式调整
|
|
||||||
refactor: 代码重构
|
|
||||||
test: 测试相关
|
|
||||||
chore: 构建过程或辅助工具的变动
|
|
||||||
```
|
|
||||||
|
|
||||||
### 性能优化
|
|
||||||
|
|
||||||
- 使用虚拟滚动处理大列表
|
|
||||||
- 图片懒加载
|
|
||||||
- 组件按需加载
|
|
||||||
- 音频预加载和缓存
|
|
||||||
- 防抖和节流优化用户交互
|
|
||||||
|
|
||||||
## 待补充功能
|
|
||||||
|
|
||||||
1. **歌词显示**: 滚动歌词、逐字高亮
|
|
||||||
2. **音效处理**: 均衡器、音效增强
|
|
||||||
3. **主题系统**: 多主题切换、自定义主题
|
|
||||||
4. **快捷键**: 全局快捷键支持
|
|
||||||
5. **系统集成**: 媒体键支持、系统通知
|
|
||||||
6. **云同步**: 播放列表云端同步
|
|
||||||
7. **插件系统**: 支持第三方插件扩展
|
|
||||||
8. **音乐推荐**: 基于听歌历史的智能推荐
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_本设计文档将随着项目开发进度持续更新和完善。_
|
|
||||||
18
docs/guide/sponsorship.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 赞助名单
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
| 昵称 | 赞助金额 |
|
||||||
|
| :-------------------------: | :------: |
|
||||||
|
| **群友**:可惜情绪落泪零碎 | 6.66 |
|
||||||
|
| **群友**:🍀 | 5 |
|
||||||
|
| **群友**:涟漪 | 50 |
|
||||||
|
| **作者朋友** | 188 |
|
||||||
|
| **群友**:我叫阿狸 | 3 |
|
||||||
|
| RiseSun | 9.9 |
|
||||||
|
| **b站小友**:光牙阿普斯木兰 | 5 |
|
||||||
|
| 青禾 | 8.8 |
|
||||||
|
| li peng | 200 |
|
||||||
|
| **群友**:XIZ | 3 |
|
||||||
|
|
||||||
|
据不完全统计 如有疏漏可联系sqj@shiqianjiang.cn
|
||||||
@@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
- [ ] 搜索框可以弄对应的音乐源的排行榜 搜索联想
|
- [ ] 搜索框可以弄对应的音乐源的排行榜 搜索联想
|
||||||
- [ ] 导航上面这几个按钮可以稍微优化一下
|
- [ ] 导航上面这几个按钮可以稍微优化一下
|
||||||
- [ ] 大熊好暖: 09-21 12:52:25 在搜索歌曲出来后 大熊好暖: 09-21 12:52:34 一般都是双击播放 大熊好暖: 09-21 12:54:16 这个是双击加入播放列表 可以优化 或者可以把这个双击做一个选项,让人选择
|
- [x] 支持在线导入插件
|
||||||
|
- [ ] 大熊好暖: 09-21 12:52:25 在搜索歌曲出来后 大熊好暖: 09-21 12:52:34 一般都是双击播放 大熊好暖: 09-21 12:54:16 这个是双击加入播放列表 可以优化 或者可以把这个双击做一个选项,让人选择
|
||||||
- [ ] 播放页 样式模板选择 歌词背景律动很有沉浸感,但是黑胶唱片的专辑样式不是很喜欢,希望增加一个选项如图二一样只有圆形专辑封面
|
- [ ] 播放页 样式模板选择 歌词背景律动很有沉浸感,但是黑胶唱片的专辑样式不是很喜欢,希望增加一个选项如图二一样只有圆形专辑封面
|
||||||
- [x] 点击搜索框的 源图标实现快速切换
|
- [x] 点击搜索框的 源图标实现快速切换
|
||||||
- [ ] ai功能完善
|
- [ ] ai功能完善
|
||||||
- [ ] 支持歌词隐藏
|
- [ ] 支持歌词隐藏
|
||||||
- [x] 兼容多平台歌单导入
|
- [x] 兼容多平台歌单导入
|
||||||
- [ ] 软件能不能记住上次打开的窗口大小,每次都要手动拉
|
- [x] 软件能不能记住上次打开的窗口大小,每次都要手动拉
|
||||||
|
- [x] 歌单右键菜单
|
||||||
|
- [x] 播放列表滚动条适配
|
||||||
|
- [x] 暗色主题
|
||||||
|
- [x] 歌单页支持修改封面
|
||||||
|
|||||||
@@ -1,11 +1,89 @@
|
|||||||
# 澜音版本更新日志
|
# 澜音版本更新日志
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 日志
|
## 日志
|
||||||
|
|
||||||
- ###### 2025-9-17 **(V1.3.2)**
|
- ###### 2025-10-7 (v1.4.0)
|
||||||
|
1. 优化搜索联想功能
|
||||||
|
|
||||||
|
支持
|
||||||
|
- 歌单
|
||||||
|
- 专辑
|
||||||
|
- 歌手
|
||||||
|
- 单曲名
|
||||||
|
|
||||||
|
2. 设置功能
|
||||||
|
- 性能优化设置
|
||||||
|
- 歌词弹簧跳动 开关
|
||||||
|
- 背景布朗运动 开关
|
||||||
|
- 音频可视化 开关
|
||||||
|
|
||||||
|
- 网络负载优化设置
|
||||||
|
- 存储设置 -> 缓存可以设置是否开启
|
||||||
|
|
||||||
|
优化网络差情况歌曲加载卡顿
|
||||||
|
|
||||||
|
3. 新增播放列表 **tag** 动画
|
||||||
|
|
||||||
|
4. **debug:**
|
||||||
|
- 修复 2 条 接口失效无法获取搜索联想建议
|
||||||
|
- SMTC: 如果歌曲是播放状态时 切换到其他页面导致组件注销后 歌曲确实在播放是正常的可是切换回来时 能暂停 但是图标马上变为播放图标 后续无法播放的问题
|
||||||
|
- 去除播放歌曲多余提醒
|
||||||
|
|
||||||
|
- ###### 2025-10-6 (v1.3.13)
|
||||||
|
1. 添加搜索联想功能
|
||||||
|
2. debug: 某云歌单导入 限制1000问题
|
||||||
|
|
||||||
|
- ###### 2025-10-3 (v1.3.12)
|
||||||
|
1. 支持暗黑主题
|
||||||
|
2. 调整插件页面ui
|
||||||
|
|
||||||
|
- ###### 2025-9-29 (v1.3.11)
|
||||||
|
1. 新增插件在线导入
|
||||||
|
|
||||||
|
- ###### 2025-9-28 (v1.3.10)
|
||||||
|
1. 优化播放列表
|
||||||
|
2. 单击播放
|
||||||
|
3. 右键菜单
|
||||||
|
4. 调整播放进度调粗细
|
||||||
|
|
||||||
|
- ###### 2025-09-27 (v1.3.9)
|
||||||
|
1. debug:flac格式使用ffmpeg
|
||||||
|
2. 修复高音质下载失效
|
||||||
|
|
||||||
|
- ###### 2025-9-26 (v1.3.8)
|
||||||
|
1. 写入歌曲tag信息
|
||||||
|
2. 歌曲下载 选择音质
|
||||||
|
3. 歌单 头部自动压缩
|
||||||
|
|
||||||
|
- ###### 2025-9-25 (v1.3.7)
|
||||||
|
1. 歌单
|
||||||
|
- 新增右键移除歌曲
|
||||||
|
- local 页歌单右键操作
|
||||||
|
- 歌单页支持修改封面
|
||||||
|
2. debug:右键菜单二级菜单位置决策
|
||||||
|
|
||||||
|
- ###### 2025-9-22 (v1.3.6)
|
||||||
|
1. 歌单列表可以右键操作
|
||||||
|
- 播放
|
||||||
|
- 下载
|
||||||
|
- 添加到歌单
|
||||||
|
- 添加到播放列表
|
||||||
|
2. 播放列表滚动条
|
||||||
|
3. 搜索页切换源重新加载
|
||||||
|
|
||||||
|
- ###### 2025-9-22 (v1.3.5)
|
||||||
|
1. 软件启动位置 宽高记忆 限制软件最大宽高
|
||||||
|
2. debug: 修复歌曲音质支持短缺问题
|
||||||
|
|
||||||
|
- ###### 2025-9-21 (v1.3.4)
|
||||||
|
1. 紧急修复QQ音乐歌词失效问题
|
||||||
|
|
||||||
|
- ###### 2025-9-21(v1.3.3)
|
||||||
|
1. 兼容多平台歌单导入
|
||||||
|
2. 点击搜索框的 源图标实现快速切换
|
||||||
|
3. debug: fix:列表删除按钮冒泡
|
||||||
|
|
||||||
|
- ###### 2025-9-17 **(v1.3.2)**
|
||||||
1. 目录结构调整
|
1. 目录结构调整
|
||||||
|
|
||||||
2. **支持插件更新提示**
|
2. **支持插件更新提示**
|
||||||
@@ -13,13 +91,11 @@
|
|||||||
**洛雪** 插件请手动重装适配
|
**洛雪** 插件请手动重装适配
|
||||||
|
|
||||||
3. **debug**
|
3. **debug**
|
||||||
|
|
||||||
- SMTC 问题
|
- SMTC 问题
|
||||||
|
|
||||||
- 歌曲缓存播放多次请求和多次缓存问题
|
- 歌曲缓存播放多次请求和多次缓存问题
|
||||||
|
|
||||||
- ###### 2025-9-17 **(V1.3.1)**
|
- ###### 2025-9-17 **(v1.3.1)**
|
||||||
|
|
||||||
1. **设置功能页**
|
1. **设置功能页**
|
||||||
- 缓存路径支持自定义
|
- 缓存路径支持自定义
|
||||||
- 下载路径支持自定义
|
- 下载路径支持自定义
|
||||||
@@ -27,4 +103,4 @@
|
|||||||
- 播放页面唱针可以拖动问题
|
- 播放页面唱针可以拖动问题
|
||||||
- 播放按钮加载中 因为自动下一曲 导致动画变形问题
|
- 播放按钮加载中 因为自动下一曲 导致动画变形问题
|
||||||
- **SMTC** 功能 系统显示**未知应用**问题
|
- **SMTC** 功能 系统显示**未知应用**问题
|
||||||
- 播放页歌词**字体粗细**偶现丢失问题
|
- 播放页歌词**字体粗细**偶现丢失问题
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## 基础使用
|
## 基础使用
|
||||||
|
|
||||||
1. 默认情况下,播放 **「搜索」「歌单」** 双击中的歌曲时,会自动将该歌曲添加到 **「列表」** 中的末尾后并不会播放,这与手动将歌曲添加到 **「列表」** 等价,亦或是通过点击歌曲前面小三角播放<img src="./assets/image-20250916132248046.png" alt="image-20250916132248046" style="float:right" />可以添加到歌曲开头也可进行该歌曲添加到 **「列表」** 中的开头并播放。歌曲会按照你选择的播放顺序进行播放。
|
1. 默认情况下,播放 **「搜索」「歌单」** 双击中的歌曲时,会自动将该歌曲添加到 **「列表」** 中的末尾后并不会播放,这与手动将歌曲添加到 **「列表」** 等价,亦或是通过点击歌曲前面小三角播放<img src="./assets/image-20250916132248046.png" alt="image-20250916132248046" style="float:right" />可以添加到歌曲开头也可进行该歌曲添加到 **「列表」** 中的开头并播放。歌曲会按照你选择的播放顺序进行播放。
|
||||||
2. 对于 **「列表」** 歌曲的顺序可通过展开长按 **1.5s** 后可以进行拖拽排序,歌曲排序实时保存到本地 **LocalStorage**<img src="./assets/image-20250916133531421.png" alt="image-20250916133531421" style="zoom: 20%;" />
|
2. 对于 **「列表」** 歌曲的顺序可通过展开长按 **1.5s** 后可以进行拖拽排序,歌曲排序实时保存到本地 **LocalStorage**<img src="./assets/image-20250916133531421.png" alt="image-20250916133531421" style="zoom: 50%;" />
|
||||||
|
|
||||||
## 歌曲列表的导出和分享
|
## 歌曲列表的导出和分享
|
||||||
|
|
||||||
@@ -23,6 +23,4 @@
|
|||||||
|
|
||||||
歌单将自动选取第一首 **有效封面**[^1] 为歌单
|
歌单将自动选取第一首 **有效封面**[^1] 为歌单
|
||||||
|
|
||||||
|
[^1]: url正确的歌曲封面
|
||||||
|
|
||||||
[^1]: url正确的歌曲封面
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
|
|||||||
|
|
||||||
## 文档与资源
|
## 文档与资源
|
||||||
|
|
||||||
- [产品设计文档](./guide/design):涵盖项目架构、核心功能设计和开发规范(不含任何音乐数据源信息)。
|
- [产品设计文档](#):涵盖项目架构、核心功能设计和开发规范(不含任何音乐数据源信息)。
|
||||||
- [插件开发文档](./guide/CeruMusicPluginDev):仅提供插件开发技术规范,**明确要求插件开发者需遵守数据来源平台的用户协议与版权法**,禁止开发、传播获取非公开数据的插件。
|
- [插件开发文档](./guide/CeruMusicPluginDev):仅提供插件开发技术规范,**明确要求插件开发者需遵守数据来源平台的用户协议与版权法**,禁止开发、传播获取非公开数据的插件。
|
||||||
|
|
||||||
## 开源许可
|
## 开源许可
|
||||||
@@ -132,7 +132,7 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
|
|||||||
欢迎开发者贡献代码与反馈建议,贡献内容需符合以下要求:
|
欢迎开发者贡献代码与反馈建议,贡献内容需符合以下要求:
|
||||||
|
|
||||||
1. 仅涉及播放器框架功能优化、bug 修复、文档完善,不包含任何音乐数据源相关代码。
|
1. 仅涉及播放器框架功能优化、bug 修复、文档完善,不包含任何音乐数据源相关代码。
|
||||||
2. 遵循 [Git 提交规范](./guide/design#git提交规范) 并确保代码符合项目风格指南。
|
2. 遵循 [Git 提交规范](#) 并确保代码符合项目风格指南。
|
||||||
3. 贡献的代码需无第三方版权纠纷,且不违反开源许可协议。
|
3. 贡献的代码需无第三方版权纠纷,且不违反开源许可协议。
|
||||||
|
|
||||||
## 联系方式
|
## 联系方式
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
// 这个文件可以用来测试 NoticeCenter 功能
|
// 这个文件可以用来测试 NoticeCenter 功能
|
||||||
|
|
||||||
const pluginInfo = {
|
const pluginInfo = {
|
||||||
name: "测试通知插件",
|
name: '测试通知插件',
|
||||||
version: "1.0.0",
|
version: '1.0.0',
|
||||||
author: "CeruMusic Team",
|
author: 'CeruMusic Team',
|
||||||
description: "用于测试插件通知功能的示例插件",
|
description: '用于测试插件通知功能的示例插件',
|
||||||
type: "cr"
|
type: 'cr'
|
||||||
}
|
}
|
||||||
|
|
||||||
const sources = [
|
const sources = [
|
||||||
{
|
{
|
||||||
name: "test",
|
name: 'test',
|
||||||
qualities: ["128k", "320k"]
|
qualities: ['128k', '320k']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 模拟音乐URL获取函数
|
// 模拟音乐URL获取函数
|
||||||
async function musicUrl(source, musicInfo, quality) {
|
async function musicUrl(source, musicInfo, quality) {
|
||||||
console.log('测试插件:获取音乐URL')
|
console.log('测试插件:获取音乐URL')
|
||||||
|
|
||||||
// 测试不同类型的通知
|
// 测试不同类型的通知
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 测试信息通知
|
// 测试信息通知
|
||||||
@@ -29,7 +29,7 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
content: '插件正在正常工作'
|
content: '插件正在正常工作'
|
||||||
})
|
})
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 测试警告通知
|
// 测试警告通知
|
||||||
this.cerumusic.NoticeCenter('warning', {
|
this.cerumusic.NoticeCenter('warning', {
|
||||||
@@ -38,7 +38,7 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
content: '请注意某些设置'
|
content: '请注意某些设置'
|
||||||
})
|
})
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 测试成功通知
|
// 测试成功通知
|
||||||
this.cerumusic.NoticeCenter('success', {
|
this.cerumusic.NoticeCenter('success', {
|
||||||
@@ -47,7 +47,7 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
content: '音乐URL获取成功'
|
content: '音乐URL获取成功'
|
||||||
})
|
})
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 测试更新通知
|
// 测试更新通知
|
||||||
this.cerumusic.NoticeCenter('update', {
|
this.cerumusic.NoticeCenter('update', {
|
||||||
@@ -62,7 +62,7 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 4000)
|
}, 4000)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 测试错误通知
|
// 测试错误通知
|
||||||
this.cerumusic.NoticeCenter('error', {
|
this.cerumusic.NoticeCenter('error', {
|
||||||
@@ -71,7 +71,7 @@ async function musicUrl(source, musicInfo, quality) {
|
|||||||
error: '模拟的错误信息'
|
error: '模拟的错误信息'
|
||||||
})
|
})
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
// 返回一个测试URL
|
// 返回一个测试URL
|
||||||
return 'https://example.com/test-music.mp3'
|
return 'https://example.com/test-music.mp3'
|
||||||
}
|
}
|
||||||
@@ -81,4 +81,4 @@ module.exports = {
|
|||||||
pluginInfo,
|
pluginInfo,
|
||||||
sources,
|
sources,
|
||||||
musicUrl
|
musicUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,27 +81,27 @@ this.cerumusic.NoticeCenter('update', {
|
|||||||
|
|
||||||
#### 通用参数 (data 对象)
|
#### 通用参数 (data 对象)
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
| ------- | ------ | ---- | ------------------------------ |
|
||||||
| title | string | 否 | 通知标题,不提供时使用默认标题 |
|
| title | string | 否 | 通知标题,不提供时使用默认标题 |
|
||||||
| message | string | 否 | 通知消息内容 |
|
| message | string | 否 | 通知消息内容 |
|
||||||
| content | string | 否 | 详细内容(与 message 二选一) |
|
| content | string | 否 | 详细内容(与 message 二选一) |
|
||||||
|
|
||||||
#### 更新通知特有参数
|
#### 更新通知特有参数
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
| ----------------------- | ------------ | ---- | ---------------- |
|
||||||
| url | string | 是 | 插件更新下载链接 |
|
| url | string | 是 | 插件更新下载链接 |
|
||||||
| version | string | 否 | 新版本号 |
|
| version | string | 否 | 新版本号 |
|
||||||
| pluginInfo.name | string | 否 | 插件名称 |
|
| pluginInfo.name | string | 否 | 插件名称 |
|
||||||
| pluginInfo.type | 'cr' \| 'lx' | 否 | 插件类型 |
|
| pluginInfo.type | 'cr' \| 'lx' | 否 | 插件类型 |
|
||||||
| pluginInfo.forcedUpdate | boolean | 否 | 是否强制更新 |
|
| pluginInfo.forcedUpdate | boolean | 否 | 是否强制更新 |
|
||||||
|
|
||||||
#### 错误通知特有参数
|
#### 错误通知特有参数
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
| ----- | ------ | ---- | ------------ |
|
||||||
| error | string | 否 | 具体错误信息 |
|
| error | string | 否 | 具体错误信息 |
|
||||||
|
|
||||||
## 实现原理
|
## 实现原理
|
||||||
|
|
||||||
@@ -208,8 +208,9 @@ window.api.on('plugin-notice', (_, notice) => {
|
|||||||
## 更新日志
|
## 更新日志
|
||||||
|
|
||||||
### v1.0.0 (2025-09-20)
|
### v1.0.0 (2025-09-20)
|
||||||
|
|
||||||
- ✨ 初始版本发布
|
- ✨ 初始版本发布
|
||||||
- ✨ 支持 5 种通知类型
|
- ✨ 支持 5 种通知类型
|
||||||
- ✨ 完整的 TypeScript 类型定义
|
- ✨ 完整的 TypeScript 类型定义
|
||||||
- ✨ 响应式设计和深色主题支持
|
- ✨ 响应式设计和深色主题支持
|
||||||
- ✨ 完善的错误处理机制
|
- ✨ 完善的错误处理机制
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ files:
|
|||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
|
- node_modules/ffmpeg-static/**
|
||||||
win:
|
win:
|
||||||
executableName: ceru-music
|
executableName: ceru-music
|
||||||
icon: 'resources/icons/icon.ico'
|
icon: 'resources/icons/icon.ico'
|
||||||
@@ -20,18 +21,16 @@ win:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
- ia32
|
- ia32
|
||||||
# 简化版本信息设置,避免rcedit错误
|
- target: zip
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- ia32
|
||||||
fileAssociations:
|
fileAssociations:
|
||||||
- ext: cerumusic
|
- ext: cerumusic
|
||||||
name: CeruMusic File
|
name: CeruMusic File
|
||||||
description: CeruMusic playlist file
|
description: CeruMusic playlist file
|
||||||
# 如果有证书文件,取消注释以下配置
|
|
||||||
# certificateFile: path/to/certificate.p12
|
|
||||||
# certificatePassword: your-password
|
|
||||||
# 或者使用证书存储
|
|
||||||
# certificateSubjectName: "Your Company Name"
|
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-${version}-${arch}-setup.${ext}
|
artifactName: ${name}-${version}-win-${arch}-setup.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
@@ -43,23 +42,40 @@ nsis:
|
|||||||
mac:
|
mac:
|
||||||
icon: 'resources/icons/icon.icns'
|
icon: 'resources/icons/icon.icns'
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
target:
|
||||||
|
- target: dmg
|
||||||
|
arch:
|
||||||
|
- universal
|
||||||
|
- target: zip
|
||||||
|
arch:
|
||||||
|
- universal
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSDocumentsFolderUsageDescription: 需要访问文档文件夹来保存和打开您创建的文件。
|
- NSDocumentsFolderUsageDescription: 需要访问文档文件夹来保存和打开您创建的文件。
|
||||||
- NSDownloadsFolderUsageDescription: 需要访问下载文件夹来管理您下载的内容。
|
- NSDownloadsFolderUsageDescription: 需要访问下载文件夹来管理您下载的歌曲。
|
||||||
notarize: false
|
notarize: false
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}-${arch}.${ext}
|
||||||
title: ${productName}
|
title: ${productName}
|
||||||
linux:
|
linux:
|
||||||
icon: 'resources/icons'
|
icon: 'resources/icons'
|
||||||
target:
|
target:
|
||||||
- AppImage
|
- target: AppImage
|
||||||
- snap
|
arch:
|
||||||
- deb
|
- x64
|
||||||
|
- target: snap
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- target: deb
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
maintainer: electronjs.org
|
maintainer: electronjs.org
|
||||||
category: Utility
|
category: Utility
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}-linux-${arch}.${ext}
|
||||||
|
snap:
|
||||||
|
artifactName: ${name}-${version}-linux-${arch}.${ext}
|
||||||
|
deb:
|
||||||
|
artifactName: ${name}-${version}-linux-${arch}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: generic
|
provider: generic
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import AutoImport from 'unplugin-auto-import/vite'
|
|||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import { TDesignResolver } from '@tdesign-vue-next/auto-import-resolver'
|
import { TDesignResolver } from '@tdesign-vue-next/auto-import-resolver'
|
||||||
import wasm from 'vite-plugin-wasm'
|
import wasm from 'vite-plugin-wasm'
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
import topLevelAwait from 'vite-plugin-top-level-await'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -37,13 +38,20 @@ export default defineConfig({
|
|||||||
library: 'vue-next'
|
library: 'vue-next'
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
{
|
||||||
|
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']
|
||||||
|
}
|
||||||
|
],
|
||||||
dts: true
|
dts: true
|
||||||
}),
|
}),
|
||||||
Components({
|
Components({
|
||||||
resolvers: [
|
resolvers: [
|
||||||
TDesignResolver({
|
TDesignResolver({
|
||||||
library: 'vue-next'
|
library: 'vue-next'
|
||||||
})
|
}),
|
||||||
|
NaiveUiResolver()
|
||||||
],
|
],
|
||||||
dts: true
|
dts: true
|
||||||
})
|
})
|
||||||
|
|||||||
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ceru-music",
|
"name": "ceru-music",
|
||||||
"version": "1.3.4",
|
"version": "1.4.1",
|
||||||
"description": "一款简洁优雅的音乐播放器",
|
"description": "一款简洁优雅的音乐播放器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "sqj,wldss,star",
|
"author": "sqj,wldss,star",
|
||||||
@@ -18,9 +18,12 @@
|
|||||||
"onlybuild": "electron-vite build && electron-builder --win --x64",
|
"onlybuild": "electron-vite build && electron-builder --win --x64",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:unpack": "yarn run build && electron-builder --dir",
|
"build:unpack": "yarn run build && electron-builder --dir",
|
||||||
"build:win": "yarn run build && electron-builder --win --config --publish never",
|
"build:win": "yarn run build && electron-builder --win --x64 --config --publish never",
|
||||||
"build:win32": "yarn run build && electron-builder --win --ia32 --config --publish never",
|
"build:win32": "yarn run build && electron-builder --win --ia32 --config --publish never",
|
||||||
"build:mac": "yarn run build && electron-builder --mac --config --publish never",
|
"build:mac": "yarn run build && electron-builder --mac --config --publish never",
|
||||||
|
"build:mac:intel": "yarn run build && electron-builder --mac --x64 --config --publish never",
|
||||||
|
"build:mac:arm64": "yarn run build && electron-builder --mac --arm64 --config --publish never",
|
||||||
|
"build:mac:universal": "yarn run build && electron-builder --mac --universal --config --publish never",
|
||||||
"build:linux": "yarn run build && electron-builder --linux --config --publish never",
|
"build:linux": "yarn run build && electron-builder --linux --config --publish never",
|
||||||
"build:deps": "electron-builder install-app-deps && yarn run build && electron-builder --win --x64 --config",
|
"build:deps": "electron-builder install-app-deps && yarn run build && electron-builder --win --x64 --config",
|
||||||
"buildico": "electron-icon-builder --input=./resources/logo.png --output=resources --flatten",
|
"buildico": "electron-icon-builder --input=./resources/logo.png --output=resources --flatten",
|
||||||
@@ -44,6 +47,7 @@
|
|||||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||||
"@pixi/filter-color-matrix": "^7.4.3",
|
"@pixi/filter-color-matrix": "^7.4.3",
|
||||||
"@pixi/sprite": "^7.4.3",
|
"@pixi/sprite": "^7.4.3",
|
||||||
|
"@types/fluent-ffmpeg": "^2.1.27",
|
||||||
"@types/needle": "^3.3.0",
|
"@types/needle": "^3.3.0",
|
||||||
"NeteaseCloudMusicApi": "^4.27.0",
|
"NeteaseCloudMusicApi": "^4.27.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
@@ -53,6 +57,8 @@
|
|||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
|
"ffmpeg-static": "^5.2.0",
|
||||||
|
"fluent-ffmpeg": "^2.1.3",
|
||||||
"hpagent": "^1.2.0",
|
"hpagent": "^1.2.0",
|
||||||
"iconv-lite": "^0.7.0",
|
"iconv-lite": "^0.7.0",
|
||||||
"jss": "^10.10.0",
|
"jss": "^10.10.0",
|
||||||
@@ -63,7 +69,10 @@
|
|||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"needle": "^3.3.1",
|
"needle": "^3.3.1",
|
||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
|
"node-id3": "^0.2.9",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
|
"tdesign-icons-vue-next": "^0.4.1",
|
||||||
"tdesign-vue-next": "^1.15.2",
|
"tdesign-vue-next": "^1.15.2",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
@@ -80,12 +89,14 @@
|
|||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@types/node-fetch": "^2.6.13",
|
"@types/node-fetch": "^2.6.13",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"@vueuse/core": "^13.9.0",
|
||||||
"electron": "^38.1.0",
|
"electron": "^38.1.0",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-vite": "^4.0.0",
|
"electron-vite": "^4.0.0",
|
||||||
"eslint": "^9.31.0",
|
"eslint": "^9.31.0",
|
||||||
"eslint-plugin-vue": "^10.3.0",
|
"eslint-plugin-vue": "^10.3.0",
|
||||||
|
"naive-ui": "^2.43.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"sass-embedded": "^1.90.0",
|
"sass-embedded": "^1.90.0",
|
||||||
"scss": "^0.2.4",
|
"scss": "^0.2.4",
|
||||||
|
|||||||
9584
qodana.sarif.json
@@ -1,3 +1,3 @@
|
|||||||
version: "1.0"
|
version: '1.0'
|
||||||
profile:
|
profile:
|
||||||
name: qodana.starter
|
name: qodana.starter
|
||||||
|
|||||||
@@ -1,55 +1,79 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs')
|
||||||
const path = require('path');
|
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)
|
||||||
|
|
||||||
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)) {
|
if (
|
||||||
return;
|
basename.startsWith('.') &&
|
||||||
|
basename !== '.' &&
|
||||||
|
basename !== '..' &&
|
||||||
|
!['.github', '.workflow'].includes(basename)
|
||||||
|
) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (excludeDirs.includes(basename)) {
|
if (excludeDirs.includes(basename)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前项目显示
|
// 当前项目显示
|
||||||
if (prefix === '') {
|
if (prefix === '') {
|
||||||
console.log(`${basename}/`);
|
console.log(`${basename}/`)
|
||||||
} else {
|
} else {
|
||||||
const connector = isLast ? '└── ' : '├── ';
|
const connector = isLast ? '└── ' : '├── '
|
||||||
const displayName = fs.statSync(dir).isDirectory() ? `${basename}/` : basename;
|
const displayName = fs.statSync(dir).isDirectory() ? `${basename}/` : basename
|
||||||
console.log(prefix + connector + displayName);
|
console.log(prefix + connector + displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.statSync(dir).isDirectory()) {
|
if (!fs.statSync(dir).isDirectory()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const items = fs.readdirSync(dir)
|
const items = fs
|
||||||
.filter(item => !item.startsWith('.') || ['.github', '.workflow'].includes(item))
|
.readdirSync(dir)
|
||||||
.filter(item => !excludeDirs.includes(item))
|
.filter((item) => !item.startsWith('.') || ['.github', '.workflow'].includes(item))
|
||||||
|
.filter((item) => !excludeDirs.includes(item))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
// 目录排在前面,文件排在后面
|
// 目录排在前面,文件排在后面
|
||||||
const aIsDir = fs.statSync(path.join(dir, a)).isDirectory();
|
const aIsDir = fs.statSync(path.join(dir, a)).isDirectory()
|
||||||
const bIsDir = fs.statSync(path.join(dir, b)).isDirectory();
|
const bIsDir = fs.statSync(path.join(dir, b)).isDirectory()
|
||||||
if (aIsDir && !bIsDir) return -1;
|
if (aIsDir && !bIsDir) return -1
|
||||||
if (!aIsDir && bIsDir) return 1;
|
if (!aIsDir && bIsDir) return 1
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b)
|
||||||
});
|
})
|
||||||
|
|
||||||
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
const newPrefix = prefix + (isLast ? ' ' : '│ ')
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
const isLastItem = index === items.length - 1;
|
const isLastItem = index === items.length - 1
|
||||||
generateTree(path.join(dir, item), newPrefix, isLastItem, excludeDirs);
|
generateTree(path.join(dir, item), newPrefix, isLastItem, excludeDirs)
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error reading directory: ${dir}`, error.message);
|
console.error(`Error reading directory: ${dir}`, error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用示例
|
// 使用示例
|
||||||
const targetDir = process.argv[2] || '.';
|
const targetDir = process.argv[2] || '.'
|
||||||
console.log('项目文件结构:');
|
console.log('项目文件结构:')
|
||||||
generateTree(targetDir);
|
generateTree(targetDir)
|
||||||
|
|||||||
@@ -1,61 +1,19 @@
|
|||||||
import { ipcMain, dialog, app } from 'electron'
|
import { ipcMain, dialog } from 'electron'
|
||||||
import { join } from 'path'
|
import { configManager } from '../services/ConfigManager'
|
||||||
import fs from 'fs'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
|
|
||||||
const mkdir = promisify(fs.mkdir)
|
|
||||||
const access = promisify(fs.access)
|
|
||||||
|
|
||||||
export const CONFIG_NAME = 'sqj_config.json'
|
|
||||||
|
|
||||||
// 默认目录配置
|
|
||||||
const getDefaultDirectories = () => {
|
|
||||||
const userDataPath = app.getPath('userData')
|
|
||||||
return {
|
|
||||||
cacheDir: join(userDataPath, 'music-cache'),
|
|
||||||
downloadDir: join(app.getPath('music'), 'CeruMusic/songs')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保目录存在
|
|
||||||
const ensureDirectoryExists = async (dirPath: string) => {
|
|
||||||
try {
|
|
||||||
await access(dirPath)
|
|
||||||
} catch {
|
|
||||||
await mkdir(dirPath, { recursive: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前目录配置
|
// 获取当前目录配置
|
||||||
ipcMain.handle('directory-settings:get-directories', async () => {
|
ipcMain.handle('directory-settings:get-directories', async () => {
|
||||||
try {
|
try {
|
||||||
const defaults = getDefaultDirectories()
|
const directories = configManager.getDirectories()
|
||||||
|
|
||||||
// 从配置文件读取用户设置的目录
|
|
||||||
const configPath = join(app.getPath('userData'), CONFIG_NAME)
|
|
||||||
let userConfig: any = {}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const configData = fs.readFileSync(configPath, 'utf-8')
|
|
||||||
userConfig = JSON.parse(configData)
|
|
||||||
} catch {
|
|
||||||
// 配置文件不存在或读取失败,使用默认配置
|
|
||||||
}
|
|
||||||
|
|
||||||
const directories = {
|
|
||||||
cacheDir: userConfig.cacheDir || defaults.cacheDir,
|
|
||||||
downloadDir: userConfig.downloadDir || defaults.downloadDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保目录存在
|
// 确保目录存在
|
||||||
await ensureDirectoryExists(directories.cacheDir)
|
await configManager.ensureDirectoryExists(directories.cacheDir)
|
||||||
await ensureDirectoryExists(directories.downloadDir)
|
await configManager.ensureDirectoryExists(directories.downloadDir)
|
||||||
|
|
||||||
return directories
|
return directories
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取目录配置失败:', error)
|
console.error('获取目录配置失败:', error)
|
||||||
const defaults = getDefaultDirectories()
|
return configManager.getDirectories() // 返回默认配置
|
||||||
return defaults
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -70,7 +28,7 @@ ipcMain.handle('directory-settings:select-cache-dir', async () => {
|
|||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
const selectedPath = result.filePaths[0]
|
const selectedPath = result.filePaths[0]
|
||||||
await ensureDirectoryExists(selectedPath)
|
await configManager.ensureDirectoryExists(selectedPath)
|
||||||
return { success: true, path: selectedPath }
|
return { success: true, path: selectedPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +50,7 @@ ipcMain.handle('directory-settings:select-download-dir', async () => {
|
|||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
const selectedPath = result.filePaths[0]
|
const selectedPath = result.filePaths[0]
|
||||||
await ensureDirectoryExists(selectedPath)
|
await configManager.ensureDirectoryExists(selectedPath)
|
||||||
return { success: true, path: selectedPath }
|
return { success: true, path: selectedPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,16 +64,8 @@ ipcMain.handle('directory-settings:select-download-dir', async () => {
|
|||||||
// 保存目录配置
|
// 保存目录配置
|
||||||
ipcMain.handle('directory-settings:save-directories', async (_, directories) => {
|
ipcMain.handle('directory-settings:save-directories', async (_, directories) => {
|
||||||
try {
|
try {
|
||||||
const configPath = join(app.getPath('userData'), CONFIG_NAME)
|
const success = await configManager.saveDirectories(directories)
|
||||||
|
return { success, message: success ? '目录配置已保存' : '保存配置失败' }
|
||||||
// 确保目录存在
|
|
||||||
await ensureDirectoryExists(directories.cacheDir)
|
|
||||||
await ensureDirectoryExists(directories.downloadDir)
|
|
||||||
|
|
||||||
// 保存配置
|
|
||||||
fs.writeFileSync(configPath, JSON.stringify(directories, null, 2))
|
|
||||||
|
|
||||||
return { success: true, message: '目录配置已保存' }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存目录配置失败:', error)
|
console.error('保存目录配置失败:', error)
|
||||||
return { success: false, message: '保存配置失败' }
|
return { success: false, message: '保存配置失败' }
|
||||||
@@ -125,21 +75,19 @@ ipcMain.handle('directory-settings:save-directories', async (_, directories) =>
|
|||||||
// 重置为默认目录
|
// 重置为默认目录
|
||||||
ipcMain.handle('directory-settings:reset-directories', async () => {
|
ipcMain.handle('directory-settings:reset-directories', async () => {
|
||||||
try {
|
try {
|
||||||
const defaults = getDefaultDirectories()
|
// 重置目录配置
|
||||||
const configPath = join(app.getPath('userData'), CONFIG_NAME)
|
configManager.delete('cacheDir')
|
||||||
|
configManager.delete('downloadDir')
|
||||||
|
configManager.saveConfig()
|
||||||
|
|
||||||
// 删除配置文件
|
// 获取默认目录
|
||||||
try {
|
const directories = configManager.getDirectories()
|
||||||
fs.unlinkSync(configPath)
|
|
||||||
} catch {
|
|
||||||
// 文件不存在,忽略错误
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保默认目录存在
|
// 确保默认目录存在
|
||||||
await ensureDirectoryExists(defaults.cacheDir)
|
await configManager.ensureDirectoryExists(directories.cacheDir)
|
||||||
await ensureDirectoryExists(defaults.downloadDir)
|
await configManager.ensureDirectoryExists(directories.downloadDir)
|
||||||
|
|
||||||
return { success: true, directories: defaults }
|
return { success: true, directories }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重置目录配置失败:', error)
|
console.error('重置目录配置失败:', error)
|
||||||
return { success: false, message: '重置配置失败' }
|
return { success: false, message: '重置配置失败' }
|
||||||
@@ -161,6 +109,9 @@ ipcMain.handle('directory-settings:open-directory', async (_, dirPath) => {
|
|||||||
// 获取目录大小
|
// 获取目录大小
|
||||||
ipcMain.handle('directory-settings:get-directory-size', async (_, dirPath) => {
|
ipcMain.handle('directory-settings:get-directory-size', async (_, dirPath) => {
|
||||||
try {
|
try {
|
||||||
|
const fs = require('fs')
|
||||||
|
const { join } = require('path')
|
||||||
|
|
||||||
const getDirectorySize = (dirPath: string): number => {
|
const getDirectorySize = (dirPath: string): number => {
|
||||||
let totalSize = 0
|
let totalSize = 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { app, shell, BrowserWindow, ipcMain, Tray, Menu } from 'electron'
|
import { app, shell, BrowserWindow, ipcMain, Tray, Menu, screen, powerSaveBlocker } from 'electron'
|
||||||
|
import { configManager } from './services/ConfigManager'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import icon from '../../resources/logo.png?asset'
|
import icon from '../../resources/logo.png?asset'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import musicService from './services/music'
|
|
||||||
import pluginService from './services/plugin'
|
import pluginService from './services/plugin'
|
||||||
import aiEvents from './events/ai'
|
import aiEvents from './events/ai'
|
||||||
import './services/musicSdk/index'
|
import './services/musicSdk/index'
|
||||||
@@ -34,6 +34,7 @@ if (!gotTheLock) {
|
|||||||
// console.log(res)
|
// console.log(res)
|
||||||
// })
|
// })
|
||||||
let tray: Tray | null = null
|
let tray: Tray | null = null
|
||||||
|
let psbId: number | null = null
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let isQuitting = false
|
let isQuitting = false
|
||||||
|
|
||||||
@@ -89,20 +90,27 @@ function createTray(): void {
|
|||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// return
|
// return
|
||||||
// Create the browser window.
|
// 获取保存的窗口位置和大小
|
||||||
mainWindow = new BrowserWindow({
|
const savedBounds = configManager.getWindowBounds()
|
||||||
|
|
||||||
|
// 获取屏幕尺寸
|
||||||
|
const primaryDisplay = screen.getPrimaryDisplay()
|
||||||
|
// 使用完整屏幕尺寸而不是工作区域,以支持真正的全屏模式
|
||||||
|
const { width: screenWidth, height: screenHeight } = primaryDisplay.size
|
||||||
|
|
||||||
|
// 默认窗口配置
|
||||||
|
const defaultOptions = {
|
||||||
width: 1100,
|
width: 1100,
|
||||||
height: 750,
|
height: 750,
|
||||||
minWidth: 1100,
|
minWidth: 1100,
|
||||||
minHeight: 670,
|
minHeight: 670,
|
||||||
|
maxWidth: screenWidth,
|
||||||
|
maxHeight: screenHeight,
|
||||||
show: false,
|
show: false,
|
||||||
center: true,
|
center: !savedBounds, // 如果有保存的位置,则不居中
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
// alwaysOnTop: true,
|
titleBarStyle: 'hidden' as const,
|
||||||
// 移除最大宽高限制,以便全屏模式能够铺满整个屏幕
|
|
||||||
titleBarStyle: 'hidden',
|
|
||||||
...(process.platform === 'linux' ? { icon } : {}),
|
...(process.platform === 'linux' ? { icon } : {}),
|
||||||
// ...(process.platform !== 'darwin' ? { titleBarOverlay: true } : {}),
|
|
||||||
icon: path.join(__dirname, '../../resources/logo.ico'),
|
icon: path.join(__dirname, '../../resources/logo.ico'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
@@ -112,9 +120,57 @@ function createWindow(): void {
|
|||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
backgroundThrottling: false
|
backgroundThrottling: false
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// 如果有保存的窗口位置和大小,则使用保存的值
|
||||||
|
if (savedBounds) {
|
||||||
|
Object.assign(defaultOptions, savedBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the browser window.
|
||||||
|
mainWindow = new BrowserWindow(defaultOptions)
|
||||||
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
|
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
|
||||||
|
|
||||||
|
// 监听窗口移动和调整大小事件,保存窗口位置和大小
|
||||||
|
mainWindow.on('moved', () => {
|
||||||
|
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
||||||
|
const bounds = mainWindow.getBounds()
|
||||||
|
configManager.saveWindowBounds(bounds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('resized', () => {
|
||||||
|
if (mainWindow && !mainWindow.isMaximized() && !mainWindow.isFullScreen()) {
|
||||||
|
const bounds = mainWindow.getBounds()
|
||||||
|
|
||||||
|
// 获取当前屏幕尺寸
|
||||||
|
const { screen } = require('electron')
|
||||||
|
const currentDisplay = screen.getDisplayMatching(bounds)
|
||||||
|
const { width: screenWidth, height: screenHeight } = currentDisplay.workAreaSize
|
||||||
|
|
||||||
|
// 确保窗口不超过屏幕尺寸
|
||||||
|
let needResize = false
|
||||||
|
const newBounds = { ...bounds }
|
||||||
|
|
||||||
|
if (bounds.width > screenWidth) {
|
||||||
|
newBounds.width = screenWidth
|
||||||
|
needResize = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bounds.height > screenHeight) {
|
||||||
|
newBounds.height = screenHeight
|
||||||
|
needResize = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要调整大小,应用新的尺寸
|
||||||
|
if (needResize) {
|
||||||
|
mainWindow.setBounds(newBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
configManager.saveWindowBounds(newBounds)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
mainWindow?.show()
|
mainWindow?.show()
|
||||||
})
|
})
|
||||||
@@ -159,6 +215,15 @@ ipcMain.handle('service-plugin-selectAndAddPlugin', async (_, type): Promise<any
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('service-plugin-downloadAndAddPlugin', async (_, url, type): Promise<any> => {
|
||||||
|
try {
|
||||||
|
return await pluginService.downloadAndAddPlugin(url, type)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error downloading and adding plugin:', error)
|
||||||
|
return { error: error.message }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('service-plugin-addPlugin', async (_, pluginCode, pluginName): Promise<any> => {
|
ipcMain.handle('service-plugin-addPlugin', async (_, pluginCode, pluginName): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
return await pluginService.addPlugin(pluginCode, pluginName)
|
return await pluginService.addPlugin(pluginCode, pluginName)
|
||||||
@@ -205,10 +270,6 @@ ipcMain.handle('service-plugin-uninstallPlugin', async (_, pluginId): Promise<an
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('service-music-request', async (_, api, args) => {
|
|
||||||
return await musicService.request(api, args)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取应用版本号
|
// 获取应用版本号
|
||||||
ipcMain.handle('get-app-version', () => {
|
ipcMain.handle('get-app-version', () => {
|
||||||
return app.getVersion()
|
return app.getVersion()
|
||||||
@@ -307,6 +368,22 @@ app.whenReady().then(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 阻止系统息屏 IPC(开启/关闭)
|
||||||
|
ipcMain.handle('power-save-blocker:start', () => {
|
||||||
|
if (psbId == null) {
|
||||||
|
psbId = powerSaveBlocker.start('prevent-display-sleep')
|
||||||
|
}
|
||||||
|
return psbId
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('power-save-blocker:stop', () => {
|
||||||
|
if (psbId != null && powerSaveBlocker.isStarted(psbId)) {
|
||||||
|
powerSaveBlocker.stop(psbId)
|
||||||
|
}
|
||||||
|
psbId = null
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
createWindow()
|
createWindow()
|
||||||
createTray()
|
createTray()
|
||||||
|
|
||||||
|
|||||||
162
src/main/services/ConfigManager.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { app } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
const mkdir = promisify(fs.mkdir)
|
||||||
|
const access = promisify(fs.access)
|
||||||
|
|
||||||
|
export const CONFIG_NAME = 'sqj_config.json'
|
||||||
|
|
||||||
|
// 配置管理器类
|
||||||
|
export class ConfigManager {
|
||||||
|
private static instance: ConfigManager
|
||||||
|
private configPath: string
|
||||||
|
private config: Record<string, any> = {}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.configPath = join(app.getPath('userData'), CONFIG_NAME)
|
||||||
|
this.loadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单例模式获取实例
|
||||||
|
public static getInstance(): ConfigManager {
|
||||||
|
if (!ConfigManager.instance) {
|
||||||
|
ConfigManager.instance = new ConfigManager()
|
||||||
|
}
|
||||||
|
return ConfigManager.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
private loadConfig(): void {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(this.configPath)) {
|
||||||
|
const configData = fs.readFileSync(this.configPath, 'utf-8')
|
||||||
|
this.config = JSON.parse(configData)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置失败:', error)
|
||||||
|
this.config = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
public saveConfig(): boolean {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2))
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存配置失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置项
|
||||||
|
public get<T>(key: string, defaultValue?: T): T {
|
||||||
|
const value = this.config[key]
|
||||||
|
return value !== undefined ? value : (defaultValue as T)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置配置项
|
||||||
|
public set<T>(key: string, value: T): void {
|
||||||
|
this.config[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除配置项
|
||||||
|
public delete(key: string): void {
|
||||||
|
delete this.config[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置所有配置
|
||||||
|
public reset(): void {
|
||||||
|
this.config = {}
|
||||||
|
this.saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有配置
|
||||||
|
public getAll(): Record<string, any> {
|
||||||
|
return { ...this.config }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
public async ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await access(dirPath)
|
||||||
|
} catch {
|
||||||
|
await mkdir(dirPath, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取目录配置
|
||||||
|
public getDirectories() {
|
||||||
|
const userDataPath = app.getPath('userData')
|
||||||
|
const defaults = {
|
||||||
|
cacheDir: join(userDataPath, 'music-cache'),
|
||||||
|
downloadDir: join(app.getPath('music'), 'CeruMusic/songs')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cacheDir: this.get('cacheDir', defaults.cacheDir),
|
||||||
|
downloadDir: this.get('downloadDir', defaults.downloadDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存目录配置
|
||||||
|
public async saveDirectories(directories: {
|
||||||
|
cacheDir: string
|
||||||
|
downloadDir: string
|
||||||
|
}): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.ensureDirectoryExists(directories.cacheDir)
|
||||||
|
await this.ensureDirectoryExists(directories.downloadDir)
|
||||||
|
|
||||||
|
this.set('cacheDir', directories.cacheDir)
|
||||||
|
this.set('downloadDir', directories.downloadDir)
|
||||||
|
return this.saveConfig()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存目录配置失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存窗口位置和大小
|
||||||
|
public saveWindowBounds(bounds: { x: number; y: number; width: number; height: number }): void {
|
||||||
|
this.set('windowBounds', bounds)
|
||||||
|
this.saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取窗口位置和大小,确保窗口完全在屏幕内
|
||||||
|
public getWindowBounds(): { x: number; y: number; width: number; height: number } | null {
|
||||||
|
const bounds = this.get<{ x: number; y: number; width: number; height: number } | null>(
|
||||||
|
'windowBounds',
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (bounds) {
|
||||||
|
const { screen } = require('electron')
|
||||||
|
|
||||||
|
// 获取主显示器
|
||||||
|
const primaryDisplay = screen.getPrimaryDisplay()
|
||||||
|
const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize
|
||||||
|
|
||||||
|
// 确保窗口在屏幕内
|
||||||
|
if (bounds.x < 0) bounds.x = 0
|
||||||
|
if (bounds.y < 0) bounds.y = 0
|
||||||
|
|
||||||
|
// 确保窗口右侧不超出屏幕
|
||||||
|
if (bounds.x + bounds.width > screenWidth) {
|
||||||
|
bounds.x = Math.max(0, screenWidth - bounds.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保窗口底部不超出屏幕
|
||||||
|
if (bounds.y + bounds.height > screenHeight) {
|
||||||
|
bounds.y = Math.max(0, screenHeight - bounds.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const configManager = ConfigManager.getInstance()
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { MusicServiceBase, ServiceNamesType, ServiceArgsType } from './service-base'
|
|
||||||
import {
|
|
||||||
GetToplistArgs,
|
|
||||||
SearchArgs,
|
|
||||||
GetLyricArgs,
|
|
||||||
GetSongDetailArgs,
|
|
||||||
GetSongUrlArgs,
|
|
||||||
GetToplistDetailArgs,
|
|
||||||
GetListSongsArgs,
|
|
||||||
DownloadSingleSongArgs
|
|
||||||
} from './service-base'
|
|
||||||
import { netEaseService } from './net-ease-service'
|
|
||||||
import { AxiosError } from 'axios'
|
|
||||||
|
|
||||||
const musicService: MusicServiceBase = netEaseService
|
|
||||||
|
|
||||||
type Response = {
|
|
||||||
success: boolean
|
|
||||||
data?: any
|
|
||||||
error?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
async function request(api: ServiceNamesType, args: ServiceArgsType): Promise<any> {
|
|
||||||
const res: Response = { success: false }
|
|
||||||
try {
|
|
||||||
switch (api) {
|
|
||||||
case 'search':
|
|
||||||
res.data = await musicService.search(args as SearchArgs)
|
|
||||||
break
|
|
||||||
case 'getSongDetail':
|
|
||||||
res.data = await musicService.getSongDetail(args as GetSongDetailArgs)
|
|
||||||
break
|
|
||||||
case 'getSongUrl':
|
|
||||||
res.data = await musicService.getSongUrl(args as GetSongUrlArgs)
|
|
||||||
break
|
|
||||||
case 'getLyric':
|
|
||||||
res.data = await musicService.getLyric(args as GetLyricArgs)
|
|
||||||
break
|
|
||||||
case 'getToplist':
|
|
||||||
res.data = await musicService.getToplist(args as GetToplistArgs)
|
|
||||||
break
|
|
||||||
case 'getToplistDetail':
|
|
||||||
res.data = await musicService.getToplistDetail(args as GetToplistDetailArgs)
|
|
||||||
break
|
|
||||||
case 'getListSongs':
|
|
||||||
res.data = await musicService.getListSongs(args as GetListSongsArgs)
|
|
||||||
break
|
|
||||||
case 'downloadSingleSong':
|
|
||||||
res.data = await musicService.downloadSingleSong(args as DownloadSingleSongArgs)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
throw new Error(`未知的方法: ${api}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.success = true
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
error.message = '网络错误'
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('请求失败: ', error)
|
|
||||||
res.error = error
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
export default { request }
|
|
||||||
// netEaseService
|
|
||||||
// .search({
|
|
||||||
// keyword: '稻香',
|
|
||||||
// type: 1,
|
|
||||||
// limit: 25
|
|
||||||
// })
|
|
||||||
// .then((res) => {
|
|
||||||
// console.log(res)
|
|
||||||
// })
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import fsPromise from 'fs/promises'
|
|
||||||
|
|
||||||
import NeteaseCloudMusicApi from 'NeteaseCloudMusicApi'
|
|
||||||
import { axiosClient, MusicServiceBase } from './service-base'
|
|
||||||
import { pipeline } from 'node:stream/promises'
|
|
||||||
import pluginService from '../plugin'
|
|
||||||
import musicSdk from '../../utils/musicSdk'
|
|
||||||
|
|
||||||
import {
|
|
||||||
SearchArgs,
|
|
||||||
GetSongDetailArgs,
|
|
||||||
GetSongUrlArgs,
|
|
||||||
GetToplistDetailArgs,
|
|
||||||
GetListSongsArgs,
|
|
||||||
GetLyricArgs,
|
|
||||||
GetToplistArgs,
|
|
||||||
DownloadSingleSongArgs
|
|
||||||
} from './service-base'
|
|
||||||
|
|
||||||
import { SongDetailResponse, SongResponse } from './service-base'
|
|
||||||
|
|
||||||
import { fieldsSelector } from '../../utils/object'
|
|
||||||
import { getAppDirPath } from '../../utils/path'
|
|
||||||
|
|
||||||
// 音乐源映射
|
|
||||||
const MUSIC_SOURCES = {
|
|
||||||
kg: 'kg', // 酷狗音乐
|
|
||||||
wy: 'wy', // 网易云音乐
|
|
||||||
tx: 'tx', // QQ音乐
|
|
||||||
kw: 'kw', // 酷我音乐
|
|
||||||
mg: 'mg' // 咪咕音乐
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扩展搜索参数接口
|
|
||||||
interface ExtendedSearchArgs extends SearchArgs {
|
|
||||||
source?: string // 音乐源参数 kg|wy|tx|kw|mg
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扩展歌曲详情参数接口
|
|
||||||
interface ExtendedGetSongDetailArgs extends GetSongDetailArgs {
|
|
||||||
source?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扩展歌词参数接口
|
|
||||||
interface ExtendedGetLyricArgs extends GetLyricArgs {
|
|
||||||
source?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl: string = 'https://music.163.com'
|
|
||||||
const baseTwoUrl: string = 'https://www.lihouse.xyz/coco_widget'
|
|
||||||
|
|
||||||
const fileLock: Record<string, boolean> = {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取支持的音乐源列表
|
|
||||||
*/
|
|
||||||
export const getSupportedSources = () => {
|
|
||||||
return Object.keys(MUSIC_SOURCES).map((key) => ({
|
|
||||||
id: key,
|
|
||||||
name: getSourceName(key),
|
|
||||||
available: !!musicSdk[key]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取音乐源名称
|
|
||||||
*/
|
|
||||||
const getSourceName = (source: string): string => {
|
|
||||||
const sourceNames = {
|
|
||||||
kg: '酷狗音乐',
|
|
||||||
wy: '网易云音乐',
|
|
||||||
tx: 'QQ音乐',
|
|
||||||
kw: '酷我音乐',
|
|
||||||
mg: '咪咕音乐'
|
|
||||||
}
|
|
||||||
return sourceNames[source] || source
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 智能音乐匹配(使用musicSdk的findMusic功能)
|
|
||||||
*/
|
|
||||||
export const findMusic = async (musicInfo: {
|
|
||||||
name: string
|
|
||||||
singer?: string
|
|
||||||
albumName?: string
|
|
||||||
interval?: string
|
|
||||||
source?: string
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
return await musicSdk.findMusic(musicInfo)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('智能音乐匹配失败:', error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const netEaseService: MusicServiceBase = {
|
|
||||||
async search({
|
|
||||||
type,
|
|
||||||
keyword,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
source
|
|
||||||
}: ExtendedSearchArgs): Promise<SongResponse> {
|
|
||||||
// 如果指定了音乐源且不是网易云,使用对应的musicSdk
|
|
||||||
if (source && source !== 'wy' && MUSIC_SOURCES[source]) {
|
|
||||||
try {
|
|
||||||
const sourceModule = musicSdk[source]
|
|
||||||
if (sourceModule && sourceModule.musicSearch) {
|
|
||||||
const page = Math.floor((offset || 0) / (limit || 25)) + 1
|
|
||||||
const result = await sourceModule.musicSearch.search(keyword, page, limit || 25)
|
|
||||||
|
|
||||||
// 转换为统一格式
|
|
||||||
return {
|
|
||||||
songs: result.list || [],
|
|
||||||
songCount: result.total || result.list?.length || 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`不支持的音乐源: ${source}`)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`${source}音乐源搜索失败:`, error)
|
|
||||||
// 如果指定源失败,回退到网易云
|
|
||||||
console.log('回退到网易云音乐搜索')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认使用网易云音乐搜索
|
|
||||||
return await axiosClient
|
|
||||||
.get(`${baseUrl}/api/search/get/web`, {
|
|
||||||
params: {
|
|
||||||
s: keyword,
|
|
||||||
type: type,
|
|
||||||
limit: limit,
|
|
||||||
offset: offset ?? 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
if (data.code !== 200) {
|
|
||||||
console.error(data)
|
|
||||||
throw new Error(data.msg)
|
|
||||||
}
|
|
||||||
return data.result
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async getSongDetail({ ids, source }: ExtendedGetSongDetailArgs): Promise<SongDetailResponse> {
|
|
||||||
// 如果指定了音乐源且不是网易云,使用对应的musicSdk
|
|
||||||
if (source && source !== 'wy' && MUSIC_SOURCES[source]) {
|
|
||||||
try {
|
|
||||||
const sourceModule = musicSdk[source]
|
|
||||||
if (sourceModule && sourceModule.musicInfo) {
|
|
||||||
// 对于多个ID,并行获取详情
|
|
||||||
const promises = ids.map((id) => sourceModule.musicInfo.getMusicInfo(id))
|
|
||||||
const results = await Promise.all(promises)
|
|
||||||
return results.filter((result: any) => result) // 过滤掉失败的结果
|
|
||||||
} else {
|
|
||||||
throw new Error(`不支持的音乐源: ${source}`)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`${source}音乐源获取歌曲详情失败:`, error)
|
|
||||||
// 如果指定源失败,回退到网易云
|
|
||||||
console.log('回退到网易云音乐获取歌曲详情')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认使用网易云音乐
|
|
||||||
return await axiosClient
|
|
||||||
.get(`${baseUrl}/api/song/detail?ids=[${ids.join(',')}]`)
|
|
||||||
.then(({ data }) => {
|
|
||||||
if (data.code !== 200) {
|
|
||||||
console.error(data)
|
|
||||||
throw new Error(data.msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.songs
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async getSongUrl({ id, pluginId, quality, source }: GetSongUrlArgs): Promise<any> {
|
|
||||||
// 如果提供了插件ID、音质和音乐源,则使用插件获取音乐URL
|
|
||||||
if (pluginId && (quality || source)) {
|
|
||||||
try {
|
|
||||||
// 获取插件实例
|
|
||||||
const plugin = pluginService.getPluginById(pluginId)
|
|
||||||
if (!plugin) {
|
|
||||||
throw new Error(`未找到ID为 ${pluginId} 的插件`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备音乐信息对象,确保符合MusicInfo类型要求
|
|
||||||
const musicInfo = {
|
|
||||||
songmid: id as unknown as number,
|
|
||||||
singer: '',
|
|
||||||
name: '',
|
|
||||||
albumName: '',
|
|
||||||
albumId: 0,
|
|
||||||
source: source || 'wy',
|
|
||||||
interval: '',
|
|
||||||
img: '',
|
|
||||||
lrc: null,
|
|
||||||
types: [],
|
|
||||||
_types: {},
|
|
||||||
typeUrl: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用插件的getMusicUrl方法获取音乐URL
|
|
||||||
const url: string = await plugin.getMusicUrl(
|
|
||||||
source || 'wy',
|
|
||||||
musicInfo,
|
|
||||||
quality || 'standard'
|
|
||||||
)
|
|
||||||
|
|
||||||
// 构建返回对象
|
|
||||||
return { url }
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('通过插件获取音乐URL失败:', error)
|
|
||||||
throw new Error(`插件获取音乐URL失败: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有提供插件信息或插件调用失败,则使用默认方法获取
|
|
||||||
return await axiosClient.get(`${baseTwoUrl}/music_resource/id/${id}`).then(({ data }) => {
|
|
||||||
if (!data.status) {
|
|
||||||
throw new Error('歌曲不存在')
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.song_data
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async getLyric({ id, lv, yv, tv, source }: ExtendedGetLyricArgs): Promise<any> {
|
|
||||||
// 如果指定了音乐源且不是网易云,使用对应的musicSdk
|
|
||||||
if (source && source !== 'wy' && MUSIC_SOURCES[source]) {
|
|
||||||
try {
|
|
||||||
const sourceModule = musicSdk[source]
|
|
||||||
if (sourceModule && sourceModule.getLyric) {
|
|
||||||
// 构建歌曲信息对象,不同源可能需要不同的参数
|
|
||||||
const songInfo = { id, songmid: id, hash: id }
|
|
||||||
const result = await sourceModule.getLyric(songInfo)
|
|
||||||
|
|
||||||
// 转换为统一格式
|
|
||||||
return {
|
|
||||||
lrc: { lyric: result.lyric || '' },
|
|
||||||
tlyric: { lyric: result.tlyric || '' },
|
|
||||||
yrc: { lyric: result.yrc || '' }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`不支持的音乐源: ${source}`)
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`${source}音乐源获取歌词失败:`, error)
|
|
||||||
// 如果指定源失败,回退到网易云
|
|
||||||
console.log('回退到网易云音乐获取歌词')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认使用网易云音乐
|
|
||||||
const optionalParams: any = {}
|
|
||||||
if (lv) {
|
|
||||||
optionalParams.lv = -1
|
|
||||||
}
|
|
||||||
if (yv) {
|
|
||||||
optionalParams.yv = -1
|
|
||||||
}
|
|
||||||
if (tv) {
|
|
||||||
optionalParams.tv = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return await axiosClient
|
|
||||||
.get(`${baseUrl}/api/song/lyric`, {
|
|
||||||
params: {
|
|
||||||
id: id,
|
|
||||||
...optionalParams
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
if (data.code !== 200) {
|
|
||||||
console.error(data)
|
|
||||||
throw Error(data.msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredFields = ['lyricUser', 'lrc', 'tlyric', 'yrc']
|
|
||||||
return fieldsSelector(data, requiredFields)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async getToplist({}: GetToplistArgs): Promise<any> {
|
|
||||||
return await NeteaseCloudMusicApi.toplist({})
|
|
||||||
.then(({ body: data }) => {
|
|
||||||
return data.list
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
console.error({
|
|
||||||
code: err.body?.code,
|
|
||||||
msg: err.body?.msg?.message
|
|
||||||
})
|
|
||||||
throw err.body?.msg ?? err
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async getToplistDetail({}: GetToplistDetailArgs): Promise<any> {
|
|
||||||
return await NeteaseCloudMusicApi.toplist_detail({})
|
|
||||||
.then(({ body: data }) => {
|
|
||||||
return data.list
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
console.error({
|
|
||||||
code: err.body?.code,
|
|
||||||
msg: err.body?.msg?.message
|
|
||||||
})
|
|
||||||
throw err.body?.msg ?? err
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async getListSongs(args: GetListSongsArgs): Promise<any> {
|
|
||||||
return await NeteaseCloudMusicApi.playlist_track_all(args)
|
|
||||||
.then(({ body: data }) => {
|
|
||||||
const requiredFields = ['songs', 'privileges']
|
|
||||||
return fieldsSelector(data, requiredFields)
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
console.error({
|
|
||||||
code: err.body?.code,
|
|
||||||
msg: err.body?.msg?.message
|
|
||||||
})
|
|
||||||
throw err.body?.msg ?? err
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async downloadSingleSong({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
artist,
|
|
||||||
pluginId,
|
|
||||||
source,
|
|
||||||
quality
|
|
||||||
}: DownloadSingleSongArgs) {
|
|
||||||
const { url } = await this.getSongUrl({ id, pluginId, source, quality })
|
|
||||||
|
|
||||||
// 从URL中提取文件扩展名,如果没有则默认为mp3
|
|
||||||
const getFileExtension = (url: string): string => {
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url)
|
|
||||||
const pathname = urlObj.pathname
|
|
||||||
const lastDotIndex = pathname.lastIndexOf('.')
|
|
||||||
if (lastDotIndex !== -1 && lastDotIndex < pathname.length - 1) {
|
|
||||||
const extension = pathname.substring(lastDotIndex + 1).toLowerCase()
|
|
||||||
// 验证是否为常见的音频格式
|
|
||||||
const validExtensions = ['mp3', 'flac', 'wav', 'aac', 'm4a', 'ogg', 'wma']
|
|
||||||
if (validExtensions.includes(extension)) {
|
|
||||||
return extension
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('解析URL失败,使用默认扩展名:', error)
|
|
||||||
}
|
|
||||||
return 'mp3' // 默认扩展名
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileExtension = getFileExtension(url)
|
|
||||||
const songPath = path.join(
|
|
||||||
getAppDirPath(),
|
|
||||||
'download',
|
|
||||||
'songs',
|
|
||||||
`${name}-${artist}-${id}.${fileExtension}`
|
|
||||||
.replace(/[/\\:*?"<>|]/g, '')
|
|
||||||
.replace(/^\.+/, '')
|
|
||||||
.replace(/\.+$/, '')
|
|
||||||
.trim()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (fileLock[songPath]) {
|
|
||||||
throw new Error('歌曲正在下载中')
|
|
||||||
} else {
|
|
||||||
fileLock[songPath] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(songPath)) {
|
|
||||||
return {
|
|
||||||
message: '歌曲已存在'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fsPromise.mkdir(path.dirname(songPath), { recursive: true })
|
|
||||||
|
|
||||||
const songDataRes = await axiosClient({
|
|
||||||
method: 'GET',
|
|
||||||
url: url,
|
|
||||||
responseType: 'stream'
|
|
||||||
})
|
|
||||||
|
|
||||||
await pipeline(songDataRes.data, fs.createWriteStream(songPath))
|
|
||||||
} finally {
|
|
||||||
delete fileLock[songPath]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: '下载成功',
|
|
||||||
path: songPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
import axios, { AxiosInstance } from 'axios'
|
|
||||||
|
|
||||||
const timeout: number = 5000
|
|
||||||
|
|
||||||
const mobileHeaders = {
|
|
||||||
'User-Agent':
|
|
||||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148'
|
|
||||||
}
|
|
||||||
|
|
||||||
const axiosClient: AxiosInstance = axios.create({
|
|
||||||
timeout: timeout
|
|
||||||
})
|
|
||||||
|
|
||||||
type SearchArgs = {
|
|
||||||
type: number
|
|
||||||
keyword: string
|
|
||||||
offset?: number
|
|
||||||
limit: number
|
|
||||||
source?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetSongDetailArgs = {
|
|
||||||
ids: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetSongUrlArgs = {
|
|
||||||
id: string
|
|
||||||
pluginId?: string // 插件ID
|
|
||||||
quality?: string // 音质
|
|
||||||
source?: string // 音乐源(wy, tx等)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetLyricArgs = {
|
|
||||||
id: string
|
|
||||||
lv?: boolean
|
|
||||||
yv?: boolean // 获取逐字歌词
|
|
||||||
tv?: boolean // 获取歌词翻译
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetToplistArgs = Record<string, never>
|
|
||||||
|
|
||||||
type GetToplistDetailArgs = Record<string, never>
|
|
||||||
|
|
||||||
type GetListSongsArgs = {
|
|
||||||
id: string
|
|
||||||
limit?: number
|
|
||||||
offset?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadSingleSongArgs = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
artist: string
|
|
||||||
pluginId?: string
|
|
||||||
quality?: string
|
|
||||||
source?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceNamesType =
|
|
||||||
| 'search'
|
|
||||||
| 'getSongDetail'
|
|
||||||
| 'getSongUrl'
|
|
||||||
| 'getLyric'
|
|
||||||
| 'getToplist'
|
|
||||||
| 'getToplistDetail'
|
|
||||||
| 'getListSongs'
|
|
||||||
| 'downloadSingleSong'
|
|
||||||
|
|
||||||
type ServiceArgsType =
|
|
||||||
| SearchArgs
|
|
||||||
| GetSongDetailArgs
|
|
||||||
| GetSongUrlArgs
|
|
||||||
| GetLyricArgs
|
|
||||||
| GetToplistArgs
|
|
||||||
| GetToplistDetailArgs
|
|
||||||
| GetListSongsArgs
|
|
||||||
| DownloadSingleSongArgs
|
|
||||||
|
|
||||||
interface Artist {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
picUrl: string | null
|
|
||||||
alias: string[]
|
|
||||||
albumSize: number
|
|
||||||
picId: number
|
|
||||||
fansGroup: null
|
|
||||||
img1v1Url: string
|
|
||||||
img1v1: number
|
|
||||||
trans: null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Album {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
artist: {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
picUrl: string | null
|
|
||||||
alias: string[]
|
|
||||||
albumSize: number
|
|
||||||
picId: number
|
|
||||||
fansGroup: null
|
|
||||||
img1v1Url: string
|
|
||||||
img1v1: number
|
|
||||||
trans: null
|
|
||||||
}
|
|
||||||
publishTime: number
|
|
||||||
size: number
|
|
||||||
copyrightId: number
|
|
||||||
status: number
|
|
||||||
picId: number
|
|
||||||
alia?: string[]
|
|
||||||
mark: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Song {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
artists: Artist[]
|
|
||||||
album: Album
|
|
||||||
duration: number
|
|
||||||
copyrightId: number
|
|
||||||
status: number
|
|
||||||
alias: string[]
|
|
||||||
rtype: number
|
|
||||||
ftype: number
|
|
||||||
mvid: number
|
|
||||||
fee: number
|
|
||||||
rUrl: null
|
|
||||||
mark: number
|
|
||||||
transNames?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SongResponse {
|
|
||||||
songs: Song[]
|
|
||||||
songCount: number
|
|
||||||
}
|
|
||||||
interface AlbumDetail {
|
|
||||||
name: string
|
|
||||||
id: number
|
|
||||||
type: string
|
|
||||||
size: number
|
|
||||||
picId: number
|
|
||||||
blurPicUrl: string
|
|
||||||
companyId: number
|
|
||||||
pic: number
|
|
||||||
picUrl: string
|
|
||||||
publishTime: number
|
|
||||||
description: string
|
|
||||||
tags: string
|
|
||||||
company: string
|
|
||||||
briefDesc: string
|
|
||||||
artist: {
|
|
||||||
name: string
|
|
||||||
id: number
|
|
||||||
picId: number
|
|
||||||
img1v1Id: number
|
|
||||||
briefDesc: string
|
|
||||||
picUrl: string
|
|
||||||
img1v1Url: string
|
|
||||||
albumSize: number
|
|
||||||
alias: string[]
|
|
||||||
trans: string
|
|
||||||
musicSize: number
|
|
||||||
topicPerson: number
|
|
||||||
}
|
|
||||||
songs: any[]
|
|
||||||
alias: string[]
|
|
||||||
status: number
|
|
||||||
copyrightId: number
|
|
||||||
commentThreadId: string
|
|
||||||
artists: Artist[]
|
|
||||||
subType: string
|
|
||||||
transName: null
|
|
||||||
onSale: boolean
|
|
||||||
mark: number
|
|
||||||
gapless: number
|
|
||||||
dolbyMark: number
|
|
||||||
}
|
|
||||||
interface MusicQuality {
|
|
||||||
name: null
|
|
||||||
id: number
|
|
||||||
size: number
|
|
||||||
extension: string
|
|
||||||
sr: number
|
|
||||||
dfsId: number
|
|
||||||
bitrate: number
|
|
||||||
playTime: number
|
|
||||||
volumeDelta: number
|
|
||||||
}
|
|
||||||
interface SongDetail {
|
|
||||||
name: string
|
|
||||||
id: number
|
|
||||||
position: number
|
|
||||||
alias: string[]
|
|
||||||
status: number
|
|
||||||
fee: number
|
|
||||||
copyrightId: number
|
|
||||||
disc: string
|
|
||||||
no: number
|
|
||||||
artists: Artist[]
|
|
||||||
album: AlbumDetail
|
|
||||||
starred: boolean
|
|
||||||
popularity: number
|
|
||||||
score: number
|
|
||||||
starredNum: number
|
|
||||||
duration: number
|
|
||||||
playedNum: number
|
|
||||||
dayPlays: number
|
|
||||||
hearTime: number
|
|
||||||
sqMusic: MusicQuality
|
|
||||||
hrMusic: null
|
|
||||||
ringtone: null
|
|
||||||
crbt: null
|
|
||||||
audition: null
|
|
||||||
copyFrom: string
|
|
||||||
commentThreadId: string
|
|
||||||
rtUrl: null
|
|
||||||
ftype: number
|
|
||||||
rtUrls: any[]
|
|
||||||
copyright: number
|
|
||||||
transName: null
|
|
||||||
sign: null
|
|
||||||
mark: number
|
|
||||||
originCoverType: number
|
|
||||||
originSongSimpleData: null
|
|
||||||
single: number
|
|
||||||
noCopyrightRcmd: null
|
|
||||||
hMusic: MusicQuality
|
|
||||||
mMusic: MusicQuality
|
|
||||||
lMusic: MusicQuality
|
|
||||||
bMusic: MusicQuality
|
|
||||||
mvid: number
|
|
||||||
mp3Url: null
|
|
||||||
rtype: number
|
|
||||||
rurl: null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SongDetailResponse {
|
|
||||||
songs: SongDetail[]
|
|
||||||
equalizers: Record<string, unknown>
|
|
||||||
code: number
|
|
||||||
}
|
|
||||||
interface SongUrlResponse {
|
|
||||||
id: number
|
|
||||||
url: string // 歌曲地址
|
|
||||||
name: string
|
|
||||||
artist: string
|
|
||||||
pic: string //封面图片
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MusicServiceBase {
|
|
||||||
search({ type, keyword, offset, limit }: SearchArgs): Promise<SongResponse>
|
|
||||||
getSongDetail({ ids }: GetSongDetailArgs): Promise<SongDetailResponse>
|
|
||||||
getSongUrl({ id, pluginId, quality, source }: GetSongUrlArgs): Promise<SongUrlResponse>
|
|
||||||
getLyric({ id, lv, yv, tv }: GetLyricArgs): Promise<any>
|
|
||||||
getToplist({}: GetToplistArgs): Promise<any>
|
|
||||||
getToplistDetail({}: GetToplistDetailArgs): Promise<any>
|
|
||||||
getListSongs({ id, limit, offset }: GetListSongsArgs): Promise<any>
|
|
||||||
downloadSingleSong({ id }: DownloadSingleSongArgs): Promise<any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { MusicServiceBase, ServiceNamesType, ServiceArgsType }
|
|
||||||
export type {
|
|
||||||
SearchArgs,
|
|
||||||
GetSongDetailArgs,
|
|
||||||
GetSongUrlArgs,
|
|
||||||
GetLyricArgs,
|
|
||||||
GetToplistArgs,
|
|
||||||
GetToplistDetailArgs,
|
|
||||||
GetListSongsArgs,
|
|
||||||
DownloadSingleSongArgs
|
|
||||||
}
|
|
||||||
export type { SongResponse, SongDetailResponse, SongUrlResponse }
|
|
||||||
|
|
||||||
export { mobileHeaders, axiosClient }
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { app } from 'electron'
|
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs/promises'
|
import * as fs from 'fs/promises'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { CONFIG_NAME } from '../../events/directorySettings'
|
import { configManager } from '../ConfigManager'
|
||||||
|
|
||||||
export class MusicCacheService {
|
export class MusicCacheService {
|
||||||
private cacheIndex: Map<string, string> = new Map()
|
private cacheIndex: Map<string, string> = new Map()
|
||||||
@@ -13,21 +12,9 @@ export class MusicCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getCacheDirectory(): string {
|
private getCacheDirectory(): string {
|
||||||
try {
|
// 使用配置管理服务获取缓存目录
|
||||||
// 尝试从配置文件读取自定义缓存目录
|
const directories = configManager.getDirectories()
|
||||||
const configPath = path.join(app.getPath('userData'), CONFIG_NAME)
|
return directories.cacheDir
|
||||||
const configData = require('fs').readFileSync(configPath, 'utf-8')
|
|
||||||
const config = JSON.parse(configData)
|
|
||||||
|
|
||||||
if (config.cacheDir && typeof config.cacheDir === 'string') {
|
|
||||||
return config.cacheDir
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 配置文件不存在或读取失败,使用默认目录
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认缓存目录
|
|
||||||
return path.join(app.getPath('userData'), 'music-cache')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态获取缓存目录
|
// 动态获取缓存目录
|
||||||
|
|||||||
@@ -24,3 +24,15 @@ export function request<T extends keyof MainApi>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ipcMain.handle('service-music-sdk-request', request)
|
ipcMain.handle('service-music-sdk-request', request)
|
||||||
|
|
||||||
|
// 处理搜索联想请求
|
||||||
|
ipcMain.handle('service-music-tip-search', async (_, source, keyword) => {
|
||||||
|
try {
|
||||||
|
if (!source) throw new Error('请配置音源')
|
||||||
|
const Api = main(source)
|
||||||
|
return await Api.tipSearch({ keyword })
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('搜索联想错误:', error)
|
||||||
|
return { result: { songs: [], order: ['songs'] } }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -7,21 +7,13 @@ import {
|
|||||||
PlaylistResult,
|
PlaylistResult,
|
||||||
GetSongListDetailsArg,
|
GetSongListDetailsArg,
|
||||||
PlaylistDetailResult,
|
PlaylistDetailResult,
|
||||||
DownloadSingleSongArgs
|
DownloadSingleSongArgs,
|
||||||
|
TipSearchResult
|
||||||
} from './type'
|
} from './type'
|
||||||
import pluginService from '../plugin/index'
|
import pluginService from '../plugin/index'
|
||||||
import musicSdk from '../../utils/musicSdk/index'
|
import musicSdk from '../../utils/musicSdk/index'
|
||||||
import { musicCacheService } from '../musicCache'
|
import { musicCacheService } from '../musicCache'
|
||||||
import path from 'node:path'
|
import download from '../../utils/downloadSongs'
|
||||||
import fs from 'fs'
|
|
||||||
import fsPromise from 'fs/promises'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { pipeline } from 'node:stream/promises'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import { app } from 'electron'
|
|
||||||
import { CONFIG_NAME } from '../../events/directorySettings'
|
|
||||||
|
|
||||||
const fileLock: Record<string, boolean> = {}
|
|
||||||
|
|
||||||
function main(source: string) {
|
function main(source: string) {
|
||||||
const Api = musicSdk[source]
|
const Api = musicSdk[source]
|
||||||
@@ -30,7 +22,15 @@ function main(source: string) {
|
|||||||
return (await Api.musicSearch.search(keyword, page, limit)) as Promise<SearchResult>
|
return (await Api.musicSearch.search(keyword, page, limit)) as Promise<SearchResult>
|
||||||
},
|
},
|
||||||
|
|
||||||
async getMusicUrl({ pluginId, songInfo, quality }: GetMusicUrlArg) {
|
async tipSearch({ keyword }: { keyword: string }) {
|
||||||
|
if (!Api.tipSearch?.search) {
|
||||||
|
// 如果音乐源没有实现tipSearch方法,返回空结果
|
||||||
|
return [] as TipSearchResult
|
||||||
|
}
|
||||||
|
return (await Api.tipSearch.search(keyword)) as Promise<TipSearchResult>
|
||||||
|
},
|
||||||
|
|
||||||
|
async getMusicUrl({ pluginId, songInfo, quality, isCache }: GetMusicUrlArg) {
|
||||||
try {
|
try {
|
||||||
const usePlugin = pluginService.getPluginById(pluginId)
|
const usePlugin = pluginService.getPluginById(pluginId)
|
||||||
if (!pluginId || !usePlugin) return { error: '请配置音源来播放歌曲' }
|
if (!pluginId || !usePlugin) return { error: '请配置音源来播放歌曲' }
|
||||||
@@ -38,18 +38,22 @@ function main(source: string) {
|
|||||||
// 生成歌曲唯一标识
|
// 生成歌曲唯一标识
|
||||||
const songId = `${songInfo.name}-${songInfo.singer}-${source}-${quality}`
|
const songId = `${songInfo.name}-${songInfo.singer}-${source}-${quality}`
|
||||||
|
|
||||||
// 先检查缓存
|
// 先检查缓存(isCache !== false 时)
|
||||||
const cachedUrl = await musicCacheService.getCachedMusicUrl(songId)
|
if (isCache !== false) {
|
||||||
if (cachedUrl) {
|
const cachedUrl = await musicCacheService.getCachedMusicUrl(songId)
|
||||||
return cachedUrl
|
if (cachedUrl) {
|
||||||
|
return cachedUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有缓存时才发起网络请求
|
// 没有缓存时才发起网络请求
|
||||||
const originalUrl = await usePlugin.getMusicUrl(source, songInfo, quality)
|
const originalUrl = await usePlugin.getMusicUrl(source, songInfo, quality)
|
||||||
// 异步缓存,不阻塞返回
|
// 按需异步缓存,不阻塞返回
|
||||||
musicCacheService.cacheMusic(songId, originalUrl).catch((error) => {
|
if (isCache !== false) {
|
||||||
console.warn('缓存歌曲失败:', error)
|
musicCacheService.cacheMusic(songId, originalUrl).catch((error) => {
|
||||||
})
|
console.warn('缓存歌曲失败:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return originalUrl
|
return originalUrl
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -92,97 +96,15 @@ function main(source: string) {
|
|||||||
return (await Api.songList.getListDetail(id, page)) as PlaylistDetailResult
|
return (await Api.songList.getListDetail(id, page)) as PlaylistDetailResult
|
||||||
},
|
},
|
||||||
|
|
||||||
async downloadSingleSong({ pluginId, songInfo, quality }: DownloadSingleSongArgs) {
|
async downloadSingleSong({
|
||||||
|
pluginId,
|
||||||
|
songInfo,
|
||||||
|
quality,
|
||||||
|
tagWriteOptions
|
||||||
|
}: DownloadSingleSongArgs) {
|
||||||
const url = await this.getMusicUrl({ pluginId, songInfo, quality })
|
const url = await this.getMusicUrl({ pluginId, songInfo, quality })
|
||||||
if (typeof url === 'object') throw new Error('无法获取歌曲链接')
|
if (typeof url === 'object') throw new Error('无法获取歌曲链接')
|
||||||
|
return await download(url, songInfo, tagWriteOptions)
|
||||||
// 获取自定义下载目录
|
|
||||||
const getDownloadDirectory = (): string => {
|
|
||||||
try {
|
|
||||||
const configPath = path.join(app.getPath('userData'), CONFIG_NAME)
|
|
||||||
const configData = fs.readFileSync(configPath, 'utf-8')
|
|
||||||
const config = JSON.parse(configData)
|
|
||||||
|
|
||||||
if (config.downloadDir && typeof config.downloadDir === 'string') {
|
|
||||||
return config.downloadDir
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// 配置文件不存在或读取失败,使用默认目录
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认下载目录
|
|
||||||
return path.join(app.getPath('music'), 'CeruMusic/songs')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从URL中提取文件扩展名,如果没有则默认为mp3
|
|
||||||
const getFileExtension = (url: string): string => {
|
|
||||||
try {
|
|
||||||
const urlObj = new URL(url)
|
|
||||||
const pathname = urlObj.pathname
|
|
||||||
const lastDotIndex = pathname.lastIndexOf('.')
|
|
||||||
if (lastDotIndex !== -1 && lastDotIndex < pathname.length - 1) {
|
|
||||||
const extension = pathname.substring(lastDotIndex + 1).toLowerCase()
|
|
||||||
// 验证是否为常见的音频格式
|
|
||||||
const validExtensions = ['mp3', 'flac', 'wav', 'aac', 'm4a', 'ogg', 'wma']
|
|
||||||
if (validExtensions.includes(extension)) {
|
|
||||||
return extension
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('解析URL失败,使用默认扩展名:', error)
|
|
||||||
}
|
|
||||||
return 'mp3' // 默认扩展名
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileExtension = getFileExtension(url)
|
|
||||||
const downloadDir = getDownloadDirectory()
|
|
||||||
const songPath = path.join(
|
|
||||||
downloadDir,
|
|
||||||
`${songInfo.name}-${songInfo.singer}-${songInfo.source}.${fileExtension}`
|
|
||||||
.replace(/[/\\:*?"<>|]/g, '')
|
|
||||||
.replace(/^\.+/, '')
|
|
||||||
.replace(/\.+$/, '')
|
|
||||||
.trim()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (fileLock[songPath]) {
|
|
||||||
throw new Error('歌曲正在下载中')
|
|
||||||
} else {
|
|
||||||
fileLock[songPath] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(songPath)) {
|
|
||||||
return {
|
|
||||||
message: '歌曲已存在'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await fsPromise.mkdir(path.dirname(songPath), { recursive: true })
|
|
||||||
|
|
||||||
if (url.startsWith('file://')) {
|
|
||||||
const filePath = fileURLToPath(url)
|
|
||||||
|
|
||||||
const readStream = fs.createReadStream(filePath)
|
|
||||||
const writeStream = fs.createWriteStream(songPath)
|
|
||||||
|
|
||||||
await pipeline(readStream, writeStream)
|
|
||||||
} else {
|
|
||||||
const songDataRes = await axios({
|
|
||||||
method: 'GET',
|
|
||||||
url: url,
|
|
||||||
responseType: 'stream'
|
|
||||||
})
|
|
||||||
|
|
||||||
await pipeline(songDataRes.data, fs.createWriteStream(songPath))
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
delete fileLock[songPath]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: '下载成功',
|
|
||||||
path: songPath
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async parsePlaylistId({ url }: { url: string }) {
|
async parsePlaylistId({ url }: { url: string }) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface GetMusicUrlArg {
|
|||||||
pluginId: string
|
pluginId: string
|
||||||
songInfo: MusicItem
|
songInfo: MusicItem
|
||||||
quality: string
|
quality: string
|
||||||
|
isCache?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetMusicPicArg {
|
export interface GetMusicPicArg {
|
||||||
@@ -90,6 +91,16 @@ export interface PlaylistDetailResult {
|
|||||||
info: PlaylistInfo
|
info: PlaylistInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TagWriteOptions {
|
||||||
|
basicInfo?: boolean
|
||||||
|
cover?: boolean
|
||||||
|
lyrics?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface DownloadSingleSongArgs extends GetMusicUrlArg {
|
export interface DownloadSingleSongArgs extends GetMusicUrlArg {
|
||||||
path?: string
|
path?: string
|
||||||
|
tagWriteOptions?: TagWriteOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索联想结果的类型定义
|
||||||
|
export type TipSearchResult = string[]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fsPromise from 'fs/promises'
|
|||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { dialog } from 'electron'
|
import { dialog } from 'electron'
|
||||||
import { getAppDirPath } from '../../utils/path'
|
import { getAppDirPath } from '../../utils/path'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
import CeruMusicPluginHost from './manager/CeruMusicPluginHost'
|
import CeruMusicPluginHost from './manager/CeruMusicPluginHost'
|
||||||
import convertEventDrivenPlugin from './manager/converter-event-driven'
|
import convertEventDrivenPlugin from './manager/converter-event-driven'
|
||||||
@@ -223,6 +224,60 @@ const pluginService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async downloadAndAddPlugin(url: string, type: 'lx' | 'cr') {
|
||||||
|
try {
|
||||||
|
// 验证URL
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
throw new Error('无效的URL地址')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
let pluginCode = await this.downloadFile(url)
|
||||||
|
|
||||||
|
// 生成临时文件名
|
||||||
|
const fileName = `downloaded_${Date.now()}.js`
|
||||||
|
if (type == 'lx') {
|
||||||
|
pluginCode = convertEventDrivenPlugin(pluginCode)
|
||||||
|
}
|
||||||
|
// 调用现有的添加插件方法
|
||||||
|
return await this.addPlugin(pluginCode, fileName)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('下载并添加插件失败:', error)
|
||||||
|
return { error: error.message || '下载插件失败' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadFile(url: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
timeout: 30000, // 30秒超时
|
||||||
|
responseType: 'text',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'CeruMusic/1.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`下载失败: HTTP ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data
|
||||||
|
if (!data || !data.trim()) {
|
||||||
|
throw new Error('下载的文件内容为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
throw new Error(`下载失败: HTTP ${error.response.status}`)
|
||||||
|
} else if (error.request) {
|
||||||
|
throw new Error('网络错误: 无法连接到服务器')
|
||||||
|
} else {
|
||||||
|
throw new Error(`下载错误: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async getPluginLog(pluginId: string) {
|
async getPluginLog(pluginId: string) {
|
||||||
return await getLog(pluginId)
|
return await getLog(pluginId)
|
||||||
}
|
}
|
||||||
|
|||||||
508
src/main/utils/downloadSongs.ts
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
import NodeID3 from 'node-id3'
|
||||||
|
import ffmpegStatic from 'ffmpeg-static'
|
||||||
|
import ffmpeg from 'fluent-ffmpeg'
|
||||||
|
import path from 'node:path'
|
||||||
|
import axios from 'axios'
|
||||||
|
import fs from 'fs'
|
||||||
|
import fsPromise from 'fs/promises'
|
||||||
|
import { configManager } from '../services/ConfigManager'
|
||||||
|
import { pipeline } from 'node:stream/promises'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const fileLock: Record<string, boolean> = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换LRC格式
|
||||||
|
* 将带有字符位置信息的LRC格式转换为标准的逐字时间戳格式
|
||||||
|
* @param lrcContent 原始LRC内容
|
||||||
|
* @returns 转换后的LRC内容
|
||||||
|
*/
|
||||||
|
function convertLrcFormat(lrcContent: string): string {
|
||||||
|
if (!lrcContent) return ''
|
||||||
|
|
||||||
|
const lines = lrcContent.split('\n')
|
||||||
|
const convertedLines: string[] = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// 跳过空行
|
||||||
|
if (!line.trim()) {
|
||||||
|
convertedLines.push(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是新格式:[开始时间,持续时间](开始时间,字符持续时间,0)字符
|
||||||
|
const newFormatMatch = line.match(/^\[(\d+),(\d+)\](.*)$/)
|
||||||
|
if (newFormatMatch) {
|
||||||
|
const [, startTimeMs, , content] = newFormatMatch
|
||||||
|
const convertedLine = convertNewFormat(parseInt(startTimeMs), content)
|
||||||
|
convertedLines.push(convertedLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是旧格式:[mm:ss.xxx]字符(偏移,持续时间)
|
||||||
|
const oldFormatMatch = line.match(/^\[(\d{2}:\d{2}\.\d{3})\](.*)$/)
|
||||||
|
if (oldFormatMatch) {
|
||||||
|
const [, timestamp, content] = oldFormatMatch
|
||||||
|
|
||||||
|
// 如果内容中没有位置信息,直接返回原行
|
||||||
|
if (!content.includes('(') || !content.includes(')')) {
|
||||||
|
convertedLines.push(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedLine = convertOldFormat(timestamp, content)
|
||||||
|
convertedLines.push(convertedLine)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他行直接保留
|
||||||
|
convertedLines.push(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedLines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将毫秒时间戳格式化为 mm:ss.xxx 格式
|
||||||
|
* @param timeMs 毫秒时间戳
|
||||||
|
* @returns 格式化的时间字符串
|
||||||
|
*/
|
||||||
|
function formatTimestamp(timeMs: number): string {
|
||||||
|
const minutes = Math.floor(timeMs / 60000)
|
||||||
|
const seconds = Math.floor((timeMs % 60000) / 1000)
|
||||||
|
const milliseconds = timeMs % 1000
|
||||||
|
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换新格式:[开始时间,持续时间](开始时间,字符持续时间,0)字符
|
||||||
|
*/
|
||||||
|
function convertNewFormat(baseTimeMs: number, content: string): string {
|
||||||
|
const baseTimestamp = formatTimestamp(baseTimeMs)
|
||||||
|
let convertedContent = `<${baseTimestamp}>`
|
||||||
|
|
||||||
|
// 匹配模式:(开始时间,字符持续时间,0)字符
|
||||||
|
const charPattern = /\((\d+),(\d+),(\d+)\)([^(]*?)(?=\(|$)/g
|
||||||
|
let match
|
||||||
|
let isFirstChar = true
|
||||||
|
|
||||||
|
while ((match = charPattern.exec(content)) !== null) {
|
||||||
|
const [, charStartMs, , , char] = match
|
||||||
|
const charTimeMs = parseInt(charStartMs)
|
||||||
|
const charTimestamp = formatTimestamp(charTimeMs)
|
||||||
|
|
||||||
|
if (isFirstChar) {
|
||||||
|
// 第一个字符直接添加
|
||||||
|
convertedContent += char.trim()
|
||||||
|
isFirstChar = false
|
||||||
|
} else {
|
||||||
|
convertedContent += `<${charTimestamp}>${char.trim()}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `[${baseTimestamp}]${convertedContent}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换旧格式:[mm:ss.xxx]字符(偏移,持续时间)
|
||||||
|
*/
|
||||||
|
function convertOldFormat(timestamp: string, content: string): string {
|
||||||
|
// 解析基础时间戳(毫秒)
|
||||||
|
const [minutes, seconds] = timestamp.split(':')
|
||||||
|
const [sec, ms] = seconds.split('.')
|
||||||
|
const baseTimeMs = parseInt(minutes) * 60 * 1000 + parseInt(sec) * 1000 + parseInt(ms)
|
||||||
|
|
||||||
|
let convertedContent = `<${timestamp}>`
|
||||||
|
|
||||||
|
// 匹配所有字符(偏移,持续时间)的模式
|
||||||
|
const charPattern = /([^()]+)\((\d+),(\d+)\)/g
|
||||||
|
let match
|
||||||
|
let lastIndex = 0
|
||||||
|
let isFirstChar = true
|
||||||
|
|
||||||
|
while ((match = charPattern.exec(content)) !== null) {
|
||||||
|
const [fullMatch, char, offsetMs, _durationMs] = match
|
||||||
|
const charTimeMs = baseTimeMs + parseInt(offsetMs)
|
||||||
|
const charTimestamp = formatTimestamp(charTimeMs)
|
||||||
|
|
||||||
|
// 添加匹配前的普通文本
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
const beforeText = content.substring(lastIndex, match.index)
|
||||||
|
if (beforeText.trim()) {
|
||||||
|
convertedContent += beforeText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加带时间戳的字符
|
||||||
|
if (isFirstChar) {
|
||||||
|
// 第一个字符直接添加,不需要额外的时间戳
|
||||||
|
convertedContent += char
|
||||||
|
isFirstChar = false
|
||||||
|
} else {
|
||||||
|
convertedContent += `<${charTimestamp}>${char}`
|
||||||
|
}
|
||||||
|
lastIndex = match.index + fullMatch.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加剩余的普通文本
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
const remainingText = content.substring(lastIndex)
|
||||||
|
if (remainingText.trim()) {
|
||||||
|
convertedContent += remainingText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `[${timestamp}]${convertedContent}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入音频标签的辅助函数
|
||||||
|
async function writeAudioTags(filePath: string, songInfo: any, tagWriteOptions: any) {
|
||||||
|
try {
|
||||||
|
console.log('开始写入音频标签:', filePath, tagWriteOptions, songInfo)
|
||||||
|
|
||||||
|
// 获取文件扩展名来判断格式
|
||||||
|
const fileExtension = path.extname(filePath).toLowerCase().substring(1)
|
||||||
|
console.log('文件格式:', fileExtension)
|
||||||
|
|
||||||
|
// 根据文件格式选择不同的标签写入方法
|
||||||
|
if (fileExtension === 'mp3') {
|
||||||
|
await writeMP3Tags(filePath, songInfo, tagWriteOptions)
|
||||||
|
} else if (['flac', 'ogg', 'opus'].includes(fileExtension)) {
|
||||||
|
await writeVorbisCommentTags(filePath, songInfo, tagWriteOptions)
|
||||||
|
} else if (['m4a', 'mp4', 'aac'].includes(fileExtension)) {
|
||||||
|
await writeMP4Tags(filePath, songInfo, tagWriteOptions)
|
||||||
|
} else {
|
||||||
|
console.warn('不支持的音频格式:', fileExtension)
|
||||||
|
// 尝试使用 NodeID3 作为后备方案
|
||||||
|
await writeMP3Tags(filePath, songInfo, tagWriteOptions)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('写入音频标签时发生错误:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MP3 格式标签写入
|
||||||
|
async function writeMP3Tags(filePath: string, songInfo: any, tagWriteOptions: any) {
|
||||||
|
const tags: any = {}
|
||||||
|
|
||||||
|
// 写入基础信息
|
||||||
|
if (tagWriteOptions.basicInfo) {
|
||||||
|
tags.title = songInfo.name || ''
|
||||||
|
tags.artist = songInfo.singer || ''
|
||||||
|
tags.album = songInfo.albumName || ''
|
||||||
|
tags.year = songInfo.year || ''
|
||||||
|
tags.genre = songInfo.genre || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入歌词
|
||||||
|
if (tagWriteOptions.lyrics && songInfo.lrc) {
|
||||||
|
const convertedLrc = convertLrcFormat(songInfo.lrc)
|
||||||
|
tags.unsynchronisedLyrics = {
|
||||||
|
language: 'chi',
|
||||||
|
shortText: 'Lyrics',
|
||||||
|
text: convertedLrc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入封面
|
||||||
|
if (tagWriteOptions.cover && songInfo.img) {
|
||||||
|
try {
|
||||||
|
const coverResponse = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: songInfo.img,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
if (coverResponse.data) {
|
||||||
|
tags.image = {
|
||||||
|
mime: 'image/jpeg',
|
||||||
|
type: {
|
||||||
|
id: 3,
|
||||||
|
name: 'front cover'
|
||||||
|
},
|
||||||
|
description: 'Cover',
|
||||||
|
imageBuffer: Buffer.from(coverResponse.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (coverError) {
|
||||||
|
console.warn('获取封面失败:', coverError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入标签到文件
|
||||||
|
if (Object.keys(tags).length > 0) {
|
||||||
|
const success = NodeID3.write(tags, filePath)
|
||||||
|
if (success) {
|
||||||
|
console.log('MP3音频标签写入成功:', filePath)
|
||||||
|
} else {
|
||||||
|
console.warn('MP3音频标签写入失败:', filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLAC/OGG 格式标签写入 (使用 Vorbis Comment)
|
||||||
|
async function writeVorbisCommentTags(filePath: string, songInfo: any, tagWriteOptions: any) {
|
||||||
|
try {
|
||||||
|
console.log('开始写入 FLAC 标签:', filePath)
|
||||||
|
|
||||||
|
// 准备新的标签数据
|
||||||
|
const newTags: any = {}
|
||||||
|
|
||||||
|
// 写入基础信息
|
||||||
|
if (tagWriteOptions.basicInfo) {
|
||||||
|
if (songInfo.name) newTags.TITLE = songInfo.name
|
||||||
|
if (songInfo.singer) newTags.ARTIST = songInfo.singer
|
||||||
|
if (songInfo.albumName) newTags.ALBUM = songInfo.albumName
|
||||||
|
if (songInfo.year) newTags.DATE = songInfo.year.toString()
|
||||||
|
if (songInfo.genre) newTags.GENRE = songInfo.genre
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入歌词
|
||||||
|
if (tagWriteOptions.lyrics && songInfo.lrc) {
|
||||||
|
const convertedLrc = convertLrcFormat(songInfo.lrc)
|
||||||
|
newTags.LYRICS = convertedLrc
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('准备写入的标签:', newTags)
|
||||||
|
|
||||||
|
// 使用 ffmpeg-static 写入 FLAC 标签
|
||||||
|
if (path.extname(filePath).toLowerCase() === '.flac') {
|
||||||
|
await writeFLACTagsWithFFmpeg(filePath, newTags, songInfo, tagWriteOptions)
|
||||||
|
} else {
|
||||||
|
console.warn('暂不支持该格式的标签写入:', path.extname(filePath))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('写入 Vorbis Comment 标签失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 fluent-ffmpeg 写入 FLAC 标签
|
||||||
|
async function writeFLACTagsWithFFmpeg(
|
||||||
|
filePath: string,
|
||||||
|
tags: any,
|
||||||
|
songInfo: any,
|
||||||
|
tagWriteOptions: any
|
||||||
|
) {
|
||||||
|
let tempOutputPath: string | null = null
|
||||||
|
let tempCoverPath: string | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!ffmpegStatic) {
|
||||||
|
throw new Error('ffmpeg-static 不可用')
|
||||||
|
}
|
||||||
|
ffmpeg.setFfmpegPath(ffmpegStatic.replace('app.asar', 'app.asar.unpacked'))
|
||||||
|
|
||||||
|
// 创建临时输出文件
|
||||||
|
tempOutputPath = filePath + '.temp.flac'
|
||||||
|
|
||||||
|
// 创建 fluent-ffmpeg 实例
|
||||||
|
let command = ffmpeg(filePath)
|
||||||
|
.audioCodec('copy') // 复制音频编解码器,不重新编码
|
||||||
|
.output(tempOutputPath)
|
||||||
|
|
||||||
|
// 添加元数据标签
|
||||||
|
for (const [key, value] of Object.entries(tags)) {
|
||||||
|
if (value) {
|
||||||
|
// fluent-ffmpeg 会自动处理特殊字符转义
|
||||||
|
command = command.outputOptions(['-metadata', `${key}=${value}`])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理封面
|
||||||
|
if (tagWriteOptions.cover && songInfo.img) {
|
||||||
|
try {
|
||||||
|
console.log('开始下载封面:', songInfo.img)
|
||||||
|
const coverResponse = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: songInfo.img,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
if (coverResponse.data) {
|
||||||
|
// 保存临时封面文件
|
||||||
|
tempCoverPath = path.join(path.dirname(filePath), 'temp_cover.jpg')
|
||||||
|
await fsPromise.writeFile(tempCoverPath, Buffer.from(coverResponse.data))
|
||||||
|
|
||||||
|
// 添加封面作为输入
|
||||||
|
command = command.input(tempCoverPath).outputOptions([
|
||||||
|
'-map',
|
||||||
|
'0:a', // 映射原始文件的音频流
|
||||||
|
'-map',
|
||||||
|
'1:v', // 映射封面的视频流
|
||||||
|
'-c:v',
|
||||||
|
'copy', // 复制视频编解码器
|
||||||
|
'-disposition:v:0',
|
||||||
|
'attached_pic' // 设置为附加图片
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('封面已添加到命令中')
|
||||||
|
}
|
||||||
|
} catch (coverError) {
|
||||||
|
console.warn(
|
||||||
|
'下载封面失败,跳过封面写入:',
|
||||||
|
coverError instanceof Error ? coverError.message : coverError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行 ffmpeg 命令
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
command
|
||||||
|
.on('start', () => {
|
||||||
|
console.log('执行 ffmpeg 命令')
|
||||||
|
})
|
||||||
|
.on('progress', (progress) => {
|
||||||
|
if (progress.percent) {
|
||||||
|
console.log('处理进度:', Math.round(progress.percent) + '%')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('end', () => {
|
||||||
|
console.log('ffmpeg 处理完成')
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.on('error', (err, _, stderr) => {
|
||||||
|
console.error('ffmpeg 错误:', err.message)
|
||||||
|
if (stderr) {
|
||||||
|
console.error('ffmpeg stderr:', stderr)
|
||||||
|
}
|
||||||
|
reject(new Error(`ffmpeg 处理失败: ${err.message}`))
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查临时文件是否创建成功
|
||||||
|
if (!fs.existsSync(tempOutputPath)) {
|
||||||
|
throw new Error('ffmpeg 未能创建输出文件')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换原文件
|
||||||
|
await fsPromise.rename(tempOutputPath, filePath)
|
||||||
|
tempOutputPath = null // 标记已处理,避免重复清理
|
||||||
|
|
||||||
|
console.log('使用 fluent-ffmpeg 写入 FLAC 标签成功:', filePath)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('使用 fluent-ffmpeg 写入 FLAC 标签失败:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
// 清理所有临时文件
|
||||||
|
const filesToClean = [tempOutputPath, tempCoverPath].filter(Boolean) as string[]
|
||||||
|
|
||||||
|
for (const tempFile of filesToClean) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(tempFile)) {
|
||||||
|
await fsPromise.unlink(tempFile)
|
||||||
|
console.log('已清理临时文件:', tempFile)
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn(
|
||||||
|
'清理临时文件失败:',
|
||||||
|
tempFile,
|
||||||
|
cleanupError instanceof Error ? cleanupError.message : cleanupError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MP4/M4A 格式标签写入
|
||||||
|
async function writeMP4Tags(filePath: string, _songInfo: any, _tagWriteOptions: any) {
|
||||||
|
console.log('MP4/M4A 格式标签写入暂未实现:', filePath)
|
||||||
|
// 可以使用 ffmpeg 或其他工具实现
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取自定义下载目录
|
||||||
|
const getDownloadDirectory = (): string => {
|
||||||
|
// 使用配置管理服务获取下载目录
|
||||||
|
const directories = configManager.getDirectories()
|
||||||
|
return directories.downloadDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从URL中提取文件扩展名,如果没有则默认为mp3
|
||||||
|
const getFileExtension = (url: string): string => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url)
|
||||||
|
const pathname = urlObj.pathname
|
||||||
|
const lastDotIndex = pathname.lastIndexOf('.')
|
||||||
|
if (lastDotIndex !== -1 && lastDotIndex < pathname.length - 1) {
|
||||||
|
const extension = pathname.substring(lastDotIndex + 1).toLowerCase()
|
||||||
|
// 验证是否为常见的音频格式
|
||||||
|
const validExtensions = ['mp3', 'flac', 'wav', 'aac', 'm4a', 'ogg', 'wma']
|
||||||
|
if (validExtensions.includes(extension)) {
|
||||||
|
return extension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('解析URL失败,使用默认扩展名:', error)
|
||||||
|
}
|
||||||
|
return 'mp3' // 默认扩展名
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function download(
|
||||||
|
url: string,
|
||||||
|
songInfo: any,
|
||||||
|
tagWriteOptions: any
|
||||||
|
): Promise<any> {
|
||||||
|
const fileExtension = getFileExtension(url)
|
||||||
|
const downloadDir = getDownloadDirectory()
|
||||||
|
const songPath = path.join(
|
||||||
|
downloadDir,
|
||||||
|
`${songInfo.name}-${songInfo.singer}-${songInfo.source}.${fileExtension}`
|
||||||
|
.replace(/[/\\:*?"<>|]/g, '')
|
||||||
|
.replace(/^\.+/, '')
|
||||||
|
.replace(/\.+$/, '')
|
||||||
|
.trim()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (fileLock[songPath]) {
|
||||||
|
throw new Error('歌曲正在下载中')
|
||||||
|
} else {
|
||||||
|
fileLock[songPath] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(songPath)) {
|
||||||
|
return {
|
||||||
|
message: '歌曲已存在'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fsPromise.mkdir(path.dirname(songPath), { recursive: true })
|
||||||
|
|
||||||
|
if (url.startsWith('file://')) {
|
||||||
|
const filePath = fileURLToPath(url)
|
||||||
|
|
||||||
|
const readStream = fs.createReadStream(filePath)
|
||||||
|
const writeStream = fs.createWriteStream(songPath)
|
||||||
|
|
||||||
|
await pipeline(readStream, writeStream)
|
||||||
|
} else {
|
||||||
|
const songDataRes = await axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
responseType: 'stream'
|
||||||
|
})
|
||||||
|
|
||||||
|
await pipeline(songDataRes.data, fs.createWriteStream(songPath))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
delete fileLock[songPath]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入标签信息
|
||||||
|
if (tagWriteOptions && fs.existsSync(songPath)) {
|
||||||
|
try {
|
||||||
|
await writeAudioTags(songPath, songInfo, tagWriteOptions)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('写入音频标签失败:', error)
|
||||||
|
throw ffmpegStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message: '下载成功',
|
||||||
|
path: songPath
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,10 @@ import pic from './pic'
|
|||||||
import lyric from './lyric'
|
import lyric from './lyric'
|
||||||
import hotSearch from './hotSearch'
|
import hotSearch from './hotSearch'
|
||||||
import comment from './comment'
|
import comment from './comment'
|
||||||
// import tipSearch from './tipSearch'
|
import tipSearch from './tipSearch'
|
||||||
|
|
||||||
const kg = {
|
const kg = {
|
||||||
// tipSearch,
|
tipSearch,
|
||||||
leaderboard,
|
leaderboard,
|
||||||
songList,
|
songList,
|
||||||
musicSearch,
|
musicSearch,
|
||||||
|
|||||||
@@ -16,11 +16,28 @@ export default {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return this.requestObj.then((body) => {
|
return this.requestObj.then((body) => {
|
||||||
return body[0].RecordDatas
|
return body
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleResult(rawData) {
|
handleResult(rawData) {
|
||||||
return rawData.map((info) => info.HintInfo)
|
let list = {
|
||||||
|
order: [],
|
||||||
|
songs: [],
|
||||||
|
albums: []
|
||||||
|
}
|
||||||
|
if (rawData[0].RecordCount > 0) {
|
||||||
|
list.order.push('songs')
|
||||||
|
}
|
||||||
|
if (rawData[2].RecordCount > 0) {
|
||||||
|
list.order.push('albums')
|
||||||
|
}
|
||||||
|
list.songs = rawData[0].RecordDatas.map((info) => ({
|
||||||
|
name: info.HintInfo
|
||||||
|
}))
|
||||||
|
list.albums = rawData[2].RecordDatas.map((info) => ({
|
||||||
|
name: info.HintInfo
|
||||||
|
}))
|
||||||
|
return list
|
||||||
},
|
},
|
||||||
async search(str) {
|
async search(str) {
|
||||||
return this.tipSearchBySong(str).then((result) => this.handleResult(result))
|
return this.tipSearchBySong(str).then((result) => this.handleResult(result))
|
||||||
|
|||||||
@@ -24,7 +24,18 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleResult(rawData) {
|
handleResult(rawData) {
|
||||||
return rawData.map((item) => item.RELWORD)
|
let list = {
|
||||||
|
order: [],
|
||||||
|
songs: []
|
||||||
|
}
|
||||||
|
if (rawData.length > 0) {
|
||||||
|
list.order.push('songs')
|
||||||
|
}
|
||||||
|
list.songs = rawData.map((item) => ({
|
||||||
|
name: item.RELWORD,
|
||||||
|
artist: item.TAG_TYPE === 4 ? { name: '热搜' } : null
|
||||||
|
}))
|
||||||
|
return list
|
||||||
},
|
},
|
||||||
cancelTipSearch() {
|
cancelTipSearch() {
|
||||||
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
|
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import pic from './pic'
|
|||||||
import lyric from './lyric'
|
import lyric from './lyric'
|
||||||
import hotSearch from './hotSearch'
|
import hotSearch from './hotSearch'
|
||||||
import comment from './comment'
|
import comment from './comment'
|
||||||
// import tipSearch from './tipSearch'
|
import tipSearch from './tipSearch'
|
||||||
|
|
||||||
const mg = {
|
const mg = {
|
||||||
// tipSearch,
|
tipSearch,
|
||||||
songList,
|
songList,
|
||||||
musicSearch,
|
musicSearch,
|
||||||
leaderboard,
|
leaderboard,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export default {
|
|||||||
tipSearchBySong(str) {
|
tipSearchBySong(str) {
|
||||||
this.cancelTipSearch()
|
this.cancelTipSearch()
|
||||||
this.requestObj = createHttpFetch(
|
this.requestObj = createHttpFetch(
|
||||||
`https://music.migu.cn/v3/api/search/suggest?keyword=${encodeURIComponent(str)}`,
|
//https://app.u.nf.migu.cn/pc/resource/content/tone_search_suggest/v1.0?text=%E5%90%8E
|
||||||
|
`https://app.u.nf.migu.cn/pc/resource/content/tone_search_suggest/v1.0?text=${encodeURIComponent(str)}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
referer: 'https://music.migu.cn/v3'
|
referer: 'https://music.migu.cn/v3'
|
||||||
@@ -16,11 +17,29 @@ export default {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return this.requestObj.then((body) => {
|
return this.requestObj.then((body) => {
|
||||||
return body.songs
|
return body
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleResult(rawData) {
|
handleResult(rawData) {
|
||||||
return rawData.map((info) => `${info.name} - ${info.singerName}`)
|
let list = {
|
||||||
|
order: [],
|
||||||
|
songs: [],
|
||||||
|
artists: []
|
||||||
|
}
|
||||||
|
if (rawData.songList.length > 0) {
|
||||||
|
list.order.push('songs')
|
||||||
|
}
|
||||||
|
if (rawData.singerList.length > 0) {
|
||||||
|
list.order.push('artists')
|
||||||
|
}
|
||||||
|
list.songs = rawData.songList.map((info) => ({
|
||||||
|
name: info.songName
|
||||||
|
}))
|
||||||
|
list.artists = rawData.singerList.map((info) => ({
|
||||||
|
name: info.singerName
|
||||||
|
}))
|
||||||
|
console.log(JSON.stringify(list))
|
||||||
|
return list
|
||||||
},
|
},
|
||||||
async search(str) {
|
async search(str) {
|
||||||
return this.tipSearchBySong(str).then((result) => this.handleResult(result))
|
return this.tipSearchBySong(str).then((result) => this.handleResult(result))
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import songList from './songList'
|
|||||||
import musicSearch from './musicSearch'
|
import musicSearch from './musicSearch'
|
||||||
import hotSearch from './hotSearch'
|
import hotSearch from './hotSearch'
|
||||||
import comment from './comment'
|
import comment from './comment'
|
||||||
// import tipSearch from './tipSearch'
|
import tipSearch from './tipSearch'
|
||||||
|
|
||||||
const tx = {
|
const tx = {
|
||||||
// tipSearch,
|
tipSearch,
|
||||||
leaderboard,
|
leaderboard,
|
||||||
songList,
|
songList,
|
||||||
musicSearch,
|
musicSearch,
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ export default {
|
|||||||
lineTime2: /^\[([\d:.]+)\]/,
|
lineTime2: /^\[([\d:.]+)\]/,
|
||||||
wordTime: /\(\d+,\d+,\d+\)/,
|
wordTime: /\(\d+,\d+,\d+\)/,
|
||||||
wordTimeAll: /(\(\d+,\d+,\d+\))/g,
|
wordTimeAll: /(\(\d+,\d+,\d+\))/g,
|
||||||
timeLabelFixRxp: /(?:\.0+|0+)$/,
|
timeLabelFixRxp: /(?:\.0+|0+)$/
|
||||||
},
|
},
|
||||||
msFormat(timeMs) {
|
msFormat(timeMs) {
|
||||||
if (Number.isNaN(timeMs)) return ''
|
if (Number.isNaN(timeMs)) return ''
|
||||||
let ms = timeMs % 1000
|
let ms = timeMs % 1000
|
||||||
timeMs /= 1000
|
timeMs /= 1000
|
||||||
let m = parseInt(timeMs / 60).toString().padStart(2, '0')
|
let m = parseInt(timeMs / 60)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')
|
||||||
timeMs %= 60
|
timeMs %= 60
|
||||||
let s = parseInt(timeMs).toString().padStart(2, '0')
|
let s = parseInt(timeMs).toString().padStart(2, '0')
|
||||||
return `[${m}:${s}.${String(ms).padStart(3, '0')}]`
|
return `[${m}:${s}.${String(ms).padStart(3, '0')}]`
|
||||||
@@ -74,24 +76,24 @@ export default {
|
|||||||
|
|
||||||
let times = words.match(this.rxps.wordTimeAll)
|
let times = words.match(this.rxps.wordTimeAll)
|
||||||
if (!times) continue
|
if (!times) continue
|
||||||
|
|
||||||
let currentStart = startMsTime
|
let currentStart = startMsTime
|
||||||
const processedTimes = []
|
const processedTimes = []
|
||||||
|
|
||||||
times.forEach(time => {
|
times.forEach((time) => {
|
||||||
const result = /\((\d+),(\d+),(\d+)\)/.exec(time)
|
const result = /\((\d+),(\d+),(\d+)\)/.exec(time)
|
||||||
const duration = parseInt(result[2])
|
const duration = parseInt(result[2])
|
||||||
processedTimes.push(`(${currentStart},${duration},0)`)
|
processedTimes.push(`(${currentStart},${duration},0)`)
|
||||||
currentStart += duration
|
currentStart += duration
|
||||||
})
|
})
|
||||||
|
|
||||||
const wordArr = words.split(this.rxps.wordTime)
|
const wordArr = words.split(this.rxps.wordTime)
|
||||||
const newWords = processedTimes.map((time, index) => `${time}${wordArr[index]}`).join('')
|
const newWords = processedTimes.map((time, index) => `${time}${wordArr[index]}`).join('')
|
||||||
lxlrcLines.push(`${startTimeStr}${newWords}`)
|
lxlrcLines.push(`${startTimeStr}${newWords}`)
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
lyric: lrcLines.join('\n'),
|
lyric: lrcLines.join('\n'),
|
||||||
lxlyric: lxlrcLines.join('\n'),
|
lxlyric: lxlrcLines.join('\n')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getIntv(interval) {
|
getIntv(interval) {
|
||||||
@@ -171,12 +173,10 @@ export default {
|
|||||||
lyric: '',
|
lyric: '',
|
||||||
tlyric: '',
|
tlyric: '',
|
||||||
rlyric: '',
|
rlyric: '',
|
||||||
crlyric: '',
|
crlyric: ''
|
||||||
|
|
||||||
}
|
}
|
||||||
if (lrc) {
|
if (lrc) {
|
||||||
let { lyric } = this.parseCeru(this.removeTag(lrc))
|
let { lyric } = this.parseCeru(this.removeTag(lrc))
|
||||||
console.log(lyric, lrc)
|
|
||||||
info.lyric = lyric
|
info.lyric = lyric
|
||||||
info.crlyric = lrc
|
info.crlyric = lrc
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ export default {
|
|||||||
|
|
||||||
return info
|
return info
|
||||||
},
|
},
|
||||||
parseRlyric(lrc) {
|
parseRlyric(lrc) {
|
||||||
lrc = lrc.trim()
|
lrc = lrc.trim()
|
||||||
lrc = lrc.replace(/\r/g, '')
|
lrc = lrc.replace(/\r/g, '')
|
||||||
if (!lrc) return { lyric: '', lxlyric: '' }
|
if (!lrc) return { lyric: '', lxlyric: '' }
|
||||||
@@ -209,11 +209,7 @@ export default {
|
|||||||
return lrcLines.join('\n')
|
return lrcLines.join('\n')
|
||||||
},
|
},
|
||||||
parseLyric(lrc, tlrc, rlrc) {
|
parseLyric(lrc, tlrc, rlrc) {
|
||||||
return this.parse(
|
return this.parse(decode(lrc), decode(tlrc), decode(rlrc))
|
||||||
decode(lrc),
|
|
||||||
decode(tlrc),
|
|
||||||
decode(rlrc)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
getLyric(mInfo, retryNum = 0) {
|
getLyric(mInfo, retryNum = 0) {
|
||||||
if (retryNum > 3) return Promise.reject(new Error('Get lyric failed'))
|
if (retryNum > 3) return Promise.reject(new Error('Get lyric failed'))
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import zlib from 'zlib'
|
import zlib from 'zlib'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
|||||||
@@ -21,12 +21,33 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleResult(rawData) {
|
handleResult(rawData) {
|
||||||
return rawData.map((info) => `${info.name} - ${info.singer}`)
|
let list = {
|
||||||
|
order: [],
|
||||||
|
songs: [],
|
||||||
|
artists: [],
|
||||||
|
albums: []
|
||||||
|
}
|
||||||
|
if (rawData.song.count > 0) {
|
||||||
|
list.order.push('songs')
|
||||||
|
}
|
||||||
|
if (rawData.singer.count > 0) {
|
||||||
|
list.order.push('artists')
|
||||||
|
}
|
||||||
|
if (rawData.album.count > 0) {
|
||||||
|
list.order.push('albums')
|
||||||
|
}
|
||||||
|
list.songs = rawData.song.itemlist.map((info) => ({
|
||||||
|
name: info.name,
|
||||||
|
artist: { name: info.singer }
|
||||||
|
}))
|
||||||
|
list.artists = rawData.singer.itemlist.map((info) => ({ name: info.name }))
|
||||||
|
list.albums = rawData.album.itemlist.map((info) => ({ name: info.name }))
|
||||||
|
return list
|
||||||
},
|
},
|
||||||
cancelTipSearch() {
|
cancelTipSearch() {
|
||||||
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
|
if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()
|
||||||
},
|
},
|
||||||
async search(str) {
|
async search(str) {
|
||||||
return this.tipSearch(str).then((result) => this.handleResult(result.song.itemlist))
|
return this.tipSearch(str).then((result) => this.handleResult(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import musicSearch from './musicSearch'
|
|||||||
import songList from './songList'
|
import songList from './songList'
|
||||||
import hotSearch from './hotSearch'
|
import hotSearch from './hotSearch'
|
||||||
import comment from './comment'
|
import comment from './comment'
|
||||||
|
import tipSearch from './tipSearch'
|
||||||
|
|
||||||
const wy = {
|
const wy = {
|
||||||
// tipSearch,
|
tipSearch,
|
||||||
leaderboard,
|
leaderboard,
|
||||||
musicSearch,
|
musicSearch,
|
||||||
songList,
|
songList,
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ export default {
|
|||||||
})
|
})
|
||||||
return this.requestObj.promise.then(({ statusCode, body }) => {
|
return this.requestObj.promise.then(({ statusCode, body }) => {
|
||||||
if (statusCode != 200 || body.code != 200) return Promise.reject(new Error('请求失败'))
|
if (statusCode != 200 || body.code != 200) return Promise.reject(new Error('请求失败'))
|
||||||
return body.result.songs
|
return body.result
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleResult(rawData) {
|
handleResult(rawData) {
|
||||||
return rawData.map((info) => `${info.name} - ${formatSingerName(info.artists, 'name')}`)
|
return rawData.map((info) => `${info.name} - ${formatSingerName(info.artists, 'name')}`)
|
||||||
},
|
},
|
||||||
async search(str) {
|
async search(str) {
|
||||||
return this.tipSearchBySong(str).then((result) => this.handleResult(result))
|
return this.tipSearchBySong(str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/preload/index.d.ts
vendored
@@ -11,7 +11,6 @@ interface CustomAPI {
|
|||||||
onMusicCtrl: (callback: (event: Event, args: any) => void) => () => void
|
onMusicCtrl: (callback: (event: Event, args: any) => void) => () => void
|
||||||
|
|
||||||
music: {
|
music: {
|
||||||
request: (api: string, args: any) => Promise<any>
|
|
||||||
requestSdk: <T extends keyof MainApi>(
|
requestSdk: <T extends keyof MainApi>(
|
||||||
method: T,
|
method: T,
|
||||||
args: {
|
args: {
|
||||||
@@ -68,6 +67,7 @@ interface CustomAPI {
|
|||||||
// 插件管理API
|
// 插件管理API
|
||||||
plugins: {
|
plugins: {
|
||||||
selectAndAddPlugin: (type: 'lx' | 'cr') => Promise<any>
|
selectAndAddPlugin: (type: 'lx' | 'cr') => Promise<any>
|
||||||
|
downloadAndAddPlugin: (url: string, type: 'lx' | 'cr') => Promise<any>
|
||||||
uninstallPlugin(pluginId: string): ApiResult | PromiseLike<ApiResult>
|
uninstallPlugin(pluginId: string): ApiResult | PromiseLike<ApiResult>
|
||||||
addPlugin: (pluginCode: string, pluginName: string) => Promise<any>
|
addPlugin: (pluginCode: string, pluginName: string) => Promise<any>
|
||||||
getPluginById: (id: string) => Promise<any>
|
getPluginById: (id: string) => Promise<any>
|
||||||
@@ -79,7 +79,7 @@ interface CustomAPI {
|
|||||||
start: () => undefined
|
start: () => undefined
|
||||||
stop: () => undefined
|
stop: () => undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目录设置API
|
// 目录设置API
|
||||||
directorySettings: {
|
directorySettings: {
|
||||||
getDirectories: () => Promise<{
|
getDirectories: () => Promise<{
|
||||||
@@ -96,10 +96,7 @@ interface CustomAPI {
|
|||||||
path?: string
|
path?: string
|
||||||
message?: string
|
message?: string
|
||||||
}>
|
}>
|
||||||
saveDirectories: (directories: {
|
saveDirectories: (directories: { cacheDir: string; downloadDir: string }) => Promise<{
|
||||||
cacheDir: string
|
|
||||||
downloadDir: string
|
|
||||||
}) => Promise<{
|
|
||||||
success: boolean
|
success: boolean
|
||||||
message: string
|
message: string
|
||||||
}>
|
}>
|
||||||
@@ -119,15 +116,14 @@ interface CustomAPI {
|
|||||||
size: number
|
size: number
|
||||||
formatted: string
|
formatted: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户配置API
|
// 用户配置API
|
||||||
getUserConfig: () => Promise<any>
|
getUserConfig: () => Promise<any>
|
||||||
|
|
||||||
pluginNotice: {
|
pluginNotice: {
|
||||||
onPluginNotice: (listener: (...args: any[]) => void) => ()=>void
|
onPluginNotice: (listener: (...args: any[]) => void) => () => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ const api = {
|
|||||||
console.log('preload: 发送 window-minimize 事件')
|
console.log('preload: 发送 window-minimize 事件')
|
||||||
ipcRenderer.send('window-minimize')
|
ipcRenderer.send('window-minimize')
|
||||||
},
|
},
|
||||||
|
// 阻止系统息屏
|
||||||
|
powerSaveBlocker: {
|
||||||
|
start: () => ipcRenderer.invoke('power-save-blocker:start'),
|
||||||
|
stop: () => ipcRenderer.invoke('power-save-blocker:stop')
|
||||||
|
},
|
||||||
maximize: () => {
|
maximize: () => {
|
||||||
console.log('preload: 发送 window-maximize 事件')
|
console.log('preload: 发送 window-maximize 事件')
|
||||||
ipcRenderer.send('window-maximize')
|
ipcRenderer.send('window-maximize')
|
||||||
@@ -29,7 +34,6 @@ const api = {
|
|||||||
},
|
},
|
||||||
// 音乐相关方法
|
// 音乐相关方法
|
||||||
music: {
|
music: {
|
||||||
request: (api: string, args: any) => ipcRenderer.invoke('service-music-request', api, args),
|
|
||||||
requestSdk: (api: string, args: any) =>
|
requestSdk: (api: string, args: any) =>
|
||||||
ipcRenderer.invoke('service-music-sdk-request', api, args)
|
ipcRenderer.invoke('service-music-sdk-request', api, args)
|
||||||
},
|
},
|
||||||
@@ -37,6 +41,8 @@ const api = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
selectAndAddPlugin: (type: 'lx' | 'cr') =>
|
selectAndAddPlugin: (type: 'lx' | 'cr') =>
|
||||||
ipcRenderer.invoke('service-plugin-selectAndAddPlugin', type),
|
ipcRenderer.invoke('service-plugin-selectAndAddPlugin', type),
|
||||||
|
downloadAndAddPlugin: (url: string, type: 'lx' | 'cr') =>
|
||||||
|
ipcRenderer.invoke('service-plugin-downloadAndAddPlugin', url, type),
|
||||||
addPlugin: (pluginCode: string, pluginName: string) =>
|
addPlugin: (pluginCode: string, pluginName: string) =>
|
||||||
ipcRenderer.invoke('service-plugin-addPlugin', pluginCode, pluginName),
|
ipcRenderer.invoke('service-plugin-addPlugin', pluginCode, pluginName),
|
||||||
getPluginById: (id: string) => ipcRenderer.invoke('service-plugin-getPluginById', id),
|
getPluginById: (id: string) => ipcRenderer.invoke('service-plugin-getPluginById', id),
|
||||||
|
|||||||
88
src/renderer/auto-imports.d.ts
vendored
@@ -6,5 +6,91 @@
|
|||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
|
const DialogPlugin: (typeof import('tdesign-vue-next'))['DialogPlugin']
|
||||||
|
const EffectScope: (typeof import('vue'))['EffectScope']
|
||||||
|
const computed: (typeof import('vue'))['computed']
|
||||||
|
const createApp: (typeof import('vue'))['createApp']
|
||||||
|
const customRef: (typeof import('vue'))['customRef']
|
||||||
|
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
|
||||||
|
const defineComponent: (typeof import('vue'))['defineComponent']
|
||||||
|
const effectScope: (typeof import('vue'))['effectScope']
|
||||||
|
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
|
||||||
|
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
|
||||||
|
const getCurrentWatcher: (typeof import('vue'))['getCurrentWatcher']
|
||||||
|
const h: (typeof import('vue'))['h']
|
||||||
|
const inject: (typeof import('vue'))['inject']
|
||||||
|
const isProxy: (typeof import('vue'))['isProxy']
|
||||||
|
const isReactive: (typeof import('vue'))['isReactive']
|
||||||
|
const isReadonly: (typeof import('vue'))['isReadonly']
|
||||||
|
const isRef: (typeof import('vue'))['isRef']
|
||||||
|
const isShallow: (typeof import('vue'))['isShallow']
|
||||||
|
const markRaw: (typeof import('vue'))['markRaw']
|
||||||
|
const nextTick: (typeof import('vue'))['nextTick']
|
||||||
|
const onActivated: (typeof import('vue'))['onActivated']
|
||||||
|
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
|
||||||
|
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
|
||||||
|
const onDeactivated: (typeof import('vue'))['onDeactivated']
|
||||||
|
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
|
||||||
|
const onMounted: (typeof import('vue'))['onMounted']
|
||||||
|
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
|
||||||
|
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
|
||||||
|
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
|
||||||
|
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
|
||||||
|
const onUnmounted: (typeof import('vue'))['onUnmounted']
|
||||||
|
const onUpdated: (typeof import('vue'))['onUpdated']
|
||||||
|
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
|
||||||
|
const provide: (typeof import('vue'))['provide']
|
||||||
|
const reactive: (typeof import('vue'))['reactive']
|
||||||
|
const readonly: (typeof import('vue'))['readonly']
|
||||||
|
const ref: (typeof import('vue'))['ref']
|
||||||
|
const resolveComponent: (typeof import('vue'))['resolveComponent']
|
||||||
|
const shallowReactive: (typeof import('vue'))['shallowReactive']
|
||||||
|
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
|
||||||
|
const shallowRef: (typeof import('vue'))['shallowRef']
|
||||||
|
const toRaw: (typeof import('vue'))['toRaw']
|
||||||
|
const toRef: (typeof import('vue'))['toRef']
|
||||||
|
const toRefs: (typeof import('vue'))['toRefs']
|
||||||
|
const toValue: (typeof import('vue'))['toValue']
|
||||||
|
const triggerRef: (typeof import('vue'))['triggerRef']
|
||||||
|
const unref: (typeof import('vue'))['unref']
|
||||||
|
const useAttrs: (typeof import('vue'))['useAttrs']
|
||||||
|
const useCssModule: (typeof import('vue'))['useCssModule']
|
||||||
|
const useCssVars: (typeof import('vue'))['useCssVars']
|
||||||
|
const useDialog: (typeof import('naive-ui'))['useDialog']
|
||||||
|
const useId: (typeof import('vue'))['useId']
|
||||||
|
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar']
|
||||||
|
const useMessage: (typeof import('naive-ui'))['useMessage']
|
||||||
|
const useModel: (typeof import('vue'))['useModel']
|
||||||
|
const useNotification: (typeof import('naive-ui'))['useNotification']
|
||||||
|
const useSlots: (typeof import('vue'))['useSlots']
|
||||||
|
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
|
||||||
|
const watch: (typeof import('vue'))['watch']
|
||||||
|
const watchEffect: (typeof import('vue'))['watchEffect']
|
||||||
|
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
|
||||||
|
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type {
|
||||||
|
Component,
|
||||||
|
Slot,
|
||||||
|
Slots,
|
||||||
|
ComponentPublicInstance,
|
||||||
|
ComputedRef,
|
||||||
|
DirectiveBinding,
|
||||||
|
ExtractDefaultPropTypes,
|
||||||
|
ExtractPropTypes,
|
||||||
|
ExtractPublicPropTypes,
|
||||||
|
InjectionKey,
|
||||||
|
PropType,
|
||||||
|
Ref,
|
||||||
|
ShallowRef,
|
||||||
|
MaybeRef,
|
||||||
|
MaybeRefOrGetter,
|
||||||
|
VNode,
|
||||||
|
WritableComputedRef
|
||||||
|
} from 'vue'
|
||||||
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/renderer/components.d.ts
vendored
@@ -10,26 +10,36 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
|
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
|
||||||
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
||||||
|
ContextMenu: typeof import('./src/components/ContextMenu/ContextMenu.vue')['default']
|
||||||
|
Demo: typeof import('./src/components/ContextMenu/demo.vue')['default']
|
||||||
DirectorySettings: typeof import('./src/components/Settings/DirectorySettings.vue')['default']
|
DirectorySettings: typeof import('./src/components/Settings/DirectorySettings.vue')['default']
|
||||||
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
||||||
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
||||||
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
|
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
|
||||||
HomeLayout: typeof import('./src/components/layout/HomeLayout.vue')['default']
|
HomeLayout: typeof import('./src/components/layout/HomeLayout.vue')['default']
|
||||||
MusicCache: typeof import('./src/components/Settings/MusicCache.vue')['default']
|
MusicCache: typeof import('./src/components/Settings/MusicCache.vue')['default']
|
||||||
|
NBadge: typeof import('naive-ui')['NBadge']
|
||||||
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
|
NText: typeof import('naive-ui')['NText']
|
||||||
PlaylistActions: typeof import('./src/components/Play/PlaylistActions.vue')['default']
|
PlaylistActions: typeof import('./src/components/Play/PlaylistActions.vue')['default']
|
||||||
PlaylistDrawer: typeof import('./src/components/Play/PlaylistDrawer.vue')['default']
|
PlaylistDrawer: typeof import('./src/components/Play/PlaylistDrawer.vue')['default']
|
||||||
PlaylistSettings: typeof import('./src/components/Settings/PlaylistSettings.vue')['default']
|
PlaylistSettings: typeof import('./src/components/Settings/PlaylistSettings.vue')['default']
|
||||||
PlayMusic: typeof import('./src/components/Play/PlayMusic.vue')['default']
|
PlayMusic: typeof import('./src/components/Play/PlayMusic.vue')['default']
|
||||||
PluginNoticeDialog: typeof import('./src/components/PluginNoticeDialog.vue')['default']
|
PluginNoticeDialog: typeof import('./src/components/PluginNoticeDialog.vue')['default']
|
||||||
|
Plugins: typeof import('./src/components/Settings/plugins.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SearchSuggest: typeof import('./src/components/search/searchSuggest.vue')['default']
|
||||||
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
|
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
|
||||||
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
|
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
|
||||||
|
SvgIcon: typeof import('./src/components/SvgIcon.vue')['default']
|
||||||
TAlert: typeof import('tdesign-vue-next')['Alert']
|
TAlert: typeof import('tdesign-vue-next')['Alert']
|
||||||
TAside: typeof import('tdesign-vue-next')['Aside']
|
TAside: typeof import('tdesign-vue-next')['Aside']
|
||||||
TBadge: typeof import('tdesign-vue-next')['Badge']
|
|
||||||
TButton: typeof import('tdesign-vue-next')['Button']
|
TButton: typeof import('tdesign-vue-next')['Button']
|
||||||
TCard: typeof import('tdesign-vue-next')['Card']
|
TCard: typeof import('tdesign-vue-next')['Card']
|
||||||
|
TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
|
||||||
TContent: typeof import('tdesign-vue-next')['Content']
|
TContent: typeof import('tdesign-vue-next')['Content']
|
||||||
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
||||||
TDivider: typeof import('tdesign-vue-next')['Divider']
|
TDivider: typeof import('tdesign-vue-next')['Divider']
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||||
import { useAutoUpdate } from './composables/useAutoUpdate'
|
import { useAutoUpdate } from './composables/useAutoUpdate'
|
||||||
|
import { NConfigProvider, darkTheme, NGlobalStyle } from 'naive-ui'
|
||||||
|
|
||||||
const userInfo = LocalUserDetailStore()
|
const userInfo = LocalUserDetailStore()
|
||||||
const { checkForUpdates } = useAutoUpdate()
|
const { checkForUpdates } = useAutoUpdate()
|
||||||
@@ -25,7 +26,10 @@ import './assets/theme/cyan.css'
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
userInfo.init()
|
userInfo.init()
|
||||||
|
setupSystemThemeListener()
|
||||||
loadSavedTheme()
|
loadSavedTheme()
|
||||||
|
syncNaiveTheme()
|
||||||
|
window.addEventListener('theme-changed', () => syncNaiveTheme())
|
||||||
|
|
||||||
// 应用启动后延迟3秒检查更新,避免影响启动速度
|
// 应用启动后延迟3秒检查更新,避免影响启动速度
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -42,44 +46,121 @@ const themes = [
|
|||||||
{ name: 'orange', label: '橙色', color: '#fb9458' }
|
{ name: 'orange', label: '橙色', color: '#fb9458' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const loadSavedTheme = () => {
|
const naiveTheme = ref<any>(null)
|
||||||
const savedTheme = localStorage.getItem('selected-theme')
|
const themeOverrides = ref<any>({})
|
||||||
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
|
|
||||||
applyTheme(savedTheme)
|
function syncNaiveTheme() {
|
||||||
|
const docEl = document.documentElement
|
||||||
|
const savedDarkMode = localStorage.getItem('dark-mode')
|
||||||
|
const isDark = savedDarkMode === 'true'
|
||||||
|
naiveTheme.value = isDark ? darkTheme : null
|
||||||
|
|
||||||
|
const computed = getComputedStyle(docEl)
|
||||||
|
const primary = (computed.getPropertyValue('--td-brand-color') || '').trim()
|
||||||
|
|
||||||
|
const savedThemeName = localStorage.getItem('selected-theme') || 'default'
|
||||||
|
const fallback = themes.find((t) => t.name === savedThemeName)?.color || '#2ba55b'
|
||||||
|
const mainColor = primary || fallback
|
||||||
|
|
||||||
|
themeOverrides.value = {
|
||||||
|
common: {
|
||||||
|
primaryColor: mainColor,
|
||||||
|
primaryColorHover: mainColor,
|
||||||
|
primaryColorPressed: mainColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyTheme = (themeName) => {
|
const loadSavedTheme = () => {
|
||||||
|
const savedTheme = localStorage.getItem('selected-theme')
|
||||||
|
const savedDarkMode = localStorage.getItem('dark-mode')
|
||||||
|
|
||||||
|
let themeName = 'default'
|
||||||
|
let isDarkMode = false
|
||||||
|
|
||||||
|
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
|
||||||
|
themeName = savedTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedDarkMode !== null) {
|
||||||
|
isDarkMode = savedDarkMode === 'true'
|
||||||
|
} else {
|
||||||
|
// 如果没有保存的设置,检测系统偏好
|
||||||
|
isDarkMode = detectSystemTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme(themeName, isDarkMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyTheme = (themeName, darkMode = false) => {
|
||||||
const documentElement = document.documentElement
|
const documentElement = document.documentElement
|
||||||
|
|
||||||
// 移除之前的主题
|
// 移除之前的主题属性
|
||||||
documentElement.removeAttribute('theme-mode')
|
documentElement.removeAttribute('theme-mode')
|
||||||
|
documentElement.removeAttribute('data-theme')
|
||||||
|
|
||||||
// 应用新主题(如果不是默认主题)
|
// 应用主题色彩
|
||||||
if (themeName !== 'default') {
|
if (themeName !== 'default') {
|
||||||
documentElement.setAttribute('theme-mode', themeName)
|
documentElement.setAttribute('theme-mode', themeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 应用明暗模式
|
||||||
|
if (darkMode) {
|
||||||
|
documentElement.setAttribute('data-theme', 'dark')
|
||||||
|
} else {
|
||||||
|
documentElement.setAttribute('data-theme', 'light')
|
||||||
|
}
|
||||||
|
|
||||||
// 保存到本地存储
|
// 保存到本地存储
|
||||||
localStorage.setItem('selected-theme', themeName)
|
localStorage.setItem('selected-theme', themeName)
|
||||||
|
localStorage.setItem('dark-mode', darkMode.toString())
|
||||||
|
|
||||||
|
// 同步 Naive UI 主题
|
||||||
|
syncNaiveTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测系统主题偏好
|
||||||
|
const detectSystemTheme = () => {
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听系统主题变化
|
||||||
|
const setupSystemThemeListener = () => {
|
||||||
|
if (window.matchMedia) {
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
mediaQuery.addEventListener('change', (e) => {
|
||||||
|
const savedDarkMode = localStorage.getItem('dark-mode')
|
||||||
|
// 如果用户没有手动设置暗色模式,则跟随系统主题
|
||||||
|
if (savedDarkMode === null) {
|
||||||
|
const savedTheme = localStorage.getItem('selected-theme') || 'default'
|
||||||
|
applyTheme(savedTheme, e.matches)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<NConfigProvider :theme="naiveTheme" :theme-overrides="themeOverrides">
|
||||||
<router-view v-slot="{ Component }">
|
<NGlobalStyle />
|
||||||
<Transition
|
<div class="page">
|
||||||
:enter-active-class="`animate__animated animate__fadeIn pagesApp`"
|
<router-view v-slot="{ Component }">
|
||||||
:leave-active-class="`animate__animated animate__fadeOut pagesApp`"
|
<Transition
|
||||||
>
|
:enter-active-class="`animate__animated animate__fadeIn pagesApp`"
|
||||||
<component :is="Component" />
|
:leave-active-class="`animate__animated animate__fadeOut pagesApp`"
|
||||||
</Transition>
|
>
|
||||||
</router-view>
|
<component :is="Component" />
|
||||||
<GlobalAudio />
|
</Transition>
|
||||||
<FloatBall />
|
</router-view>
|
||||||
<PluginNoticeDialog />
|
<GlobalAudio />
|
||||||
<UpdateProgress />
|
<FloatBall />
|
||||||
</div>
|
<PluginNoticeDialog />
|
||||||
|
<UpdateProgress />
|
||||||
|
</div>
|
||||||
|
</NConfigProvider>
|
||||||
</template>
|
</template>
|
||||||
<style>
|
<style>
|
||||||
.pagesApp {
|
.pagesApp {
|
||||||
|
|||||||
@@ -60,23 +60,23 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background: #f1f5f9;
|
background: var(--td-scroll-track-color);
|
||||||
border-radius: 0.1875rem;
|
border-radius: 0.1875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #cbd5e1;
|
background: var(--td-scrollbar-color);
|
||||||
border-radius: 0.1875rem;
|
border-radius: 0.1875rem;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #94a3b8;
|
background: var(--td-scrollbar-hover-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Firefox 滚动条样式 */
|
/* Firefox 滚动条样式 */
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #cbd5e1 #f1f5f9;
|
scrollbar-color: var(--td-scrollbar-color) var(--td-scroll-track-color);
|
||||||
}
|
}
|
||||||
.t-dialog__mask{
|
.t-dialog__mask {
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/renderer/src/assets/icons/Add.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M18 13h-5v5c0 .55-.45 1-1 1s-1-.45-1-1v-5H6c-.55 0-1-.45-1-1s.45-1 1-1h5V6c0-.55.45-1 1-1s1 .45 1 1v5h5c.55 0 1 .45 1 1s-.45 1-1 1"/></svg>
|
||||||
|
After Width: | Height: | Size: 251 B |
1
src/renderer/src/assets/icons/AddList.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M14 16h2v-2h2v-2h-2v-2h-2v2h-2v2h2zM4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h6l2 2h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20z"/></svg>
|
||||||
|
After Width: | Height: | Size: 266 B |
1
src/renderer/src/assets/icons/Album.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 16.5q1.875 0 3.188-1.312T16.5 12t-1.312-3.187T12 7.5T8.813 8.813T7.5 12t1.313 3.188T12 16.5m0-3.5q-.425 0-.712-.288T11 12t.288-.712T12 11t.713.288T13 12t-.288.713T12 13m0 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>
|
||||||
|
After Width: | Height: | Size: 459 B |
1
src/renderer/src/assets/icons/Artist.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M11 14c1 0 2.05.16 3.2.44c-.81.87-1.2 1.89-1.2 3.06c0 .89.25 1.73.78 2.5H3v-2c0-1.19.91-2.15 2.74-2.88C7.57 14.38 9.33 14 11 14m0-2c-1.08 0-2-.39-2.82-1.17C7.38 10.05 7 9.11 7 8c0-1.08.38-2 1.18-2.82C9 4.38 9.92 4 11 4c1.11 0 2.05.38 2.83 1.18C14.61 6 15 6.92 15 8c0 1.11-.39 2.05-1.17 2.83c-.78.78-1.72 1.17-2.83 1.17m7.5-2H22v2h-2v5.5a2.5 2.5 0 0 1-2.5 2.5a2.5 2.5 0 0 1-2.5-2.5a2.5 2.5 0 0 1 2.5-2.5c.36 0 .69.07 1 .21z"/></svg>
|
||||||
|
After Width: | Height: | Size: 543 B |
1
src/renderer/src/assets/icons/AutoFix.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7.5 5.6L10 7L8.6 4.5L10 2L7.5 3.4L5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.996.996 0 0 0-1.41 0L1.29 18.96a.996.996 0 0 0 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05a.996.996 0 0 0 0-1.41zm-1.03 5.49l-2.12-2.12l2.44-2.44l2.12 2.12z"/></svg>
|
||||||
|
After Width: | Height: | Size: 459 B |
1
src/renderer/src/assets/icons/AutoTheme.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M10.85 12.65h2.3L12 9zM20 8.69V6c0-1.1-.9-2-2-2h-2.69l-1.9-1.9c-.78-.78-2.05-.78-2.83 0L8.69 4H6c-1.1 0-2 .9-2 2v2.69l-1.9 1.9c-.78.78-.78 2.05 0 2.83l1.9 1.9V18c0 1.1.9 2 2 2h2.69l1.9 1.9c.78.78 2.05.78 2.83 0l1.9-1.9H18c1.1 0 2-.9 2-2v-2.69l1.9-1.9c.78-.78.78-2.05 0-2.83zm-5.91 6.71L13.6 14h-3.2l-.49 1.4c-.13.36-.46.6-.84.6a.888.888 0 0 1-.84-1.19l2.44-6.86c.2-.57.73-.95 1.33-.95c.6 0 1.13.38 1.34.94l2.44 6.86a.888.888 0 0 1-.84 1.19a.874.874 0 0 1-.85-.59"/></svg>
|
||||||
|
After Width: | Height: | Size: 583 B |
1
src/renderer/src/assets/icons/Batch.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M11 8c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1s.45 1 1 1h7c.55 0 1-.45 1-1m0 8c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1s.45 1 1 1h7c.55 0 1-.45 1-1m6.05-5.71a.996.996 0 0 1-1.41 0l-2.12-2.12a.996.996 0 1 1 1.41-1.41l1.41 1.41l3.54-3.54a.996.996 0 1 1 1.41 1.41zm0 8a.996.996 0 0 1-1.41 0l-2.12-2.12a.996.996 0 1 1 1.41-1.41l1.41 1.41l3.54-3.54a.996.996 0 1 1 1.41 1.41z"/></svg>
|
||||||
|
After Width: | Height: | Size: 478 B |
1
src/renderer/src/assets/icons/Calendar-Empty.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3zm1.75 5.5h-15v9.25c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75zm-1.75-4H6.25A1.75 1.75 0 0 0 4.5 6.25V7h15v-.75a1.75 1.75 0 0 0-1.75-1.75"/></svg>
|
||||||
|
After Width: | Height: | Size: 393 B |
1
src/renderer/src/assets/icons/Chat.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m6 18l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18zm1-4h6q.425 0 .713-.288T14 13t-.288-.712T13 12H7q-.425 0-.712.288T6 13t.288.713T7 14m0-3h10q.425 0 .713-.288T18 10t-.288-.712T17 9H7q-.425 0-.712.288T6 10t.288.713T7 11m0-3h10q.425 0 .713-.288T18 7t-.288-.712T17 6H7q-.425 0-.712.288T6 7t.288.713T7 8"/></svg>
|
||||||
|
After Width: | Height: | Size: 489 B |
1
src/renderer/src/assets/icons/Cloud.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5c0-2.64-2.05-4.78-4.65-4.96"/></svg>
|
||||||
|
After Width: | Height: | Size: 269 B |
1
src/renderer/src/assets/icons/CloudLockOpen.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M14.2 13.5v1.24c-.7.6-1.2 1.5-1.2 2.46V20H6.5c-1.5 0-2.81-.5-3.89-1.57C1.54 17.38 1 16.09 1 14.58c0-1.3.39-2.46 1.17-3.48S4 9.43 5.25 9.15c.42-1.53 1.25-2.77 2.5-3.72S10.42 4 12 4c1.95 0 3.6.68 4.96 2.04a6.74 6.74 0 0 1 1.78 2.99c-2.49.13-4.54 2.12-4.54 4.47m7.6 2.5h-4.3v-2.5c0-.8.7-1.3 1.5-1.3s1.5.5 1.5 1.3v.5h1.3v-.5c0-1.4-1.4-2.5-2.8-2.5s-2.8 1.1-2.8 2.5V16c-.6 0-1.2.6-1.2 1.2v3.5c0 .7.6 1.3 1.2 1.3h5.5c.7 0 1.3-.6 1.3-1.2v-3.5c0-.7-.6-1.3-1.2-1.3"/></svg>
|
||||||
|
After Width: | Height: | Size: 575 B |
1
src/renderer/src/assets/icons/Code.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M8.7 15.9L4.8 12l3.9-3.9a.984.984 0 0 0 0-1.4a.984.984 0 0 0-1.4 0l-4.59 4.59a.996.996 0 0 0 0 1.41l4.59 4.6c.39.39 1.01.39 1.4 0a.984.984 0 0 0 0-1.4m6.6 0l3.9-3.9l-3.9-3.9a.984.984 0 0 1 0-1.4a.984.984 0 0 1 1.4 0l4.59 4.59c.39.39.39 1.02 0 1.41l-4.59 4.6a.984.984 0 0 1-1.4 0a.984.984 0 0 1 0-1.4"/></svg>
|
||||||
|
After Width: | Height: | Size: 420 B |
1
src/renderer/src/assets/icons/Copy.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 18q-.825 0-1.412-.587T7 16V4q0-.825.588-1.412T9 2h9q.825 0 1.413.588T20 4v12q0 .825-.587 1.413T18 18zm-4 4q-.825 0-1.412-.587T3 20V7q0-.425.288-.712T4 6t.713.288T5 7v13h10q.425 0 .713.288T16 21t-.288.713T15 22z"/></svg>
|
||||||
|
After Width: | Height: | Size: 334 B |
1
src/renderer/src/assets/icons/DarkTheme.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M11.01 3.05C6.51 3.54 3 7.36 3 12a9 9 0 0 0 9 9c4.63 0 8.45-3.5 8.95-8c.09-.79-.78-1.42-1.54-.95A5.403 5.403 0 0 1 11.1 7.5c0-1.06.31-2.06.84-2.89c.45-.67-.04-1.63-.93-1.56"/></svg>
|
||||||
|
After Width: | Height: | Size: 293 B |
1
src/renderer/src/assets/icons/Delete.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6zM19 4h-3.5l-1-1h-5l-1 1H5v2h14z"/></svg>
|
||||||
|
After Width: | Height: | Size: 193 B |
1
src/renderer/src/assets/icons/DeleteSweep.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M5 19q-.825 0-1.412-.587T3 17V8q-.425 0-.712-.288T2 7t.288-.712T3 6h3v-.5q0-.425.288-.712T7 4.5h2q.425 0 .713.288T10 5.5V6h3q.425 0 .713.288T14 7t-.288.713T13 8v9q0 .825-.587 1.413T11 19zm11-1q-.425 0-.712-.288T15 17t.288-.712T16 16h2q.425 0 .713.288T19 17t-.288.713T18 18zm0-4q-.425 0-.712-.288T15 13t.288-.712T16 12h4q.425 0 .713.288T21 13t-.288.713T20 14zm0-4q-.425 0-.712-.288T15 9t.288-.712T16 8h5q.425 0 .713.288T22 9t-.288.713T21 10z"/></svg>
|
||||||
|
After Width: | Height: | Size: 561 B |
1
src/renderer/src/assets/icons/DesktopLyric.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7 14h2q.425 0 .713-.288T10 13t-.288-.712T9 12H7q-.425 0-.712.288T6 13t.288.713T7 14m12-2q-1.25 0-2.125-.875T16 9t.875-2.125T19 6q.275 0 .525.05t.475.125V2q0-.425.288-.712T21 1h2q.425 0 .713.288T24 2t-.288.713T23 3h-1v6q0 1.25-.875 2.125T19 12M7 11h5q.425 0 .713-.288T13 10t-.288-.712T12 9H7q-.425 0-.712.288T6 10t.288.713T7 11m0-3h5q.425 0 .713-.288T13 7t-.288-.712T12 6H7q-.425 0-.712.288T6 7t.288.713T7 8M6 18l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h11q.775 0 1.363.475T16.95 3.7q0 .35-.162.625t-.438.45q-1.1.675-1.725 1.8T14 9q0 1.35.663 2.5t1.837 1.825q.55.325.875.863t.325 1.187q0 1.125-.788 1.875T15 18z"/></svg>
|
||||||
|
After Width: | Height: | Size: 752 B |
1
src/renderer/src/assets/icons/Discover.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" viewBox="0 0 24 24" focusable="false" aria-hidden="true"><g><g><path d="M5.9999978125,4.0176934375L18.0000078125,4.0176934375C18.4970078125,4.0176934375,18.9000078125,3.6147534375,18.9000078125,3.1176944375C18.9000078125,2.6206384375,18.4970078125,2.2176944613,18.0000078125,2.2176944613L5.9999978125,2.2176944613C5.5029478125,2.2176944613,5.0999978125,2.6206384375,5.0999978125,3.1176944375C5.0999978125,3.6147534375,5.5029478125,4.0176934375,5.9999978125,4.0176934375ZM3.1960448125,16.8951734375L2.8022507125,11.1750534375Q2.6749484125,9.3258934375,2.7196047125,8.6725834375Q2.8020806125,7.4659834375,3.4361238125,6.7867934375Q4.0701678125,6.1076034375,5.2682578125,5.9424434375Q5.9169578125,5.8530234375,7.7704578125,5.8530234375L16.2294078125,5.8530234375Q18.0829078125,5.8530234375,18.7316078125,5.9424434375Q19.9297078125,6.1076034375,20.5638078125,6.7867934375Q21.1978078125,7.4659934375,21.2803078125,8.6725834375Q21.3249078125,9.3259034375,21.1976078125,11.1750234375L20.8039078125,16.8951734375Q20.6913078125,18.5299734375,20.5753078125,19.1062734375Q20.3615078125,20.1678734375,19.7462078125,20.7422734375Q19.1309078125,21.3166734375,18.0572078125,21.4569734375Q17.474207812499998,21.5331734375,15.8356078125,21.5331734375L8.1642578125,21.5331734375Q6.5256978125,21.5331734375,5.9427178125,21.4569734375Q4.8689478125,21.3166734375,4.2536678125,20.7422734375Q3.6383718125,20.1678734375,3.4246308125000002,19.1062734375Q3.3085848125,18.5299734375,3.1960448125,16.8951734375ZM14.6375078125,8.530113437499999C14.7011078125,8.5104734375,14.7673078125,8.5004834375,14.8339078125,8.5004834375C15.2018078125,8.5004834375,15.4999078125,8.7986734375,15.4999078125,9.1665134375L15.4999078125,15.5220734375L15.4819078125,15.5220734375C15.4827078125,15.5418734375,15.4831078125,15.5618734375,15.4831078125,15.5818734375C15.4831078125,16.379873437500002,14.8362078125,17.0268734375,14.0382078125,17.0268734375C13.2402078125,17.0268734375,12.5932578125,16.379873437500002,12.5932578125,15.5818734375C12.5932578125,14.7838734375,13.2402078125,14.1369734375,14.0382078125,14.1369734375C14.2953078125,14.1369734375,14.5367078125,14.2040734375,14.7459078125,14.3218734375L14.7459078125,11.0771534375L10.3793178125,12.4171734375L10.3793178125,16.8837734375C10.386277812500001,16.9412734375,10.3898678125,16.9997734375,10.3898678125,17.0591734375C10.3898678125,17.857173437500002,9.742947812499999,18.5041734375,8.9449278125,18.5041734375C8.1469178125,18.5041734375,7.4999978125,17.857173437500002,7.4999978125,17.0591734375C7.4999978125,16.2611734375,8.1469178125,15.6142734375,8.9449278125,15.6142734375C9.190897812500001,15.6142734375,9.422507812500001,15.6756734375,9.6252478125,15.7840734375L9.6252478125,10.5687534375C9.6252478125,10.2765934375,9.8156578125,10.0185334375,10.0948278125,9.932363437500001L14.6375078125,8.530113437499999Z" fill-rule="evenodd" fill="currentColor" fill-opacity="1"></path></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
1
src/renderer/src/assets/icons/Down.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M13.06 16.06a1.5 1.5 0 0 1-2.12 0l-5.658-5.656a1.5 1.5 0 1 1 2.122-2.121L12 12.879l4.596-4.596a1.5 1.5 0 0 1 2.122 2.12l-5.657 5.658Z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 848 B |
1
src/renderer/src/assets/icons/Download.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71M5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1"/></svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
1
src/renderer/src/assets/icons/DropDown.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m11.565 13.873l-2.677-2.677q-.055-.055-.093-.129q-.037-.073-.037-.157q0-.168.11-.289q.112-.121.294-.121h5.677q.181 0 .292.124t.111.288q0 .042-.13.284l-2.677 2.677q-.093.093-.2.143t-.235.05t-.235-.05t-.2-.143"/></svg>
|
||||||
|
After Width: | Height: | Size: 328 B |
1
src/renderer/src/assets/icons/Earth.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M17.9 17.39c-.26-.8-1.01-1.39-1.9-1.39h-1v-3a1 1 0 0 0-1-1H8v-2h2a1 1 0 0 0 1-1V7h2a2 2 0 0 0 2-2v-.41a7.984 7.984 0 0 1 2.9 12.8M11 19.93c-3.95-.49-7-3.85-7-7.93c0-.62.08-1.22.21-1.79L9 15v1a2 2 0 0 0 2 2m1-16A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"/></svg>
|
||||||
|
After Width: | Height: | Size: 401 B |
1
src/renderer/src/assets/icons/EditNote.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M14 11c0 .55-.45 1-1 1H4c-.55 0-1-.45-1-1s.45-1 1-1h9c.55 0 1 .45 1 1M3 7c0 .55.45 1 1 1h9c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1m7 8c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1h5c.55 0 1-.45 1-1m8.01-2.13l.71-.71a.996.996 0 0 1 1.41 0l.71.71c.39.39.39 1.02 0 1.41l-.71.71zm-.71.71l-5.16 5.16c-.09.09-.14.21-.14.35v1.41c0 .28.22.5.5.5h1.41c.13 0 .26-.05.35-.15l5.16-5.16z"/></svg>
|
||||||
|
After Width: | Height: | Size: 500 B |
1
src/renderer/src/assets/icons/ExitToApp.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M19 3H5c-1.11 0-2 .89-2 2v4h2V5h14v14H5v-4H3v4a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-8.92 12.58L11.5 17l5-5l-5-5l-1.42 1.41L12.67 11H3v2h9.67z"/></svg>
|
||||||
|
After Width: | Height: | Size: 273 B |
1
src/renderer/src/assets/icons/Eye.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5s5 2.24 5 5s-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3"/></svg>
|
||||||
|
After Width: | Height: | Size: 329 B |
1
src/renderer/src/assets/icons/EyeLock.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M20.8 17v-1.5c0-1.4-1.4-2.5-2.8-2.5s-2.8 1.1-2.8 2.5V17c-.6 0-1.2.6-1.2 1.2v3.5c0 .7.6 1.3 1.2 1.3h5.5c.7 0 1.3-.6 1.3-1.2v-3.5c0-.7-.6-1.3-1.2-1.3m-1.3 0h-3v-1.5c0-.8.7-1.3 1.5-1.3s1.5.5 1.5 1.3zM15 12c-.9.7-1.5 1.6-1.7 2.7c-.4.2-.8.3-1.3.3c-1.7 0-3-1.3-3-3s1.3-3 3-3s3 1.3 3 3m-3 7.5c-5 0-9.3-3.1-11-7.5c1.7-4.4 6-7.5 11-7.5s9.3 3.1 11 7.5c-.2.5-.5 1-.7 1.5C21.5 12 19.8 11 18 11c-.4 0-.7.1-1.1.1C16.5 8.8 14.5 7 12 7c-2.8 0-5 2.2-5 5s2.2 5 5 5h.3q-.3.6-.3 1.2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 584 B |
1
src/renderer/src/assets/icons/Favorite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.35 20.13c-.76.69-1.93.69-2.69-.01l-.11-.1C5.3 15.27 1.87 12.16 2 8.28c.06-1.7.93-3.33 2.34-4.29c2.64-1.8 5.9-.96 7.66 1.1c1.76-2.06 5.02-2.91 7.66-1.1c1.41.96 2.28 2.59 2.34 4.29c.14 3.88-3.3 6.99-8.55 11.76z"/></svg>
|
||||||
|
After Width: | Height: | Size: 333 B |
1
src/renderer/src/assets/icons/FavoriteBorder.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M19.66 3.99c-2.64-1.8-5.9-.96-7.66 1.1c-1.76-2.06-5.02-2.91-7.66-1.1c-1.4.96-2.28 2.58-2.34 4.29c-.14 3.88 3.3 6.99 8.55 11.76l.1.09c.76.69 1.93.69 2.69-.01l.11-.1c5.25-4.76 8.68-7.87 8.55-11.75c-.06-1.7-.94-3.32-2.34-4.28M12.1 18.55l-.1.1l-.1-.1C7.14 14.24 4 11.39 4 8.5C4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5c0 2.89-3.14 5.74-7.9 10.05"/></svg>
|
||||||
|
After Width: | Height: | Size: 513 B |
1
src/renderer/src/assets/icons/Fire.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M17.66 11.2c-.23-.3-.51-.56-.77-.82c-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32c-2.59 2.08-3.61 5.75-2.39 8.9c.04.1.08.2.08.33c0 .22-.15.42-.35.5c-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5c.14.6.41 1.2.71 1.73c1.08 1.73 2.95 2.97 4.96 3.22c2.14.27 4.43-.12 6.07-1.6c1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6c-1.12.4-2.24-.16-2.9-.82c1.19-.28 1.9-1.16 2.11-2.05c.17-.8-.15-1.46-.28-2.23c-.12-.74-.1-1.37.17-2.06c.19.38.39.76.63 1.06c.77 1 1.98 1.44 2.24 2.8c.04.14.06.28.06.43c.03.82-.33 1.72-.93 2.27"/></svg>
|
||||||
|
After Width: | Height: | Size: 786 B |
1
src/renderer/src/assets/icons/Folder.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M10.59 4.59C10.21 4.21 9.7 4 9.17 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
1
src/renderer/src/assets/icons/FolderCog.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h8.08a7 7 0 0 1-.08-1a7 7 0 0 1 7-7a7 7 0 0 1 3 .69V8a2 2 0 0 0-2-2h-8l-2-2zm14 10a.26.26 0 0 0-.26.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82a4.193 4.193 0 0 0 0 1l-1.06.82a.26.26 0 0 0-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.26.21h2c.11 0 .22-.09.24-.21l.19-1.32c.3-.13.57-.29.84-.47l1.23.5c.13 0 .26 0 .33-.13l1-1.73a.26.26 0 0 0-.06-.32l-1.07-.82c.02-.17.04-.33.04-.5c0-.17-.01-.33-.04-.5l1.06-.82a.26.26 0 0 0 .06-.32l-1-1.73c-.06-.13-.19-.13-.32-.13l-1.23.5c-.27-.18-.54-.35-.85-.47l-.19-1.32A.236.236 0 0 0 20 14zm1 3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5c-.84 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 862 B |