mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 11:29:42 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a1e0af39 | ||
|
|
c1d3a61f9f | ||
|
|
d6d806c96e | ||
|
|
089406464b | ||
|
|
c28d5d6ad0 | ||
|
|
471147ac82 | ||
|
|
7558a67df3 | ||
|
|
4a3f0ee124 | ||
|
|
5fe6d93d5e | ||
|
|
30fd2ebb9f | ||
|
|
0f78f117d0 | ||
|
|
c79b6951d6 | ||
|
|
5118874712 | ||
|
|
7e5baba969 | ||
|
|
a7af89e35d | ||
|
|
d511efdfce | ||
|
|
9b6050be7a | ||
|
|
57736e60f3 | ||
|
|
6165a2619e | ||
|
|
c933b6e0b4 | ||
|
|
e0e01cbdca | ||
|
|
9b34ecbed9 | ||
|
|
1dda213013 | ||
|
|
94c2dc740f | ||
|
|
61f062455e | ||
|
|
79827f14f7 | ||
|
|
394bdd573c | ||
|
|
d03d62c8d4 | ||
|
|
be0b0b0390 | ||
|
|
c1d2f3dc8d | ||
|
|
e590c33c66 | ||
|
|
604ac7b553 | ||
|
|
18e233ae10 | ||
|
|
61699c4853 | ||
|
|
6e69920a5d | ||
|
|
a767b008a0 | ||
|
|
41b104e96d | ||
|
|
8562b7c954 | ||
|
|
b61e88b7d9 | ||
|
|
cc1dbcaf3f | ||
|
|
941af10830 | ||
|
|
576f9697d4 |
158
.github/workflows/auto-sync-release.yml
vendored
Normal file
158
.github/workflows/auto-sync-release.yml
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
name: Auto Sync New Release to WebDAV
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
auto-sync-to-webdav:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl jq
|
||||
|
||||
- name: Get release info
|
||||
id: release-info
|
||||
run: |
|
||||
echo "tag_name=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
|
||||
echo "release_id=${{ github.event.release.id }}" >> $GITHUB_OUTPUT
|
||||
echo "release_name=${{ github.event.release.name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Sync new release to WebDAV
|
||||
run: |
|
||||
TAG_NAME="${{ steps.release-info.outputs.tag_name }}"
|
||||
RELEASE_ID="${{ steps.release-info.outputs.release_id }}"
|
||||
RELEASE_NAME="${{ steps.release-info.outputs.release_name }}"
|
||||
|
||||
echo "🚀 开始同步新发布的版本到 WebDAV..."
|
||||
echo "版本标签: $TAG_NAME"
|
||||
echo "版本名称: $RELEASE_NAME"
|
||||
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"
|
||||
if curl -s -f -X MKCOL \
|
||||
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||
"$dir_url"; then
|
||||
echo "✅ 目录创建成功"
|
||||
else
|
||||
echo "ℹ️ 目录可能已存在或创建失败,继续执行"
|
||||
fi
|
||||
|
||||
# 处理每个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))
|
||||
|
||||
# 验证文件是否存在
|
||||
sleep 1
|
||||
if curl -s -f -u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||
-X PROPFIND \
|
||||
-H "Depth: 0" \
|
||||
"$full_url" > /dev/null 2>&1; then
|
||||
echo "✅ 文件验证成功: $asset_name"
|
||||
else
|
||||
echo "⚠️ 文件验证失败,但上传可能成功: $asset_name"
|
||||
fi
|
||||
|
||||
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 个文件"
|
||||
|
||||
if [ "$failed_count" -gt 0 ]; then
|
||||
echo "⚠️ 有文件同步失败,请检查日志"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ 所有文件同步成功!"
|
||||
fi
|
||||
|
||||
- name: Notify completion
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ job.status }}" == "success" ]; then
|
||||
echo "✅ 版本 ${{ steps.release-info.outputs.tag_name }} 已成功同步到 alist"
|
||||
else
|
||||
echo "❌ 版本 ${{ steps.release-info.outputs.tag_name }} 同步失败"
|
||||
fi
|
||||
143
.github/workflows/main.yml
vendored
143
.github/workflows/main.yml
vendored
@@ -70,3 +70,146 @@ jobs:
|
||||
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
|
||||
|
||||
154
.github/workflows/sync-releases-to-webdav.yml
vendored
Normal file
154
.github/workflows/sync-releases-to-webdav.yml
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
name: Sync Existing Releases to Alist
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: '要同步的特定标签(如 v1.0.0),留空则同步所有版本'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-releases-to-alist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl jq
|
||||
|
||||
- name: Get all releases
|
||||
id: get-releases
|
||||
run: |
|
||||
response=$(curl -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases")
|
||||
echo "releases_json=$(echo "$response" | jq -c '.')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Sync releases to Alist(自动登录 & 上传)
|
||||
run: |
|
||||
# ========== 1. 读取输入参数 ==========
|
||||
SPECIFIC_TAG="${{ github.event.inputs.tag_name }}"
|
||||
RELEASES_JSON='${{ steps.get-releases.outputs.releases_json }}'
|
||||
|
||||
# ========== 2. Alist 连接信息 ==========
|
||||
ALIST_URL="https://alist.shiqianjiang.cn" # https://pan.example.com
|
||||
ALIST_USER="${{ secrets.WEBDAV_USERNAME }}" # Alist 登录账号
|
||||
ALIST_PASS="${{ secrets.WEBDAV_PASSWORD }}" # Alist 登录密码
|
||||
ALIST_DIR="/yd/ceru" # 目标根目录
|
||||
|
||||
# ========== 3. 登录拿 token ==========
|
||||
echo "正在登录 Alist ..."
|
||||
login_resp=$(curl -s -X POST "$ALIST_URL/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"username\": \"$ALIST_USER\",
|
||||
\"password\": \"$ALIST_PASS\"
|
||||
}")
|
||||
echo "$login_resp"
|
||||
token=$(echo "$login_resp" | jq -r '.data.token // empty')
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
echo "❌ 登录失败,返回:$login_resp"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 登录成功,token 已获取"
|
||||
|
||||
# ========== 4. 循环处理 release ==========
|
||||
releases_count=$(echo "$RELEASES_JSON" | jq '. | length')
|
||||
echo "找到 $releases_count 个 releases"
|
||||
for i in $(seq 0 $(($releases_count - 1))); do
|
||||
release=$(echo "$RELEASES_JSON" | jq -c ".[$i]")
|
||||
tag_name=$(echo "$release" | jq -r '.tag_name')
|
||||
release_id=$(echo "$release" | jq -r '.id')
|
||||
|
||||
[ -n "$SPECIFIC_TAG" ] && [ "$tag_name" != "$SPECIFIC_TAG" ] && {
|
||||
echo "跳过 $tag_name,不是指定标签"
|
||||
continue
|
||||
}
|
||||
|
||||
echo "处理版本: $tag_name (ID: $release_id)"
|
||||
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 个资源文件"
|
||||
|
||||
for j in $(seq 0 $(($assets_count - 1))); do
|
||||
asset=$(echo "$assets_json" | jq -c ".[$j]")
|
||||
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_download_$(date +%s)_$j"
|
||||
|
||||
# 下载
|
||||
curl -sL -o "$safe_filename" \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Accept: application/octet-stream" \
|
||||
"$asset_url" || {
|
||||
echo "❌ 下载失败: $asset_name"
|
||||
continue
|
||||
}
|
||||
|
||||
# 大小校验
|
||||
actual_size=$(wc -c < "$safe_filename")
|
||||
[ "$actual_size" -ne "$asset_size" ] && {
|
||||
echo "❌ 文件大小不匹配: $asset_name"
|
||||
rm -f "$safe_filename"
|
||||
continue
|
||||
}
|
||||
|
||||
# 组装远程路径(URL 编码)
|
||||
remote_path="$ALIST_DIR/$tag_name/$asset_name"
|
||||
file_path_encoded=$(printf %s "$remote_path" | jq -sRr @uri)
|
||||
echo "上传到 Alist: $remote_path"
|
||||
|
||||
# 调用 /api/fs/put 上传(带 As-Task 异步)
|
||||
response=$(
|
||||
curl -s -X PUT "$ALIST_URL/api/fs/put" \
|
||||
-H "Authorization: $token" \
|
||||
-H "File-Path: $file_path_encoded" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
-H "Content-Length: $actual_size" \
|
||||
-H "As-Task: true" \
|
||||
--data-binary @"$safe_filename"
|
||||
)
|
||||
echo "==== 上传接口原始返回 ===="
|
||||
echo "$response"
|
||||
code=$(echo "$response" | jq -r '.code // empty')
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "✅ Alist 上传任务创建成功: $asset_name"
|
||||
else
|
||||
echo "❌ Alist 上传失败: $asset_name"
|
||||
fi
|
||||
|
||||
rm -f "$safe_filename"
|
||||
echo "----------------------------------------"
|
||||
done
|
||||
echo "版本 $tag_name 处理完成"
|
||||
echo "========================================"
|
||||
done
|
||||
|
||||
# ========== 5. 退出登录 ==========
|
||||
echo "退出登录 ..."
|
||||
curl -s -X POST "$ALIST_URL/api/auth/logout" \
|
||||
-H "Authorization: $token" > /dev/null || true
|
||||
|
||||
echo "🎉 Alist 同步完成"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "同步任务已完成!"
|
||||
echo "请检查 Alist 中的文件是否正确上传。"
|
||||
echo "如果遇到问题,请检查以下配置:"
|
||||
echo "1. ALIST_URL - Alist 服务器地址"
|
||||
echo "2. ALIST_USERNAME - Alist 登录账号"
|
||||
echo "3. ALIST_PASSWORD - Alist 登录密码"
|
||||
echo "4. GITHUB_TOKEN - GitHub 访问令牌"
|
||||
160
.github/workflows/uploadpan.yml
vendored
Normal file
160
.github/workflows/uploadpan.yml
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
name: Sync Existing Releases to WebDAV
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: '要同步的特定标签(如 v1.0.0),留空则同步所有版本'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-releases-to-webdav:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl
|
||||
|
||||
- name: Get all releases
|
||||
id: get-releases
|
||||
run: |
|
||||
response=$(curl -s \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases")
|
||||
echo "releases_json=$(echo "$response" | jq -c '.')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Sync releases to WebDAV
|
||||
run: |
|
||||
# 读取输入参数
|
||||
SPECIFIC_TAG="${{ github.event.inputs.tag_name }}"
|
||||
RELEASES_JSON='${{ steps.get-releases.outputs.releases_json }}'
|
||||
|
||||
echo "开始同步 releases..."
|
||||
echo "特定标签: ${SPECIFIC_TAG:-所有版本}"
|
||||
echo "WebDAV 根路径: ${{ secrets.WEBDAV_BASE_URL }}/yd/ceru"
|
||||
|
||||
# 处理每个 release
|
||||
releases_count=$(echo "$RELEASES_JSON" | jq '. | length')
|
||||
echo "找到 $releases_count 个 releases"
|
||||
|
||||
for i in $(seq 0 $(($releases_count - 1))); do
|
||||
release=$(echo "$RELEASES_JSON" | jq -c ".[$i]")
|
||||
tag_name=$(echo "$release" | jq -r '.tag_name')
|
||||
release_id=$(echo "$release" | jq -r '.id')
|
||||
|
||||
if [ -n "$SPECIFIC_TAG" ] && [ "$tag_name" != "$SPECIFIC_TAG" ]; then
|
||||
echo "跳过 $tag_name,不是指定的标签 $SPECIFIC_TAG"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "正在处理版本: $tag_name (ID: $release_id)"
|
||||
|
||||
# 获取该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 个资源文件"
|
||||
|
||||
# 处理每个asset
|
||||
for j in $(seq 0 $(($assets_count - 1))); do
|
||||
asset=$(echo "$assets_json" | jq -c ".[$j]")
|
||||
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_download"
|
||||
|
||||
if ! curl -sL -o "$safe_filename" \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Accept: application/octet-stream" \
|
||||
"$asset_url"; then
|
||||
echo "❌ 下载失败: $asset_name"
|
||||
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"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "上传到 WebDAV: $asset_name"
|
||||
|
||||
# 构建远程路径
|
||||
remote_path="/yd/ceru/$tag_name/$asset_name"
|
||||
full_url="${{ secrets.WEBDAV_BASE_URL }}$remote_path"
|
||||
|
||||
echo "完整路径: $full_url"
|
||||
|
||||
# 使用 WebDAV PUT 方法上传文件
|
||||
if curl -s -f -X PUT \
|
||||
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||
-T "$safe_filename" \
|
||||
"$full_url"; then
|
||||
|
||||
echo "✅ WebDAV 上传成功: $asset_name"
|
||||
|
||||
# 验证文件是否存在
|
||||
echo "验证文件是否存在..."
|
||||
sleep 2
|
||||
|
||||
if curl -s -f -u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||
-X PROPFIND \
|
||||
-H "Depth: 0" \
|
||||
"$full_url" > /dev/null 2>&1; then
|
||||
echo "✅ 文件确认存在: $asset_name"
|
||||
else
|
||||
echo "⚠️ 文件验证失败,但上传可能成功"
|
||||
fi
|
||||
|
||||
else
|
||||
echo "❌ WebDAV 上传失败: $asset_name"
|
||||
echo "尝试创建目录后重新上传..."
|
||||
|
||||
# 尝试先创建目录
|
||||
dir_path="/yd/ceru/$tag_name"
|
||||
dir_url="${{ secrets.WEBDAV_BASE_URL }}$dir_path"
|
||||
|
||||
if curl -s -f -X MKCOL \
|
||||
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||
"$dir_url"; then
|
||||
echo "✅ 目录创建成功: $dir_path"
|
||||
|
||||
# 重新尝试上传文件
|
||||
if curl -s -f -X PUT \
|
||||
-u "${{ secrets.WEBDAV_USERNAME }}:${{ secrets.WEBDAV_PASSWORD }}" \
|
||||
-T "$safe_filename" \
|
||||
"$full_url"; then
|
||||
echo "✅ 重新上传成功: $asset_name"
|
||||
else
|
||||
echo "❌ 重新上传失败: $asset_name"
|
||||
fi
|
||||
else
|
||||
echo "❌ 目录创建失败: $dir_path"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 安全删除临时文件
|
||||
rm -f "$safe_filename"
|
||||
echo "----------------------------------------"
|
||||
else
|
||||
echo "❌ 文件不存在: $safe_filename"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "🎉 WebDAV 同步完成"
|
||||
14
README.md
14
README.md
@@ -1,14 +1,12 @@
|
||||
# Ceru Music(澜音)
|
||||
|
||||
|
||||
|
||||
一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。
|
||||
|
||||
## 项目简介
|
||||
|
||||
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-20250827175023917.png" alt="image-20250827175023917" style="zoom: 33%;" /><img src="assets/image-20250827175109430.png" alt="image-20250827175109430" style="zoom:33%;" />
|
||||
|
||||
## 技术栈
|
||||
|
||||
@@ -22,8 +20,6 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
|
||||
|
||||
## 主要功能
|
||||
|
||||
|
||||
|
||||
- 提供插件加载与管理功能,支持通过合规插件获取公开音乐信息
|
||||
- 支持通过插件获取歌词、专辑封面等公开元数据
|
||||
- 支持虚拟滚动列表,优化大量数据渲染性能
|
||||
@@ -36,16 +32,12 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
|
||||
|
||||
### 推荐开发环境
|
||||
|
||||
|
||||
|
||||
- **IDE**: VS Code 或 WebStorm
|
||||
- **Node.js 版本**: 22 及以上
|
||||
- **包管理器**: **yarn**
|
||||
|
||||
### 项目设置
|
||||
|
||||
|
||||
|
||||
1. 安装依赖:
|
||||
|
||||
```bash
|
||||
@@ -66,8 +58,6 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
|
||||
|
||||
### 平台构建指令
|
||||
|
||||
|
||||
|
||||
- Windows
|
||||
|
||||
```bash
|
||||
@@ -86,8 +76,6 @@ Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工
|
||||
yarn build:linux
|
||||
```
|
||||
|
||||
|
||||
|
||||
> 提示:构建后的应用仅包含播放器框架,需用户自行配置合规插件方可获取音乐数据。
|
||||
|
||||
## 文档与资源
|
||||
|
||||
@@ -3,9 +3,10 @@ import { defineConfig } from 'vitepress'
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
lang: 'zh-CN',
|
||||
title: "Ceru Music",
|
||||
base: process.env.BASE_URL ?? '/CeruMusic/',
|
||||
description: "Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。",
|
||||
title: 'Ceru Music',
|
||||
base: '/',
|
||||
description:
|
||||
'Ceru Music 是基于 Electron 和 Vue 开发的跨平台桌面音乐播放器工具,一个跨平台的音乐播放器应用,支持基于合规插件获取公开音乐信息与播放功能。',
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: '/logo.svg',
|
||||
@@ -37,7 +38,6 @@ export default defineConfig({
|
||||
{ icon: 'qq', link: 'https://qm.qq.com/q/IDpQnbGd06' },
|
||||
{ icon: 'beatsbydre', link: 'https://shiqianjiang.cn' },
|
||||
{ icon: 'bilibili', link: 'https://space.bilibili.com/696709986' }
|
||||
|
||||
],
|
||||
footer: {
|
||||
message: 'Released under the Apache License 2.0 License.',
|
||||
@@ -51,6 +51,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
lastUpdated: true,
|
||||
head: [['link', { rel: 'icon', href: (process.env.BASE_URL ?? '/CeruMusic/') + 'logo.svg' }]]
|
||||
head: [['link', { rel: 'icon', href: '/logo.svg' }]]
|
||||
})
|
||||
console.log(process.env.BASE_URL_DOCS)
|
||||
// Smooth scrolling functions
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
::view-transition-new(*) {
|
||||
animation: globalDark .5s ease-in;
|
||||
animation: globalDark 0.5s ease-in;
|
||||
}
|
||||
|
||||
@keyframes globalDark {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { nextTick, provide } from 'vue'
|
||||
// 判断是否能使用 startViewTransition
|
||||
const enableTransitions = () => {
|
||||
return 'startViewTransition' in document && window.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
||||
return (
|
||||
'startViewTransition' in document &&
|
||||
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
|
||||
)
|
||||
}
|
||||
// 切换动画
|
||||
export const toggleDark = (isDark: any) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './style.css'
|
||||
import './dark.css'
|
||||
import MyLayout from './MyLayout.vue';
|
||||
import MyLayout from './MyLayout.vue'
|
||||
// history.scrollRestoration = 'manual'
|
||||
|
||||
export default {
|
||||
@@ -11,6 +11,3 @@ export default {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
142
docs/alist-config.md
Normal file
142
docs/alist-config.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Alist 下载配置说明
|
||||
|
||||
## 概述
|
||||
|
||||
项目已从 GitHub 下载方式切换到 Alist API 下载方式,包括:
|
||||
|
||||
- 桌面应用的自动更新功能 (`src/main/autoUpdate.ts`)
|
||||
- 官方网站的下载功能 (`website/script.js`)
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 1. 修改 Alist 域名
|
||||
|
||||
#### 桌面应用配置
|
||||
|
||||
在 `src/main/autoUpdate.ts` 文件中,Alist 域名已配置为:
|
||||
|
||||
```typescript
|
||||
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
|
||||
```
|
||||
|
||||
#### 网站配置
|
||||
|
||||
在 `website/script.js` 文件中,Alist 域名已配置为:
|
||||
|
||||
```javascript
|
||||
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
|
||||
```
|
||||
|
||||
如需修改域名,请同时更新这两个文件中的 `ALIST_BASE_URL` 配置。
|
||||
|
||||
### 2. 认证信息
|
||||
|
||||
已配置的认证信息:
|
||||
|
||||
- 用户名: `ceruupdata`
|
||||
- 密码: `123456`
|
||||
|
||||
### 3. 文件路径格式
|
||||
|
||||
文件在 Alist 中的路径格式为:`/{version}/{文件名}`
|
||||
|
||||
例如:
|
||||
|
||||
- 版本 `v1.0.0` 的安装包 `app-setup.exe` 路径为:`/v1.0.0/app-setup.exe`
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 桌面应用自动更新
|
||||
|
||||
1. **认证**: 使用配置的用户名和密码向 Alist API 获取认证 token
|
||||
2. **获取文件信息**: 使用 token 调用 `/api/fs/get` 接口获取文件信息和签名
|
||||
3. **下载**: 使用带签名的直接下载链接下载文件
|
||||
4. **备用方案**: 如果 Alist 失败,自动回退到原始 URL 下载
|
||||
|
||||
### 网站下载功能
|
||||
|
||||
1. **获取版本列表**: 调用 `/api/fs/list` 获取根目录下的版本文件夹
|
||||
2. **获取文件列表**: 获取最新版本文件夹中的所有文件
|
||||
3. **平台匹配**: 根据用户平台自动匹配对应的安装包文件
|
||||
4. **生成下载链接**: 获取文件的直接下载链接
|
||||
5. **备用方案**: 如果 Alist 失败,自动回退到 GitHub API
|
||||
|
||||
## API 接口
|
||||
|
||||
### 认证接口
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
{
|
||||
"username": "ceruupdata",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
### 获取文件信息接口
|
||||
|
||||
```
|
||||
POST /api/fs/get
|
||||
Headers: Authorization: {token} # 注意:直接使用 token,不需要 "Bearer " 前缀
|
||||
{
|
||||
"path": "/{version}/{fileName}"
|
||||
}
|
||||
```
|
||||
|
||||
### 获取文件列表接口
|
||||
|
||||
```
|
||||
POST /api/fs/list
|
||||
Headers: Authorization: {token} # 注意:直接使用 token,不需要 "Bearer " 前缀
|
||||
{
|
||||
"path": "/",
|
||||
"password": "",
|
||||
"page": 1,
|
||||
"per_page": 100,
|
||||
"refresh": false
|
||||
}
|
||||
```
|
||||
|
||||
### 下载链接格式
|
||||
|
||||
```
|
||||
{ALIST_BASE_URL}/d/{filePath}?sign={sign}
|
||||
```
|
||||
|
||||
## 测试方法
|
||||
|
||||
项目包含了一个测试脚本来验证 Alist 连接:
|
||||
|
||||
```bash
|
||||
node scripts/test-alist.js
|
||||
```
|
||||
|
||||
该脚本会:
|
||||
|
||||
1. 测试服务器连通性
|
||||
2. 测试用户认证
|
||||
3. 测试文件列表获取
|
||||
4. 测试文件信息获取
|
||||
|
||||
## 备用机制
|
||||
|
||||
两个组件都实现了备用机制:
|
||||
|
||||
### 桌面应用
|
||||
|
||||
- 主要:使用 Alist API 下载
|
||||
- 备用:如果 Alist 失败,使用原始 URL 下载
|
||||
|
||||
### 网站
|
||||
|
||||
- 主要:使用 Alist API 获取版本和文件信息
|
||||
- 备用:如果 Alist 失败,回退到 GitHub API
|
||||
- 最终备用:跳转到 GitHub releases 页面
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保 Alist 服务器可以正常访问
|
||||
2. 确保配置的用户名和密码有权限访问相应的文件路径
|
||||
3. 文件必须按照指定的路径格式存放在 Alist 中
|
||||
4. 网站会自动检测用户操作系统并推荐对应的下载版本
|
||||
5. 所有下载都会显示文件大小信息
|
||||
99
docs/alist-migration-summary.md
Normal file
99
docs/alist-migration-summary.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Alist 迁移完成总结
|
||||
|
||||
## 修改概述
|
||||
|
||||
项目已成功从 GitHub 下载方式迁移到 Alist API 下载方式。
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. 桌面应用自动更新 (`src/main/autoUpdate.ts`)
|
||||
|
||||
- ✅ 添加了 Alist API 配置
|
||||
- ✅ 实现了 Alist 认证功能
|
||||
- ✅ 实现了 Alist 文件下载功能
|
||||
- ✅ 添加了备用机制(Alist 失败时回退到原始 URL)
|
||||
- ✅ 修复了 Authorization 头格式(使用直接 token 而非 Bearer 格式)
|
||||
|
||||
### 2. 官方网站下载功能 (`website/script.js`)
|
||||
|
||||
- ✅ 添加了 Alist API 配置
|
||||
- ✅ 实现了 Alist 认证功能
|
||||
- ✅ 实现了版本列表获取功能
|
||||
- ✅ 实现了文件列表获取功能
|
||||
- ✅ 实现了平台文件匹配功能
|
||||
- ✅ 添加了多层备用机制(Alist → GitHub API → GitHub 页面)
|
||||
- ✅ 修复了 Authorization 头格式
|
||||
|
||||
### 3. 配置文档
|
||||
|
||||
- ✅ 创建了详细的配置说明 (`docs/alist-config.md`)
|
||||
- ✅ 创建了迁移总结文档 (`docs/alist-migration-summary.md`)
|
||||
|
||||
### 4. 测试脚本
|
||||
|
||||
- ✅ 创建了 Alist 连接测试脚本 (`scripts/test-alist.js`)
|
||||
- ✅ 创建了认证格式测试脚本 (`scripts/auth-test.js`)
|
||||
|
||||
## 配置信息
|
||||
|
||||
### Alist 服务器配置
|
||||
|
||||
- **服务器地址**: `http://47.96.72.224:5244`
|
||||
- **用户名**: `ceruupdate`
|
||||
- **密码**: `123456`
|
||||
- **文件路径格式**: `/{version}/{文件名}`
|
||||
|
||||
### Authorization 头格式
|
||||
|
||||
经过测试确认,正确的格式是:
|
||||
|
||||
```
|
||||
Authorization: {token}
|
||||
```
|
||||
|
||||
**注意**: 不需要 "Bearer " 前缀
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 桌面应用
|
||||
|
||||
1. **智能下载**: 优先使用 Alist API,失败时自动回退
|
||||
2. **进度显示**: 支持下载进度显示和节流
|
||||
3. **错误处理**: 完善的错误处理和日志记录
|
||||
|
||||
### 网站
|
||||
|
||||
1. **自动检测**: 自动检测用户操作系统并推荐对应版本
|
||||
2. **版本信息**: 自动获取最新版本信息和文件大小
|
||||
3. **多层备用**: Alist → GitHub API → GitHub 页面的三层备用机制
|
||||
4. **用户体验**: 加载状态、成功通知、错误提示
|
||||
|
||||
## 测试结果
|
||||
|
||||
✅ **Alist 连接测试**: 通过
|
||||
✅ **认证测试**: 通过
|
||||
✅ **文件列表获取**: 通过
|
||||
✅ **Authorization 头格式**: 已修复并验证
|
||||
|
||||
## 可用文件
|
||||
|
||||
测试显示 Alist 服务器当前包含以下文件:
|
||||
|
||||
- `v1.2.1/` (版本目录)
|
||||
- `1111`
|
||||
- `L3YxLjIuMS8tMS4yLjEtYXJtNjQtbWFjLnppcA==`
|
||||
- `file2.msi`
|
||||
- `file.msi`
|
||||
|
||||
## 后续维护
|
||||
|
||||
1. **添加新版本**: 在 Alist 中创建新的版本目录(如 `v1.2.2/`)
|
||||
2. **上传文件**: 将对应平台的安装包上传到版本目录中
|
||||
3. **文件命名**: 确保文件名包含平台标识(如 `windows`, `mac`, `linux` 等)
|
||||
|
||||
## 备注
|
||||
|
||||
- 所有修改都保持了向后兼容性
|
||||
- 实现了完善的错误处理和备用机制
|
||||
- 用户体验不会因为迁移而受到影响
|
||||
- 可以随时回退到 GitHub 下载方式
|
||||
444
docs/songlist-api.md
Normal file
444
docs/songlist-api.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# 歌单管理 API 文档
|
||||
|
||||
本文档介绍了 CeruMusic 中歌单管理功能的使用方法,包括后端服务类和前端 API 接口。
|
||||
|
||||
## 概述
|
||||
|
||||
歌单管理系统提供了完整的歌单和歌曲管理功能,包括:
|
||||
|
||||
- 📁 **歌单管理**:创建、删除、编辑、搜索歌单
|
||||
- 🎵 **歌曲管理**:添加、移除、搜索歌单中的歌曲
|
||||
- 📊 **统计分析**:获取歌单和歌曲的统计信息
|
||||
- 🔧 **数据维护**:验证和修复歌单数据完整性
|
||||
- ⚡ **批量操作**:支持批量删除和批量移除操作
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
前端 (Renderer Process)
|
||||
├── src/renderer/src/api/songList.ts # 前端 API 封装
|
||||
├── src/renderer/src/examples/songListUsage.ts # 使用示例
|
||||
└── src/types/songList.ts # TypeScript 类型定义
|
||||
|
||||
主进程 (Main Process)
|
||||
├── src/main/events/songList.ts # IPC 事件处理
|
||||
├── src/main/services/songList/ManageSongList.ts # 歌单管理服务
|
||||
└── src/main/services/songList/PlayListSongs.ts # 歌曲管理基类
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 前端使用
|
||||
|
||||
```typescript
|
||||
import songListAPI from '@/api/songList'
|
||||
|
||||
// 创建歌单
|
||||
const result = await songListAPI.create('我的收藏', '我最喜欢的歌曲')
|
||||
if (result.success) {
|
||||
console.log('歌单创建成功,ID:', result.data?.id)
|
||||
}
|
||||
|
||||
// 获取所有歌单
|
||||
const playlists = await songListAPI.getAll()
|
||||
if (playlists.success) {
|
||||
console.log('歌单列表:', playlists.data)
|
||||
}
|
||||
|
||||
// 添加歌曲到歌单
|
||||
const songs = [
|
||||
/* 歌曲数据 */
|
||||
]
|
||||
await songListAPI.addSongs(playlistId, songs)
|
||||
```
|
||||
|
||||
### 2. 类型安全
|
||||
|
||||
所有 API 都提供了完整的 TypeScript 类型支持:
|
||||
|
||||
```typescript
|
||||
import type { IPCResponse, SongListStatistics } from '@/types/songList'
|
||||
|
||||
const stats: IPCResponse<SongListStatistics> = await songListAPI.getStatistics()
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### 歌单管理
|
||||
|
||||
#### `create(name, description?, source?)`
|
||||
|
||||
创建新歌单
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.create('我的收藏', '描述', 'local')
|
||||
// 返回: { success: boolean, data?: { id: string }, error?: string }
|
||||
```
|
||||
|
||||
#### `getAll()`
|
||||
|
||||
获取所有歌单
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.getAll()
|
||||
// 返回: { success: boolean, data?: SongList[], error?: string }
|
||||
```
|
||||
|
||||
#### `getById(hashId)`
|
||||
|
||||
根据ID获取歌单
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.getById('playlist-id')
|
||||
// 返回: { success: boolean, data?: SongList | null, error?: string }
|
||||
```
|
||||
|
||||
#### `delete(hashId)`
|
||||
|
||||
删除歌单
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.delete('playlist-id')
|
||||
// 返回: { success: boolean, error?: string }
|
||||
```
|
||||
|
||||
#### `batchDelete(hashIds)`
|
||||
|
||||
批量删除歌单
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.batchDelete(['id1', 'id2'])
|
||||
// 返回: { success: boolean, data?: { success: string[], failed: string[] } }
|
||||
```
|
||||
|
||||
#### `edit(hashId, updates)`
|
||||
|
||||
编辑歌单信息
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.edit('playlist-id', {
|
||||
name: '新名称',
|
||||
description: '新描述'
|
||||
})
|
||||
```
|
||||
|
||||
#### `search(keyword, source?)`
|
||||
|
||||
搜索歌单
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.search('关键词', 'local')
|
||||
// 返回: { success: boolean, data?: SongList[], error?: string }
|
||||
```
|
||||
|
||||
### 歌曲管理
|
||||
|
||||
#### `addSongs(hashId, songs)`
|
||||
|
||||
添加歌曲到歌单
|
||||
|
||||
```typescript
|
||||
const songs: Songs[] = [
|
||||
/* 歌曲数据 */
|
||||
]
|
||||
const result = await songListAPI.addSongs('playlist-id', songs)
|
||||
```
|
||||
|
||||
#### `removeSong(hashId, songmid)`
|
||||
|
||||
移除单首歌曲
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.removeSong('playlist-id', 'song-id')
|
||||
// 返回: { success: boolean, data?: boolean, error?: string }
|
||||
```
|
||||
|
||||
#### `removeSongs(hashId, songmids)`
|
||||
|
||||
批量移除歌曲
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.removeSongs('playlist-id', ['song1', 'song2'])
|
||||
// 返回: { success: boolean, data?: { removed: number, notFound: number } }
|
||||
```
|
||||
|
||||
#### `getSongs(hashId)`
|
||||
|
||||
获取歌单中的歌曲
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.getSongs('playlist-id')
|
||||
// 返回: { success: boolean, data?: readonly Songs[], error?: string }
|
||||
```
|
||||
|
||||
#### `searchSongs(hashId, keyword)`
|
||||
|
||||
搜索歌单中的歌曲
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.searchSongs('playlist-id', '关键词')
|
||||
// 返回: { success: boolean, data?: Songs[], error?: string }
|
||||
```
|
||||
|
||||
### 统计信息
|
||||
|
||||
#### `getStatistics()`
|
||||
|
||||
获取歌单统计信息
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.getStatistics()
|
||||
// 返回: {
|
||||
// success: boolean,
|
||||
// data?: {
|
||||
// total: number,
|
||||
// bySource: Record<string, number>,
|
||||
// lastUpdated: string
|
||||
// }
|
||||
// }
|
||||
```
|
||||
|
||||
#### `getSongStatistics(hashId)`
|
||||
|
||||
获取歌单歌曲统计信息
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.getSongStatistics('playlist-id')
|
||||
// 返回: {
|
||||
// success: boolean,
|
||||
// data?: {
|
||||
// total: number,
|
||||
// bySinger: Record<string, number>,
|
||||
// byAlbum: Record<string, number>,
|
||||
// lastModified: string
|
||||
// }
|
||||
// }
|
||||
```
|
||||
|
||||
### 数据维护
|
||||
|
||||
#### `validateIntegrity(hashId)`
|
||||
|
||||
验证歌单数据完整性
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.validateIntegrity('playlist-id')
|
||||
// 返回: { success: boolean, data?: { isValid: boolean, issues: string[] } }
|
||||
```
|
||||
|
||||
#### `repairData(hashId)`
|
||||
|
||||
修复歌单数据
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.repairData('playlist-id')
|
||||
// 返回: { success: boolean, data?: { fixed: boolean, changes: string[] } }
|
||||
```
|
||||
|
||||
### 便捷方法
|
||||
|
||||
#### `getPlaylistDetail(hashId)`
|
||||
|
||||
获取歌单详细信息(包含歌曲列表)
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.getPlaylistDetail('playlist-id')
|
||||
// 返回: {
|
||||
// playlist: SongList | null,
|
||||
// songs: readonly Songs[],
|
||||
// success: boolean,
|
||||
// error?: string
|
||||
// }
|
||||
```
|
||||
|
||||
#### `checkAndRepair(hashId)`
|
||||
|
||||
检查并修复歌单数据
|
||||
|
||||
```typescript
|
||||
const result = await songListAPI.checkAndRepair('playlist-id')
|
||||
// 返回: {
|
||||
// needsRepair: boolean,
|
||||
// repairResult?: RepairResult,
|
||||
// success: boolean,
|
||||
// error?: string
|
||||
// }
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有 API 都返回统一的响应格式:
|
||||
|
||||
```typescript
|
||||
interface IPCResponse<T = any> {
|
||||
success: boolean // 操作是否成功
|
||||
data?: T // 返回的数据
|
||||
error?: string // 错误信息
|
||||
message?: string // 附加消息
|
||||
code?: string // 错误码
|
||||
}
|
||||
```
|
||||
|
||||
### 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
| -------------------- | ------------ |
|
||||
| `INVALID_HASH_ID` | 无效的歌单ID |
|
||||
| `PLAYLIST_NOT_FOUND` | 歌单不存在 |
|
||||
| `EMPTY_NAME` | 歌单名称为空 |
|
||||
| `CREATE_FAILED` | 创建失败 |
|
||||
| `DELETE_FAILED` | 删除失败 |
|
||||
| `EDIT_FAILED` | 编辑失败 |
|
||||
| `READ_FAILED` | 读取失败 |
|
||||
| `WRITE_FAILED` | 写入失败 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 完整的歌单管理流程
|
||||
|
||||
```typescript
|
||||
import songListAPI from '@/api/songList'
|
||||
|
||||
async function managePlaylist() {
|
||||
try {
|
||||
// 1. 创建歌单
|
||||
const createResult = await songListAPI.create('我的收藏', '我最喜欢的歌曲')
|
||||
if (!createResult.success) {
|
||||
throw new Error(createResult.error)
|
||||
}
|
||||
|
||||
const playlistId = createResult.data!.id
|
||||
|
||||
// 2. 添加歌曲
|
||||
const songs = [
|
||||
{
|
||||
songmid: 'song1',
|
||||
name: '歌曲1',
|
||||
singer: '歌手1',
|
||||
albumName: '专辑1',
|
||||
albumId: 'album1',
|
||||
duration: 240,
|
||||
source: 'local'
|
||||
}
|
||||
]
|
||||
|
||||
await songListAPI.addSongs(playlistId, songs)
|
||||
|
||||
// 3. 获取歌单详情
|
||||
const detail = await songListAPI.getPlaylistDetail(playlistId)
|
||||
console.log('歌单信息:', detail.playlist)
|
||||
console.log('歌曲列表:', detail.songs)
|
||||
|
||||
// 4. 搜索歌曲
|
||||
const searchResult = await songListAPI.searchSongs(playlistId, '歌曲')
|
||||
console.log('搜索结果:', searchResult.data)
|
||||
|
||||
// 5. 获取统计信息
|
||||
const stats = await songListAPI.getSongStatistics(playlistId)
|
||||
console.log('统计信息:', stats.data)
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React 组件中的使用
|
||||
|
||||
```typescript
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import songListAPI from '@/api/songList'
|
||||
import type { SongList } from '@common/types/songList'
|
||||
|
||||
const PlaylistManager: React.FC = () => {
|
||||
const [playlists, setPlaylists] = useState<SongList[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 加载歌单列表
|
||||
const loadPlaylists = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await songListAPI.getAll()
|
||||
if (result.success) {
|
||||
setPlaylists(result.data || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载歌单失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新歌单
|
||||
const createPlaylist = async (name: string) => {
|
||||
const result = await songListAPI.create(name)
|
||||
if (result.success) {
|
||||
await loadPlaylists() // 重新加载列表
|
||||
}
|
||||
}
|
||||
|
||||
// 删除歌单
|
||||
const deletePlaylist = async (id: string) => {
|
||||
const result = await songListAPI.safeDelete(id, async () => {
|
||||
return confirm('确定要删除这个歌单吗?')
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
await loadPlaylists() // 重新加载列表
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadPlaylists()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading ? (
|
||||
<div>加载中...</div>
|
||||
) : (
|
||||
<div>
|
||||
{playlists.map(playlist => (
|
||||
<div key={playlist.id}>
|
||||
<h3>{playlist.name}</h3>
|
||||
<p>{playlist.description}</p>
|
||||
<button onClick={() => deletePlaylist(playlist.id)}>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
1. **批量操作**:使用 `batchDelete` 和 `removeSongs` 进行批量操作
|
||||
2. **数据缓存**:在前端适当缓存歌单列表,避免频繁请求
|
||||
3. **懒加载**:歌曲列表可以按需加载,不必一次性加载所有数据
|
||||
4. **错误恢复**:使用 `checkAndRepair` 定期检查数据完整性
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有 API 都是异步的,需要使用 `await` 或 `.then()`
|
||||
2. 歌单 ID (`hashId`) 是唯一标识符,不要与数组索引混淆
|
||||
3. 歌曲 ID (`songmid`) 可能是字符串或数字类型
|
||||
4. 删除操作是不可逆的,建议使用 `safeDelete` 方法
|
||||
5. 大量数据操作时注意性能影响
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-10)
|
||||
|
||||
- ✨ 初始版本发布
|
||||
- ✨ 完整的歌单管理功能
|
||||
- ✨ 批量操作支持
|
||||
- ✨ 数据完整性检查
|
||||
- ✨ TypeScript 类型支持
|
||||
- ✨ 详细的使用文档和示例
|
||||
|
||||
---
|
||||
|
||||
如有问题或建议,请提交 Issue 或 Pull Request。
|
||||
150
docs/webdav-sync-setup.md
Normal file
150
docs/webdav-sync-setup.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# WebDAV 同步配置指南
|
||||
|
||||
本项目包含两个 GitHub Actions 工作流,用于自动将 GitHub Releases 同步到 alist(WebDAV 服务器)。
|
||||
|
||||
## 工作流说明
|
||||
|
||||
### 1. 手动同步工作流 (`sync-releases-to-webdav.yml`)
|
||||
|
||||
- **触发方式**: 手动触发 (workflow_dispatch)
|
||||
- **功能**: 同步现有的所有版本或指定版本到 WebDAV
|
||||
- **参数**:
|
||||
- `tag_name`: 可选,指定要同步的版本标签(如 v1.0.0),留空则同步所有版本
|
||||
|
||||
### 2. 自动同步工作流 (集成在 `main.yml` 中)
|
||||
|
||||
- **触发方式**: 在 AutoBuild 完成后自动触发
|
||||
- **功能**: 自动将新构建的版本同步到 WebDAV
|
||||
- **参数**: 无需手动设置,自动获取发布信息
|
||||
|
||||
### 3. 独立自动同步工作流 (`auto-sync-release.yml`)
|
||||
|
||||
- **触发方式**: 当新版本发布时自动触发 (on release published)
|
||||
- **功能**: 备用的自动同步机制
|
||||
- **参数**: 无需手动设置,自动获取发布信息
|
||||
|
||||
## 配置要求
|
||||
|
||||
在 GitHub 仓库的 Settings > Secrets and variables > Actions 中添加以下密钥:
|
||||
|
||||
### 必需的 Secrets
|
||||
|
||||
1. **WEBDAV_BASE_URL**
|
||||
- 描述: WebDAV 服务器的基础 URL
|
||||
- 示例: `https://your-alist-domain.com/dav`
|
||||
- 注意: 不要在末尾添加斜杠
|
||||
|
||||
2. **WEBDAV_USERNAME**
|
||||
- 描述: WebDAV 服务器的用户名
|
||||
- 示例: `admin`
|
||||
|
||||
3. **WEBDAV_PASSWORD**
|
||||
- 描述: WebDAV 服务器的密码
|
||||
- 示例: `your-password`
|
||||
|
||||
4. **GITHUB_TOKEN**
|
||||
- 描述: GitHub 访问令牌(通常自动提供)
|
||||
- 注意: 如果默认的 `GITHUB_TOKEN` 权限不足,可能需要创建个人访问令牌
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 手动同步现有版本
|
||||
|
||||
1. 进入 GitHub 仓库的 Actions 页面
|
||||
2. 选择 "Sync Existing Releases to WebDAV" 工作流
|
||||
3. 点击 "Run workflow"
|
||||
4. 可选择指定版本标签或留空同步所有版本
|
||||
5. 点击 "Run workflow" 开始执行
|
||||
|
||||
### 自动同步新版本
|
||||
|
||||
现在有两种自动同步方式:
|
||||
|
||||
1. **集成同步** (推荐): 在主构建工作流 (`main.yml`) 中集成了 WebDAV 同步,当您推送 `v*` 标签时,会自动执行:
|
||||
- 构建应用 → 创建 Release → 同步到 WebDAV
|
||||
2. **独立同步**: 当您手动发布 Release 时,`auto-sync-release.yml` 工作流会自动触发
|
||||
|
||||
推荐使用集成同步方式,因为它确保了构建和同步的一致性。
|
||||
|
||||
## 文件结构
|
||||
|
||||
同步后的文件将按以下结构存储在 alist 中:
|
||||
|
||||
```
|
||||
/yd/ceru/
|
||||
├── v1.0.0/
|
||||
│ ├── app-setup.exe
|
||||
│ ├── app.dmg
|
||||
│ └── app.AppImage
|
||||
├── v1.1.0/
|
||||
│ ├── app-setup.exe
|
||||
│ ├── app.dmg
|
||||
│ └── app.AppImage
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **上传失败**
|
||||
- 检查 WebDAV 服务器是否正常运行
|
||||
- 验证用户名和密码是否正确
|
||||
- 确认 WebDAV URL 格式正确
|
||||
|
||||
2. **权限错误**
|
||||
- 确保 WebDAV 用户有写入权限
|
||||
- 检查目标目录是否存在且可写
|
||||
|
||||
3. **文件大小不匹配**
|
||||
- 网络问题导致下载不完整
|
||||
- GitHub API 限制或临时故障
|
||||
|
||||
4. **目录创建失败**
|
||||
- WebDAV 服务器不支持 MKCOL 方法
|
||||
- 权限不足或路径错误
|
||||
|
||||
### 调试步骤
|
||||
|
||||
1. 查看 Actions 运行日志
|
||||
2. 检查 WebDAV 服务器日志
|
||||
3. 验证所有 Secrets 配置正确
|
||||
4. 测试 WebDAV 连接是否正常
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
1. **密钥管理**
|
||||
- 不要在代码中硬编码密码
|
||||
- 定期更换 WebDAV 密码
|
||||
- 使用强密码
|
||||
|
||||
2. **权限控制**
|
||||
- 为 WebDAV 用户设置最小必要权限
|
||||
- 考虑使用专用的同步账户
|
||||
|
||||
3. **网络安全**
|
||||
- 建议使用 HTTPS 连接
|
||||
- 考虑 IP 白名单限制
|
||||
|
||||
## 自定义配置
|
||||
|
||||
如需修改同步路径或其他配置,请编辑对应的工作流文件:
|
||||
|
||||
- 修改存储路径: 更改 `remote_path` 变量
|
||||
- 调整重试逻辑: 修改错误处理部分
|
||||
- 添加通知: 集成 Slack、邮件等通知服务
|
||||
|
||||
## 支持的文件类型
|
||||
|
||||
工作流支持同步所有类型的 Release 资源文件,包括但不限于:
|
||||
|
||||
- 可执行文件 (.exe, .dmg, .AppImage)
|
||||
- 压缩包 (.zip, .tar.gz, .7z)
|
||||
- 安装包 (.msi, .deb, .rpm)
|
||||
- 其他二进制文件
|
||||
|
||||
## 版本兼容性
|
||||
|
||||
- GitHub Actions: 支持最新版本
|
||||
- alist: 支持 WebDAV 协议的版本
|
||||
- 操作系统: Ubuntu Latest (工作流运行环境)
|
||||
@@ -19,6 +19,7 @@ win:
|
||||
- target: nsis
|
||||
arch:
|
||||
- x64
|
||||
- ia32
|
||||
# 简化版本信息设置,避免rcedit错误
|
||||
fileAssociations:
|
||||
- ext: cerumusic
|
||||
@@ -30,7 +31,7 @@ win:
|
||||
# 或者使用证书存储
|
||||
# certificateSubjectName: "Your Company Name"
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
artifactName: ${name}-${version}-${arch}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ceru-music",
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.7",
|
||||
"description": "一款简洁优雅的音乐播放器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "sqj,wldss,star",
|
||||
@@ -18,7 +18,8 @@
|
||||
"onlybuild": "electron-vite build && electron-builder --win --x64",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:unpack": "yarn run build && electron-builder --dir",
|
||||
"build:win": "yarn run build && electron-builder --win --x64 --config --publish never",
|
||||
"build:win": "yarn run build && electron-builder --win --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: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",
|
||||
@@ -94,7 +95,8 @@
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.0",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vue": "^3.5.17",
|
||||
"vitepress": "^2.0.0-alpha.12",
|
||||
"vue": "^3.5.21",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.3"
|
||||
}
|
||||
|
||||
BIN
resources/default-cover.png
Normal file
BIN
resources/default-cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 824 KiB |
66
scripts/auth-test.js
Normal file
66
scripts/auth-test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const axios = require('axios')
|
||||
|
||||
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
|
||||
const ALIST_USERNAME = 'ceruupdate'
|
||||
const ALIST_PASSWORD = '123456'
|
||||
|
||||
async function test() {
|
||||
// 认证
|
||||
const auth = await axios.post(`${ALIST_BASE_URL}/api/auth/login`, {
|
||||
username: ALIST_USERNAME,
|
||||
password: ALIST_PASSWORD
|
||||
})
|
||||
|
||||
const token = auth.data.data.token
|
||||
console.log('Token received')
|
||||
|
||||
// 测试直接 token 格式
|
||||
try {
|
||||
const list = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/fs/list`,
|
||||
{
|
||||
path: '/',
|
||||
password: '',
|
||||
page: 1,
|
||||
per_page: 30,
|
||||
refresh: false
|
||||
},
|
||||
{
|
||||
headers: { Authorization: token }
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Direct token works:', list.data.code === 200)
|
||||
if (list.data.code === 200) {
|
||||
console.log(
|
||||
'Files:',
|
||||
list.data.data.content.map((f) => f.name)
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Direct token failed')
|
||||
}
|
||||
|
||||
// 测试 Bearer 格式
|
||||
try {
|
||||
const list2 = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/fs/list`,
|
||||
{
|
||||
path: '/',
|
||||
password: '',
|
||||
page: 1,
|
||||
per_page: 30,
|
||||
refresh: false
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Bearer format works:', list2.data.code === 200)
|
||||
} catch (e) {
|
||||
console.log('Bearer format failed')
|
||||
}
|
||||
}
|
||||
|
||||
test().catch(console.error)
|
||||
148
scripts/test-alist.js
Normal file
148
scripts/test-alist.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const axios = require('axios')
|
||||
|
||||
// Alist API 配置
|
||||
const ALIST_BASE_URL = 'http://47.96.72.224:5244'
|
||||
const ALIST_USERNAME = 'ceruupdate'
|
||||
const ALIST_PASSWORD = '123456'
|
||||
|
||||
async function testAlistConnection() {
|
||||
console.log('Testing Alist connection...')
|
||||
|
||||
try {
|
||||
// 0. 首先测试服务器是否可访问
|
||||
console.log('0. Testing server accessibility...')
|
||||
const pingResponse = await axios.get(`${ALIST_BASE_URL}/ping`, {
|
||||
timeout: 5000
|
||||
})
|
||||
console.log('Server ping successful:', pingResponse.status)
|
||||
|
||||
// 1. 测试认证
|
||||
console.log('1. Testing authentication...')
|
||||
console.log(`Trying to authenticate with username: ${ALIST_USERNAME}`)
|
||||
|
||||
const authResponse = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/auth/login`,
|
||||
{
|
||||
username: ALIST_USERNAME,
|
||||
password: ALIST_PASSWORD
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Auth response:', authResponse.data)
|
||||
|
||||
if (authResponse.data.code !== 200) {
|
||||
// 尝试获取公共访问权限
|
||||
console.log('Authentication failed, trying public access...')
|
||||
|
||||
// 尝试不使用认证直接访问文件列表
|
||||
const publicListResponse = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/fs/list`,
|
||||
{
|
||||
path: '/',
|
||||
password: '',
|
||||
page: 1,
|
||||
per_page: 30,
|
||||
refresh: false
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Public access response:', publicListResponse.data)
|
||||
|
||||
if (publicListResponse.data.code === 200) {
|
||||
console.log('✓ Public access successful')
|
||||
return // 如果公共访问成功,就不需要认证
|
||||
}
|
||||
|
||||
throw new Error(`Authentication failed: ${authResponse.data.message}`)
|
||||
}
|
||||
|
||||
const token = authResponse.data.data.token
|
||||
console.log('✓ Authentication successful')
|
||||
|
||||
// 2. 测试文件列表
|
||||
console.log('2. Testing file listing...')
|
||||
const listResponse = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/fs/list`,
|
||||
{
|
||||
path: '/',
|
||||
password: '',
|
||||
page: 1,
|
||||
per_page: 30,
|
||||
refresh: false
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('List response:', listResponse.data)
|
||||
|
||||
if (listResponse.data.code === 200) {
|
||||
console.log('✓ File listing successful')
|
||||
console.log('Available directories/files:')
|
||||
listResponse.data.data.content.forEach((item) => {
|
||||
console.log(` - ${item.name} (${item.is_dir ? 'directory' : 'file'})`)
|
||||
})
|
||||
}
|
||||
|
||||
// 3. 测试获取特定文件信息(如果存在版本目录)
|
||||
console.log('3. Testing file info retrieval...')
|
||||
try {
|
||||
const fileInfoResponse = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/fs/get`,
|
||||
{
|
||||
path: '/v1.0.0' // 测试版本目录
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('File info response:', fileInfoResponse.data)
|
||||
|
||||
if (fileInfoResponse.data.code === 200) {
|
||||
console.log('✓ File info retrieval successful')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'ℹ Version directory /v1.0.0 not found (this is expected if no updates are available)'
|
||||
)
|
||||
}
|
||||
|
||||
console.log('\n✅ Alist connection test completed successfully!')
|
||||
} catch (error) {
|
||||
console.error('❌ Alist connection test failed:', error.message)
|
||||
|
||||
if (error.response) {
|
||||
console.error('Response status:', error.response.status)
|
||||
console.error('Response data:', error.response.data)
|
||||
} else if (error.request) {
|
||||
console.error('No response received. Check if the Alist server is running and accessible.')
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testAlistConnection()
|
||||
15
src/common/types/playList.ts
Normal file
15
src/common/types/playList.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export default interface PlayList {
|
||||
songmid: string | number
|
||||
hash?: string
|
||||
singer: string
|
||||
name: string
|
||||
albumName: string
|
||||
albumId: string | number
|
||||
source: string
|
||||
interval: string
|
||||
img: string
|
||||
lrc: null | string
|
||||
types: string[]
|
||||
_types: Record<string, any>
|
||||
typeUrl: Record<string, any>
|
||||
}
|
||||
12
src/common/types/songList.ts
Normal file
12
src/common/types/songList.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import PlayList from './playList'
|
||||
export type Songs = PlayList
|
||||
|
||||
export type SongList = {
|
||||
id: string //hashId 对应歌单文件名.json
|
||||
name: string // 歌单名
|
||||
createTime: string
|
||||
updateTime: string
|
||||
description: string // 歌单描述
|
||||
coverImgUrl: string //歌单封面 默认第一首歌的图片
|
||||
source: 'local' | 'wy' | 'tx' | 'mg' | 'kg' | 'kw' // 来源
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// 业务工具方法
|
||||
|
||||
import { LX } from "../../types/global"
|
||||
import { LX } from '../../types/global'
|
||||
|
||||
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {
|
||||
const meta: Record<string, any> = {
|
||||
|
||||
@@ -1,54 +1,160 @@
|
||||
import { BrowserWindow, app, shell } from 'electron';
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import path from 'node:path';
|
||||
import { BrowserWindow, app, shell } from 'electron'
|
||||
import axios from 'axios'
|
||||
import fs from 'fs'
|
||||
import path from 'node:path'
|
||||
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let currentUpdateInfo: UpdateInfo | null = null;
|
||||
let downloadProgress = { percent: 0, transferred: 0, total: 0 };
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let currentUpdateInfo: UpdateInfo | null = null
|
||||
let downloadProgress = { percent: 0, transferred: 0, total: 0 }
|
||||
|
||||
// 更新信息接口
|
||||
interface UpdateInfo {
|
||||
url: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
pub_date: string;
|
||||
url: string
|
||||
name: string
|
||||
notes: string
|
||||
pub_date: string
|
||||
}
|
||||
|
||||
// 更新服务器配置
|
||||
const UPDATE_SERVER = 'https://update.ceru.shiqianjiang.cn';
|
||||
const UPDATE_API_URL = `${UPDATE_SERVER}/update/${process.platform}/${app.getVersion()}`;
|
||||
const UPDATE_SERVER = 'https://update.ceru.shiqianjiang.cn'
|
||||
const UPDATE_API_URL = `${UPDATE_SERVER}/update/${process.platform}/${app.getVersion()}`
|
||||
|
||||
// Alist API 配置
|
||||
const ALIST_BASE_URL = 'https://alist.shiqianjiang.cn' // 请替换为实际的 alist 域名
|
||||
const ALIST_USERNAME = 'ceruupdate'
|
||||
const ALIST_PASSWORD = '123456'
|
||||
|
||||
// Alist 认证 token
|
||||
let alistToken: string | null = null
|
||||
|
||||
// 获取 Alist 认证 token
|
||||
async function getAlistToken(): Promise<string> {
|
||||
if (alistToken) {
|
||||
return alistToken
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Authenticating with Alist...')
|
||||
const response = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/auth/login`,
|
||||
{
|
||||
username: ALIST_USERNAME,
|
||||
password: ALIST_PASSWORD
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Alist auth response:', response.data)
|
||||
|
||||
if (response.data.code === 200) {
|
||||
alistToken = response.data.data.token
|
||||
console.log('Alist authentication successful')
|
||||
return alistToken! // 我们已经确认 token 存在
|
||||
} else {
|
||||
throw new Error(`Alist authentication failed: ${response.data.message || 'Unknown error'}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Alist authentication error:', error)
|
||||
if (error.response) {
|
||||
throw new Error(
|
||||
`Failed to authenticate with Alist: HTTP ${error.response.status} - ${error.response.data?.message || error.response.statusText}`
|
||||
)
|
||||
} else {
|
||||
throw new Error(`Failed to authenticate with Alist: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 Alist 文件下载链接
|
||||
async function getAlistDownloadUrl(version: string, fileName: string): Promise<string> {
|
||||
const token = await getAlistToken()
|
||||
const filePath = `/${version}/${fileName}`
|
||||
|
||||
try {
|
||||
console.log(`Getting file info for: ${filePath}`)
|
||||
const response = await axios.post(
|
||||
`${ALIST_BASE_URL}/api/fs/get`,
|
||||
{
|
||||
path: filePath
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('Alist file info response:', response.data)
|
||||
|
||||
if (response.data.code === 200) {
|
||||
const fileInfo = response.data.data
|
||||
|
||||
// 检查文件是否存在且有下载链接
|
||||
if (fileInfo && fileInfo.raw_url) {
|
||||
console.log('Using raw_url for download:', fileInfo.raw_url)
|
||||
return fileInfo.raw_url
|
||||
} else if (fileInfo && fileInfo.sign) {
|
||||
// 使用签名构建下载链接
|
||||
const downloadUrl = `${ALIST_BASE_URL}/d${filePath}?sign=${fileInfo.sign}`
|
||||
console.log('Using signed download URL:', downloadUrl)
|
||||
return downloadUrl
|
||||
} else {
|
||||
// 尝试直接下载链接(无签名)
|
||||
const directUrl = `${ALIST_BASE_URL}/d${filePath}`
|
||||
console.log('Using direct download URL:', directUrl)
|
||||
return directUrl
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to get file info: ${response.data.message || 'Unknown error'}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Alist file info error:', error)
|
||||
if (error.response) {
|
||||
throw new Error(
|
||||
`Failed to get download URL from Alist: HTTP ${error.response.status} - ${error.response.data?.message || error.response.statusText}`
|
||||
)
|
||||
} else {
|
||||
throw new Error(`Failed to get download URL from Alist: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化自动更新器
|
||||
export function initAutoUpdater(window: BrowserWindow) {
|
||||
mainWindow = window;
|
||||
console.log('Auto updater initialized');
|
||||
mainWindow = window
|
||||
console.log('Auto updater initialized')
|
||||
}
|
||||
|
||||
// 检查更新
|
||||
export async function checkForUpdates(window?: BrowserWindow) {
|
||||
if (window) {
|
||||
mainWindow = window;
|
||||
mainWindow = window
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Checking for updates...');
|
||||
mainWindow?.webContents.send('auto-updater:checking-for-update');
|
||||
console.log('Checking for updates...')
|
||||
mainWindow?.webContents.send('auto-updater:checking-for-update')
|
||||
|
||||
const updateInfo = await fetchUpdateInfo();
|
||||
const updateInfo = await fetchUpdateInfo()
|
||||
|
||||
if (updateInfo && isNewerVersion(updateInfo.name, app.getVersion())) {
|
||||
console.log('Update available:', updateInfo);
|
||||
currentUpdateInfo = updateInfo;
|
||||
mainWindow?.webContents.send('auto-updater:update-available', updateInfo);
|
||||
console.log('Update available:', updateInfo)
|
||||
currentUpdateInfo = updateInfo
|
||||
mainWindow?.webContents.send('auto-updater:update-available', updateInfo)
|
||||
} else {
|
||||
console.log('No update available');
|
||||
mainWindow?.webContents.send('auto-updater:update-not-available');
|
||||
console.log('No update available')
|
||||
mainWindow?.webContents.send('auto-updater:update-not-available')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error);
|
||||
mainWindow?.webContents.send('auto-updater:error', (error as Error).message);
|
||||
console.error('Error checking for updates:', error)
|
||||
mainWindow?.webContents.send('auto-updater:error', (error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,26 +164,26 @@ async function fetchUpdateInfo(): Promise<UpdateInfo | null> {
|
||||
const response = await axios.get(UPDATE_API_URL, {
|
||||
timeout: 10000, // 10秒超时
|
||||
validateStatus: (status) => status === 200 || status === 204 // 允许 200 和 204 状态码
|
||||
});
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
return response.data as UpdateInfo;
|
||||
return response.data as UpdateInfo
|
||||
} else if (response.status === 204) {
|
||||
// 204 表示没有更新
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
// 服务器响应了错误状态码
|
||||
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
||||
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`)
|
||||
} else if (error.request) {
|
||||
// 请求已发出但没有收到响应
|
||||
throw new Error('Network error: No response received');
|
||||
throw new Error('Network error: No response received')
|
||||
} else {
|
||||
// 其他错误
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
throw new Error(`Request failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,101 +191,119 @@ async function fetchUpdateInfo(): Promise<UpdateInfo | null> {
|
||||
// 比较版本号
|
||||
function isNewerVersion(remoteVersion: string, currentVersion: string): boolean {
|
||||
const parseVersion = (version: string) => {
|
||||
return version.replace(/^v/, '').split('.').map(num => parseInt(num, 10));
|
||||
};
|
||||
|
||||
const remote = parseVersion(remoteVersion);
|
||||
const current = parseVersion(currentVersion);
|
||||
|
||||
for (let i = 0; i < Math.max(remote.length, current.length); i++) {
|
||||
const r = remote[i] || 0;
|
||||
const c = current[i] || 0;
|
||||
|
||||
if (r > c) return true;
|
||||
if (r < c) return false;
|
||||
return version
|
||||
.replace(/^v/, '')
|
||||
.split('.')
|
||||
.map((num) => parseInt(num, 10))
|
||||
}
|
||||
|
||||
return false;
|
||||
const remote = parseVersion(remoteVersion)
|
||||
const current = parseVersion(currentVersion)
|
||||
|
||||
for (let i = 0; i < Math.max(remote.length, current.length); i++) {
|
||||
const r = remote[i] || 0
|
||||
const c = current[i] || 0
|
||||
|
||||
if (r > c) return true
|
||||
if (r < c) return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 下载更新
|
||||
export async function downloadUpdate() {
|
||||
if (!currentUpdateInfo) {
|
||||
throw new Error('No update info available');
|
||||
throw new Error('No update info available')
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Starting download:', currentUpdateInfo.url);
|
||||
|
||||
// 通知渲染进程开始下载
|
||||
mainWindow?.webContents.send('auto-updater:download-started', currentUpdateInfo);
|
||||
console.log('Starting download:', currentUpdateInfo.url)
|
||||
|
||||
const downloadPath = await downloadFile(currentUpdateInfo.url);
|
||||
console.log('Download completed:', downloadPath);
|
||||
// 通知渲染进程开始下载
|
||||
mainWindow?.webContents.send('auto-updater:download-started', currentUpdateInfo)
|
||||
|
||||
const downloadPath = await downloadFile(currentUpdateInfo.url)
|
||||
console.log('Download completed:', downloadPath)
|
||||
|
||||
mainWindow?.webContents.send('auto-updater:update-downloaded', {
|
||||
downloadPath,
|
||||
updateInfo: currentUpdateInfo
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
mainWindow?.webContents.send('auto-updater:error', (error as Error).message);
|
||||
console.error('Download failed:', error)
|
||||
mainWindow?.webContents.send('auto-updater:error', (error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
async function downloadFile(url: string): Promise<string> {
|
||||
const fileName = path.basename(url);
|
||||
const downloadPath = path.join(app.getPath('temp'), fileName);
|
||||
async function downloadFile(originalUrl: string): Promise<string> {
|
||||
const fileName = path.basename(originalUrl)
|
||||
const downloadPath = path.join(app.getPath('temp'), fileName)
|
||||
|
||||
// 进度节流变量
|
||||
let lastProgressSent = 0;
|
||||
let lastProgressTime = 0;
|
||||
const PROGRESS_THROTTLE_INTERVAL = 500; // 500ms 发送一次进度
|
||||
const PROGRESS_THRESHOLD = 1; // 进度变化超过1%才发送
|
||||
let lastProgressSent = 0
|
||||
let lastProgressTime = 0
|
||||
const PROGRESS_THROTTLE_INTERVAL = 500 // 500ms 发送一次进度
|
||||
const PROGRESS_THRESHOLD = 1 // 进度变化超过1%才发送
|
||||
|
||||
try {
|
||||
let downloadUrl = originalUrl
|
||||
|
||||
try {
|
||||
// 从当前更新信息中提取版本号
|
||||
const version = currentUpdateInfo?.name || app.getVersion()
|
||||
|
||||
// 尝试使用 alist API 获取下载链接
|
||||
downloadUrl = await getAlistDownloadUrl(version, fileName)
|
||||
console.log('Using Alist download URL:', downloadUrl)
|
||||
} catch (alistError) {
|
||||
console.warn('Alist download failed, falling back to original URL:', alistError)
|
||||
console.log('Using original download URL:', originalUrl)
|
||||
downloadUrl = originalUrl
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
url: downloadUrl,
|
||||
responseType: 'stream',
|
||||
timeout: 30000, // 30秒超时
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
const { loaded, total } = progressEvent;
|
||||
const percent = total ? (loaded / total) * 100 : 0;
|
||||
const currentTime = Date.now();
|
||||
const { loaded, total } = progressEvent
|
||||
const percent = total ? (loaded / total) * 100 : 0
|
||||
const currentTime = Date.now()
|
||||
|
||||
// 节流逻辑:只在进度变化显著或时间间隔足够时发送
|
||||
const progressDiff = Math.abs(percent - lastProgressSent);
|
||||
const timeDiff = currentTime - lastProgressTime;
|
||||
|
||||
const progressDiff = Math.abs(percent - lastProgressSent)
|
||||
const timeDiff = currentTime - lastProgressTime
|
||||
|
||||
if (progressDiff >= PROGRESS_THRESHOLD || timeDiff >= PROGRESS_THROTTLE_INTERVAL) {
|
||||
downloadProgress = {
|
||||
percent,
|
||||
transferred: loaded,
|
||||
total: total || 0
|
||||
};
|
||||
}
|
||||
|
||||
mainWindow?.webContents.send('auto-updater:download-progress', downloadProgress);
|
||||
lastProgressSent = percent;
|
||||
lastProgressTime = currentTime;
|
||||
mainWindow?.webContents.send('auto-updater:download-progress', downloadProgress)
|
||||
lastProgressSent = percent
|
||||
lastProgressTime = currentTime
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// 发送初始进度
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0', 10);
|
||||
const totalSize = parseInt(response.headers['content-length'] || '0', 10)
|
||||
mainWindow?.webContents.send('auto-updater:download-progress', {
|
||||
percent: 0,
|
||||
transferred: 0,
|
||||
total: totalSize
|
||||
});
|
||||
})
|
||||
|
||||
// 创建写入流
|
||||
const writer = fs.createWriteStream(downloadPath);
|
||||
const writer = fs.createWriteStream(downloadPath)
|
||||
|
||||
// 将响应数据流写入文件
|
||||
response.data.pipe(writer);
|
||||
response.data.pipe(writer)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => {
|
||||
@@ -188,37 +312,36 @@ async function downloadFile(url: string): Promise<string> {
|
||||
percent: 100,
|
||||
transferred: totalSize,
|
||||
total: totalSize
|
||||
});
|
||||
})
|
||||
|
||||
console.log('File download completed:', downloadPath);
|
||||
resolve(downloadPath);
|
||||
});
|
||||
console.log('File download completed:', downloadPath)
|
||||
resolve(downloadPath)
|
||||
})
|
||||
|
||||
writer.on('error', (error) => {
|
||||
// 删除部分下载的文件
|
||||
fs.unlink(downloadPath, () => {});
|
||||
reject(error);
|
||||
});
|
||||
fs.unlink(downloadPath, () => {})
|
||||
reject(error)
|
||||
})
|
||||
|
||||
response.data.on('error', (error: Error) => {
|
||||
writer.destroy();
|
||||
fs.unlink(downloadPath, () => {});
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
writer.destroy()
|
||||
fs.unlink(downloadPath, () => {})
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
} catch (error: any) {
|
||||
// 删除可能创建的文件
|
||||
if (fs.existsSync(downloadPath)) {
|
||||
fs.unlink(downloadPath, () => {});
|
||||
fs.unlink(downloadPath, () => {})
|
||||
}
|
||||
|
||||
if (error.response) {
|
||||
throw new Error(`Download failed: HTTP ${error.response.status} ${error.response.statusText}`);
|
||||
throw new Error(`Download failed: HTTP ${error.response.status} ${error.response.statusText}`)
|
||||
} else if (error.request) {
|
||||
throw new Error('Download failed: Network error');
|
||||
throw new Error('Download failed: Network error')
|
||||
} else {
|
||||
throw new Error(`Download failed: ${error.message}`);
|
||||
throw new Error(`Download failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,37 +349,37 @@ async function downloadFile(url: string): Promise<string> {
|
||||
// 退出并安装
|
||||
export function quitAndInstall() {
|
||||
if (!currentUpdateInfo) {
|
||||
console.error('No update info available for installation');
|
||||
return;
|
||||
console.error('No update info available for installation')
|
||||
return
|
||||
}
|
||||
|
||||
// 对于不同平台,处理方式不同
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: 打开安装程序
|
||||
const fileName = path.basename(currentUpdateInfo.url);
|
||||
const downloadPath = path.join(app.getPath('temp'), fileName);
|
||||
const fileName = path.basename(currentUpdateInfo.url)
|
||||
const downloadPath = path.join(app.getPath('temp'), fileName)
|
||||
|
||||
if (fs.existsSync(downloadPath)) {
|
||||
shell.openPath(downloadPath).then(() => {
|
||||
app.quit();
|
||||
});
|
||||
app.quit()
|
||||
})
|
||||
} else {
|
||||
console.error('Downloaded file not found:', downloadPath);
|
||||
console.error('Downloaded file not found:', downloadPath)
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
// macOS: 打开 dmg 或 zip 文件
|
||||
const fileName = path.basename(currentUpdateInfo.url);
|
||||
const downloadPath = path.join(app.getPath('temp'), fileName);
|
||||
const fileName = path.basename(currentUpdateInfo.url)
|
||||
const downloadPath = path.join(app.getPath('temp'), fileName)
|
||||
|
||||
if (fs.existsSync(downloadPath)) {
|
||||
shell.openPath(downloadPath).then(() => {
|
||||
app.quit();
|
||||
});
|
||||
app.quit()
|
||||
})
|
||||
} else {
|
||||
console.error('Downloaded file not found:', downloadPath);
|
||||
console.error('Downloaded file not found:', downloadPath)
|
||||
}
|
||||
} else {
|
||||
// Linux: 打开下载文件夹
|
||||
shell.showItemInFolder(path.join(app.getPath('temp'), path.basename(currentUpdateInfo.url)));
|
||||
shell.showItemInFolder(path.join(app.getPath('temp'), path.basename(currentUpdateInfo.url)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { ipcMain, BrowserWindow } from 'electron';
|
||||
import { initAutoUpdater, checkForUpdates, downloadUpdate, quitAndInstall } from '../autoUpdate';
|
||||
import { ipcMain, BrowserWindow } from 'electron'
|
||||
import { initAutoUpdater, checkForUpdates, downloadUpdate, quitAndInstall } from '../autoUpdate'
|
||||
|
||||
// 注册自动更新相关的IPC事件
|
||||
export function registerAutoUpdateEvents() {
|
||||
// 检查更新
|
||||
ipcMain.handle('auto-updater:check-for-updates', (event) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
const window = BrowserWindow.fromWebContents(event.sender)
|
||||
if (window) {
|
||||
checkForUpdates(window);
|
||||
checkForUpdates(window)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// 下载更新
|
||||
ipcMain.handle('auto-updater:download-update', () => {
|
||||
downloadUpdate();
|
||||
});
|
||||
downloadUpdate()
|
||||
})
|
||||
|
||||
// 安装更新
|
||||
ipcMain.handle('auto-updater:quit-and-install', () => {
|
||||
quitAndInstall();
|
||||
});
|
||||
quitAndInstall()
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化自动更新(在主窗口创建后调用)
|
||||
export function initAutoUpdateForWindow(window: BrowserWindow) {
|
||||
initAutoUpdater(window);
|
||||
}
|
||||
initAutoUpdater(window)
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ ipcMain.handle('music-cache:get-size', async () => {
|
||||
console.error('获取缓存大小失败:', error)
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
361
src/main/events/songList.ts
Normal file
361
src/main/events/songList.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import ManageSongList, { SongListError } from '../services/songList/ManageSongList'
|
||||
import type { SongList, Songs } from '@common/types/songList'
|
||||
|
||||
// 创建新歌单
|
||||
ipcMain.handle(
|
||||
'songlist:create',
|
||||
async (_, name: string, description: string = '', source: SongList['source']) => {
|
||||
try {
|
||||
const result = ManageSongList.createPlaylist(name, description, source)
|
||||
return { success: true, data: result, message: '歌单创建成功' }
|
||||
} catch (error) {
|
||||
console.error('创建歌单失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '创建歌单失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 获取所有歌单
|
||||
ipcMain.handle('songlist:get-all', async () => {
|
||||
try {
|
||||
const songLists = ManageSongList.Read()
|
||||
return { success: true, data: songLists }
|
||||
} catch (error) {
|
||||
console.error('获取歌单列表失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '获取歌单列表失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 根据ID获取歌单信息
|
||||
ipcMain.handle('songlist:get-by-id', async (_, hashId: string) => {
|
||||
try {
|
||||
const songList = ManageSongList.getById(hashId)
|
||||
return { success: true, data: songList }
|
||||
} catch (error) {
|
||||
console.error('获取歌单信息失败:', error)
|
||||
return { success: false, error: '获取歌单信息失败' }
|
||||
}
|
||||
})
|
||||
|
||||
// 删除歌单
|
||||
ipcMain.handle('songlist:delete', async (_, hashId: string) => {
|
||||
try {
|
||||
ManageSongList.deleteById(hashId)
|
||||
return { success: true, message: '歌单删除成功' }
|
||||
} catch (error) {
|
||||
console.error('删除歌单失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '删除歌单失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 批量删除歌单
|
||||
ipcMain.handle('songlist:batch-delete', async (_, hashIds: string[]) => {
|
||||
try {
|
||||
const result = ManageSongList.batchDelete(hashIds)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
message: `成功删除 ${result.success.length} 个歌单,失败 ${result.failed.length} 个`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除歌单失败:', error)
|
||||
return { success: false, error: '批量删除歌单失败' }
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑歌单信息
|
||||
ipcMain.handle(
|
||||
'songlist:edit',
|
||||
async (_, hashId: string, updates: Partial<Omit<SongList, 'id' | 'createTime'>>) => {
|
||||
try {
|
||||
ManageSongList.editById(hashId, updates)
|
||||
return { success: true, message: '歌单信息更新成功' }
|
||||
} catch (error) {
|
||||
console.error('编辑歌单失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '编辑歌单失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 更新歌单封面
|
||||
ipcMain.handle('songlist:update-cover', async (_, hashId: string, coverImgUrl: string) => {
|
||||
try {
|
||||
ManageSongList.updateCoverImgById(hashId, coverImgUrl)
|
||||
return { success: true, message: '封面更新成功' }
|
||||
} catch (error) {
|
||||
console.error('更新封面失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '更新封面失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 搜索歌单
|
||||
ipcMain.handle('songlist:search', async (_, keyword: string, source?: SongList['source']) => {
|
||||
try {
|
||||
const results = ManageSongList.search(keyword, source)
|
||||
return { success: true, data: results }
|
||||
} catch (error) {
|
||||
console.error('搜索歌单失败:', error)
|
||||
return { success: false, error: '搜索歌单失败', data: [] }
|
||||
}
|
||||
})
|
||||
|
||||
// 获取歌单统计信息
|
||||
ipcMain.handle('songlist:get-statistics', async () => {
|
||||
try {
|
||||
const statistics = ManageSongList.getStatistics()
|
||||
return { success: true, data: statistics }
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
return { success: false, error: '获取统计信息失败' }
|
||||
}
|
||||
})
|
||||
|
||||
// 检查歌单是否存在
|
||||
ipcMain.handle('songlist:exists', async (_, hashId: string) => {
|
||||
try {
|
||||
const exists = ManageSongList.exists(hashId)
|
||||
return { success: true, data: exists }
|
||||
} catch (error) {
|
||||
console.error('检查歌单存在性失败:', error)
|
||||
return { success: false, error: '检查歌单存在性失败', data: false }
|
||||
}
|
||||
})
|
||||
|
||||
// === 歌曲管理相关 IPC 事件 ===
|
||||
|
||||
// 添加歌曲到歌单
|
||||
ipcMain.handle('songlist:add-songs', async (_, hashId: string, songs: Songs[]) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
instance.addSongs(songs)
|
||||
return { success: true, message: `成功添加 ${songs.length} 首歌曲` }
|
||||
} catch (error) {
|
||||
console.error('添加歌曲失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '添加歌曲失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 从歌单移除歌曲
|
||||
ipcMain.handle('songlist:remove-song', async (_, hashId: string, songmid: string | number) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const removed = instance.removeSong(songmid)
|
||||
return {
|
||||
success: true,
|
||||
data: removed,
|
||||
message: removed ? '歌曲移除成功' : '歌曲不存在'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('移除歌曲失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '移除歌曲失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 批量移除歌曲
|
||||
ipcMain.handle(
|
||||
'songlist:remove-songs',
|
||||
async (_, hashId: string, songmids: (string | number)[]) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const result = instance.removeSongs(songmids)
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
message: `成功移除 ${result.removed} 首歌曲,${result.notFound} 首未找到`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量移除歌曲失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '批量移除歌曲失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 清空歌单
|
||||
ipcMain.handle('songlist:clear-songs', async (_, hashId: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
instance.clearSongs()
|
||||
return { success: true, message: '歌单已清空' }
|
||||
} catch (error) {
|
||||
console.error('清空歌单失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '清空歌单失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 获取歌单中的歌曲列表
|
||||
ipcMain.handle('songlist:get-songs', async (_, hashId: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const songs = instance.getSongs()
|
||||
return { success: true, data: songs }
|
||||
} catch (error) {
|
||||
console.error('获取歌曲列表失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '获取歌曲列表失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 获取歌单歌曲数量
|
||||
ipcMain.handle('songlist:get-song-count', async (_, hashId: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const count = instance.getCount()
|
||||
return { success: true, data: count }
|
||||
} catch (error) {
|
||||
console.error('获取歌曲数量失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '获取歌曲数量失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 检查歌曲是否在歌单中
|
||||
ipcMain.handle('songlist:has-song', async (_, hashId: string, songmid: string | number) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const hasSong = instance.hasSong(songmid)
|
||||
return { success: true, data: hasSong }
|
||||
} catch (error) {
|
||||
console.error('检查歌曲存在性失败:', error)
|
||||
return { success: false, error: '检查歌曲存在性失败', data: false }
|
||||
}
|
||||
})
|
||||
|
||||
// 根据ID获取歌曲
|
||||
ipcMain.handle('songlist:get-song', async (_, hashId: string, songmid: string | number) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const song = instance.getSong(songmid)
|
||||
return { success: true, data: song }
|
||||
} catch (error) {
|
||||
console.error('获取歌曲失败:', error)
|
||||
return { success: false, error: '获取歌曲失败', data: null }
|
||||
}
|
||||
})
|
||||
|
||||
// 搜索歌单中的歌曲
|
||||
ipcMain.handle('songlist:search-songs', async (_, hashId: string, keyword: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const results = instance.searchSongs(keyword)
|
||||
return { success: true, data: results }
|
||||
} catch (error) {
|
||||
console.error('搜索歌曲失败:', error)
|
||||
return { success: false, error: '搜索歌曲失败', data: [] }
|
||||
}
|
||||
})
|
||||
|
||||
// 获取歌单歌曲统计信息
|
||||
ipcMain.handle('songlist:get-song-statistics', async (_, hashId: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const statistics = instance.getStatistics()
|
||||
return { success: true, data: statistics }
|
||||
} catch (error) {
|
||||
console.error('获取歌曲统计信息失败:', error)
|
||||
return { success: false, error: '获取歌曲统计信息失败' }
|
||||
}
|
||||
})
|
||||
|
||||
// 验证歌单完整性
|
||||
ipcMain.handle('songlist:validate-integrity', async (_, hashId: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const result = instance.validateIntegrity()
|
||||
return { success: true, data: result }
|
||||
} catch (error) {
|
||||
console.error('验证歌单完整性失败:', error)
|
||||
return { success: false, error: '验证歌单完整性失败' }
|
||||
}
|
||||
})
|
||||
|
||||
// 修复歌单数据
|
||||
ipcMain.handle('songlist:repair-data', async (_, hashId: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
const result = instance.repairData()
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
message: result.fixed ? `数据修复完成: ${result.changes.join(', ')}` : '数据无需修复'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('修复歌单数据失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '修复歌单数据失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 强制保存歌单
|
||||
ipcMain.handle('songlist:force-save', async (_, hashId: string) => {
|
||||
try {
|
||||
const instance = new ManageSongList(hashId)
|
||||
instance.forceSave()
|
||||
return { success: true, message: '歌单保存成功' }
|
||||
} catch (error) {
|
||||
console.error('强制保存歌单失败:', error)
|
||||
const message = error instanceof SongListError ? error.message : '强制保存歌单失败'
|
||||
return {
|
||||
success: false,
|
||||
error: message,
|
||||
code: error instanceof SongListError ? error.code : 'UNKNOWN_ERROR'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -8,6 +8,24 @@ import pluginService from './services/plugin'
|
||||
import aiEvents from './events/ai'
|
||||
import './services/musicSdk/index'
|
||||
|
||||
// 获取单实例锁
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
|
||||
if (!gotTheLock) {
|
||||
// 如果没有获得锁,说明已经有实例在运行,退出当前实例
|
||||
app.quit()
|
||||
} else {
|
||||
// 当第二个实例尝试启动时,聚焦到第一个实例的窗口
|
||||
app.on('second-instance', () => {
|
||||
// 如果有窗口存在,聚焦到该窗口
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
if (!mainWindow.isVisible()) mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// import wy from './utils/musicSdk/wy/index'
|
||||
// import kg from './utils/musicSdk/kg/index'
|
||||
// wy.hotSearch.getList().then((res) => {
|
||||
@@ -40,6 +58,7 @@ function createTray(): void {
|
||||
label: '播放/暂停',
|
||||
click: () => {
|
||||
// 这里可以添加播放控制逻辑
|
||||
console.log('music-control')
|
||||
mainWindow?.webContents.send('music-control')
|
||||
}
|
||||
},
|
||||
@@ -75,7 +94,7 @@ function createWindow(): void {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1100,
|
||||
height: 750,
|
||||
minWidth: 970,
|
||||
minWidth: 1100,
|
||||
minHeight: 670,
|
||||
show: false,
|
||||
center: true,
|
||||
@@ -94,7 +113,6 @@ function createWindow(): void {
|
||||
contextIsolation: false,
|
||||
backgroundThrottling: false
|
||||
}
|
||||
|
||||
})
|
||||
if (process.platform == 'darwin') mainWindow.setWindowButtonVisibility(false)
|
||||
|
||||
@@ -199,6 +217,7 @@ ipcMain.handle('get-app-version', () => {
|
||||
|
||||
aiEvents(mainWindow)
|
||||
import './events/musicCache'
|
||||
import './events/songList'
|
||||
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -217,7 +236,7 @@ app.whenReady().then(() => {
|
||||
} catch (error) {
|
||||
console.error('插件系统初始化失败:', error)
|
||||
}
|
||||
},1000)
|
||||
}, 1000)
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
|
||||
@@ -4,7 +4,6 @@ import * as fs from 'fs/promises'
|
||||
import * as crypto from 'crypto'
|
||||
import axios from 'axios'
|
||||
|
||||
|
||||
export class MusicCacheService {
|
||||
private cacheDir: string
|
||||
private cacheIndex: Map<string, string> = new Map()
|
||||
@@ -20,7 +19,7 @@ export class MusicCacheService {
|
||||
try {
|
||||
// 确保缓存目录存在
|
||||
await fs.mkdir(this.cacheDir, { recursive: true })
|
||||
|
||||
|
||||
// 加载缓存索引
|
||||
await this.loadCacheIndex()
|
||||
} catch (error) {
|
||||
@@ -60,12 +59,12 @@ export class MusicCacheService {
|
||||
|
||||
async getCachedMusicUrl(songId: string, originalUrlPromise: Promise<string>): Promise<string> {
|
||||
const cacheKey = this.generateCacheKey(songId)
|
||||
console.log('hash',cacheKey)
|
||||
|
||||
console.log('hash', cacheKey)
|
||||
|
||||
// 检查是否已缓存
|
||||
if (this.cacheIndex.has(cacheKey)) {
|
||||
const cachedFilePath = this.cacheIndex.get(cacheKey)!
|
||||
|
||||
|
||||
try {
|
||||
// 验证文件是否存在
|
||||
await fs.access(cachedFilePath)
|
||||
@@ -86,7 +85,7 @@ export class MusicCacheService {
|
||||
private async downloadAndCache(songId: string, url: string, cacheKey: string): Promise<string> {
|
||||
try {
|
||||
console.log(`开始下载歌曲: ${songId}`)
|
||||
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
@@ -108,7 +107,7 @@ export class MusicCacheService {
|
||||
// 更新缓存索引
|
||||
this.cacheIndex.set(cacheKey, cacheFilePath)
|
||||
await this.saveCacheIndex()
|
||||
|
||||
|
||||
console.log(`歌曲缓存完成: ${cacheFilePath}`)
|
||||
resolve(`file://${cacheFilePath}`)
|
||||
} catch (error) {
|
||||
@@ -143,7 +142,7 @@ export class MusicCacheService {
|
||||
// 清空缓存索引
|
||||
this.cacheIndex.clear()
|
||||
await this.saveCacheIndex()
|
||||
|
||||
|
||||
console.log('音乐缓存已清空')
|
||||
} catch (error) {
|
||||
console.error('清空缓存失败:', error)
|
||||
@@ -152,7 +151,7 @@ export class MusicCacheService {
|
||||
|
||||
async getCacheSize(): Promise<number> {
|
||||
let totalSize = 0
|
||||
|
||||
|
||||
for (const filePath of this.cacheIndex.values()) {
|
||||
try {
|
||||
const stats = await fs.stat(filePath)
|
||||
@@ -161,14 +160,14 @@ export class MusicCacheService {
|
||||
// 文件不存在,忽略
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
async getCacheInfo(): Promise<{ count: number; size: number; sizeFormatted: string }> {
|
||||
const size = await this.getCacheSize()
|
||||
const count = this.cacheIndex.size
|
||||
|
||||
|
||||
const formatSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
@@ -186,4 +185,4 @@ export class MusicCacheService {
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
export const musicCacheService = new MusicCacheService()
|
||||
export const musicCacheService = new MusicCacheService()
|
||||
|
||||
@@ -19,7 +19,7 @@ export function request<T extends keyof MainApi>(
|
||||
return (Api[method] as (args: any) => any)(args)
|
||||
}
|
||||
throw new Error(`未知的方法: ${method}`)
|
||||
}catch (error:any){
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import { fileURLToPath } from 'url'
|
||||
|
||||
const fileLock: Record<string, boolean> = {}
|
||||
|
||||
|
||||
function main(source: string) {
|
||||
const Api = musicSdk[source]
|
||||
return {
|
||||
@@ -38,7 +37,6 @@ function main(source: string) {
|
||||
// 获取原始URL
|
||||
const originalUrlPromise = usePlugin.getMusicUrl(source, songInfo, quality)
|
||||
|
||||
|
||||
// 生成歌曲唯一标识
|
||||
const songId = `${songInfo.name}-${songInfo.singer}-${source}-${quality}`
|
||||
|
||||
@@ -161,6 +159,26 @@ function main(source: string) {
|
||||
message: '下载成功',
|
||||
path: songPath
|
||||
}
|
||||
},
|
||||
|
||||
async parsePlaylistId({ url }: { url: string }) {
|
||||
try {
|
||||
return await Api.songList.handleParseId(url)
|
||||
} catch (e: any) {
|
||||
return {
|
||||
error: '解析歌单链接失败 ' + (e.error || e.message || e)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async getPlaylistDetailById(id: string, page: number = 1) {
|
||||
try {
|
||||
return await Api.songList.getListDetail(id, page)
|
||||
} catch (e: any) {
|
||||
return {
|
||||
error: '获取歌单详情失败 ' + (e.error || e.message || e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,4 @@ export interface PlaylistDetailResult {
|
||||
|
||||
export interface DownloadSingleSongArgs extends GetMusicUrlArg {
|
||||
path?: string
|
||||
}
|
||||
}
|
||||
|
||||
755
src/main/services/songList/ManageSongList.ts
Normal file
755
src/main/services/songList/ManageSongList.ts
Normal file
@@ -0,0 +1,755 @@
|
||||
import type { SongList, Songs } from '@common/types/songList'
|
||||
import PlayListSongs from './PlayListSongs'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
import { getAppDirPath } from '../../utils/path'
|
||||
|
||||
// 常量定义
|
||||
const DEFAULT_COVER_IDENTIFIER = 'default-cover'
|
||||
const SONGLIST_DIR = 'songList'
|
||||
const INDEX_FILE = 'index.json'
|
||||
|
||||
// 错误类型定义
|
||||
class SongListError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code?: string
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'SongListError'
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数类
|
||||
class SongListUtils {
|
||||
/**
|
||||
* 获取默认封面标识符
|
||||
*/
|
||||
static getDefaultCoverUrl(): string {
|
||||
return DEFAULT_COVER_IDENTIFIER
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单管理入口文件路径
|
||||
*/
|
||||
static getSongListIndexPath(): string {
|
||||
return path.join(getAppDirPath('userData'), SONGLIST_DIR, INDEX_FILE)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单文件路径
|
||||
*/
|
||||
static getSongListFilePath(hashId: string): string {
|
||||
return path.join(getAppDirPath('userData'), SONGLIST_DIR, `${hashId}.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保目录存在
|
||||
*/
|
||||
static ensureDirectoryExists(dirPath: string): void {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一hashId
|
||||
*/
|
||||
static generateUniqueId(name: string): string {
|
||||
return crypto.createHash('md5').update(`${name}_${Date.now()}_${Math.random()}`).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证歌曲封面URL是否有效
|
||||
*/
|
||||
static isValidCoverUrl(url: string | undefined | null): boolean {
|
||||
return Boolean(url && url.trim() !== '' && url !== DEFAULT_COVER_IDENTIFIER)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证hashId格式
|
||||
*/
|
||||
static isValidHashId(hashId: string): boolean {
|
||||
return Boolean(hashId && typeof hashId === 'string' && hashId.trim().length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的JSON解析
|
||||
*/
|
||||
static safeJsonParse<T>(content: string, defaultValue: T): T {
|
||||
try {
|
||||
return JSON.parse(content) as T
|
||||
} catch {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class ManageSongList extends PlayListSongs {
|
||||
private readonly hashId: string
|
||||
|
||||
constructor(hashId: string) {
|
||||
if (!SongListUtils.isValidHashId(hashId)) {
|
||||
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
|
||||
}
|
||||
|
||||
super(hashId)
|
||||
this.hashId = hashId.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态方法:创建新歌单
|
||||
* @param name 歌单名称
|
||||
* @param description 歌单描述
|
||||
* @param source 歌单来源
|
||||
* @returns 包含hashId的对象 (id字段就是hashId)
|
||||
*/
|
||||
static createPlaylist(
|
||||
name: string,
|
||||
description: string = '',
|
||||
source: SongList['source']
|
||||
): { id: string } {
|
||||
// 参数验证
|
||||
if (!name?.trim()) {
|
||||
throw new SongListError('歌单名称不能为空', 'EMPTY_NAME')
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
throw new SongListError('歌单来源不能为空', 'EMPTY_SOURCE')
|
||||
}
|
||||
|
||||
try {
|
||||
const id = SongListUtils.generateUniqueId(name)
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const songListInfo: SongList = {
|
||||
id,
|
||||
name: name.trim(),
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
description: description?.trim() || '',
|
||||
coverImgUrl: SongListUtils.getDefaultCoverUrl(),
|
||||
source
|
||||
}
|
||||
|
||||
// 创建歌单文件
|
||||
ManageSongList.createSongListFile(id)
|
||||
|
||||
// 更新入口文件
|
||||
ManageSongList.updateIndexFile(songListInfo, 'add')
|
||||
|
||||
// 验证歌单可以正常实例化
|
||||
try {
|
||||
new ManageSongList(id)
|
||||
// 如果能成功创建实例,说明文件创建成功
|
||||
} catch (verifyError) {
|
||||
console.error('歌单创建验证失败:', verifyError)
|
||||
// 清理已创建的文件
|
||||
try {
|
||||
const filePath = SongListUtils.getSongListFilePath(id)
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error('清理失败的歌单文件时出错:', cleanupError)
|
||||
}
|
||||
throw new SongListError('歌单创建后验证失败', 'CREATION_VERIFICATION_FAILED')
|
||||
}
|
||||
|
||||
return { id }
|
||||
} catch (error) {
|
||||
console.error('创建歌单失败:', error)
|
||||
if (error instanceof SongListError) {
|
||||
throw error
|
||||
}
|
||||
throw new SongListError(
|
||||
`创建歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'CREATE_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建歌单文件
|
||||
* @param hashId 歌单hashId
|
||||
*/
|
||||
private static createSongListFile(hashId: string): void {
|
||||
const songListFilePath = SongListUtils.getSongListFilePath(hashId)
|
||||
const dir = path.dirname(songListFilePath)
|
||||
|
||||
SongListUtils.ensureDirectoryExists(dir)
|
||||
|
||||
try {
|
||||
// 使用原子性写入确保文件完整性
|
||||
const tempPath = `${songListFilePath}.tmp`
|
||||
const content = JSON.stringify([], null, 2)
|
||||
|
||||
fs.writeFileSync(tempPath, content)
|
||||
fs.renameSync(tempPath, songListFilePath)
|
||||
|
||||
// 确保文件确实存在且可读
|
||||
if (!fs.existsSync(songListFilePath)) {
|
||||
throw new Error('文件创建后验证失败')
|
||||
}
|
||||
|
||||
// 验证文件内容
|
||||
const verifyContent = fs.readFileSync(songListFilePath, 'utf-8')
|
||||
JSON.parse(verifyContent) // 确保内容是有效的JSON
|
||||
} catch (error) {
|
||||
throw new SongListError(
|
||||
`创建歌单文件失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'FILE_CREATE_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前歌单
|
||||
*/
|
||||
delete(): void {
|
||||
const hashId = this.getHashId()
|
||||
|
||||
try {
|
||||
// 检查歌单是否存在
|
||||
if (!ManageSongList.exists(hashId)) {
|
||||
throw new SongListError('歌单不存在', 'PLAYLIST_NOT_FOUND')
|
||||
}
|
||||
|
||||
// 删除歌单文件
|
||||
const filePath = SongListUtils.getSongListFilePath(hashId)
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
|
||||
// 从入口文件中移除
|
||||
ManageSongList.updateIndexFile({ id: hashId } as SongList, 'remove')
|
||||
} catch (error) {
|
||||
console.error('删除歌单失败:', error)
|
||||
if (error instanceof SongListError) {
|
||||
throw error
|
||||
}
|
||||
throw new SongListError(
|
||||
`删除歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'DELETE_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改当前歌单信息
|
||||
* @param updates 要更新的字段
|
||||
*/
|
||||
edit(updates: Partial<Omit<SongList, 'id' | 'createTime'>>): void {
|
||||
if (!updates || Object.keys(updates).length === 0) {
|
||||
throw new SongListError('更新内容不能为空', 'EMPTY_UPDATES')
|
||||
}
|
||||
|
||||
const hashId = this.getHashId()
|
||||
|
||||
try {
|
||||
const songLists = ManageSongList.readIndexFile()
|
||||
const index = songLists.findIndex((item) => item.id === hashId)
|
||||
|
||||
if (index === -1) {
|
||||
throw new SongListError('歌单不存在', 'PLAYLIST_NOT_FOUND')
|
||||
}
|
||||
|
||||
// 验证和清理更新数据
|
||||
const cleanUpdates = ManageSongList.validateAndCleanUpdates(updates)
|
||||
|
||||
// 更新歌单信息
|
||||
songLists[index] = {
|
||||
...songLists[index],
|
||||
...cleanUpdates,
|
||||
updateTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
// 保存到入口文件
|
||||
ManageSongList.writeIndexFile(songLists)
|
||||
} catch (error) {
|
||||
console.error('修改歌单失败:', error)
|
||||
if (error instanceof SongListError) {
|
||||
throw error
|
||||
}
|
||||
throw new SongListError(
|
||||
`修改歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'EDIT_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前歌单的hashId
|
||||
* @returns hashId
|
||||
*/
|
||||
private getHashId(): string {
|
||||
return this.hashId
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证和清理更新数据
|
||||
* @param updates 原始更新数据
|
||||
* @returns 清理后的更新数据
|
||||
*/
|
||||
private static validateAndCleanUpdates(
|
||||
updates: Partial<Omit<SongList, 'id' | 'createTime'>>
|
||||
): Partial<Omit<SongList, 'id' | 'createTime'>> {
|
||||
const cleanUpdates: Partial<Omit<SongList, 'id' | 'createTime'>> = {}
|
||||
|
||||
// 验证歌单名称
|
||||
if (updates.name !== undefined) {
|
||||
const trimmedName = updates.name.trim()
|
||||
if (!trimmedName) {
|
||||
throw new SongListError('歌单名称不能为空', 'EMPTY_NAME')
|
||||
}
|
||||
cleanUpdates.name = trimmedName
|
||||
}
|
||||
|
||||
// 处理描述
|
||||
if (updates.description !== undefined) {
|
||||
cleanUpdates.description = updates.description?.trim() || ''
|
||||
}
|
||||
|
||||
// 处理封面URL
|
||||
if (updates.coverImgUrl !== undefined) {
|
||||
cleanUpdates.coverImgUrl = updates.coverImgUrl || SongListUtils.getDefaultCoverUrl()
|
||||
}
|
||||
|
||||
// 处理来源
|
||||
if (updates.source !== undefined) {
|
||||
if (!updates.source) {
|
||||
throw new SongListError('歌单来源不能为空', 'EMPTY_SOURCE')
|
||||
}
|
||||
cleanUpdates.source = updates.source
|
||||
}
|
||||
|
||||
return cleanUpdates
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取歌单列表
|
||||
* @returns 歌单列表数组
|
||||
*/
|
||||
static Read(): SongList[] {
|
||||
try {
|
||||
return ManageSongList.readIndexFile()
|
||||
} catch (error) {
|
||||
console.error('读取歌单列表失败:', error)
|
||||
if (error instanceof SongListError) {
|
||||
throw error
|
||||
}
|
||||
throw new SongListError(
|
||||
`读取歌单列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'READ_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据hashId获取单个歌单信息
|
||||
* @param hashId 歌单hashId
|
||||
* @returns 歌单信息或null
|
||||
*/
|
||||
static getById(hashId: string): SongList | null {
|
||||
if (!SongListUtils.isValidHashId(hashId)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const songLists = ManageSongList.readIndexFile()
|
||||
return songLists.find((item) => item.id === hashId) || null
|
||||
} catch (error) {
|
||||
console.error('获取歌单信息失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取入口文件
|
||||
* @returns 歌单列表数组
|
||||
*/
|
||||
private static readIndexFile(): SongList[] {
|
||||
const indexPath = SongListUtils.getSongListIndexPath()
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
ManageSongList.initializeIndexFile()
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(indexPath, 'utf-8')
|
||||
const parsed = SongListUtils.safeJsonParse<unknown>(content, [])
|
||||
|
||||
// 验证数据格式
|
||||
if (!Array.isArray(parsed)) {
|
||||
console.warn('入口文件格式错误,重新初始化')
|
||||
ManageSongList.initializeIndexFile()
|
||||
return []
|
||||
}
|
||||
|
||||
return parsed as SongList[]
|
||||
} catch (error) {
|
||||
console.error('解析入口文件失败:', error)
|
||||
// 备份损坏的文件并重新初始化
|
||||
ManageSongList.backupCorruptedFile(indexPath)
|
||||
ManageSongList.initializeIndexFile()
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份损坏的文件
|
||||
* @param filePath 文件路径
|
||||
*/
|
||||
private static backupCorruptedFile(filePath: string): void {
|
||||
try {
|
||||
const backupPath = `${filePath}.backup.${Date.now()}`
|
||||
fs.copyFileSync(filePath, backupPath)
|
||||
console.log(`已备份损坏的文件到: ${backupPath}`)
|
||||
} catch (error) {
|
||||
console.error('备份损坏文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化入口文件
|
||||
*/
|
||||
private static initializeIndexFile(): void {
|
||||
const indexPath = SongListUtils.getSongListIndexPath()
|
||||
const dir = path.dirname(indexPath)
|
||||
|
||||
SongListUtils.ensureDirectoryExists(dir)
|
||||
|
||||
try {
|
||||
fs.writeFileSync(indexPath, JSON.stringify([], null, 2))
|
||||
} catch (error) {
|
||||
throw new SongListError(
|
||||
`初始化入口文件失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'INIT_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入入口文件
|
||||
* @param songLists 歌单列表
|
||||
*/
|
||||
private static writeIndexFile(songLists: SongList[]): void {
|
||||
if (!Array.isArray(songLists)) {
|
||||
throw new SongListError('歌单列表必须是数组格式', 'INVALID_DATA_FORMAT')
|
||||
}
|
||||
|
||||
const indexPath = SongListUtils.getSongListIndexPath()
|
||||
const dir = path.dirname(indexPath)
|
||||
|
||||
SongListUtils.ensureDirectoryExists(dir)
|
||||
|
||||
try {
|
||||
// 先写入临时文件,再重命名,确保原子性操作
|
||||
const tempPath = `${indexPath}.tmp`
|
||||
fs.writeFileSync(tempPath, JSON.stringify(songLists, null, 2))
|
||||
fs.renameSync(tempPath, indexPath)
|
||||
} catch (error) {
|
||||
console.error('写入入口文件失败:', error)
|
||||
throw new SongListError(
|
||||
`写入入口文件失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'WRITE_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新入口文件
|
||||
* @param songListInfo 歌单信息
|
||||
* @param action 操作类型
|
||||
*/
|
||||
private static updateIndexFile(songListInfo: SongList, action: 'add' | 'remove'): void {
|
||||
const songLists = ManageSongList.readIndexFile()
|
||||
|
||||
switch (action) {
|
||||
case 'add':
|
||||
// 检查是否已存在,避免重复添加
|
||||
if (!songLists.some((item) => item.id === songListInfo.id)) {
|
||||
songLists.push(songListInfo)
|
||||
}
|
||||
break
|
||||
|
||||
case 'remove':
|
||||
const index = songLists.findIndex((item) => item.id === songListInfo.id)
|
||||
if (index !== -1) {
|
||||
songLists.splice(index, 1)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
throw new SongListError(`不支持的操作类型: ${action}`, 'INVALID_ACTION')
|
||||
}
|
||||
|
||||
ManageSongList.writeIndexFile(songLists)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前歌单封面图片URL
|
||||
* @param coverImgUrl 封面图片URL
|
||||
*/
|
||||
updateCoverImg(coverImgUrl: string): void {
|
||||
try {
|
||||
const finalCoverUrl = coverImgUrl || SongListUtils.getDefaultCoverUrl()
|
||||
this.edit({ coverImgUrl: finalCoverUrl })
|
||||
} catch (error) {
|
||||
console.error('更新封面失败:', error)
|
||||
if (error instanceof SongListError) {
|
||||
throw error
|
||||
}
|
||||
throw new SongListError(
|
||||
`更新封面失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'UPDATE_COVER_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写父类的addSongs方法,添加自动设置封面功能
|
||||
* @param songs 要添加的歌曲列表
|
||||
*/
|
||||
addSongs(songs: Songs[]): void {
|
||||
if (!Array.isArray(songs) || songs.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 调用父类方法添加歌曲
|
||||
super.addSongs(songs)
|
||||
|
||||
// 异步更新封面,不阻塞主要功能
|
||||
setImmediate(() => {
|
||||
this.updateCoverIfNeeded(songs)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并更新封面图片
|
||||
* @param newSongs 新添加的歌曲列表
|
||||
*/
|
||||
private updateCoverIfNeeded(newSongs: Songs[]): void {
|
||||
try {
|
||||
const currentPlaylist = ManageSongList.getById(this.hashId)
|
||||
|
||||
if (!currentPlaylist) {
|
||||
console.warn(`歌单 ${this.hashId} 不存在,跳过封面更新`)
|
||||
return
|
||||
}
|
||||
|
||||
const shouldUpdateCover = this.shouldUpdateCover(currentPlaylist.coverImgUrl)
|
||||
|
||||
if (shouldUpdateCover) {
|
||||
const validCoverUrl = this.findValidCoverFromSongs(newSongs)
|
||||
|
||||
if (validCoverUrl) {
|
||||
this.updateCoverImg(validCoverUrl)
|
||||
} else if (
|
||||
!currentPlaylist.coverImgUrl ||
|
||||
currentPlaylist.coverImgUrl === SongListUtils.getDefaultCoverUrl()
|
||||
) {
|
||||
// 如果没有找到有效封面且当前也没有封面,设置默认封面
|
||||
this.updateCoverImg(SongListUtils.getDefaultCoverUrl())
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新封面失败:', error)
|
||||
// 不抛出错误,避免影响添加歌曲的主要功能
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该更新封面
|
||||
* @param currentCoverUrl 当前封面URL
|
||||
* @returns 是否应该更新
|
||||
*/
|
||||
private shouldUpdateCover(currentCoverUrl: string): boolean {
|
||||
return !currentCoverUrl || currentCoverUrl === SongListUtils.getDefaultCoverUrl()
|
||||
}
|
||||
|
||||
/**
|
||||
* 从歌曲列表中查找有效的封面图片
|
||||
* @param songs 歌曲列表
|
||||
* @returns 有效的封面URL或null
|
||||
*/
|
||||
private findValidCoverFromSongs(songs: Songs[]): string | null {
|
||||
// 优先检查新添加的歌曲
|
||||
for (const song of songs) {
|
||||
if (SongListUtils.isValidCoverUrl(song.img)) {
|
||||
return song.img
|
||||
}
|
||||
}
|
||||
|
||||
// 如果新添加的歌曲都没有封面,检查当前歌单中的所有歌曲
|
||||
try {
|
||||
for (const song of this.list) {
|
||||
if (SongListUtils.isValidCoverUrl(song.img)) {
|
||||
return song.img
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取歌单歌曲列表失败:', error)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查歌单是否存在
|
||||
* @param hashId 歌单hashId
|
||||
* @returns 是否存在
|
||||
*/
|
||||
static exists(hashId: string): boolean {
|
||||
if (!SongListUtils.isValidHashId(hashId)) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const songLists = ManageSongList.readIndexFile()
|
||||
return songLists.some((item) => item.id === hashId)
|
||||
} catch (error) {
|
||||
console.error('检查歌单存在性失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单统计信息
|
||||
* @returns 统计信息
|
||||
*/
|
||||
static getStatistics(): { total: number; bySource: Record<string, number>; lastUpdated: string } {
|
||||
try {
|
||||
const songLists = ManageSongList.readIndexFile()
|
||||
const bySource: Record<string, number> = {}
|
||||
|
||||
songLists.forEach((playlist) => {
|
||||
const source = playlist.source || 'unknown'
|
||||
bySource[source] = (bySource[source] || 0) + 1
|
||||
})
|
||||
|
||||
return {
|
||||
total: songLists.length,
|
||||
bySource,
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
return {
|
||||
total: 0,
|
||||
bySource: {},
|
||||
lastUpdated: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前歌单信息
|
||||
* @returns 歌单信息或null
|
||||
*/
|
||||
getPlaylistInfo(): SongList | null {
|
||||
return ManageSongList.getById(this.hashId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作:删除多个歌单
|
||||
* @param hashIds 歌单ID数组
|
||||
* @returns 操作结果
|
||||
*/
|
||||
static batchDelete(hashIds: string[]): { success: string[]; failed: string[] } {
|
||||
const result = { success: [] as string[], failed: [] as string[] }
|
||||
|
||||
for (const hashId of hashIds) {
|
||||
try {
|
||||
ManageSongList.deleteById(hashId)
|
||||
result.success.push(hashId)
|
||||
} catch (error) {
|
||||
console.error(`删除歌单 ${hashId} 失败:`, error)
|
||||
result.failed.push(hashId)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌单
|
||||
* @param keyword 搜索关键词
|
||||
* @param source 可选的来源筛选
|
||||
* @returns 匹配的歌单列表
|
||||
*/
|
||||
static search(keyword: string, source?: SongList['source']): SongList[] {
|
||||
if (!keyword?.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
const songLists = ManageSongList.readIndexFile()
|
||||
const lowerKeyword = keyword.toLowerCase()
|
||||
|
||||
return songLists.filter((playlist) => {
|
||||
const matchesKeyword =
|
||||
playlist.name.toLowerCase().includes(lowerKeyword) ||
|
||||
playlist.description.toLowerCase().includes(lowerKeyword)
|
||||
const matchesSource = !source || playlist.source === source
|
||||
|
||||
return matchesKeyword && matchesSource
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('搜索歌单失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 静态方法别名,用于删除和编辑指定hashId的歌单
|
||||
/**
|
||||
* 静态方法:删除指定歌单
|
||||
* @param hashId 歌单hashId
|
||||
*/
|
||||
static deleteById(hashId: string): void {
|
||||
if (!SongListUtils.isValidHashId(hashId)) {
|
||||
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
|
||||
}
|
||||
|
||||
const instance = new ManageSongList(hashId)
|
||||
instance.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态方法:编辑指定歌单
|
||||
* @param hashId 歌单hashId
|
||||
* @param updates 要更新的字段
|
||||
*/
|
||||
static editById(hashId: string, updates: Partial<Omit<SongList, 'id' | 'createTime'>>): void {
|
||||
if (!SongListUtils.isValidHashId(hashId)) {
|
||||
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
|
||||
}
|
||||
|
||||
const instance = new ManageSongList(hashId)
|
||||
instance.edit(updates)
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态方法:更新指定歌单封面
|
||||
* @param hashId 歌单hashId
|
||||
* @param coverImgUrl 封面图片URL
|
||||
*/
|
||||
static updateCoverImgById(hashId: string, coverImgUrl: string): void {
|
||||
if (!SongListUtils.isValidHashId(hashId)) {
|
||||
throw new SongListError('无效的歌单ID', 'INVALID_HASH_ID')
|
||||
}
|
||||
|
||||
const instance = new ManageSongList(hashId)
|
||||
instance.updateCoverImg(coverImgUrl)
|
||||
}
|
||||
|
||||
// 保持向后兼容的别名方法
|
||||
static Delete = ManageSongList.deleteById
|
||||
static Edit = ManageSongList.editById
|
||||
static read = ManageSongList.Read
|
||||
}
|
||||
|
||||
// 导出错误类供外部使用
|
||||
export { SongListError }
|
||||
452
src/main/services/songList/PlayListSongs.ts
Normal file
452
src/main/services/songList/PlayListSongs.ts
Normal file
@@ -0,0 +1,452 @@
|
||||
import type { Songs as SongItem } from '@common/types/songList'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getAppDirPath } from '../../utils/path'
|
||||
|
||||
// 错误类定义
|
||||
class PlayListError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code?: string
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'PlayListError'
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数类
|
||||
class PlayListUtils {
|
||||
/**
|
||||
* 获取歌单文件路径
|
||||
*/
|
||||
static getFilePath(hashId: string): string {
|
||||
if (!hashId || typeof hashId !== 'string' || !hashId.trim()) {
|
||||
throw new PlayListError('无效的歌单ID', 'INVALID_HASH_ID')
|
||||
}
|
||||
return path.join(getAppDirPath('userData'), 'songList', `${hashId.trim()}.json`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保目录存在
|
||||
*/
|
||||
static ensureDirectoryExists(dirPath: string): void {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的JSON解析
|
||||
*/
|
||||
static safeJsonParse<T>(content: string, defaultValue: T): T {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
return parsed as T
|
||||
} catch {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的JSON解析(专门用于数组)
|
||||
*/
|
||||
static safeJsonParseArray<T>(content: string, defaultValue: T[]): T[] {
|
||||
try {
|
||||
const parsed = JSON.parse(content)
|
||||
return Array.isArray(parsed) ? parsed : defaultValue
|
||||
} catch {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证歌曲对象
|
||||
*/
|
||||
static isValidSong(song: any): song is SongItem {
|
||||
return (
|
||||
song &&
|
||||
typeof song === 'object' &&
|
||||
(typeof song.songmid === 'string' || typeof song.songmid === 'number') &&
|
||||
String(song.songmid).trim().length > 0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重歌曲列表
|
||||
*/
|
||||
static deduplicateSongs(songs: SongItem[]): SongItem[] {
|
||||
const seen = new Set<string>()
|
||||
return songs.filter((song) => {
|
||||
const songmidStr = String(song.songmid)
|
||||
if (seen.has(songmidStr)) {
|
||||
return false
|
||||
}
|
||||
seen.add(songmidStr)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default class PlayListSongs {
|
||||
protected readonly filePath: string
|
||||
protected list: SongItem[]
|
||||
private isDirty: boolean = false
|
||||
|
||||
constructor(hashId: string) {
|
||||
this.filePath = PlayListUtils.getFilePath(hashId)
|
||||
this.list = []
|
||||
this.initList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化歌单列表
|
||||
*/
|
||||
private initList(): void {
|
||||
// 增加重试机制,处理文件创建的时序问题
|
||||
const maxRetries = 3
|
||||
const retryDelay = 100 // 100ms
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
if (!fs.existsSync(this.filePath)) {
|
||||
if (attempt < maxRetries - 1) {
|
||||
// 等待一段时间后重试
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < retryDelay) {
|
||||
// 简单的同步等待
|
||||
}
|
||||
continue
|
||||
}
|
||||
throw new PlayListError('歌单文件不存在', 'FILE_NOT_FOUND')
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(this.filePath, 'utf-8')
|
||||
const parsed = PlayListUtils.safeJsonParseArray<SongItem>(content, [])
|
||||
|
||||
// 验证和清理数据
|
||||
this.list = parsed.filter(PlayListUtils.isValidSong)
|
||||
|
||||
// 如果数据被清理过,标记为需要保存
|
||||
if (this.list.length !== parsed.length) {
|
||||
this.isDirty = true
|
||||
console.warn(
|
||||
`歌单文件包含无效数据,已自动清理 ${parsed.length - this.list.length} 条无效记录`
|
||||
)
|
||||
}
|
||||
|
||||
// 成功读取,退出重试循环
|
||||
return
|
||||
} catch (error) {
|
||||
if (attempt < maxRetries - 1) {
|
||||
console.warn(`读取歌单文件失败,第 ${attempt + 1} 次重试:`, error)
|
||||
// 等待一段时间后重试
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < retryDelay) {
|
||||
// 简单的同步等待
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
console.error('读取歌单文件失败:', error)
|
||||
throw new PlayListError(
|
||||
`读取歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'READ_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查歌单文件是否存在
|
||||
*/
|
||||
static hasListFile(hashId: string): boolean {
|
||||
try {
|
||||
const filePath = PlayListUtils.getFilePath(hashId)
|
||||
return fs.existsSync(filePath)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加歌曲到歌单
|
||||
*/
|
||||
addSongs(songs: SongItem[]): void {
|
||||
if (!Array.isArray(songs) || songs.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证和过滤有效歌曲
|
||||
const validSongs = songs.filter(PlayListUtils.isValidSong)
|
||||
if (validSongs.length === 0) {
|
||||
console.warn('没有有效的歌曲可添加')
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 Set 提高查重性能,统一转换为字符串进行比较
|
||||
const existingSongMids = new Set(this.list.map((song) => String(song.songmid)))
|
||||
|
||||
// 添加不重复的歌曲
|
||||
const newSongs = validSongs.filter((song) => !existingSongMids.has(String(song.songmid)))
|
||||
|
||||
if (newSongs.length > 0) {
|
||||
this.list.push(...newSongs)
|
||||
this.isDirty = true
|
||||
this.saveToFile()
|
||||
|
||||
console.log(
|
||||
`成功添加 ${newSongs.length} 首歌曲,跳过 ${validSongs.length - newSongs.length} 首重复歌曲`
|
||||
)
|
||||
} else {
|
||||
console.log('所有歌曲都已存在,未添加任何歌曲')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从歌单中移除歌曲
|
||||
*/
|
||||
removeSong(songmid: string | number): boolean {
|
||||
if (!songmid && songmid !== 0) {
|
||||
throw new PlayListError('无效的歌曲ID', 'INVALID_SONG_ID')
|
||||
}
|
||||
|
||||
const songmidStr = String(songmid)
|
||||
const index = this.list.findIndex((item) => String(item.songmid) === songmidStr)
|
||||
if (index !== -1) {
|
||||
this.list.splice(index, 1)
|
||||
this.isDirty = true
|
||||
this.saveToFile()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移除歌曲
|
||||
*/
|
||||
removeSongs(songmids: (string | number)[]): { removed: number; notFound: number } {
|
||||
if (!Array.isArray(songmids) || songmids.length === 0) {
|
||||
return { removed: 0, notFound: 0 }
|
||||
}
|
||||
|
||||
const validSongMids = songmids.filter(
|
||||
(id) => (id || id === 0) && (typeof id === 'string' || typeof id === 'number')
|
||||
)
|
||||
const songMidSet = new Set(validSongMids.map((id) => String(id)))
|
||||
|
||||
const initialLength = this.list.length
|
||||
this.list = this.list.filter((song) => !songMidSet.has(String(song.songmid)))
|
||||
|
||||
const removedCount = initialLength - this.list.length
|
||||
const notFoundCount = validSongMids.length - removedCount
|
||||
|
||||
if (removedCount > 0) {
|
||||
this.isDirty = true
|
||||
this.saveToFile()
|
||||
}
|
||||
|
||||
return { removed: removedCount, notFound: notFoundCount }
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空歌单
|
||||
*/
|
||||
clearSongs(): void {
|
||||
if (this.list.length > 0) {
|
||||
this.list = []
|
||||
this.isDirty = true
|
||||
this.saveToFile()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存到文件
|
||||
*/
|
||||
private saveToFile(): void {
|
||||
if (!this.isDirty) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const dir = path.dirname(this.filePath)
|
||||
PlayListUtils.ensureDirectoryExists(dir)
|
||||
|
||||
// 原子性写入:先写临时文件,再重命名
|
||||
const tempPath = `${this.filePath}.tmp`
|
||||
const content = JSON.stringify(this.list, null, 2)
|
||||
|
||||
fs.writeFileSync(tempPath, content)
|
||||
fs.renameSync(tempPath, this.filePath)
|
||||
|
||||
this.isDirty = false
|
||||
} catch (error) {
|
||||
console.error('保存歌单文件失败:', error)
|
||||
throw new PlayListError(
|
||||
`保存歌单失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
'SAVE_FAILED'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制保存到文件
|
||||
*/
|
||||
forceSave(): void {
|
||||
this.isDirty = true
|
||||
this.saveToFile()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲列表
|
||||
*/
|
||||
getSongs(): readonly SongItem[] {
|
||||
return Object.freeze([...this.list])
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌曲数量
|
||||
*/
|
||||
getCount(): number {
|
||||
return this.list.length
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查歌曲是否存在
|
||||
*/
|
||||
hasSong(songmid: string | number): boolean {
|
||||
if (!songmid && songmid !== 0) {
|
||||
return false
|
||||
}
|
||||
const songmidStr = String(songmid)
|
||||
return this.list.some((song) => String(song.songmid) === songmidStr)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据songmid获取歌曲
|
||||
*/
|
||||
getSong(songmid: string | number): SongItem | null {
|
||||
if (!songmid && songmid !== 0) {
|
||||
return null
|
||||
}
|
||||
const songmidStr = String(songmid)
|
||||
return this.list.find((song) => String(song.songmid) === songmidStr) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
*/
|
||||
searchSongs(keyword: string): SongItem[] {
|
||||
if (!keyword || typeof keyword !== 'string') {
|
||||
return []
|
||||
}
|
||||
|
||||
const lowerKeyword = keyword.toLowerCase()
|
||||
return this.list.filter(
|
||||
(song) =>
|
||||
song.name?.toLowerCase().includes(lowerKeyword) ||
|
||||
song.singer?.toLowerCase().includes(lowerKeyword) ||
|
||||
song.albumName?.toLowerCase().includes(lowerKeyword)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单统计信息
|
||||
*/
|
||||
getStatistics(): {
|
||||
total: number
|
||||
bySinger: Record<string, number>
|
||||
byAlbum: Record<string, number>
|
||||
lastModified: string
|
||||
} {
|
||||
const bySinger: Record<string, number> = {}
|
||||
const byAlbum: Record<string, number> = {}
|
||||
|
||||
this.list.forEach((song) => {
|
||||
// 统计歌手
|
||||
if (song.singer) {
|
||||
const singerName = String(song.singer)
|
||||
bySinger[singerName] = (bySinger[singerName] || 0) + 1
|
||||
}
|
||||
|
||||
// 统计专辑
|
||||
if (song.albumName) {
|
||||
const albumName = String(song.albumName)
|
||||
byAlbum[albumName] = (byAlbum[albumName] || 0) + 1
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
total: this.list.length,
|
||||
bySinger,
|
||||
byAlbum,
|
||||
lastModified: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证歌单完整性
|
||||
*/
|
||||
validateIntegrity(): { isValid: boolean; issues: string[] } {
|
||||
const issues: string[] = []
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(this.filePath)) {
|
||||
issues.push('歌单文件不存在')
|
||||
}
|
||||
|
||||
// 检查数据完整性
|
||||
const invalidSongs = this.list.filter((song) => !PlayListUtils.isValidSong(song))
|
||||
if (invalidSongs.length > 0) {
|
||||
issues.push(`发现 ${invalidSongs.length} 首无效歌曲`)
|
||||
}
|
||||
|
||||
// 检查重复歌曲
|
||||
const songMids = this.list.map((song) => String(song.songmid))
|
||||
const uniqueSongMids = new Set(songMids)
|
||||
if (songMids.length !== uniqueSongMids.size) {
|
||||
issues.push(`发现 ${songMids.length - uniqueSongMids.size} 首重复歌曲`)
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: issues.length === 0,
|
||||
issues
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复歌单数据
|
||||
*/
|
||||
repairData(): { fixed: boolean; changes: string[] } {
|
||||
const changes: string[] = []
|
||||
let hasChanges = false
|
||||
|
||||
// 移除无效歌曲
|
||||
const validSongs = this.list.filter(PlayListUtils.isValidSong)
|
||||
if (validSongs.length !== this.list.length) {
|
||||
changes.push(`移除了 ${this.list.length - validSongs.length} 首无效歌曲`)
|
||||
this.list = validSongs
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
// 去重
|
||||
const deduplicatedSongs = PlayListUtils.deduplicateSongs(this.list)
|
||||
if (deduplicatedSongs.length !== this.list.length) {
|
||||
changes.push(`移除了 ${this.list.length - deduplicatedSongs.length} 首重复歌曲`)
|
||||
this.list = deduplicatedSongs
|
||||
hasChanges = true
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
this.isDirty = true
|
||||
this.saveToFile()
|
||||
}
|
||||
|
||||
return {
|
||||
fixed: hasChanges,
|
||||
changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出错误类供外部使用
|
||||
export { PlayListError }
|
||||
@@ -407,7 +407,6 @@ export default {
|
||||
return result.list[0].global_collection_id
|
||||
},
|
||||
|
||||
|
||||
async getUserListDetailByLink({ info }, link) {
|
||||
let listInfo = info['0']
|
||||
let total = listInfo.count
|
||||
|
||||
@@ -153,14 +153,14 @@ export default {
|
||||
_types.flac = {
|
||||
size
|
||||
}
|
||||
|
||||
|
||||
case 320000:
|
||||
size = item.h ? sizeFormate(item.h.size) : null
|
||||
types.push({ type: '320k', size })
|
||||
_types['320k'] = {
|
||||
size
|
||||
}
|
||||
|
||||
|
||||
case 192000:
|
||||
case 128000:
|
||||
size = item.l ? sizeFormate(item.l.size) : null
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import electron from 'electron'
|
||||
|
||||
function getAppDirPath(name?: "home"
|
||||
| "appData"
|
||||
| "assets"
|
||||
| "userData"
|
||||
| "sessionData"
|
||||
| "temp"
|
||||
| "exe"
|
||||
| "module"
|
||||
| "desktop"
|
||||
| "documents"
|
||||
| "downloads"
|
||||
| "music"
|
||||
| "pictures"
|
||||
| "videos"
|
||||
| "recent"
|
||||
| "logs"
|
||||
| "crashDumps") {
|
||||
function getAppDirPath(
|
||||
name?:
|
||||
| 'home'
|
||||
| 'appData'
|
||||
| 'assets'
|
||||
| 'userData'
|
||||
| 'sessionData'
|
||||
| 'temp'
|
||||
| 'exe'
|
||||
| 'module'
|
||||
| 'desktop'
|
||||
| 'documents'
|
||||
| 'downloads'
|
||||
| 'music'
|
||||
| 'pictures'
|
||||
| 'videos'
|
||||
| 'recent'
|
||||
| 'logs'
|
||||
| 'crashDumps'
|
||||
) {
|
||||
let dirPath: string = electron.app.getPath(name ?? 'userData')
|
||||
return dirPath
|
||||
}
|
||||
|
||||
36
src/preload/index.d.ts
vendored
36
src/preload/index.d.ts
vendored
@@ -8,7 +8,7 @@ interface CustomAPI {
|
||||
close: () => void
|
||||
setMiniMode: (isMini: boolean) => void
|
||||
toggleFullscreen: () => void
|
||||
onMusicCtrl: (callback: (event: Event, args: any) => void) => void
|
||||
onMusicCtrl: (callback: (event: Event, args: any) => void) => () => void
|
||||
|
||||
music: {
|
||||
request: (api: string, args: any) => Promise<any>
|
||||
@@ -24,7 +24,37 @@ interface CustomAPI {
|
||||
getInfo: () => Promise<any>
|
||||
clear: () => Promise
|
||||
getSize: () => Promise<string>
|
||||
},
|
||||
}
|
||||
|
||||
// 歌单管理 API
|
||||
songList: {
|
||||
// === 歌单管理 ===
|
||||
create: (name: string, description?: string, source?: string) => Promise<any>
|
||||
getAll: () => Promise<any>
|
||||
getById: (hashId: string) => Promise<any>
|
||||
delete: (hashId: string) => Promise<any>
|
||||
batchDelete: (hashIds: string[]) => Promise<any>
|
||||
edit: (hashId: string, updates: any) => Promise<any>
|
||||
updateCover: (hashId: string, coverImgUrl: string) => Promise<any>
|
||||
search: (keyword: string, source?: string) => Promise<any>
|
||||
getStatistics: () => Promise<any>
|
||||
exists: (hashId: string) => Promise<any>
|
||||
|
||||
// === 歌曲管理 ===
|
||||
addSongs: (hashId: string, songs: any[]) => Promise<any>
|
||||
removeSong: (hashId: string, songmid: string | number) => Promise<any>
|
||||
removeSongs: (hashId: string, songmids: (string | number)[]) => Promise<any>
|
||||
clearSongs: (hashId: string) => Promise<any>
|
||||
getSongs: (hashId: string) => Promise<any>
|
||||
getSongCount: (hashId: string) => Promise<any>
|
||||
hasSong: (hashId: string, songmid: string | number) => Promise<any>
|
||||
getSong: (hashId: string, songmid: string | number) => Promise<any>
|
||||
searchSongs: (hashId: string, keyword: string) => Promise<any>
|
||||
getSongStatistics: (hashId: string) => Promise<any>
|
||||
validateIntegrity: (hashId: string) => Promise<any>
|
||||
repairData: (hashId: string) => Promise<any>
|
||||
forceSave: (hashId: string) => Promise<any>
|
||||
}
|
||||
|
||||
ai: {
|
||||
ask: (prompt: string) => Promise<any>
|
||||
@@ -46,7 +76,7 @@ interface CustomAPI {
|
||||
}
|
||||
ping: (callback: Function<any>) => undefined
|
||||
pingService: {
|
||||
start: () => undefined,
|
||||
start: () => undefined
|
||||
stop: () => undefined
|
||||
}
|
||||
// 用户配置API
|
||||
|
||||
@@ -21,8 +21,11 @@ const api = {
|
||||
ipcRenderer.send('window-mini-mode', isMini)
|
||||
},
|
||||
toggleFullscreen: () => ipcRenderer.send('window-toggle-fullscreen'),
|
||||
onMusicCtrl: (callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) =>
|
||||
ipcRenderer.on('music-control', callback),
|
||||
onMusicCtrl: (callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
||||
const handler = (event: Electron.IpcRendererEvent) => callback(event)
|
||||
ipcRenderer.on('music-control', handler)
|
||||
return () => ipcRenderer.removeListener('music-control', handler)
|
||||
},
|
||||
|
||||
music: {
|
||||
request: (api: string, args: any) => ipcRenderer.invoke('service-music-request', api, args),
|
||||
@@ -67,6 +70,47 @@ const api = {
|
||||
getSize: () => ipcRenderer.invoke('music-cache:get-size')
|
||||
},
|
||||
|
||||
// 歌单管理 API
|
||||
songList: {
|
||||
// === 歌单管理 ===
|
||||
create: (name: string, description?: string, source?: string) =>
|
||||
ipcRenderer.invoke('songlist:create', name, description, source),
|
||||
getAll: () => ipcRenderer.invoke('songlist:get-all'),
|
||||
getById: (hashId: string) => ipcRenderer.invoke('songlist:get-by-id', hashId),
|
||||
delete: (hashId: string) => ipcRenderer.invoke('songlist:delete', hashId),
|
||||
batchDelete: (hashIds: string[]) => ipcRenderer.invoke('songlist:batch-delete', hashIds),
|
||||
edit: (hashId: string, updates: any) => ipcRenderer.invoke('songlist:edit', hashId, updates),
|
||||
updateCover: (hashId: string, coverImgUrl: string) =>
|
||||
ipcRenderer.invoke('songlist:update-cover', hashId, coverImgUrl),
|
||||
search: (keyword: string, source?: string) =>
|
||||
ipcRenderer.invoke('songlist:search', keyword, source),
|
||||
getStatistics: () => ipcRenderer.invoke('songlist:get-statistics'),
|
||||
exists: (hashId: string) => ipcRenderer.invoke('songlist:exists', hashId),
|
||||
|
||||
// === 歌曲管理 ===
|
||||
addSongs: (hashId: string, songs: any[]) =>
|
||||
ipcRenderer.invoke('songlist:add-songs', hashId, songs),
|
||||
removeSong: (hashId: string, songmid: string | number) =>
|
||||
ipcRenderer.invoke('songlist:remove-song', hashId, songmid),
|
||||
removeSongs: (hashId: string, songmids: (string | number)[]) =>
|
||||
ipcRenderer.invoke('songlist:remove-songs', hashId, songmids),
|
||||
clearSongs: (hashId: string) => ipcRenderer.invoke('songlist:clear-songs', hashId),
|
||||
getSongs: (hashId: string) => ipcRenderer.invoke('songlist:get-songs', hashId),
|
||||
getSongCount: (hashId: string) => ipcRenderer.invoke('songlist:get-song-count', hashId),
|
||||
hasSong: (hashId: string, songmid: string | number) =>
|
||||
ipcRenderer.invoke('songlist:has-song', hashId, songmid),
|
||||
getSong: (hashId: string, songmid: string | number) =>
|
||||
ipcRenderer.invoke('songlist:get-song', hashId, songmid),
|
||||
searchSongs: (hashId: string, keyword: string) =>
|
||||
ipcRenderer.invoke('songlist:search-songs', hashId, keyword),
|
||||
getSongStatistics: (hashId: string) =>
|
||||
ipcRenderer.invoke('songlist:get-song-statistics', hashId),
|
||||
validateIntegrity: (hashId: string) =>
|
||||
ipcRenderer.invoke('songlist:validate-integrity', hashId),
|
||||
repairData: (hashId: string) => ipcRenderer.invoke('songlist:repair-data', hashId),
|
||||
forceSave: (hashId: string) => ipcRenderer.invoke('songlist:force-save', hashId)
|
||||
},
|
||||
|
||||
getUserConfig: () => ipcRenderer.invoke('get-user-config'),
|
||||
|
||||
// 自动更新相关
|
||||
@@ -77,42 +121,47 @@ const api = {
|
||||
|
||||
// 监听更新事件
|
||||
onCheckingForUpdate: (callback: () => void) => {
|
||||
ipcRenderer.on('auto-updater:checking-for-update', callback);
|
||||
ipcRenderer.on('auto-updater:checking-for-update', callback)
|
||||
},
|
||||
onUpdateAvailable: (callback: () => void) => {
|
||||
ipcRenderer.on('auto-updater:update-available', callback);
|
||||
ipcRenderer.on('auto-updater:update-available', callback)
|
||||
},
|
||||
onUpdateNotAvailable: (callback: () => void) => {
|
||||
ipcRenderer.on('auto-updater:update-not-available', callback);
|
||||
ipcRenderer.on('auto-updater:update-not-available', callback)
|
||||
},
|
||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||
ipcRenderer.on('auto-updater:download-progress', (_, progress) => callback(progress));
|
||||
ipcRenderer.on('auto-updater:download-progress', (_, progress) => callback(progress))
|
||||
},
|
||||
onUpdateDownloaded: (callback: () => void) => {
|
||||
ipcRenderer.on('auto-updater:update-downloaded', callback);
|
||||
ipcRenderer.on('auto-updater:update-downloaded', callback)
|
||||
},
|
||||
onError: (callback: (error: string) => void) => {
|
||||
ipcRenderer.on('auto-updater:error', (_, error) => callback(error));
|
||||
ipcRenderer.on('auto-updater:error', (_, error) => callback(error))
|
||||
},
|
||||
onDownloadStarted: (callback: (updateInfo: any) => void) => {
|
||||
ipcRenderer.on('auto-updater:download-started', (_, updateInfo) => callback(updateInfo));
|
||||
ipcRenderer.on('auto-updater:download-started', (_, updateInfo) => callback(updateInfo))
|
||||
},
|
||||
|
||||
// 移除所有监听器
|
||||
removeAllListeners: () => {
|
||||
ipcRenderer.removeAllListeners('auto-updater:checking-for-update');
|
||||
ipcRenderer.removeAllListeners('auto-updater:update-available');
|
||||
ipcRenderer.removeAllListeners('auto-updater:update-not-available');
|
||||
ipcRenderer.removeAllListeners('auto-updater:download-started');
|
||||
ipcRenderer.removeAllListeners('auto-updater:download-progress');
|
||||
ipcRenderer.removeAllListeners('auto-updater:update-downloaded');
|
||||
ipcRenderer.removeAllListeners('auto-updater:error');
|
||||
ipcRenderer.removeAllListeners('auto-updater:checking-for-update')
|
||||
ipcRenderer.removeAllListeners('auto-updater:update-available')
|
||||
ipcRenderer.removeAllListeners('auto-updater:update-not-available')
|
||||
ipcRenderer.removeAllListeners('auto-updater:download-started')
|
||||
ipcRenderer.removeAllListeners('auto-updater:download-progress')
|
||||
ipcRenderer.removeAllListeners('auto-updater:update-downloaded')
|
||||
ipcRenderer.removeAllListeners('auto-updater:error')
|
||||
}
|
||||
},
|
||||
ping: (callbaack: Function) => ipcRenderer.on('song-ended', () => callbaack()),
|
||||
pingService: {
|
||||
start: () => { ipcRenderer.send('startPing'); console.log('eventStart') },
|
||||
stop: () => { ipcRenderer.send('stopPing') }
|
||||
start: () => {
|
||||
ipcRenderer.send('startPing')
|
||||
console.log('eventStart')
|
||||
},
|
||||
stop: () => {
|
||||
ipcRenderer.send('stopPing')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
src/renderer/components.d.ts
vendored
16
src/renderer/components.d.ts
vendored
@@ -9,6 +9,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
|
||||
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
||||
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
||||
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
||||
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
|
||||
@@ -21,8 +22,23 @@ declare module 'vue' {
|
||||
SearchComponent: typeof import('./src/components/Search/SearchComponent.vue')['default']
|
||||
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
|
||||
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
|
||||
TAlert: typeof import('tdesign-vue-next')['Alert']
|
||||
TAside: typeof import('tdesign-vue-next')['Aside']
|
||||
TBadge: typeof import('tdesign-vue-next')['Badge']
|
||||
TButton: typeof import('tdesign-vue-next')['Button']
|
||||
TContent: typeof import('tdesign-vue-next')['Content']
|
||||
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
||||
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
|
||||
TForm: typeof import('tdesign-vue-next')['Form']
|
||||
TFormItem: typeof import('tdesign-vue-next')['FormItem']
|
||||
ThemeSelector: typeof import('./src/components/ThemeSelector.vue')['default']
|
||||
TImage: typeof import('tdesign-vue-next')['Image']
|
||||
TInput: typeof import('tdesign-vue-next')['Input']
|
||||
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
|
||||
TLayout: typeof import('tdesign-vue-next')['Layout']
|
||||
TLoading: typeof import('tdesign-vue-next')['Loading']
|
||||
TTextarea: typeof import('tdesign-vue-next')['Textarea']
|
||||
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
|
||||
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
|
||||
UpdateProgress: typeof import('./src/components/UpdateProgress.vue')['default']
|
||||
UpdateSettings: typeof import('./src/components/Settings/UpdateSettings.vue')['default']
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head lang="zh-CN">
|
||||
<meta charset="UTF-8" />
|
||||
<title>Electron</title>
|
||||
<title>澜音 Music</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<!-- <meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
BIN
src/renderer/public/default-cover.png
Normal file
BIN
src/renderer/public/default-cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 824 KiB |
BIN
src/renderer/public/head.jpg
Normal file
BIN
src/renderer/public/head.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
BIN
src/renderer/public/star.png
Normal file
BIN
src/renderer/public/star.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
src/renderer/public/wldss.png
Normal file
BIN
src/renderer/public/wldss.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
@@ -35,7 +35,7 @@ const themes = [
|
||||
|
||||
const loadSavedTheme = () => {
|
||||
const savedTheme = localStorage.getItem('selected-theme')
|
||||
if (savedTheme && themes.some(t => t.name === savedTheme)) {
|
||||
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
|
||||
applyTheme(savedTheme)
|
||||
}
|
||||
}
|
||||
@@ -59,9 +59,11 @@ const applyTheme = (themeName) => {
|
||||
<template>
|
||||
<div class="page">
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition :enter-active-class="`animate__animated animate__fadeIn pagesApp`"
|
||||
:leave-active-class="`animate__animated animate__fadeOut pagesApp`">
|
||||
<component :is="Component" />
|
||||
<Transition
|
||||
:enter-active-class="`animate__animated animate__fadeIn pagesApp`"
|
||||
:leave-active-class="`animate__animated animate__fadeOut pagesApp`"
|
||||
>
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</router-view>
|
||||
<GlobalAudio />
|
||||
@@ -74,4 +76,4 @@ const applyTheme = (themeName) => {
|
||||
width: 100vw;
|
||||
position: fixed;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
478
src/renderer/src/api/songList.ts
Normal file
478
src/renderer/src/api/songList.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
import type {
|
||||
SongListAPI,
|
||||
IPCResponse,
|
||||
BatchOperationResult,
|
||||
RemoveSongsResult,
|
||||
SongListStatistics,
|
||||
SongStatistics,
|
||||
IntegrityCheckResult,
|
||||
RepairResult
|
||||
} from '../../../types/songList'
|
||||
import type { SongList, Songs } from '@common/types/songList'
|
||||
|
||||
// 检查是否在 Electron 环境中
|
||||
const isElectron = typeof window !== 'undefined' && window.api && window.api.songList
|
||||
|
||||
/**
|
||||
* 歌单管理 API 封装类
|
||||
*/
|
||||
class SongListService implements SongListAPI {
|
||||
private get songListAPI() {
|
||||
if (!isElectron) {
|
||||
throw new Error('当前环境不支持 Electron API 调用')
|
||||
}
|
||||
return window.api.songList
|
||||
}
|
||||
|
||||
// === 歌单管理方法 ===
|
||||
|
||||
/**
|
||||
* 创建新歌单
|
||||
*/
|
||||
async create(
|
||||
name: string,
|
||||
description: string = '',
|
||||
source: SongList['source'] = 'local'
|
||||
): Promise<IPCResponse<{ id: string }>> {
|
||||
try {
|
||||
return await this.songListAPI.create(name, description, source)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '创建歌单失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有歌单
|
||||
*/
|
||||
async getAll(): Promise<IPCResponse<SongList[]>> {
|
||||
try {
|
||||
return await this.songListAPI.getAll()
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取歌单列表失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取歌单信息
|
||||
*/
|
||||
async getById(hashId: string): Promise<IPCResponse<SongList | null>> {
|
||||
try {
|
||||
return await this.songListAPI.getById(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取歌单信息失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除歌单
|
||||
*/
|
||||
async delete(hashId: string): Promise<IPCResponse> {
|
||||
try {
|
||||
return await this.songListAPI.delete(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '删除歌单失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除歌单
|
||||
*/
|
||||
async batchDelete(hashIds: string[]): Promise<IPCResponse<BatchOperationResult>> {
|
||||
try {
|
||||
return await this.songListAPI.batchDelete(hashIds)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '批量删除歌单失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑歌单信息
|
||||
*/
|
||||
async edit(
|
||||
hashId: string,
|
||||
updates: Partial<Omit<SongList, 'id' | 'createTime'>>
|
||||
): Promise<IPCResponse> {
|
||||
try {
|
||||
return await this.songListAPI.edit(hashId, updates)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '编辑歌单失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新歌单封面
|
||||
*/
|
||||
async updateCover(hashId: string, coverImgUrl: string): Promise<IPCResponse> {
|
||||
try {
|
||||
return await this.songListAPI.updateCover(hashId, coverImgUrl)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '更新封面失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌单
|
||||
*/
|
||||
async search(keyword: string, source?: SongList['source']): Promise<IPCResponse<SongList[]>> {
|
||||
try {
|
||||
return await this.songListAPI.search(keyword, source)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '搜索歌单失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单统计信息
|
||||
*/
|
||||
async getStatistics(): Promise<IPCResponse<SongListStatistics>> {
|
||||
try {
|
||||
return await this.songListAPI.getStatistics()
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取统计信息失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查歌单是否存在
|
||||
*/
|
||||
async exists(hashId: string): Promise<IPCResponse<boolean>> {
|
||||
try {
|
||||
return await this.songListAPI.exists(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '检查歌单存在性失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 歌曲管理方法 ===
|
||||
|
||||
/**
|
||||
* 添加歌曲到歌单
|
||||
*/
|
||||
async addSongs(hashId: string, songs: Songs[]): Promise<IPCResponse> {
|
||||
try {
|
||||
return await this.songListAPI.addSongs(hashId, songs)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '添加歌曲失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从歌单移除歌曲
|
||||
*/
|
||||
async removeSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>> {
|
||||
try {
|
||||
return await this.songListAPI.removeSong(hashId, songmid)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '移除歌曲失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移除歌曲
|
||||
*/
|
||||
async removeSongs(
|
||||
hashId: string,
|
||||
songmids: (string | number)[]
|
||||
): Promise<IPCResponse<RemoveSongsResult>> {
|
||||
try {
|
||||
return await this.songListAPI.removeSongs(hashId, songmids)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '批量移除歌曲失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空歌单
|
||||
*/
|
||||
async clearSongs(hashId: string): Promise<IPCResponse> {
|
||||
try {
|
||||
return await this.songListAPI.clearSongs(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '清空歌单失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单中的歌曲列表
|
||||
*/
|
||||
async getSongs(hashId: string): Promise<IPCResponse<readonly Songs[]>> {
|
||||
try {
|
||||
return await this.songListAPI.getSongs(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取歌曲列表失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单歌曲数量
|
||||
*/
|
||||
async getSongCount(hashId: string): Promise<IPCResponse<number>> {
|
||||
try {
|
||||
return await this.songListAPI.getSongCount(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取歌曲数量失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查歌曲是否在歌单中
|
||||
*/
|
||||
async hasSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>> {
|
||||
try {
|
||||
return await this.songListAPI.hasSong(hashId, songmid)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '检查歌曲存在性失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取歌曲
|
||||
*/
|
||||
async getSong(hashId: string, songmid: string | number): Promise<IPCResponse<Songs | null>> {
|
||||
try {
|
||||
return await this.songListAPI.getSong(hashId, songmid)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取歌曲信息失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索歌单中的歌曲
|
||||
*/
|
||||
async searchSongs(hashId: string, keyword: string): Promise<IPCResponse<Songs[]>> {
|
||||
try {
|
||||
return await this.songListAPI.searchSongs(hashId, keyword)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '搜索歌曲失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单歌曲统计信息
|
||||
*/
|
||||
async getSongStatistics(hashId: string): Promise<IPCResponse<SongStatistics>> {
|
||||
try {
|
||||
return await this.songListAPI.getSongStatistics(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取歌曲统计信息失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证歌单完整性
|
||||
*/
|
||||
async validateIntegrity(hashId: string): Promise<IPCResponse<IntegrityCheckResult>> {
|
||||
try {
|
||||
return await this.songListAPI.validateIntegrity(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '验证数据完整性失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复歌单数据
|
||||
*/
|
||||
async repairData(hashId: string): Promise<IPCResponse<RepairResult>> {
|
||||
try {
|
||||
return await this.songListAPI.repairData(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '修复数据失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制保存歌单
|
||||
*/
|
||||
async forceSave(hashId: string): Promise<IPCResponse> {
|
||||
try {
|
||||
return await this.songListAPI.forceSave(hashId)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '强制保存失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 便捷方法 ===
|
||||
|
||||
/**
|
||||
* 创建本地歌单的便捷方法
|
||||
*/
|
||||
async createLocal(name: string, description?: string): Promise<IPCResponse<{ id: string }>> {
|
||||
return this.create(name, description, 'local')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取歌单详细信息(包含歌曲列表)
|
||||
*/
|
||||
async getPlaylistDetail(hashId: string): Promise<{
|
||||
playlist: SongList | null
|
||||
songs: readonly Songs[]
|
||||
success: boolean
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const [playlistRes, songsRes] = await Promise.all([
|
||||
this.getById(hashId),
|
||||
this.getSongs(hashId)
|
||||
])
|
||||
|
||||
if (!playlistRes.success) {
|
||||
return {
|
||||
playlist: null,
|
||||
songs: [],
|
||||
success: false,
|
||||
error: playlistRes.error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
playlist: playlistRes.data || null,
|
||||
songs: songsRes.success ? songsRes.data || [] : [],
|
||||
success: true
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
playlist: null,
|
||||
songs: [],
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '获取歌单详情失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全删除歌单(带确认)
|
||||
*/
|
||||
async safeDelete(hashId: string, confirmCallback?: () => Promise<boolean>): Promise<IPCResponse> {
|
||||
if (confirmCallback) {
|
||||
const confirmed = await confirmCallback()
|
||||
if (!confirmed) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户取消删除操作'
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.delete(hashId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并修复歌单数据
|
||||
*/
|
||||
async checkAndRepair(hashId: string): Promise<{
|
||||
needsRepair: boolean
|
||||
repairResult?: RepairResult
|
||||
success: boolean
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
const integrityRes = await this.validateIntegrity(hashId)
|
||||
if (!integrityRes.success) {
|
||||
return {
|
||||
needsRepair: false,
|
||||
success: false,
|
||||
error: integrityRes.error
|
||||
}
|
||||
}
|
||||
|
||||
const { isValid } = integrityRes.data!
|
||||
if (isValid) {
|
||||
return {
|
||||
needsRepair: false,
|
||||
success: true
|
||||
}
|
||||
}
|
||||
|
||||
const repairRes = await this.repairData(hashId)
|
||||
return {
|
||||
needsRepair: true,
|
||||
repairResult: repairRes.data,
|
||||
success: repairRes.success,
|
||||
error: repairRes.error
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
needsRepair: false,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '检查修复失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
export const songListAPI = new SongListService()
|
||||
|
||||
// 默认导出
|
||||
export default songListAPI
|
||||
|
||||
// 导出类型
|
||||
export type { SongListAPI, IPCResponse }
|
||||
@@ -1,8 +1,12 @@
|
||||
@import './icon_font/iconfont.css';
|
||||
:root {
|
||||
--play-bottom-height: max(min(10vh, 86px), 70px);
|
||||
--play-bottom-height: max(min(10vh, 80px), 70px);
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'lyricfont';
|
||||
src: url('./lyricfont.ttf');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
|
||||
BIN
src/renderer/src/assets/logo.png
Normal file
BIN
src/renderer/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
src/renderer/src/assets/lyricfont.ttf
Normal file
BIN
src/renderer/src/assets/lyricfont.ttf
Normal file
Binary file not shown.
@@ -1,4 +1,3 @@
|
||||
|
||||
:root,
|
||||
:root[theme-mode='light'] {
|
||||
--td-brand-color-1: #e2fae2;
|
||||
|
||||
@@ -1,355 +1,379 @@
|
||||
:root[theme-mode="blue"] {
|
||||
--td-brand-color-1: #ecf4ff;
|
||||
--td-brand-color-2: #cde5ff;
|
||||
--td-brand-color-3: #9aceff;
|
||||
--td-brand-color-4: #57b4ff;
|
||||
--td-brand-color-5: #3198e2;
|
||||
--td-brand-color-6: #007dc5;
|
||||
--td-brand-color-7: #00629c;
|
||||
--td-brand-color-8: #004a77;
|
||||
--td-brand-color-9: #003355;
|
||||
--td-brand-color-10: #00213a;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-4);
|
||||
--td-brand-color-active: var(--td-brand-color-5);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
:root[theme-mode='blue'] {
|
||||
--td-brand-color-1: #ecf4ff;
|
||||
--td-brand-color-2: #cde5ff;
|
||||
--td-brand-color-3: #9aceff;
|
||||
--td-brand-color-4: #57b4ff;
|
||||
--td-brand-color-5: #3198e2;
|
||||
--td-brand-color-6: #007dc5;
|
||||
--td-brand-color-7: #00629c;
|
||||
--td-brand-color-8: #004a77;
|
||||
--td-brand-color-9: #003355;
|
||||
--td-brand-color-10: #00213a;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-4);
|
||||
--td-brand-color-active: var(--td-brand-color-5);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1:
|
||||
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2:
|
||||
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
|
||||
0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3:
|
||||
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
|
||||
0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
:root[theme-mode="dark"] {
|
||||
--td-brand-color-1: #3198e220;
|
||||
--td-brand-color-2: #003355;
|
||||
--td-brand-color-3: #004a77;
|
||||
--td-brand-color-4: #00629c;
|
||||
--td-brand-color-5: #007dc5;
|
||||
--td-brand-color-6: #3198e2;
|
||||
--td-brand-color-7: #53b1fd;
|
||||
--td-brand-color-8: #9aceff;
|
||||
--td-brand-color-9: #cde5ff;
|
||||
--td-brand-color-10: #ecf4ff;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
:root[theme-mode='dark'] {
|
||||
--td-brand-color-1: #3198e220;
|
||||
--td-brand-color-2: #003355;
|
||||
--td-brand-color-3: #004a77;
|
||||
--td-brand-color-4: #00629c;
|
||||
--td-brand-color-5: #007dc5;
|
||||
--td-brand-color-6: #3198e2;
|
||||
--td-brand-color-7: #53b1fd;
|
||||
--td-brand-color-8: #9aceff;
|
||||
--td-brand-color-9: #cde5ff;
|
||||
--td-brand-color-10: #ecf4ff;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1:
|
||||
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2:
|
||||
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3:
|
||||
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
|
||||
var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
|
||||
var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
|
||||
var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
|
||||
var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
|
||||
var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
|
||||
var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
|
||||
var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
|
||||
var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
|
||||
var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) /
|
||||
var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
|
||||
@@ -1,355 +1,379 @@
|
||||
:root[theme-mode="cyan"] {
|
||||
--td-brand-color-1: #e3fcf8;
|
||||
--td-brand-color-2: #beefe9;
|
||||
--td-brand-color-3: #86dad1;
|
||||
--td-brand-color-4: #3ac2b8;
|
||||
--td-brand-color-5: #00a59b;
|
||||
--td-brand-color-6: #00877e;
|
||||
--td-brand-color-7: #006b64;
|
||||
--td-brand-color-8: #00524c;
|
||||
--td-brand-color-9: #003b36;
|
||||
--td-brand-color-10: #002724;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-4);
|
||||
--td-brand-color-active: var(--td-brand-color-5);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
:root[theme-mode='cyan'] {
|
||||
--td-brand-color-1: #e3fcf8;
|
||||
--td-brand-color-2: #beefe9;
|
||||
--td-brand-color-3: #86dad1;
|
||||
--td-brand-color-4: #3ac2b8;
|
||||
--td-brand-color-5: #00a59b;
|
||||
--td-brand-color-6: #00877e;
|
||||
--td-brand-color-7: #006b64;
|
||||
--td-brand-color-8: #00524c;
|
||||
--td-brand-color-9: #003b36;
|
||||
--td-brand-color-10: #002724;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-4);
|
||||
--td-brand-color-active: var(--td-brand-color-5);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1:
|
||||
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2:
|
||||
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
|
||||
0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3:
|
||||
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
|
||||
0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
:root[theme-mode="dark"] {
|
||||
--td-brand-color-1: #00a59b20;
|
||||
--td-brand-color-2: #003b36;
|
||||
--td-brand-color-3: #00524c;
|
||||
--td-brand-color-4: #006b64;
|
||||
--td-brand-color-5: #00877e;
|
||||
--td-brand-color-6: #00a59b;
|
||||
--td-brand-color-7: #0ed6ca;
|
||||
--td-brand-color-8: #86dad1;
|
||||
--td-brand-color-9: #beefe9;
|
||||
--td-brand-color-10: #e3fcf8;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
:root[theme-mode='dark'] {
|
||||
--td-brand-color-1: #00a59b20;
|
||||
--td-brand-color-2: #003b36;
|
||||
--td-brand-color-3: #00524c;
|
||||
--td-brand-color-4: #006b64;
|
||||
--td-brand-color-5: #00877e;
|
||||
--td-brand-color-6: #00a59b;
|
||||
--td-brand-color-7: #0ed6ca;
|
||||
--td-brand-color-8: #86dad1;
|
||||
--td-brand-color-9: #beefe9;
|
||||
--td-brand-color-10: #e3fcf8;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1:
|
||||
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2:
|
||||
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3:
|
||||
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
|
||||
var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
|
||||
var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
|
||||
var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
|
||||
var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
|
||||
var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
|
||||
var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
|
||||
var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
|
||||
var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
|
||||
var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) /
|
||||
var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
|
||||
@@ -1,355 +1,379 @@
|
||||
:root[theme-mode="orange"] {
|
||||
--td-brand-color-1: #fff1ea;
|
||||
--td-brand-color-2: #ffd9c5;
|
||||
--td-brand-color-3: #ffb991;
|
||||
--td-brand-color-4: #fb9458;
|
||||
--td-brand-color-5: #e47228;
|
||||
--td-brand-color-6: #c05708;
|
||||
--td-brand-color-7: #9a4200;
|
||||
--td-brand-color-8: #753000;
|
||||
--td-brand-color-9: #552100;
|
||||
--td-brand-color-10: #3d1600;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-4);
|
||||
--td-brand-color-active: var(--td-brand-color-5);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
:root[theme-mode='orange'] {
|
||||
--td-brand-color-1: #fff1ea;
|
||||
--td-brand-color-2: #ffd9c5;
|
||||
--td-brand-color-3: #ffb991;
|
||||
--td-brand-color-4: #fb9458;
|
||||
--td-brand-color-5: #e47228;
|
||||
--td-brand-color-6: #c05708;
|
||||
--td-brand-color-7: #9a4200;
|
||||
--td-brand-color-8: #753000;
|
||||
--td-brand-color-9: #552100;
|
||||
--td-brand-color-10: #3d1600;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-3);
|
||||
--td-brand-color: var(--td-brand-color-4);
|
||||
--td-brand-color-active: var(--td-brand-color-5);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1:
|
||||
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2:
|
||||
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
|
||||
0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3:
|
||||
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
|
||||
0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
:root[theme-mode="dark"] {
|
||||
--td-brand-color-1: #e4722820;
|
||||
--td-brand-color-2: #552100;
|
||||
--td-brand-color-3: #753000;
|
||||
--td-brand-color-4: #9a4200;
|
||||
--td-brand-color-5: #c05708;
|
||||
--td-brand-color-6: #e47228;
|
||||
--td-brand-color-7: #fd853a;
|
||||
--td-brand-color-8: #ffb991;
|
||||
--td-brand-color-9: #ffd9c5;
|
||||
--td-brand-color-10: #fff1ea;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
:root[theme-mode='dark'] {
|
||||
--td-brand-color-1: #e4722820;
|
||||
--td-brand-color-2: #552100;
|
||||
--td-brand-color-3: #753000;
|
||||
--td-brand-color-4: #9a4200;
|
||||
--td-brand-color-5: #c05708;
|
||||
--td-brand-color-6: #e47228;
|
||||
--td-brand-color-7: #fd853a;
|
||||
--td-brand-color-8: #ffb991;
|
||||
--td-brand-color-9: #ffd9c5;
|
||||
--td-brand-color-10: #fff1ea;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1:
|
||||
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2:
|
||||
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3:
|
||||
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
|
||||
var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
|
||||
var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
|
||||
var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
|
||||
var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
|
||||
var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
|
||||
var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
|
||||
var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
|
||||
var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
|
||||
var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) /
|
||||
var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
|
||||
@@ -1,355 +1,379 @@
|
||||
:root[theme-mode="pink"] {
|
||||
--td-brand-color-1: #fff0f1;
|
||||
--td-brand-color-2: #ffd8dd;
|
||||
--td-brand-color-3: #ffb7c1;
|
||||
--td-brand-color-4: #ff8fa2;
|
||||
--td-brand-color-5: #fc5e7e;
|
||||
--td-brand-color-6: #db3d62;
|
||||
--td-brand-color-7: #b2294b;
|
||||
--td-brand-color-8: #8d1135;
|
||||
--td-brand-color-9: #690021;
|
||||
--td-brand-color-10: #480014;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-4);
|
||||
--td-brand-color: var(--td-brand-color-5);
|
||||
--td-brand-color-active: var(--td-brand-color-6);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%), 0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%), 0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
:root[theme-mode='pink'] {
|
||||
--td-brand-color-1: #fff0f1;
|
||||
--td-brand-color-2: #ffd8dd;
|
||||
--td-brand-color-3: #ffb7c1;
|
||||
--td-brand-color-4: #ff8fa2;
|
||||
--td-brand-color-5: #fc5e7e;
|
||||
--td-brand-color-6: #db3d62;
|
||||
--td-brand-color-7: #b2294b;
|
||||
--td-brand-color-8: #8d1135;
|
||||
--td-brand-color-9: #690021;
|
||||
--td-brand-color-10: #480014;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-4);
|
||||
--td-brand-color: var(--td-brand-color-5);
|
||||
--td-brand-color-active: var(--td-brand-color-6);
|
||||
--td-warning-color-1: #fef3e6;
|
||||
--td-warning-color-2: #f9e0c7;
|
||||
--td-warning-color-3: #f7c797;
|
||||
--td-warning-color-4: #f2995f;
|
||||
--td-warning-color-5: #ed7b2f;
|
||||
--td-warning-color-6: #d35a21;
|
||||
--td-warning-color-7: #ba431b;
|
||||
--td-warning-color-8: #9e3610;
|
||||
--td-warning-color-9: #842b0b;
|
||||
--td-warning-color-10: #5a1907;
|
||||
--td-warning-color: var(--td-warning-color-5);
|
||||
--td-warning-color-hover: var(--td-warning-color-4);
|
||||
--td-warning-color-focus: var(--td-warning-color-2);
|
||||
--td-warning-color-active: var(--td-warning-color-6);
|
||||
--td-warning-color-disabled: var(--td-warning-color-3);
|
||||
--td-warning-color-light: var(--td-warning-color-1);
|
||||
--td-error-color-1: #fdecee;
|
||||
--td-error-color-2: #f9d7d9;
|
||||
--td-error-color-3: #f8b9be;
|
||||
--td-error-color-4: #f78d94;
|
||||
--td-error-color-5: #f36d78;
|
||||
--td-error-color-6: #e34d59;
|
||||
--td-error-color-7: #c9353f;
|
||||
--td-error-color-8: #b11f26;
|
||||
--td-error-color-9: #951114;
|
||||
--td-error-color-10: #680506;
|
||||
--td-error-color: var(--td-error-color-6);
|
||||
--td-error-color-hover: var(--td-error-color-5);
|
||||
--td-error-color-focus: var(--td-error-color-2);
|
||||
--td-error-color-active: var(--td-error-color-7);
|
||||
--td-error-color-disabled: var(--td-error-color-3);
|
||||
--td-error-color-light: var(--td-error-color-1);
|
||||
--td-success-color-1: #e8f8f2;
|
||||
--td-success-color-2: #bcebdc;
|
||||
--td-success-color-3: #85dbbe;
|
||||
--td-success-color-4: #48c79c;
|
||||
--td-success-color-5: #00a870;
|
||||
--td-success-color-6: #078d5c;
|
||||
--td-success-color-7: #067945;
|
||||
--td-success-color-8: #056334;
|
||||
--td-success-color-9: #044f2a;
|
||||
--td-success-color-10: #033017;
|
||||
--td-success-color: var(--td-success-color-5);
|
||||
--td-success-color-hover: var(--td-success-color-4);
|
||||
--td-success-color-focus: var(--td-success-color-2);
|
||||
--td-success-color-active: var(--td-success-color-6);
|
||||
--td-success-color-disabled: var(--td-success-color-3);
|
||||
--td-success-color-light: var(--td-success-color-1);
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-container: #fff;
|
||||
--td-bg-color-container-select: #fff;
|
||||
--td-bg-color-page: var(--td-gray-color-2);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-1);
|
||||
--td-bg-color-container-active: var(--td-gray-color-3);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-1);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4);
|
||||
--td-bg-color-component: var(--td-gray-color-3);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-4);
|
||||
--td-bg-color-component-active: var(--td-gray-color-6);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-2);
|
||||
--td-component-stroke: var(--td-gray-color-3);
|
||||
--td-component-border: var(--td-gray-color-4);
|
||||
--td-font-white-1: #ffffff;
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-gray-1);
|
||||
--td-text-color-secondary: var(--td-font-gray-2);
|
||||
--td-text-color-placeholder: var(--td-font-gray-3);
|
||||
--td-text-color-disabled: var(--td-font-gray-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-brand-color-light-hover: var(--td-brand-color-2);
|
||||
--td-warning-color-light-hover: var(--td-warning-color-2);
|
||||
--td-error-color-light-hover: var(--td-error-color-2);
|
||||
--td-success-color-light-hover: var(--td-success-color-2);
|
||||
--td-bg-color-secondarycomponent: var(--td-gray-color-4);
|
||||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5);
|
||||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6);
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 8%);
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 10%);
|
||||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%);
|
||||
--td-scroll-track-color: #fff;
|
||||
--td-bg-color-specialcomponent: #fff;
|
||||
--td-border-level-1-color: var(--td-gray-color-3);
|
||||
--td-border-level-2-color: var(--td-gray-color-4);
|
||||
--td-shadow-1:
|
||||
0 1px 10px rgba(0, 0, 0, 5%), 0 4px 5px rgba(0, 0, 0, 8%), 0 2px 4px -1px rgba(0, 0, 0, 12%);
|
||||
--td-shadow-2:
|
||||
0 3px 14px 2px rgba(0, 0, 0, 5%), 0 8px 10px 1px rgba(0, 0, 0, 6%),
|
||||
0 5px 5px -3px rgba(0, 0, 0, 10%);
|
||||
--td-shadow-3:
|
||||
0 6px 30px 5px rgba(0, 0, 0, 5%), 0 16px 24px 2px rgba(0, 0, 0, 4%),
|
||||
0 8px 10px -5px rgba(0, 0, 0, 8%);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc;
|
||||
--td-mask-active: rgba(0, 0, 0, 0.6);
|
||||
--td-mask-disabled: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
:root[theme-mode="dark"] {
|
||||
--td-brand-color-1: #ff547920;
|
||||
--td-brand-color-2: #690021;
|
||||
--td-brand-color-3: #8d1135;
|
||||
--td-brand-color-4: #b2294b;
|
||||
--td-brand-color-5: #db3d62;
|
||||
--td-brand-color-6: #ff5479;
|
||||
--td-brand-color-7: #ff8fa2;
|
||||
--td-brand-color-8: #ffb7c1;
|
||||
--td-brand-color-9: #ffd8dd;
|
||||
--td-brand-color-10: #fff0f1;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1: 0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2: 0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.10), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3: 0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.20);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
:root[theme-mode='dark'] {
|
||||
--td-brand-color-1: #ff547920;
|
||||
--td-brand-color-2: #690021;
|
||||
--td-brand-color-3: #8d1135;
|
||||
--td-brand-color-4: #b2294b;
|
||||
--td-brand-color-5: #db3d62;
|
||||
--td-brand-color-6: #ff5479;
|
||||
--td-brand-color-7: #ff8fa2;
|
||||
--td-brand-color-8: #ffb7c1;
|
||||
--td-brand-color-9: #ffd8dd;
|
||||
--td-brand-color-10: #fff0f1;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
--td-brand-color-hover: var(--td-brand-color-5);
|
||||
--td-brand-color: var(--td-brand-color-6);
|
||||
--td-brand-color-active: var(--td-brand-color-7);
|
||||
--td-warning-color-1: #4f2a1d;
|
||||
--td-warning-color-2: #582f21;
|
||||
--td-warning-color-3: #733c23;
|
||||
--td-warning-color-4: #a75d2b;
|
||||
--td-warning-color-5: #cf6e2d;
|
||||
--td-warning-color-6: #dc7633;
|
||||
--td-warning-color-7: #e8935c;
|
||||
--td-warning-color-8: #ecbf91;
|
||||
--td-warning-color-9: #eed7bf;
|
||||
--td-warning-color-10: #f3e9dc;
|
||||
--td-error-color-1: #472324;
|
||||
--td-error-color-2: #5e2a2d;
|
||||
--td-error-color-3: #703439;
|
||||
--td-error-color-4: #83383e;
|
||||
--td-error-color-5: #a03f46;
|
||||
--td-error-color-6: #c64751;
|
||||
--td-error-color-7: #de6670;
|
||||
--td-error-color-8: #ec888e;
|
||||
--td-error-color-9: #edb1b6;
|
||||
--td-error-color-10: #eeced0;
|
||||
--td-success-color-1: #193a2a;
|
||||
--td-success-color-2: #1a4230;
|
||||
--td-success-color-3: #17533d;
|
||||
--td-success-color-4: #0d7a55;
|
||||
--td-success-color-5: #059465;
|
||||
--td-success-color-6: #43af8a;
|
||||
--td-success-color-7: #46bf96;
|
||||
--td-success-color-8: #80d2b6;
|
||||
--td-success-color-9: #b4e1d3;
|
||||
--td-success-color-10: #deede8;
|
||||
--td-gray-color-1: #f3f3f3;
|
||||
--td-gray-color-2: #eee;
|
||||
--td-gray-color-3: #e8e8e8;
|
||||
--td-gray-color-4: #ddd;
|
||||
--td-gray-color-5: #c5c5c5;
|
||||
--td-gray-color-6: #a6a6a6;
|
||||
--td-gray-color-7: #8b8b8b;
|
||||
--td-gray-color-8: #777;
|
||||
--td-gray-color-9: #5e5e5e;
|
||||
--td-gray-color-10: #4b4b4b;
|
||||
--td-gray-color-11: #383838;
|
||||
--td-gray-color-12: #2c2c2c;
|
||||
--td-gray-color-13: #242424;
|
||||
--td-gray-color-14: #181818;
|
||||
--td-bg-color-page: var(--td-gray-color-14);
|
||||
--td-bg-color-container: var(--td-gray-color-13);
|
||||
--td-bg-color-container-hover: var(--td-gray-color-12);
|
||||
--td-bg-color-container-active: var(--td-gray-color-10);
|
||||
--td-bg-color-container-select: var(--td-gray-color-9);
|
||||
--td-bg-color-secondarycontainer: var(--td-gray-color-12);
|
||||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11);
|
||||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component: var(--td-gray-color-11);
|
||||
--td-bg-color-component-hover: var(--td-gray-color-10);
|
||||
--td-bg-color-component-active: var(--td-gray-color-9);
|
||||
--td-bg-color-component-disabled: var(--td-gray-color-12);
|
||||
--td-component-stroke: var(--td-gray-color-11);
|
||||
--td-component-border: var(--td-gray-color-9);
|
||||
--td-font-white-1: rgba(255, 255, 255, 0.9);
|
||||
--td-font-white-2: rgba(255, 255, 255, 0.55);
|
||||
--td-font-white-3: rgba(255, 255, 255, 0.35);
|
||||
--td-font-white-4: rgba(255, 255, 255, 0.22);
|
||||
--td-font-gray-1: rgba(0, 0, 0, 0.9);
|
||||
--td-font-gray-2: rgba(0, 0, 0, 0.6);
|
||||
--td-font-gray-3: rgba(0, 0, 0, 0.4);
|
||||
--td-font-gray-4: rgba(0, 0, 0, 0.26);
|
||||
--td-text-color-primary: var(--td-font-white-1);
|
||||
--td-text-color-secondary: var(--td-font-white-2);
|
||||
--td-text-color-placeholder: var(--td-font-white-3);
|
||||
--td-text-color-disabled: var(--td-font-white-4);
|
||||
--td-text-color-anti: #fff;
|
||||
--td-text-color-brand: var(--td-brand-color);
|
||||
--td-text-color-link: var(--td-brand-color);
|
||||
--td-shadow-1:
|
||||
0 4px 6px rgba(0, 0, 0, 0.06), 0 1px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2:
|
||||
0 8px 10px rgba(0, 0, 0, 0.12), 0 3px 14px rgba(0, 0, 0, 0.1), 0 5px 5px rgba(0, 0, 0, 0.16);
|
||||
--td-shadow-3:
|
||||
0 16px 24px rgba(0, 0, 0, 0.14), 0 6px 30px rgba(0, 0, 0, 0.12), 0 8px 10px rgba(0, 0, 0, 0.2);
|
||||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e;
|
||||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e;
|
||||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e;
|
||||
--td-table-shadow-color: rgba(0, 0, 0, 55%);
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 10%);
|
||||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%);
|
||||
--td-scroll-track-color: #333;
|
||||
--td-bg-color-specialcomponent: transparent;
|
||||
--td-border-level-1-color: var(--td-gray-color-11);
|
||||
--td-border-level-2-color: var(--td-gray-color-9);
|
||||
--td-mask-active: rgba(0, 0, 0, 0.4);
|
||||
--td-mask-disabled: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
--td-font-family: pingfang sc, microsoft yahei, arial regular;
|
||||
--td-font-family-medium: pingfang sc, microsoft yahei, arial medium;
|
||||
--td-font-size-link-small: 12px;
|
||||
--td-font-size-link-medium: 14px;
|
||||
--td-font-size-link-large: 16px;
|
||||
--td-font-size-mark-small: 12px;
|
||||
--td-font-size-mark-medium: 14px;
|
||||
--td-font-size-body-small: 12px;
|
||||
--td-font-size-body-medium: 14px;
|
||||
--td-font-size-body-large: 16px;
|
||||
--td-font-size-title-small: 14px;
|
||||
--td-font-size-title-medium: 16px;
|
||||
--td-font-size-title-large: 20px;
|
||||
--td-font-size-headline-small: 24px;
|
||||
--td-font-size-headline-medium: 28px;
|
||||
--td-font-size-headline-large: 36px;
|
||||
--td-font-size-display-medium: 48px;
|
||||
--td-font-size-display-large: 64px;
|
||||
--td-line-height-link-small: 20px;
|
||||
--td-line-height-link-medium: 22px;
|
||||
--td-line-height-link-large: 24px;
|
||||
--td-line-height-mark-small: 20px;
|
||||
--td-line-height-mark-medium: 22px;
|
||||
--td-line-height-body-small: 20px;
|
||||
--td-line-height-body-medium: 22px;
|
||||
--td-line-height-body-large: 24px;
|
||||
--td-line-height-title-small: 22px;
|
||||
--td-line-height-title-medium: 24px;
|
||||
--td-line-height-title-large: 28px;
|
||||
--td-line-height-headline-small: 32px;
|
||||
--td-line-height-headline-medium: 36px;
|
||||
--td-line-height-headline-large: 44px;
|
||||
--td-line-height-display-medium: 56px;
|
||||
--td-line-height-display-large: 72px;
|
||||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small)
|
||||
var(--td-font-family);
|
||||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small)
|
||||
var(--td-font-family);
|
||||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small)
|
||||
var(--td-font-family);
|
||||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large)
|
||||
var(--td-font-family);
|
||||
--td-font-title-small: 600 var(--td-font-size-title-small) / var(--td-line-height-title-small)
|
||||
var(--td-font-family);
|
||||
--td-font-title-medium: 600 var(--td-font-size-title-medium) / var(--td-line-height-title-medium)
|
||||
var(--td-font-family);
|
||||
--td-font-title-large: 600 var(--td-font-size-title-large) / var(--td-line-height-title-large)
|
||||
var(--td-font-family);
|
||||
--td-font-headline-small: 600 var(--td-font-size-headline-small) /
|
||||
var(--td-line-height-headline-small) var(--td-font-family);
|
||||
--td-font-headline-medium: 600 var(--td-font-size-headline-medium) /
|
||||
var(--td-line-height-headline-medium) var(--td-font-family);
|
||||
--td-font-headline-large: 600 var(--td-font-size-headline-large) /
|
||||
var(--td-line-height-headline-large) var(--td-font-family);
|
||||
--td-font-display-medium: 600 var(--td-font-size-display-medium) /
|
||||
var(--td-line-height-display-medium) var(--td-font-family);
|
||||
--td-font-display-large: 600 var(--td-font-size-display-large) /
|
||||
var(--td-line-height-display-large) var(--td-font-family);
|
||||
--td-radius-small: 2px;
|
||||
--td-radius-default: 3px;
|
||||
--td-radius-medium: 6px;
|
||||
--td-radius-large: 9px;
|
||||
--td-radius-extraLarge: 12px;
|
||||
--td-radius-round: 999px;
|
||||
--td-radius-circle: 50%;
|
||||
--td-size-1: 2px;
|
||||
--td-size-2: 4px;
|
||||
--td-size-3: 6px;
|
||||
--td-size-4: 8px;
|
||||
--td-size-5: 12px;
|
||||
--td-size-6: 16px;
|
||||
--td-size-7: 20px;
|
||||
--td-size-8: 24px;
|
||||
--td-size-9: 28px;
|
||||
--td-size-10: 32px;
|
||||
--td-size-11: 36px;
|
||||
--td-size-12: 40px;
|
||||
--td-size-13: 48px;
|
||||
--td-size-14: 56px;
|
||||
--td-size-15: 64px;
|
||||
--td-size-16: 72px;
|
||||
--td-comp-size-xxxs: var(--td-size-6);
|
||||
--td-comp-size-xxs: var(--td-size-7);
|
||||
--td-comp-size-xs: var(--td-size-8);
|
||||
--td-comp-size-s: var(--td-size-9);
|
||||
--td-comp-size-m: var(--td-size-10);
|
||||
--td-comp-size-l: var(--td-size-11);
|
||||
--td-comp-size-xl: var(--td-size-12);
|
||||
--td-comp-size-xxl: var(--td-size-13);
|
||||
--td-comp-size-xxxl: var(--td-size-14);
|
||||
--td-comp-size-xxxxl: var(--td-size-15);
|
||||
--td-comp-size-xxxxxl: var(--td-size-16);
|
||||
--td-pop-padding-s: var(--td-size-2);
|
||||
--td-pop-padding-m: var(--td-size-3);
|
||||
--td-pop-padding-l: var(--td-size-4);
|
||||
--td-pop-padding-xl: var(--td-size-5);
|
||||
--td-pop-padding-xxl: var(--td-size-6);
|
||||
--td-comp-paddingLR-xxs: var(--td-size-1);
|
||||
--td-comp-paddingLR-xs: var(--td-size-2);
|
||||
--td-comp-paddingLR-s: var(--td-size-4);
|
||||
--td-comp-paddingLR-m: var(--td-size-5);
|
||||
--td-comp-paddingLR-l: var(--td-size-6);
|
||||
--td-comp-paddingLR-xl: var(--td-size-8);
|
||||
--td-comp-paddingLR-xxl: var(--td-size-10);
|
||||
--td-comp-paddingTB-xxs: var(--td-size-1);
|
||||
--td-comp-paddingTB-xs: var(--td-size-2);
|
||||
--td-comp-paddingTB-s: var(--td-size-4);
|
||||
--td-comp-paddingTB-m: var(--td-size-5);
|
||||
--td-comp-paddingTB-l: var(--td-size-6);
|
||||
--td-comp-paddingTB-xl: var(--td-size-8);
|
||||
--td-comp-paddingTB-xxl: var(--td-size-10);
|
||||
--td-comp-margin-xxs: var(--td-size-1);
|
||||
--td-comp-margin-xs: var(--td-size-2);
|
||||
--td-comp-margin-s: var(--td-size-4);
|
||||
--td-comp-margin-m: var(--td-size-5);
|
||||
--td-comp-margin-l: var(--td-size-6);
|
||||
--td-comp-margin-xl: var(--td-size-7);
|
||||
--td-comp-margin-xxl: var(--td-size-8);
|
||||
--td-comp-margin-xxxl: var(--td-size-10);
|
||||
--td-comp-margin-xxxxl: var(--td-size-12);
|
||||
}
|
||||
|
||||
@@ -393,13 +393,13 @@ const handleResize = () => {
|
||||
const maxX = windowSize.value.width - 120
|
||||
const maxY = windowSize.value.height - 176
|
||||
const minY = 90 // 顶部边界限制
|
||||
|
||||
|
||||
// 如果悬浮球在右侧,随窗口宽度变化更新位置
|
||||
if (!isOnLeft.value) {
|
||||
// 重新计算右侧位置
|
||||
ballPosition.value.x = windowSize.value.width - 106
|
||||
}
|
||||
|
||||
|
||||
// 确保位置在有效范围内
|
||||
ballPosition.value.x = Math.max(0, Math.min(ballPosition.value.x, maxX))
|
||||
ballPosition.value.y = Math.max(minY, Math.min(ballPosition.value.y, maxY))
|
||||
|
||||
@@ -128,7 +128,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showDuration: true
|
||||
})
|
||||
|
||||
const emit = defineEmits(['play', 'pause', 'addToPlaylist', 'download','scroll'])
|
||||
const emit = defineEmits(['play', 'pause', 'addToPlaylist', 'download', 'scroll'])
|
||||
|
||||
// 虚拟滚动相关状态
|
||||
const scrollContainer = ref<HTMLElement>()
|
||||
|
||||
328
src/renderer/src/components/Play/AudioVisualizer.vue
Normal file
328
src/renderer/src/components/Play/AudioVisualizer.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import { ControlAudioStore } from '@renderer/store/ControlAudio'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import audioManager from '@renderer/utils/audioManager'
|
||||
|
||||
interface Props {
|
||||
show?: boolean
|
||||
height?: number
|
||||
barCount?: number
|
||||
color?: string
|
||||
backgroundColor?: string
|
||||
}
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
lowFreqUpdate: [volume: number]
|
||||
}>()
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
show: true,
|
||||
height: 80,
|
||||
barCount: 64,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
backgroundColor: 'transparent'
|
||||
})
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const animationId = ref<number>()
|
||||
const analyser = ref<AnalyserNode>()
|
||||
const dataArray = ref<Uint8Array>()
|
||||
const resizeObserver = ref<ResizeObserver>()
|
||||
const componentId = ref<string>(`visualizer-${Date.now()}-${Math.random()}`)
|
||||
|
||||
const controlAudio = ControlAudioStore()
|
||||
const { Audio } = storeToRefs(controlAudio)
|
||||
|
||||
// 初始化音频分析器
|
||||
const initAudioAnalyser = () => {
|
||||
if (!Audio.value.audio) return
|
||||
|
||||
try {
|
||||
// 计算所需的 fftSize - 必须是 2 的幂次方
|
||||
const minSize = props.barCount * 2
|
||||
let fftSize = 32
|
||||
while (fftSize < minSize) {
|
||||
fftSize *= 2
|
||||
}
|
||||
fftSize = Math.min(fftSize, 2048) // 限制最大值
|
||||
|
||||
// 使用音频管理器创建分析器
|
||||
const createdAnalyser = audioManager.createAnalyser(
|
||||
Audio.value.audio,
|
||||
componentId.value,
|
||||
fftSize
|
||||
)
|
||||
analyser.value = createdAnalyser || undefined
|
||||
|
||||
if (analyser.value) {
|
||||
// 创建数据数组,明确指定 ArrayBuffer 类型
|
||||
const bufferLength = analyser.value.frequencyBinCount
|
||||
dataArray.value = new Uint8Array(new ArrayBuffer(bufferLength))
|
||||
console.log('音频分析器初始化成功')
|
||||
} else {
|
||||
console.warn('无法创建音频分析器,使用模拟数据')
|
||||
// 创建一个默认大小的数据数组用于模拟数据
|
||||
const bufferLength = fftSize / 2
|
||||
dataArray.value = new Uint8Array(new ArrayBuffer(bufferLength))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('音频分析器初始化失败:', error)
|
||||
// 创建一个默认大小的数据数组用于模拟数据
|
||||
dataArray.value = new Uint8Array(new ArrayBuffer(256))
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制可视化
|
||||
const draw = () => {
|
||||
if (!canvasRef.value || !analyser.value || !dataArray.value) return
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// 获取频域数据或生成模拟数据
|
||||
if (analyser.value && dataArray.value) {
|
||||
// 有真实音频分析器,获取真实数据
|
||||
analyser.value.getByteFrequencyData(dataArray.value as Uint8Array<ArrayBuffer>)
|
||||
} else {
|
||||
// 没有音频分析器,生成模拟数据
|
||||
const time = Date.now() * 0.001
|
||||
for (let i = 0; i < dataArray.value.length; i++) {
|
||||
// 生成基于时间的模拟频谱数据
|
||||
const frequency = i / dataArray.value.length
|
||||
const amplitude = Math.sin(time * 2 + frequency * 10) * 0.5 + 0.5
|
||||
const bass = Math.sin(time * 4) * 0.3 + 0.7 // 低频变化
|
||||
dataArray.value[i] = Math.floor(amplitude * bass * 255 * (1 - frequency * 0.7))
|
||||
}
|
||||
}
|
||||
|
||||
// 计算低频音量 (80hz-120hz 范围)
|
||||
// 假设采样率为 44100Hz,fftSize 为 256,则每个频率 bin 约为 172Hz
|
||||
// 80-120Hz 大约对应前 1-2 个 bin
|
||||
const lowFreqStart = 0
|
||||
const lowFreqEnd = Math.min(3, dataArray.value.length) // 取前几个低频 bin
|
||||
let lowFreqSum = 0
|
||||
for (let i = lowFreqStart; i < lowFreqEnd; i++) {
|
||||
lowFreqSum += dataArray.value[i]
|
||||
}
|
||||
const lowFreqVolume = lowFreqSum / (lowFreqEnd - lowFreqStart) / 255
|
||||
|
||||
// 发送低频音量给父组件
|
||||
emit('lowFreqUpdate', lowFreqVolume)
|
||||
|
||||
// 完全清空画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 如果有背景色,再填充背景
|
||||
if (props.backgroundColor !== 'transparent') {
|
||||
ctx.fillStyle = props.backgroundColor
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
// 使用容器的实际尺寸进行计算,因为 ctx 已经缩放过了
|
||||
const container = canvas.parentElement
|
||||
if (!container) return
|
||||
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const canvasWidth = containerRect.width
|
||||
const canvasHeight = props.height
|
||||
|
||||
// 计算对称柱状图参数
|
||||
const halfBarCount = Math.floor(props.barCount / 2)
|
||||
const barWidth = canvasWidth / 2 / halfBarCount
|
||||
const maxBarHeight = canvasHeight * 0.9
|
||||
const centerX = canvasWidth / 2
|
||||
|
||||
// 绘制左右对称的频谱柱状图
|
||||
for (let i = 0; i < halfBarCount; i++) {
|
||||
// 增强低频响应,让可视化更敏感
|
||||
let barHeight = (dataArray.value[i] / 255) * maxBarHeight
|
||||
|
||||
// 对数据进行增强处理,让变化更明显
|
||||
barHeight = Math.pow(barHeight / maxBarHeight, 0.6) * maxBarHeight
|
||||
|
||||
const y = canvasHeight - barHeight
|
||||
|
||||
// 创建渐变色
|
||||
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, y)
|
||||
gradient.addColorStop(0, props.color)
|
||||
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
|
||||
|
||||
ctx.fillStyle = gradient
|
||||
|
||||
// 绘制左侧柱状图(从中心向左)
|
||||
const leftX = centerX - (i + 1) * barWidth
|
||||
ctx.fillRect(leftX, y, barWidth, barHeight)
|
||||
|
||||
// 绘制右侧柱状图(从中心向右)
|
||||
const rightX = centerX + i * barWidth
|
||||
ctx.fillRect(rightX, y, barWidth, barHeight)
|
||||
}
|
||||
|
||||
// 继续动画
|
||||
if (props.show && Audio.value.isPlay) {
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始可视化
|
||||
const startVisualization = () => {
|
||||
if (!props.show || !Audio.value.isPlay) return
|
||||
|
||||
if (!analyser.value) {
|
||||
initAudioAnalyser()
|
||||
}
|
||||
|
||||
draw()
|
||||
}
|
||||
|
||||
// 停止可视化
|
||||
const stopVisualization = () => {
|
||||
try {
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value)
|
||||
animationId.value = undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('停止动画帧时出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听播放状态变化
|
||||
watch(
|
||||
() => Audio.value.isPlay,
|
||||
(isPlaying) => {
|
||||
if (isPlaying && props.show) {
|
||||
startVisualization()
|
||||
} else {
|
||||
stopVisualization()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听显示状态变化
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show && Audio.value.isPlay) {
|
||||
startVisualization()
|
||||
} else {
|
||||
stopVisualization()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 设置画布尺寸的函数
|
||||
const resizeCanvas = () => {
|
||||
if (!canvasRef.value) return
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const container = canvas.parentElement
|
||||
if (!container) return
|
||||
|
||||
// 获取容器的实际尺寸
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
|
||||
// 设置 canvas 的实际尺寸
|
||||
canvas.width = containerRect.width * dpr
|
||||
canvas.height = props.height * dpr
|
||||
|
||||
// 设置 CSS 尺寸
|
||||
canvas.style.width = containerRect.width + 'px'
|
||||
canvas.style.height = props.height + 'px'
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
// 重置变换矩阵并重新缩放
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
||||
ctx.scale(dpr, dpr)
|
||||
}
|
||||
|
||||
console.log('Canvas resized:', containerRect.width, 'x', props.height)
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
if (canvasRef.value) {
|
||||
resizeCanvas()
|
||||
|
||||
// 使用 ResizeObserver 监听容器尺寸变化
|
||||
resizeObserver.value = new ResizeObserver((entries) => {
|
||||
for (const _entry of entries) {
|
||||
// 使用 nextTick 确保 DOM 更新完成
|
||||
nextTick(() => {
|
||||
resizeCanvas()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 观察 canvas 元素的父容器
|
||||
const container = canvasRef.value.parentElement
|
||||
if (container) {
|
||||
resizeObserver.value.observe(container)
|
||||
}
|
||||
}
|
||||
|
||||
if (Audio.value.audio && props.show && Audio.value.isPlay) {
|
||||
initAudioAnalyser()
|
||||
startVisualization()
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onBeforeUnmount(() => {
|
||||
console.log('AudioVisualizer 组件开始卸载')
|
||||
|
||||
// 停止可视化动画
|
||||
stopVisualization()
|
||||
|
||||
// 清理音频上下文和相关资源
|
||||
try {
|
||||
// 只断开分析器连接,不断开共享的音频源
|
||||
if (analyser.value) {
|
||||
analyser.value.disconnect()
|
||||
analyser.value = undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('清理音频资源时出错:', error)
|
||||
}
|
||||
|
||||
// 断开 ResizeObserver
|
||||
try {
|
||||
if (resizeObserver.value) {
|
||||
resizeObserver.value.disconnect()
|
||||
resizeObserver.value = undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('断开 ResizeObserver 时出错:', error)
|
||||
}
|
||||
|
||||
// 清理数据数组
|
||||
dataArray.value = undefined
|
||||
|
||||
console.log('AudioVisualizer 组件卸载完成')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="audio-visualizer" :style="{ height: `${height}px` }">
|
||||
<canvas ref="canvasRef" class="visualizer-canvas" :style="{ height: `${height}px` }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.audio-visualizer {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.visualizer-canvas {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- eslint-disable vue/require-toggle-inside-transition -->
|
||||
<script lang="ts" setup>
|
||||
import TitleBarControls from '../TitleBarControls.vue'
|
||||
import AudioVisualizer from './AudioVisualizer.vue'
|
||||
// import ShaderBackground from './ShaderBackground.vue'
|
||||
import {
|
||||
BackgroundRender,
|
||||
@@ -23,13 +24,15 @@ interface Props {
|
||||
show?: boolean
|
||||
coverImage?: string
|
||||
songId?: string | null
|
||||
songInfo: SongList | { songmid: number | null }
|
||||
songInfo: SongList | { songmid: number | null | string }
|
||||
mainColor: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
show: false,
|
||||
coverImage: '@assets/images/Default.jpg',
|
||||
songId: ''
|
||||
songId: '',
|
||||
mainColor: '#fff'
|
||||
})
|
||||
// 定义事件
|
||||
const emit = defineEmits(['toggle-fullscreen'])
|
||||
@@ -76,7 +79,8 @@ const state = reactive({
|
||||
albumUrl: props.coverImage,
|
||||
albumIsVideo: false,
|
||||
currentTime: 0,
|
||||
lyricLines: [] as LyricLine[]
|
||||
lyricLines: [] as LyricLine[],
|
||||
lowFreqVolume: 1.0
|
||||
})
|
||||
|
||||
// 监听歌曲ID变化,获取歌词
|
||||
@@ -242,6 +246,11 @@ watch(
|
||||
state.currentTime = Math.round(newTime * 1000)
|
||||
}
|
||||
)
|
||||
|
||||
// 处理低频音量更新
|
||||
const handleLowFreqUpdate = (volume: number) => {
|
||||
state.lowFreqVolume = volume
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -253,7 +262,6 @@ watch(
|
||||
:album-is-video="false"
|
||||
:fps="30"
|
||||
:flow-speed="4"
|
||||
:low-freq-volume="1"
|
||||
:has-lyric="state.lyricLines.length > 10"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1"
|
||||
/>
|
||||
@@ -282,14 +290,10 @@ watch(
|
||||
/>
|
||||
</Transition>
|
||||
<div class="playbox">
|
||||
<!-- 播放控件内容
|
||||
<div v-if="props.show" class="song-info">
|
||||
<h1>当前播放</h1>
|
||||
<p>这里将显示歌曲信息</p>
|
||||
</div> -->
|
||||
<div class="left" :style="state.lyricLines.length <= 0 && 'width:100vw'">
|
||||
<div
|
||||
class="box"
|
||||
class="cd-container"
|
||||
:class="{ playing: Audio.isPlay }"
|
||||
:style="
|
||||
!Audio.isPlay
|
||||
? 'animation-play-state: paused;'
|
||||
@@ -299,16 +303,15 @@ watch(
|
||||
: '')
|
||||
"
|
||||
>
|
||||
<t-image
|
||||
:src="coverImage"
|
||||
:style="
|
||||
state.lyricLines.length > 0
|
||||
? 'width: min(20vw, 380px); height: min(20vw, 380px)'
|
||||
: 'width: 45vh; height: 45vh;transition: width 0.3s ease, height 0.3s ease; transition-delay: 1s;'
|
||||
"
|
||||
shape="circle"
|
||||
class="cover"
|
||||
/>
|
||||
<!-- 黑胶唱片 -->
|
||||
<div class="vinyl-record"></div>
|
||||
<!-- 唱片标签 -->
|
||||
<div class="vinyl-label">
|
||||
<t-image :src="coverImage" shape="circle" class="cover" />
|
||||
<div class="label-shine"></div>
|
||||
</div>
|
||||
<!-- 中心孔 -->
|
||||
<div class="center-hole"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="state.lyricLines.length > 0" class="right">
|
||||
@@ -316,8 +319,6 @@ watch(
|
||||
ref="lyricPlayerRef"
|
||||
:lyric-lines="props.show ? state.lyricLines : []"
|
||||
:current-time="state.currentTime"
|
||||
:align-position="0.38"
|
||||
style="mix-blend-mode: plus-lighter"
|
||||
class="lyric-player"
|
||||
@line-click="
|
||||
(e) => {
|
||||
@@ -329,6 +330,16 @@ watch(
|
||||
</LyricPlayer>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 音频可视化组件 -->
|
||||
<div class="audio-visualizer-container" v-if="props.show && coverImage">
|
||||
<AudioVisualizer
|
||||
:show="props.show && Audio.isPlay"
|
||||
:height="70"
|
||||
:bar-count="80"
|
||||
:color="mainColor"
|
||||
@low-freq-update="handleLowFreqUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -429,17 +440,14 @@ watch(
|
||||
.playbox {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// background-color: rgba(0, 0, 0, 0.256);
|
||||
background-color: rgba(0, 0, 0, 0.256);
|
||||
drop-filter: blur(10px);
|
||||
padding: 0 10vw;
|
||||
-webkit-drop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
:deep(.lyric-player) {
|
||||
--amll-lyric-player-font-size: max(2vw, 29px);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: 40%;
|
||||
transition: all 0.3s ease;
|
||||
@@ -454,33 +462,246 @@ watch(
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 0 var(--play-bottom-height) 0;
|
||||
perspective: 1000px;
|
||||
|
||||
.box {
|
||||
.cd-container {
|
||||
width: min(30vw, 700px);
|
||||
height: min(30vw, 700px);
|
||||
background-color: #000000;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 1000%;
|
||||
animation: rotateRecord 10s linear infinite;
|
||||
animation: rotateRecord 33s linear infinite;
|
||||
transition: filter 0.3s ease;
|
||||
filter: drop-shadow(0 15px 35px rgba(0, 0, 0, 0.6));
|
||||
|
||||
:deep(.cover) img {
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
&:hover {
|
||||
filter: drop-shadow(0 20px 45px rgba(0, 0, 0, 0.7));
|
||||
}
|
||||
|
||||
/* 黑胶唱片主体 */
|
||||
.vinyl-record {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 50% 50%, #1a1a1a 0%, #0d0d0d 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
/* 唱片纹理轨道 */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
repeating-conic-gradient(
|
||||
from 0deg,
|
||||
transparent 0deg,
|
||||
rgba(255, 255, 255, 0.02) 0.5deg,
|
||||
transparent 1deg,
|
||||
rgba(255, 255, 255, 0.01) 1.5deg,
|
||||
transparent 2deg
|
||||
),
|
||||
repeating-radial-gradient(
|
||||
circle at 50% 50%,
|
||||
transparent 0px,
|
||||
rgba(255, 255, 255, 0.03) 1px,
|
||||
transparent 2px,
|
||||
transparent 8px
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 唱片光泽效果 */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
ellipse at 30% 30%,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.04) 25%,
|
||||
rgba(255, 255, 255, 0.02) 50%,
|
||||
rgba(255, 255, 255, 0.01) 75%,
|
||||
transparent 100%
|
||||
);
|
||||
border-radius: 50%;
|
||||
z-index: 2;
|
||||
animation: vinylShine 6s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* 唱片标签区域 */
|
||||
.vinyl-label {
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
background: radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(40, 40, 40, 0.95) 0%,
|
||||
rgba(25, 25, 25, 0.98) 70%,
|
||||
rgba(15, 15, 15, 1) 100%
|
||||
);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 3;
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(0, 0, 0, 0.8),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
0 0 10px rgba(0, 0, 0, 0.5);
|
||||
|
||||
:deep(.cover) {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 20px rgba(0, 0, 0, 0.4),
|
||||
inset 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
width: 95% !important;
|
||||
height: 95% !important;
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
border-radius: 50%;
|
||||
filter: brightness(0.85) contrast(1.15) saturate(1.1);
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签光泽 */
|
||||
.label-shine {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
ellipse at 25% 25%,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
border-radius: 50%;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
animation: labelShine 8s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中心孔 */
|
||||
.center-hole {
|
||||
position: absolute;
|
||||
width: 8%;
|
||||
height: 8%;
|
||||
background: radial-gradient(circle, #000 0%, #111 30%, #222 70%, #333 100%);
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
box-shadow:
|
||||
inset 0 0 8px rgba(0, 0, 0, 0.9),
|
||||
0 0 3px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
:deep(.lyric-player) {
|
||||
font-family: lyricfont;
|
||||
--amll-lyric-player-font-size: min(2.6vw, 32px);
|
||||
|
||||
// bottom: max(2vw, 29px);
|
||||
|
||||
height: 200%;
|
||||
transform: translateY(-25%);
|
||||
& > div {
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
padding: 0 20px;
|
||||
padding-top: 90px;
|
||||
|
||||
margin: 80px 0 calc(var(--play-bottom-height)) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-visualizer-container {
|
||||
position: absolute;
|
||||
bottom: calc(var(--play-bottom-height) - 10px);
|
||||
z-index: 9999;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
// background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
|
||||
filter: blur(6px);
|
||||
// max-width: 1000px;
|
||||
// -webkit-backdrop-filter: blur(10px);
|
||||
// border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 5;
|
||||
// padding: 0 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotateRecord {
|
||||
to {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vinylShine {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
transform: rotate(180deg) scale(1.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(360deg) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes labelShine {
|
||||
0% {
|
||||
opacity: 0.05;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.1;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
75% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.05;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
nextTick,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
toRaw
|
||||
toRaw,
|
||||
provide
|
||||
} from 'vue'
|
||||
import { ControlAudioStore } from '@renderer/store/ControlAudio'
|
||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
destroyPlaylistEventListeners,
|
||||
getSongRealUrl
|
||||
} from '@renderer/utils/playlistManager'
|
||||
import defaultCoverImg from '/default-cover.png'
|
||||
|
||||
const controlAudio = ControlAudioStore()
|
||||
const localUserStore = LocalUserDetailStore()
|
||||
@@ -34,7 +36,7 @@ const { setCurrentTime, start, stop, setVolume, setUrl } = controlAudio
|
||||
const showFullPlay = ref(false)
|
||||
document.addEventListener('keydown', KeyEvent)
|
||||
// 处理最小化右键的事件
|
||||
window.api.onMusicCtrl(() => {
|
||||
const removeMusicCtrlListener = window.api.onMusicCtrl(() => {
|
||||
togglePlayPause()
|
||||
})
|
||||
let timer: any = null
|
||||
@@ -118,7 +120,7 @@ const waitForAudioReady = (): Promise<void> => {
|
||||
|
||||
// 存储待恢复的播放位置
|
||||
let pendingRestorePosition = 0
|
||||
let pendingRestoreSongId: number | null = null
|
||||
let pendingRestoreSongId: number | string | null = null
|
||||
|
||||
// 记录组件被停用前的播放状态
|
||||
let wasPlaying = false
|
||||
@@ -226,7 +228,7 @@ const playSong = async (song: SongList) => {
|
||||
MessagePlugin.error('播放失败,原因:' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
provide('PlaySong', playSong)
|
||||
// 歌曲信息
|
||||
// const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
|
||||
const playMode = ref(PlayMode.SEQUENCE)
|
||||
@@ -377,13 +379,10 @@ const playNext = async () => {
|
||||
if (Audio.value.audio) {
|
||||
Audio.value.audio.currentTime = 0
|
||||
}
|
||||
|
||||
// 如果当前正在播放,继续播放;如果暂停,保持暂停
|
||||
if (Audio.value.isPlay) {
|
||||
const startResult = start()
|
||||
if (startResult && typeof startResult.then === 'function') {
|
||||
await startResult
|
||||
}
|
||||
const startResult = start()
|
||||
if (startResult && typeof startResult.then === 'function') {
|
||||
await startResult
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -422,6 +421,7 @@ onMounted(async () => {
|
||||
// 监听音频结束事件,根据播放模式播放下一首
|
||||
unEnded = controlAudio.subscribe('ended', () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
console.log('播放结束')
|
||||
playNext()
|
||||
})
|
||||
})
|
||||
@@ -473,6 +473,9 @@ onUnmounted(() => {
|
||||
if (savePositionInterval !== null) {
|
||||
clearInterval(savePositionInterval)
|
||||
}
|
||||
if (removeMusicCtrlListener) {
|
||||
removeMusicCtrlListener()
|
||||
}
|
||||
unEnded()
|
||||
})
|
||||
|
||||
@@ -668,13 +671,13 @@ const handleProgressDragStart = (event: MouseEvent) => {
|
||||
}
|
||||
|
||||
// 歌曲信息
|
||||
const songInfo = ref<Omit<SongList, 'songmid'> & { songmid: null | number }>({
|
||||
const songInfo = ref<Omit<SongList, 'songmid'> & { songmid: null | number | string }>({
|
||||
songmid: null,
|
||||
hash: '',
|
||||
name: '欢迎使用CeruMusic 🎉',
|
||||
singer: '可以配置音源插件来播放你的歌曲',
|
||||
albumName: '',
|
||||
albumId: 0,
|
||||
albumId: '0',
|
||||
source: '',
|
||||
interval: '00:00',
|
||||
img: '',
|
||||
@@ -700,20 +703,41 @@ async function setColor() {
|
||||
playbg.value = 'rgba(255,255,255,0.2)'
|
||||
playbghover.value = 'rgba(255,255,255,0.33)'
|
||||
}
|
||||
const bg = ref('#ffffff46')
|
||||
|
||||
watch(
|
||||
songInfo,
|
||||
async (newVal) => {
|
||||
bg.value = bg.value === '#ffffff' ? '#ffffff46' : toRaw(bg.value)
|
||||
if (newVal.img) {
|
||||
await setColor()
|
||||
} else if (songInfo.value.songmid) {
|
||||
songInfo.value.img = defaultCoverImg
|
||||
await setColor()
|
||||
} else {
|
||||
bg.value = '#ffffff'
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
watch(showFullPlay, (val) => {
|
||||
if (val) {
|
||||
console.log('背景hei')
|
||||
bg.value = '#00000020'
|
||||
} else {
|
||||
bg.value = '#ffffff46'
|
||||
}
|
||||
})
|
||||
// onMounted(setColor)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="player-container" @click.stop="toggleFullPlay">
|
||||
<div
|
||||
class="player-container"
|
||||
:style="!showFullPlay && 'box-shadow: none'"
|
||||
@click.stop="toggleFullPlay"
|
||||
>
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-bar-container">
|
||||
<div
|
||||
@@ -731,8 +755,9 @@ watch(
|
||||
<div class="player-content">
|
||||
<!-- 左侧:封面和歌曲信息 -->
|
||||
<div class="left-section">
|
||||
<div class="album-cover" v-show="songInfo.img">
|
||||
<img :src="songInfo.img" alt="专辑封面" />
|
||||
<div class="album-cover" v-if="songInfo.albumId">
|
||||
<img :src="songInfo.img" alt="专辑封面" v-if="songInfo.img" />
|
||||
<img :src="defaultCoverImg" alt="默认封面" />
|
||||
</div>
|
||||
|
||||
<div class="song-info">
|
||||
@@ -828,6 +853,7 @@ watch(
|
||||
:cover-image="songInfo.img"
|
||||
@toggle-fullscreen="toggleFullPlay"
|
||||
:song-info="songInfo"
|
||||
:main-color="maincolor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -901,11 +927,13 @@ watch(
|
||||
}
|
||||
|
||||
.player-container {
|
||||
box-shadow: 0px -2px 20px 0px #00000039;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff31;
|
||||
transition: background 0.3s;
|
||||
background: v-bind(bg);
|
||||
// border-top: 1px solid #e5e7eb;
|
||||
backdrop-filter: blur(1000px);
|
||||
z-index: 1000;
|
||||
@@ -939,7 +967,7 @@ watch(
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background: #ffffff71;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.progress-filled {
|
||||
@@ -991,6 +1019,7 @@ watch(
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
|
||||
.album-cover {
|
||||
width: 50px;
|
||||
|
||||
@@ -62,6 +62,7 @@ import ThemeSelector from '@/components/ThemeSelector.vue'
|
||||
组件通过以下方式实现主题切换:
|
||||
|
||||
1. **默认主题**: 移除 `theme-mode` 属性
|
||||
|
||||
```javascript
|
||||
document.documentElement.removeAttribute('theme-mode')
|
||||
```
|
||||
@@ -73,13 +74,13 @@ import ThemeSelector from '@/components/ThemeSelector.vue'
|
||||
|
||||
## 支持的主题
|
||||
|
||||
| 主题名称 | 属性值 | 主色调 |
|
||||
|---------|--------|--------|
|
||||
| 默认 | `default` | #57b4ff |
|
||||
| 粉色 | `pink` | #fc5e7e |
|
||||
| 蓝色 | `blue` | #57b4ff |
|
||||
| 青色 | `cyan` | #3ac2b8 |
|
||||
| 橙色 | `orange` | #fb9458 |
|
||||
| 主题名称 | 属性值 | 主色调 |
|
||||
| -------- | --------- | ------- |
|
||||
| 默认 | `default` | #57b4ff |
|
||||
| 粉色 | `pink` | #fc5e7e |
|
||||
| 蓝色 | `blue` | #57b4ff |
|
||||
| 青色 | `cyan` | #3ac2b8 |
|
||||
| 橙色 | `orange` | #fb9458 |
|
||||
|
||||
## 自定义配置
|
||||
|
||||
@@ -90,7 +91,7 @@ import ThemeSelector from '@/components/ThemeSelector.vue'
|
||||
在 `src/renderer/src/assets/theme/` 目录下创建新的主题文件,例如 `green.css`:
|
||||
|
||||
```css
|
||||
:root[theme-mode="green"] {
|
||||
:root[theme-mode='green'] {
|
||||
--td-brand-color: #10b981;
|
||||
--td-brand-color-hover: #059669;
|
||||
/* 其他主题变量... */
|
||||
@@ -158,4 +159,4 @@ import ThemeDemo from '@/components/ThemeDemo.vue'
|
||||
</script>
|
||||
```
|
||||
|
||||
这个演示组件展示了不同UI元素在各种主题下的表现。
|
||||
这个演示组件展示了不同UI元素在各种主题下的表现。
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<template>
|
||||
<div class="float-ball-settings">
|
||||
<t-card hover-shadow title="AI悬浮球设置">
|
||||
<div class="card-body">
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">显示AI悬浮球</span>
|
||||
<t-switch v-model="showFloatBall" @change="handleFloatBallToggle" />
|
||||
</div>
|
||||
<div class="setting-description">
|
||||
<p>开启后,AI悬浮球将显示在应用界面上,您可以随时与AI助手交流</p>
|
||||
<p>关闭后,AI悬浮球将被隐藏,您可以随时在此处重新开启</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="setting-item">
|
||||
<span class="setting-label">显示AI悬浮球</span>
|
||||
<t-switch v-model="showFloatBall" @change="handleFloatBallToggle" />
|
||||
</div>
|
||||
</t-card>
|
||||
<div class="setting-description">
|
||||
<p>开启后,AI悬浮球将显示在应用界面上,您可以随时与AI助手交流</p>
|
||||
<p>关闭后,AI悬浮球将被隐藏,您可以随时在此处重新开启</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -78,4 +76,4 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
<template>
|
||||
<div class="music-cache">
|
||||
<t-card hover-shadow :loading="cacheInfo ? false : true" title="本地歌曲缓存配置">
|
||||
<template #actions>
|
||||
已有歌曲缓存大小:{{ cacheInfo.sizeFormatted }}
|
||||
</template>
|
||||
<template #actions> 已有歌曲缓存大小:{{ cacheInfo.sizeFormatted }} </template>
|
||||
<div class="card-body">
|
||||
<t-button size="large" @click="clearCache">
|
||||
清除本地缓存
|
||||
</t-button>
|
||||
<t-button size="large" @click="clearCache"> 清除本地缓存 </t-button>
|
||||
</div>
|
||||
</t-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DialogPlugin } from 'tdesign-vue-next';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { DialogPlugin } from 'tdesign-vue-next'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const cacheInfo: any = ref({})
|
||||
onMounted(() => {
|
||||
window.api.musicCache.getInfo().then(res => cacheInfo.value = res)
|
||||
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
|
||||
})
|
||||
const clearCache = () => {
|
||||
const confirm = DialogPlugin.confirm({
|
||||
@@ -27,7 +23,7 @@ const clearCache = () => {
|
||||
body: '这可能会导致歌曲加载缓慢,你确定要清除所有缓存吗?',
|
||||
confirmBtn: '确定清除',
|
||||
cancelBtn: '我再想想',
|
||||
placement:'center',
|
||||
placement: 'center',
|
||||
onClose: () => {
|
||||
confirm.hide()
|
||||
},
|
||||
@@ -35,8 +31,7 @@ const clearCache = () => {
|
||||
confirm.hide()
|
||||
cacheInfo.value = {}
|
||||
await window.api.musicCache.clear()
|
||||
window.api.musicCache.getInfo().then(res => cacheInfo.value = res)
|
||||
|
||||
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -51,4 +46,4 @@ const clearCache = () => {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
<h3>自动更新</h3>
|
||||
<div class="update-info">
|
||||
<p>当前版本: {{ currentVersion }}</p>
|
||||
<t-button
|
||||
theme="primary"
|
||||
:loading="isChecking"
|
||||
@click="handleCheckUpdate"
|
||||
>
|
||||
<t-button theme="primary" :loading="isChecking" @click="handleCheckUpdate">
|
||||
{{ isChecking ? '检查中...' : '检查更新' }}
|
||||
</t-button>
|
||||
</div>
|
||||
@@ -70,4 +66,4 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -5,24 +5,36 @@
|
||||
<div class="theme-color-preview" :style="{ backgroundColor: currentThemeColor }"></div>
|
||||
<span class="theme-name">{{ currentThemeName }}</span>
|
||||
</div>
|
||||
<svg class="dropdown-icon" :class="{ 'rotated': isDropdownOpen }" viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M7 10l5 5 5-5z" fill="currentColor"/>
|
||||
<svg
|
||||
class="dropdown-icon"
|
||||
:class="{ rotated: isDropdownOpen }"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path d="M7 10l5 5 5-5z" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
<transition name="dropdown">
|
||||
<div v-if="isDropdownOpen" class="theme-dropdown">
|
||||
<div
|
||||
v-for="theme in themes"
|
||||
<div
|
||||
v-for="theme in themes"
|
||||
:key="theme.name"
|
||||
class="theme-option"
|
||||
:class="{ 'active': currentTheme === theme.name }"
|
||||
:class="{ active: currentTheme === theme.name }"
|
||||
@click="selectTheme(theme.name)"
|
||||
>
|
||||
<div class="theme-color-dot" :style="{ backgroundColor: theme.color }"></div>
|
||||
<span class="theme-label">{{ theme.label }}</span>
|
||||
<svg v-if="currentTheme === theme.name" class="check-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor"/>
|
||||
<svg
|
||||
v-if="currentTheme === theme.name"
|
||||
class="check-icon"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +59,7 @@ const themes = [
|
||||
|
||||
const loadSavedTheme = () => {
|
||||
const savedTheme = localStorage.getItem('selected-theme')
|
||||
if (savedTheme && themes.some(t => t.name === savedTheme)) {
|
||||
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
|
||||
currentTheme.value = savedTheme
|
||||
applyTheme(savedTheme)
|
||||
}
|
||||
@@ -55,26 +67,26 @@ const loadSavedTheme = () => {
|
||||
|
||||
const applyTheme = (themeName) => {
|
||||
const documentElement = document.documentElement
|
||||
|
||||
|
||||
// 移除之前的主题
|
||||
documentElement.removeAttribute('theme-mode')
|
||||
|
||||
|
||||
// 应用新主题(如果不是默认主题)
|
||||
if (themeName !== 'default') {
|
||||
documentElement.setAttribute('theme-mode', themeName)
|
||||
}
|
||||
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('selected-theme', themeName)
|
||||
}
|
||||
|
||||
const currentThemeColor = computed(() => {
|
||||
const theme = themes.find(t => t.name === currentTheme.value)
|
||||
const theme = themes.find((t) => t.name === currentTheme.value)
|
||||
return theme ? theme.color : '#2ba55b'
|
||||
})
|
||||
|
||||
const currentThemeName = computed(() => {
|
||||
const theme = themes.find(t => t.name === currentTheme.value)
|
||||
const theme = themes.find((t) => t.name === currentTheme.value)
|
||||
return theme ? theme.label : '默认'
|
||||
})
|
||||
|
||||
@@ -93,10 +105,6 @@ const selectTheme = (themeName) => {
|
||||
isDropdownOpen.value = false
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
const themeSelector = event.target.closest('.theme-selector')
|
||||
if (!themeSelector) {
|
||||
@@ -178,7 +186,11 @@ onUnmounted(() => {
|
||||
background: var(--td-bg-color-container, #ffffff);
|
||||
border: 1px solid var(--td-component-border, #e2e8f0);
|
||||
border-radius: var(--td-radius-medium, 6px);
|
||||
box-shadow: var(--td-shadow-2, 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06));
|
||||
box-shadow: var(
|
||||
--td-shadow-2,
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06)
|
||||
);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -241,11 +253,11 @@ onUnmounted(() => {
|
||||
min-width: 100px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
|
||||
.theme-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
.theme-option {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
</t-button>
|
||||
|
||||
<!-- 测试按钮 -->
|
||||
<t-button theme="default" @click="testProgress">
|
||||
测试进度显示
|
||||
</t-button>
|
||||
<t-button theme="default" @click="testProgress"> 测试进度显示 </t-button>
|
||||
</div>
|
||||
|
||||
<!-- 显示当前下载状态 -->
|
||||
@@ -142,4 +140,4 @@ const formatBytes = (bytes: number): string => {
|
||||
margin: 4px 0;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { autoUpdateService, downloadState } from '../services/autoUpdateService';
|
||||
import { autoUpdateService, downloadState } from '../services/autoUpdateService'
|
||||
|
||||
export function useAutoUpdate() {
|
||||
// 检查更新
|
||||
const checkForUpdates = async () => {
|
||||
await autoUpdateService.checkForUpdates();
|
||||
};
|
||||
await autoUpdateService.checkForUpdates()
|
||||
}
|
||||
|
||||
// 下载更新
|
||||
const downloadUpdate = async () => {
|
||||
await autoUpdateService.downloadUpdate();
|
||||
};
|
||||
await autoUpdateService.downloadUpdate()
|
||||
}
|
||||
|
||||
// 安装更新
|
||||
const quitAndInstall = async () => {
|
||||
await autoUpdateService.quitAndInstall();
|
||||
};
|
||||
await autoUpdateService.quitAndInstall()
|
||||
}
|
||||
|
||||
return {
|
||||
checkForUpdates,
|
||||
downloadUpdate,
|
||||
quitAndInstall,
|
||||
downloadState // 导出下载状态供组件使用
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import './assets/base.css'
|
||||
import 'animate.css';
|
||||
import 'animate.css'
|
||||
|
||||
// 引入组件库的少量全局样式变量
|
||||
// import 'tdesign-vue-next/es/style/index.css' //tdesign 组件样式
|
||||
|
||||
@@ -42,8 +42,8 @@ let routes: RouteRecordRaw[] = [
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
meta: {
|
||||
transitionIn: "animate__fadeIn",
|
||||
transitionOut: "animate__fadeOut",
|
||||
transitionIn: 'animate__fadeIn',
|
||||
transitionOut: 'animate__fadeOut'
|
||||
},
|
||||
component: () => import('@renderer/views/settings/index.vue')
|
||||
},
|
||||
@@ -55,14 +55,14 @@ let routes: RouteRecordRaw[] = [
|
||||
]
|
||||
function setAnimate(routerObj: RouteRecordRaw[]) {
|
||||
for (let i = 0; i < routerObj.length; i++) {
|
||||
let item = routerObj[i];
|
||||
let item = routerObj[i]
|
||||
if (item.children && item.children.length > 0) {
|
||||
setAnimate(item.children);
|
||||
setAnimate(item.children)
|
||||
} else {
|
||||
if (item.meta) continue
|
||||
item.meta = item.meta || {};
|
||||
item.meta.transitionIn = 'animate__fadeInRight';
|
||||
item.meta.transitionOut = 'animate__fadeOutLeft';
|
||||
item.meta = item.meta || {}
|
||||
item.meta.transitionIn = 'animate__fadeInRight'
|
||||
item.meta.transitionOut = 'animate__fadeOutLeft'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,11 +72,11 @@ const option: RouterOptions = {
|
||||
routes,
|
||||
scrollBehavior(_to_, _from_, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
return savedPosition
|
||||
} else {
|
||||
return { top: 0 };
|
||||
return { top: 0 }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const router = createRouter(option)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { NotifyPlugin, DialogPlugin } from 'tdesign-vue-next';
|
||||
import { NotifyPlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
|
||||
import { reactive } from 'vue';
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export interface DownloadProgress {
|
||||
percent: number;
|
||||
transferred: number;
|
||||
total: number;
|
||||
percent: number
|
||||
transferred: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface UpdateInfo {
|
||||
url: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
pub_date: string;
|
||||
url: string
|
||||
name: string
|
||||
notes: string
|
||||
pub_date: string
|
||||
}
|
||||
|
||||
// 响应式的下载状态
|
||||
@@ -24,109 +24,109 @@ export const downloadState = reactive({
|
||||
total: 0
|
||||
} as DownloadProgress,
|
||||
updateInfo: null as UpdateInfo | null
|
||||
});
|
||||
})
|
||||
|
||||
export class AutoUpdateService {
|
||||
private static instance: AutoUpdateService;
|
||||
private isListening = false;
|
||||
private static instance: AutoUpdateService
|
||||
private isListening = false
|
||||
|
||||
constructor() {
|
||||
// 构造函数中自动开始监听
|
||||
this.startListening();
|
||||
this.startListening()
|
||||
}
|
||||
|
||||
static getInstance(): AutoUpdateService {
|
||||
if (!AutoUpdateService.instance) {
|
||||
AutoUpdateService.instance = new AutoUpdateService();
|
||||
AutoUpdateService.instance = new AutoUpdateService()
|
||||
}
|
||||
return AutoUpdateService.instance;
|
||||
return AutoUpdateService.instance
|
||||
}
|
||||
|
||||
// 开始监听更新消息
|
||||
startListening() {
|
||||
if (this.isListening) return;
|
||||
|
||||
this.isListening = true;
|
||||
|
||||
if (this.isListening) return
|
||||
|
||||
this.isListening = true
|
||||
|
||||
// 监听各种更新事件
|
||||
window.api.autoUpdater.onCheckingForUpdate(() => {
|
||||
this.showCheckingNotification();
|
||||
});
|
||||
|
||||
window.api.autoUpdater.onUpdateAvailable((_,updateInfo: UpdateInfo) => {
|
||||
this.showUpdateAvailableDialog(updateInfo);
|
||||
});
|
||||
|
||||
this.showCheckingNotification()
|
||||
})
|
||||
|
||||
window.api.autoUpdater.onUpdateAvailable((_, updateInfo: UpdateInfo) => {
|
||||
this.showUpdateAvailableDialog(updateInfo)
|
||||
})
|
||||
|
||||
window.api.autoUpdater.onUpdateNotAvailable(() => {
|
||||
this.showNoUpdateNotification();
|
||||
});
|
||||
|
||||
this.showNoUpdateNotification()
|
||||
})
|
||||
|
||||
window.api.autoUpdater.onDownloadStarted((updateInfo: UpdateInfo) => {
|
||||
this.handleDownloadStarted(updateInfo);
|
||||
});
|
||||
|
||||
this.handleDownloadStarted(updateInfo)
|
||||
})
|
||||
|
||||
window.api.autoUpdater.onDownloadProgress((progress: DownloadProgress) => {
|
||||
console.log(progress)
|
||||
|
||||
this.showDownloadProgressNotification(progress);
|
||||
});
|
||||
|
||||
this.showDownloadProgressNotification(progress)
|
||||
})
|
||||
|
||||
window.api.autoUpdater.onUpdateDownloaded(() => {
|
||||
this.showUpdateDownloadedDialog();
|
||||
});
|
||||
|
||||
window.api.autoUpdater.onError((_,error: string) => {
|
||||
this.showUpdateErrorNotification(error);
|
||||
});
|
||||
this.showUpdateDownloadedDialog()
|
||||
})
|
||||
|
||||
window.api.autoUpdater.onError((_, error: string) => {
|
||||
this.showUpdateErrorNotification(error)
|
||||
})
|
||||
}
|
||||
|
||||
// 停止监听更新消息
|
||||
stopListening() {
|
||||
if (!this.isListening) return;
|
||||
|
||||
this.isListening = false;
|
||||
window.api.autoUpdater.removeAllListeners();
|
||||
if (!this.isListening) return
|
||||
|
||||
this.isListening = false
|
||||
window.api.autoUpdater.removeAllListeners()
|
||||
}
|
||||
|
||||
// 检查更新
|
||||
async checkForUpdates() {
|
||||
try {
|
||||
await window.api.autoUpdater.checkForUpdates();
|
||||
await window.api.autoUpdater.checkForUpdates()
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error);
|
||||
console.error('检查更新失败:', error)
|
||||
NotifyPlugin.error({
|
||||
title: '更新检查失败',
|
||||
content: '无法检查更新,请稍后重试',
|
||||
duration: 3000
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 下载更新
|
||||
async downloadUpdate() {
|
||||
try {
|
||||
await window.api.autoUpdater.downloadUpdate();
|
||||
await window.api.autoUpdater.downloadUpdate()
|
||||
} catch (error) {
|
||||
console.error('下载更新失败:', error);
|
||||
console.error('下载更新失败:', error)
|
||||
NotifyPlugin.error({
|
||||
title: '下载更新失败',
|
||||
content: '无法下载更新,请稍后重试',
|
||||
duration: 3000
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 安装更新
|
||||
async quitAndInstall() {
|
||||
try {
|
||||
await window.api.autoUpdater.quitAndInstall();
|
||||
await window.api.autoUpdater.quitAndInstall()
|
||||
} catch (error) {
|
||||
console.error('安装更新失败:', error);
|
||||
console.error('安装更新失败:', error)
|
||||
NotifyPlugin.error({
|
||||
title: '安装更新失败',
|
||||
content: '无法安装更新,请稍后重试',
|
||||
duration: 3000
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,29 +136,29 @@ export class AutoUpdateService {
|
||||
title: '检查更新',
|
||||
content: '正在检查是否有新版本...',
|
||||
duration: 2000
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// 显示有更新可用对话框
|
||||
private showUpdateAvailableDialog(updateInfo: UpdateInfo) {
|
||||
// 保存更新信息到状态中
|
||||
downloadState.updateInfo = updateInfo;
|
||||
downloadState.updateInfo = updateInfo
|
||||
console.log(updateInfo)
|
||||
const releaseDate = new Date(updateInfo.pub_date).toLocaleDateString('zh-CN');
|
||||
|
||||
const dialog =DialogPlugin.confirm({
|
||||
const releaseDate = new Date(updateInfo.pub_date).toLocaleDateString('zh-CN')
|
||||
|
||||
const dialog = DialogPlugin.confirm({
|
||||
header: `发现新版本 ${updateInfo.name}`,
|
||||
body: `发布时间: ${releaseDate}\n\n更新说明:\n${updateInfo.notes || '暂无更新说明'}\n\n是否立即下载此更新?`,
|
||||
confirmBtn: '立即下载',
|
||||
cancelBtn: '稍后提醒',
|
||||
onConfirm: () => {
|
||||
this.downloadUpdate();
|
||||
this.downloadUpdate()
|
||||
dialog.hide()
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('用户选择稍后下载更新');
|
||||
console.log('用户选择稍后下载更新')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// 显示无更新通知
|
||||
@@ -167,49 +167,51 @@ export class AutoUpdateService {
|
||||
title: '已是最新版本',
|
||||
content: '当前已是最新版本,无需更新',
|
||||
duration: 3000
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// 处理下载开始事件
|
||||
private handleDownloadStarted(updateInfo: UpdateInfo) {
|
||||
downloadState.isDownloading = true;
|
||||
downloadState.updateInfo = updateInfo;
|
||||
downloadState.isDownloading = true
|
||||
downloadState.updateInfo = updateInfo
|
||||
downloadState.progress = {
|
||||
percent: 0,
|
||||
transferred: 0,
|
||||
total: 0
|
||||
};
|
||||
|
||||
console.log('开始下载更新:', updateInfo.name);
|
||||
}
|
||||
|
||||
console.log('开始下载更新:', updateInfo.name)
|
||||
}
|
||||
|
||||
// 更新下载进度状态
|
||||
private showDownloadProgressNotification(progress: DownloadProgress) {
|
||||
// 更新响应式状态
|
||||
downloadState.isDownloading = true;
|
||||
downloadState.progress = progress;
|
||||
|
||||
console.log(`下载进度: ${Math.round(progress.percent)}% (${this.formatBytes(progress.transferred)} / ${this.formatBytes(progress.total)})`);
|
||||
downloadState.isDownloading = true
|
||||
downloadState.progress = progress
|
||||
|
||||
console.log(
|
||||
`下载进度: ${Math.round(progress.percent)}% (${this.formatBytes(progress.transferred)} / ${this.formatBytes(progress.total)})`
|
||||
)
|
||||
}
|
||||
|
||||
// 显示更新下载完成对话框
|
||||
private showUpdateDownloadedDialog() {
|
||||
// 更新下载状态
|
||||
downloadState.isDownloading = false;
|
||||
downloadState.progress.percent = 100;
|
||||
|
||||
downloadState.isDownloading = false
|
||||
downloadState.progress.percent = 100
|
||||
|
||||
DialogPlugin.confirm({
|
||||
header: '更新下载完成',
|
||||
body: '新版本已下载完成,是否立即重启应用以完成更新?',
|
||||
confirmBtn: '立即重启',
|
||||
cancelBtn: '稍后重启',
|
||||
onConfirm: () => {
|
||||
this.quitAndInstall();
|
||||
this.quitAndInstall()
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('用户选择稍后重启');
|
||||
console.log('用户选择稍后重启')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// 显示更新错误通知
|
||||
@@ -218,20 +220,20 @@ export class AutoUpdateService {
|
||||
title: '更新失败',
|
||||
content: `更新过程中出现错误: ${error}`,
|
||||
duration: 5000
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化字节大小
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const autoUpdateService = AutoUpdateService.getInstance();
|
||||
export const autoUpdateService = AutoUpdateService.getInstance()
|
||||
|
||||
@@ -79,12 +79,16 @@ export const LocalUserDetailStore = defineStore('Local', () => {
|
||||
return list.value
|
||||
}
|
||||
|
||||
function removeSong(songId: number) {
|
||||
function removeSong(songId: number | string) {
|
||||
const index = list.value.findIndex((item) => item.songmid === songId)
|
||||
if (index !== -1) {
|
||||
list.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function clearList() {
|
||||
list.value = []
|
||||
}
|
||||
const userSource = computed(() => {
|
||||
return {
|
||||
pluginId: userInfo.value.pluginId,
|
||||
@@ -100,6 +104,7 @@ export const LocalUserDetailStore = defineStore('Local', () => {
|
||||
addSong,
|
||||
addSongToFirst,
|
||||
removeSong,
|
||||
clearList,
|
||||
userSource
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
} catch (error) {
|
||||
console.error('加载设置失败:', error)
|
||||
}
|
||||
|
||||
|
||||
// 默认设置
|
||||
return {
|
||||
showFloatBall: true
|
||||
@@ -47,4 +47,4 @@ export const useSettingsStore = defineStore('settings', () => {
|
||||
updateSettings,
|
||||
toggleFloatBall
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import playList from '@common/types/playList'
|
||||
|
||||
// 音频事件相关类型定义
|
||||
|
||||
// 事件回调函数类型定义
|
||||
@@ -44,18 +46,4 @@ export type ControlAudioState = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export type SongList = {
|
||||
songmid: number
|
||||
hash?: string
|
||||
singer: string
|
||||
name: string
|
||||
albumName: string
|
||||
albumId: number
|
||||
source: string
|
||||
interval: string
|
||||
img: string
|
||||
lrc: null | string
|
||||
types: string[]
|
||||
_types: Record<string, any>
|
||||
typeUrl: Record<string, any>
|
||||
}
|
||||
export type SongList = playList
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PlayMode } from './audio'
|
||||
import { Sources } from './Sources'
|
||||
|
||||
export interface UserInfo {
|
||||
lastPlaySongId?: number | null
|
||||
lastPlaySongId?: number | string | null
|
||||
currentTime?: number
|
||||
volume?: number
|
||||
topBarStyle?: boolean
|
||||
|
||||
122
src/renderer/src/utils/audioManager.ts
Normal file
122
src/renderer/src/utils/audioManager.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// 全局音频管理器,用于管理音频源和分析器
|
||||
class AudioManager {
|
||||
private static instance: AudioManager
|
||||
private audioSources = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>()
|
||||
private audioContexts = new WeakMap<HTMLAudioElement, AudioContext>()
|
||||
private analysers = new Map<string, AnalyserNode>()
|
||||
|
||||
static getInstance(): AudioManager {
|
||||
if (!AudioManager.instance) {
|
||||
AudioManager.instance = new AudioManager()
|
||||
}
|
||||
return AudioManager.instance
|
||||
}
|
||||
|
||||
// 获取或创建音频源
|
||||
getOrCreateAudioSource(
|
||||
audioElement: HTMLAudioElement
|
||||
): { source: MediaElementAudioSourceNode; context: AudioContext } | null {
|
||||
try {
|
||||
// 检查是否已经有音频源
|
||||
let source = this.audioSources.get(audioElement)
|
||||
let context = this.audioContexts.get(audioElement)
|
||||
|
||||
if (!source || !context || context.state === 'closed') {
|
||||
// 创建新的音频上下文和源
|
||||
context = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||
source = context.createMediaElementSource(audioElement)
|
||||
|
||||
// 连接到输出,确保音频能正常播放
|
||||
source.connect(context.destination)
|
||||
|
||||
// 存储引用
|
||||
this.audioSources.set(audioElement, source)
|
||||
this.audioContexts.set(audioElement, context)
|
||||
|
||||
console.log('AudioManager: 创建新的音频源和上下文')
|
||||
}
|
||||
|
||||
return { source, context }
|
||||
} catch (error) {
|
||||
console.error('AudioManager: 创建音频源失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建分析器
|
||||
createAnalyser(
|
||||
audioElement: HTMLAudioElement,
|
||||
id: string,
|
||||
fftSize: number = 256
|
||||
): AnalyserNode | null {
|
||||
const audioData = this.getOrCreateAudioSource(audioElement)
|
||||
if (!audioData) return null
|
||||
|
||||
const { source, context } = audioData
|
||||
|
||||
try {
|
||||
// 创建分析器
|
||||
const analyser = context.createAnalyser()
|
||||
analyser.fftSize = fftSize
|
||||
analyser.smoothingTimeConstant = 0.6
|
||||
|
||||
// 创建增益节点作为中介,避免直接断开主音频链
|
||||
const gainNode = context.createGain()
|
||||
gainNode.gain.value = 1.0
|
||||
|
||||
// 连接:source -> gainNode -> analyser
|
||||
// -> destination (保持音频播放)
|
||||
source.disconnect() // 先断开所有连接
|
||||
source.connect(gainNode)
|
||||
gainNode.connect(context.destination) // 确保音频继续播放
|
||||
gainNode.connect(analyser) // 连接到分析器
|
||||
|
||||
// 存储分析器引用
|
||||
this.analysers.set(id, analyser)
|
||||
|
||||
console.log('AudioManager: 创建分析器成功')
|
||||
return analyser
|
||||
} catch (error) {
|
||||
console.error('AudioManager: 创建分析器失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 移除分析器
|
||||
removeAnalyser(id: string): void {
|
||||
const analyser = this.analysers.get(id)
|
||||
if (analyser) {
|
||||
try {
|
||||
analyser.disconnect()
|
||||
this.analysers.delete(id)
|
||||
console.log('AudioManager: 移除分析器成功')
|
||||
} catch (error) {
|
||||
console.warn('AudioManager: 移除分析器时出错:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清理音频元素的所有资源
|
||||
cleanupAudioElement(audioElement: HTMLAudioElement): void {
|
||||
try {
|
||||
const context = this.audioContexts.get(audioElement)
|
||||
if (context && context.state !== 'closed') {
|
||||
context.close()
|
||||
}
|
||||
|
||||
this.audioSources.delete(audioElement)
|
||||
this.audioContexts.delete(audioElement)
|
||||
|
||||
console.log('AudioManager: 清理音频元素资源')
|
||||
} catch (error) {
|
||||
console.warn('AudioManager: 清理资源时出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分析器
|
||||
getAnalyser(id: string): AnalyserNode | undefined {
|
||||
return this.analysers.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioManager.getInstance()
|
||||
@@ -1,4 +1,92 @@
|
||||
import { extractDominantColor } from './colorExtractor'
|
||||
import DefaultCover from '@renderer/assets/images/Default.jpg'
|
||||
import CoverImage from '@renderer/assets/images/cover.png'
|
||||
|
||||
/**
|
||||
* 直接从图片分析平均亮度,不依赖颜色提取器
|
||||
* @param imageSrc 图片路径
|
||||
* @returns 返回平均亮度值 (0-1)
|
||||
*/
|
||||
async function getImageAverageLuminance(imageSrc: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 处理相对路径
|
||||
let actualSrc = imageSrc
|
||||
if (
|
||||
imageSrc.includes('@assets/images/Default.jpg') ||
|
||||
imageSrc.includes('@renderer/assets/images/Default.jpg')
|
||||
) {
|
||||
actualSrc = DefaultCover
|
||||
} else if (
|
||||
imageSrc.includes('@assets/images/cover.png') ||
|
||||
imageSrc.includes('@renderer/assets/images/cover.png')
|
||||
) {
|
||||
actualSrc = CoverImage
|
||||
}
|
||||
|
||||
// 如果仍然是相对路径,使用默认亮度
|
||||
if (actualSrc.startsWith('@')) {
|
||||
console.warn('无法解析相对路径,使用默认亮度')
|
||||
resolve(0.5) // 中等亮度
|
||||
return
|
||||
}
|
||||
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'Anonymous'
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
reject(new Error('无法创建canvas上下文'))
|
||||
return
|
||||
}
|
||||
|
||||
// 使用较小的采样尺寸提高性能
|
||||
const size = 50
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
|
||||
ctx.drawImage(img, 0, 0, size, size)
|
||||
const imageData = ctx.getImageData(0, 0, size, size).data
|
||||
|
||||
let totalLuminance = 0
|
||||
let pixelCount = 0
|
||||
|
||||
// 计算所有非透明像素的平均亮度
|
||||
for (let i = 0; i < imageData.length; i += 4) {
|
||||
// 忽略透明像素
|
||||
if (imageData[i + 3] < 128) continue
|
||||
|
||||
const r = imageData[i] / 255
|
||||
const g = imageData[i + 1] / 255
|
||||
const b = imageData[i + 2] / 255
|
||||
|
||||
// 使用WCAG 2.0相对亮度公式
|
||||
const R = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
|
||||
const G = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
|
||||
const B = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
|
||||
|
||||
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
|
||||
totalLuminance += luminance
|
||||
pixelCount++
|
||||
}
|
||||
|
||||
const averageLuminance = pixelCount > 0 ? totalLuminance / pixelCount : 0.5
|
||||
resolve(averageLuminance)
|
||||
} catch (error) {
|
||||
console.error('分析图片亮度失败:', error)
|
||||
resolve(0.5) // 默认中等亮度
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
console.error('图片加载失败:', actualSrc)
|
||||
resolve(0.5) // 默认中等亮度
|
||||
}
|
||||
|
||||
img.src = actualSrc
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断图片应该使用黑色还是白色作为对比色
|
||||
@@ -7,46 +95,24 @@ import { extractDominantColor } from './colorExtractor'
|
||||
*/
|
||||
export async function shouldUseBlackText(imageSrc: string): Promise<boolean> {
|
||||
try {
|
||||
// 提取主要颜色
|
||||
const dominantColor = await extractDominantColor(imageSrc)
|
||||
|
||||
// 使用更准确的相对亮度计算公式 (sRGB相对亮度)
|
||||
// 先将RGB值标准化到0-1范围
|
||||
const r = dominantColor.r / 255
|
||||
const g = dominantColor.g / 255
|
||||
const b = dominantColor.b / 255
|
||||
|
||||
// 应用sRGB转换
|
||||
const R = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
|
||||
const G = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
|
||||
const B = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
|
||||
|
||||
// 计算相对亮度 (WCAG 2.0公式)
|
||||
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
|
||||
|
||||
// 计算与黑色和白色的对比度
|
||||
// 对比度计算公式: (L1 + 0.05) / (L2 + 0.05),其中L1是较亮的颜色,L2是较暗的颜色
|
||||
const contrastWithBlack = (luminance + 0.05) / 0.05
|
||||
const contrastWithWhite = 1.05 / (luminance + 0.05)
|
||||
// 直接分析图片的平均亮度
|
||||
const averageLuminance = await getImageAverageLuminance(imageSrc)
|
||||
|
||||
console.log(
|
||||
`颜色: RGB(${dominantColor.r},${dominantColor.g},${dominantColor.b}), ` +
|
||||
`亮度: ${luminance.toFixed(3)}, ` +
|
||||
`与黑色对比度: ${contrastWithBlack.toFixed(2)}, ` +
|
||||
`与白色对比度: ${contrastWithWhite.toFixed(2)}`
|
||||
`图片: ${imageSrc}, ` +
|
||||
`平均亮度: ${averageLuminance.toFixed(3)} ` +
|
||||
`(考虑半透明黑色背景覆盖效果)`
|
||||
)
|
||||
|
||||
// 不仅考虑亮度,还要考虑对比度
|
||||
// 如果与黑色的对比度更高,说明背景较亮,应该使用黑色文字
|
||||
// 如果与白色的对比度更高,说明背景较暗,应该使用白色文字
|
||||
// 但对于中等亮度的颜色,我们需要更精细的判断
|
||||
// 考虑到图片会受到rgba(0, 0, 0, 0.256)背景覆盖,实际显示会更暗
|
||||
// 大幅提高阈值,让白色文字在更多情况下被选择
|
||||
// 只有非常明亮的图片才使用黑色文字
|
||||
|
||||
// 对于中等亮度的颜色(0.3-0.6),我们更倾向于使用黑色文本,因为黑色文本通常更易读
|
||||
if (luminance > 0.3) {
|
||||
return true // 使用黑色文本
|
||||
} else {
|
||||
return false // 使用白色文本
|
||||
}
|
||||
const shouldUseBlack = averageLuminance >= 0.6
|
||||
|
||||
console.log(`决定使用${shouldUseBlack ? '黑色' : '白色'}文字`)
|
||||
|
||||
return shouldUseBlack
|
||||
} catch (error) {
|
||||
console.error('计算对比色失败:', error)
|
||||
// 默认返回白色作为安全选择
|
||||
@@ -75,24 +141,11 @@ export async function getBestContrastTextColorWithOpacity(
|
||||
opacity: number = 1
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 提取主要颜色
|
||||
const dominantColor = await extractDominantColor(imageSrc)
|
||||
// 使用相同的亮度分析逻辑
|
||||
const averageLuminance = await getImageAverageLuminance(imageSrc)
|
||||
|
||||
// 使用更准确的相对亮度计算公式 (sRGB相对亮度)
|
||||
const r = dominantColor.r / 255
|
||||
const g = dominantColor.g / 255
|
||||
const b = dominantColor.b / 255
|
||||
|
||||
// 应用sRGB转换
|
||||
const R = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)
|
||||
const G = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)
|
||||
const B = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4)
|
||||
|
||||
// 计算相对亮度
|
||||
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B
|
||||
|
||||
// 根据亮度决定文本颜色,使用更低的阈值
|
||||
if (luminance > 0.3) {
|
||||
// 使用与shouldUseBlackText相同的逻辑
|
||||
if (averageLuminance >= 0.6) {
|
||||
// 背景较亮,使用黑色文本
|
||||
return `rgba(0, 0, 0, ${opacity})`
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NotifyPlugin } from 'tdesign-vue-next'
|
||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
import { toRaw } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
|
||||
interface MusicItem {
|
||||
singer: string
|
||||
@@ -31,22 +31,23 @@ const qualityMap: Record<string, string> = {
|
||||
const qualityKey = Object.keys(qualityMap)
|
||||
|
||||
async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
||||
|
||||
try {
|
||||
const LocalUserDetail = LocalUserDetailStore()
|
||||
let quality = LocalUserDetail.userSource.quality as string
|
||||
let quality = LocalUserDetail.userSource.quality as string
|
||||
if (
|
||||
qualityKey.indexOf(quality) >
|
||||
qualityKey.indexOf((songInfo.types[songInfo.types.length - 1] as unknown as { type: any }).type)
|
||||
qualityKey.indexOf(
|
||||
(songInfo.types[songInfo.types.length - 1] as unknown as { type: any }).type
|
||||
)
|
||||
) {
|
||||
quality = (songInfo.types[songInfo.types.length - 1] as unknown as { type: any }).type
|
||||
}
|
||||
const tip = MessagePlugin.success('开始下载歌曲:'+ songInfo.name)
|
||||
const result = await window.api.music.requestSdk('downloadSingleSong',{
|
||||
pluginId:LocalUserDetail.userSource.pluginId?.toString() || '',
|
||||
source:songInfo.source,
|
||||
const tip = MessagePlugin.success('开始下载歌曲:' + songInfo.name)
|
||||
const result = await window.api.music.requestSdk('downloadSingleSong', {
|
||||
pluginId: LocalUserDetail.userSource.pluginId?.toString() || '',
|
||||
source: songInfo.source,
|
||||
quality,
|
||||
songInfo:toRaw(songInfo)
|
||||
songInfo: toRaw(songInfo)
|
||||
})
|
||||
;(await tip).close()
|
||||
if (!Object.hasOwn(result, 'path')) {
|
||||
@@ -61,7 +62,7 @@ async function downloadSingleSong(songInfo: MusicItem): Promise<void> {
|
||||
console.error('下载失败:', error)
|
||||
await NotifyPlugin.error({
|
||||
title: '下载失败',
|
||||
content: `${error.message.includes('歌曲正在')? '歌曲正在下载中':'未知错误'}`
|
||||
content: `${error.message.includes('歌曲正在') ? '歌曲正在下载中' : '未知错误'}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
type PlaylistEvents = {
|
||||
addToPlaylistAndPlay: SongList
|
||||
addToPlaylistEnd: SongList
|
||||
replacePlaylist: SongList[]
|
||||
}
|
||||
|
||||
// 创建全局事件总线
|
||||
const emitter = mitt<PlaylistEvents>()
|
||||
|
||||
// 将事件总线挂载到全局
|
||||
; (window as any).musicEmitter = emitter
|
||||
// 将事件总线挂载到全局
|
||||
;(window as any).musicEmitter = emitter
|
||||
const qualityMap: Record<string, string> = {
|
||||
'128k': '标准音质',
|
||||
'192k': '高品音质',
|
||||
@@ -46,7 +47,7 @@ export async function getSongRealUrl(song: SongList): Promise<string> {
|
||||
const urlData = await window.api.music.requestSdk('getMusicUrl', {
|
||||
pluginId: LocalUserDetail.userSource.pluginId as unknown as string,
|
||||
source: song.source,
|
||||
songInfo: song,
|
||||
songInfo: song as any,
|
||||
quality
|
||||
})
|
||||
console.log(urlData)
|
||||
@@ -124,6 +125,52 @@ export async function addToPlaylistEnd(song: SongList, localUserStore: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换整个播放列表
|
||||
* @param songs 要替换的歌曲列表
|
||||
* @param localUserStore LocalUserDetail store实例
|
||||
* @param playSongCallback 播放歌曲的回调函数
|
||||
*/
|
||||
export async function replacePlaylist(
|
||||
songs: SongList[],
|
||||
localUserStore: any,
|
||||
playSongCallback: (song: SongList) => Promise<void>
|
||||
) {
|
||||
try {
|
||||
if (songs.length === 0) {
|
||||
await MessagePlugin.warning('歌曲列表为空')
|
||||
return
|
||||
}
|
||||
|
||||
// 清空当前播放列表
|
||||
localUserStore.list.length = 0
|
||||
|
||||
// 添加所有歌曲到播放列表
|
||||
songs.forEach((song) => {
|
||||
localUserStore.addSong(song)
|
||||
})
|
||||
|
||||
// 播放第一首歌曲
|
||||
if (songs[0]) {
|
||||
await getSongRealUrl(songs[0])
|
||||
const playResult = playSongCallback(songs[0])
|
||||
|
||||
if (playResult && typeof playResult.then === 'function') {
|
||||
await playResult
|
||||
}
|
||||
}
|
||||
|
||||
await MessagePlugin.success(`已用 ${songs.length} 首歌曲替换播放列表`)
|
||||
} catch (error: any) {
|
||||
console.error('替换播放列表失败:', error)
|
||||
if (error.message) {
|
||||
await MessagePlugin.error('替换失败: ' + error.message)
|
||||
return
|
||||
}
|
||||
await MessagePlugin.error('替换播放列表失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化播放列表事件监听器
|
||||
* @param localUserStore LocalUserDetail store实例
|
||||
@@ -142,6 +189,11 @@ export function initPlaylistEventListeners(
|
||||
emitter.on('addToPlaylistEnd', async (song: SongList) => {
|
||||
await addToPlaylistEnd(song, localUserStore)
|
||||
})
|
||||
|
||||
// 监听替换播放列表的事件
|
||||
emitter.on('replacePlaylist', async (songs: SongList[]) => {
|
||||
await replacePlaylist(songs, localUserStore, playSongCallback)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +202,7 @@ export function initPlaylistEventListeners(
|
||||
export function destroyPlaylistEventListeners() {
|
||||
emitter.off('addToPlaylistAndPlay')
|
||||
emitter.off('addToPlaylistEnd')
|
||||
emitter.off('replacePlaylist')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
<div class="settings-page">
|
||||
<div class="settings-container">
|
||||
<h2>应用设置</h2>
|
||||
|
||||
|
||||
<!-- 其他设置项 -->
|
||||
<div class="settings-section">
|
||||
<h3>常规设置</h3>
|
||||
<!-- 这里可以添加其他设置项 -->
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 自动更新设置 -->
|
||||
<UpdateSettings />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,4 +47,4 @@ import UpdateSettings from '../components/Settings/UpdateSettings.vue'
|
||||
font-weight: 600;
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -104,8 +104,14 @@ const handleKeyDown = () => {
|
||||
</div>
|
||||
|
||||
<nav class="nav-section">
|
||||
<t-button v-for="(item, index) in menuList" :key="index" :variant="menuActive == index ? 'base' : 'text'"
|
||||
:class="menuActive == index ? 'nav-button active' : 'nav-button'" block @click="handleClick(index)">
|
||||
<t-button
|
||||
v-for="(item, index) in menuList"
|
||||
:key="index"
|
||||
:variant="menuActive == index ? 'base' : 'text'"
|
||||
:class="menuActive == index ? 'nav-button active' : 'nav-button'"
|
||||
block
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
<i :class="`iconfont ${item.icon} nav-icon`"></i>
|
||||
{{ item.name }}
|
||||
</t-button>
|
||||
@@ -130,10 +136,20 @@ const handleKeyDown = () => {
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use :xlink:href="`#icon-${source}`"></use>
|
||||
</svg>
|
||||
<t-input v-model="keyword" placeholder="搜索音乐、歌手" style="width: 100%" @enter="handleKeyDown">
|
||||
<t-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索音乐、歌手"
|
||||
style="width: 100%"
|
||||
@enter="handleKeyDown"
|
||||
>
|
||||
<template #suffix>
|
||||
<t-button theme="primary" variant="text" shape="circle"
|
||||
style="display: flex; align-items: center; justify-content: center" @click="handleSearch">
|
||||
<t-button
|
||||
theme="primary"
|
||||
variant="text"
|
||||
shape="circle"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
@click="handleSearch"
|
||||
>
|
||||
<SearchIcon style="font-size: 16px; color: #000" />
|
||||
</t-button>
|
||||
</template>
|
||||
@@ -146,9 +162,11 @@ const handleKeyDown = () => {
|
||||
|
||||
<div class="mainContent">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<Transition name="page"
|
||||
<Transition
|
||||
name="page"
|
||||
:enter-active-class="`animate__animated ${route.meta.transitionIn} animate__fast`"
|
||||
:leave-active-class="`animate__animated ${route.meta.transitionOut} animate__fast`">
|
||||
:leave-active-class="`animate__animated ${route.meta.transitionOut} animate__fast`"
|
||||
>
|
||||
<KeepAlive exclude="list">
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
@@ -182,7 +200,7 @@ const handleKeyDown = () => {
|
||||
|
||||
.sidebar {
|
||||
width: 15rem;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -140vh, #ffffff 30vh);
|
||||
border-right: 0.0625rem solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -270,7 +288,8 @@ const handleKeyDown = () => {
|
||||
|
||||
.content {
|
||||
padding: 0;
|
||||
background: #f6f6f6;
|
||||
background-image: linear-gradient(to bottom, var(--td-brand-color-4) -110vh, #ffffff 15vh);
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@@ -309,12 +328,16 @@ const handleKeyDown = () => {
|
||||
-webkit-app-region: no-drag;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: width 0.3s;
|
||||
padding: 0 0.5rem;
|
||||
width: 18.75rem;
|
||||
width: min(18.75rem, 400px);
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 1.25rem !important;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
&:has(input:focus) {
|
||||
width: max(18.75rem, 400px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-input) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch,WatchHandle, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, watch, WatchHandle, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
import { extractDominantColor } from '../../utils/colorExtractor'
|
||||
|
||||
// 路由实例
|
||||
const router = useRouter()
|
||||
|
||||
@@ -9,20 +11,25 @@ const router = useRouter()
|
||||
const recommendPlaylists: any = ref([])
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const mainColors = ref<any[]>([])
|
||||
const textColors = ref<string[]>([])
|
||||
|
||||
// 热门歌曲数据
|
||||
const hotSongs: any = ref([])
|
||||
|
||||
|
||||
let watchSource:WatchHandle |null = null
|
||||
let watchSource: WatchHandle | null = null
|
||||
// 获取热门歌单数据
|
||||
const fetchHotSonglist = async () => {
|
||||
const LocalUserDetail = LocalUserDetailStore()
|
||||
watchSource =watch(LocalUserDetail.userSource,()=>{
|
||||
if(LocalUserDetail.userSource.source){
|
||||
fetchHotSonglist()
|
||||
}
|
||||
},{deep:true})
|
||||
watchSource = watch(
|
||||
LocalUserDetail.userSource,
|
||||
() => {
|
||||
if (LocalUserDetail.userSource.source) {
|
||||
fetchHotSonglist()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
@@ -31,13 +38,12 @@ const fetchHotSonglist = async () => {
|
||||
const result = await window.api.music.requestSdk('getHotSonglist', {
|
||||
source: LocalUserDetail.userSource.source
|
||||
})
|
||||
|
||||
if (result && result.list) {
|
||||
recommendPlaylists.value = result.list.map((item: any) => ({
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
description: item.desc || '精选歌单',
|
||||
cover: item.img || 'https://via.placeholder.com/200x200/f97316/ffffff?text=歌单',
|
||||
cover: item.img,
|
||||
playCount: item.play_count, // 直接使用返回的格式化字符串
|
||||
author: item.author,
|
||||
total: item.total,
|
||||
@@ -45,6 +51,38 @@ const fetchHotSonglist = async () => {
|
||||
source: item.source
|
||||
}))
|
||||
}
|
||||
// 初始化主题色和文字颜色数组
|
||||
mainColors.value = Array.from({ length: recommendPlaylists.value.length }).map(() => '#55C277')
|
||||
textColors.value = Array.from({ length: recommendPlaylists.value.length }).map(() => '#fff')
|
||||
|
||||
// 异步获取每个封面的主题色和对应的文字颜色
|
||||
|
||||
const colorPromises = recommendPlaylists.value.map(async (item: any, index: number) => {
|
||||
try {
|
||||
const color = await extractDominantColor(item.cover)
|
||||
// const textColor = await getBestContrastTextColor(item.cover)
|
||||
return { index, color }
|
||||
} catch (error) {
|
||||
console.warn(`获取封面主题色失败 (索引 ${index}):`, error)
|
||||
textColors.value[index] = '#000'
|
||||
return { index, color: '#fff' }
|
||||
}
|
||||
})
|
||||
|
||||
// 等待所有颜色提取完成
|
||||
const results = await Promise.all(colorPromises)
|
||||
|
||||
// 更新主题色和文字颜色数组
|
||||
results.forEach(({ index, color }) => {
|
||||
if (index < mainColors.value.length) {
|
||||
// 深化颜色值,让颜色更深邃
|
||||
const deepR = Math.floor(color.r * 0.7)
|
||||
const deepG = Math.floor(color.g * 0.7)
|
||||
const deepB = Math.floor(color.b * 0.7)
|
||||
mainColors.value[index] = `rgba(${deepR}, ${deepG}, ${deepB}, 0.85)`
|
||||
// textColors.value[index] = textColor
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('获取热门歌单失败:', err)
|
||||
error.value = '获取数据失败,请稍后重试'
|
||||
@@ -78,8 +116,8 @@ const playSong = (song: any): void => {
|
||||
onMounted(() => {
|
||||
fetchHotSonglist()
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
if(watchSource){
|
||||
onUnmounted(() => {
|
||||
if (watchSource) {
|
||||
watchSource()
|
||||
}
|
||||
})
|
||||
@@ -92,7 +130,6 @@ onUnmounted(()=>{
|
||||
<h2>发现音乐</h2>
|
||||
<p>探索最新最热的音乐内容</p>
|
||||
</div>
|
||||
|
||||
<!-- 推荐歌单 -->
|
||||
<div class="section">
|
||||
<h3 class="section-title">热门歌单Top{{ recommendPlaylists.length }}</h3>
|
||||
@@ -113,7 +150,7 @@ onUnmounted(()=>{
|
||||
<!-- 歌单列表 -->
|
||||
<div v-else class="playlist-grid">
|
||||
<div
|
||||
v-for="playlist in recommendPlaylists"
|
||||
v-for="(playlist, index) in recommendPlaylists"
|
||||
:key="playlist.id"
|
||||
class="playlist-card"
|
||||
@click="playPlaylist(playlist)"
|
||||
@@ -121,15 +158,27 @@ onUnmounted(()=>{
|
||||
<div class="playlist-cover">
|
||||
<img :src="playlist.cover" :alt="playlist.title" />
|
||||
</div>
|
||||
<div class="playlist-info">
|
||||
<h4 class="playlist-title">{{ playlist.title }}</h4>
|
||||
<p class="playlist-desc">{{ playlist.description }}</p>
|
||||
<div
|
||||
class="playlist-info"
|
||||
:style="{
|
||||
'background-color': mainColors[index],
|
||||
color: textColors[index]
|
||||
}"
|
||||
>
|
||||
<h4 class="playlist-title" :style="{ color: textColors[index] }">
|
||||
{{ playlist.title }}
|
||||
</h4>
|
||||
<p class="playlist-desc" :style="{ color: textColors[index] }">
|
||||
{{ playlist.description }}
|
||||
</p>
|
||||
<div class="playlist-meta">
|
||||
<span class="play-count">
|
||||
<span class="play-count" :style="{ color: textColors[index] }">
|
||||
<i class="iconfont icon-bofang"></i>
|
||||
{{ playlist.playCount }}
|
||||
</span>
|
||||
<span class="song-count" v-if="playlist.total">{{ playlist.total }}首</span>
|
||||
<span class="song-count" v-if="playlist.total" :style="{ color: textColors[index] }"
|
||||
>{{ playlist.total }}首</span
|
||||
>
|
||||
</div>
|
||||
<!-- <div class="playlist-author">by {{ playlist.author }}</div> -->
|
||||
</div>
|
||||
@@ -212,68 +261,139 @@ onUnmounted(()=>{
|
||||
|
||||
.playlist-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1.5rem;
|
||||
gap: 1.25rem;
|
||||
|
||||
// 响应式grid列数
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
// 响应式断点优化
|
||||
@media (max-width: 480px) {
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 481px) and (max-width: 768px) {
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
gap: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-card {
|
||||
// 卡片样式
|
||||
background: #fff;
|
||||
border-radius: 0.75rem;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
box-shadow:
|
||||
0 2px 8px rgba(0, 0, 0, 0.06),
|
||||
0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
// 现代化悬浮效果
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow:
|
||||
0 8px 25px rgba(0, 0, 0, 0.12),
|
||||
0 4px 10px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.play-overlay {
|
||||
.playlist-cover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
}
|
||||
|
||||
// 活跃状态
|
||||
&:active {
|
||||
transform: translateY(-2px) scale(1.01);
|
||||
}
|
||||
|
||||
.playlist-cover {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
|
||||
// 悬浮遮罩层
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.3) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
// 图片悬浮缩放效果
|
||||
&:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
padding: 1rem;
|
||||
padding: 1.25rem 1rem;
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(4px);
|
||||
transition: backdrop-filter 0.3s ease;
|
||||
|
||||
.playlist-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 0.25rem;
|
||||
white-space: nowrap;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 2.8rem; // 确保标题区域高度一致
|
||||
}
|
||||
|
||||
.playlist-desc {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
min-height: 2.625rem; // 确保描述区域高度一致
|
||||
}
|
||||
|
||||
.playlist-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto; // 推到底部
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
.play-count {
|
||||
@@ -282,21 +402,29 @@ onUnmounted(()=>{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-weight: 500;
|
||||
|
||||
.iconfont {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.song-count {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
font-weight: 500;
|
||||
background: rgba(156, 163, 175, 0.1);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.playlist-author {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
margin-top: 0.25rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, toRaw } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
import { downloadSingleSong } from '@renderer/utils/download'
|
||||
import SongVirtualList from '@renderer/components/Music/SongVirtualList.vue'
|
||||
@@ -53,12 +54,79 @@ const fetchPlaylistSongs = async () => {
|
||||
source: (route.query.source as string) || (LocalUserDetail.userSource.source as any)
|
||||
}
|
||||
|
||||
// 检查是否是本地歌单
|
||||
const isLocalPlaylist = route.query.type === 'local' || route.query.source === 'local'
|
||||
|
||||
if (isLocalPlaylist) {
|
||||
// 处理本地歌单
|
||||
await fetchLocalPlaylistSongs()
|
||||
} else {
|
||||
// 处理网络歌单
|
||||
await fetchNetworkPlaylistSongs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取歌单歌曲失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取本地歌单歌曲
|
||||
const fetchLocalPlaylistSongs = async () => {
|
||||
try {
|
||||
// 调用本地歌单API获取歌曲列表
|
||||
const result = await window.api.songList.getSongs(playlistInfo.value.id)
|
||||
|
||||
if (result.success && result.data) {
|
||||
songs.value = result.data.map((song: any) => ({
|
||||
singer: song.singer || '未知歌手',
|
||||
name: song.name || '未知歌曲',
|
||||
albumName: song.albumName || '未知专辑',
|
||||
albumId: song.albumId || 0,
|
||||
source: song.source || 'local',
|
||||
interval: song.interval || '0:00',
|
||||
songmid: song.songmid,
|
||||
img: song.img || '',
|
||||
lrc: song.lrc || null,
|
||||
types: song.types || [],
|
||||
_types: song._types || {},
|
||||
typeUrl: song.typeUrl || {}
|
||||
}))
|
||||
|
||||
// 更新歌单信息中的歌曲总数
|
||||
playlistInfo.value.total = songs.value.length
|
||||
|
||||
// 获取歌单详细信息
|
||||
const playlistResult = await window.api.songList.getById(playlistInfo.value.id)
|
||||
if (playlistResult.success && playlistResult.data) {
|
||||
const playlist = playlistResult.data
|
||||
playlistInfo.value = {
|
||||
...playlistInfo.value,
|
||||
title: playlist.name,
|
||||
cover: playlist.coverImgUrl || playlistInfo.value.cover,
|
||||
total: songs.value.length
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('获取本地歌单失败:', result.error)
|
||||
songs.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取本地歌单歌曲失败:', error)
|
||||
songs.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网络歌单歌曲
|
||||
const fetchNetworkPlaylistSongs = async () => {
|
||||
try {
|
||||
// 调用API获取歌单详情和歌曲列表
|
||||
const result = await window.api.music.requestSdk('getPlaylistDetail', {
|
||||
const result = (await window.api.music.requestSdk('getPlaylistDetail', {
|
||||
source: playlistInfo.value.source,
|
||||
id: playlistInfo.value.id,
|
||||
page: 1
|
||||
}) as any
|
||||
})) as any
|
||||
|
||||
console.log(result)
|
||||
if (result && result.list) {
|
||||
songs.value = result.list
|
||||
@@ -78,9 +146,8 @@ const fetchPlaylistSongs = async () => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取歌单歌曲失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
console.error('获取网络歌单失败:', error)
|
||||
songs.value = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +201,92 @@ const handleAddToPlaylist = (song: MusicItem) => {
|
||||
;(window as any).musicEmitter.emit('addToPlaylistEnd', toRaw(song))
|
||||
}
|
||||
}
|
||||
|
||||
// 替换播放列表的通用函数
|
||||
const replacePlaylist = (songsToReplace: MusicItem[], shouldShuffle = false) => {
|
||||
if (!(window as any).musicEmitter) {
|
||||
MessagePlugin.error('播放器未初始化')
|
||||
return
|
||||
}
|
||||
|
||||
let finalSongs = [...songsToReplace]
|
||||
|
||||
if (shouldShuffle) {
|
||||
// 创建歌曲索引数组并打乱
|
||||
const shuffledIndexes = Array.from({ length: songsToReplace.length }, (_, i) => i)
|
||||
for (let i = shuffledIndexes.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[shuffledIndexes[i], shuffledIndexes[j]] = [shuffledIndexes[j], shuffledIndexes[i]]
|
||||
}
|
||||
|
||||
// 按打乱的顺序重新排列歌曲
|
||||
finalSongs = shuffledIndexes.map((index) => songsToReplace[index])
|
||||
}
|
||||
|
||||
// 使用自定义事件替换整个播放列表
|
||||
if ((window as any).musicEmitter) {
|
||||
;(window as any).musicEmitter.emit(
|
||||
'replacePlaylist',
|
||||
finalSongs.map((song) => toRaw(song))
|
||||
)
|
||||
}
|
||||
|
||||
// // 更新当前播放状态
|
||||
// if (finalSongs[0]) {
|
||||
// currentSong.value = finalSongs[0]
|
||||
// isPlaying.value = true
|
||||
// }
|
||||
// const playerSong = inject('PlaySong',(...args:any)=>args)
|
||||
// nextTick(()=>{
|
||||
// playerSong(finalSongs[0])
|
||||
// })
|
||||
MessagePlugin.success(`请稍等歌曲加载完成播放`)
|
||||
}
|
||||
|
||||
// 播放整个歌单
|
||||
const handlePlayPlaylist = () => {
|
||||
if (songs.value.length === 0) {
|
||||
MessagePlugin.warning('歌单为空,无法播放')
|
||||
return
|
||||
}
|
||||
|
||||
const dialog = DialogPlugin.confirm({
|
||||
header: '播放歌单',
|
||||
body: `确定要用歌单"${playlistInfo.value.title}"中的 ${songs.value.length} 首歌曲替换当前播放列表吗?`,
|
||||
confirmBtn: '确定替换',
|
||||
cancelBtn: '取消',
|
||||
onConfirm: () => {
|
||||
console.log('播放歌单:', playlistInfo.value.title)
|
||||
replacePlaylist(songs.value, false)
|
||||
dialog.destroy()
|
||||
},
|
||||
onCancel: () => {
|
||||
dialog.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
// 随机播放歌单
|
||||
const handleShufflePlaylist = () => {
|
||||
if (songs.value.length === 0) {
|
||||
MessagePlugin.warning('歌单为空,无法播放')
|
||||
return
|
||||
}
|
||||
|
||||
const dialog = DialogPlugin.confirm({
|
||||
header: '随机播放歌单',
|
||||
body: `确定要用歌单"${playlistInfo.value.title}"中的 ${songs.value.length} 首歌曲随机替换当前播放列表吗?`,
|
||||
confirmBtn: '确定替换',
|
||||
cancelBtn: '取消',
|
||||
onConfirm: () => {
|
||||
console.log('随机播放歌单:', playlistInfo.value.title)
|
||||
replacePlaylist(songs.value, true)
|
||||
dialog.destroy()
|
||||
},
|
||||
onCancel: () => {
|
||||
dialog.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchPlaylistSongs()
|
||||
@@ -153,6 +306,41 @@ onMounted(() => {
|
||||
<h1 class="playlist-title">{{ playlistInfo.title }}</h1>
|
||||
<p class="playlist-author">by {{ playlistInfo.author }}</p>
|
||||
<p class="playlist-stats">{{ playlistInfo.total }} 首歌曲</p>
|
||||
|
||||
<!-- 播放控制按钮 -->
|
||||
<div class="playlist-actions">
|
||||
<t-button
|
||||
theme="primary"
|
||||
size="medium"
|
||||
@click="handlePlayPlaylist"
|
||||
:disabled="songs.length === 0 || loading"
|
||||
class="play-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</template>
|
||||
播放全部
|
||||
</t-button>
|
||||
|
||||
<t-button
|
||||
variant="outline"
|
||||
size="medium"
|
||||
@click="handleShufflePlaylist"
|
||||
:disabled="songs.length === 0 || loading"
|
||||
class="shuffle-btn"
|
||||
>
|
||||
<template #icon>
|
||||
<svg class="shuffle-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
随机播放
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,7 +478,24 @@ onMounted(() => {
|
||||
.playlist-stats {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.playlist-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.play-btn,
|
||||
.shuffle-btn {
|
||||
min-width: 120px;
|
||||
|
||||
.play-icon,
|
||||
.shuffle-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,6 +521,36 @@ onMounted(() => {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.playlist-details {
|
||||
.playlist-actions {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.play-btn,
|
||||
.shuffle-btn {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.playlist-header {
|
||||
.playlist-details {
|
||||
.playlist-actions {
|
||||
.play-btn,
|
||||
.shuffle-btn {
|
||||
.play-icon,
|
||||
.shuffle-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -105,10 +105,10 @@ async function setPic(offset: number, source: string) {
|
||||
if (typeof url !== 'object') {
|
||||
searchResults.value[i].img = url
|
||||
} else {
|
||||
searchResults.value[i].img = 'resources/logo.png'
|
||||
searchResults.value[i].img = ''
|
||||
}
|
||||
} catch (e) {
|
||||
searchResults.value[i].img = 'logo.svg'
|
||||
searchResults.value[i].img = ''
|
||||
console.log('获取失败 index' + i, e)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
<div class="logo-section">
|
||||
<div class="image-container">
|
||||
<div class="image-bg"></div>
|
||||
<img class="logo-image" src="/logo.svg" alt="Ceru Music Logo">
|
||||
<img class="logo-image" src="/logo.svg" alt="Ceru Music Logo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -181,7 +181,7 @@ onMounted(async () => {
|
||||
|
||||
.tag {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: #B8F1CE;
|
||||
background: #b8f1ce;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
@@ -309,4 +309,4 @@ onMounted(async () => {
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -7,4 +7,4 @@ export interface CacheInfo {
|
||||
export interface CacheOperationResult {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
216
src/types/songList.ts
Normal file
216
src/types/songList.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import type { SongList, Songs } from '@common/types/songList'
|
||||
|
||||
// IPC 响应基础类型
|
||||
export interface IPCResponse<T = any> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
message?: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
// 歌单管理相关类型定义
|
||||
export interface SongListAPI {
|
||||
// === 歌单管理 ===
|
||||
|
||||
/**
|
||||
* 创建新歌单
|
||||
*/
|
||||
create(
|
||||
name: string,
|
||||
description?: string,
|
||||
source?: SongList['source']
|
||||
): Promise<IPCResponse<{ id: string }>>
|
||||
|
||||
/**
|
||||
* 获取所有歌单
|
||||
*/
|
||||
getAll(): Promise<IPCResponse<SongList[]>>
|
||||
|
||||
/**
|
||||
* 根据ID获取歌单信息
|
||||
*/
|
||||
getById(hashId: string): Promise<IPCResponse<SongList | null>>
|
||||
|
||||
/**
|
||||
* 删除歌单
|
||||
*/
|
||||
delete(hashId: string): Promise<IPCResponse>
|
||||
|
||||
/**
|
||||
* 批量删除歌单
|
||||
*/
|
||||
batchDelete(hashIds: string[]): Promise<IPCResponse<{ success: string[]; failed: string[] }>>
|
||||
|
||||
/**
|
||||
* 编辑歌单信息
|
||||
*/
|
||||
edit(hashId: string, updates: Partial<Omit<SongList, 'id' | 'createTime'>>): Promise<IPCResponse>
|
||||
|
||||
/**
|
||||
* 更新歌单封面
|
||||
*/
|
||||
updateCover(hashId: string, coverImgUrl: string): Promise<IPCResponse>
|
||||
|
||||
/**
|
||||
* 搜索歌单
|
||||
*/
|
||||
search(keyword: string, source?: SongList['source']): Promise<IPCResponse<SongList[]>>
|
||||
|
||||
/**
|
||||
* 获取歌单统计信息
|
||||
*/
|
||||
getStatistics(): Promise<
|
||||
IPCResponse<{
|
||||
total: number
|
||||
bySource: Record<string, number>
|
||||
lastUpdated: string
|
||||
}>
|
||||
>
|
||||
|
||||
/**
|
||||
* 检查歌单是否存在
|
||||
*/
|
||||
exists(hashId: string): Promise<IPCResponse<boolean>>
|
||||
|
||||
// === 歌曲管理 ===
|
||||
|
||||
/**
|
||||
* 添加歌曲到歌单
|
||||
*/
|
||||
addSongs(hashId: string, songs: Songs[]): Promise<IPCResponse>
|
||||
|
||||
/**
|
||||
* 从歌单移除歌曲
|
||||
*/
|
||||
removeSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>>
|
||||
|
||||
/**
|
||||
* 批量移除歌曲
|
||||
*/
|
||||
removeSongs(
|
||||
hashId: string,
|
||||
songmids: (string | number)[]
|
||||
): Promise<IPCResponse<{ removed: number; notFound: number }>>
|
||||
|
||||
/**
|
||||
* 清空歌单
|
||||
*/
|
||||
clearSongs(hashId: string): Promise<IPCResponse>
|
||||
|
||||
/**
|
||||
* 获取歌单中的歌曲列表
|
||||
*/
|
||||
getSongs(hashId: string): Promise<IPCResponse<readonly Songs[]>>
|
||||
|
||||
/**
|
||||
* 获取歌单歌曲数量
|
||||
*/
|
||||
getSongCount(hashId: string): Promise<IPCResponse<number>>
|
||||
|
||||
/**
|
||||
* 检查歌曲是否在歌单中
|
||||
*/
|
||||
hasSong(hashId: string, songmid: string | number): Promise<IPCResponse<boolean>>
|
||||
|
||||
/**
|
||||
* 根据ID获取歌曲
|
||||
*/
|
||||
getSong(hashId: string, songmid: string | number): Promise<IPCResponse<Songs | null>>
|
||||
|
||||
/**
|
||||
* 搜索歌单中的歌曲
|
||||
*/
|
||||
searchSongs(hashId: string, keyword: string): Promise<IPCResponse<Songs[]>>
|
||||
|
||||
/**
|
||||
* 获取歌单歌曲统计信息
|
||||
*/
|
||||
getSongStatistics(hashId: string): Promise<
|
||||
IPCResponse<{
|
||||
total: number
|
||||
bySinger: Record<string, number>
|
||||
byAlbum: Record<string, number>
|
||||
lastModified: string
|
||||
}>
|
||||
>
|
||||
|
||||
/**
|
||||
* 验证歌单完整性
|
||||
*/
|
||||
validateIntegrity(hashId: string): Promise<IPCResponse<{ isValid: boolean; issues: string[] }>>
|
||||
|
||||
/**
|
||||
* 修复歌单数据
|
||||
*/
|
||||
repairData(hashId: string): Promise<IPCResponse<{ fixed: boolean; changes: string[] }>>
|
||||
|
||||
/**
|
||||
* 强制保存歌单
|
||||
*/
|
||||
forceSave(hashId: string): Promise<IPCResponse>
|
||||
}
|
||||
|
||||
// 错误码枚举
|
||||
export enum SongListErrorCode {
|
||||
INVALID_HASH_ID = 'INVALID_HASH_ID',
|
||||
EMPTY_NAME = 'EMPTY_NAME',
|
||||
EMPTY_SOURCE = 'EMPTY_SOURCE',
|
||||
CREATE_FAILED = 'CREATE_FAILED',
|
||||
FILE_CREATE_FAILED = 'FILE_CREATE_FAILED',
|
||||
PLAYLIST_NOT_FOUND = 'PLAYLIST_NOT_FOUND',
|
||||
DELETE_FAILED = 'DELETE_FAILED',
|
||||
EMPTY_UPDATES = 'EMPTY_UPDATES',
|
||||
EDIT_FAILED = 'EDIT_FAILED',
|
||||
READ_FAILED = 'READ_FAILED',
|
||||
INIT_FAILED = 'INIT_FAILED',
|
||||
WRITE_FAILED = 'WRITE_FAILED',
|
||||
INVALID_ACTION = 'INVALID_ACTION',
|
||||
UPDATE_COVER_FAILED = 'UPDATE_COVER_FAILED',
|
||||
INVALID_DATA_FORMAT = 'INVALID_DATA_FORMAT',
|
||||
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
|
||||
INVALID_SONG_ID = 'INVALID_SONG_ID',
|
||||
SAVE_FAILED = 'SAVE_FAILED',
|
||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
|
||||
}
|
||||
|
||||
// 歌单来源类型
|
||||
export type SongListSource = 'local' | 'wy' | 'tx' | 'mg' | 'kg' | 'kw'
|
||||
|
||||
// 批量操作结果类型
|
||||
export interface BatchOperationResult {
|
||||
success: string[]
|
||||
failed: string[]
|
||||
}
|
||||
|
||||
// 歌曲移除结果类型
|
||||
export interface RemoveSongsResult {
|
||||
removed: number
|
||||
notFound: number
|
||||
}
|
||||
|
||||
// 统计信息类型
|
||||
export interface SongListStatistics {
|
||||
total: number
|
||||
bySource: Record<string, number>
|
||||
lastUpdated: string
|
||||
}
|
||||
|
||||
export interface SongStatistics {
|
||||
total: number
|
||||
bySinger: Record<string, number>
|
||||
byAlbum: Record<string, number>
|
||||
lastModified: string
|
||||
}
|
||||
|
||||
// 数据完整性检查结果
|
||||
export interface IntegrityCheckResult {
|
||||
isValid: boolean
|
||||
issues: string[]
|
||||
}
|
||||
|
||||
// 数据修复结果
|
||||
export interface RepairResult {
|
||||
fixed: boolean
|
||||
changes: string[]
|
||||
}
|
||||
@@ -3,7 +3,13 @@
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/common/**/*", "src/types/**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["electron-vite/node"],
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@common/*":[
|
||||
"src/common/*"
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"src/renderer/src/**/*",
|
||||
"src/renderer/src/**/*.vue",
|
||||
"src/preload/*.d.ts",
|
||||
"src/types/**/*"
|
||||
"src/types/**/*",
|
||||
"src/common/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
3479
website/CeruUse.html
3479
website/CeruUse.html
File diff suppressed because it is too large
Load Diff
4190
website/design.html
4190
website/design.html
File diff suppressed because one or more lines are too long
@@ -1,258 +1,267 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Ceru Music - 跨平台音乐播放器</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<div class="nav-logo">
|
||||
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img">
|
||||
<span class="logo-text">Ceru Music</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="#features">功能特色</a>
|
||||
<a href="#download">下载</a>
|
||||
<a href="./CeruUse.html" target="_blank">文档</a>
|
||||
</div>
|
||||
<div class="nav-container">
|
||||
<div class="nav-logo">
|
||||
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img" />
|
||||
<span class="logo-text">Ceru Music</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="#features">功能特色</a>
|
||||
<a href="#download">下载</a>
|
||||
<a href="./CeruUse.html" target="_blank">文档</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="hero-container">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">
|
||||
<span class="gradient-text">Ceru Music</span>
|
||||
<br>跨平台音乐播放器
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
集成多平台音乐源,提供优雅的桌面音乐体验。支持网易云音乐、QQ音乐等多个平台,让你的音乐世界更加丰富。
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
<button class="btn btn-primary" onclick="scrollToDownload()">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7,10 12,15 17,10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
立即下载
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="scrollToFeatures()">
|
||||
了解更多
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-image">
|
||||
<div class="app-preview">
|
||||
<div class="app-window">
|
||||
<div class="window-header">
|
||||
<div class="window-controls">
|
||||
<span class="control close"></span>
|
||||
<span class="control minimize"></span>
|
||||
<span class="control maximize"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
<div class="music-player-preview">
|
||||
<div class="album-art"></div>
|
||||
<div class="player-info">
|
||||
<div class="song-title"></div>
|
||||
<div class="artist-name"></div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-container">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">
|
||||
<span class="gradient-text">Ceru Music</span>
|
||||
<br />跨平台音乐播放器
|
||||
</h1>
|
||||
<p class="hero-description">
|
||||
集成多平台音乐源,提供优雅的桌面音乐体验。支持网易云音乐、QQ音乐等多个平台,让你的音乐世界更加丰富。
|
||||
</p>
|
||||
<div class="hero-buttons">
|
||||
<button class="btn btn-primary" onclick="scrollToDownload()">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
立即下载
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="scrollToFeatures()">了解更多</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-image">
|
||||
<div class="app-preview">
|
||||
<div class="app-window">
|
||||
<div class="window-header">
|
||||
<div class="window-controls">
|
||||
<span class="control close"></span>
|
||||
<span class="control minimize"></span>
|
||||
<span class="control maximize"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
<div class="music-player-preview">
|
||||
<div class="album-art"></div>
|
||||
<div class="player-info">
|
||||
<div class="song-title"></div>
|
||||
<div class="artist-name"></div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="features">
|
||||
<div class="container">
|
||||
<h2 class="section-title">功能特色</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 1v6m0 6v6"/>
|
||||
<path d="m21 12-6-3-6 3-6-3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>多平台音源</h3>
|
||||
<p>支持网易云音乐、QQ音乐等多个平台,一站式访问海量音乐资源</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>跨平台支持</h3>
|
||||
<p>原生桌面应用,支持 Windows、macOS、Linux 三大操作系统</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M9 18V5l12-2v13"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
<circle cx="18" cy="16" r="3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>歌词显示</h3>
|
||||
<p>实时歌词显示,支持专辑信息获取,让音乐体验更加丰富</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>性能优化</h3>
|
||||
<p>虚拟滚动技术,轻松处理大型音乐列表,流畅的用户体验</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>本地播放列表</h3>
|
||||
<p>创建和管理个人播放列表,本地数据存储,个性化音乐体验</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>优雅界面</h3>
|
||||
<p>现代化设计语言,流畅动画效果,为你带来愉悦的视觉体验</p>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h2 class="section-title">功能特色</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 1v6m0 6v6" />
|
||||
<path d="m21 12-6-3-6 3-6-3" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>多平台音源</h3>
|
||||
<p>支持网易云音乐、QQ音乐等多个平台,一站式访问海量音乐资源</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>跨平台支持</h3>
|
||||
<p>原生桌面应用,支持 Windows、macOS、Linux 三大操作系统</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M9 18V5l12-2v13" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<circle cx="18" cy="16" r="3" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>歌词显示</h3>
|
||||
<p>实时歌词显示,支持专辑信息获取,让音乐体验更加丰富</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>性能优化</h3>
|
||||
<p>虚拟滚动技术,轻松处理大型音乐列表,流畅的用户体验</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>本地播放列表</h3>
|
||||
<p>创建和管理个人播放列表,本地数据存储,个性化音乐体验</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path
|
||||
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>优雅界面</h3>
|
||||
<p>现代化设计语言,流畅动画效果,为你带来愉悦的视觉体验</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Download Section -->
|
||||
<section id="download" class="download">
|
||||
<div class="container">
|
||||
<h2 class="section-title">立即下载</h2>
|
||||
<p class="section-subtitle">选择适合你操作系统的版本</p>
|
||||
<div class="download-cards">
|
||||
<div class="download-card">
|
||||
<div class="platform-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M0 12v-2h24v2H12.5c-.2 0-.5.2-.5.5s.3.5.5.5H24v2H0v-2h11.5c.2 0 .5-.2.5-.5s-.3-.5-.5-.5H0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Windows</h3>
|
||||
<p>Windows 10/11 (64-bit)</p>
|
||||
<button class="btn btn-download" onclick="downloadApp('windows')">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7,10 12,15 17,10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
下载 .exe
|
||||
</button>
|
||||
</div>
|
||||
<div class="download-card">
|
||||
<div class="platform-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12.5 2C13.3 2 14 2.7 14 3.5S13.3 5 12.5 5 11 4.3 11 3.5 11.7 2 12.5 2M21 9H15L13.5 7.5C13.1 7.1 12.6 6.9 12 6.9S10.9 7.1 10.5 7.5L9 9H3C1.9 9 1 9.9 1 11V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V11C23 9.9 22.1 9 21 9Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>macOS</h3>
|
||||
<p>macOS 10.15+ (Intel & Apple Silicon)</p>
|
||||
<button class="btn btn-download" onclick="downloadApp('macos')">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7,10 12,15 17,10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
下载 .dmg
|
||||
</button>
|
||||
</div>
|
||||
<div class="download-card">
|
||||
<div class="platform-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2M21 9V7L15 1H5C3.9 1 3 1.9 3 3V21C3 22.1 3.9 23 5 23H19C20.1 23 21 22.1 21 21V9H21Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Linux</h3>
|
||||
<p>Ubuntu 18.04+ / Debian 10+</p>
|
||||
<button class="btn btn-download" onclick="downloadApp('linux')">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7,10 12,15 17,10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
下载 .AppImage
|
||||
</button>
|
||||
</div>
|
||||
<div class="container">
|
||||
<h2 class="section-title">立即下载</h2>
|
||||
<p class="section-subtitle">选择适合你操作系统的版本</p>
|
||||
<div class="download-cards">
|
||||
<div class="download-card">
|
||||
<div class="platform-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M0 12v-2h24v2H12.5c-.2 0-.5.2-.5.5s.3.5.5.5H24v2H0v-2h11.5c.2 0 .5-.2.5-.5s-.3-.5-.5-.5H0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<p>当前版本: <span class="version">v1.0.0</span> | 更新时间: 2024年12月</p>
|
||||
<h3>Windows</h3>
|
||||
<p>Windows 10/11 (64-bit)</p>
|
||||
<button class="btn btn-download" onclick="downloadApp('windows')">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
下载 .exe
|
||||
</button>
|
||||
</div>
|
||||
<div class="download-card">
|
||||
<div class="platform-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12.5 2C13.3 2 14 2.7 14 3.5S13.3 5 12.5 5 11 4.3 11 3.5 11.7 2 12.5 2M21 9H15L13.5 7.5C13.1 7.1 12.6 6.9 12 6.9S10.9 7.1 10.5 7.5L9 9H3C1.9 9 1 9.9 1 11V19C1 20.1 1.9 21 3 21H21C22.1 21 23 20.1 23 19V11C23 9.9 22.1 9 21 9Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>macOS</h3>
|
||||
<p>macOS 10.15+ (Intel & Apple Silicon)</p>
|
||||
<button class="btn btn-download" onclick="downloadApp('macos')">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
下载 .dmg
|
||||
</button>
|
||||
</div>
|
||||
<div class="download-card">
|
||||
<div class="platform-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2M21 9V7L15 1H5C3.9 1 3 1.9 3 3V21C3 22.1 3.9 23 5 23H19C20.1 23 21 22.1 21 21V9H21Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Linux</h3>
|
||||
<p>Ubuntu 18.04+ / Debian 10+</p>
|
||||
<button class="btn btn-download" onclick="downloadApp('linux')">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7,10 12,15 17,10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
下载 .AppImage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<p>当前版本: <span class="version">v1.0.0</span> | 更新时间: 2024年12月</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<div class="footer-logo">
|
||||
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img">
|
||||
<span class="logo-text">Ceru Music</span>
|
||||
</div>
|
||||
<p>跨平台音乐播放器,为你带来优雅的音乐体验</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>产品</h4>
|
||||
<ul>
|
||||
<li><a href="#features">功能特色</a></li>
|
||||
<li><a href="#download">下载</a></li>
|
||||
<li><a href="./design.html">设计文档</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>开发者</h4>
|
||||
<ul>
|
||||
<!-- <li><a href="../docs/api.md">API 文档</a></li> -->
|
||||
<li><a href="./pluginDev.html">插件开发</a></li>
|
||||
<li><a href="https://github.com/timeshiftsauce">GitHub</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>支持</h4>
|
||||
<ul>
|
||||
<li><a href="./CeruUse.html">使用文档</a></li>
|
||||
<li><a href="#contact">联系我们</a></li>
|
||||
<li><a href="#feedback">反馈建议</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2025 Ceru Music. All rights reserved.</p>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<div class="footer-logo">
|
||||
<img src="./resources/logo.svg" alt="Ceru Music" class="logo-img" />
|
||||
<span class="logo-text">Ceru Music</span>
|
||||
</div>
|
||||
<p>跨平台音乐播放器,为你带来优雅的音乐体验</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>产品</h4>
|
||||
<ul>
|
||||
<li><a href="#features">功能特色</a></li>
|
||||
<li><a href="#download">下载</a></li>
|
||||
<li><a href="./design.html">设计文档</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>开发者</h4>
|
||||
<ul>
|
||||
<!-- <li><a href="../docs/api.md">API 文档</a></li> -->
|
||||
<li><a href="./pluginDev.html">插件开发</a></li>
|
||||
<li><a href="https://github.com/timeshiftsauce">GitHub</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h4>支持</h4>
|
||||
<ul>
|
||||
<li><a href="./CeruUse.html">使用文档</a></li>
|
||||
<li><a href="#contact">联系我们</a></li>
|
||||
<li><a href="#feedback">反馈建议</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2025 Ceru Music. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
1018
website/script.js
1018
website/script.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
618
yarn.lock
618
yarn.lock
@@ -327,6 +327,16 @@
|
||||
ajv "^6.12.0"
|
||||
ajv-keywords "^3.4.1"
|
||||
|
||||
"@docsearch/css@^4.0.0-beta.7":
|
||||
version "4.0.0-beta.8"
|
||||
resolved "https://registry.npmmirror.com/@docsearch/css/-/css-4.0.0-beta.8.tgz#836ac7c3eeecf87cfc9c518210f4dfd27e49b05f"
|
||||
integrity sha512-/ZlyvZCjIJM4aaOYoJpVNHPJckX7J5KIbt6IWjnZXvo0QAUI1aH976vKEJUC9olgUbE3LWafB8yuX4qoqahIQg==
|
||||
|
||||
"@docsearch/js@^4.0.0-beta.7":
|
||||
version "4.0.0-beta.8"
|
||||
resolved "https://registry.npmmirror.com/@docsearch/js/-/js-4.0.0-beta.8.tgz#eca030c793fad34487c0c1c9147112544edb02bc"
|
||||
integrity sha512-elgqPYpykRQr5MlfqoO8U2uC3BcPgjUQhzmHt/H4lSzP7khJ9Jpv/cCB4tiZreXb6GkdRgWr5csiItNq6jjnhg==
|
||||
|
||||
"@electron-toolkit/eslint-config-prettier@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/@electron-toolkit/eslint-config-prettier/-/eslint-config-prettier-3.0.0.tgz"
|
||||
@@ -673,6 +683,18 @@
|
||||
resolved "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz"
|
||||
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
|
||||
|
||||
"@iconify-json/simple-icons@^1.2.47":
|
||||
version "1.2.50"
|
||||
resolved "https://registry.npmmirror.com/@iconify-json/simple-icons/-/simple-icons-1.2.50.tgz#62475b9a6b82e1b55d78ac5c769f550c7b2bc932"
|
||||
integrity sha512-Z2ggRwKYEBB9eYAEi4NqEgIzyLhu0Buh4+KGzMPD6+xG7mk52wZJwLT/glDPtfslV503VtJbqzWqBUGkCMKOFA==
|
||||
dependencies:
|
||||
"@iconify/types" "*"
|
||||
|
||||
"@iconify/types@*":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57"
|
||||
integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
|
||||
|
||||
"@img/sharp-darwin-arm64@0.33.5":
|
||||
version "0.33.5"
|
||||
resolved "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz"
|
||||
@@ -1121,7 +1143,7 @@
|
||||
resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
|
||||
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
|
||||
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
|
||||
version "1.5.5"
|
||||
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
|
||||
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
|
||||
@@ -1568,6 +1590,68 @@
|
||||
resolved "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz"
|
||||
integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==
|
||||
|
||||
"@shikijs/core@3.12.2", "@shikijs/core@^3.9.2":
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/core/-/core-3.12.2.tgz#c1690b9d9d1f982491a59a1e01cb49bb5fe0c43c"
|
||||
integrity sha512-L1Safnhra3tX/oJK5kYHaWmLEBJi1irASwewzY3taX5ibyXyMkkSDZlq01qigjryOBwrXSdFgTiZ3ryzSNeu7Q==
|
||||
dependencies:
|
||||
"@shikijs/types" "3.12.2"
|
||||
"@shikijs/vscode-textmate" "^10.0.2"
|
||||
"@types/hast" "^3.0.4"
|
||||
hast-util-to-html "^9.0.5"
|
||||
|
||||
"@shikijs/engine-javascript@3.12.2":
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.12.2.tgz#c0badb74b4d5ded6f600b39f9abd3c83ab9f28a4"
|
||||
integrity sha512-Nm3/azSsaVS7hk6EwtHEnTythjQfwvrO5tKqMlaH9TwG1P+PNaR8M0EAKZ+GaH2DFwvcr4iSfTveyxMIvXEHMw==
|
||||
dependencies:
|
||||
"@shikijs/types" "3.12.2"
|
||||
"@shikijs/vscode-textmate" "^10.0.2"
|
||||
oniguruma-to-es "^4.3.3"
|
||||
|
||||
"@shikijs/engine-oniguruma@3.12.2":
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.2.tgz#3314a43666d751f80c0d1fc584029a3074af4e9a"
|
||||
integrity sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==
|
||||
dependencies:
|
||||
"@shikijs/types" "3.12.2"
|
||||
"@shikijs/vscode-textmate" "^10.0.2"
|
||||
|
||||
"@shikijs/langs@3.12.2":
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.12.2.tgz#f41f1e0eb940c5c41f1017f8765be969e74a0c9b"
|
||||
integrity sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==
|
||||
dependencies:
|
||||
"@shikijs/types" "3.12.2"
|
||||
|
||||
"@shikijs/themes@3.12.2":
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.12.2.tgz#e4b9b636669658576cdc532ebe1c405cadba6272"
|
||||
integrity sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==
|
||||
dependencies:
|
||||
"@shikijs/types" "3.12.2"
|
||||
|
||||
"@shikijs/transformers@^3.9.2":
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-3.12.2.tgz#0cc01cba547f9a1dff550bec20c047eb284fa39d"
|
||||
integrity sha512-+z1aMq4N5RoNGY8i7qnTYmG2MBYzFmwkm/yOd6cjEI7OVzcldVvzQCfxU1YbIVgsyB0xHVc2jFe1JhgoXyUoSQ==
|
||||
dependencies:
|
||||
"@shikijs/core" "3.12.2"
|
||||
"@shikijs/types" "3.12.2"
|
||||
|
||||
"@shikijs/types@3.12.2", "@shikijs/types@^3.9.2":
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/types/-/types-3.12.2.tgz#8f7f02a1fd67a93470aac9409d072880b3148612"
|
||||
integrity sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==
|
||||
dependencies:
|
||||
"@shikijs/vscode-textmate" "^10.0.2"
|
||||
"@types/hast" "^3.0.4"
|
||||
|
||||
"@shikijs/vscode-textmate@^10.0.2":
|
||||
version "10.0.2"
|
||||
resolved "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
|
||||
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
|
||||
|
||||
"@sindresorhus/is@^4.0.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/@sindresorhus/is/-/is-4.6.0.tgz"
|
||||
@@ -1738,6 +1822,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/hast@^3.0.0", "@types/hast@^3.0.4":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
|
||||
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
|
||||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/http-cache-semantics@*":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmmirror.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz"
|
||||
@@ -1755,6 +1846,11 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/linkify-it@^5":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76"
|
||||
integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==
|
||||
|
||||
"@types/lodash-es@^4.17.12":
|
||||
version "4.17.12"
|
||||
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz"
|
||||
@@ -1767,6 +1863,26 @@
|
||||
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz"
|
||||
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==
|
||||
|
||||
"@types/markdown-it@^14.1.2":
|
||||
version "14.1.2"
|
||||
resolved "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61"
|
||||
integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==
|
||||
dependencies:
|
||||
"@types/linkify-it" "^5"
|
||||
"@types/mdurl" "^2"
|
||||
|
||||
"@types/mdast@^4.0.0":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
|
||||
integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
|
||||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/mdurl@^2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
|
||||
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
|
||||
|
||||
"@types/ms@*":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz"
|
||||
@@ -1839,6 +1955,11 @@
|
||||
resolved "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz"
|
||||
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||
|
||||
"@types/unist@*", "@types/unist@^3.0.0":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
|
||||
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
|
||||
|
||||
"@types/uuid@^10.0.0":
|
||||
version "10.0.0"
|
||||
resolved "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz"
|
||||
@@ -1854,6 +1975,11 @@
|
||||
resolved "https://registry.npmmirror.com/@types/verror/-/verror-1.10.11.tgz"
|
||||
integrity sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==
|
||||
|
||||
"@types/web-bluetooth@^0.0.21":
|
||||
version "0.0.21"
|
||||
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63"
|
||||
integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.10.3"
|
||||
resolved "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz"
|
||||
@@ -1959,7 +2085,12 @@
|
||||
"@typescript-eslint/types" "8.39.1"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@vitejs/plugin-vue@^6.0.0":
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
|
||||
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
|
||||
|
||||
"@vitejs/plugin-vue@^6.0.0", "@vitejs/plugin-vue@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz"
|
||||
integrity sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==
|
||||
@@ -2029,6 +2160,17 @@
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
"@vue/compiler-core@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.21.tgz#5915b19273f0492336f0beb227aba86813e2c8a8"
|
||||
integrity sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.28.3"
|
||||
"@vue/shared" "3.5.21"
|
||||
entities "^4.5.0"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
"@vue/compiler-dom@3.5.18", "@vue/compiler-dom@^3.3.4", "@vue/compiler-dom@^3.5.0":
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz"
|
||||
@@ -2037,7 +2179,30 @@
|
||||
"@vue/compiler-core" "3.5.18"
|
||||
"@vue/shared" "3.5.18"
|
||||
|
||||
"@vue/compiler-sfc@3.5.18", "@vue/compiler-sfc@^3.5.18":
|
||||
"@vue/compiler-dom@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz#26126447fe1e1d16c8cbac45b26e66b3f7175f65"
|
||||
integrity sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.5.21"
|
||||
"@vue/shared" "3.5.21"
|
||||
|
||||
"@vue/compiler-sfc@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz#e48189ef3ffe334c864c2625389ebe3bb4fa41eb"
|
||||
integrity sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.28.3"
|
||||
"@vue/compiler-core" "3.5.21"
|
||||
"@vue/compiler-dom" "3.5.21"
|
||||
"@vue/compiler-ssr" "3.5.21"
|
||||
"@vue/shared" "3.5.21"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.18"
|
||||
postcss "^8.5.6"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
"@vue/compiler-sfc@^3.5.18":
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz"
|
||||
integrity sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==
|
||||
@@ -2060,6 +2225,14 @@
|
||||
"@vue/compiler-dom" "3.5.18"
|
||||
"@vue/shared" "3.5.18"
|
||||
|
||||
"@vue/compiler-ssr@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz#f351c27aa5c075faa609596b2269c53df0df3aa1"
|
||||
integrity sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.21"
|
||||
"@vue/shared" "3.5.21"
|
||||
|
||||
"@vue/compiler-vue2@^2.7.16":
|
||||
version "2.7.16"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz"
|
||||
@@ -2080,6 +2253,13 @@
|
||||
dependencies:
|
||||
"@vue/devtools-kit" "^7.7.7"
|
||||
|
||||
"@vue/devtools-api@^8.0.0":
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-8.0.1.tgz#142f94161ec80698f57ee8379bffad969868b251"
|
||||
integrity sha512-YBvjfpM7LEp5+b7ZDm4+mFrC+TgGjUmN8ff9lZcbHQ1MKhmftT/urCTZP0y1j26YQWr25l9TPaEbNLbILRiGoQ==
|
||||
dependencies:
|
||||
"@vue/devtools-kit" "^8.0.1"
|
||||
|
||||
"@vue/devtools-core@^8.0.0":
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.0.0.tgz"
|
||||
@@ -2118,6 +2298,19 @@
|
||||
speakingurl "^14.0.1"
|
||||
superjson "^2.2.2"
|
||||
|
||||
"@vue/devtools-kit@^8.0.1":
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.1.tgz#84b8741bbd16fa7d6d5ce0baf3c2e8b3822237c2"
|
||||
integrity sha512-7kiPhgTKNtNeXltEHnJJjIDlndlJP4P+UJvCw54uVHNDlI6JzwrSiRmW4cxKTug2wDbc/dkGaMnlZghcwV+aWA==
|
||||
dependencies:
|
||||
"@vue/devtools-shared" "^8.0.1"
|
||||
birpc "^2.5.0"
|
||||
hookable "^5.5.3"
|
||||
mitt "^3.0.1"
|
||||
perfect-debounce "^1.0.0"
|
||||
speakingurl "^14.0.1"
|
||||
superjson "^2.2.2"
|
||||
|
||||
"@vue/devtools-shared@^7.7.7":
|
||||
version "7.7.7"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz"
|
||||
@@ -2132,6 +2325,13 @@
|
||||
dependencies:
|
||||
rfdc "^1.4.1"
|
||||
|
||||
"@vue/devtools-shared@^8.0.1":
|
||||
version "8.0.1"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.1.tgz#ace79d39fec42f35e02d1053d5c83c4f492423a9"
|
||||
integrity sha512-PqtWqPPRpMwZ9FjTzyugb5KeV9kmg2C3hjxZHwjl0lijT4QIJDd0z6AWcnbM9w2nayjDymyTt0+sbdTv3pVeNg==
|
||||
dependencies:
|
||||
rfdc "^1.4.1"
|
||||
|
||||
"@vue/language-core@3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.5.tgz"
|
||||
@@ -2146,44 +2346,76 @@
|
||||
path-browserify "^1.0.1"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
"@vue/reactivity@3.5.18":
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.18.tgz"
|
||||
integrity sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==
|
||||
"@vue/reactivity@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.21.tgz#34d4532c325876cf5481206060a3d525862c8ac5"
|
||||
integrity sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==
|
||||
dependencies:
|
||||
"@vue/shared" "3.5.18"
|
||||
"@vue/shared" "3.5.21"
|
||||
|
||||
"@vue/runtime-core@3.5.18":
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.18.tgz"
|
||||
integrity sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==
|
||||
"@vue/runtime-core@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.21.tgz#d97a4e7223a99644129f95c7d8318a7e92f255e4"
|
||||
integrity sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.18"
|
||||
"@vue/shared" "3.5.18"
|
||||
"@vue/reactivity" "3.5.21"
|
||||
"@vue/shared" "3.5.21"
|
||||
|
||||
"@vue/runtime-dom@3.5.18":
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz"
|
||||
integrity sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==
|
||||
"@vue/runtime-dom@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz#a3d35d53320abe8462c3bf2a469f729d8c9f78ff"
|
||||
integrity sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.18"
|
||||
"@vue/runtime-core" "3.5.18"
|
||||
"@vue/shared" "3.5.18"
|
||||
"@vue/reactivity" "3.5.21"
|
||||
"@vue/runtime-core" "3.5.21"
|
||||
"@vue/shared" "3.5.21"
|
||||
csstype "^3.1.3"
|
||||
|
||||
"@vue/server-renderer@3.5.18":
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.18.tgz"
|
||||
integrity sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==
|
||||
"@vue/server-renderer@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.21.tgz#1d0be5059a0c10f2c0483eef71ebf5bfd21a8b49"
|
||||
integrity sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.5.18"
|
||||
"@vue/shared" "3.5.18"
|
||||
"@vue/compiler-ssr" "3.5.21"
|
||||
"@vue/shared" "3.5.21"
|
||||
|
||||
"@vue/shared@3.5.18", "@vue/shared@^3.5.0", "@vue/shared@^3.5.18":
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.18.tgz"
|
||||
integrity sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==
|
||||
|
||||
"@vue/shared@3.5.21":
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.21.tgz#505edb122629d1979f70a2a65ca0bd4050dc2e54"
|
||||
integrity sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==
|
||||
|
||||
"@vueuse/core@13.9.0", "@vueuse/core@^13.6.0":
|
||||
version "13.9.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-13.9.0.tgz#051aeff47a259e9e4d7d0cc3e54879817b0cbcad"
|
||||
integrity sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==
|
||||
dependencies:
|
||||
"@types/web-bluetooth" "^0.0.21"
|
||||
"@vueuse/metadata" "13.9.0"
|
||||
"@vueuse/shared" "13.9.0"
|
||||
|
||||
"@vueuse/integrations@^13.6.0":
|
||||
version "13.9.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/integrations/-/integrations-13.9.0.tgz#1bd1d77093a327321cca00e2bbf5da7b18aa6b43"
|
||||
integrity sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==
|
||||
dependencies:
|
||||
"@vueuse/core" "13.9.0"
|
||||
"@vueuse/shared" "13.9.0"
|
||||
|
||||
"@vueuse/metadata@13.9.0":
|
||||
version "13.9.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.9.0.tgz#57c738d99661c33347080c0bc4cd11160e0d0881"
|
||||
integrity sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==
|
||||
|
||||
"@vueuse/shared@13.9.0":
|
||||
version "13.9.0"
|
||||
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.9.0.tgz#7168b4ed647e625b05eb4e7e80fe8aabd00e3923"
|
||||
integrity sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==
|
||||
|
||||
"@xmldom/xmldom@^0.8.8":
|
||||
version "0.8.11"
|
||||
resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz"
|
||||
@@ -2805,6 +3037,11 @@ caseless@~0.12.0:
|
||||
resolved "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz"
|
||||
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
|
||||
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
|
||||
|
||||
centra@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmmirror.com/centra/-/centra-2.7.0.tgz"
|
||||
@@ -2829,6 +3066,16 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
character-entities-html4@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
|
||||
integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==
|
||||
|
||||
character-entities-legacy@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b"
|
||||
integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
|
||||
|
||||
charenc@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz"
|
||||
@@ -3016,6 +3263,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
comma-separated-tokens@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
|
||||
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
|
||||
|
||||
commander@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz"
|
||||
@@ -3305,6 +3557,11 @@ depd@2.0.0:
|
||||
resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
|
||||
dequal@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
|
||||
destroy@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz"
|
||||
@@ -3325,6 +3582,13 @@ detect-node@^2.0.4:
|
||||
resolved "https://registry.npmmirror.com/detect-node/-/detect-node-2.1.0.tgz"
|
||||
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
|
||||
|
||||
devlop@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
|
||||
integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
|
||||
dependencies:
|
||||
dequal "^2.0.0"
|
||||
|
||||
dijkstrajs@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz"
|
||||
@@ -3997,7 +4261,7 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fdir@^6.4.4, fdir@^6.4.6:
|
||||
fdir@^6.4.4, fdir@^6.4.6, fdir@^6.5.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz"
|
||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||
@@ -4094,6 +4358,13 @@ flatted@^3.2.9:
|
||||
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz"
|
||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||
|
||||
focus-trap@^7.6.5:
|
||||
version "7.6.5"
|
||||
resolved "https://registry.npmmirror.com/focus-trap/-/focus-trap-7.6.5.tgz#56f0814286d43c1a2688e9bc4f31f17ae047fb76"
|
||||
integrity sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==
|
||||
dependencies:
|
||||
tabbable "^6.2.0"
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.11"
|
||||
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz"
|
||||
@@ -4495,6 +4766,30 @@ hasown@^2.0.2:
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
hast-util-to-html@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.npmmirror.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005"
|
||||
integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==
|
||||
dependencies:
|
||||
"@types/hast" "^3.0.0"
|
||||
"@types/unist" "^3.0.0"
|
||||
ccount "^2.0.0"
|
||||
comma-separated-tokens "^2.0.0"
|
||||
hast-util-whitespace "^3.0.0"
|
||||
html-void-elements "^3.0.0"
|
||||
mdast-util-to-hast "^13.0.0"
|
||||
property-information "^7.0.0"
|
||||
space-separated-tokens "^2.0.0"
|
||||
stringify-entities "^4.0.0"
|
||||
zwitch "^2.0.4"
|
||||
|
||||
hast-util-whitespace@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
|
||||
integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==
|
||||
dependencies:
|
||||
"@types/hast" "^3.0.0"
|
||||
|
||||
he@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz"
|
||||
@@ -4522,6 +4817,11 @@ hpagent@^1.2.0:
|
||||
resolved "https://registry.npmmirror.com/hpagent/-/hpagent-1.2.0.tgz#0ae417895430eb3770c03443456b8d90ca464903"
|
||||
integrity sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==
|
||||
|
||||
html-void-elements@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
|
||||
integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
|
||||
|
||||
http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz"
|
||||
@@ -5357,6 +5657,13 @@ magic-string@^0.30.17, magic-string@^0.30.4:
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.0"
|
||||
|
||||
magic-string@^0.30.18:
|
||||
version "0.30.19"
|
||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9"
|
||||
integrity sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.5"
|
||||
|
||||
make-fetch-happen@^10.0.3:
|
||||
version "10.2.1"
|
||||
resolved "https://registry.npmmirror.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz"
|
||||
@@ -5379,6 +5686,11 @@ make-fetch-happen@^10.0.3:
|
||||
socks-proxy-agent "^7.0.0"
|
||||
ssri "^9.0.0"
|
||||
|
||||
mark.js@8.11.1:
|
||||
version "8.11.1"
|
||||
resolved "https://registry.npmmirror.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5"
|
||||
integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==
|
||||
|
||||
marked@^16.1.2:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.npmmirror.com/marked/-/marked-16.2.0.tgz"
|
||||
@@ -5405,6 +5717,21 @@ md5@^2.3.0:
|
||||
crypt "0.0.2"
|
||||
is-buffer "~1.1.6"
|
||||
|
||||
mdast-util-to-hast@^13.0.0:
|
||||
version "13.2.0"
|
||||
resolved "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4"
|
||||
integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
|
||||
dependencies:
|
||||
"@types/hast" "^3.0.0"
|
||||
"@types/mdast" "^4.0.0"
|
||||
"@ungap/structured-clone" "^1.0.0"
|
||||
devlop "^1.0.0"
|
||||
micromark-util-sanitize-uri "^2.0.0"
|
||||
trim-lines "^3.0.0"
|
||||
unist-util-position "^5.0.0"
|
||||
unist-util-visit "^5.0.0"
|
||||
vfile "^6.0.0"
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz"
|
||||
@@ -5430,6 +5757,38 @@ methods@~1.1.2:
|
||||
resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz"
|
||||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||
|
||||
micromark-util-character@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6"
|
||||
integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==
|
||||
dependencies:
|
||||
micromark-util-symbol "^2.0.0"
|
||||
micromark-util-types "^2.0.0"
|
||||
|
||||
micromark-util-encode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8"
|
||||
integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==
|
||||
|
||||
micromark-util-sanitize-uri@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7"
|
||||
integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==
|
||||
dependencies:
|
||||
micromark-util-character "^2.0.0"
|
||||
micromark-util-encode "^2.0.0"
|
||||
micromark-util-symbol "^2.0.0"
|
||||
|
||||
micromark-util-symbol@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8"
|
||||
integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==
|
||||
|
||||
micromark-util-types@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e"
|
||||
integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==
|
||||
|
||||
micromatch@^4.0.5, micromatch@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz"
|
||||
@@ -5571,6 +5930,11 @@ minipass@^5.0.0:
|
||||
resolved "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
minisearch@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.npmmirror.com/minisearch/-/minisearch-7.1.2.tgz#296ee8d1906cc378f7e57a3a71f07e5205a75df5"
|
||||
integrity sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==
|
||||
|
||||
minizlib@^2.1.1, minizlib@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz"
|
||||
@@ -5878,6 +6242,20 @@ onetime@^5.1.0:
|
||||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
oniguruma-parser@^0.12.1:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz#82ba2208d7a2b69ee344b7efe0ae930c627dcc4a"
|
||||
integrity sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==
|
||||
|
||||
oniguruma-to-es@^4.3.3:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz#50db2c1e28ec365e102c1863dfd3d1d1ad18613e"
|
||||
integrity sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==
|
||||
dependencies:
|
||||
oniguruma-parser "^0.12.1"
|
||||
regex "^6.0.1"
|
||||
regex-recursion "^6.0.2"
|
||||
|
||||
open@^10.2.0:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.npmmirror.com/open/-/open-10.2.0.tgz"
|
||||
@@ -6367,6 +6745,11 @@ promise-retry@^2.0.1:
|
||||
err-code "^2.0.2"
|
||||
retry "^0.12.0"
|
||||
|
||||
property-information@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d"
|
||||
integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==
|
||||
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz"
|
||||
@@ -6537,6 +6920,25 @@ regenerator-runtime@^0.13.3:
|
||||
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
regex-recursion@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33"
|
||||
integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==
|
||||
dependencies:
|
||||
regex-utilities "^2.3.0"
|
||||
|
||||
regex-utilities@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz#87163512a15dce2908cf079c8960d5158ff43280"
|
||||
integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==
|
||||
|
||||
regex@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/regex/-/regex-6.0.1.tgz#282fa4435d0c700b09c0eb0982b602e05ab6a34f"
|
||||
integrity sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==
|
||||
dependencies:
|
||||
regex-utilities "^2.3.0"
|
||||
|
||||
request-progress@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/request-progress/-/request-progress-2.0.1.tgz"
|
||||
@@ -7002,6 +7404,20 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shiki@^3.9.2:
|
||||
version "3.12.2"
|
||||
resolved "https://registry.npmmirror.com/shiki/-/shiki-3.12.2.tgz#0ca67fb4b76519f0c0057acb758d521efa6e13a0"
|
||||
integrity sha512-uIrKI+f9IPz1zDT+GMz+0RjzKJiijVr6WDWm9Pe3NNY6QigKCfifCEv9v9R2mDASKKjzjQ2QpFLcxaR3iHSnMA==
|
||||
dependencies:
|
||||
"@shikijs/core" "3.12.2"
|
||||
"@shikijs/engine-javascript" "3.12.2"
|
||||
"@shikijs/engine-oniguruma" "3.12.2"
|
||||
"@shikijs/langs" "3.12.2"
|
||||
"@shikijs/themes" "3.12.2"
|
||||
"@shikijs/types" "3.12.2"
|
||||
"@shikijs/vscode-textmate" "^10.0.2"
|
||||
"@types/hast" "^3.0.4"
|
||||
|
||||
side-channel-list@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz"
|
||||
@@ -7148,6 +7564,11 @@ source-map@^0.6.0, source-map@~0.6.1:
|
||||
resolved "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
space-separated-tokens@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
|
||||
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
|
||||
|
||||
spdx-correct@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmmirror.com/spdx-correct/-/spdx-correct-3.2.0.tgz"
|
||||
@@ -7271,6 +7692,14 @@ string_decoder@~1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
stringify-entities@^4.0.0:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3"
|
||||
integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==
|
||||
dependencies:
|
||||
character-entities-html4 "^2.0.0"
|
||||
character-entities-legacy "^3.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
|
||||
@@ -7405,6 +7834,11 @@ synckit@^0.11.7:
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
|
||||
tabbable@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.npmmirror.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
|
||||
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
|
||||
|
||||
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz"
|
||||
@@ -7484,6 +7918,14 @@ tinyglobby@^0.2.14:
|
||||
fdir "^6.4.4"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
tinyglobby@^0.2.15:
|
||||
version "0.2.15"
|
||||
resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
|
||||
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.3"
|
||||
|
||||
tmp-promise@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz"
|
||||
@@ -7534,6 +7976,11 @@ tr46@~0.0.3:
|
||||
resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
trim-lines@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
||||
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
|
||||
|
||||
truncate-utf8-bytes@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz"
|
||||
@@ -7662,6 +8109,44 @@ unique-slug@^3.0.0:
|
||||
dependencies:
|
||||
imurmurhash "^0.1.4"
|
||||
|
||||
unist-util-is@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
|
||||
integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
|
||||
unist-util-position@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4"
|
||||
integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
|
||||
unist-util-stringify-position@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
|
||||
integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
|
||||
unist-util-visit-parents@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
|
||||
integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
unist-util-is "^6.0.0"
|
||||
|
||||
unist-util-visit@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
|
||||
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
unist-util-is "^6.0.0"
|
||||
unist-util-visit-parents "^6.0.0"
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz"
|
||||
@@ -7836,6 +8321,22 @@ verror@^1.10.0:
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vfile-message@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4"
|
||||
integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
unist-util-stringify-position "^4.0.0"
|
||||
|
||||
vfile@^6.0.0:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab"
|
||||
integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
vfile-message "^4.0.0"
|
||||
|
||||
vite-dev-rpc@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz"
|
||||
@@ -7926,6 +8427,44 @@ vite@^7.0.5:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vite@^7.1.2:
|
||||
version "7.1.5"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-7.1.5.tgz#4dbcb48c6313116689be540466fc80faa377be38"
|
||||
integrity sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==
|
||||
dependencies:
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.3"
|
||||
postcss "^8.5.6"
|
||||
rollup "^4.43.0"
|
||||
tinyglobby "^0.2.15"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vitepress@^2.0.0-alpha.12:
|
||||
version "2.0.0-alpha.12"
|
||||
resolved "https://registry.npmmirror.com/vitepress/-/vitepress-2.0.0-alpha.12.tgz#e75648eec6c43bff1d669f9a7f81f777acc6e4fd"
|
||||
integrity sha512-yZwCwRRepcpN5QeAhwSnEJxS3I6zJcVixqL1dnm6km4cnriLpQyy2sXQDsE5Ti3pxGPbhU51nTMwI+XC1KNnJg==
|
||||
dependencies:
|
||||
"@docsearch/css" "^4.0.0-beta.7"
|
||||
"@docsearch/js" "^4.0.0-beta.7"
|
||||
"@iconify-json/simple-icons" "^1.2.47"
|
||||
"@shikijs/core" "^3.9.2"
|
||||
"@shikijs/transformers" "^3.9.2"
|
||||
"@shikijs/types" "^3.9.2"
|
||||
"@types/markdown-it" "^14.1.2"
|
||||
"@vitejs/plugin-vue" "^6.0.1"
|
||||
"@vue/devtools-api" "^8.0.0"
|
||||
"@vue/shared" "^3.5.18"
|
||||
"@vueuse/core" "^13.6.0"
|
||||
"@vueuse/integrations" "^13.6.0"
|
||||
focus-trap "^7.6.5"
|
||||
mark.js "8.11.1"
|
||||
minisearch "^7.1.2"
|
||||
shiki "^3.9.2"
|
||||
vite "^7.1.2"
|
||||
vue "^3.5.18"
|
||||
|
||||
vscode-uri@^3.0.8:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz"
|
||||
@@ -7958,16 +8497,16 @@ vue-tsc@^3.0.3:
|
||||
"@volar/typescript" "2.4.22"
|
||||
"@vue/language-core" "3.0.5"
|
||||
|
||||
vue@^3.5.17:
|
||||
version "3.5.18"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz"
|
||||
integrity sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==
|
||||
vue@^3.5.18, vue@^3.5.21:
|
||||
version "3.5.21"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz#30af9553fd9642870321b7dc547b46c395cb7b91"
|
||||
integrity sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.18"
|
||||
"@vue/compiler-sfc" "3.5.18"
|
||||
"@vue/runtime-dom" "3.5.18"
|
||||
"@vue/server-renderer" "3.5.18"
|
||||
"@vue/shared" "3.5.18"
|
||||
"@vue/compiler-dom" "3.5.21"
|
||||
"@vue/compiler-sfc" "3.5.21"
|
||||
"@vue/runtime-dom" "3.5.21"
|
||||
"@vue/server-renderer" "3.5.21"
|
||||
"@vue/shared" "3.5.21"
|
||||
|
||||
wcwidth@^1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -8263,3 +8802,8 @@ zod@^3.25.32:
|
||||
version "3.25.76"
|
||||
resolved "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz"
|
||||
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||
|
||||
zwitch@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
|
||||
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
|
||||
|
||||
Reference in New Issue
Block a user