diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..cd346743 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,35 @@ +# Configuration for development teams +language: 'en-US' + +reviews: + profile: 'assertive' + high_level_summary: true + auto_review: + enabled: true + drafts: false + base_branches: + - 'alpha' + - 'beta' + - 'main' + - 'dev' + ignore_title_keywords: + - 'wip' + - 'draft' +tools: + eslint: + enabled: true + ruff: + enabled: true + gitleaks: + enabled: true + hadolint: + enabled: true + +chat: + auto_reply: true + +knowledge_base: + code_guidelines: + enabled: true + filePatterns: + - '**/.cursorrules' diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml new file mode 100644 index 00000000..88c35215 --- /dev/null +++ b/.github/workflows/dev-release.yml @@ -0,0 +1,236 @@ +name: Dev Release Build + +on: + workflow_dispatch: + inputs: + platform: + description: 'Target platform' + required: true + type: choice + options: + - macos + - linux + - windows + version: + description: 'Version to set (optional)' + required: false + type: string + +jobs: + build-macos: + runs-on: macos-latest + if: ${{ inputs.platform == 'macos' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Set version + if: ${{ inputs.version != '' }} + shell: bash + run: | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + pkg.version = '${{ inputs.version }}'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + console.log(\`Version set to \${pkg.version}\`); + " + + - name: Build macOS package + run: | + pnpm build + pnpm exec electron-builder --mac --publish never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ELECTRON_BUILDER_CHANNEL: dev + + - name: List build artifacts + shell: bash + run: | + echo "Build artifacts:" + find dist -type f -name "*" | head -20 + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-dev-artifacts + path: | + dist/*.dmg + dist/*.zip + dist/*.yml + dist/*.yaml + dist/*.blockmap + retention-days: 30 + if-no-files-found: warn + + build-linux: + runs-on: ubuntu-latest + if: ${{ inputs.platform == 'linux' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Set version + if: ${{ inputs.version != '' }} + shell: bash + run: | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + pkg.version = '${{ inputs.version }}'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + console.log(\`Version set to \${pkg.version}\`); + " + + - name: Build Linux package + run: | + pnpm build + pnpm exec electron-builder --linux --publish never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ELECTRON_BUILDER_CHANNEL: dev + + - name: List build artifacts + shell: bash + run: | + echo "Build artifacts:" + find dist -type f -name "*" | head -20 + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-dev-artifacts + path: | + dist/*.AppImage + dist/*.deb + dist/*.yml + dist/*.yaml + dist/*.blockmap + retention-days: 30 + if-no-files-found: warn + + build-windows: + runs-on: windows-latest + if: ${{ inputs.platform == 'windows' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Set version + if: ${{ inputs.version != '' }} + shell: bash + run: | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + pkg.version = '${{ inputs.version }}'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + console.log(\`Version set to \${pkg.version}\`); + " + + - name: Build Windows package + run: | + pnpm build + pnpm exec electron-builder --win --publish never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ELECTRON_BUILDER_CHANNEL: dev + + - name: List build artifacts + shell: bash + run: | + echo "Build artifacts:" + find dist -type f -name "*" | head -20 + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: windows-dev-artifacts + path: | + dist/*.exe + dist/*.yml + dist/*.yaml + dist/*.blockmap + retention-days: 30 + if-no-files-found: warn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a72ffec0..96194489 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,108 +1,129 @@ name: Release +run-name: 🚀 Release ${{ github.ref_name }} + on: workflow_dispatch: - inputs: - version: - description: 'Release version (e.g., v1.0.0, v1.0.0-beta.1, v1.0.0-alpha.1)' - required: false - force_version_type: - description: 'Force version type (overrides auto-detection)' - required: false - type: choice - options: - - 'dev' - - 'test' - - 'alpha' - - 'beta' - - 'stable' + push: + branches: + - main + - alpha + - beta permissions: contents: write pull-requests: read jobs: - detect-version: + version-analysis: runs-on: ubuntu-latest outputs: - version: ${{ steps.version.outputs.version }} - version_type: ${{ steps.version.outputs.version_type }} - is_prerelease: ${{ steps.version.outputs.is_prerelease }} - upload_path: ${{ steps.version.outputs.upload_path }} - autoupdate_path: ${{ steps.version.outputs.autoupdate_path }} + next_version: ${{ steps.semantic.outputs.next_version }} + version_type: ${{ steps.branch.outputs.version_type }} + is_prerelease: ${{ steps.branch.outputs.is_prerelease }} + channel: ${{ steps.branch.outputs.channel }} + should_release: ${{ steps.semantic.outputs.should_release }} steps: - name: Checkout repository uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + + - name: Install dependencies + run: pnpm install - - name: Detect version and type - id: version + - name: Detect branch type and channel + id: branch shell: bash run: | - # Get version from input, tag, or package.json - if [ -n "${{ github.event.inputs.version }}" ]; then - VERSION="${{ github.event.inputs.version }}" - elif [ -n "${{ github.ref_name }}" ] && [[ "${{ github.ref_name }}" == v* ]]; then - VERSION="${{ github.ref_name }}" - else - VERSION="v$(node -p "require('./package.json').version")" - fi - - # Remove 'v' prefix for processing - VERSION_NO_V="${VERSION#v}" - - # Detect version type - if [ -n "${{ github.event.inputs.force_version_type }}" ]; then - VERSION_TYPE="${{ github.event.inputs.force_version_type }}" - elif [[ "$VERSION_NO_V" == *"-dev"* ]]; then - VERSION_TYPE="dev" - elif [[ "$VERSION_NO_V" == *"-test"* ]]; then - VERSION_TYPE="test" - elif [[ "$VERSION_NO_V" == *"-alpha"* ]]; then - VERSION_TYPE="alpha" - elif [[ "$VERSION_NO_V" == *"-beta"* ]]; then - VERSION_TYPE="beta" - else + # 根据分支名确定版本类型和更新渠道 + if [[ "${{ github.ref_name }}" == "main" ]]; then VERSION_TYPE="stable" - fi - - # Determine if it's a prerelease - if [[ "$VERSION_TYPE" != "stable" ]]; then + IS_PRERELEASE="false" + CHANNEL="latest" + elif [[ "${{ github.ref_name }}" == "beta" ]]; then + VERSION_TYPE="beta" IS_PRERELEASE="true" + CHANNEL="beta" + elif [[ "${{ github.ref_name }}" == "alpha" ]]; then + VERSION_TYPE="alpha" + IS_PRERELEASE="true" + CHANNEL="alpha" else - IS_PRERELEASE="false" + echo "❌ Unsupported branch: ${{ github.ref_name }}" + exit 1 fi - # Set upload paths based on version type - case "$VERSION_TYPE" in - "dev"|"test") - UPLOAD_PATH="/test-releases/" - AUTOUPDATE_PATH="/test-autoupdate/" - ;; - "alpha"|"beta") - UPLOAD_PATH="/prerelease/" - AUTOUPDATE_PATH="/prerelease-autoupdate/" - ;; - "stable") - UPLOAD_PATH="/releases/" - AUTOUPDATE_PATH="/autoupdate/" - ;; - esac - - echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version_type=$VERSION_TYPE" >> $GITHUB_OUTPUT echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT - echo "upload_path=$UPLOAD_PATH" >> $GITHUB_OUTPUT - echo "autoupdate_path=$AUTOUPDATE_PATH" >> $GITHUB_OUTPUT + echo "channel=$CHANNEL" >> $GITHUB_OUTPUT - echo "🏷️ Version: $VERSION" echo "📦 Version Type: $VERSION_TYPE" echo "🚀 Is Prerelease: $IS_PRERELEASE" - echo "📁 Upload Path: $UPLOAD_PATH" - echo "🔄 AutoUpdate Path: $AUTOUPDATE_PATH" + echo "📺 Channel: $CHANNEL" + + - name: Run semantic-release for version analysis + id: semantic + shell: bash + run: | + echo "🔍 Analyzing commits to determine next version..." + + # 创建临时的分析配置文件 + cat > .releaserc.temp.js << 'EOF' + module.exports = { + branches: [ + 'main', + { name: 'beta', prerelease: true }, + { name: 'alpha', prerelease: true } + ], + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator' + ] + } + EOF + + # 使用临时配置进行版本分析,输出到临时文件 + pnpm semantic-release --dry-run -c .releaserc.temp.js > semantic_output.txt 2>&1 + + # 显示输出内容以便调试 + echo "=== Semantic Release Output ===" + cat semantic_output.txt + echo "=== End of Output ===" + + # 清理临时文件 + rm -f .releaserc.temp.js + + # 检查是否有新版本需要发布(从文件中搜索,避免变量长度限制) + if grep -q "next release version is" semantic_output.txt; then + NEXT_VERSION=$(grep "next release version is" semantic_output.txt | sed 's/.*next release version is \([0-9.a-z-]*\).*/\1/' | head -1) + echo "✅ Next version: $NEXT_VERSION" + echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "should_release=true" >> $GITHUB_OUTPUT + else + echo "ℹ️ No new version to release" + echo "should_release=false" >> $GITHUB_OUTPUT + fi + + # 清理临时输出文件 + rm -f semantic_output.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release: - needs: detect-version + needs: version-analysis runs-on: ${{ matrix.os }} + if: needs.version-analysis.outputs.should_release == 'true' strategy: fail-fast: false matrix: @@ -150,26 +171,40 @@ jobs: - name: Install dependencies run: pnpm install + - name: Update package.json version + shell: bash + run: | + echo "📋 Current package.json version: $(node -p "require('./package.json').version")" + echo "🔄 Target version: ${{ needs.version-analysis.outputs.next_version }}" + echo "🌿 Branch: ${{ github.ref_name }}" + echo "📦 Version type: ${{ needs.version-analysis.outputs.version_type }}" + echo "🚀 Is prerelease: ${{ needs.version-analysis.outputs.is_prerelease }}" + echo "📺 Channel: ${{ needs.version-analysis.outputs.channel }}" + + # 更新 package.json 中的版本号 + node -e " + const fs = require('fs'); + const package = JSON.parse(fs.readFileSync('package.json', 'utf8')); + package.version = '${{ needs.version-analysis.outputs.next_version }}'; + fs.writeFileSync('package.json', JSON.stringify(package, null, 2) + '\n'); + console.log('✅ Updated package.json version to:', package.version); + " + + echo "📋 Updated package.json version: $(node -p "require('./package.json').version")" + - name: Build for ${{ matrix.platform }} shell: bash run: | echo "🏗️ Building for ${{ matrix.platform }}" - echo "构建平台: ${{ matrix.platform }}" - echo "📦 版本类型: ${{ needs.detect-version.outputs.version_type }}" - echo "🔄 更新渠道: ${{ needs.detect-version.outputs.version_type == 'stable' && 'latest' || needs.detect-version.outputs.version_type }}" + echo "📦 版本号: ${{ needs.version-analysis.outputs.next_version }}" + echo "📦 版本类型: ${{ needs.version-analysis.outputs.version_type }}" + echo "🔄 更新渠道: ${{ needs.version-analysis.outputs.channel }}" pnpm build - # 根据版本类型决定发布策略 - if [[ "${{ needs.detect-version.outputs.version_type }}" == "dev" || "${{ needs.detect-version.outputs.version_type }}" == "test" ]]; then - echo "🚫 Dev/Test version - building without publishing" - pnpm exec electron-builder ${{ matrix.target }} --publish never - else - echo "🚀 Publishing version to GitHub" - # 使用 always 策略,无论是否有标签都会发布 - # electron-builder 会根据版本号自动判断是否为 prerelease - echo "📦 Publishing with always strategy (supports both release and prerelease)" - pnpm exec electron-builder ${{ matrix.target }} --publish always - fi + # 构建产物但不发布,版本号已经正确 + echo "🏗️ Building artifacts with correct version number" + echo "📦 Artifacts will be uploaded by final release job" + pnpm exec electron-builder ${{ matrix.target }} --publish never env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_ID: ${{ secrets.APPLE_ID }} @@ -177,8 +212,8 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - # 根据版本类型设置更新渠道,stable 版本使用 latest,其他版本使用对应类型名称 - ELECTRON_BUILDER_CHANNEL: ${{ needs.detect-version.outputs.version_type == 'stable' && 'latest' || needs.detect-version.outputs.version_type }} + # 根据分支设置更新渠道 + ELECTRON_BUILDER_CHANNEL: ${{ needs.version-analysis.outputs.channel }} - name: List build artifacts shell: bash @@ -219,12 +254,14 @@ jobs: if-no-files-found: warn semantic-release: - needs: [detect-version, release] + needs: [version-analysis, release] runs-on: ubuntu-latest - if: needs.detect-version.outputs.version_type != 'dev' && needs.detect-version.outputs.version_type != 'test' + if: needs.version-analysis.outputs.should_release == 'true' steps: - name: Checkout repository uses: actions/checkout@v4 + with: + fetch-depth: 0 # semantic-release 需要完整的 git 历史 - name: Setup Node.js uses: actions/setup-node@v4 @@ -252,13 +289,47 @@ jobs: - name: Install dependencies run: pnpm install + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Update package.json version for release + shell: bash + run: | + echo "🔄 Updating package.json version for semantic-release" + echo "Target version: ${{ needs.version-analysis.outputs.next_version }}" + + # 更新 package.json 中的版本号,确保与分析阶段一致 + node -e " + const fs = require('fs'); + const package = JSON.parse(fs.readFileSync('package.json', 'utf8')); + package.version = '${{ needs.version-analysis.outputs.next_version }}'; + fs.writeFileSync('package.json', JSON.stringify(package, null, 2) + '\n'); + console.log('✅ Updated package.json version to:', package.version); + " + + - name: Prepare artifacts for release + run: | + echo "📦 Preparing artifacts for GitHub Release" + mkdir -p dist + # 复制所有构建产物到 dist 目录 + find artifacts -name "*.exe" -o -name "*.dmg" -o -name "*.zip" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.yml" -o -name "*.yaml" -o -name "*.blockmap" | while read file; do + cp "$file" dist/ + echo "📁 Copied: $(basename "$file")" + done + ls -la dist/ + - name: Run Semantic Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: pnpm semantic-release + run: | + echo "🚀 Creating GitHub Release with semantic-release" + echo "📦 Version: ${{ needs.version-analysis.outputs.next_version }}" + pnpm semantic-release notify: - needs: [detect-version, release] + needs: [version-analysis, release, semantic-release] runs-on: ubuntu-latest if: always() steps: @@ -266,19 +337,27 @@ jobs: run: | echo "🏗️ Build Summary" echo "===============" - echo "Version: ${{ needs.detect-version.outputs.version }}" - echo "Type: ${{ needs.detect-version.outputs.version_type }}" - echo "Prerelease: ${{ needs.detect-version.outputs.is_prerelease }}" - echo "Status: ${{ needs.release.result }}" + echo "Branch: ${{ github.ref_name }}" + echo "Next Version: ${{ needs.version-analysis.outputs.next_version }}" + echo "Type: ${{ needs.version-analysis.outputs.version_type }}" + echo "Channel: ${{ needs.version-analysis.outputs.channel }}" + echo "Prerelease: ${{ needs.version-analysis.outputs.is_prerelease }}" + echo "Should Release: ${{ needs.version-analysis.outputs.should_release }}" + echo "Build Status: ${{ needs.release.result }}" + echo "Release Status: ${{ needs.semantic-release.result }}" echo "Trigger: ${{ github.event_name }}" echo "" - if [ "${{ needs.release.result }}" == "success" ]; then - if [ "${{ needs.detect-version.outputs.version_type }}" != "dev" ] && [ "${{ needs.detect-version.outputs.version_type }}" != "test" ]; then - echo "🎉 GitHub Release created successfully!" + if [ "${{ needs.version-analysis.outputs.should_release }}" == "false" ]; then + echo "ℹ️ No new version to release - no commits since last release" + elif [ "${{ needs.release.result }}" == "success" ]; then + if [ "${{ needs.semantic-release.result }}" == "success" ]; then + echo "🎉 Release ${{ needs.version-analysis.outputs.next_version }} created successfully!" echo "📍 Check: https://github.com/${{ github.repository }}/releases" + echo "🔄 Version was automatically determined based on commit messages" + echo "📦 Artifacts have correct version numbers in filenames" echo "" - if [ "${{ needs.detect-version.outputs.is_prerelease }}" == "true" ]; then + if [ "${{ needs.version-analysis.outputs.is_prerelease }}" == "true" ]; then echo "🧪 This is a prerelease version:" echo "- Alpha/Beta versions are marked as prerelease" echo "- Auto-update is enabled for prerelease users" @@ -286,7 +365,8 @@ jobs: echo "🚀 This is a stable release" fi else - echo "📦 Build completed but not published (dev/test version)" + echo "📦 Build completed but semantic-release failed" + echo "💡 Check semantic-release logs for details" fi else echo "❌ Build failed - check logs for details" diff --git a/.github/workflows/sync-release-to-gitcode.yml b/.github/workflows/sync-release-to-gitcode.yml new file mode 100644 index 00000000..c7765e2a --- /dev/null +++ b/.github/workflows/sync-release-to-gitcode.yml @@ -0,0 +1,475 @@ +name: Sync Release to GitCode + +on: + release: + types: [published, prereleased, edited] + workflow_dispatch: + inputs: + tag_name: + description: 'Tag name to sync (e.g., v1.0.0)' + required: true + type: string + release_name: + description: 'Release name' + required: false + type: string + release_body: + description: 'Release description/body' + required: false + type: string + prerelease: + description: 'Is this a prerelease?' + required: false + type: boolean + default: false + draft: + description: 'Is this a draft release?' + required: false + type: boolean + default: false + test_mode: + description: 'Test mode (dry run - no actual sync to GitCode)' + required: false + type: boolean + default: false + +env: + GITCODE_API_BASE: https://gitcode.com/api/v5 + GITCODE_OWNER: ${{ vars.GITCODE_OWNER || 'mkdir700' }} + GITCODE_REPO: EchoPlayer + +jobs: + sync-to-gitcode: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get release information + id: release + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # Manual trigger - use inputs + echo "🔧 Manual trigger detected, using workflow inputs" + echo "tag_name=${{ github.event.inputs.tag_name }}" >> $GITHUB_OUTPUT + echo "release_name=${{ github.event.inputs.release_name || github.event.inputs.tag_name }}" >> $GITHUB_OUTPUT + + # Handle release body - fetch from GitHub if not provided + if [ -z "${{ github.event.inputs.release_body }}" ]; then + echo "📄 No release description provided, fetching from GitHub API..." + + release_response=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ github.event.inputs.tag_name }}") + + if [ "$(echo "$release_response" | jq -r '.message // empty')" = "Not Found" ]; then + echo "⚠️ Release not found for tag: ${{ github.event.inputs.tag_name }}, using default description" + github_release_body="Release created via manual trigger for tag ${{ github.event.inputs.tag_name }}" + else + github_release_body=$(echo "$release_response" | jq -r '.body // empty') + if [ -z "$github_release_body" ] || [ "$github_release_body" = "null" ]; then + echo "⚠️ No description found in GitHub release, using default description" + github_release_body="Release created via manual trigger for tag ${{ github.event.inputs.tag_name }}" + else + echo "✅ Successfully fetched release description from GitHub" + fi + fi + + echo "release_body<> $GITHUB_OUTPUT + echo "$github_release_body" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + echo "📝 Using provided release description" + echo "release_body<> $GITHUB_OUTPUT + echo "${{ github.event.inputs.release_body }}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + echo "prerelease=${{ github.event.inputs.prerelease }}" >> $GITHUB_OUTPUT + echo "draft=${{ github.event.inputs.draft }}" >> $GITHUB_OUTPUT + echo "test_mode=${{ github.event.inputs.test_mode }}" >> $GITHUB_OUTPUT + else + # Automatic trigger - use release event data + echo "🚀 Release event detected, using release data" + echo "tag_name=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + echo "release_name=${{ github.event.release.name }}" >> $GITHUB_OUTPUT + echo "release_body<> $GITHUB_OUTPUT + echo "${{ github.event.release.body }}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "prerelease=${{ github.event.release.prerelease }}" >> $GITHUB_OUTPUT + echo "draft=${{ github.event.release.draft }}" >> $GITHUB_OUTPUT + echo "test_mode=false" >> $GITHUB_OUTPUT + fi + + - name: Sync repository to GitCode + if: steps.release.outputs.test_mode != 'true' + run: | + echo "🔄 Syncing repository to GitCode using HTTPS..." + + # Configure git with token authentication + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + + # Construct GitCode repository URL with token authentication + GITCODE_REPO_URL="https://oauth2:${{ secrets.GITCODE_ACCESS_TOKEN }}@gitcode.com/$GITCODE_OWNER/$GITCODE_REPO.git" + + echo "Repository: $GITCODE_OWNER/$GITCODE_REPO" + + # Add GitCode remote (remove if exists) + if git remote | grep -q "gitcode"; then + echo "Removing existing gitcode remote" + git remote remove gitcode + fi + + echo "Adding GitCode remote with HTTPS authentication" + git remote add gitcode "$GITCODE_REPO_URL" + + echo "📤 Force pushing branches to GitCode..." + + # Show available branches + echo "Available branches:" + git branch -a | grep -E "(main|dev|alpha|beta)" || echo "Target branches not found" + + # Force push main branches to GitCode + for branch in main dev alpha beta; do + if git show-ref --verify --quiet refs/heads/$branch || git show-ref --verify --quiet refs/remotes/origin/$branch; then + echo "Pushing branch: $branch" + if git show-ref --verify --quiet refs/heads/$branch; then + git push --force gitcode $branch:$branch || { + echo "❌ Failed to push local branch $branch" + exit 1 + } + else + git push --force gitcode origin/$branch:$branch || { + echo "❌ Failed to push remote branch $branch" + exit 1 + } + fi + echo "✅ Successfully pushed branch: $branch" + else + echo "⚠️ Branch $branch not found, skipping" + fi + done + + echo "🏷️ Pushing all tags to GitCode..." + echo "Available tags (last 10):" + git tag | tail -10 || echo "No tags found" + + git push --force gitcode --tags || { + echo "❌ Failed to push tags" + exit 1 + } + + echo "✅ Repository sync completed successfully" + + - name: Test mode - Skip repository sync + if: steps.release.outputs.test_mode == 'true' + run: | + echo "🧪 Test mode enabled - skipping repository sync to GitCode" + echo "Would sync the following branches: main, dev, alpha, beta" + echo "Would force push all tags to GitCode" + echo "This would ensure tag ${{ steps.release.outputs.tag_name }} exists before creating release" + + - name: Download release assets + id: download-assets + run: | + mkdir -p ./release-assets + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # Manual trigger - fetch release data from GitHub API + echo "📦 Fetching release assets for tag: ${{ steps.release.outputs.tag_name }}" + + release_response=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ steps.release.outputs.tag_name }}") + + if [ "$(echo "$release_response" | jq -r '.message // empty')" = "Not Found" ]; then + echo "⚠️ Release not found for tag: ${{ steps.release.outputs.tag_name }}" + assets_json='[]' + else + assets_json=$(echo "$release_response" | jq '.assets') + fi + else + # Automatic trigger - use event data + assets_json='${{ toJson(github.event.release.assets) }}' + fi + + echo "Assets to download:" + echo "$assets_json" | jq -r '.[] | "\(.name) - \(.browser_download_url)"' + + asset_files="" + if [ "$(echo "$assets_json" | jq 'length')" -gt 0 ]; then + for asset in $(echo "$assets_json" | jq -r '.[] | @base64'); do + name=$(echo "$asset" | base64 --decode | jq -r '.name') + url=$(echo "$asset" | base64 --decode | jq -r '.browser_download_url') + + echo "Downloading $name from $url" + curl -L -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -o "./release-assets/$name" "$url" + + if [ -n "$asset_files" ]; then + asset_files="$asset_files," + fi + asset_files="$asset_files./release-assets/$name" + done + fi + + echo "asset_files=$asset_files" >> $GITHUB_OUTPUT + echo "has_assets=$([ -n "$asset_files" ] && echo "true" || echo "false")" >> $GITHUB_OUTPUT + + - name: Check if release exists on GitCode + id: check-release + run: | + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "🧪 Test mode enabled - skipping GitCode API check" + echo "exists=false" >> $GITHUB_OUTPUT + echo "Test mode: Simulating release does not exist on GitCode" + else + # First check if tag exists using GitCode tags API + echo "Checking if tag exists..." + tags_response=$(curl -s -w "%{http_code}" \ + -H "Accept: application/json" \ + "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/tags?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") + + tags_http_code="${tags_response: -3}" + tags_response_body="${tags_response%???}" + + echo "Tags API HTTP Code: $tags_http_code" + + tag_exists=false + if [ "$tags_http_code" = "200" ] || [ "$tags_http_code" = "201" ]; then + echo "Available tags (first 20):" + echo "$tags_response_body" | jq -r '.[] | .name' 2>/dev/null | head -20 || echo "Failed to parse tags" + + # Check if our target tag exists + if echo "$tags_response_body" | jq -e --arg tag "${{ steps.release.outputs.tag_name }}" '.[] | select(.name == $tag)' > /dev/null 2>&1; then + tag_exists=true + echo "✅ Tag ${{ steps.release.outputs.tag_name }} exists on GitCode" + else + echo "❌ Tag ${{ steps.release.outputs.tag_name }} does not exist on GitCode" + fi + else + echo "❌ Failed to fetch tags from GitCode (HTTP $tags_http_code): $tags_response_body" + fi + + # Then check if release exists (only if tag exists) + if [ "$tag_exists" = "true" ]; then + echo "Checking if release exists..." + response=$(curl -s -w "%{http_code}" \ + -H "Accept: application/json" \ + "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases/tags/${{ steps.release.outputs.tag_name }}?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") + else + echo "⚠️ Skipping release check since tag does not exist" + response="404Not Found" + fi + + http_code="${response: -3}" + response_body="${response%???}" + + echo "HTTP Code: $http_code" + echo "Response: $response_body" + + if [ "$http_code" = "200" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Release already exists on GitCode" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Release does not exist on GitCode" + fi + fi + + - name: Create release on GitCode + if: steps.check-release.outputs.exists == 'false' + id: create-release + run: | + payload=$(jq -n \ + --arg tag_name "${{ steps.release.outputs.tag_name }}" \ + --arg name "${{ steps.release.outputs.release_name }}" \ + --arg body "${{ steps.release.outputs.release_body }}" \ + --argjson prerelease "${{ steps.release.outputs.prerelease }}" \ + --argjson draft "${{ steps.release.outputs.draft }}" \ + '{ + tag_name: $tag_name, + name: $name, + body: $body, + prerelease: $prerelease, + draft: $draft + }') + + echo "Creating release with payload:" + echo "$payload" | jq . + + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "🧪 Test mode enabled - skipping release creation on GitCode" + echo "✅ Test mode: Would create release successfully on GitCode" + echo "created=true" >> $GITHUB_OUTPUT + else + response=$(curl -s -w "%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "$payload" \ + "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") + + http_code="${response: -3}" + response_body="${response%???}" + + echo "Create Release Response Code: $http_code" + echo "Create Release Response: $response_body" + + if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then + echo "✅ Release created successfully on GitCode (HTTP $http_code)" + echo "created=true" >> $GITHUB_OUTPUT + else + echo "❌ Failed to create release on GitCode (HTTP $http_code)" + echo "Response: $response_body" + echo "created=false" >> $GITHUB_OUTPUT + exit 1 + fi + fi + + - name: Update existing release on GitCode + if: steps.check-release.outputs.exists == 'true' + id: update-release + run: | + payload=$(jq -n \ + --arg name "${{ steps.release.outputs.release_name }}" \ + --arg body "${{ steps.release.outputs.release_body }}" \ + --argjson prerelease "${{ steps.release.outputs.prerelease }}" \ + --argjson draft "${{ steps.release.outputs.draft }}" \ + '{ + name: $name, + body: $body, + prerelease: $prerelease, + draft: $draft + }') + + echo "Updating release with payload:" + echo "$payload" | jq . + + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "🧪 Test mode enabled - skipping release update on GitCode" + echo "✅ Test mode: Would update release successfully on GitCode" + echo "updated=true" >> $GITHUB_OUTPUT + else + response=$(curl -s -w "%{http_code}" \ + -X PATCH \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "$payload" \ + "$GITCODE_API_BASE/repos/$GITCODE_OWNER/$GITCODE_REPO/releases/${{ steps.release.outputs.tag_name }}?access_token=${{ secrets.GITCODE_ACCESS_TOKEN }}") + + http_code="${response: -3}" + response_body="${response%???}" + + echo "Update Release Response Code: $http_code" + echo "Update Release Response: $response_body" + + if [ "$http_code" = "200" ]; then + echo "✅ Release updated successfully on GitCode" + echo "updated=true" >> $GITHUB_OUTPUT + else + echo "❌ Failed to update release on GitCode" + echo "updated=false" >> $GITHUB_OUTPUT + exit 1 + fi + fi + + - name: Upload assets to GitCode release + if: steps.download-assets.outputs.has_assets == 'true' + run: | + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "🧪 Test mode enabled - skipping asset upload to GitCode" + echo "Would upload the following assets:" + IFS=',' read -ra ASSET_FILES <<< "${{ steps.download-assets.outputs.asset_files }}" + for asset_file in "${ASSET_FILES[@]}"; do + if [ -f "$asset_file" ]; then + echo " - $(basename "$asset_file")" + fi + done + echo "✅ Test mode: Would upload all assets successfully to GitCode" + else + echo "📦 Uploading assets to GitCode release using JavaScript uploader..." + + # Make upload script executable + chmod +x ./scripts/upload-assets.js + + # Convert comma-separated asset files to array for JavaScript uploader + IFS=',' read -ra ASSET_FILES <<< "${{ steps.download-assets.outputs.asset_files }}" + + # Upload assets using the JavaScript uploader + node ./scripts/upload-assets.js \ + --token "${{ secrets.GITCODE_ACCESS_TOKEN }}" \ + --owner "$GITCODE_OWNER" \ + --repo "$GITCODE_REPO" \ + --tag "${{ steps.release.outputs.tag_name }}" \ + --concurrency 3 \ + --retry 3 \ + "${ASSET_FILES[@]}" + + upload_exit_code=$? + if [ $upload_exit_code -eq 0 ]; then + echo "✅ All assets uploaded successfully to GitCode" + else + echo "❌ Asset upload failed with exit code: $upload_exit_code" + exit 1 + fi + fi + + - name: Summary + run: | + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "## 🧪 Test Mode - Release Sync Summary" >> $GITHUB_STEP_SUMMARY + else + echo "## 🚀 Release Sync Summary" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + + echo "**Trigger:** ${{ github.event_name == 'workflow_dispatch' && '🔧 Manual' || '🚀 Automatic' }}" >> $GITHUB_STEP_SUMMARY + echo "**Release:** ${{ steps.release.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY + echo "**Name:** ${{ steps.release.outputs.release_name }}" >> $GITHUB_STEP_SUMMARY + echo "**GitCode Repository:** $GITCODE_OWNER/$GITCODE_REPO" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "**Mode:** 🧪 Test Mode (Dry Run)" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.check-release.outputs.exists }}" = "true" ]; then + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "**Action:** Would update existing release ✅" >> $GITHUB_STEP_SUMMARY + else + echo "**Action:** Updated existing release ✅" >> $GITHUB_STEP_SUMMARY + fi + else + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "**Action:** Would create new release ✅" >> $GITHUB_STEP_SUMMARY + else + echo "**Action:** Created new release ✅" >> $GITHUB_STEP_SUMMARY + fi + fi + + if [ "${{ steps.download-assets.outputs.has_assets }}" = "true" ]; then + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "**Assets:** Would upload to GitCode ✅" >> $GITHUB_STEP_SUMMARY + else + echo "**Assets:** Uploaded to GitCode ✅" >> $GITHUB_STEP_SUMMARY + fi + else + echo "**Assets:** No assets to upload" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.release.outputs.test_mode }}" = "true" ]; then + echo "Test completed successfully! 🧪 No actual changes were made to GitCode." >> $GITHUB_STEP_SUMMARY + else + echo "Release has been successfully synced to GitCode! 🎉" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf16b129..ecae4791 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Test on: push: - branches: [main, develop] + branches: [main, develop, alpha, beta, dev] pull_request: - branches: [main] + branches: [main, alpha, beta, dev] jobs: test: diff --git a/.gitignore b/.gitignore index cd0847af..e4f0f550 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ docs/.vitepress/cache/deps .mcp.json dist-test + +resources/ffmpeg/ +.ffmpeg-cache diff --git a/.releaserc.js b/.releaserc.js index 414c16e6..a17d61a4 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,7 +1,26 @@ module.exports = { - branches: ['main', { name: 'beta', prerelease: true }, { name: 'alpha', prerelease: true }], + branches: [ + // main 分支:稳定版本 (v1.0.0) + 'main', + // beta 分支:测试版本 (v1.0.0-beta.1) + { name: 'beta', prerelease: true }, + // alpha 分支:开发版本 (v1.0.0-alpha.1) + { name: 'alpha', prerelease: true } + ], plugins: [ - '@semantic-release/commit-analyzer', + [ + '@semantic-release/commit-analyzer', + { + preset: 'angular', + releaseRules: [ + { + type: 'chore', + scope: 'release', + release: 'patch' + } + ] + } + ], '@semantic-release/release-notes-generator', // 更新 changelog 文件 @@ -24,12 +43,27 @@ module.exports = { } ], - // 创建 Draft Release + // 创建 Release 并上传构建产物 [ '@semantic-release/github', { - draft: true, - assets: [] // 不上传产物,electron-builder 会自动创建 Release 并上传产物 + assets: [ + // Windows 产物 + 'dist/*.exe', + 'dist/*.zip', + + // macOS 产物 + 'dist/*.dmg', + + // Linux 产物 + 'dist/*.AppImage', + 'dist/*.deb', + + // 自动更新文件 + 'dist/*.yml', + 'dist/*.yaml', + 'dist/*.blockmap' + ] } ] ] diff --git a/CHANGELOG.md b/CHANGELOG.md index a7219a54..41a00d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,694 @@ +# [1.0.0-beta.2](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2025-09-14) + +### Bug Fixes + +- **logger:** optimize logger memory management and reduce high-frequency logging ([#156](https://github.com/mkdir700/EchoPlayer/issues/156)) ([64e36a2](https://github.com/mkdir700/EchoPlayer/commit/64e36a282b08349971cd10741c01be194f4e7b55)) +- **player:** persist relocated video file path to database ([#162](https://github.com/mkdir700/EchoPlayer/issues/162)) ([25bc32b](https://github.com/mkdir700/EchoPlayer/commit/25bc32b5eabcef9342dff9f9319036e85052506e)) +- **subtitle:** resolve overlay pause/seek update delays with immediate state sync ([#153](https://github.com/mkdir700/EchoPlayer/issues/153)) ([582168f](https://github.com/mkdir700/EchoPlayer/commit/582168fd12824d3b8dd1629f263bb3b0b87bc0c7)) +- **ui:** use system title bar for Windows and Linux platforms ([#158](https://github.com/mkdir700/EchoPlayer/issues/158)) ([ee435ce](https://github.com/mkdir700/EchoPlayer/commit/ee435ce3439344d625af03e99e83d9967a46374a)) +- **updater:** remove detailed release notes from system update dialog ([#152](https://github.com/mkdir700/EchoPlayer/issues/152)) ([996e76a](https://github.com/mkdir700/EchoPlayer/commit/996e76ad394f4f37f73d7b4366058c04b2d0ac36)) +- **updater:** resolve pre-release version detection issue ([#161](https://github.com/mkdir700/EchoPlayer/issues/161)) ([3d90e67](https://github.com/mkdir700/EchoPlayer/commit/3d90e674821cffb0fe1732bfbd18830fbda9b1e9)) + +### Features + +- add Windows ARM64 architecture support ([#157](https://github.com/mkdir700/EchoPlayer/issues/157)) ([30496b1](https://github.com/mkdir700/EchoPlayer/commit/30496b1bd36ffd3765b075f9081fb876d33ee1b8)) +- **ffmpeg:** add China mirror support for FFmpeg downloads ([#164](https://github.com/mkdir700/EchoPlayer/issues/164)) ([61efdad](https://github.com/mkdir700/EchoPlayer/commit/61efdad12214b5fb1a4fd1344a96c2b496d27749)) +- **ffmpeg:** implement dynamic FFmpeg download system with runtime management ([#155](https://github.com/mkdir700/EchoPlayer/issues/155)) ([95dae5a](https://github.com/mkdir700/EchoPlayer/commit/95dae5a6594348e494a66d29ef85e948d0a8d21a)) + +### BREAKING CHANGES + +- **ffmpeg:** Service now defaults to China mirror for + better performance in Chinese regions + +- fix(test): remove unused parameter in FFmpegDownloadService test + +* Fix TypeScript error TS6133 for unused 'url' parameter +* Replace unused 'url' with underscore in mock implementation + +# 1.0.0-beta.1 (2025-09-13) + +### Bug Fixes + +- **build:** correct alpha channel update file naming ([#79](https://github.com/mkdir700/EchoPlayer/issues/79)) ([95e2ed2](https://github.com/mkdir700/EchoPlayer/commit/95e2ed262d6f29d2a645033089afe36a24afd56f)) +- **build:** Fix FFmpeg cross-platform build on macOS for Windows targets ([#145](https://github.com/mkdir700/EchoPlayer/issues/145)) ([e2857e9](https://github.com/mkdir700/EchoPlayer/commit/e2857e96ed6f3780ede4e32a9b457861b58324d8)) +- **build:** 修复 Linux 构建产物架构命名转换问题 ([1f732ba](https://github.com/mkdir700/EchoPlayer/commit/1f732ba84ed69c803c6795c19ae7b5a2e11c3b70)) +- **ci:** Failed to get next version ([8a94ceb](https://github.com/mkdir700/EchoPlayer/commit/8a94ceb304adcfc598baa0f491326ce19b0361a7)) +- **ci:** resolve duplicate GitHub releases issue ([#90](https://github.com/mkdir700/EchoPlayer/issues/90)) ([07c3e5f](https://github.com/mkdir700/EchoPlayer/commit/07c3e5f754502cd5780cbfc704aa305b3e9c2a95)) +- **ci:** resolve GitHub Release creation issue with always publish strategy ([#85](https://github.com/mkdir700/EchoPlayer/issues/85)) ([712f0e8](https://github.com/mkdir700/EchoPlayer/commit/712f0e8cc8c11241678334c80e95f778055f57b2)) +- **ci:** resolve semantic-release configuration issues ([#88](https://github.com/mkdir700/EchoPlayer/issues/88)) ([0a9e4a3](https://github.com/mkdir700/EchoPlayer/commit/0a9e4a3eb4501ade7aa25f377baab627de27b872)) +- **ci:** resolve Windows build shell syntax compatibility issue ([#84](https://github.com/mkdir700/EchoPlayer/issues/84)) ([59b8460](https://github.com/mkdir700/EchoPlayer/commit/59b846044060a4c6ddd82c490c3c8706fe9daac7)) +- **ci:** sync package.json version with manual trigger input ([#116](https://github.com/mkdir700/EchoPlayer/issues/116)) ([0ca1d39](https://github.com/mkdir700/EchoPlayer/commit/0ca1d3963208d9eb7b825c7c1b31e269532fa3eb)) +- fix type check ([eae1e37](https://github.com/mkdir700/EchoPlayer/commit/eae1e378262d1f9162fd630cbb6dd867df933fb3)) +- Fix TypeScript build errors and improve type safety ([#77](https://github.com/mkdir700/EchoPlayer/issues/77)) ([7861279](https://github.com/mkdir700/EchoPlayer/commit/7861279d8d5fd8c8e3bd5d5639f8e4b8f999b0ca)) +- **homepage:** Fix UI desynchronization issue after deleting video records + i18n support ([#120](https://github.com/mkdir700/EchoPlayer/issues/120)) ([7879ef4](https://github.com/mkdir700/EchoPlayer/commit/7879ef442b888d6956a74739c3c0c1c54bb87387)) +- improve release workflow and build configuration ([#91](https://github.com/mkdir700/EchoPlayer/issues/91)) ([4534162](https://github.com/mkdir700/EchoPlayer/commit/4534162cae55c7bc4cb28200abe86df62af9a662)) +- **player:** ensure video always starts paused and sync UI state correctly ([#102](https://github.com/mkdir700/EchoPlayer/issues/102)) ([c6c8909](https://github.com/mkdir700/EchoPlayer/commit/c6c890986d6a0137cb6a70e01144c9c995589840)) +- **player:** Fix subtitle navigation when activeCueIndex is -1 ([#119](https://github.com/mkdir700/EchoPlayer/issues/119)) ([b4ad16f](https://github.com/mkdir700/EchoPlayer/commit/b4ad16f2115d324aabd34b08b2a05ca98a3de101)) +- **player:** Fix subtitle overlay dragging to bottom and improve responsive design ([#122](https://github.com/mkdir700/EchoPlayer/issues/122)) ([d563c92](https://github.com/mkdir700/EchoPlayer/commit/d563c924c9471caeeab45a3d2ddd6dda5520fcac)) +- **player:** improve play/pause button reliability ([#141](https://github.com/mkdir700/EchoPlayer/issues/141)) ([805e774](https://github.com/mkdir700/EchoPlayer/commit/805e774cda1855ed7b4bafedc1d95c6f1e75b74a)) +- **player:** improve subtitle overlay positioning and remove i18n dependencies ([#109](https://github.com/mkdir700/EchoPlayer/issues/109)) ([bd7f5c3](https://github.com/mkdir700/EchoPlayer/commit/bd7f5c3ec319c174bd8b0244e935daef8ec90d9d)) +- **player:** integrate volume state in player engine context ([5ff32d9](https://github.com/mkdir700/EchoPlayer/commit/5ff32d91ce39d9499ae762ee433e61461e926c46)) +- **player:** Prevent subtitle overlay interactions from triggering video play/pause ([#128](https://github.com/mkdir700/EchoPlayer/issues/128)) ([9730ba1](https://github.com/mkdir700/EchoPlayer/commit/9730ba1184cffe2012dd30e9207a562e88eec140)) +- **release:** remove custom labels from GitHub release assets ([#92](https://github.com/mkdir700/EchoPlayer/issues/92)) ([f066209](https://github.com/mkdir700/EchoPlayer/commit/f066209bdab482481a4564827490580b753b3c8e)) +- remove cheerio dependency to resolve Electron packaging issues - Remove cheerio and @types/cheerio from package.json dependencies - Replace cheerio-based HTML parsing with native regex implementation - Refactor parseEudicHtml() to parseEudicHtmlWithRegex() in dictionaryHandlers.ts - Support multiple HTML formats: list items, phonetics, examples, translations - Delete related test files that depend on cheerio - Fix TypeScript type errors for regex variables - Improve Electron runtime compatibility and reduce bundle size Fixes [#50](https://github.com/mkdir700/EchoPlayer/issues/50) ([b01fe4e](https://github.com/mkdir700/EchoPlayer/commit/b01fe4e33a0027d3c4fc6fdbb7e5577fb7f4165b)) +- remove path unique constraint to allow duplicate video file addition ([#97](https://github.com/mkdir700/EchoPlayer/issues/97)) ([237dd30](https://github.com/mkdir700/EchoPlayer/commit/237dd301c995133e1781e76c2f56cf167b7a78c9)) +- **renderer:** resolve subsrt dynamic require issue in production build ([#78](https://github.com/mkdir700/EchoPlayer/issues/78)) ([028a8fb](https://github.com/mkdir700/EchoPlayer/commit/028a8fb9a9446ebb8dc7b25fb4a70fadc02fb085)) +- resolve dead links in documentation and add missing pages ([fc36263](https://github.com/mkdir700/EchoPlayer/commit/fc3626305bdbf96c0efc70ae9d989ba02a0ededa)) +- **subtitle:** improve ASS subtitle parsing for bilingual text ([#111](https://github.com/mkdir700/EchoPlayer/issues/111)) ([444476b](https://github.com/mkdir700/EchoPlayer/commit/444476bae35afcd916f098dbc544eced7542a4b9)) +- **subtitle:** prevent overlay showing content during subtitle gaps ([#138](https://github.com/mkdir700/EchoPlayer/issues/138)) ([0eb4697](https://github.com/mkdir700/EchoPlayer/commit/0eb4697b73e2a94ff1b85557d796a38e86d5ff4c)) +- **test:** resolve SubtitleLibraryDAO schema validation and test framework improvements ([#80](https://github.com/mkdir700/EchoPlayer/issues/80)) ([4be2b8a](https://github.com/mkdir700/EchoPlayer/commit/4be2b8a390c454dc1b0287e352d15ceedb4ed67b)) +- **titlebar:** keep title bar fixed at top during page scroll ([b3ff5c2](https://github.com/mkdir700/EchoPlayer/commit/b3ff5c2c6b5a8bea67da69b8e82c9200d5eb05fd)) +- **ui:** Remove white border shadow from modal buttons in dark mode ([#124](https://github.com/mkdir700/EchoPlayer/issues/124)) ([29f70f6](https://github.com/mkdir700/EchoPlayer/commit/29f70f66806f3dc0e3e473a9b3b27867cf67ac0d)) +- **updater:** resolve auto-update channel handling and version-based test defaults ([#98](https://github.com/mkdir700/EchoPlayer/issues/98)) ([e30213b](https://github.com/mkdir700/EchoPlayer/commit/e30213babd3f0cc391864df8e2b95774e6d41051)) +- **windows:** resolve file extension validation requiring double dots (.mp4 vs ..mp4) ([#126](https://github.com/mkdir700/EchoPlayer/issues/126)) ([eadebcf](https://github.com/mkdir700/EchoPlayer/commit/eadebcfba3c20912c507ed8aea3b7f0ed0e40396)), closes [#118](https://github.com/mkdir700/EchoPlayer/issues/118) [#118](https://github.com/mkdir700/EchoPlayer/issues/118) +- 优化文件路径处理逻辑以支持不同平台 ([dc4e1e3](https://github.com/mkdir700/EchoPlayer/commit/dc4e1e384588dac7e1aacc27eccf165fe2e43e4d)) +- 修复 settings 相关组件找不到的问题 ([08f88ba](https://github.com/mkdir700/EchoPlayer/commit/08f88bad7099ac110a0bae109b3501a0348f0b78)) +- 修复全屏模式下速度选择窗口溢出的问题 ([6309046](https://github.com/mkdir700/EchoPlayer/commit/63090466881d8df3e5dc062c0f235995dfe4134e)) +- 修复在 Windows 上的 FFmpeg 文件下载和 ZIP 解压 ([6347b4e](https://github.com/mkdir700/EchoPlayer/commit/6347b4e62207dc104a1fa44f27af08667ff893a2)) +- 修复在启用单句循环模式下,无法调整到下一句的问题 ([ec479be](https://github.com/mkdir700/EchoPlayer/commit/ec479beeff5c931821eed5aaffeaa054226b13c2)) +- 修复文件路径处理逻辑以支持不同的 file URL 前缀 ([740015d](https://github.com/mkdir700/EchoPlayer/commit/740015d955f8266b96d2aa49bdc244d084937355)) +- 修复方向键冲突检测问题 ([4a466c7](https://github.com/mkdir700/EchoPlayer/commit/4a466c7367860120d9a4ccc6f23ab5e79a2d8cae)) +- 修复无法通过按钮退出全屏模式的问题 ([e69562b](https://github.com/mkdir700/EchoPlayer/commit/e69562b9ead8ea66c0933ad21b5cbeae3d88142f)) +- 修复构建产物架构冲突问题 ([2398bd7](https://github.com/mkdir700/EchoPlayer/commit/2398bd78be4526a9f3f636c8f945df644bbc3d5b)) +- 修复组件导出语句和优化字幕加载逻辑,移除未使用的状态 ([39708ce](https://github.com/mkdir700/EchoPlayer/commit/39708ce48bd6652488abce7d21752a2afe994d99)) +- 删除上传到 cos 的步骤,因为网络波动问题上传失败 ([1cac918](https://github.com/mkdir700/EchoPlayer/commit/1cac918f21ad3198827138512ee61d770bd1367f)) +- 在 UpdateNotification 组件中添加关闭对话框的逻辑,确保用户在操作后能够顺利关闭对话框 ([845a070](https://github.com/mkdir700/EchoPlayer/commit/845a070ac74b513ce5bda3cdc3d3e7a803a3b8d1)) +- 始终在脚本直接执行时运行主函数,确保功能正常 ([a15378a](https://github.com/mkdir700/EchoPlayer/commit/a15378a914e54967f50642e04a111af184255344)) +- 忽略依赖项警告 ([fc3f038](https://github.com/mkdir700/EchoPlayer/commit/fc3f038bb7d9b7e6962a5346bee00c858998ade0)) +- 更新主题样式,使用 token 中的 zIndex 替代硬编码值 ([3940caf](https://github.com/mkdir700/EchoPlayer/commit/3940caf3b768efcba2043b3734bc7c7962f8c5a8)) +- 更新测试文件中的 useTheme 和 useVideoPlaybackHooks 的路径 ([4fa9758](https://github.com/mkdir700/EchoPlayer/commit/4fa9758ae7bcb26789e8a458312ef23d577a34e6)) +- 移除构建和发布工作流中的空选项,始终将草稿发布设置为 true,以确保发布过程的一致性 ([171028a](https://github.com/mkdir700/EchoPlayer/commit/171028adff214b3c696b7aaacb617c7c41b0302b)) + +### Features + +- add API communication type definitions and unified export ([ea9f1c0](https://github.com/mkdir700/EchoPlayer/commit/ea9f1c0690d3b7fe5f6a2e2406b5fb88817aa8d1)) +- add common base type definitions and interfaces for application ([73bd604](https://github.com/mkdir700/EchoPlayer/commit/73bd6046239341716eea727d95b316e9a3652ec8)) +- add debounce hooks and corresponding tests ([7646088](https://github.com/mkdir700/EchoPlayer/commit/7646088b78106e40f00ff15a3cdd86b44aa541cc)) +- add domain type definitions and constants for video, subtitle, playback, and UI ([a1c3209](https://github.com/mkdir700/EchoPlayer/commit/a1c3209271336891e0e9dbde444abc8c4e7d8e4b)) +- add git hooks with lint-staged for automated code quality checks ([1311af9](https://github.com/mkdir700/EchoPlayer/commit/1311af96159b7e7b5d31f43f27f479cc9035d5a5)) +- add handler to read directory contents ([6ce1d9e](https://github.com/mkdir700/EchoPlayer/commit/6ce1d9eff64968cef3a5673a67f8753de582d501)) +- add IPC Client Service implementation with integration tests ([fe4400f](https://github.com/mkdir700/EchoPlayer/commit/fe4400ff63ff0640f32ca94f0b4d0d4c47b246ed)) +- add performance optimization hooks and corresponding tests ([d7e1d0f](https://github.com/mkdir700/EchoPlayer/commit/d7e1d0f006dfe8c6c58a20bb0305621a657c9a65)) +- add selectors for subtitle, UI, and video states with computed properties and hooks ([c64f41d](https://github.com/mkdir700/EchoPlayer/commit/c64f41dd27496bd311ff588474041e4ebacbd3a9)) +- Add service layer type definitions for storage, video, subtitle, and dictionary services ([c658217](https://github.com/mkdir700/EchoPlayer/commit/c658217a5acc7e8a066004e6d7c1cd103be43a3b)) +- add subtitle, UI, and video state actions for V2 ([1a4042a](https://github.com/mkdir700/EchoPlayer/commit/1a4042af3e1e917d6303a560c49e4aa8d52300ab)) +- add unified export for V2 infrastructure layer type system ([ad94ea8](https://github.com/mkdir700/EchoPlayer/commit/ad94ea849bc17b5e569bd2296cf73c30ca06747d)) +- add V2 state stores with type-safe validation and comprehensive documentation ([264cc66](https://github.com/mkdir700/EchoPlayer/commit/264cc661c2be83c9d886ba673d104b499ade0729)) +- **api:** add request and response type definitions for video, subtitle, file operations, and playback settings ([c0e9324](https://github.com/mkdir700/EchoPlayer/commit/c0e9324642d6920def8dcb79a97c92ab0f552397)) +- **AutoResumeCountdown:** add auto-dismissal when playback manually resumed ([3852bca](https://github.com/mkdir700/EchoPlayer/commit/3852bca30af23c203698bc413e0b482a595c96d6)) +- **ci:** add alpha and beta branch support to test workflow ([#94](https://github.com/mkdir700/EchoPlayer/issues/94)) ([a47466b](https://github.com/mkdir700/EchoPlayer/commit/a47466b8e236af17785db098a761fa6dd30c67b5)) +- **ci:** add dynamic workflow names to show release version in actions list ([#115](https://github.com/mkdir700/EchoPlayer/issues/115)) ([1ff9550](https://github.com/mkdir700/EchoPlayer/commit/1ff95509c8849d19028bdaa29e8f79703d69424e)) +- **ci:** configure CodeRabbit for alpha, beta, and main branch PR reviews ([#108](https://github.com/mkdir700/EchoPlayer/issues/108)) ([4f5bad2](https://github.com/mkdir700/EchoPlayer/commit/4f5bad2e220f4b8e814d9a9a3df16224f4d620ae)) +- **ci:** implement semantic-release with automatic version detection ([#117](https://github.com/mkdir700/EchoPlayer/issues/117)) ([4030234](https://github.com/mkdir700/EchoPlayer/commit/4030234c7dbaf3b358f4697c5321565e7c0fdd64)) +- **ci:** implement semantic-release with three-branch strategy ([#89](https://github.com/mkdir700/EchoPlayer/issues/89)) ([c39da6e](https://github.com/mkdir700/EchoPlayer/commit/c39da6e77d4e995299a6df62f9fa565a6dd46807)) +- **ci:** integrate semantic-release automation with GitHub workflow ([#87](https://github.com/mkdir700/EchoPlayer/issues/87)) ([874bd5a](https://github.com/mkdir700/EchoPlayer/commit/874bd5a0987c5944c14926230b32a49b4886158b)) +- **ci:** migrate from action-gh-release to native electron-builder publishing ([#82](https://github.com/mkdir700/EchoPlayer/issues/82)) ([eab9ba1](https://github.com/mkdir700/EchoPlayer/commit/eab9ba1f1d8cc55d4cf6e7e6c3c8633b02938715)) +- comprehensive auto-update system implementation ([#73](https://github.com/mkdir700/EchoPlayer/issues/73)) ([0dac065](https://github.com/mkdir700/EchoPlayer/commit/0dac065d54643bc761c741bae914057b9784e419)) +- **ControllerPanel:** add disabled state for Loop and AutoPause controls when subtitles are empty ([a35f3e6](https://github.com/mkdir700/EchoPlayer/commit/a35f3e6cbf5c33ee4ebc6ca21dfb31a5b2b1b1a6)) +- **ControllerPanel:** implement centralized menu management system for player controls ([1523758](https://github.com/mkdir700/EchoPlayer/commit/152375846dc6d5acf84d07a0d182160e29de4358)) +- **db:** implement complete SQLite3 database layer with migrations and DAOs ([0a8a7dd](https://github.com/mkdir700/EchoPlayer/commit/0a8a7ddb5a240c6d6c145b4cfa0790e2292d3697)) +- **db:** migrate from Dexie to Kysely with better-sqlite3 backend ([6b75cd8](https://github.com/mkdir700/EchoPlayer/commit/6b75cd877bd117b84d6205eb1c680450042b6eaa)) +- define domain types for video, subtitle, and UI, and refactor RecentPlayItem interface imports ([f632beb](https://github.com/mkdir700/EchoPlayer/commit/f632beba5f9b20afb2ec6fc8e6df7dcba6fd29f0)) +- **dictionary:** expose DictionaryService API in preload and add comprehensive tests ([#143](https://github.com/mkdir700/EchoPlayer/issues/143)) ([70d289d](https://github.com/mkdir700/EchoPlayer/commit/70d289d323c6f429e4cfd80cb1a6a7dc9081c063)) +- enhance macOS build configuration with additional entitlements and notarization support ([d6e8ced](https://github.com/mkdir700/EchoPlayer/commit/d6e8ced7611b9f09743bc29344c6a0020ed0b19d)) +- **ffmpeg:** integrate bundled FFmpeg with automatic fallback mechanism ([#112](https://github.com/mkdir700/EchoPlayer/issues/112)) ([5a4f26a](https://github.com/mkdir700/EchoPlayer/commit/5a4f26a230f8bf54f31873d30ed05abfbf887b06)) +- **home:** implement empty state with video file selection integration ([b6f6e40](https://github.com/mkdir700/EchoPlayer/commit/b6f6e401f622f1390926eb154f47fad812d5d0a7)) +- implement Ctrl+C subtitle copy with lightweight toast notification ([#140](https://github.com/mkdir700/EchoPlayer/issues/140)) ([22005bb](https://github.com/mkdir700/EchoPlayer/commit/22005bb1a75714fbe5c4e85821fe47a235401a73)), closes [#142](https://github.com/mkdir700/EchoPlayer/issues/142) +- Implement dictionary engine framework ([0d74a83](https://github.com/mkdir700/EchoPlayer/commit/0d74a8315f86209c530ad2f306b6099e97328c1f)) +- implement useThrottle hooks and corresponding tests ([da30344](https://github.com/mkdir700/EchoPlayer/commit/da303443b6847fe049be9c4592c98418aeea9785)) +- implement version parsing and channel mapping logic ([8c95a2f](https://github.com/mkdir700/EchoPlayer/commit/8c95a2feeef1d066fb46f246724d8a94014fa627)) +- **infrastructure:** add entry points for constants and shared modules, and refine video playback rate type ([94da255](https://github.com/mkdir700/EchoPlayer/commit/94da2556dd6909eec75d4edfe2b549e3858e2629)) +- **logger:** export logger instance for easier access in modules ([5328152](https://github.com/mkdir700/EchoPlayer/commit/5328152999408cbec0515e501aea9f393032b933)) +- **performance:** implement video import performance optimization with parallel processing and warmup strategies ([#121](https://github.com/mkdir700/EchoPlayer/issues/121)) ([2c65f5a](https://github.com/mkdir700/EchoPlayer/commit/2c65f5ae92460391302c24f3fb291f386043e7cd)) +- **persistence:** add V2 state persistence manager and configuration files ([a545020](https://github.com/mkdir700/EchoPlayer/commit/a545020f3f676b526b3f3bf3ecff6d23c4c1a471)) +- **playback:** update playback rate label for clarity and add storage type definitions ([5c40b98](https://github.com/mkdir700/EchoPlayer/commit/5c40b983ea22c1e57e5e678922c5d6492a39359c)) +- player page ([aa79279](https://github.com/mkdir700/EchoPlayer/commit/aa792799580524ac65c8bf7e4d9bc7e13988a716)) +- **player,logging,state:** orchestrated player engine with intent strategies and new controller panel ([73d7cfd](https://github.com/mkdir700/EchoPlayer/commit/73d7cfdc4b58f190afe8981eddadbca54c6763b0)) +- **player:** add Ctrl+] shortcut for subtitle panel toggle ([#69](https://github.com/mkdir700/EchoPlayer/issues/69)) ([e1628f2](https://github.com/mkdir700/EchoPlayer/commit/e1628f2d04ea03cac20893960a3a9fec2dd9fdb2)) +- **player:** hide sidebar and optimize navbar for player page ([#70](https://github.com/mkdir700/EchoPlayer/issues/70)) ([5bb71e4](https://github.com/mkdir700/EchoPlayer/commit/5bb71e4465721c3b1f0dc326f9a92e879e1c048b)) +- **player:** implement auto-resume countdown with UI notification ([5468f65](https://github.com/mkdir700/EchoPlayer/commit/5468f6531c3e2f3d8aaf94f631ec4f760d04241f)) +- **player:** implement comprehensive video error recovery ([#113](https://github.com/mkdir700/EchoPlayer/issues/113)) ([c25f17a](https://github.com/mkdir700/EchoPlayer/commit/c25f17ab83b0be7a60bbd589d971637bf74eb8b6)) +- **player:** implement favorite playback rates with hover menu system ([#100](https://github.com/mkdir700/EchoPlayer/issues/100)) ([df83095](https://github.com/mkdir700/EchoPlayer/commit/df830955971080e5db393590a082e86718da10cd)) +- **player:** Implement fullscreen toggle functionality with keyboard shortcuts ([#127](https://github.com/mkdir700/EchoPlayer/issues/127)) ([78d3629](https://github.com/mkdir700/EchoPlayer/commit/78d3629c7d5a14e8bc378967a7f161135c5b5042)) +- **player:** implement hover menu system for control panel components ([#99](https://github.com/mkdir700/EchoPlayer/issues/99)) ([526e71a](https://github.com/mkdir700/EchoPlayer/commit/526e71a7a40268e88a0ba6c9f7dab9565d929a21)) +- **player:** implement volume wheel control with intelligent acceleration ([#105](https://github.com/mkdir700/EchoPlayer/issues/105)) ([b675150](https://github.com/mkdir700/EchoPlayer/commit/b6751504ccff70f71fe518393587b3e70e6d7dba)) +- **player:** reposition progress bar between video and controls ([#71](https://github.com/mkdir700/EchoPlayer/issues/71)) ([248feed](https://github.com/mkdir700/EchoPlayer/commit/248feed534974b6c05ef2918a3758ae5fed1f42d)) +- refine RecentPlayItem interface with detailed video info and playback metrics ([81679b6](https://github.com/mkdir700/EchoPlayer/commit/81679b6eb54f7a8f07a33065c743fa51d1eecc5d)) +- replace FFmpeg with MediaInfo for video metadata extraction ([#95](https://github.com/mkdir700/EchoPlayer/issues/95)) ([2f64029](https://github.com/mkdir700/EchoPlayer/commit/2f640299078bc354735c8c665c190c849dc52615)) +- **scripts:** optimize FFmpeg download progress display ([#125](https://github.com/mkdir700/EchoPlayer/issues/125)) ([be33316](https://github.com/mkdir700/EchoPlayer/commit/be33316f0a66f7b5b2de64d275d7166f12f50379)) +- **search:** implement video search engine with live results and highlighting ([#110](https://github.com/mkdir700/EchoPlayer/issues/110)) ([bb2ac30](https://github.com/mkdir700/EchoPlayer/commit/bb2ac3028a45df5e49f74ac4cab6752806af048d)) +- setup GitHub Pages deployment for documentation ([b8a42b9](https://github.com/mkdir700/EchoPlayer/commit/b8a42b974d7490ddddb4292608315a58df14a24b)) +- **sidebar:** 禁用收藏按钮并添加开发中提示 ([#81](https://github.com/mkdir700/EchoPlayer/issues/81)) ([76e9b54](https://github.com/mkdir700/EchoPlayer/commit/76e9b5418ec5f07f4d3052d8130163d108965f47)) +- **SplashScreen:** add animated splash screen with typewriter effect and smooth transitions ([31cfeca](https://github.com/mkdir700/EchoPlayer/commit/31cfeca9f0773db12b9a7b299820a32c309d9daf)) +- **startup:** implement configurable startup intro with preloading optimization ([#104](https://github.com/mkdir700/EchoPlayer/issues/104)) ([e5f4109](https://github.com/mkdir700/EchoPlayer/commit/e5f41092843333c39dc2029bd040aae6854a1036)) +- **state.store:** 新增多个 store ([54e7ff5](https://github.com/mkdir700/EchoPlayer/commit/54e7ff5993631c6133e21948c2697bcd13919df6)) +- **state:** implement V2 state management infrastructure with storage engine, middleware, and utility functions ([e225746](https://github.com/mkdir700/EchoPlayer/commit/e22574659d1ec63fbc622152fec2371323b4fe53)) +- **storage:** implement application configuration storage service ([1209b56](https://github.com/mkdir700/EchoPlayer/commit/1209b56fae690a9876440faa679f072cb2ebc6da)) +- **subtitle-library:** Add subtitle data caching for improved loading performance ([#86](https://github.com/mkdir700/EchoPlayer/issues/86)) ([40be325](https://github.com/mkdir700/EchoPlayer/commit/40be325f09f9f70e702260dfe29e354c6c7435b6)) +- **SubtitleContent:** implement word-level tokenization and interactive text selection ([10c0cdf](https://github.com/mkdir700/EchoPlayer/commit/10c0cdf38fdbad53bfba4b82148b635e45657b11)) +- **types:** add Serializable interface for flexible data structures ([32981df](https://github.com/mkdir700/EchoPlayer/commit/32981df183af23d9153ae65765b9d1c8a533540e)) +- **ui:** enhance video selection clarity and simplify display ([#101](https://github.com/mkdir700/EchoPlayer/issues/101)) ([a951877](https://github.com/mkdir700/EchoPlayer/commit/a95187723ddc7706f52e72a66f61ceb6b2ceb439)) +- update macOS notarization configuration to enable automatic notarization ([6630e79](https://github.com/mkdir700/EchoPlayer/commit/6630e79975a5d39eea83bec44ef1ff0271c984da)) +- **video.store:** add format property to CurrentVideoState and update video loading simulation ([0349a63](https://github.com/mkdir700/EchoPlayer/commit/0349a6351ba8fa2a1235a48b268825ad18ea37ff)) +- **VolumeControl:** change volume popup from horizontal to vertical layout ([d4d435b](https://github.com/mkdir700/EchoPlayer/commit/d4d435b93a72b8bb8e1f9d4fe356fa41632d8993)) +- **wsl:** add WSL detection and hardware acceleration optimization ([c99403e](https://github.com/mkdir700/EchoPlayer/commit/c99403efa8af9fa9ebf106edcbdc4a0d21b31b2e)) +- 为字幕组件新增右键菜单功能 ([62334d5](https://github.com/mkdir700/EchoPlayer/commit/62334d56bb0956b28582d5d70e7ea0a3c2f9e42d)) +- 为音量控制组件添加音量调节快捷键 ([144d49c](https://github.com/mkdir700/EchoPlayer/commit/144d49c314688c6b7b0abbdd0c21f57c98f3084d)) +- 优化 PlayPage 组件性能,减少不必要的重新渲染;重构播放状态管理逻辑,提升用户体验 ([24a2ebc](https://github.com/mkdir700/EchoPlayer/commit/24a2ebc6d7c040ffce5ffc1a404f338ad77a6791)) +- 优化最近观看记录加载状态显示 ([e5f7e11](https://github.com/mkdir700/EchoPlayer/commit/e5f7e11498d52028cf20765ea2fe486cca49f3d1)) +- 优化单词查询逻辑,增加超时处理和取消请求功能,提升用户体验和性能 ([c98dc4b](https://github.com/mkdir700/EchoPlayer/commit/c98dc4b5d65bdc7c68d82f6e0c1bcda248929503)) +- 优化字幕列表滚动体验 ([63807c5](https://github.com/mkdir700/EchoPlayer/commit/63807c5a4a668d5000c7c920dcae5eb96623314e)) +- 优化字幕控制功能,新增字幕模式选择器,提升用户交互体验;重构相关组件,移除不必要的代码,简化逻辑 ([559aada](https://github.com/mkdir700/EchoPlayer/commit/559aada0c7d224bdbc941b30aa9da71b08d84636)) +- 优化字幕文本分段逻辑 ([33e591c](https://github.com/mkdir700/EchoPlayer/commit/33e591c08ac1d520886b20b2b0219e15eeea1d0d)) +- 优化字幕模式选择器组件,增强用户体验和视觉一致性,添加响应式设计和毛玻璃效果 ([4b59a1b](https://github.com/mkdir700/EchoPlayer/commit/4b59a1b0ac1d3a66dfd0c129177f2820722f2416)) +- 优化快捷键设置界面,增强输入状态反馈,更新样式以提升用户体验和一致性 ([64db31c](https://github.com/mkdir700/EchoPlayer/commit/64db31cd8112b08f509e16ef653f687381f5f636)) +- 优化日志记录功能,新增组件渲染节流和数据简化处理,提升性能和可读性 ([a6e8480](https://github.com/mkdir700/EchoPlayer/commit/a6e8480ecb2458eefd899d0828af3955f3cbef6e)) +- 优化标题栏平台信息处理 ([fb5a470](https://github.com/mkdir700/EchoPlayer/commit/fb5a470084ed46f6f662e060d4cbca89fca1736e)) +- 优化视频卡片和视频网格组件的样式与布局 ([af59b44](https://github.com/mkdir700/EchoPlayer/commit/af59b44aa7cefeb06dfd87656532af3652013574)) +- 优化词典查询逻辑,增加未找到释义时的警告提示,并调整相关代码结构 ([9f528bd](https://github.com/mkdir700/EchoPlayer/commit/9f528bd89e905af2490c9c1b2db3d9a00b19e1f8)) +- 优化进度条handler显示和对齐 ([77d0496](https://github.com/mkdir700/EchoPlayer/commit/77d04965d4ac4aa2bfff965fe2852c82d9e4c91e)) +- 在 FullscreenTestInfo 组件中新增折叠功能 ([b2eac46](https://github.com/mkdir700/EchoPlayer/commit/b2eac461e616b3da6be84ceabaffd08c583d1b59)) +- 在 HomePage 组件中新增音频兼容性诊断功能,优化视频播放体验;更新视频兼容性报告以支持音频编解码器检测;重构相关逻辑以提升代码可读性和维护性 ([3cac307](https://github.com/mkdir700/EchoPlayer/commit/3cac307f50c791b2f4f76b3e0aec7571d4e30a98)) +- 在 SubtitleListContent 组件中引入 rc-virtual-list 以优化字幕列表渲染性能,增强自动滚动功能 ([30165dc](https://github.com/mkdir700/EchoPlayer/commit/30165dcf42f2770be23392f6c6d07a8d1786f95f)) +- 在 UpdatePromptDialog 组件中添加内容展开/折叠功能 ([96d9b1f](https://github.com/mkdir700/EchoPlayer/commit/96d9b1f32b15abea3661836a684e177e774b80e5)) +- 在构建和发布工作流中添加Windows、Mac和Linux平台的上传步骤,优化版本变量设置和上传路径逻辑 ([3cfacb9](https://github.com/mkdir700/EchoPlayer/commit/3cfacb91cd3c60428af09bdb1b1ed74be1538e29)) +- 在构建和发布工作流中添加更新 package.json 版本的步骤,确保版本号自动更新;优化草稿发布条件以支持预发布版本 ([a78fbc7](https://github.com/mkdir700/EchoPlayer/commit/a78fbc72166e08eeec641c0970eebf83763aba39)) +- 在构建和发布工作流中添加测试构建选项,更新版本变量设置和上传路径逻辑 ([2848f92](https://github.com/mkdir700/EchoPlayer/commit/2848f92f7216dee720d84608ffce2840f5f67bcd)) +- 在视频上传时重置字幕控制状态,新增重置状态功能;更新快捷键设置以支持单句循环功能,优化用户体验 ([688dcd6](https://github.com/mkdir700/EchoPlayer/commit/688dcd6e7ddfe43499035fd828bcf26f04e08d79)) +- 增强全屏模式下的样式支持 ([94a77b1](https://github.com/mkdir700/EchoPlayer/commit/94a77b1166173b73789d14daaba12d0b7de2790a)) +- 增强全屏模式的快捷键支持 ([218882c](https://github.com/mkdir700/EchoPlayer/commit/218882cdbdd597dff7cf41df3dac9e0587b43dd0)) +- 增强字幕显示组件,新增中文字符检测和智能文本分割功能,优化用户交互体验 ([8cd50d9](https://github.com/mkdir700/EchoPlayer/commit/8cd50d9df0e2884f27187bb0df66a2f0f3c232b2)) +- 增强字幕空状态组件,支持拖拽文件导入 ([db1f608](https://github.com/mkdir700/EchoPlayer/commit/db1f60833f27b75594790387c0382cc30abd28fe)) +- 增强字幕组件交互功能 ([3e7e8c7](https://github.com/mkdir700/EchoPlayer/commit/3e7e8c74651da43cbcd5e525ba76324e6c403fd8)) +- 增强字幕组件和文本选择功能 ([36c44aa](https://github.com/mkdir700/EchoPlayer/commit/36c44aae0884e22030aae37db7424ac92e3f2c60)) +- 增强快捷键设置功能,新增快捷键冲突检查和平台特定符号显示,优化用户输入体验和界面样式 ([bde034b](https://github.com/mkdir700/EchoPlayer/commit/bde034bccab0dec6dbeb305fd5c4b7aca76caa91)) +- 增强更新通知系统,添加红点提示和用户交互逻辑 ([fdf4c81](https://github.com/mkdir700/EchoPlayer/commit/fdf4c811e2cf2611319adc6b706e12b5510fe5c8)) +- 增强版本比较逻辑,优化更新通知系统 ([f29a25f](https://github.com/mkdir700/EchoPlayer/commit/f29a25fc4d9859375654c8c3e1f532224e8e3049)) +- 增强视频兼容性模态框功能,支持初始步骤和分析结果 ([3aba45c](https://github.com/mkdir700/EchoPlayer/commit/3aba45c3464151598c1b8400c8e14d2c612f53bf)) +- 多平台构建和发布 ([cc521ea](https://github.com/mkdir700/EchoPlayer/commit/cc521ea8befde2b839810292e93475a806db4dd1)) +- 实现动态 electron-updater 渠道配置 ([28d2836](https://github.com/mkdir700/EchoPlayer/commit/28d28360a4e5cee11603cd68f959098d4e40ca0b)), closes [#3](https://github.com/mkdir700/EchoPlayer/issues/3) +- 将发布提供者从 generic 更改为 github,更新仓库和所有者信息,以支持自动更新功能 ([b6d4076](https://github.com/mkdir700/EchoPlayer/commit/b6d4076ff094f31d5f4eedf08e6b943f41f5fed6)) +- 引入常量以支持视频容器格式检查 ([da68183](https://github.com/mkdir700/EchoPlayer/commit/da681831b60f4655b72731fd1ba34e5550149543)) +- 新增 AimButton 组件以支持手动定位当前字幕并启用自动滚动;更新 SubtitleListContent 组件以集成 AimButton,优化用户滚动体验与字幕自动滚动逻辑 ([3c8a092](https://github.com/mkdir700/EchoPlayer/commit/3c8a09208d773f7a7e5d86bfb6a7ef26cfadf444)) +- 新增 AppHeader 组件并更新样式,调整导航菜单布局以提升用户体验 ([94e35c3](https://github.com/mkdir700/EchoPlayer/commit/94e35c30ff96b534046190cbd654097f0b960095)) +- 新增 cmd-reason.mdc 文件并更新 cmd-refactor-theme.mdc 规则 ([43d2222](https://github.com/mkdir700/EchoPlayer/commit/43d22225b7dd3546a90aec05ee5efb2dd158c8f6)) +- 新增 E2E 测试用例和文件选择器助手 ([9928349](https://github.com/mkdir700/EchoPlayer/commit/99283494eddf4612fa0d9434473974337045b052)) +- 新增 git commit 内容生成规则文件 ([6e0ee23](https://github.com/mkdir700/EchoPlayer/commit/6e0ee238be5d22aed2e23bf8cb4f51d5918d2a51)) +- 新增主题系统 ([369d828](https://github.com/mkdir700/EchoPlayer/commit/369d828232f0e07d1212e750b961871fe8024a3f)) +- 新增全屏模式支持 ([e8c9542](https://github.com/mkdir700/EchoPlayer/commit/e8c9542fef5766a048bd1fa65f11858cf1a44e7e)) +- 新增全屏视频进度条组件并重构视频控制逻辑 ([7fc587f](https://github.com/mkdir700/EchoPlayer/commit/7fc587f93312c6d34869543ffab8153a20aa2975)) +- 新增划词选中和快捷复制功能 ([9e22b44](https://github.com/mkdir700/EchoPlayer/commit/9e22b44a921ddea67f1ee95931c65116c619a9c2)) +- 新增单词卡片组件,支持单词点击后显示详细信息和发音功能;优化字幕显示样式,提升用户交互体验 ([c6a4ab6](https://github.com/mkdir700/EchoPlayer/commit/c6a4ab6446e9ebc9e55d46b52d84e93987673706)) +- 新增字幕列表上下文及相关钩子,重构播放页面以使用新的字幕管理逻辑,提升代码可读性与功能性 ([7766b74](https://github.com/mkdir700/EchoPlayer/commit/7766b74f5b7d472c94e78678f685ae1934e9c617)) +- 新增字幕列表项样式并禁用焦点样式 ([654a0d1](https://github.com/mkdir700/EchoPlayer/commit/654a0d1d6581749a8c651d322444df60252dff38)) +- 新增字幕布局锁定功能 ([82e75dc](https://github.com/mkdir700/EchoPlayer/commit/82e75dcb0741f6275fcf2863fccb6244383c75b2)) +- 新增字幕模式覆盖层组件及相关逻辑 ([e75740c](https://github.com/mkdir700/EchoPlayer/commit/e75740cd20e588ae2542ece91acebd4f86206b51)) +- 新增字幕空状态组件和外部链接打开功能 ([5bd4bd6](https://github.com/mkdir700/EchoPlayer/commit/5bd4bd6cb5f283114c83188c15301afe50b5d3c6)) +- 新增字幕组件样式,重构相关组件以支持主题系统,提升视觉一致性和用户体验 ([822cb74](https://github.com/mkdir700/EchoPlayer/commit/822cb74a9348d89527f3871ba7d37f92952e3165)) +- 新增字幕重置功能,优化字幕设置管理;重构相关组件以提升用户体验和代码可维护性 ([f4702a5](https://github.com/mkdir700/EchoPlayer/commit/f4702a5f59b77e36a8301c856c1ab81c3d8e26b5)) +- 新增存储管理功能,添加最近播放项的增删改查接口,优化用户体验;重构相关组件,提升代码结构与可维护性 ([a746ed3](https://github.com/mkdir700/EchoPlayer/commit/a746ed388e2476c8a84f45ec13f7e5ab6af8ad82)) +- 新增当前字幕显示上下文管理,优化字幕点击交互逻辑,确保用户体验流畅;重构相关组件以提升代码可维护性 ([91a215d](https://github.com/mkdir700/EchoPlayer/commit/91a215d0fa116f04c8f88403123574f0d6d7dd6f)) +- 新增快捷键设置模态框和快捷键显示组件,优化用户输入体验 ([b605257](https://github.com/mkdir700/EchoPlayer/commit/b605257cd97fec50e58143eba479e39defe449b6)) +- 新增控制弹窗样式并优化字幕模式选择器的交互体验;重构相关组件以提升代码可读性和用户体验 ([79eabdf](https://github.com/mkdir700/EchoPlayer/commit/79eabdfc684ba172425afc80dc61bb47ea95c78d)) +- 新增播放设置上下文,重构相关组件以支持播放设置的管理;更新播放页面以使用新的播放设置上下文,提升代码可读性与功能性 ([6fe8b4f](https://github.com/mkdir700/EchoPlayer/commit/6fe8b4fed2d3ea0bf9b2487f48fe7bf98d293ba6)) +- 新增播放速度覆盖层和相关功能 [#1](https://github.com/mkdir700/EchoPlayer/issues/1) ([d8637eb](https://github.com/mkdir700/EchoPlayer/commit/d8637eb6046ce8f24b0e4e08794681bf59a93ba9)) +- 新增数据清理功能,优化日志记录中的数据序列化,确保记录的日志信息更为准确和安全 ([8ada21a](https://github.com/mkdir700/EchoPlayer/commit/8ada21a07acc9dcb06a59b205f9b42c326d6472f)) +- 新增数据目录管理功能 ([2c93e19](https://github.com/mkdir700/EchoPlayer/commit/2c93e19e51efadcf0a91e55ae07b40c0589f2f2a)) +- 新增日志系统,集成 electron-log 以支持主进程和渲染进程的日志记录;更新相关 API 以便于日志管理和调试 ([1f621d4](https://github.com/mkdir700/EchoPlayer/commit/1f621d42eaa8cce3ca13a1eec4c6fb5235a2d671)) +- 新增智能分段功能及相关测试 ([f5b8f5c](https://github.com/mkdir700/EchoPlayer/commit/f5b8f5c96a00b64bc820335a3ed16083a7e44ce0)) +- 新增视频UI配置管理功能 ([eaf7e41](https://github.com/mkdir700/EchoPlayer/commit/eaf7e418bf8d6ea7169f243e3afef5d2b8cb542a)) +- 新增视频管理组件和确认模态框 ([4263c67](https://github.com/mkdir700/EchoPlayer/commit/4263c672a1bba108b83d80a1cfa78d71b6c6edb9)) +- 新增视频转码功能及兼容性警告模态框 ([4fc86a2](https://github.com/mkdir700/EchoPlayer/commit/4fc86a28338e9814fb1b2c98780645cf23f35cda)) +- 新增第三方服务配置组件,整合 OpenAI 和词典服务设置,优化用户界面和交互体验;引入模块化样式,提升整体一致性 ([3e45359](https://github.com/mkdir700/EchoPlayer/commit/3e45359efb188e7108ee4eb9663768b18b444678)) +- 新增获取所有字幕的功能,优化字幕查找逻辑以支持根据当前时间查找上下句字幕,提升用户体验 ([04c5155](https://github.com/mkdir700/EchoPlayer/commit/04c5155f1276968967591acbaedb36200915a5cc)) +- 新增词典服务相关的 IPC 处理器,支持有道和欧陆词典的 API 请求;实现 SHA256 哈希计算功能,增强应用的词典查询能力 ([707ee97](https://github.com/mkdir700/EchoPlayer/commit/707ee97b2680efcf9057acfd802e6113d4f89d8d)) +- 新增边距验证逻辑,优化字幕拖拽和调整大小功能,确保字幕区域不超出容器边界 ([2294bcf](https://github.com/mkdir700/EchoPlayer/commit/2294bcffac6fc83e4d21e543c276cceaea0189ff)) +- 更新 AppHeader 组件,增加背景装饰、应用图标和名称,优化导航按钮和辅助功能按钮的样式,提升用户体验 ([651c8d7](https://github.com/mkdir700/EchoPlayer/commit/651c8d79acf0649f24a30acc4a7a714f112ec85a)) +- 更新 AppHeader 组件,调整文本样式和名称,提升视觉效果 ([f208d66](https://github.com/mkdir700/EchoPlayer/commit/f208d66199d33de47c8b3f885c6f95ca655081ac)) +- 更新 GitHub Actions 工作流和文档,支持更多发布文件 ([c4bf6f7](https://github.com/mkdir700/EchoPlayer/commit/c4bf6f7a00d332a3e71f0796dbbdcf3c397ef175)) +- 更新 index.html 文件,修改内容安全策略以支持新的脚本源,添加本地开发服务器的支持,优化页面加载逻辑 ([8c11edf](https://github.com/mkdir700/EchoPlayer/commit/8c11edfc841058448c24be872d641b98beda52ec)) +- 更新 PlaybackRateSelector 组件样式和文本 ([034e758](https://github.com/mkdir700/EchoPlayer/commit/034e7581ec6facdffbd7cafc276449f7733c231b)) +- 更新 SubtitleListContent 组件,替换 rc-virtual-list 为 react-virtualized,优化字幕列表渲染性能与用户体验;调整样式以适配虚拟列表,增强滚动效果与响应式设计 ([63d9ef4](https://github.com/mkdir700/EchoPlayer/commit/63d9ef4229b9e159b0da5ae272229d192cc27a25)) +- 更新 SubtitleListContent 组件,添加激活字幕索引状态以优化渲染逻辑;重构字幕项组件以减少不必要的重渲染并提升性能;增强自动滚动逻辑,确保用户体验流畅 ([c997109](https://github.com/mkdir700/EchoPlayer/commit/c997109154faf0a92186bb94a8d2a019d85086e2)) +- 更新E2E测试,移除冗余测试用例并优化测试ID使用 ([51fd721](https://github.com/mkdir700/EchoPlayer/commit/51fd721ecd84bf54472f18298e0541d20d0d1cb8)) +- 更新E2E测试配置,添加Linux虚拟显示器支持并检查构建输出 ([ac1999f](https://github.com/mkdir700/EchoPlayer/commit/ac1999f8b30cf8cf48f9528b93b3fcb68b1c1b79)) +- 更新主题系统,新增字体粗细、间距、圆角等设计令牌,优化组件样式一致性 ([62f87dd](https://github.com/mkdir700/EchoPlayer/commit/62f87dd4fc868eefaf7d26008204edde9e778bb4)) +- 更新侧边栏导航功能和禁用状态提示 ([d41b25f](https://github.com/mkdir700/EchoPlayer/commit/d41b25f88d5a5b428b3a159db432fa951178a469)) +- 更新最近播放项管理,使用文件ID替代原有ID,新增根据文件ID获取最近播放项的功能,优化播放设置管理,提升代码可维护性 ([920856c](https://github.com/mkdir700/EchoPlayer/commit/920856c095a8ac4d5d41dab635390493c13774ad)) +- 更新图标文件,替换Mac和Windows平台的图标,优化SVG图标文件结构 ([bfe456f](https://github.com/mkdir700/EchoPlayer/commit/bfe456f9109fd99022796d8be8c533ba31c1fd9f)) +- 更新图标资源,替换 PNG 格式图标并新增 SVG 格式图标,提升图标的可扩展性与清晰度 ([8eaf560](https://github.com/mkdir700/EchoPlayer/commit/8eaf5600cff468fceb1d36bca6416a52e43f9aa9)) +- 更新字典引擎设置,默认选择为 'eudic-html',提升用户体验 ([ebaa5d2](https://github.com/mkdir700/EchoPlayer/commit/ebaa5d290cbbfcd1c2a5f5d7b8ed99ce9bbad449)) +- 更新字幕上下文菜单,优化重置按钮状态和样式 ([cc542f2](https://github.com/mkdir700/EchoPlayer/commit/cc542f27e241c920e80272cc2c68d2aaa7ba00da)) +- 更新字幕列表项组件,添加注释以说明仅展示学习语言,优化双语字幕显示逻辑 ([89e2b33](https://github.com/mkdir700/EchoPlayer/commit/89e2b33e65f7565ee4b89a329963c66b29a78df6)) +- 更新字幕加载功能,新增对 ASS/SSA 格式的支持;优化字幕文件扩展名和解析逻辑,提升用户体验 ([9cab843](https://github.com/mkdir700/EchoPlayer/commit/9cab843eeb33c3429ce1c2a9e78f33eeee743191)) +- 更新字幕加载模态框样式,新增加载状态提示与取消功能;重构相关逻辑以提升用户体验与代码可读性 ([1f8442a](https://github.com/mkdir700/EchoPlayer/commit/1f8442a0f6eec1afd4421f520774b4132528a3a2)) +- 更新字幕展示组件样式,添加浮动控制按钮及其样式,优化响应式设计 ([ac586e2](https://github.com/mkdir700/EchoPlayer/commit/ac586e2cd1e7670855b0b92bc3dc887ec9586658)) +- 更新字幕控制功能,添加自动暂停选项,修改快捷键设置,优化相关逻辑和组件交互 ([428e4cf](https://github.com/mkdir700/EchoPlayer/commit/428e4cfc1ba2f3856e604dd82614388c1e2d09a0)) +- 更新字幕模式选择器,整合字幕显示模式的获取逻辑,优化状态管理,增强调试信息 ([c2d3c90](https://github.com/mkdir700/EchoPlayer/commit/c2d3c90cfa07c64fbd4a21ef0ee962cc389b121f)) +- 更新循环播放设置,支持无限循环和自定义次数 ([e6c5d2e](https://github.com/mkdir700/EchoPlayer/commit/e6c5d2e3b291b3e5e4c562e43da278673c51ae23)) +- 更新快捷键设置,修改单句循环和字幕导航的快捷键,优化用户体验 ([ce66e62](https://github.com/mkdir700/EchoPlayer/commit/ce66e6208e920bc6d75a1750c06de27c2958f7cd)) +- 更新总结规则,启用始终应用选项;新增指令处理逻辑以提取项目开发指导内容并编写开发文档,确保文档规范性 ([d627e2e](https://github.com/mkdir700/EchoPlayer/commit/d627e2ec7676413f96950f580a6cddc73c9ff325)) +- 更新构建产物处理逻辑,支持多架构文件重命名和 YAML 文件引用更新 ([e206e1d](https://github.com/mkdir700/EchoPlayer/commit/e206e1d5386855e5819ce6b74000487d51aa2d77)) +- 更新构建配置,支持多架构构建和文件重命名 ([17b862d](https://github.com/mkdir700/EchoPlayer/commit/17b862d57bde74e4cff8c4f89ae423b183b1e9ed)) +- 更新样式文件,优化警告框和卡片组件的视觉效果,增强响应式设计支持 ([ea6b4ab](https://github.com/mkdir700/EchoPlayer/commit/ea6b4ab9142e5cade134113e613c69b109b86889)) +- 更新滚动条样式以支持 WebKit 规范 ([224f41d](https://github.com/mkdir700/EchoPlayer/commit/224f41d853a274324d5d1bbbf4ac7d07214cca96)) +- 更新视频上传钩子,使用日志系统记录视频DAR信息和错误警告,提升调试能力 ([2392b38](https://github.com/mkdir700/EchoPlayer/commit/2392b3806dfdf8134555a6b006ea833065459a09)) +- 更新视频兼容性模态框样式,提升用户体验 ([f5c1ba5](https://github.com/mkdir700/EchoPlayer/commit/f5c1ba5e42d44d65b5c0df55c70e9e2f44cbb855)) +- 更新视频播放器和播放状态管理逻辑,重构字幕处理方式,统一使用 subtitleItems 以提升代码一致性与可读性;优化播放状态保存与恢复机制,确保更流畅的用户体验 ([0cbe11d](https://github.com/mkdir700/EchoPlayer/commit/0cbe11d4324806dbdab67dd181ac28acd5e45c06)) +- 更新视频播放器的时间跳转逻辑,支持来源标记 ([f170ff1](https://github.com/mkdir700/EchoPlayer/commit/f170ff1b508c40bb122a20262ba436e4132c77da)) +- 更新视频文件信息样式,添加文件名截断功能,优化头部布局以提升用户体验 ([a6639f1](https://github.com/mkdir700/EchoPlayer/commit/a6639f1494862620bc0fec6f7e140e6bd773335f)) +- 更新窗口管理和标题栏组件,优化样式和功能 ([a1b50f6](https://github.com/mkdir700/EchoPlayer/commit/a1b50f6c52142a9cc2c2df12b98644e1e11ddfa6)) +- 更新窗口管理器的窗口尺寸和最小尺寸,优化用户界面;移除不必要的响应式设计样式,简化 CSS 结构 ([dd561cf](https://github.com/mkdir700/EchoPlayer/commit/dd561cf35995ebd504553a26d2c83b73da06e3f1)) +- 更新第三方服务配置组件,修改标签和提示文本为中文,增强用户友好性;新增申请应用ID和密钥的链接提示,提升信息获取便利性 ([5e68e85](https://github.com/mkdir700/EchoPlayer/commit/5e68e8507f1d5509cdcb2fb3459a570d92287aa9)) +- 更新设置导航组件样式和功能 ([535f267](https://github.com/mkdir700/EchoPlayer/commit/535f267b140bc918672fdadaae6445b9eda0707f)) +- 更新设置页面,移除视频转换相关功能 ([0d96fac](https://github.com/mkdir700/EchoPlayer/commit/0d96facf476cd73aa64fd023fb01c3a2442d0dbe)) +- 更新设置页面,简化快捷键和数据管理部分的渲染逻辑,新增存储设置选项,优化用户界面和交互体验 ([9942740](https://github.com/mkdir700/EchoPlayer/commit/9942740d9bca7ba55cd4f730ed2214ed405ed867)) +- 更新设置页面样式和主题支持 ([816ca6d](https://github.com/mkdir700/EchoPlayer/commit/816ca6d3d747ace1a18cf5f01523ee56ab8cb120)) +- 更新设置页面的按钮样式和移除音频兼容性警告 ([f0be1e2](https://github.com/mkdir700/EchoPlayer/commit/f0be1e206fb69f3a75ad292dc8c3a90f02fced14)) +- 更新通知系统优化,增强用户交互体验 ([6df4374](https://github.com/mkdir700/EchoPlayer/commit/6df4374ceb90b401799107c344211b164f7a0164)) +- 更新页面渲染逻辑,添加页面冻结功能,确保首页始终挂载并优化其他页面的条件渲染,提升用户体验 ([7a4b2ba](https://github.com/mkdir700/EchoPlayer/commit/7a4b2ba5d72a83f3765b003384d09295e70403e5)) +- 替换应用头部为侧边栏组件 ([0e621fc](https://github.com/mkdir700/EchoPlayer/commit/0e621fca1703f7461a101d2899ce7d85626156ff)) +- 沉浸式标题栏 ([9c7c7d9](https://github.com/mkdir700/EchoPlayer/commit/9c7c7d9b91ba0c505d72cc3cf2d11b9049bd62a3)) +- 添加 @ant-design/v5-patch-for-react-19 支持 React19 ([95d1019](https://github.com/mkdir700/EchoPlayer/commit/95d1019a02fb244e558f8819e4c52e3a7b0bc1bf)) +- 添加 Stagewise 工具栏支持,仅在开发模式下初始化,更新 CSP 设置以允许外部样式源 ([ededb64](https://github.com/mkdir700/EchoPlayer/commit/ededb643573969a41ebc57a9666fbfd928e44e7c)) +- 添加Codecov配置文件,更新测试配置以支持覆盖率报告上传 ([d9ec00d](https://github.com/mkdir700/EchoPlayer/commit/d9ec00d895792eca2c9ad6ea455f5b1eaadb2078)) +- 添加E2E测试支持,更新Playwright配置和相关脚本 ([247b851](https://github.com/mkdir700/EchoPlayer/commit/247b85122ab88b05e789388e18b696769256e226)) +- 添加全屏功能支持,优化视频播放器组件,更新样式以移除不必要的自定义样式,提升用户体验 ([a7d4b1c](https://github.com/mkdir700/EchoPlayer/commit/a7d4b1c1408ec6177ec07c60280993f23af8c605)) +- 添加字幕控制组件,支持单句循环和自动循环功能,更新快捷键设置,优化样式和响应式设计 ([2902f2d](https://github.com/mkdir700/EchoPlayer/commit/2902f2d54e929b433ba7dfa2ed9ebe32dc8b2d58)) +- 添加应用图标 ([b86e142](https://github.com/mkdir700/EchoPlayer/commit/b86e1420b4ca8701354d644b45653ac039845db2)) +- 添加应用图标并优化代码中的事件监听和清理逻辑 ([c39da08](https://github.com/mkdir700/EchoPlayer/commit/c39da08c7ab5a17ed4fb718bcfc10df4a2b94cb9)) +- 添加当前字幕展示组件,支持多种字幕显示模式及单词hover交互,优化视频控制区样式和响应式设计 ([df4b74a](https://github.com/mkdir700/EchoPlayer/commit/df4b74a98c5ae66e6c2d3be24e25c7e4261fc70e)) +- 添加循环播放功能,支持自定义循环次数设置 ([1dbccfa](https://github.com/mkdir700/EchoPlayer/commit/1dbccfae97c22ac49a08e78287211f89ccf3aa46)) +- 添加文件系统相关的 IPC 处理器,支持文件存在性检查、读取文件内容、获取文件 URL、文件信息获取及文件完整性验证;更新 preload 和 renderer 逻辑以支持视频和字幕文件的选择与恢复功能,优化用户体验 ([6d361eb](https://github.com/mkdir700/EchoPlayer/commit/6d361eb0ec1e8f8aa2eaca5167736bd1373d93bb)) +- 添加更新通知和提示对话框组件 ([38df4d2](https://github.com/mkdir700/EchoPlayer/commit/38df4d2b55af83f3007f1b242da19eb02cca8a11)) +- 添加更新通知跳过版本功能,优化用户体验 ([165adb6](https://github.com/mkdir700/EchoPlayer/commit/165adb69c7a4dae1d2749592b01f1561580c58ec)) +- 添加本地更新测试环境脚本和相关功能 ([00aa019](https://github.com/mkdir700/EchoPlayer/commit/00aa01940583ec30e79034495fe804febe4479ab)) +- 添加构建产物重命名和验证脚本 - 新增 rename-artifacts.ts 用于重命名构建产物以符合发布要求 - 新增 verify-build-artifacts.ts 用于验证构建产物的存在性和完整性 ([696cedc](https://github.com/mkdir700/EchoPlayer/commit/696cedc090caaa56b3f2c4921022d9e131d361ac)) +- 添加构建和发布工作流,更新测试和发布脚本 ([2744005](https://github.com/mkdir700/EchoPlayer/commit/2744005aefb85874651d7e7937e5af1f9ead8b35)) +- 添加欧陆词典HTML解析服务和单元测试框架 ([52ace3e](https://github.com/mkdir700/EchoPlayer/commit/52ace3ef0ba4aa0b58d000996d1f933365c093ce)) +- 添加测试Electron CDP连接的脚本 ([9982514](https://github.com/mkdir700/EchoPlayer/commit/9982514f56ccbb6b048d0a4d961f8a5b7b29eea0)) +- 添加版本管理脚本,支持版本类型检测和版本号递增功能;更新构建和发布工作流,优化版本变量设置和上传路径逻辑;新增发布指南文档,详细说明版本管理和发布流程 ([282bde8](https://github.com/mkdir700/EchoPlayer/commit/282bde883d4c8ae965963e555feb1cd4a011ab88)) +- 添加视频播放器点击事件处理,优化用户交互体验 ([69c378f](https://github.com/mkdir700/EchoPlayer/commit/69c378fad8aa8c15b29833e668fd150775c477e3)) +- 添加视频文件选择加载状态和清空确认模态框 ([ca95a7d](https://github.com/mkdir700/EchoPlayer/commit/ca95a7d5f2cae1e42423dbe7cfe3c7d09352e16c)) +- 添加视频格式转换功能,新增视频兼容性检测与转换指南,优化视频播放器与文件上传逻辑,提升用户体验;重构相关组件,简化代码结构 ([5fd89fe](https://github.com/mkdir700/EchoPlayer/commit/5fd89fed2b346efbb4d0e5c0d029af51e60f07a1)) +- 添加腾讯云COS上传功能,支持发布文件和自动更新文件的上传 ([e79e5a9](https://github.com/mkdir700/EchoPlayer/commit/e79e5a9c5b5e29f2092d50aa9af58dafa6297612)) +- 添加自动更新功能,整合更新处理器,更新设置界面,支持版本检查和下载 ([5e5a03e](https://github.com/mkdir700/EchoPlayer/commit/5e5a03e5966e3978ba16d76ed202ce943903e3a1)) +- 添加页面切换过渡效果,优化播放页面与性能监控功能;重构相关组件,提升用户交互体验与代码结构 ([e583ecc](https://github.com/mkdir700/EchoPlayer/commit/e583ecc78836dc241392039c76092833ca354695)) +- 添加页面导航功能,重构 App 组件以支持多页面切换,新增关于、收藏、设置等页面,优化样式和用户体验 ([51f4263](https://github.com/mkdir700/EchoPlayer/commit/51f426365c12474091e8581211da3e7e36d29749)) +- 添加高效测试标识符管理指南及相关工具函数,优化E2E测试中的测试ID使用 ([2dcfe5e](https://github.com/mkdir700/EchoPlayer/commit/2dcfe5e7443095890acc7034a5b919059dcad2bc)) +- 现代化视频控制组件,优化样式和交互逻辑,增强用户体验;添加音量和设置控制,支持自动隐藏功能 ([dc45b83](https://github.com/mkdir700/EchoPlayer/commit/dc45b83bbaf02f90ccde2559f203520b172a0388)) +- 移除 HomePage 组件中的 subtitleIndex 属性,优化视频播放状态管理逻辑;调整视频网格布局以提升用户界面的一致性与可读性 ([8f54e7f](https://github.com/mkdir700/EchoPlayer/commit/8f54e7fb54574552a308c7af5188f5f46d5a37ce)) +- 移除 PlayPageHeader 的 CSS 模块,改为使用主题系统样式管理,提升组件的可维护性和一致性 ([52cedbc](https://github.com/mkdir700/EchoPlayer/commit/52cedbc09e95d3fe91308e1bdc70a38d1c988315)) +- 移除 useSidebarResize 钩子及相关样式,改用 Ant Design 的 Splitter 组件实现侧边栏调整功能,优化播放页面布局与用户体验 ([bead645](https://github.com/mkdir700/EchoPlayer/commit/bead645f2680363621a7cc7dd6139aa990aa7750)) +- 移除字幕位置控制相关组件及其逻辑,简化视频控制界面以提升用户体验 ([1edc857](https://github.com/mkdir700/EchoPlayer/commit/1edc857e3ef1f8ac87c35e6c62f1bdfcd4b545c6)) +- 移除字幕设置相关功能和组件 ([32f0138](https://github.com/mkdir700/EchoPlayer/commit/32f0138c6285b6a5805720c9306dad2d4cfd7783)) +- 移除推荐视频假数据,更新欢迎信息,优化首页布局和用户体验 ([78b000f](https://github.com/mkdir700/EchoPlayer/commit/78b000fcf2bd8511ef35e79f5a03114dfed297d4)) +- 移除视频播放器和播放控制钩子,简化代码结构以提升可维护性 ([513ba3c](https://github.com/mkdir700/EchoPlayer/commit/513ba3c21f67fbc76fc3a61ac5b128506cca68db)) +- 移除视频播放器的响应式设计中不必要的内边距,简化 CSS 结构 ([f8c8c28](https://github.com/mkdir700/EchoPlayer/commit/f8c8c2899d8545486a23421d882ecfd1c186446c)) +- 调整 HomePage 组件的响应式布局,优化列宽设置以提升用户体验 ([3c435bf](https://github.com/mkdir700/EchoPlayer/commit/3c435bfee87ab529333a2dcf3fe51553b089cc45)) +- 调整主题样式宽度 ([2fe9ff2](https://github.com/mkdir700/EchoPlayer/commit/2fe9ff24d2f35791712b3bf5b848fc7464b08fef)) +- 调整全屏视频控制组件的进度条位置和样式 ([679521f](https://github.com/mkdir700/EchoPlayer/commit/679521f2f92c1c04646a89866944b20d22d6a917)) +- 调整字幕覆盖层样式,修改底部位置为0%,移除移动端特定样式,简化 CSS 结构 ([515151d](https://github.com/mkdir700/EchoPlayer/commit/515151d022fc16d886471aad43aeda43f482214c)) +- 重命名视频控制组件为 VideoControlsFullScreen,更新相关导入,提升代码可读性 ([0fe7954](https://github.com/mkdir700/EchoPlayer/commit/0fe795404702dd1a9b68c32a21fec5ad003dcf8d)) +- 重构 SidebarSection 和 SubtitleListContent 组件,简化属性传递,增强字幕索引处理逻辑,优化自动滚动功能;新增获取指定时间点字幕索引的功能,提升用户体验与代码可读性 ([dabcbeb](https://github.com/mkdir700/EchoPlayer/commit/dabcbeb0718e2f8d6923a223d3c57e79453366a9)) +- 重构字幕控制组件样式,使用主题系统优化按钮和图标样式,提升视觉一致性和用户体验 ([12e38f2](https://github.com/mkdir700/EchoPlayer/commit/12e38f2f260467e297ad11831ff1a44eea08c317)) +- 重构字幕状态管理,新增视频特定字幕设置 ([ff5b5de](https://github.com/mkdir700/EchoPlayer/commit/ff5b5def52690c082eae9f26029f6d139d80cd47)) +- 重构字幕组件,新增字幕覆盖层和文本组件,优化字幕显示逻辑和性能;移除旧版字幕组件,提升代码可维护性 ([4fbef84](https://github.com/mkdir700/EchoPlayer/commit/4fbef8419f703f398593043932f07a14a78e170c)) +- 重构存储处理器模块,优化应用配置和通用存储功能 ([065c30d](https://github.com/mkdir700/EchoPlayer/commit/065c30d7cbc8fc5b01f3f3b59211e6548d679cdc)) +- 重构存储管理功能,更新最近播放项的类型定义,优化播放设置管理,增强用户体验;新增播放设置的深度合并逻辑,提升代码可维护性 ([3f928d4](https://github.com/mkdir700/EchoPlayer/commit/3f928d4c84df465574fde222fcb1dccf72c3dfc6)) +- 重构应用布局与样式,新增主页与播放页面组件,优化用户交互体验;整合最近文件管理功能,提升视频文件选择与加载逻辑 ([f3fefad](https://github.com/mkdir700/EchoPlayer/commit/f3fefadd3643f20e2935d2d72eeea1e56a65a1d1)) +- 重构循环切换功能,简化状态管理和播放逻辑 ([fe11037](https://github.com/mkdir700/EchoPlayer/commit/fe11037cb82119f3ff3e74c825a44e80022158f7)) +- 重构播放状态管理,替换为使用最近播放列表钩子,简化参数传递并优化代码逻辑;新增最近播放列表钩子以支持播放项的增删改查功能 ([1ec2cac](https://github.com/mkdir700/EchoPlayer/commit/1ec2cac7f2a84f11b1ff4ddd2d482a45c8eae1bd)) +- 重构播放页面,整合视频播放器与字幕控制逻辑,新增 VideoPlayerProvider 以管理视频播放状态,优化组件结构与性能;移除不再使用的 SubtitleControls 组件,简化属性传递,提升代码可读性 ([e4111c9](https://github.com/mkdir700/EchoPlayer/commit/e4111c9274cb6b6dd112c5dd7f629b244450f802)) +- 重构视频控制组件,新增全屏控制样式与逻辑,优化播放控制体验;更新相关类型定义,提升代码可读性与功能性 ([5c72a1b](https://github.com/mkdir700/EchoPlayer/commit/5c72a1b0ce481c63b0d9c03f048b527f612052e0)) +- 重构视频播放上下文,新增视频文件上传和选择功能;更新相关组件以支持新的上下文逻辑,提升代码可读性与功能性 ([37e128e](https://github.com/mkdir700/EchoPlayer/commit/37e128eede0ad02f70ee7dc2aa2aab7d49a121df)) +- 重构视频播放器组件,移除 CSS Modules,采用主题系统样式管理,提升代码可维护性和一致性 ([b3981bc](https://github.com/mkdir700/EchoPlayer/commit/b3981bc2d86a7ea7d343f1e068f404b46af509f0)) +- 重构视频播放器逻辑,整合视频播放状态管理,优化组件结构;移除不再使用的 usePlayingVideoContext 钩子,新增多个视频控制钩子以提升性能与可读性 ([b1a6dc2](https://github.com/mkdir700/EchoPlayer/commit/b1a6dc29acb83fbdf4a167c9ded069f2e53d0491)) +- 重构视频播放设置管理,整合字幕显示设置,优化状态管理逻辑,提升用户体验和代码可维护性 ([6c3d852](https://github.com/mkdir700/EchoPlayer/commit/6c3d852fbb8d66e5f07942fdf792b661327b3a4a)) +- 重构设置页面,新增快捷键、数据管理和占位符组件,优化用户界面和交互体验;引入快捷键上下文管理,支持自定义快捷键功能 ([a498905](https://github.com/mkdir700/EchoPlayer/commit/a4989050d3a747b6a66d7deb2da21a1cf9a2a0be)) + +### Reverts + +- Revert "build: 在构建和发布工作流中添加调试步骤,列出下载的文件并检查Windows、Mac和Linux平台的自动更新配置文件是否存在" ([d0f8fc4](https://github.com/mkdir700/EchoPlayer/commit/d0f8fc4be0f3b976df0752a57437eb3cd16321ef)) +- Revert "chore: 更新 Linux 构建环境配置" ([cc179a0](https://github.com/mkdir700/EchoPlayer/commit/cc179a072721fd508662924073cf03bdfa684611)) +- Revert "ci(sync): add workflow to sync main to beta ([#132](https://github.com/mkdir700/EchoPlayer/issues/132))" ([93ac160](https://github.com/mkdir700/EchoPlayer/commit/93ac16079a190883b4529efeb34c11937ece02bd)) +- Revert "feat: 在构建和发布工作流中添加更新 package.json 版本的步骤,确保版本号自动更新;优化草稿发布条件以支持预发布版本" ([be1cf26](https://github.com/mkdir700/EchoPlayer/commit/be1cf2668cf7ad777739bdb40e5b75e145775386)) + +### BREAKING CHANGES + +- **player,logging,state:** - Removed TransportBar and deprecated hooks (usePlayerControls/useVideoEvents/useSubtitleSync). Migrate to ControllerPanel with usePlayerEngine/usePlayerCommandsOrchestrated. + +* Player store control actions are engine-only; components should send commands via the orchestrator instead of mutating store directly. + +# [1.0.0-alpha.8](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) (2025-09-10) + +### Features + +- **ci:** implement semantic-release with automatic version detection ([#117](https://github.com/mkdir700/EchoPlayer/issues/117)) ([4030234](https://github.com/mkdir700/EchoPlayer/commit/4030234c7dbaf3b358f4697c5321565e7c0fdd64)) + +# [1.0.0-alpha.7](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) (2025-09-10) + +### Bug Fixes + +- **ci:** sync package.json version with manual trigger input ([#116](https://github.com/mkdir700/EchoPlayer/issues/116)) ([0ca1d39](https://github.com/mkdir700/EchoPlayer/commit/0ca1d3963208d9eb7b825c7c1b31e269532fa3eb)) + +### Features + +- **ci:** add dynamic workflow names to show release version in actions list ([#115](https://github.com/mkdir700/EchoPlayer/issues/115)) ([1ff9550](https://github.com/mkdir700/EchoPlayer/commit/1ff95509c8849d19028bdaa29e8f79703d69424e)) +- **ffmpeg:** integrate bundled FFmpeg with automatic fallback mechanism ([#112](https://github.com/mkdir700/EchoPlayer/issues/112)) ([5a4f26a](https://github.com/mkdir700/EchoPlayer/commit/5a4f26a230f8bf54f31873d30ed05abfbf887b06)) +- **player:** implement comprehensive video error recovery ([#113](https://github.com/mkdir700/EchoPlayer/issues/113)) ([c25f17a](https://github.com/mkdir700/EchoPlayer/commit/c25f17ab83b0be7a60bbd589d971637bf74eb8b6)) + +# [1.0.0-alpha.6](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) (2025-09-10) + +### Bug Fixes + +- **subtitle:** improve ASS subtitle parsing for bilingual text ([#111](https://github.com/mkdir700/EchoPlayer/issues/111)) ([444476b](https://github.com/mkdir700/EchoPlayer/commit/444476bae35afcd916f098dbc544eced7542a4b9)) + +### Features + +- **search:** implement video search engine with live results and highlighting ([#110](https://github.com/mkdir700/EchoPlayer/issues/110)) ([bb2ac30](https://github.com/mkdir700/EchoPlayer/commit/bb2ac3028a45df5e49f74ac4cab6752806af048d)) + +# [1.0.0-alpha.5](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2025-09-10) + +### Bug Fixes + +- **player:** ensure video always starts paused and sync UI state correctly ([#102](https://github.com/mkdir700/EchoPlayer/issues/102)) ([c6c8909](https://github.com/mkdir700/EchoPlayer/commit/c6c890986d6a0137cb6a70e01144c9c995589840)) +- **player:** improve subtitle overlay positioning and remove i18n dependencies ([#109](https://github.com/mkdir700/EchoPlayer/issues/109)) ([bd7f5c3](https://github.com/mkdir700/EchoPlayer/commit/bd7f5c3ec319c174bd8b0244e935daef8ec90d9d)) + +### Features + +- **ci:** configure CodeRabbit for alpha, beta, and main branch PR reviews ([#108](https://github.com/mkdir700/EchoPlayer/issues/108)) ([4f5bad2](https://github.com/mkdir700/EchoPlayer/commit/4f5bad2e220f4b8e814d9a9a3df16224f4d620ae)) +- **player:** implement favorite playback rates with hover menu system ([#100](https://github.com/mkdir700/EchoPlayer/issues/100)) ([df83095](https://github.com/mkdir700/EchoPlayer/commit/df830955971080e5db393590a082e86718da10cd)) +- **player:** implement volume wheel control with intelligent acceleration ([#105](https://github.com/mkdir700/EchoPlayer/issues/105)) ([b675150](https://github.com/mkdir700/EchoPlayer/commit/b6751504ccff70f71fe518393587b3e70e6d7dba)) +- **startup:** implement configurable startup intro with preloading optimization ([#104](https://github.com/mkdir700/EchoPlayer/issues/104)) ([e5f4109](https://github.com/mkdir700/EchoPlayer/commit/e5f41092843333c39dc2029bd040aae6854a1036)) +- **ui:** enhance video selection clarity and simplify display ([#101](https://github.com/mkdir700/EchoPlayer/issues/101)) ([a951877](https://github.com/mkdir700/EchoPlayer/commit/a95187723ddc7706f52e72a66f61ceb6b2ceb439)) + +# [1.0.0-alpha.4](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2025-09-08) + +### Bug Fixes + +- **updater:** resolve auto-update channel handling and version-based test defaults ([#98](https://github.com/mkdir700/EchoPlayer/issues/98)) ([e30213b](https://github.com/mkdir700/EchoPlayer/commit/e30213babd3f0cc391864df8e2b95774e6d41051)) + +### Features + +- **player:** implement hover menu system for control panel components ([#99](https://github.com/mkdir700/EchoPlayer/issues/99)) ([526e71a](https://github.com/mkdir700/EchoPlayer/commit/526e71a7a40268e88a0ba6c9f7dab9565d929a21)) + +# [1.0.0-alpha.3](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2025-09-08) + +### Bug Fixes + +- **release:** remove custom labels from GitHub release assets ([#92](https://github.com/mkdir700/EchoPlayer/issues/92)) ([f066209](https://github.com/mkdir700/EchoPlayer/commit/f066209bdab482481a4564827490580b753b3c8e)) +- remove path unique constraint to allow duplicate video file addition ([#97](https://github.com/mkdir700/EchoPlayer/issues/97)) ([237dd30](https://github.com/mkdir700/EchoPlayer/commit/237dd301c995133e1781e76c2f56cf167b7a78c9)) + +### Features + +- **ci:** add alpha and beta branch support to test workflow ([#94](https://github.com/mkdir700/EchoPlayer/issues/94)) ([a47466b](https://github.com/mkdir700/EchoPlayer/commit/a47466b8e236af17785db098a761fa6dd30c67b5)) +- replace FFmpeg with MediaInfo for video metadata extraction ([#95](https://github.com/mkdir700/EchoPlayer/issues/95)) ([2f64029](https://github.com/mkdir700/EchoPlayer/commit/2f640299078bc354735c8c665c190c849dc52615)) + +# [1.0.0-alpha.2](https://github.com/mkdir700/EchoPlayer/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2025-09-08) + +### Bug Fixes + +- **ci:** resolve duplicate GitHub releases issue ([#90](https://github.com/mkdir700/EchoPlayer/issues/90)) ([07c3e5f](https://github.com/mkdir700/EchoPlayer/commit/07c3e5f754502cd5780cbfc704aa305b3e9c2a95)) +- improve release workflow and build configuration ([#91](https://github.com/mkdir700/EchoPlayer/issues/91)) ([4534162](https://github.com/mkdir700/EchoPlayer/commit/4534162cae55c7bc4cb28200abe86df62af9a662)) + +# 1.0.0-alpha.1 (2025-09-08) + +### Bug Fixes + +- **build:** correct alpha channel update file naming ([#79](https://github.com/mkdir700/EchoPlayer/issues/79)) ([95e2ed2](https://github.com/mkdir700/EchoPlayer/commit/95e2ed262d6f29d2a645033089afe36a24afd56f)) +- **build:** 修复 Linux 构建产物架构命名转换问题 ([1f732ba](https://github.com/mkdir700/EchoPlayer/commit/1f732ba84ed69c803c6795c19ae7b5a2e11c3b70)) +- **ci:** resolve GitHub Release creation issue with always publish strategy ([#85](https://github.com/mkdir700/EchoPlayer/issues/85)) ([712f0e8](https://github.com/mkdir700/EchoPlayer/commit/712f0e8cc8c11241678334c80e95f778055f57b2)) +- **ci:** resolve semantic-release configuration issues ([#88](https://github.com/mkdir700/EchoPlayer/issues/88)) ([0a9e4a3](https://github.com/mkdir700/EchoPlayer/commit/0a9e4a3eb4501ade7aa25f377baab627de27b872)) +- **ci:** resolve Windows build shell syntax compatibility issue ([#84](https://github.com/mkdir700/EchoPlayer/issues/84)) ([59b8460](https://github.com/mkdir700/EchoPlayer/commit/59b846044060a4c6ddd82c490c3c8706fe9daac7)) +- fix type check ([eae1e37](https://github.com/mkdir700/EchoPlayer/commit/eae1e378262d1f9162fd630cbb6dd867df933fb3)) +- Fix TypeScript build errors and improve type safety ([#77](https://github.com/mkdir700/EchoPlayer/issues/77)) ([7861279](https://github.com/mkdir700/EchoPlayer/commit/7861279d8d5fd8c8e3bd5d5639f8e4b8f999b0ca)) +- **player:** integrate volume state in player engine context ([5ff32d9](https://github.com/mkdir700/EchoPlayer/commit/5ff32d91ce39d9499ae762ee433e61461e926c46)) +- remove cheerio dependency to resolve Electron packaging issues - Remove cheerio and @types/cheerio from package.json dependencies - Replace cheerio-based HTML parsing with native regex implementation - Refactor parseEudicHtml() to parseEudicHtmlWithRegex() in dictionaryHandlers.ts - Support multiple HTML formats: list items, phonetics, examples, translations - Delete related test files that depend on cheerio - Fix TypeScript type errors for regex variables - Improve Electron runtime compatibility and reduce bundle size Fixes [#50](https://github.com/mkdir700/EchoPlayer/issues/50) ([b01fe4e](https://github.com/mkdir700/EchoPlayer/commit/b01fe4e33a0027d3c4fc6fdbb7e5577fb7f4165b)) +- **renderer:** resolve subsrt dynamic require issue in production build ([#78](https://github.com/mkdir700/EchoPlayer/issues/78)) ([028a8fb](https://github.com/mkdir700/EchoPlayer/commit/028a8fb9a9446ebb8dc7b25fb4a70fadc02fb085)) +- resolve dead links in documentation and add missing pages ([fc36263](https://github.com/mkdir700/EchoPlayer/commit/fc3626305bdbf96c0efc70ae9d989ba02a0ededa)) +- **test:** resolve SubtitleLibraryDAO schema validation and test framework improvements ([#80](https://github.com/mkdir700/EchoPlayer/issues/80)) ([4be2b8a](https://github.com/mkdir700/EchoPlayer/commit/4be2b8a390c454dc1b0287e352d15ceedb4ed67b)) +- **titlebar:** keep title bar fixed at top during page scroll ([b3ff5c2](https://github.com/mkdir700/EchoPlayer/commit/b3ff5c2c6b5a8bea67da69b8e82c9200d5eb05fd)) +- 优化文件路径处理逻辑以支持不同平台 ([dc4e1e3](https://github.com/mkdir700/EchoPlayer/commit/dc4e1e384588dac7e1aacc27eccf165fe2e43e4d)) +- 修复 settings 相关组件找不到的问题 ([08f88ba](https://github.com/mkdir700/EchoPlayer/commit/08f88bad7099ac110a0bae109b3501a0348f0b78)) +- 修复全屏模式下速度选择窗口溢出的问题 ([6309046](https://github.com/mkdir700/EchoPlayer/commit/63090466881d8df3e5dc062c0f235995dfe4134e)) +- 修复在 Windows 上的 FFmpeg 文件下载和 ZIP 解压 ([6347b4e](https://github.com/mkdir700/EchoPlayer/commit/6347b4e62207dc104a1fa44f27af08667ff893a2)) +- 修复在启用单句循环模式下,无法调整到下一句的问题 ([ec479be](https://github.com/mkdir700/EchoPlayer/commit/ec479beeff5c931821eed5aaffeaa054226b13c2)) +- 修复文件路径处理逻辑以支持不同的 file URL 前缀 ([740015d](https://github.com/mkdir700/EchoPlayer/commit/740015d955f8266b96d2aa49bdc244d084937355)) +- 修复方向键冲突检测问题 ([4a466c7](https://github.com/mkdir700/EchoPlayer/commit/4a466c7367860120d9a4ccc6f23ab5e79a2d8cae)) +- 修复无法通过按钮退出全屏模式的问题 ([e69562b](https://github.com/mkdir700/EchoPlayer/commit/e69562b9ead8ea66c0933ad21b5cbeae3d88142f)) +- 修复构建产物架构冲突问题 ([2398bd7](https://github.com/mkdir700/EchoPlayer/commit/2398bd78be4526a9f3f636c8f945df644bbc3d5b)) +- 修复组件导出语句和优化字幕加载逻辑,移除未使用的状态 ([39708ce](https://github.com/mkdir700/EchoPlayer/commit/39708ce48bd6652488abce7d21752a2afe994d99)) +- 删除上传到 cos 的步骤,因为网络波动问题上传失败 ([1cac918](https://github.com/mkdir700/EchoPlayer/commit/1cac918f21ad3198827138512ee61d770bd1367f)) +- 在 UpdateNotification 组件中添加关闭对话框的逻辑,确保用户在操作后能够顺利关闭对话框 ([845a070](https://github.com/mkdir700/EchoPlayer/commit/845a070ac74b513ce5bda3cdc3d3e7a803a3b8d1)) +- 始终在脚本直接执行时运行主函数,确保功能正常 ([a15378a](https://github.com/mkdir700/EchoPlayer/commit/a15378a914e54967f50642e04a111af184255344)) +- 忽略依赖项警告 ([fc3f038](https://github.com/mkdir700/EchoPlayer/commit/fc3f038bb7d9b7e6962a5346bee00c858998ade0)) +- 更新主题样式,使用 token 中的 zIndex 替代硬编码值 ([3940caf](https://github.com/mkdir700/EchoPlayer/commit/3940caf3b768efcba2043b3734bc7c7962f8c5a8)) +- 更新测试文件中的 useTheme 和 useVideoPlaybackHooks 的路径 ([4fa9758](https://github.com/mkdir700/EchoPlayer/commit/4fa9758ae7bcb26789e8a458312ef23d577a34e6)) +- 移除构建和发布工作流中的空选项,始终将草稿发布设置为 true,以确保发布过程的一致性 ([171028a](https://github.com/mkdir700/EchoPlayer/commit/171028adff214b3c696b7aaacb617c7c41b0302b)) + +### Features + +- add API communication type definitions and unified export ([ea9f1c0](https://github.com/mkdir700/EchoPlayer/commit/ea9f1c0690d3b7fe5f6a2e2406b5fb88817aa8d1)) +- add common base type definitions and interfaces for application ([73bd604](https://github.com/mkdir700/EchoPlayer/commit/73bd6046239341716eea727d95b316e9a3652ec8)) +- add debounce hooks and corresponding tests ([7646088](https://github.com/mkdir700/EchoPlayer/commit/7646088b78106e40f00ff15a3cdd86b44aa541cc)) +- add domain type definitions and constants for video, subtitle, playback, and UI ([a1c3209](https://github.com/mkdir700/EchoPlayer/commit/a1c3209271336891e0e9dbde444abc8c4e7d8e4b)) +- add git hooks with lint-staged for automated code quality checks ([1311af9](https://github.com/mkdir700/EchoPlayer/commit/1311af96159b7e7b5d31f43f27f479cc9035d5a5)) +- add handler to read directory contents ([6ce1d9e](https://github.com/mkdir700/EchoPlayer/commit/6ce1d9eff64968cef3a5673a67f8753de582d501)) +- add IPC Client Service implementation with integration tests ([fe4400f](https://github.com/mkdir700/EchoPlayer/commit/fe4400ff63ff0640f32ca94f0b4d0d4c47b246ed)) +- add performance optimization hooks and corresponding tests ([d7e1d0f](https://github.com/mkdir700/EchoPlayer/commit/d7e1d0f006dfe8c6c58a20bb0305621a657c9a65)) +- add selectors for subtitle, UI, and video states with computed properties and hooks ([c64f41d](https://github.com/mkdir700/EchoPlayer/commit/c64f41dd27496bd311ff588474041e4ebacbd3a9)) +- Add service layer type definitions for storage, video, subtitle, and dictionary services ([c658217](https://github.com/mkdir700/EchoPlayer/commit/c658217a5acc7e8a066004e6d7c1cd103be43a3b)) +- add subtitle, UI, and video state actions for V2 ([1a4042a](https://github.com/mkdir700/EchoPlayer/commit/1a4042af3e1e917d6303a560c49e4aa8d52300ab)) +- add unified export for V2 infrastructure layer type system ([ad94ea8](https://github.com/mkdir700/EchoPlayer/commit/ad94ea849bc17b5e569bd2296cf73c30ca06747d)) +- add V2 state stores with type-safe validation and comprehensive documentation ([264cc66](https://github.com/mkdir700/EchoPlayer/commit/264cc661c2be83c9d886ba673d104b499ade0729)) +- **api:** add request and response type definitions for video, subtitle, file operations, and playback settings ([c0e9324](https://github.com/mkdir700/EchoPlayer/commit/c0e9324642d6920def8dcb79a97c92ab0f552397)) +- **AutoResumeCountdown:** add auto-dismissal when playback manually resumed ([3852bca](https://github.com/mkdir700/EchoPlayer/commit/3852bca30af23c203698bc413e0b482a595c96d6)) +- **ci:** implement semantic-release with three-branch strategy ([#89](https://github.com/mkdir700/EchoPlayer/issues/89)) ([c39da6e](https://github.com/mkdir700/EchoPlayer/commit/c39da6e77d4e995299a6df62f9fa565a6dd46807)) +- **ci:** integrate semantic-release automation with GitHub workflow ([#87](https://github.com/mkdir700/EchoPlayer/issues/87)) ([874bd5a](https://github.com/mkdir700/EchoPlayer/commit/874bd5a0987c5944c14926230b32a49b4886158b)) +- **ci:** migrate from action-gh-release to native electron-builder publishing ([#82](https://github.com/mkdir700/EchoPlayer/issues/82)) ([eab9ba1](https://github.com/mkdir700/EchoPlayer/commit/eab9ba1f1d8cc55d4cf6e7e6c3c8633b02938715)) +- comprehensive auto-update system implementation ([#73](https://github.com/mkdir700/EchoPlayer/issues/73)) ([0dac065](https://github.com/mkdir700/EchoPlayer/commit/0dac065d54643bc761c741bae914057b9784e419)) +- **ControllerPanel:** add disabled state for Loop and AutoPause controls when subtitles are empty ([a35f3e6](https://github.com/mkdir700/EchoPlayer/commit/a35f3e6cbf5c33ee4ebc6ca21dfb31a5b2b1b1a6)) +- **ControllerPanel:** implement centralized menu management system for player controls ([1523758](https://github.com/mkdir700/EchoPlayer/commit/152375846dc6d5acf84d07a0d182160e29de4358)) +- **db:** implement complete SQLite3 database layer with migrations and DAOs ([0a8a7dd](https://github.com/mkdir700/EchoPlayer/commit/0a8a7ddb5a240c6d6c145b4cfa0790e2292d3697)) +- **db:** migrate from Dexie to Kysely with better-sqlite3 backend ([6b75cd8](https://github.com/mkdir700/EchoPlayer/commit/6b75cd877bd117b84d6205eb1c680450042b6eaa)) +- define domain types for video, subtitle, and UI, and refactor RecentPlayItem interface imports ([f632beb](https://github.com/mkdir700/EchoPlayer/commit/f632beba5f9b20afb2ec6fc8e6df7dcba6fd29f0)) +- enhance macOS build configuration with additional entitlements and notarization support ([d6e8ced](https://github.com/mkdir700/EchoPlayer/commit/d6e8ced7611b9f09743bc29344c6a0020ed0b19d)) +- **home:** implement empty state with video file selection integration ([b6f6e40](https://github.com/mkdir700/EchoPlayer/commit/b6f6e401f622f1390926eb154f47fad812d5d0a7)) +- Implement dictionary engine framework ([0d74a83](https://github.com/mkdir700/EchoPlayer/commit/0d74a8315f86209c530ad2f306b6099e97328c1f)) +- implement useThrottle hooks and corresponding tests ([da30344](https://github.com/mkdir700/EchoPlayer/commit/da303443b6847fe049be9c4592c98418aeea9785)) +- implement version parsing and channel mapping logic ([8c95a2f](https://github.com/mkdir700/EchoPlayer/commit/8c95a2feeef1d066fb46f246724d8a94014fa627)) +- **infrastructure:** add entry points for constants and shared modules, and refine video playback rate type ([94da255](https://github.com/mkdir700/EchoPlayer/commit/94da2556dd6909eec75d4edfe2b549e3858e2629)) +- **logger:** export logger instance for easier access in modules ([5328152](https://github.com/mkdir700/EchoPlayer/commit/5328152999408cbec0515e501aea9f393032b933)) +- **persistence:** add V2 state persistence manager and configuration files ([a545020](https://github.com/mkdir700/EchoPlayer/commit/a545020f3f676b526b3f3bf3ecff6d23c4c1a471)) +- **playback:** update playback rate label for clarity and add storage type definitions ([5c40b98](https://github.com/mkdir700/EchoPlayer/commit/5c40b983ea22c1e57e5e678922c5d6492a39359c)) +- player page ([aa79279](https://github.com/mkdir700/EchoPlayer/commit/aa792799580524ac65c8bf7e4d9bc7e13988a716)) +- **player,logging,state:** orchestrated player engine with intent strategies and new controller panel ([73d7cfd](https://github.com/mkdir700/EchoPlayer/commit/73d7cfdc4b58f190afe8981eddadbca54c6763b0)) +- **player:** add Ctrl+] shortcut for subtitle panel toggle ([#69](https://github.com/mkdir700/EchoPlayer/issues/69)) ([e1628f2](https://github.com/mkdir700/EchoPlayer/commit/e1628f2d04ea03cac20893960a3a9fec2dd9fdb2)) +- **player:** hide sidebar and optimize navbar for player page ([#70](https://github.com/mkdir700/EchoPlayer/issues/70)) ([5bb71e4](https://github.com/mkdir700/EchoPlayer/commit/5bb71e4465721c3b1f0dc326f9a92e879e1c048b)) +- **player:** implement auto-resume countdown with UI notification ([5468f65](https://github.com/mkdir700/EchoPlayer/commit/5468f6531c3e2f3d8aaf94f631ec4f760d04241f)) +- **player:** reposition progress bar between video and controls ([#71](https://github.com/mkdir700/EchoPlayer/issues/71)) ([248feed](https://github.com/mkdir700/EchoPlayer/commit/248feed534974b6c05ef2918a3758ae5fed1f42d)) +- refine RecentPlayItem interface with detailed video info and playback metrics ([81679b6](https://github.com/mkdir700/EchoPlayer/commit/81679b6eb54f7a8f07a33065c743fa51d1eecc5d)) +- setup GitHub Pages deployment for documentation ([b8a42b9](https://github.com/mkdir700/EchoPlayer/commit/b8a42b974d7490ddddb4292608315a58df14a24b)) +- **sidebar:** 禁用收藏按钮并添加开发中提示 ([#81](https://github.com/mkdir700/EchoPlayer/issues/81)) ([76e9b54](https://github.com/mkdir700/EchoPlayer/commit/76e9b5418ec5f07f4d3052d8130163d108965f47)) +- **SplashScreen:** add animated splash screen with typewriter effect and smooth transitions ([31cfeca](https://github.com/mkdir700/EchoPlayer/commit/31cfeca9f0773db12b9a7b299820a32c309d9daf)) +- **state.store:** 新增多个 store ([54e7ff5](https://github.com/mkdir700/EchoPlayer/commit/54e7ff5993631c6133e21948c2697bcd13919df6)) +- **state:** implement V2 state management infrastructure with storage engine, middleware, and utility functions ([e225746](https://github.com/mkdir700/EchoPlayer/commit/e22574659d1ec63fbc622152fec2371323b4fe53)) +- **storage:** implement application configuration storage service ([1209b56](https://github.com/mkdir700/EchoPlayer/commit/1209b56fae690a9876440faa679f072cb2ebc6da)) +- **subtitle-library:** Add subtitle data caching for improved loading performance ([#86](https://github.com/mkdir700/EchoPlayer/issues/86)) ([40be325](https://github.com/mkdir700/EchoPlayer/commit/40be325f09f9f70e702260dfe29e354c6c7435b6)) +- **SubtitleContent:** implement word-level tokenization and interactive text selection ([10c0cdf](https://github.com/mkdir700/EchoPlayer/commit/10c0cdf38fdbad53bfba4b82148b635e45657b11)) +- **types:** add Serializable interface for flexible data structures ([32981df](https://github.com/mkdir700/EchoPlayer/commit/32981df183af23d9153ae65765b9d1c8a533540e)) +- update macOS notarization configuration to enable automatic notarization ([6630e79](https://github.com/mkdir700/EchoPlayer/commit/6630e79975a5d39eea83bec44ef1ff0271c984da)) +- **video.store:** add format property to CurrentVideoState and update video loading simulation ([0349a63](https://github.com/mkdir700/EchoPlayer/commit/0349a6351ba8fa2a1235a48b268825ad18ea37ff)) +- **VolumeControl:** change volume popup from horizontal to vertical layout ([d4d435b](https://github.com/mkdir700/EchoPlayer/commit/d4d435b93a72b8bb8e1f9d4fe356fa41632d8993)) +- **wsl:** add WSL detection and hardware acceleration optimization ([c99403e](https://github.com/mkdir700/EchoPlayer/commit/c99403efa8af9fa9ebf106edcbdc4a0d21b31b2e)) +- 为字幕组件新增右键菜单功能 ([62334d5](https://github.com/mkdir700/EchoPlayer/commit/62334d56bb0956b28582d5d70e7ea0a3c2f9e42d)) +- 为音量控制组件添加音量调节快捷键 ([144d49c](https://github.com/mkdir700/EchoPlayer/commit/144d49c314688c6b7b0abbdd0c21f57c98f3084d)) +- 优化 PlayPage 组件性能,减少不必要的重新渲染;重构播放状态管理逻辑,提升用户体验 ([24a2ebc](https://github.com/mkdir700/EchoPlayer/commit/24a2ebc6d7c040ffce5ffc1a404f338ad77a6791)) +- 优化最近观看记录加载状态显示 ([e5f7e11](https://github.com/mkdir700/EchoPlayer/commit/e5f7e11498d52028cf20765ea2fe486cca49f3d1)) +- 优化单词查询逻辑,增加超时处理和取消请求功能,提升用户体验和性能 ([c98dc4b](https://github.com/mkdir700/EchoPlayer/commit/c98dc4b5d65bdc7c68d82f6e0c1bcda248929503)) +- 优化字幕列表滚动体验 ([63807c5](https://github.com/mkdir700/EchoPlayer/commit/63807c5a4a668d5000c7c920dcae5eb96623314e)) +- 优化字幕控制功能,新增字幕模式选择器,提升用户交互体验;重构相关组件,移除不必要的代码,简化逻辑 ([559aada](https://github.com/mkdir700/EchoPlayer/commit/559aada0c7d224bdbc941b30aa9da71b08d84636)) +- 优化字幕文本分段逻辑 ([33e591c](https://github.com/mkdir700/EchoPlayer/commit/33e591c08ac1d520886b20b2b0219e15eeea1d0d)) +- 优化字幕模式选择器组件,增强用户体验和视觉一致性,添加响应式设计和毛玻璃效果 ([4b59a1b](https://github.com/mkdir700/EchoPlayer/commit/4b59a1b0ac1d3a66dfd0c129177f2820722f2416)) +- 优化快捷键设置界面,增强输入状态反馈,更新样式以提升用户体验和一致性 ([64db31c](https://github.com/mkdir700/EchoPlayer/commit/64db31cd8112b08f509e16ef653f687381f5f636)) +- 优化日志记录功能,新增组件渲染节流和数据简化处理,提升性能和可读性 ([a6e8480](https://github.com/mkdir700/EchoPlayer/commit/a6e8480ecb2458eefd899d0828af3955f3cbef6e)) +- 优化标题栏平台信息处理 ([fb5a470](https://github.com/mkdir700/EchoPlayer/commit/fb5a470084ed46f6f662e060d4cbca89fca1736e)) +- 优化视频卡片和视频网格组件的样式与布局 ([af59b44](https://github.com/mkdir700/EchoPlayer/commit/af59b44aa7cefeb06dfd87656532af3652013574)) +- 优化词典查询逻辑,增加未找到释义时的警告提示,并调整相关代码结构 ([9f528bd](https://github.com/mkdir700/EchoPlayer/commit/9f528bd89e905af2490c9c1b2db3d9a00b19e1f8)) +- 优化进度条handler显示和对齐 ([77d0496](https://github.com/mkdir700/EchoPlayer/commit/77d04965d4ac4aa2bfff965fe2852c82d9e4c91e)) +- 在 FullscreenTestInfo 组件中新增折叠功能 ([b2eac46](https://github.com/mkdir700/EchoPlayer/commit/b2eac461e616b3da6be84ceabaffd08c583d1b59)) +- 在 HomePage 组件中新增音频兼容性诊断功能,优化视频播放体验;更新视频兼容性报告以支持音频编解码器检测;重构相关逻辑以提升代码可读性和维护性 ([3cac307](https://github.com/mkdir700/EchoPlayer/commit/3cac307f50c791b2f4f76b3e0aec7571d4e30a98)) +- 在 SubtitleListContent 组件中引入 rc-virtual-list 以优化字幕列表渲染性能,增强自动滚动功能 ([30165dc](https://github.com/mkdir700/EchoPlayer/commit/30165dcf42f2770be23392f6c6d07a8d1786f95f)) +- 在 UpdatePromptDialog 组件中添加内容展开/折叠功能 ([96d9b1f](https://github.com/mkdir700/EchoPlayer/commit/96d9b1f32b15abea3661836a684e177e774b80e5)) +- 在构建和发布工作流中添加Windows、Mac和Linux平台的上传步骤,优化版本变量设置和上传路径逻辑 ([3cfacb9](https://github.com/mkdir700/EchoPlayer/commit/3cfacb91cd3c60428af09bdb1b1ed74be1538e29)) +- 在构建和发布工作流中添加更新 package.json 版本的步骤,确保版本号自动更新;优化草稿发布条件以支持预发布版本 ([a78fbc7](https://github.com/mkdir700/EchoPlayer/commit/a78fbc72166e08eeec641c0970eebf83763aba39)) +- 在构建和发布工作流中添加测试构建选项,更新版本变量设置和上传路径逻辑 ([2848f92](https://github.com/mkdir700/EchoPlayer/commit/2848f92f7216dee720d84608ffce2840f5f67bcd)) +- 在视频上传时重置字幕控制状态,新增重置状态功能;更新快捷键设置以支持单句循环功能,优化用户体验 ([688dcd6](https://github.com/mkdir700/EchoPlayer/commit/688dcd6e7ddfe43499035fd828bcf26f04e08d79)) +- 增强全屏模式下的样式支持 ([94a77b1](https://github.com/mkdir700/EchoPlayer/commit/94a77b1166173b73789d14daaba12d0b7de2790a)) +- 增强全屏模式的快捷键支持 ([218882c](https://github.com/mkdir700/EchoPlayer/commit/218882cdbdd597dff7cf41df3dac9e0587b43dd0)) +- 增强字幕显示组件,新增中文字符检测和智能文本分割功能,优化用户交互体验 ([8cd50d9](https://github.com/mkdir700/EchoPlayer/commit/8cd50d9df0e2884f27187bb0df66a2f0f3c232b2)) +- 增强字幕空状态组件,支持拖拽文件导入 ([db1f608](https://github.com/mkdir700/EchoPlayer/commit/db1f60833f27b75594790387c0382cc30abd28fe)) +- 增强字幕组件交互功能 ([3e7e8c7](https://github.com/mkdir700/EchoPlayer/commit/3e7e8c74651da43cbcd5e525ba76324e6c403fd8)) +- 增强字幕组件和文本选择功能 ([36c44aa](https://github.com/mkdir700/EchoPlayer/commit/36c44aae0884e22030aae37db7424ac92e3f2c60)) +- 增强快捷键设置功能,新增快捷键冲突检查和平台特定符号显示,优化用户输入体验和界面样式 ([bde034b](https://github.com/mkdir700/EchoPlayer/commit/bde034bccab0dec6dbeb305fd5c4b7aca76caa91)) +- 增强更新通知系统,添加红点提示和用户交互逻辑 ([fdf4c81](https://github.com/mkdir700/EchoPlayer/commit/fdf4c811e2cf2611319adc6b706e12b5510fe5c8)) +- 增强版本比较逻辑,优化更新通知系统 ([f29a25f](https://github.com/mkdir700/EchoPlayer/commit/f29a25fc4d9859375654c8c3e1f532224e8e3049)) +- 增强视频兼容性模态框功能,支持初始步骤和分析结果 ([3aba45c](https://github.com/mkdir700/EchoPlayer/commit/3aba45c3464151598c1b8400c8e14d2c612f53bf)) +- 多平台构建和发布 ([cc521ea](https://github.com/mkdir700/EchoPlayer/commit/cc521ea8befde2b839810292e93475a806db4dd1)) +- 实现动态 electron-updater 渠道配置 ([28d2836](https://github.com/mkdir700/EchoPlayer/commit/28d28360a4e5cee11603cd68f959098d4e40ca0b)), closes [#3](https://github.com/mkdir700/EchoPlayer/issues/3) +- 将发布提供者从 generic 更改为 github,更新仓库和所有者信息,以支持自动更新功能 ([b6d4076](https://github.com/mkdir700/EchoPlayer/commit/b6d4076ff094f31d5f4eedf08e6b943f41f5fed6)) +- 引入常量以支持视频容器格式检查 ([da68183](https://github.com/mkdir700/EchoPlayer/commit/da681831b60f4655b72731fd1ba34e5550149543)) +- 新增 AimButton 组件以支持手动定位当前字幕并启用自动滚动;更新 SubtitleListContent 组件以集成 AimButton,优化用户滚动体验与字幕自动滚动逻辑 ([3c8a092](https://github.com/mkdir700/EchoPlayer/commit/3c8a09208d773f7a7e5d86bfb6a7ef26cfadf444)) +- 新增 AppHeader 组件并更新样式,调整导航菜单布局以提升用户体验 ([94e35c3](https://github.com/mkdir700/EchoPlayer/commit/94e35c30ff96b534046190cbd654097f0b960095)) +- 新增 cmd-reason.mdc 文件并更新 cmd-refactor-theme.mdc 规则 ([43d2222](https://github.com/mkdir700/EchoPlayer/commit/43d22225b7dd3546a90aec05ee5efb2dd158c8f6)) +- 新增 E2E 测试用例和文件选择器助手 ([9928349](https://github.com/mkdir700/EchoPlayer/commit/99283494eddf4612fa0d9434473974337045b052)) +- 新增 git commit 内容生成规则文件 ([6e0ee23](https://github.com/mkdir700/EchoPlayer/commit/6e0ee238be5d22aed2e23bf8cb4f51d5918d2a51)) +- 新增主题系统 ([369d828](https://github.com/mkdir700/EchoPlayer/commit/369d828232f0e07d1212e750b961871fe8024a3f)) +- 新增全屏模式支持 ([e8c9542](https://github.com/mkdir700/EchoPlayer/commit/e8c9542fef5766a048bd1fa65f11858cf1a44e7e)) +- 新增全屏视频进度条组件并重构视频控制逻辑 ([7fc587f](https://github.com/mkdir700/EchoPlayer/commit/7fc587f93312c6d34869543ffab8153a20aa2975)) +- 新增划词选中和快捷复制功能 ([9e22b44](https://github.com/mkdir700/EchoPlayer/commit/9e22b44a921ddea67f1ee95931c65116c619a9c2)) +- 新增单词卡片组件,支持单词点击后显示详细信息和发音功能;优化字幕显示样式,提升用户交互体验 ([c6a4ab6](https://github.com/mkdir700/EchoPlayer/commit/c6a4ab6446e9ebc9e55d46b52d84e93987673706)) +- 新增字幕列表上下文及相关钩子,重构播放页面以使用新的字幕管理逻辑,提升代码可读性与功能性 ([7766b74](https://github.com/mkdir700/EchoPlayer/commit/7766b74f5b7d472c94e78678f685ae1934e9c617)) +- 新增字幕列表项样式并禁用焦点样式 ([654a0d1](https://github.com/mkdir700/EchoPlayer/commit/654a0d1d6581749a8c651d322444df60252dff38)) +- 新增字幕布局锁定功能 ([82e75dc](https://github.com/mkdir700/EchoPlayer/commit/82e75dcb0741f6275fcf2863fccb6244383c75b2)) +- 新增字幕模式覆盖层组件及相关逻辑 ([e75740c](https://github.com/mkdir700/EchoPlayer/commit/e75740cd20e588ae2542ece91acebd4f86206b51)) +- 新增字幕空状态组件和外部链接打开功能 ([5bd4bd6](https://github.com/mkdir700/EchoPlayer/commit/5bd4bd6cb5f283114c83188c15301afe50b5d3c6)) +- 新增字幕组件样式,重构相关组件以支持主题系统,提升视觉一致性和用户体验 ([822cb74](https://github.com/mkdir700/EchoPlayer/commit/822cb74a9348d89527f3871ba7d37f92952e3165)) +- 新增字幕重置功能,优化字幕设置管理;重构相关组件以提升用户体验和代码可维护性 ([f4702a5](https://github.com/mkdir700/EchoPlayer/commit/f4702a5f59b77e36a8301c856c1ab81c3d8e26b5)) +- 新增存储管理功能,添加最近播放项的增删改查接口,优化用户体验;重构相关组件,提升代码结构与可维护性 ([a746ed3](https://github.com/mkdir700/EchoPlayer/commit/a746ed388e2476c8a84f45ec13f7e5ab6af8ad82)) +- 新增当前字幕显示上下文管理,优化字幕点击交互逻辑,确保用户体验流畅;重构相关组件以提升代码可维护性 ([91a215d](https://github.com/mkdir700/EchoPlayer/commit/91a215d0fa116f04c8f88403123574f0d6d7dd6f)) +- 新增快捷键设置模态框和快捷键显示组件,优化用户输入体验 ([b605257](https://github.com/mkdir700/EchoPlayer/commit/b605257cd97fec50e58143eba479e39defe449b6)) +- 新增控制弹窗样式并优化字幕模式选择器的交互体验;重构相关组件以提升代码可读性和用户体验 ([79eabdf](https://github.com/mkdir700/EchoPlayer/commit/79eabdfc684ba172425afc80dc61bb47ea95c78d)) +- 新增播放设置上下文,重构相关组件以支持播放设置的管理;更新播放页面以使用新的播放设置上下文,提升代码可读性与功能性 ([6fe8b4f](https://github.com/mkdir700/EchoPlayer/commit/6fe8b4fed2d3ea0bf9b2487f48fe7bf98d293ba6)) +- 新增播放速度覆盖层和相关功能 [#1](https://github.com/mkdir700/EchoPlayer/issues/1) ([d8637eb](https://github.com/mkdir700/EchoPlayer/commit/d8637eb6046ce8f24b0e4e08794681bf59a93ba9)) +- 新增数据清理功能,优化日志记录中的数据序列化,确保记录的日志信息更为准确和安全 ([8ada21a](https://github.com/mkdir700/EchoPlayer/commit/8ada21a07acc9dcb06a59b205f9b42c326d6472f)) +- 新增数据目录管理功能 ([2c93e19](https://github.com/mkdir700/EchoPlayer/commit/2c93e19e51efadcf0a91e55ae07b40c0589f2f2a)) +- 新增日志系统,集成 electron-log 以支持主进程和渲染进程的日志记录;更新相关 API 以便于日志管理和调试 ([1f621d4](https://github.com/mkdir700/EchoPlayer/commit/1f621d42eaa8cce3ca13a1eec4c6fb5235a2d671)) +- 新增智能分段功能及相关测试 ([f5b8f5c](https://github.com/mkdir700/EchoPlayer/commit/f5b8f5c96a00b64bc820335a3ed16083a7e44ce0)) +- 新增视频UI配置管理功能 ([eaf7e41](https://github.com/mkdir700/EchoPlayer/commit/eaf7e418bf8d6ea7169f243e3afef5d2b8cb542a)) +- 新增视频管理组件和确认模态框 ([4263c67](https://github.com/mkdir700/EchoPlayer/commit/4263c672a1bba108b83d80a1cfa78d71b6c6edb9)) +- 新增视频转码功能及兼容性警告模态框 ([4fc86a2](https://github.com/mkdir700/EchoPlayer/commit/4fc86a28338e9814fb1b2c98780645cf23f35cda)) +- 新增第三方服务配置组件,整合 OpenAI 和词典服务设置,优化用户界面和交互体验;引入模块化样式,提升整体一致性 ([3e45359](https://github.com/mkdir700/EchoPlayer/commit/3e45359efb188e7108ee4eb9663768b18b444678)) +- 新增获取所有字幕的功能,优化字幕查找逻辑以支持根据当前时间查找上下句字幕,提升用户体验 ([04c5155](https://github.com/mkdir700/EchoPlayer/commit/04c5155f1276968967591acbaedb36200915a5cc)) +- 新增词典服务相关的 IPC 处理器,支持有道和欧陆词典的 API 请求;实现 SHA256 哈希计算功能,增强应用的词典查询能力 ([707ee97](https://github.com/mkdir700/EchoPlayer/commit/707ee97b2680efcf9057acfd802e6113d4f89d8d)) +- 新增边距验证逻辑,优化字幕拖拽和调整大小功能,确保字幕区域不超出容器边界 ([2294bcf](https://github.com/mkdir700/EchoPlayer/commit/2294bcffac6fc83e4d21e543c276cceaea0189ff)) +- 更新 AppHeader 组件,增加背景装饰、应用图标和名称,优化导航按钮和辅助功能按钮的样式,提升用户体验 ([651c8d7](https://github.com/mkdir700/EchoPlayer/commit/651c8d79acf0649f24a30acc4a7a714f112ec85a)) +- 更新 AppHeader 组件,调整文本样式和名称,提升视觉效果 ([f208d66](https://github.com/mkdir700/EchoPlayer/commit/f208d66199d33de47c8b3f885c6f95ca655081ac)) +- 更新 GitHub Actions 工作流和文档,支持更多发布文件 ([c4bf6f7](https://github.com/mkdir700/EchoPlayer/commit/c4bf6f7a00d332a3e71f0796dbbdcf3c397ef175)) +- 更新 index.html 文件,修改内容安全策略以支持新的脚本源,添加本地开发服务器的支持,优化页面加载逻辑 ([8c11edf](https://github.com/mkdir700/EchoPlayer/commit/8c11edfc841058448c24be872d641b98beda52ec)) +- 更新 PlaybackRateSelector 组件样式和文本 ([034e758](https://github.com/mkdir700/EchoPlayer/commit/034e7581ec6facdffbd7cafc276449f7733c231b)) +- 更新 SubtitleListContent 组件,替换 rc-virtual-list 为 react-virtualized,优化字幕列表渲染性能与用户体验;调整样式以适配虚拟列表,增强滚动效果与响应式设计 ([63d9ef4](https://github.com/mkdir700/EchoPlayer/commit/63d9ef4229b9e159b0da5ae272229d192cc27a25)) +- 更新 SubtitleListContent 组件,添加激活字幕索引状态以优化渲染逻辑;重构字幕项组件以减少不必要的重渲染并提升性能;增强自动滚动逻辑,确保用户体验流畅 ([c997109](https://github.com/mkdir700/EchoPlayer/commit/c997109154faf0a92186bb94a8d2a019d85086e2)) +- 更新E2E测试,移除冗余测试用例并优化测试ID使用 ([51fd721](https://github.com/mkdir700/EchoPlayer/commit/51fd721ecd84bf54472f18298e0541d20d0d1cb8)) +- 更新E2E测试配置,添加Linux虚拟显示器支持并检查构建输出 ([ac1999f](https://github.com/mkdir700/EchoPlayer/commit/ac1999f8b30cf8cf48f9528b93b3fcb68b1c1b79)) +- 更新主题系统,新增字体粗细、间距、圆角等设计令牌,优化组件样式一致性 ([62f87dd](https://github.com/mkdir700/EchoPlayer/commit/62f87dd4fc868eefaf7d26008204edde9e778bb4)) +- 更新侧边栏导航功能和禁用状态提示 ([d41b25f](https://github.com/mkdir700/EchoPlayer/commit/d41b25f88d5a5b428b3a159db432fa951178a469)) +- 更新最近播放项管理,使用文件ID替代原有ID,新增根据文件ID获取最近播放项的功能,优化播放设置管理,提升代码可维护性 ([920856c](https://github.com/mkdir700/EchoPlayer/commit/920856c095a8ac4d5d41dab635390493c13774ad)) +- 更新图标文件,替换Mac和Windows平台的图标,优化SVG图标文件结构 ([bfe456f](https://github.com/mkdir700/EchoPlayer/commit/bfe456f9109fd99022796d8be8c533ba31c1fd9f)) +- 更新图标资源,替换 PNG 格式图标并新增 SVG 格式图标,提升图标的可扩展性与清晰度 ([8eaf560](https://github.com/mkdir700/EchoPlayer/commit/8eaf5600cff468fceb1d36bca6416a52e43f9aa9)) +- 更新字典引擎设置,默认选择为 'eudic-html',提升用户体验 ([ebaa5d2](https://github.com/mkdir700/EchoPlayer/commit/ebaa5d290cbbfcd1c2a5f5d7b8ed99ce9bbad449)) +- 更新字幕上下文菜单,优化重置按钮状态和样式 ([cc542f2](https://github.com/mkdir700/EchoPlayer/commit/cc542f27e241c920e80272cc2c68d2aaa7ba00da)) +- 更新字幕列表项组件,添加注释以说明仅展示学习语言,优化双语字幕显示逻辑 ([89e2b33](https://github.com/mkdir700/EchoPlayer/commit/89e2b33e65f7565ee4b89a329963c66b29a78df6)) +- 更新字幕加载功能,新增对 ASS/SSA 格式的支持;优化字幕文件扩展名和解析逻辑,提升用户体验 ([9cab843](https://github.com/mkdir700/EchoPlayer/commit/9cab843eeb33c3429ce1c2a9e78f33eeee743191)) +- 更新字幕加载模态框样式,新增加载状态提示与取消功能;重构相关逻辑以提升用户体验与代码可读性 ([1f8442a](https://github.com/mkdir700/EchoPlayer/commit/1f8442a0f6eec1afd4421f520774b4132528a3a2)) +- 更新字幕展示组件样式,添加浮动控制按钮及其样式,优化响应式设计 ([ac586e2](https://github.com/mkdir700/EchoPlayer/commit/ac586e2cd1e7670855b0b92bc3dc887ec9586658)) +- 更新字幕控制功能,添加自动暂停选项,修改快捷键设置,优化相关逻辑和组件交互 ([428e4cf](https://github.com/mkdir700/EchoPlayer/commit/428e4cfc1ba2f3856e604dd82614388c1e2d09a0)) +- 更新字幕模式选择器,整合字幕显示模式的获取逻辑,优化状态管理,增强调试信息 ([c2d3c90](https://github.com/mkdir700/EchoPlayer/commit/c2d3c90cfa07c64fbd4a21ef0ee962cc389b121f)) +- 更新循环播放设置,支持无限循环和自定义次数 ([e6c5d2e](https://github.com/mkdir700/EchoPlayer/commit/e6c5d2e3b291b3e5e4c562e43da278673c51ae23)) +- 更新快捷键设置,修改单句循环和字幕导航的快捷键,优化用户体验 ([ce66e62](https://github.com/mkdir700/EchoPlayer/commit/ce66e6208e920bc6d75a1750c06de27c2958f7cd)) +- 更新总结规则,启用始终应用选项;新增指令处理逻辑以提取项目开发指导内容并编写开发文档,确保文档规范性 ([d627e2e](https://github.com/mkdir700/EchoPlayer/commit/d627e2ec7676413f96950f580a6cddc73c9ff325)) +- 更新构建产物处理逻辑,支持多架构文件重命名和 YAML 文件引用更新 ([e206e1d](https://github.com/mkdir700/EchoPlayer/commit/e206e1d5386855e5819ce6b74000487d51aa2d77)) +- 更新构建配置,支持多架构构建和文件重命名 ([17b862d](https://github.com/mkdir700/EchoPlayer/commit/17b862d57bde74e4cff8c4f89ae423b183b1e9ed)) +- 更新样式文件,优化警告框和卡片组件的视觉效果,增强响应式设计支持 ([ea6b4ab](https://github.com/mkdir700/EchoPlayer/commit/ea6b4ab9142e5cade134113e613c69b109b86889)) +- 更新滚动条样式以支持 WebKit 规范 ([224f41d](https://github.com/mkdir700/EchoPlayer/commit/224f41d853a274324d5d1bbbf4ac7d07214cca96)) +- 更新视频上传钩子,使用日志系统记录视频DAR信息和错误警告,提升调试能力 ([2392b38](https://github.com/mkdir700/EchoPlayer/commit/2392b3806dfdf8134555a6b006ea833065459a09)) +- 更新视频兼容性模态框样式,提升用户体验 ([f5c1ba5](https://github.com/mkdir700/EchoPlayer/commit/f5c1ba5e42d44d65b5c0df55c70e9e2f44cbb855)) +- 更新视频播放器和播放状态管理逻辑,重构字幕处理方式,统一使用 subtitleItems 以提升代码一致性与可读性;优化播放状态保存与恢复机制,确保更流畅的用户体验 ([0cbe11d](https://github.com/mkdir700/EchoPlayer/commit/0cbe11d4324806dbdab67dd181ac28acd5e45c06)) +- 更新视频播放器的时间跳转逻辑,支持来源标记 ([f170ff1](https://github.com/mkdir700/EchoPlayer/commit/f170ff1b508c40bb122a20262ba436e4132c77da)) +- 更新视频文件信息样式,添加文件名截断功能,优化头部布局以提升用户体验 ([a6639f1](https://github.com/mkdir700/EchoPlayer/commit/a6639f1494862620bc0fec6f7e140e6bd773335f)) +- 更新窗口管理和标题栏组件,优化样式和功能 ([a1b50f6](https://github.com/mkdir700/EchoPlayer/commit/a1b50f6c52142a9cc2c2df12b98644e1e11ddfa6)) +- 更新窗口管理器的窗口尺寸和最小尺寸,优化用户界面;移除不必要的响应式设计样式,简化 CSS 结构 ([dd561cf](https://github.com/mkdir700/EchoPlayer/commit/dd561cf35995ebd504553a26d2c83b73da06e3f1)) +- 更新第三方服务配置组件,修改标签和提示文本为中文,增强用户友好性;新增申请应用ID和密钥的链接提示,提升信息获取便利性 ([5e68e85](https://github.com/mkdir700/EchoPlayer/commit/5e68e8507f1d5509cdcb2fb3459a570d92287aa9)) +- 更新设置导航组件样式和功能 ([535f267](https://github.com/mkdir700/EchoPlayer/commit/535f267b140bc918672fdadaae6445b9eda0707f)) +- 更新设置页面,移除视频转换相关功能 ([0d96fac](https://github.com/mkdir700/EchoPlayer/commit/0d96facf476cd73aa64fd023fb01c3a2442d0dbe)) +- 更新设置页面,简化快捷键和数据管理部分的渲染逻辑,新增存储设置选项,优化用户界面和交互体验 ([9942740](https://github.com/mkdir700/EchoPlayer/commit/9942740d9bca7ba55cd4f730ed2214ed405ed867)) +- 更新设置页面样式和主题支持 ([816ca6d](https://github.com/mkdir700/EchoPlayer/commit/816ca6d3d747ace1a18cf5f01523ee56ab8cb120)) +- 更新设置页面的按钮样式和移除音频兼容性警告 ([f0be1e2](https://github.com/mkdir700/EchoPlayer/commit/f0be1e206fb69f3a75ad292dc8c3a90f02fced14)) +- 更新通知系统优化,增强用户交互体验 ([6df4374](https://github.com/mkdir700/EchoPlayer/commit/6df4374ceb90b401799107c344211b164f7a0164)) +- 更新页面渲染逻辑,添加页面冻结功能,确保首页始终挂载并优化其他页面的条件渲染,提升用户体验 ([7a4b2ba](https://github.com/mkdir700/EchoPlayer/commit/7a4b2ba5d72a83f3765b003384d09295e70403e5)) +- 替换应用头部为侧边栏组件 ([0e621fc](https://github.com/mkdir700/EchoPlayer/commit/0e621fca1703f7461a101d2899ce7d85626156ff)) +- 沉浸式标题栏 ([9c7c7d9](https://github.com/mkdir700/EchoPlayer/commit/9c7c7d9b91ba0c505d72cc3cf2d11b9049bd62a3)) +- 添加 @ant-design/v5-patch-for-react-19 支持 React19 ([95d1019](https://github.com/mkdir700/EchoPlayer/commit/95d1019a02fb244e558f8819e4c52e3a7b0bc1bf)) +- 添加 Stagewise 工具栏支持,仅在开发模式下初始化,更新 CSP 设置以允许外部样式源 ([ededb64](https://github.com/mkdir700/EchoPlayer/commit/ededb643573969a41ebc57a9666fbfd928e44e7c)) +- 添加Codecov配置文件,更新测试配置以支持覆盖率报告上传 ([d9ec00d](https://github.com/mkdir700/EchoPlayer/commit/d9ec00d895792eca2c9ad6ea455f5b1eaadb2078)) +- 添加E2E测试支持,更新Playwright配置和相关脚本 ([247b851](https://github.com/mkdir700/EchoPlayer/commit/247b85122ab88b05e789388e18b696769256e226)) +- 添加全屏功能支持,优化视频播放器组件,更新样式以移除不必要的自定义样式,提升用户体验 ([a7d4b1c](https://github.com/mkdir700/EchoPlayer/commit/a7d4b1c1408ec6177ec07c60280993f23af8c605)) +- 添加字幕控制组件,支持单句循环和自动循环功能,更新快捷键设置,优化样式和响应式设计 ([2902f2d](https://github.com/mkdir700/EchoPlayer/commit/2902f2d54e929b433ba7dfa2ed9ebe32dc8b2d58)) +- 添加应用图标 ([b86e142](https://github.com/mkdir700/EchoPlayer/commit/b86e1420b4ca8701354d644b45653ac039845db2)) +- 添加应用图标并优化代码中的事件监听和清理逻辑 ([c39da08](https://github.com/mkdir700/EchoPlayer/commit/c39da08c7ab5a17ed4fb718bcfc10df4a2b94cb9)) +- 添加当前字幕展示组件,支持多种字幕显示模式及单词hover交互,优化视频控制区样式和响应式设计 ([df4b74a](https://github.com/mkdir700/EchoPlayer/commit/df4b74a98c5ae66e6c2d3be24e25c7e4261fc70e)) +- 添加循环播放功能,支持自定义循环次数设置 ([1dbccfa](https://github.com/mkdir700/EchoPlayer/commit/1dbccfae97c22ac49a08e78287211f89ccf3aa46)) +- 添加文件系统相关的 IPC 处理器,支持文件存在性检查、读取文件内容、获取文件 URL、文件信息获取及文件完整性验证;更新 preload 和 renderer 逻辑以支持视频和字幕文件的选择与恢复功能,优化用户体验 ([6d361eb](https://github.com/mkdir700/EchoPlayer/commit/6d361eb0ec1e8f8aa2eaca5167736bd1373d93bb)) +- 添加更新通知和提示对话框组件 ([38df4d2](https://github.com/mkdir700/EchoPlayer/commit/38df4d2b55af83f3007f1b242da19eb02cca8a11)) +- 添加更新通知跳过版本功能,优化用户体验 ([165adb6](https://github.com/mkdir700/EchoPlayer/commit/165adb69c7a4dae1d2749592b01f1561580c58ec)) +- 添加本地更新测试环境脚本和相关功能 ([00aa019](https://github.com/mkdir700/EchoPlayer/commit/00aa01940583ec30e79034495fe804febe4479ab)) +- 添加构建产物重命名和验证脚本 - 新增 rename-artifacts.ts 用于重命名构建产物以符合发布要求 - 新增 verify-build-artifacts.ts 用于验证构建产物的存在性和完整性 ([696cedc](https://github.com/mkdir700/EchoPlayer/commit/696cedc090caaa56b3f2c4921022d9e131d361ac)) +- 添加构建和发布工作流,更新测试和发布脚本 ([2744005](https://github.com/mkdir700/EchoPlayer/commit/2744005aefb85874651d7e7937e5af1f9ead8b35)) +- 添加欧陆词典HTML解析服务和单元测试框架 ([52ace3e](https://github.com/mkdir700/EchoPlayer/commit/52ace3ef0ba4aa0b58d000996d1f933365c093ce)) +- 添加测试Electron CDP连接的脚本 ([9982514](https://github.com/mkdir700/EchoPlayer/commit/9982514f56ccbb6b048d0a4d961f8a5b7b29eea0)) +- 添加版本管理脚本,支持版本类型检测和版本号递增功能;更新构建和发布工作流,优化版本变量设置和上传路径逻辑;新增发布指南文档,详细说明版本管理和发布流程 ([282bde8](https://github.com/mkdir700/EchoPlayer/commit/282bde883d4c8ae965963e555feb1cd4a011ab88)) +- 添加视频播放器点击事件处理,优化用户交互体验 ([69c378f](https://github.com/mkdir700/EchoPlayer/commit/69c378fad8aa8c15b29833e668fd150775c477e3)) +- 添加视频文件选择加载状态和清空确认模态框 ([ca95a7d](https://github.com/mkdir700/EchoPlayer/commit/ca95a7d5f2cae1e42423dbe7cfe3c7d09352e16c)) +- 添加视频格式转换功能,新增视频兼容性检测与转换指南,优化视频播放器与文件上传逻辑,提升用户体验;重构相关组件,简化代码结构 ([5fd89fe](https://github.com/mkdir700/EchoPlayer/commit/5fd89fed2b346efbb4d0e5c0d029af51e60f07a1)) +- 添加腾讯云COS上传功能,支持发布文件和自动更新文件的上传 ([e79e5a9](https://github.com/mkdir700/EchoPlayer/commit/e79e5a9c5b5e29f2092d50aa9af58dafa6297612)) +- 添加自动更新功能,整合更新处理器,更新设置界面,支持版本检查和下载 ([5e5a03e](https://github.com/mkdir700/EchoPlayer/commit/5e5a03e5966e3978ba16d76ed202ce943903e3a1)) +- 添加页面切换过渡效果,优化播放页面与性能监控功能;重构相关组件,提升用户交互体验与代码结构 ([e583ecc](https://github.com/mkdir700/EchoPlayer/commit/e583ecc78836dc241392039c76092833ca354695)) +- 添加页面导航功能,重构 App 组件以支持多页面切换,新增关于、收藏、设置等页面,优化样式和用户体验 ([51f4263](https://github.com/mkdir700/EchoPlayer/commit/51f426365c12474091e8581211da3e7e36d29749)) +- 添加高效测试标识符管理指南及相关工具函数,优化E2E测试中的测试ID使用 ([2dcfe5e](https://github.com/mkdir700/EchoPlayer/commit/2dcfe5e7443095890acc7034a5b919059dcad2bc)) +- 现代化视频控制组件,优化样式和交互逻辑,增强用户体验;添加音量和设置控制,支持自动隐藏功能 ([dc45b83](https://github.com/mkdir700/EchoPlayer/commit/dc45b83bbaf02f90ccde2559f203520b172a0388)) +- 移除 HomePage 组件中的 subtitleIndex 属性,优化视频播放状态管理逻辑;调整视频网格布局以提升用户界面的一致性与可读性 ([8f54e7f](https://github.com/mkdir700/EchoPlayer/commit/8f54e7fb54574552a308c7af5188f5f46d5a37ce)) +- 移除 PlayPageHeader 的 CSS 模块,改为使用主题系统样式管理,提升组件的可维护性和一致性 ([52cedbc](https://github.com/mkdir700/EchoPlayer/commit/52cedbc09e95d3fe91308e1bdc70a38d1c988315)) +- 移除 useSidebarResize 钩子及相关样式,改用 Ant Design 的 Splitter 组件实现侧边栏调整功能,优化播放页面布局与用户体验 ([bead645](https://github.com/mkdir700/EchoPlayer/commit/bead645f2680363621a7cc7dd6139aa990aa7750)) +- 移除字幕位置控制相关组件及其逻辑,简化视频控制界面以提升用户体验 ([1edc857](https://github.com/mkdir700/EchoPlayer/commit/1edc857e3ef1f8ac87c35e6c62f1bdfcd4b545c6)) +- 移除字幕设置相关功能和组件 ([32f0138](https://github.com/mkdir700/EchoPlayer/commit/32f0138c6285b6a5805720c9306dad2d4cfd7783)) +- 移除推荐视频假数据,更新欢迎信息,优化首页布局和用户体验 ([78b000f](https://github.com/mkdir700/EchoPlayer/commit/78b000fcf2bd8511ef35e79f5a03114dfed297d4)) +- 移除视频播放器和播放控制钩子,简化代码结构以提升可维护性 ([513ba3c](https://github.com/mkdir700/EchoPlayer/commit/513ba3c21f67fbc76fc3a61ac5b128506cca68db)) +- 移除视频播放器的响应式设计中不必要的内边距,简化 CSS 结构 ([f8c8c28](https://github.com/mkdir700/EchoPlayer/commit/f8c8c2899d8545486a23421d882ecfd1c186446c)) +- 调整 HomePage 组件的响应式布局,优化列宽设置以提升用户体验 ([3c435bf](https://github.com/mkdir700/EchoPlayer/commit/3c435bfee87ab529333a2dcf3fe51553b089cc45)) +- 调整主题样式宽度 ([2fe9ff2](https://github.com/mkdir700/EchoPlayer/commit/2fe9ff24d2f35791712b3bf5b848fc7464b08fef)) +- 调整全屏视频控制组件的进度条位置和样式 ([679521f](https://github.com/mkdir700/EchoPlayer/commit/679521f2f92c1c04646a89866944b20d22d6a917)) +- 调整字幕覆盖层样式,修改底部位置为0%,移除移动端特定样式,简化 CSS 结构 ([515151d](https://github.com/mkdir700/EchoPlayer/commit/515151d022fc16d886471aad43aeda43f482214c)) +- 重命名视频控制组件为 VideoControlsFullScreen,更新相关导入,提升代码可读性 ([0fe7954](https://github.com/mkdir700/EchoPlayer/commit/0fe795404702dd1a9b68c32a21fec5ad003dcf8d)) +- 重构 SidebarSection 和 SubtitleListContent 组件,简化属性传递,增强字幕索引处理逻辑,优化自动滚动功能;新增获取指定时间点字幕索引的功能,提升用户体验与代码可读性 ([dabcbeb](https://github.com/mkdir700/EchoPlayer/commit/dabcbeb0718e2f8d6923a223d3c57e79453366a9)) +- 重构字幕控制组件样式,使用主题系统优化按钮和图标样式,提升视觉一致性和用户体验 ([12e38f2](https://github.com/mkdir700/EchoPlayer/commit/12e38f2f260467e297ad11831ff1a44eea08c317)) +- 重构字幕状态管理,新增视频特定字幕设置 ([ff5b5de](https://github.com/mkdir700/EchoPlayer/commit/ff5b5def52690c082eae9f26029f6d139d80cd47)) +- 重构字幕组件,新增字幕覆盖层和文本组件,优化字幕显示逻辑和性能;移除旧版字幕组件,提升代码可维护性 ([4fbef84](https://github.com/mkdir700/EchoPlayer/commit/4fbef8419f703f398593043932f07a14a78e170c)) +- 重构存储处理器模块,优化应用配置和通用存储功能 ([065c30d](https://github.com/mkdir700/EchoPlayer/commit/065c30d7cbc8fc5b01f3f3b59211e6548d679cdc)) +- 重构存储管理功能,更新最近播放项的类型定义,优化播放设置管理,增强用户体验;新增播放设置的深度合并逻辑,提升代码可维护性 ([3f928d4](https://github.com/mkdir700/EchoPlayer/commit/3f928d4c84df465574fde222fcb1dccf72c3dfc6)) +- 重构应用布局与样式,新增主页与播放页面组件,优化用户交互体验;整合最近文件管理功能,提升视频文件选择与加载逻辑 ([f3fefad](https://github.com/mkdir700/EchoPlayer/commit/f3fefadd3643f20e2935d2d72eeea1e56a65a1d1)) +- 重构循环切换功能,简化状态管理和播放逻辑 ([fe11037](https://github.com/mkdir700/EchoPlayer/commit/fe11037cb82119f3ff3e74c825a44e80022158f7)) +- 重构播放状态管理,替换为使用最近播放列表钩子,简化参数传递并优化代码逻辑;新增最近播放列表钩子以支持播放项的增删改查功能 ([1ec2cac](https://github.com/mkdir700/EchoPlayer/commit/1ec2cac7f2a84f11b1ff4ddd2d482a45c8eae1bd)) +- 重构播放页面,整合视频播放器与字幕控制逻辑,新增 VideoPlayerProvider 以管理视频播放状态,优化组件结构与性能;移除不再使用的 SubtitleControls 组件,简化属性传递,提升代码可读性 ([e4111c9](https://github.com/mkdir700/EchoPlayer/commit/e4111c9274cb6b6dd112c5dd7f629b244450f802)) +- 重构视频控制组件,新增全屏控制样式与逻辑,优化播放控制体验;更新相关类型定义,提升代码可读性与功能性 ([5c72a1b](https://github.com/mkdir700/EchoPlayer/commit/5c72a1b0ce481c63b0d9c03f048b527f612052e0)) +- 重构视频播放上下文,新增视频文件上传和选择功能;更新相关组件以支持新的上下文逻辑,提升代码可读性与功能性 ([37e128e](https://github.com/mkdir700/EchoPlayer/commit/37e128eede0ad02f70ee7dc2aa2aab7d49a121df)) +- 重构视频播放器组件,移除 CSS Modules,采用主题系统样式管理,提升代码可维护性和一致性 ([b3981bc](https://github.com/mkdir700/EchoPlayer/commit/b3981bc2d86a7ea7d343f1e068f404b46af509f0)) +- 重构视频播放器逻辑,整合视频播放状态管理,优化组件结构;移除不再使用的 usePlayingVideoContext 钩子,新增多个视频控制钩子以提升性能与可读性 ([b1a6dc2](https://github.com/mkdir700/EchoPlayer/commit/b1a6dc29acb83fbdf4a167c9ded069f2e53d0491)) +- 重构视频播放设置管理,整合字幕显示设置,优化状态管理逻辑,提升用户体验和代码可维护性 ([6c3d852](https://github.com/mkdir700/EchoPlayer/commit/6c3d852fbb8d66e5f07942fdf792b661327b3a4a)) +- 重构设置页面,新增快捷键、数据管理和占位符组件,优化用户界面和交互体验;引入快捷键上下文管理,支持自定义快捷键功能 ([a498905](https://github.com/mkdir700/EchoPlayer/commit/a4989050d3a747b6a66d7deb2da21a1cf9a2a0be)) + +### Reverts + +- Revert "build: 在构建和发布工作流中添加调试步骤,列出下载的文件并检查Windows、Mac和Linux平台的自动更新配置文件是否存在" ([d0f8fc4](https://github.com/mkdir700/EchoPlayer/commit/d0f8fc4be0f3b976df0752a57437eb3cd16321ef)) +- Revert "chore: 更新 Linux 构建环境配置" ([cc179a0](https://github.com/mkdir700/EchoPlayer/commit/cc179a072721fd508662924073cf03bdfa684611)) +- Revert "feat: 在构建和发布工作流中添加更新 package.json 版本的步骤,确保版本号自动更新;优化草稿发布条件以支持预发布版本" ([be1cf26](https://github.com/mkdir700/EchoPlayer/commit/be1cf2668cf7ad777739bdb40e5b75e145775386)) + +### BREAKING CHANGES + +- **player,logging,state:** - Removed TransportBar and deprecated hooks (usePlayerControls/useVideoEvents/useSubtitleSync). Migrate to ControllerPanel with usePlayerEngine/usePlayerCommandsOrchestrated. + +* Player store control actions are engine-only; components should send commands via the orchestrator instead of mutating store directly. + ## [v0.2.0-alpha.7](https://github.com/mkdir700/echolab/tree/v0.2.0-alpha.7)(2025-06-20) ### ⚙️ 构建优化 diff --git a/CLAUDE.md b/CLAUDE.md index cfa44d42..877897ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,44 @@ - 在布局实现上,后续优先使用 flex 布局(尽量避免使用 grid 作为默认方案)。 - 项目优先使用 antd 组件库,如果组件可以被 antd 复用则优先使用 antd 而不是自定义开发 +## 主题变量使用最佳实践 + +项目启用了 Ant Design 的 CSS 变量模式 (`cssVar: true`),在 styled-components 中应采用分类使用策略: + +### 使用 CSS 变量的场景(主题相关属性): + +- 颜色系统:`var(--ant-color-bg-elevated, fallback)` +- 阴影效果:`var(--ant-box-shadow-secondary, fallback)` +- 主题切换时会发生变化的属性 + +### 使用 JS 变量的场景(设计系统常量): + +- 尺寸间距:`${SPACING.XS}px`、`${BORDER_RADIUS.SM}px` +- 动画配置:`${ANIMATION_DURATION.SLOW}`、`${EASING.APPLE}` +- 层级关系:`${Z_INDEX.MODAL}` +- 字体配置:`${FONT_SIZES.SM}px`、`${FONT_WEIGHTS.MEDIUM}` +- 毛玻璃效果:`${GLASS_EFFECT.BACKGROUND_ALPHA.LIGHT}` + +### 推荐模式: + +```typescript +const StyledComponent = styled.div` + /* 主题相关:使用 CSS 变量 */ + background: var(--ant-color-bg-elevated, rgba(0, 0, 0, ${GLASS_EFFECT.BACKGROUND_ALPHA.LIGHT})); + color: var(--ant-color-white, #ffffff); + box-shadow: var(--ant-box-shadow-secondary, ${SHADOWS.SM}); + + /* 设计系统常量:使用 JS 变量 */ + padding: ${SPACING.XS}px ${SPACING.MD}px; + border-radius: ${BORDER_RADIUS.SM}px; + font-size: ${FONT_SIZES.SM}px; + transition: opacity ${ANIMATION_DURATION.SLOW} ${EASING.APPLE}; + z-index: ${Z_INDEX.MODAL}; +` +``` + +这种混合模式既保持了类型安全和构建时优化,又支持主题的运行时切换,是当前架构下的最佳实践。 + # State Management - 项目使用 Zustand 作为状态管理库,配合 Immer 中间件支持不可变状态更新,使用自定义的中间件栈包含持久化、DevTools 和订阅选择器功能 @@ -43,3 +81,7 @@ - 任何组件或页面都不要支持写入currentTime,关于播放器的控制应该全权由编排器来控制 - 包管理器工具请使用 pnpm - logger 的使用例子: `logger.error('Error in MediaClock listener:', { error: error })`, 第二参数必须接收为 `{}` + +## Issues & Solutions + +1. DictionaryPopover 组件主题兼容性问题已修复:将硬编码的深色主题颜色(白色文字、深色背景)替换为 Ant Design CSS 变量(如 `var(--ant-color-text)`、`var(--ant-color-bg-elevated)`),实现浅色和深色主题的自动适配,包括文字颜色、背景色、边框、滚动条和交互状态的完整主题化。 diff --git a/LICENSE b/LICENSE index 261eeb9e..0ad25db4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 496826a4..25d3d6e9 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,10 @@ pnpm test:ui ## 🙏 致谢 -- [Cherry Studio](https://github.com/CherryHQ/cherry-studio) - 一款为创造而生的 AI 助手 +| 项目名 | 简介 | +| ------------------------------------------------------ | -------------------------------- | +| [EchoPlayer](https://github.com/CherryHQ/EchoPlayer) | 一款为创造而生的 AI 助手 | +| [DashPlayer](https://github.com/solidSpoon/DashPlayer) | 为英语学习者量身打造的视频播放器 | --- diff --git a/db/migrations/20250908171153_remove_path_unique_constraint.js b/db/migrations/20250908171153_remove_path_unique_constraint.js new file mode 100644 index 00000000..0661737a --- /dev/null +++ b/db/migrations/20250908171153_remove_path_unique_constraint.js @@ -0,0 +1,95 @@ +const { sql } = require('kysely') + +/** + * 删除 files 表中 path 字段的唯一约束 + * + * 由于 SQLite 不支持直接删除约束,我们需要: + * 1. 创建新表(无 path 唯一约束) + * 2. 迁移现有数据 + * 3. 删除旧表并重命名新表 + * 4. 重建必要的索引 + */ +async function up(db) { + // 1. 创建临时表,与原表结构相同但 path 字段无唯一约束 + await db.schema + .createTable('files_temp') + .addColumn('id', 'text', (col) => col.primaryKey().notNull()) + .addColumn('name', 'text', (col) => col.notNull()) + .addColumn('origin_name', 'text', (col) => col.notNull()) + .addColumn('path', 'text', (col) => col.notNull()) // 注意:这里去掉了 .unique() + .addColumn('size', 'integer', (col) => col.notNull()) + .addColumn('ext', 'text', (col) => col.notNull()) + .addColumn('type', 'text', (col) => col.notNull()) + .addColumn('created_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) + .execute() + + // 2. 复制现有数据到临时表 + await db + .insertInto('files_temp') + .columns(['id', 'name', 'origin_name', 'path', 'size', 'ext', 'type', 'created_at']) + .expression( + db + .selectFrom('files') + .select(['id', 'name', 'origin_name', 'path', 'size', 'ext', 'type', 'created_at']) + ) + .execute() + + // 3. 删除原表 + await db.schema.dropTable('files').execute() + + // 4. 将临时表重命名为原表名 + await db.schema.alterTable('files_temp').renameTo('files').execute() + + // 5. 重建所有索引(除了 path 的唯一约束) + await db.schema.createIndex('idx_files_name').on('files').column('name').execute() + await db.schema.createIndex('idx_files_type').on('files').column('type').execute() + await db.schema.createIndex('idx_files_created_at').on('files').column('created_at').execute() + await db.schema.createIndex('idx_files_ext').on('files').column('ext').execute() + + // 添加 path 字段的普通索引(非唯一)以保持查询性能 + await db.schema.createIndex('idx_files_path').on('files').column('path').execute() +} + +/** + * 回滚:重新添加 path 字段的唯一约束 + * 注意:如果数据中已存在重复路径,回滚可能会失败 + */ +async function down(db) { + // 1. 创建临时表,恢复 path 字段的唯一约束 + await db.schema + .createTable('files_temp') + .addColumn('id', 'text', (col) => col.primaryKey().notNull()) + .addColumn('name', 'text', (col) => col.notNull()) + .addColumn('origin_name', 'text', (col) => col.notNull()) + .addColumn('path', 'text', (col) => col.notNull().unique()) // 恢复唯一约束 + .addColumn('size', 'integer', (col) => col.notNull()) + .addColumn('ext', 'text', (col) => col.notNull()) + .addColumn('type', 'text', (col) => col.notNull()) + .addColumn('created_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) + .execute() + + // 2. 复制数据(如果有重复路径,这一步会失败) + await db + .insertInto('files_temp') + .columns(['id', 'name', 'origin_name', 'path', 'size', 'ext', 'type', 'created_at']) + .expression( + db + .selectFrom('files') + .select(['id', 'name', 'origin_name', 'path', 'size', 'ext', 'type', 'created_at']) + ) + .execute() + + // 3. 删除当前表 + await db.schema.dropTable('files').execute() + + // 4. 重命名临时表 + await db.schema.alterTable('files_temp').renameTo('files').execute() + + // 5. 重建索引(包括原有的所有索引) + await db.schema.createIndex('idx_files_name').on('files').column('name').execute() + await db.schema.createIndex('idx_files_type').on('files').column('type').execute() + await db.schema.createIndex('idx_files_created_at').on('files').column('created_at').execute() + await db.schema.createIndex('idx_files_ext').on('files').column('ext').execute() +} + +module.exports = { up, down } diff --git a/db/migrations/20250908220526_add_favorite_rates_fields.js b/db/migrations/20250908220526_add_favorite_rates_fields.js new file mode 100644 index 00000000..9a1beb72 --- /dev/null +++ b/db/migrations/20250908220526_add_favorite_rates_fields.js @@ -0,0 +1,115 @@ +const { sql } = require('kysely') + +/** + * Migration: Add favorite rates field to player settings + * + * Adds the following field to the playerSettings table: + * - favoriteRates: JSON string containing array of favorite playback rates + * + * Note: currentFavoriteIndex is a runtime state and doesn't need to be persisted + * + * This field supports the favorite playback rates feature that allows users to: + * 1. Configure default favorite rates in global settings + * 2. Customize favorite rates per video (persisted) + * 3. Cycle through favorite rates with mouse clicks and keyboard shortcuts (runtime) + */ +async function up(db) { + // Add favoriteRates column (JSON string) + await db.schema + .alterTable('playerSettings') + .addColumn('favoriteRates', 'text', (col) => + col.notNull().defaultTo(JSON.stringify([0.75, 1.0, 1.25, 1.5])) + ) + .execute() + + console.log('✅ Added favoriteRates field to playerSettings table') +} + +/** + * Rollback: Remove favorite rates field from player settings + */ +async function down(db) { + // SQLite doesn't support DROP COLUMN directly, so we need to recreate the table + // 1. Create temporary table without the favoriteRates column + await db.schema + .createTable('playerSettings_temp') + .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement()) + .addColumn('videoId', 'integer', (col) => + col.notNull().references('videoLibrary.id').onDelete('cascade') + ) + .addColumn('playbackRate', 'real', (col) => col.notNull().defaultTo(1.0)) + .addColumn('volume', 'real', (col) => col.notNull().defaultTo(1.0)) + .addColumn('muted', 'integer', (col) => col.notNull().defaultTo(0)) + .addColumn('loopSettings', 'text') + .addColumn('autoPauseSettings', 'text') + .addColumn('subtitleOverlaySettings', 'text') + .addColumn('created_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) + .addColumn('updated_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) + .execute() + + // 2. Copy existing data (excluding the favoriteRates column) + await db + .insertInto('playerSettings_temp') + .columns([ + 'id', + 'videoId', + 'playbackRate', + 'volume', + 'muted', + 'loopSettings', + 'autoPauseSettings', + 'subtitleOverlaySettings', + 'created_at', + 'updated_at' + ]) + .expression( + db + .selectFrom('playerSettings') + .select([ + 'id', + 'videoId', + 'playbackRate', + 'volume', + 'muted', + 'loopSettings', + 'autoPauseSettings', + 'subtitleOverlaySettings', + 'created_at', + 'updated_at' + ]) + ) + .execute() + + // 3. Drop original table + await db.schema.dropTable('playerSettings').execute() + + // 4. Rename temporary table + await db.schema.alterTable('playerSettings_temp').renameTo('playerSettings').execute() + + // 5. Recreate original indices + await db.schema + .createIndex('idx_playerSettings_videoId') + .ifNotExists() + .on('playerSettings') + .column('videoId') + .execute() + + await db.schema + .createIndex('idx_playerSettings_updated_at') + .ifNotExists() + .on('playerSettings') + .column('updated_at') + .execute() + + await db.schema + .createIndex('idx_playerSettings_videoId_unique') + .ifNotExists() + .on('playerSettings') + .column('videoId') + .unique() + .execute() + + console.log('✅ Removed favoriteRates field from playerSettings table') +} + +module.exports = { up, down } diff --git a/docs/FFmpeg-Integration.md b/docs/FFmpeg-Integration.md new file mode 100644 index 00000000..bf0521a3 --- /dev/null +++ b/docs/FFmpeg-Integration.md @@ -0,0 +1,113 @@ +# FFmpeg 内置集成功能 + +EchoPlayer 现在内置了 FFmpeg,无需用户手动安装。 + +## 功能特点 + +- ✅ **内置集成**:应用包含完整的 FFmpeg 二进制文件 +- ✅ **跨平台支持**:Windows (x64, arm64), macOS (x64, arm64), Linux (x64, arm64) +- ✅ **GPL 完整版**:包含所有编解码器,支持更多视频格式 +- ✅ **智能降级**:内置 FFmpeg 不可用时自动使用系统版本 +- ✅ **缓存机制**:避免重复下载,加速构建过程 + +## 开发命令 + +### FFmpeg 管理命令 + +```bash +# 下载当前平台的 FFmpeg +pnpm run ffmpeg:download + +# 下载所有支持平台的 FFmpeg +pnpm run ffmpeg:download-all + +# 清理下载缓存 +pnpm run ffmpeg:clean + +# 测试 FFmpeg 集成 +pnpm run ffmpeg:test +``` + +### 构建命令 + +```bash +# 正常构建(会自动下载 FFmpeg) +pnpm run build + +# 手动下载后构建 +pnpm run ffmpeg:download && pnpm run build +``` + +## 技术实现 + +### 1. 文件结构 + +``` +resources/ffmpeg/ + ├── darwin-x64/ffmpeg # macOS Intel + ├── darwin-arm64/ffmpeg # macOS Apple Silicon + ├── win32-x64/ffmpeg.exe # Windows x64 + ├── win32-arm64/ffmpeg.exe # Windows ARM64 + ├── linux-x64/ffmpeg # Linux x64 + └── linux-arm64/ffmpeg # Linux ARM64 +``` + +### 2. 路径解析策略 + +1. **内置优先**:优先使用应用内置的 FFmpeg +2. **环境降级**:内置不可用时使用系统 FFmpeg +3. **开发支持**:开发环境自动检测本地构建的 FFmpeg + +### 3. 构建集成 + +- **预构建钩子**:`prebuild` 脚本自动下载 FFmpeg +- **构建时复制**:Vite 插件自动复制文件到输出目录 +- **打包配置**:electron-builder 将 FFmpeg 包含在应用包中 + +## FFmpeg 版本信息 + +- **版本**:6.1 (GPL) +- **许可证**:GPL v3(与项目开源协议兼容) +- **包体积**:约 24MB 每平台 +- **功能完整**:支持所有主要编解码器 + +## 常见问题 + +### Q: 构建时下载失败怎么办? + +A: + +1. 检查网络连接 +2. 手动运行 `pnpm run ffmpeg:download` +3. 必要时清理缓存:`pnpm run ffmpeg:clean` + +### Q: 如何验证 FFmpeg 是否正确集成? + +A: 运行测试命令:`pnpm run ffmpeg:test` + +### Q: 应用包会增加多少大小? + +A: 每个平台约增加 24MB,实际应用只包含目标平台的版本 + +### Q: 如何指定下载特定平台? + +A: + +```bash +# 下载指定平台 +tsx scripts/download-ffmpeg.ts platform win32 x64 +tsx scripts/download-ffmpeg.ts platform darwin arm64 +tsx scripts/download-ffmpeg.ts platform linux x64 +``` + +## 许可证说明 + +本项目使用的 FFmpeg 为 GPL 版本,这与 EchoPlayer 的开源许可证兼容。GPL 版本提供了完整的编解码器支持,确保最佳的视频处理能力。 + +## 支持的格式 + +内置的 GPL 版本 FFmpeg 支持: + +- 视频编解码器:H.264, H.265, VP8, VP9, AV1 等 +- 音频编解码器:AAC, MP3, Opus, Vorbis 等 +- 容器格式:MP4, MKV, AVI, MOV, WebM 等 diff --git a/electron-builder.yml b/electron-builder.yml index 2d8c829d..d44b266b 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -55,9 +55,11 @@ win: - target: nsis arch: - x64 + - arm64 - target: portable arch: - x64 + - arm64 signtoolOptions: sign: scripts/win-sign.js verifyUpdateCodeSignature: false @@ -132,5 +134,5 @@ publish: electronDownload: mirror: https://npmmirror.com/mirrors/electron/ releaseInfo: - releaseName: 'EchoPlayer v${version}' + releaseName: EchoPlayer v${version} releaseNotesFile: 'build/release-notes.md' diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 15cc73b3..7c0a0668 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -13,9 +13,8 @@ export default defineConfig({ main: { plugins: [ externalizeDepsPlugin(), - // 复制迁移文件到构建目录 { - name: 'copy-migrations', + name: 'copy-files', generateBundle() { // 优先使用新的 db/migrations 路径 const newMigrationsDir = path.resolve('db/migrations') diff --git a/package.json b/package.json index 2c7c48f0..387a1977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "echoplayer", - "version": "1.0.0-alpha.1", + "version": "1.0.0-beta.2", "description": "EchoPlayer is a video player designed for language learners, helping users learn foreign languages efficiently through sentence-by-sentence intensive listening.", "main": "./out/main/index.js", "author": "echoplayer.cc", @@ -20,11 +20,12 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", + "build:release": "npm run typecheck && electron-vite build", "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir --publish never", - "build:win": "npm run build && electron-builder --win --publish never", - "build:win:x64": "npm run build && electron-builder --win --x64 --publish never", - "build:win:arm64": "npm run build && electron-builder --win --arm64 --publish never", + "build:win": "cross-env BUILD_TARGET_PLATFORM=win32 npm run build && electron-builder --win --publish never", + "build:win:x64": "cross-env BUILD_TARGET_PLATFORM=win32 BUILD_TARGET_ARCH=x64 npm run build && electron-builder --win --x64 --publish never", + "build:win:arm64": "cross-env BUILD_TARGET_PLATFORM=win32 BUILD_TARGET_ARCH=arm64 npm run build && electron-builder --win --arm64 --publish never", "build:mac": "electron-vite build && electron-builder --mac --publish never", "build:mac:x64": "npm run build && electron-builder --mac --x64 --publish never", "build:mac:arm64": "npm run build && electron-builder --mac --arm64 --publish never", @@ -39,40 +40,31 @@ "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", "test:e2e:report": "playwright show-report", - "version:current": "tsx scripts/version-manager.ts current", - "version:set": "tsx scripts/version-manager.ts set", - "version:major": "tsx scripts/version-manager.ts major", - "version:minor": "tsx scripts/version-manager.ts minor", - "version:patch": "tsx scripts/version-manager.ts patch", - "version:prerelease": "tsx scripts/version-manager.ts prerelease", - "version:beta": "tsx scripts/version-manager.ts minor beta", - "version:beta-patch": "tsx scripts/version-manager.ts patch beta", - "release": "npm run build && electron-builder --publish onTagOrDraft", - "release:all": "npm run build && electron-builder --publish always", - "release:never": "npm run build && electron-builder --publish never", - "release:draft": "npm run build && electron-builder --publish onTagOrDraft", "migrate": "tsx src/main/db/migration-cli.ts", "migrate:up": "npm run migrate up", "migrate:down": "npm run migrate down", "migrate:status": "npm run migrate status", "migrate:create": "npm run migrate create", "migrate:validate": "npm run migrate validate", - "release:rename": "tsx scripts/rename-artifacts.ts", - "release:auto": "tsx scripts/release.ts", - "release:check": "tsx scripts/pre-release-check.ts", "semantic-release": "semantic-release", "semantic-release:dry-run": "semantic-release --dry-run", "prepare": "husky", "check:i18n": "tsx scripts/check-i18n.ts", "sync:i18n": "tsx scripts/sync-i18n.ts", "update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts", - "auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts" + "auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts", + "ffmpeg:download": "tsx scripts/download-ffmpeg.ts current", + "ffmpeg:download-all": "tsx scripts/download-ffmpeg.ts all", + "ffmpeg:clean": "tsx scripts/download-ffmpeg.ts clean", + "ffmpeg:test": "tsx scripts/test-ffmpeg-integration.ts" }, "dependencies": { "@ant-design/icons": "^6.0.1", "@ant-design/v5-patch-for-react-19": "^1.0.3", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", + "@remotion/media-parser": "^4.0.344", + "@sentry/electron": "^5.12.0", "antd": "^5.27.3", "better-sqlite3": "^12.2.0", "dompurify": "^3.2.6", @@ -122,6 +114,7 @@ "@welldone-software/why-did-you-render": "^10.0.1", "cli-progress": "^3.12.0", "code-inspector-plugin": "^1.2.7", + "cross-env": "^10.0.0", "electron": "37.2.4", "electron-builder": "26.0.19", "electron-devtools-installer": "^4.0.0", @@ -158,6 +151,7 @@ "tsx": "^4.20.5", "typescript": "^5.9.2", "vite": "^6.3.5", + "vite-plugin-static-copy": "^3.1.2", "vitest": "^2.1.9", "winston-daily-rotate-file": "^5.0.0", "yaml": "^2.8.1", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 592a94bb..ee6b15a4 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -74,10 +74,27 @@ export enum IpcChannel { Ffmpeg_GetPath = 'ffmpeg:get-path', Ffmpeg_CheckExists = 'ffmpeg:check-exists', Ffmpeg_GetVersion = 'ffmpeg:get-version', - Ffmpeg_Download = 'ffmpeg:download', Ffmpeg_GetVideoInfo = 'ffmpeg:get-video-info', - Ffmpeg_Transcode = 'ffmpeg:transcode', - Ffmpeg_CancelTranscode = 'ffmpeg:cancel-transcode', + Ffmpeg_Warmup = 'ffmpeg:warmup', + Ffmpeg_GetWarmupStatus = 'ffmpeg:get-warmup-status', + Ffmpeg_GetInfo = 'ffmpeg:get-info', + Ffmpeg_AutoDetectAndDownload = 'ffmpeg:auto-detect-and-download', + + // FFmpeg 下载相关 IPC 通道 / FFmpeg download related IPC channels + FfmpegDownload_CheckExists = 'ffmpeg-download:check-exists', + FfmpegDownload_GetVersion = 'ffmpeg-download:get-version', + FfmpegDownload_Download = 'ffmpeg-download:download', + FfmpegDownload_GetProgress = 'ffmpeg-download:get-progress', + FfmpegDownload_Cancel = 'ffmpeg-download:cancel', + FfmpegDownload_Remove = 'ffmpeg-download:remove', + FfmpegDownload_GetAllVersions = 'ffmpeg-download:get-all-versions', + FfmpegDownload_CleanupTemp = 'ffmpeg-download:cleanup-temp', + + // MediaInfo 相关 IPC 通道 / MediaInfo related IPC channels + MediaInfo_CheckExists = 'mediainfo:check-exists', + MediaInfo_GetVersion = 'mediainfo:get-version', + MediaInfo_GetVideoInfo = 'mediainfo:get-video-info', + MediaInfo_GetVideoInfoWithStrategy = 'mediainfo:get-video-info-with-strategy', // 文件系统相关 IPC 通道 / File system related IPC channels Fs_CheckFileExists = 'fs:check-file-exists', diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index a1c1c154..0f36dc97 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -1,15 +1,35 @@ export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'] export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac'] +/** + * 将扩展名数组转换为 Electron dialog 所需的格式(不含点) + * @param extArray 扩展名数组(可包含或不包含点) + * @returns 不含点的扩展名数组 + */ +export function toDialogExtensions(extArray: string[]): string[] { + return extArray.map((ext) => (ext.startsWith('.') ? ext.slice(1) : ext)) +} + +/** + * 获取用于 Electron dialog 的视频文件扩展名数组 + * @returns 不含点的视频扩展名数组 + */ +export function getVideoDialogExtensions(): string[] { + return toDialogExtensions(videoExts) +} + export const KB = 1024 export const MB = 1024 * KB export const GB = 1024 * MB export const defaultLanguage = 'zh-CN' export enum FeedUrl { - PRODUCTION = 'https://releases.echoplayer.cc', + PRODUCTION = 'http://release.echoplayer.z2blog.com/api/releases', GITHUB_LATEST = 'https://github.com/mkdir700/EchoPlayer/releases/latest/download', - PRERELEASE_LOWEST = 'https://github.com/mkdir700/EchoPlayer/releases/download/v0.1.0' + PRERELEASE_LOWEST = 'https://github.com/mkdir700/EchoPlayer/releases/download/v0.1.0', + // 中国用户特有的预发布渠道 feed URL + CN_BETA = 'http://release.echoplayer.z2blog.com/api/beta/releases', + CN_ALPHA = 'http://release.echoplayer.z2blog.com/api/alpha/releases' } export enum UpgradeChannel { diff --git a/packages/shared/schema.ts b/packages/shared/schema.ts index 0d27e532..297f5557 100644 --- a/packages/shared/schema.ts +++ b/packages/shared/schema.ts @@ -78,6 +78,8 @@ export interface PlayerSettingsTable { videoId: number /** 播放速度 */ playbackRate: number + /** 收藏的播放速度 JSON 数组字符串 */ + favoriteRates: string /** 音量 (0-1) */ volume: number /** 是否静音 */ diff --git a/packages/shared/types/mediainfo.ts b/packages/shared/types/mediainfo.ts new file mode 100644 index 00000000..adb4874e --- /dev/null +++ b/packages/shared/types/mediainfo.ts @@ -0,0 +1,139 @@ +// MediaInfo 相关类型定义 + +/** + * MediaInfo 服务状态接口 + */ +export interface MediaInfoStatus { + isInitialized: boolean + version: string | null + lastError?: string +} + +/** + * MediaInfo 原始结果接口 + * 基于 mediainfo.js 返回的标准格式 + */ +export interface MediaInfoRawResult { + media: { + '@ref': string + track: MediaInfoTrack[] + } +} + +/** + * MediaInfo 轨道信息接口 + */ +export interface MediaInfoTrack { + '@type': 'General' | 'Video' | 'Audio' | 'Text' | 'Other' + [key: string]: any + + // 通用字段 + ID?: string + UniqueID?: string + + // General 轨道字段 + CompleteName?: string + FileName?: string + FileExtension?: string + Format?: string + Duration?: string + FileSize?: string + OverallBitRate?: string + + // Video 轨道字段 + Width?: string + Height?: string + DisplayAspectRatio?: string + FrameRate?: string + BitRate?: string + CodecID?: string + + // Audio 轨道字段 + Channels?: string + SamplingRate?: string + BitDepth?: string +} + +/** + * MediaInfo 扩展视频信息接口 + * 包含比 FFmpegVideoInfo 更详细的信息 + */ +export interface MediaInfoVideoDetails { + // 基本信息(兼容 FFmpegVideoInfo) + duration: number + videoCodec: string + audioCodec: string + resolution: string + bitrate: string + + // 扩展信息 + fileSize?: number + frameRate?: number + aspectRatio?: string + audioChannels?: number + audioSampleRate?: number + audioBitDepth?: number + + // 元数据 + title?: string + creationTime?: string + + // 技术细节 + pixelFormat?: string + colorSpace?: string + profile?: string + level?: string +} + +/** + * MediaInfo 分析选项接口 + */ +export interface MediaInfoOptions { + /** 是否包含扩展信息 */ + includeExtendedInfo?: boolean + + /** 超时时间(毫秒) */ + timeout?: number + + /** 是否缓存结果 */ + enableCache?: boolean + + /** 自定义解析器 */ + customParser?: (result: MediaInfoRawResult) => any +} + +/** + * MediaInfo 性能统计接口 + */ +export interface MediaInfoPerformanceStats { + initializationTime: number + pathConversionTime: number + fileCheckTime: number + fileReadTime: number + analysisTime: number + parseTime: number + totalTime: number + fileSize: number +} + +/** + * MediaInfo 错误类型 + */ +export enum MediaInfoErrorType { + INITIALIZATION_FAILED = 'INITIALIZATION_FAILED', + FILE_NOT_FOUND = 'FILE_NOT_FOUND', + FILE_READ_ERROR = 'FILE_READ_ERROR', + ANALYSIS_FAILED = 'ANALYSIS_FAILED', + PARSE_ERROR = 'PARSE_ERROR', + UNSUPPORTED_FORMAT = 'UNSUPPORTED_FORMAT', + WASM_LOAD_ERROR = 'WASM_LOAD_ERROR' +} + +/** + * MediaInfo 错误接口 + */ +export interface MediaInfoError extends Error { + type: MediaInfoErrorType + filePath?: string + details?: any +} diff --git a/packages/shared/types/sentry.d.ts b/packages/shared/types/sentry.d.ts new file mode 100644 index 00000000..b38180fb --- /dev/null +++ b/packages/shared/types/sentry.d.ts @@ -0,0 +1,60 @@ +export interface SentryMainOptions { + dsn: string + release?: string + environment?: string + sampleRate?: number + enableNative?: boolean + beforeSend?: (event: any, hint?: any) => any | null | Promise + beforeBreadcrumb?: (breadcrumb: any, hint?: any) => any | null + initialScope?: { + user?: any + tags?: { [key: string]: string } + contexts?: { [key: string]: any } + } + integrations?: any[] + debug?: boolean + maxBreadcrumbs?: number + attachStacktrace?: boolean + sendDefaultPii?: boolean + serverName?: string + captureUnhandledRejections?: boolean + captureUncaughtException?: boolean +} + +export interface SentryRendererOptions { + dsn: string + release?: string + environment?: string + sampleRate?: number + beforeSend?: (event: any, hint?: any) => any | null | Promise + beforeBreadcrumb?: (breadcrumb: any, hint?: any) => any | null + initialScope?: { + user?: any + tags?: { [key: string]: string } + contexts?: { [key: string]: any } + } + integrations?: any[] + debug?: boolean + maxBreadcrumbs?: number + attachStacktrace?: boolean + sendDefaultPii?: boolean + captureUnhandledRejections?: boolean + captureConsoleIntegration?: boolean + captureGlobalErrorHandlers?: boolean +} + +declare module '@sentry/electron/main' { + export function init(options: SentryMainOptions): void + export function captureException(exception: any, hint?: any): string + export function captureMessage(message: string, level?: string): string + export function addBreadcrumb(breadcrumb: any): void + export function configureScope(callback: (scope: any) => void): void +} + +declare module '@sentry/electron/renderer' { + export function init(options: SentryRendererOptions): void + export function captureException(exception: any, hint?: any): string + export function captureMessage(message: string, level?: string): string + export function addBreadcrumb(breadcrumb: any): void + export function configureScope(callback: (scope: any) => void): void +} diff --git a/packages/shared/utils/PathConverter.ts b/packages/shared/utils/PathConverter.ts new file mode 100644 index 00000000..6318208c --- /dev/null +++ b/packages/shared/utils/PathConverter.ts @@ -0,0 +1,200 @@ +/** + * 优化的路径转换工具 + * 提供高性能的文件路径转换和验证功能 + */ + +export interface PathValidationResult { + isValid: boolean + localPath: string + error?: string + isConverted: boolean // 是否进行了转换 +} + +/** + * 路径转换器 + * 优化的路径转换逻辑,减少重复操作和性能开销 + */ +export class PathConverter { + // 缓存转换结果,避免重复转换 + private static conversionCache = new Map() + + // 缓存大小限制 + private static readonly MAX_CACHE_SIZE = 1000 + + // 路径验证正则表达式(预编译) + private static readonly FILE_URL_REGEX = /^file:\/\// + private static readonly WINDOWS_DRIVE_REGEX = /^\/[A-Za-z]:/ + + /** + * 快速检查是否为 file:// URL + */ + static isFileUrl(path: string): boolean { + return this.FILE_URL_REGEX.test(path) + } + + /** + * 优化的路径转换方法 + * 带缓存和错误处理 + */ + static convertToLocalPath(inputPath: string): PathValidationResult { + // 如果不是 file:// URL,直接返回 + if (!this.isFileUrl(inputPath)) { + return { + isValid: true, + localPath: inputPath, + isConverted: false + } + } + + // 检查缓存 + const cached = this.conversionCache.get(inputPath) + if (cached !== undefined) { + return { + isValid: true, + localPath: cached, + isConverted: true + } + } + + try { + const url = new URL(inputPath) + let localPath = decodeURIComponent(url.pathname) + + // Windows 路径处理:移除开头的斜杠 + if (process.platform === 'win32' && this.WINDOWS_DRIVE_REGEX.test(localPath)) { + localPath = localPath.substring(1) + } + + // 添加到缓存(控制缓存大小) + if (this.conversionCache.size >= this.MAX_CACHE_SIZE) { + // 删除最老的缓存项(简单的 LRU) + const firstKey = this.conversionCache.keys().next().value + if (firstKey) { + this.conversionCache.delete(firstKey) + } + } + this.conversionCache.set(inputPath, localPath) + + return { + isValid: true, + localPath, + isConverted: true + } + } catch (error) { + return { + isValid: false, + localPath: inputPath, + error: error instanceof Error ? error.message : String(error), + isConverted: false + } + } + } + + /** + * 批量路径转换 + * 用于批量操作优化 + */ + static convertPaths(inputPaths: string[]): PathValidationResult[] { + return inputPaths.map((path) => this.convertToLocalPath(path)) + } + + /** + * 清除转换缓存 + */ + static clearCache(): void { + this.conversionCache.clear() + } + + /** + * 获取缓存统计信息 + */ + static getCacheStats(): { + size: number + maxSize: number + hitRatio: number + } { + // 简化的统计(在真实应用中可以添加更详细的统计) + return { + size: this.conversionCache.size, + maxSize: this.MAX_CACHE_SIZE, + hitRatio: 0 // 需要额外的计数器来实现 + } + } + + /** + * 验证路径有效性(不进行转换) + */ + static validatePath(inputPath: string): boolean { + if (!inputPath || typeof inputPath !== 'string') { + return false + } + + // 检查基本路径格式 + if (this.isFileUrl(inputPath)) { + try { + new URL(inputPath) + return true + } catch { + return false + } + } + + // 检查本地路径(简单验证) + return inputPath.length > 0 && !inputPath.includes('\0') + } + + /** + * 规范化路径 + * 统一路径格式,减少后续处理的复杂性 + */ + static normalizePath(inputPath: string): string { + const result = this.convertToLocalPath(inputPath) + if (!result.isValid) { + return inputPath + } + + let normalizedPath = result.localPath + + // 统一路径分隔符(Windows) + if (process.platform === 'win32') { + normalizedPath = normalizedPath.replace(/\//g, '\\') + } + + return normalizedPath + } + + /** + * 生成缓存键 + * 用于其他需要缓存路径相关计算的场景 + */ + static generateCacheKey(inputPath: string, suffix?: string): string { + const normalized = this.normalizePath(inputPath) + return suffix ? `${normalized}:${suffix}` : normalized + } +} + +/** + * 便捷函数 + */ + +/** + * 快速转换单个路径 + */ +export function convertToLocalPath(inputPath: string): string { + const result = PathConverter.convertToLocalPath(inputPath) + return result.localPath +} + +/** + * 快速验证路径 + */ +export function isValidPath(inputPath: string): boolean { + return PathConverter.validatePath(inputPath) +} + +/** + * 快速规范化路径 + */ +export function normalizePath(inputPath: string): string { + return PathConverter.normalizePath(inputPath) +} diff --git a/packages/shared/utils/PerformanceMonitor.ts b/packages/shared/utils/PerformanceMonitor.ts new file mode 100644 index 00000000..fa2bcba1 --- /dev/null +++ b/packages/shared/utils/PerformanceMonitor.ts @@ -0,0 +1,548 @@ +/** + * @fileoverview 性能监控工具类 - 提供精确的性能计时、指标收集和瓶颈分析功能 + * + * @description 这个模块提供了一套完整的性能监控解决方案,包括: + * - PerformanceMonitor 类:核心性能监控器 + * - 便捷函数:createPerformanceMonitor、measureAsync、measureSync + * - 装饰器:monitorPerformance(用于自动监控方法性能) + * - 类型定义:PerformanceMetric、PerformanceReport + * + * @example + * // 基础使用 + * import { PerformanceMonitor, measureAsync } from '@/utils/PerformanceMonitor' + * + * const monitor = new PerformanceMonitor('VideoProcessing') + * monitor.startTiming('encode') + * // ... 执行操作 + * monitor.endTiming('encode') + * const report = monitor.finish() + * + * @example + * // 使用便捷函数 + * const { result, duration } = await measureAsync( + * 'fetchData', + * () => fetch('/api/data').then(r => r.json()) + * ) + * + * @example + * // 使用装饰器 + * class Service { + * @monitorPerformance() + * async processData() { + * // 自动监控这个方法的性能 + * } + * } + * + * @author mkdir700 + * @since 1.0.0 + */ + +import { loggerService } from '@logger' + +const logger = loggerService.withContext('PerformanceMonitor') + +/** + * 性能指标接口 + * + * @interface PerformanceMetric + * @description 描述单个性能测量操作的详细信息 + * + * @example + * const metric: PerformanceMetric = { + * name: 'videoEncode', + * startTime: 1640995200000, + * endTime: 1640995205000, + * duration: 5000, + * metadata: { resolution: '1080p', codec: 'h264' } + * } + */ +export interface PerformanceMetric { + /** 操作名称 */ + name: string + /** 开始时间戳(毫秒) */ + startTime: number + /** 结束时间戳(毫秒),可选 */ + endTime?: number + /** 操作耗时(毫秒),可选 */ + duration?: number + /** 附加元数据,可选 */ + metadata?: Record +} + +/** + * 性能报告接口 + * + * @interface PerformanceReport + * @description 包含完整的性能分析结果 + * + * @example + * const report: PerformanceReport = { + * totalDuration: 15234, + * metrics: [metric1, metric2, metric3], + * bottlenecks: [slowMetric], + * summary: { + * 'videoEncode': 5000, + * 'audioProcess': 2000, + * 'fileWrite': 1500 + * } + * } + */ +export interface PerformanceReport { + /** 总耗时(毫秒) */ + totalDuration: number + /** 所有性能指标 */ + metrics: PerformanceMetric[] + /** 性能瓶颈列表(超过阈值的操作) */ + bottlenecks: PerformanceMetric[] + /** 操作耗时汇总 */ + summary: Record +} + +/** + * 性能监控器类 - 用于监控和分析代码性能 + * + * @class PerformanceMonitor + * @description 提供精确的性能计时、指标收集和瓶颈分析功能 + * + * @example + * // 基本使用 + * const monitor = new PerformanceMonitor('VideoProcessor') + * + * monitor.startTiming('loadVideo') + * await loadVideoFile() + * monitor.endTiming('loadVideo') + * + * const report = monitor.finish() + * console.log(`总耗时: ${report.totalDuration}ms`) + * + * @example + * // 复杂场景监控 + * const monitor = new PerformanceMonitor('DataProcessing') + * + * monitor.startTiming('fetchData', { url: 'api/data' }) + * const data = await fetchData() + * monitor.endTiming('fetchData', { records: data.length }) + * + * monitor.startTiming('processData') + * const processed = processData(data) + * monitor.endTiming('processData', { operations: processed.operations }) + * + * // 获取报告并检查瓶颈 + * const report = monitor.getReport(50) // 50ms阈值 + * if (monitor.hasBottlenecks(50)) { + * console.warn('检测到性能瓶颈:', report.bottlenecks) + * } + * + * @example + * // 记录已知耗时的操作 + * const monitor = new PerformanceMonitor('FileIO') + * + * // 记录外部测量的耗时 + * monitor.recordTiming('fileRead', 125, { size: '2MB' }) + * monitor.recordTiming('fileWrite', 98, { size: '1.5MB' }) + * + * const summary = monitor.getReport().summary + * console.log('IO操作汇总:', summary) + */ +export class PerformanceMonitor { + private metrics: Map = new Map() + private startTime: number + private context: string + + /** + * 创建性能监控器实例 + * + * @param {string} context - 监控上下文名称,用于日志标识 + * + * @example + * const monitor = new PerformanceMonitor('VideoImport') + */ + constructor(context: string) { + this.context = context + this.startTime = performance.now() + logger.info(`🚀 开始性能监控: ${context}`) + } + + /** + * 开始计时一个操作 + * + * @param {string | Function} nameOrFunction - 操作名称或函数引用 + * @param {Record} [metadata] - 可选的元数据 + * + * @example + * // 使用字符串名称 + * monitor.startTiming('videoEncode', { resolution: '1080p', codec: 'h264' }) + * + * @example + * // 使用函数引用,自动提取函数名 + * function processVideo() + * monitor.startTiming(processVideo, { type: 'h264' }) + * + * @example + * // 使用方法引用,自动提取方法名 + * class VideoProcessor { + * encodeVideo() + * } + * const processor = new VideoProcessor() + * monitor.startTiming(processor.encodeVideo.bind(processor), { quality: 'high' }) + */ + startTiming( + nameOrFunction: string | ((...args: any[]) => any), + metadata?: Record + ): void { + const name = + typeof nameOrFunction === 'function' ? nameOrFunction.name || 'anonymous' : nameOrFunction + + const metric: PerformanceMetric = { + name, + startTime: performance.now(), + metadata + } + this.metrics.set(name, metric) + logger.info(`⏱️ 开始计时: ${name}`, metadata) + } + + /** + * 结束计时一个操作 + * + * @param {string | Function} nameOrFunction - 操作名称或函数引用(需与startTiming对应) + * @param {Record} [metadata] - 可选的元数据 + * @returns {number} 操作耗时(毫秒) + * + * @example + * // 使用字符串名称 + * const duration = monitor.endTiming('videoEncode', { outputSize: '2.5MB' }) + * console.log(`编码耗时: ${duration}ms`) + * + * @example + * // 使用函数引用,自动提取函数名 + * function processVideo() + * monitor.startTiming(processVideo) + * // ... 执行操作 + * const duration = monitor.endTiming(processVideo, { result: 'success' }) + * + * @example + * // 完整的函数监控示例 + * const videoProcessor = { + * encodeVideo() { + * monitor.startTiming(this.encodeVideo, { input: 'raw.mp4' }) + * // ... 编码逻辑 + * monitor.endTiming(this.encodeVideo, { output: 'encoded.mp4' }) + * } + * } + */ + endTiming( + nameOrFunction: string | ((...args: any[]) => any), + metadata?: Record + ): number { + const name = + typeof nameOrFunction === 'function' ? nameOrFunction.name || 'anonymous' : nameOrFunction + + const metric = this.metrics.get(name) + if (!metric) { + logger.warn(`⚠️ 未找到计时器: ${name}`) + return 0 + } + + const endTime = performance.now() + const duration = endTime - metric.startTime + + metric.endTime = endTime + metric.duration = duration + if (metadata) { + metric.metadata = { ...metric.metadata, ...metadata } + } + + logger.info(`✅ 完成计时: ${name}, 耗时: ${duration.toFixed(2)}ms`, { + ...metric.metadata, + duration: `${duration.toFixed(2)}ms` + }) + + return duration + } + + /** + * 记录一个瞬时操作的耗时 + * + * @param {string} name - 操作名称 + * @param {number} duration - 耗时(毫秒) + * @param {Record} [metadata] - 可选的元数据 + * + * @example + * // 记录外部测量的耗时 + * const externalDuration = await measureExternalOperation() + * monitor.recordTiming('externalAPI', externalDuration, { api: 'transcription' }) + */ + recordTiming(name: string, duration: number, metadata?: Record): void { + const now = performance.now() + const metric: PerformanceMetric = { + name, + startTime: now - duration, + endTime: now, + duration, + metadata + } + this.metrics.set(name, metric) + logger.info(`📊 记录耗时: ${name}, 耗时: ${duration.toFixed(2)}ms`, { + ...metadata, + duration: `${duration.toFixed(2)}ms` + }) + } + + /** + * 获取性能报告 + * + * @param {number} [bottleneckThreshold=100] - 性能瓶颈阈值(毫秒) + * @returns {PerformanceReport} 性能报告对象 + * + * @example + * const report = monitor.getReport(50) + * console.log(`总耗时: ${report.totalDuration}ms`) + * console.log(`瓶颈数量: ${report.bottlenecks.length}`) + * console.log('操作汇总:', report.summary) + */ + getReport(bottleneckThreshold: number = 100): PerformanceReport { + const totalDuration = performance.now() - this.startTime + const metrics = Array.from(this.metrics.values()) + const bottlenecks = metrics.filter((m) => (m.duration || 0) > bottleneckThreshold) + + const summary: Record = {} + metrics.forEach((metric) => { + if (metric.duration !== undefined) { + summary[metric.name] = metric.duration + } + }) + + const report: PerformanceReport = { + totalDuration, + metrics, + bottlenecks, + summary + } + + logger.info(`📈 性能报告 - ${this.context}`, { + totalE: `${totalDuration.toFixed(2)}ms`, + metric: metrics.length, + bottlenecks: bottlenecks.length, + details: Object.fromEntries( + Object.entries(summary).map(([key, value]) => [key, `${value.toFixed(2)}ms`]) + ) + }) + + // 如果有性能瓶颈,记录警告 + if (bottlenecks.length > 0) { + logger.warn( + `⚠️ 检测到 ${bottlenecks.length} 个性能瓶颈 (>${bottleneckThreshold}ms):`, + bottlenecks.map((b) => `${b.name}: ${b.duration?.toFixed(2)}ms`) + ) + } + + return report + } + + /** + * 完成监控并生成报告 + * + * @param {number} [bottleneckThreshold] - 性能瓶颈阈值(毫秒) + * @returns {PerformanceReport} 最终性能报告 + * + * @example + * // 完成监控并获取报告 + * const finalReport = monitor.finish() + * if (finalReport.bottlenecks.length > 0) { + * console.warn('发现性能瓶颈:', finalReport.bottlenecks) + * } + */ + finish(bottleneckThreshold?: number): PerformanceReport { + const report = this.getReport(bottleneckThreshold) + logger.info(`🏁 性能监控完成: ${this.context}, 总耗时: ${report.totalDuration.toFixed(2)}ms`) + return report + } + + /** + * 获取单个操作的耗时 + * + * @param {string} name - 操作名称 + * @returns {number | undefined} 操作耗时(毫秒),如果操作不存在则返回undefined + * + * @example + * const encodeDuration = monitor.getDuration('videoEncode') + * if (encodeDuration !== undefined) { + * console.log(`视频编码耗时: ${encodeDuration}ms`) + * } + */ + getDuration(name: string): number | undefined { + return this.metrics.get(name)?.duration + } + + /** + * 检查是否存在性能瓶颈 + * + * @param {number} [threshold=100] - 性能瓶颈阈值(毫秒) + * @returns {boolean} 是否存在性能瓶颈 + * + * @example + * if (monitor.hasBottlenecks(50)) { + * const report = monitor.getReport(50) + * console.warn('检测到性能瓶颈:', report.bottlenecks.map(b => b.name)) + * } + */ + hasBottlenecks(threshold: number = 100): boolean { + return Array.from(this.metrics.values()).some((m) => (m.duration || 0) > threshold) + } + + /** + * 清除所有计时数据 + * + * @example + * monitor.clear() // 清除之前的计时数据,重新开始监控 + */ + clear(): void { + this.metrics.clear() + this.startTime = performance.now() + logger.info(`🧹 清除性能监控数据: ${this.context}`) + } +} + +/** + * 创建性能监控器的便捷函数 + * + * @param {string} context - 监控上下文名称 + * @returns {PerformanceMonitor} 性能监控器实例 + * + * @example + * const monitor = createPerformanceMonitor('MediaProcessing') + * monitor.startTiming('processVideo') + * // ... 处理视频 + * monitor.endTiming('processVideo') + */ +export function createPerformanceMonitor(context: string): PerformanceMonitor { + return new PerformanceMonitor(context) +} + +/** + * 装饰器:自动监控异步函数的性能 + * + * @param {string} [name] - 自定义监控名称 + * @returns {CallableFunction} 装饰器函数 + * + * @example + * class VideoProcessor { + * @monitorPerformance('customEncode') + * async encodeVideo(inputPath: string) { + * // 视频编码逻辑 + * return encodedVideo + * } + * + * @monitorPerformance() // 使用默认名称: VideoProcessor.processAudio + * async processAudio() { + * // 音频处理逻辑 + * } + * } + */ +export function monitorPerformance(name?: string): CallableFunction { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value + const methodName = name || `${target.constructor.name}.${propertyKey}` + + descriptor.value = async function (...args: any[]) { + const monitor = createPerformanceMonitor(methodName) + monitor.startTiming('execution') + + try { + const result = await originalMethod.apply(this, args) + monitor.endTiming('execution') + monitor.finish() + return result + } catch (error) { + monitor.endTiming('execution', { error: true }) + monitor.finish() + throw error + } + } + + return descriptor + } +} + +/** + * 简单的异步性能计时工具函数 + * + * @template T + * @param {string} name - 操作名称 + * @param {() => Promise} operation - 要执行的异步操作 + * @param {string} [context] - 可选的日志上下文 + * @returns {Promise<{result: T, duration: number}>} 包含结果和耗时的对象 + * + * @example + * const { result, duration } = await measureAsync( + * 'fetchUserData', + * () => fetch('/api/users').then(r => r.json()), + * 'UserService' + * ) + * console.log(`获取用户数据耗时: ${duration}ms, 用户数量: ${result.length}`) + */ +export async function measureAsync( + name: string, + operation: () => Promise, + context?: string +): Promise<{ result: T; duration: number }> { + const startTime = performance.now() + const contextLogger = context ? loggerService.withContext(context) : logger + + contextLogger.info(`⏱️ 开始测量: ${name}`) + + try { + const result = await operation() + const duration = performance.now() - startTime + + contextLogger.info(`✅ 测量完成: ${name}, 耗时: ${duration.toFixed(2)}ms`) + + return { result, duration } + } catch (error) { + const duration = performance.now() - startTime + contextLogger.error(`❌ 测量失败: ${name}, 耗时: ${duration.toFixed(2)}ms`, error as Error) + throw error + } +} + +/** + * 同步操作的性能计时工具函数 + * + * @template T + * @param {string} name - 操作名称 + * @param {() => T} operation - 要执行的同步操作 + * @param {string} [context] - 可选的日志上下文 + * @returns {{result: T, duration: number}} 包含结果和耗时的对象 + * + * @example + * const { result, duration } = measureSync( + * 'processArray', + * () => largeArray.map(item => processItem(item)), + * 'DataProcessor' + * ) + * console.log(`数组处理耗时: ${duration}ms, 处理了 ${result.length} 个项目`) + */ +export function measureSync( + name: string, + operation: () => T, + context?: string +): { result: T; duration: number } { + const startTime = performance.now() + const contextLogger = context ? loggerService.withContext(context) : logger + + contextLogger.info(`⏱️ 开始测量: ${name}`) + + try { + const result = operation() + const duration = performance.now() - startTime + + contextLogger.info(`✅ 测量完成: ${name}, 耗时: ${duration.toFixed(2)}ms`) + + return { result, duration } + } catch (error) { + const duration = performance.now() - startTime + contextLogger.error(`❌ 测量失败: ${name}, 耗时: ${duration.toFixed(2)}ms`, error as Error) + throw error + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1571a04a..fab583f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: '@electron-toolkit/utils': specifier: ^4.0.0 version: 4.0.0(electron@37.2.4) + '@remotion/media-parser': + specifier: ^4.0.344 + version: 4.0.344 + '@sentry/electron': + specifier: ^5.12.0 + version: 5.12.0 antd: specifier: ^5.27.3 version: 5.27.3(moment@2.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -168,6 +174,9 @@ importers: code-inspector-plugin: specifier: ^1.2.7 version: 1.2.7 + cross-env: + specifier: ^10.0.0 + version: 10.0.0 electron: specifier: 37.2.4 version: 37.2.4 @@ -276,6 +285,9 @@ importers: vite: specifier: ^6.3.5 version: 6.3.5(@types/node@22.18.1)(jiti@2.5.1)(sass@1.92.1)(tsx@4.20.5)(yaml@2.8.1) + vite-plugin-static-copy: + specifier: ^3.1.2 + version: 3.1.2(vite@6.3.5(@types/node@22.18.1)(jiti@2.5.1)(sass@1.92.1)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^2.1.9 version: 2.1.9(@types/node@22.18.1)(@vitest/ui@2.1.9)(jsdom@25.0.1)(msw@2.11.1(@types/node@22.18.1)(typescript@5.9.2))(sass@1.92.1) @@ -679,6 +691,9 @@ packages: '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.25.8': resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} @@ -1236,6 +1251,230 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.53.0': + resolution: {integrity: sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==} + engines: {node: '>=14'} + + '@opentelemetry/api-logs@0.57.1': + resolution: {integrity: sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==} + engines: {node: '>=14'} + + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation-amqplib@0.46.1': + resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.43.0': + resolution: {integrity: sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.16.0': + resolution: {integrity: sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.47.0': + resolution: {integrity: sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.44.1': + resolution: {integrity: sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.19.0': + resolution: {integrity: sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.43.0': + resolution: {integrity: sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.47.0': + resolution: {integrity: sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.45.1': + resolution: {integrity: sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.57.1': + resolution: {integrity: sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.47.0': + resolution: {integrity: sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.7.0': + resolution: {integrity: sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.44.0': + resolution: {integrity: sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.47.0': + resolution: {integrity: sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.44.0': + resolution: {integrity: sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.51.0': + resolution: {integrity: sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.46.0': + resolution: {integrity: sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.45.0': + resolution: {integrity: sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.45.0': + resolution: {integrity: sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.44.0': + resolution: {integrity: sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.50.0': + resolution: {integrity: sha512-TtLxDdYZmBhFswm8UIsrDjh/HFBeDXd4BLmE8h2MxirNHewLJ0VS9UUddKKEverb5Sm2qFVjqRjcU+8Iw4FJ3w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.46.0': + resolution: {integrity: sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.18.0': + resolution: {integrity: sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.10.0': + resolution: {integrity: sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation@0.53.0': + resolution: {integrity: sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.57.1': + resolution: {integrity: sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.27.0': + resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.37.0': + resolution: {integrity: sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -1346,6 +1585,9 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@prisma/instrumentation@5.22.0': + resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==} + '@rc-component/async-validator@5.0.4': resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==} engines: {node: '>=14.x'} @@ -1412,6 +1654,9 @@ packages: peerDependencies: redux: ^3.1.0 || ^4.0.0 || ^5.0.0 + '@remotion/media-parser@4.0.344': + resolution: {integrity: sha512-HI7Jz6OkhN53FTz5K1uiiXMPM0mDJXy/RmbUcYZBDukdRBgCWv4O6g4enjywgYY80FH17IHIXz2gVmdRXAGBfg==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1567,6 +1812,48 @@ packages: peerDependencies: semantic-release: '>=20.1.0' + '@sentry-internal/browser-utils@8.55.0': + resolution: {integrity: sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==} + engines: {node: '>=14.18'} + + '@sentry-internal/feedback@8.55.0': + resolution: {integrity: sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==} + engines: {node: '>=14.18'} + + '@sentry-internal/replay-canvas@8.55.0': + resolution: {integrity: sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==} + engines: {node: '>=14.18'} + + '@sentry-internal/replay@8.55.0': + resolution: {integrity: sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==} + engines: {node: '>=14.18'} + + '@sentry/browser@8.55.0': + resolution: {integrity: sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==} + engines: {node: '>=14.18'} + + '@sentry/core@8.55.0': + resolution: {integrity: sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==} + engines: {node: '>=14.18'} + + '@sentry/electron@5.12.0': + resolution: {integrity: sha512-bJdj/AD+Jp2kX/J5cx8qMuieHcl6akhxRVjcGV9pdttLzJHz/Ve7PavKA9W0T3F647+4KVHguFoUnnOzM6hPQg==} + + '@sentry/node@8.55.0': + resolution: {integrity: sha512-h10LJLDTRAzYgay60Oy7moMookqqSZSviCWkkmHZyaDn+4WURnPp5SKhhfrzPRQcXKrweiOwDSHBgn1tweDssg==} + engines: {node: '>=14.18'} + + '@sentry/opentelemetry@8.55.0': + resolution: {integrity: sha512-UvatdmSr3Xf+4PLBzJNLZ2JjG1yAPWGe/VrJlJAqyTJ2gKeTzgXJJw8rp4pbvNZO8NaTGEYhhO+scLUj0UtLAQ==} + engines: {node: '>=14.18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 + '@opentelemetry/core': ^1.30.1 + '@opentelemetry/instrumentation': ^0.57.1 + '@opentelemetry/sdk-trace-base': ^1.30.1 + '@opentelemetry/semantic-conventions': ^1.28.0 + '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} @@ -1718,6 +2005,9 @@ packages: '@types/cli-progress@3.11.6': resolution: {integrity: sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==} + '@types/connect@3.4.36': + resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} + '@types/conventional-commits-parser@5.0.1': resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} @@ -1758,6 +2048,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.26': + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + '@types/node@16.9.1': resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} @@ -1767,6 +2060,12 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/plist@3.0.5': resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} @@ -1787,12 +2086,18 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} '@types/stylis@4.2.5': resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} @@ -1956,6 +2261,11 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2049,6 +2359,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + app-builder-bin@5.0.0-alpha.12: resolution: {integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==} @@ -2177,6 +2491,10 @@ packages: resolution: {integrity: sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==} engines: {node: 20.x || 22.x || 23.x || 24.x} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -2327,6 +2645,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -2345,6 +2667,9 @@ packages: resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} engines: {node: '>=8'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -2563,6 +2888,11 @@ packages: cross-dirname@0.1.0: resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + cross-env@10.0.0: + resolution: {integrity: sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==} + engines: {node: '>=20'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3286,6 +3616,9 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + framer-motion@12.23.12: resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} peerDependencies: @@ -3700,6 +4033,9 @@ packages: resolution: {integrity: sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g==} engines: {node: '>=18.20'} + import-in-the-middle@1.14.2: + resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -3779,6 +4115,10 @@ packages: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -4552,6 +4892,9 @@ packages: engines: {node: '>=10'} hasBin: true + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} @@ -4645,6 +4988,10 @@ packages: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + normalize-url@6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} @@ -5032,6 +5379,17 @@ packages: performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + phantomjs-prebuilt@2.1.16: resolution: {integrity: sha512-PIiRzBhW85xco2fuj41FmsyuYHKjKuXWmhjy3A/Y+CMpN/63TV+s9uzfVhsUwFe0G77xWtHBG8xmXf5BqEUEuQ==} deprecated: this package is now deprecated @@ -5133,6 +5491,22 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + postject@1.0.0-alpha.6: resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} engines: {node: '>=14.0.0'} @@ -5574,6 +5948,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -5622,6 +6000,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + require-main-filename@1.0.1: resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} @@ -5821,6 +6203,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -6505,6 +6890,12 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-plugin-static-copy@3.1.2: + resolution: {integrity: sha512-aVmYOzptLVOI2b1jL+cmkF7O6uhRv1u5fvOkQgbohWZp2CbR22kn9ZqkCUIt9umKF7UhdbsEpshn1rf4720QFg==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vite@5.4.19: resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -7394,6 +7785,8 @@ snapshots: '@emotion/unitless@0.8.1': {} + '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.25.8': optional: true @@ -8038,6 +8431,299 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.53.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.57.1': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.43.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/connect': 3.4.36 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.16.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.19.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.43.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.57.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.7.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.44.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.44.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.51.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.46.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.45.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.45.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.44.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis-4@0.46.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.37.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.18.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.10.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.53.0 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.57.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.1 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/redis-common@0.36.2': {} + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/semantic-conventions@1.27.0': {} + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.37.0': {} + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -8122,6 +8808,14 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@prisma/instrumentation@5.22.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + '@rc-component/async-validator@5.0.4': dependencies: '@babel/runtime': 7.28.4 @@ -8204,6 +8898,8 @@ snapshots: immutable: 4.3.7 redux: 4.2.1 + '@remotion/media-parser@4.0.344': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.50.0': @@ -8366,6 +9062,93 @@ snapshots: transitivePeerDependencies: - supports-color + '@sentry-internal/browser-utils@8.55.0': + dependencies: + '@sentry/core': 8.55.0 + + '@sentry-internal/feedback@8.55.0': + dependencies: + '@sentry/core': 8.55.0 + + '@sentry-internal/replay-canvas@8.55.0': + dependencies: + '@sentry-internal/replay': 8.55.0 + '@sentry/core': 8.55.0 + + '@sentry-internal/replay@8.55.0': + dependencies: + '@sentry-internal/browser-utils': 8.55.0 + '@sentry/core': 8.55.0 + + '@sentry/browser@8.55.0': + dependencies: + '@sentry-internal/browser-utils': 8.55.0 + '@sentry-internal/feedback': 8.55.0 + '@sentry-internal/replay': 8.55.0 + '@sentry-internal/replay-canvas': 8.55.0 + '@sentry/core': 8.55.0 + + '@sentry/core@8.55.0': {} + + '@sentry/electron@5.12.0': + dependencies: + '@sentry/browser': 8.55.0 + '@sentry/core': 8.55.0 + '@sentry/node': 8.55.0 + deepmerge: 4.3.1 + transitivePeerDependencies: + - supports-color + + '@sentry/node@8.55.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.16.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.19.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.7.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.44.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.51.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.45.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.45.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.44.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.18.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.10.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@prisma/instrumentation': 5.22.0 + '@sentry/core': 8.55.0 + '@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0) + import-in-the-middle: 1.14.2 + transitivePeerDependencies: + - supports-color + + '@sentry/opentelemetry@8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.37.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.37.0 + '@sentry/core': 8.55.0 + '@sindresorhus/is@4.6.0': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -8506,6 +9289,10 @@ snapshots: dependencies: '@types/node': 22.18.1 + '@types/connect@3.4.36': + dependencies: + '@types/node': 22.18.1 + '@types/conventional-commits-parser@5.0.1': dependencies: '@types/node': 22.18.1 @@ -8548,6 +9335,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/mysql@2.15.26': + dependencies: + '@types/node': 22.18.1 + '@types/node@16.9.1': {} '@types/node@22.18.1': @@ -8556,6 +9347,16 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.6.1 + + '@types/pg@8.6.1': + dependencies: + '@types/node': 22.18.1 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + '@types/plist@3.0.5': dependencies: '@types/node': 22.18.1 @@ -8581,10 +9382,16 @@ snapshots: dependencies: '@types/node': 22.18.1 + '@types/shimmer@1.2.0': {} + '@types/statuses@2.0.6': {} '@types/stylis@4.2.5': {} + '@types/tedious@4.0.14': + dependencies: + '@types/node': 22.18.1 + '@types/triple-beam@1.3.5': {} '@types/trusted-types@2.0.7': @@ -8820,6 +9627,10 @@ snapshots: abbrev@1.1.1: {} + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -8956,6 +9767,11 @@ snapshots: any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + app-builder-bin@5.0.0-alpha.12: {} app-builder-lib@26.0.19(dmg-builder@26.0.19(electron-builder-squirrel-windows@26.0.19))(electron-builder-squirrel-windows@26.0.19(dmg-builder@26.0.19)): @@ -9129,6 +9945,8 @@ snapshots: bindings: 1.5.0 prebuild-install: 7.1.3 + binary-extensions@2.3.0: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -9323,6 +10141,18 @@ snapshots: check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -9335,6 +10165,8 @@ snapshots: ci-info@4.3.0: {} + cjs-module-lexer@1.4.3: {} + classnames@2.5.1: {} clean-stack@2.2.0: {} @@ -9562,6 +10394,11 @@ snapshots: cross-dirname@0.1.0: optional: true + cross-env@10.0.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -10529,6 +11366,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded-parse@2.1.2: {} + framer-motion@12.23.12(@emotion/is-prop-valid@1.2.2)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: motion-dom: 12.23.12 @@ -11005,6 +11844,13 @@ snapshots: transitivePeerDependencies: - supports-color + import-in-the-middle@1.14.2: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} @@ -11074,6 +11920,10 @@ snapshots: dependencies: has-bigints: 1.1.0 + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -11979,6 +12829,8 @@ snapshots: mkdirp@1.0.4: {} + module-details-from-path@1.0.4: {} + moment@2.30.1: {} motion-dom@12.23.12: @@ -12080,6 +12932,8 @@ snapshots: semver: 7.7.2 validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} + normalize-url@6.1.0: {} normalize-url@8.0.2: {} @@ -12370,6 +13224,18 @@ snapshots: performance-now@2.1.0: {} + pg-int8@1.0.1: {} + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + phantomjs-prebuilt@2.1.16: dependencies: es6-promise: 4.2.8 @@ -12464,6 +13330,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + postject@1.0.0-alpha.6: dependencies: commander: 9.5.0 @@ -13011,6 +13887,10 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + readdirp@4.1.2: {} redent@3.0.0: @@ -13096,6 +13976,14 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.1 + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + require-main-filename@1.0.1: {} resedit@1.7.2: @@ -13343,6 +14231,8 @@ snapshots: shebang-regex@3.0.0: {} + shimmer@1.2.1: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -14103,6 +14993,15 @@ snapshots: - supports-color - terser + vite-plugin-static-copy@3.1.2(vite@6.3.5(@types/node@22.18.1)(jiti@2.5.1)(sass@1.92.1)(tsx@4.20.5)(yaml@2.8.1)): + dependencies: + chokidar: 3.6.0 + fs-extra: 11.3.1 + p-map: 7.0.3 + picocolors: 1.1.1 + tinyglobby: 0.2.15 + vite: 6.3.5(@types/node@22.18.1)(jiti@2.5.1)(sass@1.92.1)(tsx@4.20.5)(yaml@2.8.1) + vite@5.4.19(@types/node@22.18.1)(sass@1.92.1): dependencies: esbuild: 0.25.8 diff --git a/scripts/download-ffmpeg.ts b/scripts/download-ffmpeg.ts new file mode 100644 index 00000000..442d5034 --- /dev/null +++ b/scripts/download-ffmpeg.ts @@ -0,0 +1,453 @@ +#!/usr/bin/env tsx + +import { spawn } from 'child_process' +import * as crypto from 'crypto' +import * as fs from 'fs' +import * as https from 'https' +import * as path from 'path' + +interface PlatformConfig { + url: string + executable: string + extractPath?: string // 解压后的相对路径 + skipExtraction?: boolean // 跳过解压(对于单文件下载) +} + +interface FFmpegDownloadConfig { + [platform: string]: { + [arch: string]: PlatformConfig + } +} + +// FFmpeg 下载配置 - 使用 GPL 版本获得完整功能 +const FFMPEG_CONFIG: FFmpegDownloadConfig = { + win32: { + x64: { + url: 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip', + executable: 'ffmpeg.exe', + extractPath: 'ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe' + }, + arm64: { + url: 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-winarm64-gpl.zip', + executable: 'ffmpeg.exe', + extractPath: 'ffmpeg-master-latest-winarm64-gpl/bin/ffmpeg.exe' + } + }, + darwin: { + x64: { + url: 'https://evermeet.cx/ffmpeg/ffmpeg-6.1.zip', + executable: 'ffmpeg', + extractPath: 'ffmpeg' + }, + arm64: { + url: 'https://evermeet.cx/ffmpeg/ffmpeg-6.1.zip', // 通用二进制文件 + executable: 'ffmpeg', + extractPath: 'ffmpeg' + } + }, + linux: { + x64: { + url: 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz', + executable: 'ffmpeg', + extractPath: 'ffmpeg-*-amd64-static/ffmpeg' + }, + arm64: { + url: 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz', + executable: 'ffmpeg', + extractPath: 'ffmpeg-*-arm64-static/ffmpeg' + } + } +} + +class FFmpegDownloader { + private readonly outputDir: string + private readonly cacheDir: string + + constructor(outputDir: string = 'resources/ffmpeg') { + this.outputDir = path.resolve(outputDir) + this.cacheDir = path.resolve('.ffmpeg-cache') + } + + // 确保目录存在 + private ensureDir(dir: string): void { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + } + + // 计算文件哈希用于缓存 + private getFileHash(filePath: string): string { + if (!fs.existsSync(filePath)) return '' + const fileBuffer = fs.readFileSync(filePath) + return crypto.createHash('md5').update(fileBuffer).digest('hex') + } + + // 获取缓存文件路径 + private getCachePath(platform: string, arch: string): string { + const config = FFMPEG_CONFIG[platform]?.[arch] + if (!config) throw new Error(`不支持的平台: ${platform}-${arch}`) + + const filename = path.basename(config.url) + return path.join(this.cacheDir, `${platform}-${arch}-${filename}`) + } + + // 下载文件 + private async downloadFile( + url: string, + outputPath: string, + onProgress?: (progress: number) => void + ): Promise { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(outputPath) + let downloadedSize = 0 + let totalSize = 0 + + const download = (currentUrl: string, redirectCount = 0): void => { + if (redirectCount > 5) { + reject(new Error('重定向次数过多')) + return + } + + const request = https.get( + currentUrl, + { + headers: { + 'User-Agent': 'EchoPlayer-FFmpeg-Downloader/1.0' + }, + timeout: 30000 + }, + (response) => { + // 处理重定向 + if (response.statusCode === 301 || response.statusCode === 302) { + const redirectUrl = response.headers.location + if (redirectUrl) { + console.log(`重定向到: ${redirectUrl}`) + download(redirectUrl, redirectCount + 1) + return + } + } + + if (response.statusCode !== 200) { + reject(new Error(`下载失败: HTTP ${response.statusCode}`)) + return + } + + totalSize = parseInt(response.headers['content-length'] || '0', 10) + + response.on('data', (chunk) => { + downloadedSize += chunk.length + if (onProgress && totalSize > 0) { + onProgress((downloadedSize / totalSize) * 100) + } + }) + + response.pipe(file) + + file.on('finish', () => { + file.close() + resolve() + }) + + file.on('error', (err) => { + fs.unlink(outputPath, () => {}) // 清理失败的文件 + reject(err) + }) + + response.on('error', reject) + } + ) + + request.on('error', reject) + request.on('timeout', () => { + request.destroy() + reject(new Error('下载超时')) + }) + } + + download(url) + }) + } + + // 解压 ZIP 文件 + private async extractZip(zipPath: string, extractDir: string): Promise { + return new Promise((resolve, reject) => { + let command: string + let args: string[] + + if (process.platform === 'win32') { + command = 'powershell' + args = [ + '-Command', + `Expand-Archive -Path "${zipPath}" -DestinationPath "${extractDir}" -Force` + ] + } else { + command = 'unzip' + args = ['-o', zipPath, '-d', extractDir] + } + + const child = spawn(command, args, { stdio: 'pipe' }) + + child.on('close', (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`解压失败,退出代码: ${code}`)) + } + }) + + child.on('error', reject) + }) + } + + // 解压 TAR.XZ 文件 + private async extractTarXz(tarPath: string, extractDir: string): Promise { + return new Promise((resolve, reject) => { + const child = spawn('tar', ['-xJf', tarPath, '-C', extractDir], { stdio: 'pipe' }) + + child.on('close', (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`解压失败,退出代码: ${code}`)) + } + }) + + child.on('error', reject) + }) + } + + // 递归查找文件 + private async findFile(dir: string, pattern: string): Promise { + try { + const items = await fs.promises.readdir(dir, { withFileTypes: true }) + + for (const item of items) { + const fullPath = path.join(dir, item.name) + + if (item.isDirectory()) { + const found = await this.findFile(fullPath, pattern) + if (found) return found + } else if (item.isFile()) { + if (pattern.includes('*')) { + // 简单的通配符匹配 + const regex = new RegExp(pattern.replace(/\*/g, '.*')) + if (regex.test(item.name)) { + return fullPath + } + } else if (item.name === pattern) { + return fullPath + } + } + } + + return null + } catch (error) { + console.error(`搜索文件失败: ${error}`) + return null + } + } + + // 下载并安装 FFmpeg + public async downloadFFmpeg(platform?: string, arch?: string): Promise { + const targetPlatform = platform || process.platform + const targetArch = arch || process.arch + + console.log(`开始下载 FFmpeg ${targetPlatform}-${targetArch}...`) + + const config = FFMPEG_CONFIG[targetPlatform]?.[targetArch] + if (!config) { + throw new Error(`不支持的平台: ${targetPlatform}-${targetArch}`) + } + + this.ensureDir(this.cacheDir) + this.ensureDir(this.outputDir) + + const cachePath = this.getCachePath(targetPlatform, targetArch) + const outputPlatformDir = path.join(this.outputDir, `${targetPlatform}-${targetArch}`) + const finalBinaryPath = path.join(outputPlatformDir, config.executable) + + // 检查是否已存在 + if (fs.existsSync(finalBinaryPath)) { + console.log(`FFmpeg 已存在: ${finalBinaryPath}`) + return + } + + this.ensureDir(outputPlatformDir) + + // 检查缓存 + if (!fs.existsSync(cachePath)) { + console.log(`下载 ${config.url}...`) + let lastLoggedProgress = -1 + await this.downloadFile(config.url, cachePath, (progress) => { + const currentProgress = Math.floor(progress / 10) * 10 // 取整到10的倍数 + if (currentProgress !== lastLoggedProgress && currentProgress >= lastLoggedProgress + 10) { + process.stdout.write(`\r下载进度: ${currentProgress}%`) + lastLoggedProgress = currentProgress + } + }) + console.log('\n下载完成') + } else { + console.log('使用缓存文件') + } + + // 解压并安装 + console.log('解压中...') + const tempExtractDir = path.join(this.cacheDir, `extract-${targetPlatform}-${targetArch}`) + this.ensureDir(tempExtractDir) + + try { + if (cachePath.endsWith('.zip')) { + await this.extractZip(cachePath, tempExtractDir) + } else if (cachePath.endsWith('.tar.xz')) { + await this.extractTarXz(cachePath, tempExtractDir) + } + + // 查找可执行文件 + let executablePath: string | null = null + + if (config.extractPath) { + if (config.extractPath.includes('*')) { + // 通配符搜索 + executablePath = await this.findFile(tempExtractDir, path.basename(config.extractPath)) + } else { + const fullPath = path.join(tempExtractDir, config.extractPath) + if (fs.existsSync(fullPath)) { + executablePath = fullPath + } + } + } + + if (!executablePath) { + throw new Error(`找不到可执行文件: ${config.extractPath || config.executable}`) + } + + // 复制到目标位置 + fs.copyFileSync(executablePath, finalBinaryPath) + + // 设置执行权限(Unix 系统) + if (targetPlatform !== 'win32') { + fs.chmodSync(finalBinaryPath, 0o755) + } + + console.log(`FFmpeg 安装完成: ${finalBinaryPath}`) + + // 清理临时目录 + fs.rmSync(tempExtractDir, { recursive: true, force: true }) + } catch (error) { + // 清理临时目录 + if (fs.existsSync(tempExtractDir)) { + fs.rmSync(tempExtractDir, { recursive: true, force: true }) + } + throw error + } + } + + // 下载所有支持的平台 + public async downloadAllPlatforms(): Promise { + console.log('开始下载所有平台的 FFmpeg...') + + for (const [platform, archConfigs] of Object.entries(FFMPEG_CONFIG)) { + for (const arch of Object.keys(archConfigs)) { + try { + await this.downloadFFmpeg(platform, arch) + } catch (error) { + console.error(`下载 ${platform}-${arch} 失败:`, error) + } + } + } + + console.log('所有平台下载完成') + } + + // 仅下载当前平台 + public async downloadCurrentPlatform(): Promise { + await this.downloadFFmpeg() + } + + // 清理缓存 + public cleanCache(): void { + if (fs.existsSync(this.cacheDir)) { + fs.rmSync(this.cacheDir, { recursive: true, force: true }) + console.log('缓存已清理') + } + } +} + +// CLI 入口 +async function main() { + const args = process.argv.slice(2) + let command = args[0] + + const downloader = new FFmpegDownloader() + + try { + // 优先检查环境变量,如果设置了构建目标则使用目标平台 + if (process.env.BUILD_TARGET_PLATFORM) { + console.log( + `检测到构建目标平台: ${process.env.BUILD_TARGET_PLATFORM}-${process.env.BUILD_TARGET_ARCH || process.arch}` + ) + await downloader.downloadFFmpeg( + process.env.BUILD_TARGET_PLATFORM, + process.env.BUILD_TARGET_ARCH || process.arch + ) + return + } + + // 如果没有环境变量,按原逻辑处理命令参数 + if (!command) { + command = 'current' + } + + switch (command) { + case 'all': + await downloader.downloadAllPlatforms() + break + case 'current': + await downloader.downloadCurrentPlatform() + break + case 'clean': + downloader.cleanCache() + break + case 'platform': { + const platform = args[1] + const arch = args[2] + if (!platform || !arch) { + console.error('用法: tsx download-ffmpeg.ts platform ') + process.exit(1) + } + await downloader.downloadFFmpeg(platform, arch) + break + } + default: + console.log(` +使用方法: + tsx scripts/download-ffmpeg.ts [command] + +命令: + current - 下载当前平台的 FFmpeg (默认) + all - 下载所有支持平台的 FFmpeg + clean - 清理下载缓存 + platform - 下载指定平台的 FFmpeg + +支持的平台: + win32: x64, arm64 + darwin: x64, arm64 + linux: x64, arm64 + +环境变量: + BUILD_TARGET_PLATFORM - 构建目标平台 (win32, darwin, linux) + BUILD_TARGET_ARCH - 构建目标架构 (x64, arm64) + `) + } + } catch (error) { + console.error('错误:', error) + process.exit(1) + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + main() +} + +export { FFmpegDownloader } diff --git a/scripts/pre-release-check.ts b/scripts/pre-release-check.ts deleted file mode 100644 index b6a53323..00000000 --- a/scripts/pre-release-check.ts +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env node - -/** - * 发布前检查脚本 / Pre-release Check Script - * - * 功能 / Features: - * 1. 检查版本号是否需要更新 / Check if version needs update - * 2. 检查 Git 状态 / Check Git status - * 3. 运行基本测试 / Run basic tests - * 4. 检查构建状态 / Check build status - */ - -import { execSync } from 'child_process' -import * as fs from 'fs' -import * as path from 'path' -import { fileURLToPath } from 'url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json') - -interface PackageJson { - version: string - [key: string]: unknown -} - -function readPackageJson(): PackageJson { - const content = fs.readFileSync(PACKAGE_JSON_PATH, 'utf8') - return JSON.parse(content) as PackageJson -} - -function execCommand(command: string): string { - try { - return execSync(command, { encoding: 'utf8', stdio: 'pipe' }) - } catch { - return '' - } -} - -function checkGitStatus(): { isClean: boolean; hasUncommitted: boolean; branch: string } { - const status = execCommand('git status --porcelain') - const branch = execCommand('git branch --show-current').trim() - - return { - isClean: !status.trim(), - hasUncommitted: !!status.trim(), - branch - } -} - -// function getLastCommitMessage(): string { -// return execCommand('git log -1 --pretty=%B').trim() -// } - -// function getGitTagsSinceVersion(version: string): string[] { -// const tags = execCommand(`git tag --list --sort=-version:refname`) -// return tags.split('\n').filter((tag) => tag.trim().startsWith('v')) -// } - -function checkVersionNeedsUpdate(): { - needsUpdate: boolean - currentVersion: string - lastTag: string - commitsSinceTag: number -} { - const packageData = readPackageJson() - const currentVersion = packageData.version - - // 获取最新的版本标签 / Get latest version tag - const lastTag = execCommand('git describe --tags --abbrev=0').trim() - - // 计算自上次标签以来的提交数 / Count commits since last tag - const commitsSinceTag = parseInt( - execCommand('git rev-list --count HEAD ^' + lastTag).trim() || '0' - ) - - // 检查当前版本是否与最新标签匹配 / Check if current version matches latest tag - const needsUpdate = lastTag !== `v${currentVersion}` || commitsSinceTag > 0 - - return { - needsUpdate, - currentVersion, - lastTag: lastTag.replace('v', ''), - commitsSinceTag - } -} - -function analyzeChanges(): { hasFeatures: boolean; hasFixes: boolean; hasBreaking: boolean } { - // 分析自上次标签以来的提交类型 / Analyze commit types since last tag - const lastTag = execCommand('git describe --tags --abbrev=0').trim() - const commits = execCommand(`git log ${lastTag}..HEAD --oneline`).trim() - - if (!commits) { - return { hasFeatures: false, hasFixes: false, hasBreaking: false } - } - - const hasFeatures = /feat(\(.*\))?:/i.test(commits) - const hasFixes = /fix(\(.*\))?:/i.test(commits) - const hasBreaking = /BREAKING CHANGE|!:/i.test(commits) - - return { hasFeatures, hasFixes, hasBreaking } -} - -function suggestVersionType(): string { - const changes = analyzeChanges() - - if (changes.hasBreaking) { - return 'major' - } else if (changes.hasFeatures) { - return 'minor' - } else if (changes.hasFixes) { - return 'patch' - } else { - return 'patch' - } -} - -function main(): void { - console.log('🔍 EchoPlayer 发布前检查 / Pre-release Check') - console.log('=====================================') - - // 检查 Git 状态 / Check Git status - const gitStatus = checkGitStatus() - console.log(`\n📋 Git 状态 / Git Status:`) - console.log(`当前分支 / Current branch: ${gitStatus.branch}`) - console.log( - `工作区状态 / Working directory: ${gitStatus.isClean ? '✅ 干净' : '⚠️ 有未提交的更改'}` - ) - - if (gitStatus.hasUncommitted) { - console.log('\n⚠️ 检测到未提交的更改,建议先提交所有更改') - const status = execCommand('git status --porcelain') - console.log(status) - } - - // 检查版本状态 / Check version status - const versionInfo = checkVersionNeedsUpdate() - console.log(`\n📦 版本信息 / Version Information:`) - console.log(`当前版本 / Current version: ${versionInfo.currentVersion}`) - console.log(`最新标签 / Latest tag: ${versionInfo.lastTag}`) - console.log(`自标签以来的提交 / Commits since tag: ${versionInfo.commitsSinceTag}`) - - if (versionInfo.needsUpdate) { - console.log('\n🎯 版本更新建议 / Version Update Recommendation:') - const suggestedType = suggestVersionType() - console.log(`建议的版本类型 / Suggested version type: ${suggestedType}`) - - const changes = analyzeChanges() - if (changes.hasBreaking) { - console.log(' - 检测到破坏性更改 / Breaking changes detected') - } - if (changes.hasFeatures) { - console.log(' - 检测到新功能 / New features detected') - } - if (changes.hasFixes) { - console.log(' - 检测到修复 / Bug fixes detected') - } - - console.log('\n💡 更新版本命令建议 / Suggested version update commands:') - console.log(`npm run version:${suggestedType}`) - console.log('或使用自动化发布工具 / Or use automated release tool:') - console.log('npm run release:auto') - } else { - console.log('\n✅ 版本号已是最新') - } - - if (gitStatus.hasUncommitted || versionInfo.needsUpdate) { - console.log('\n⚠️ 建议在发布前完成以下操作:') - if (gitStatus.hasUncommitted) { - console.log(' 1. 提交所有未保存的更改') - } - if (versionInfo.needsUpdate) { - console.log(' 2. 更新版本号') - } - console.log(' 3. 运行完整测试套件') - console.log(' 4. 使用 npm run release:auto 进行自动化发布') - } else { - console.log('\n🎉 所有检查通过,可以进行发布!') - console.log('💡 使用以下命令进行发布:') - console.log(' npm run release:auto') - } -} - -main() diff --git a/scripts/release.ts b/scripts/release.ts deleted file mode 100644 index d6df72e9..00000000 --- a/scripts/release.ts +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env node - -/** - * 自动化发布脚本 / Automated Release Script - * - * 功能 / Features: - * 1. 检查当前版本状态 / Check current version status - * 2. 提示用户选择版本类型 / Prompt user to select version type - * 3. 自动更新版本号 / Automatically update version number - * 4. 构建项目 / Build project - * 5. 创建 Git 标签 / Create Git tag - * 6. 发布应用 / Publish application - */ - -import { execSync } from 'child_process' -import * as fs from 'fs' -import * as path from 'path' -import * as readline from 'readline' -import { fileURLToPath } from 'url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json') - -interface PackageJson { - version: string - [key: string]: unknown -} - -function readPackageJson(): PackageJson { - const content = fs.readFileSync(PACKAGE_JSON_PATH, 'utf8') - return JSON.parse(content) as PackageJson -} - -function execCommand(command: string, description: string): void { - console.log(`\n🔄 ${description}...`) - try { - execSync(command, { stdio: 'inherit' }) - console.log(`✅ ${description} 完成`) - } catch { - console.error(`❌ ${description} 失败`) - process.exit(1) - } -} - -function promptUser(question: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }) - - return new Promise((resolve) => { - rl.question(question, (answer: string) => { - rl.close() - resolve(answer.trim()) - }) - }) -} - -async function selectVersionType(): Promise { - console.log('\n📦 请选择版本类型 / Please select version type:') - console.log('1. patch - 补丁版本 (0.2.0 -> 0.2.1)') - console.log('2. minor - 次版本 (0.2.0 -> 0.3.0)') - console.log('3. major - 主版本 (0.2.0 -> 1.0.0)') - console.log('4. prerelease - 预发布递增 (0.2.0-alpha.2 -> 0.2.0-alpha.3)') - console.log('5. beta - Beta 版本') - console.log('6. beta-patch - Beta 补丁版本') - console.log('7. custom - 自定义版本号') - - const choice = await promptUser('请输入选择 (1-7): ') - - switch (choice) { - case '1': - return 'patch' - case '2': - return 'minor' - case '3': - return 'major' - case '4': - return 'prerelease' - case '5': - return 'beta' - case '6': - return 'beta-patch' - case '7': { - const customVersion = await promptUser('请输入自定义版本号 (例如: 1.0.0 或 1.0.0-beta.1): ') - return `custom:${customVersion}` - } - default: { - console.log('无效选择,使用默认的 patch 版本') - return 'patch' - } - } -} - -async function confirmRelease(currentVersion: string, newVersion: string): Promise { - console.log(`\n📋 发布信息 / Release Information:`) - console.log(`当前版本 / Current Version: ${currentVersion}`) - console.log(`新版本 / New Version: ${newVersion}`) - - const confirm = await promptUser('\n确认发布? (y/N): ') - return confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes' -} - -async function selectReleaseChannel(): Promise { - console.log('\n🚀 请选择发布渠道 / Please select release channel:') - console.log('1. draft - 草稿发布 (推荐)') - console.log('2. onTagOrDraft - 标签或草稿发布') - console.log('3. always - 总是发布') - console.log('4. never - 仅构建不发布') - - const choice = await promptUser('请输入选择 (1-4): ') - - switch (choice) { - case '1': - return 'release:draft' - case '2': - return 'release' - case '3': - return 'release:all' - case '4': - return 'release:never' - default: { - console.log('无效选择,使用默认的草稿发布') - return 'release:draft' - } - } -} - -async function main(): Promise { - console.log('🎯 EchoPlayer 自动化发布工具 / Automated Release Tool') - console.log('=====================================') - - // 检查当前版本 / Check current version - const packageData = readPackageJson() - const currentVersion = packageData.version - console.log(`\n📍 当前版本 / Current Version: ${currentVersion}`) - - // 检查 Git 状态 / Check Git status - try { - const gitStatus = execSync('git status --porcelain', { encoding: 'utf8' }) - if (gitStatus.trim()) { - console.log('\n⚠️ 检测到未提交的更改 / Uncommitted changes detected:') - console.log(gitStatus) - const proceed = await promptUser('是否继续发布? (y/N): ') - if (proceed.toLowerCase() !== 'y') { - console.log('发布已取消') - process.exit(0) - } - } - } catch (error) { - console.log('⚠️ 无法检查 Git 状态,继续执行...') - } - - // 选择版本类型 / Select version type - const versionChoice = await selectVersionType() - - // 更新版本号 / Update version number - let newVersion: string - if (versionChoice.startsWith('custom:')) { - const customVersion = versionChoice.replace('custom:', '') - execCommand(`npm run version:set -- ${customVersion}`, '设置自定义版本') - newVersion = customVersion - } else { - execCommand(`npm run version:${versionChoice}`, '更新版本号') - const updatedPackageData = readPackageJson() - newVersion = updatedPackageData.version - } - - // 确认发布 / Confirm release - const shouldRelease = await confirmRelease(currentVersion, newVersion) - if (!shouldRelease) { - console.log('发布已取消') - process.exit(0) - } - - // 运行测试 / Run tests - const runTests = await promptUser('\n是否运行测试? (Y/n): ') - if (runTests.toLowerCase() !== 'n' && runTests.toLowerCase() !== 'no') { - execCommand('npm run test:run', '运行单元测试') - execCommand('npm run lint', '代码检查') - execCommand('npm run typecheck', '类型检查') - } - - // 选择发布渠道 / Select release channel - const releaseChannel = await selectReleaseChannel() - - // 提交版本更改 / Commit version changes - try { - execCommand(`git add package.json`, '添加版本文件到 Git') - execCommand(`git commit -m "chore: release v${newVersion}"`, '提交版本更改') - execCommand(`git tag v${newVersion}`, '创建 Git 标签') - } catch (error) { - console.log('⚠️ Git 操作可能失败,继续构建...') - } - - // 构建和发布 / Build and release - execCommand(`npm run ${releaseChannel}`, '构建和发布应用') - - console.log('\n🎉 发布完成! / Release completed!') - console.log(`✅ 版本 ${newVersion} 已成功发布`) - - // 推送到远程仓库 / Push to remote repository - const pushToRemote = await promptUser('\n是否推送到远程仓库? (Y/n): ') - if (pushToRemote.toLowerCase() !== 'n' && pushToRemote.toLowerCase() !== 'no') { - try { - execCommand('git push origin main', '推送代码到远程仓库') - execCommand('git push origin --tags', '推送标签到远程仓库') - } catch (error) { - console.log('⚠️ 推送失败,请手动推送') - } - } - - console.log('\n🏁 所有操作完成!') -} - -// 处理未捕获的异常 / Handle uncaught exceptions -process.on('unhandledRejection', (error) => { - console.error('❌ 发布过程中出现错误:', error) - process.exit(1) -}) - -main().catch((error) => { - console.error('❌ 发布失败:', error) - process.exit(1) -}) diff --git a/scripts/rename-artifacts.ts b/scripts/rename-artifacts.ts deleted file mode 100644 index 8419eed9..00000000 --- a/scripts/rename-artifacts.ts +++ /dev/null @@ -1,499 +0,0 @@ -#!/usr/bin/env node - -/** - * 构建产物重命名脚本 / Build Artifacts Rename Script - * - * 功能 / Features: - * 1. 重命名构建产物以符合发布要求 / Rename build artifacts to meet release requirements - * 2. 处理不同平台的文件格式 / Handle different platform file formats - * 3. 确保文件名一致性 / Ensure filename consistency - * 4. 支持版本号和架构标识 / Support version and architecture identification - */ - -import * as fs from 'fs' -import * as path from 'path' - -// 项目根目录 / Project root directory -const PROJECT_ROOT = path.join(process.cwd()) -const DIST_DIR = path.join(PROJECT_ROOT, 'dist') -const PACKAGE_JSON_PATH = path.join(PROJECT_ROOT, 'package.json') - -interface PackageJson { - version: string - productName?: string - [key: string]: unknown -} - -/** - * 读取 package.json 获取版本信息 / Read package.json to get version info - */ -function getPackageInfo(): { version: string; productName: string } { - try { - const packageJson: PackageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8')) - return { - version: packageJson.version, - productName: packageJson.productName || 'echoplayer' - } - } catch (error) { - console.error('❌ 无法读取 package.json:', error) - process.exit(1) - } -} - -/** - * 获取平台和架构信息 / Get platform and architecture info - */ -function getPlatformInfo(): { platform: string; arch: string } { - // 优先使用 GitHub Actions 矩阵变量 / Prefer GitHub Actions matrix variables - const buildPlatform = process.env.BUILD_PLATFORM - const buildArch = process.env.BUILD_ARCH - - if (buildPlatform && buildArch) { - console.log(`🎯 使用 GitHub Actions 矩阵配置: ${buildPlatform}-${buildArch}`) - return { - platform: buildPlatform, - arch: buildArch - } - } - - // 回退到系统检测 / Fallback to system detection - const platform = process.env.RUNNER_OS?.toLowerCase() || process.platform - const arch = process.env.RUNNER_ARCH || process.arch - - // 标准化平台名称 / Normalize platform names - const normalizedPlatform = - platform === 'windows' || platform === 'win32' - ? 'win' - : platform === 'macos' || platform === 'darwin' - ? 'mac' - : platform === 'linux' - ? 'linux' - : platform - - // 标准化架构名称 / Normalize architecture names - // 对于 Linux 平台,保留 amd64 架构名称 / For Linux platform, keep amd64 architecture name - const normalizedArch = (() => { - if (normalizedPlatform === 'linux') { - // Linux 平台保留原有架构名称,特别是 amd64 / Keep original arch names for Linux, especially amd64 - return arch === 'x86_64' ? 'amd64' : arch === 'x64' ? 'amd64' : arch - } else { - // 其他平台使用标准化命名 / Use normalized naming for other platforms - return arch === 'x64' ? 'x64' : arch === 'arm64' ? 'arm64' : arch === 'x86_64' ? 'x64' : arch - } - })() - - console.log(`🔍 使用系统检测: ${normalizedPlatform}-${normalizedArch}`) - return { - platform: normalizedPlatform, - arch: normalizedArch - } -} - -/** - * 检查文件是否存在 / Check if file exists - */ -function fileExists(filePath: string): boolean { - try { - return fs.existsSync(filePath) - } catch { - return false - } -} - -/** - * 重命名文件 / Rename file - */ -function renameFile(oldPath: string, newPath: string): boolean { - try { - if (!fileExists(oldPath)) { - console.log(`⚠️ 源文件不存在: ${oldPath}`) - return false - } - - if (fileExists(newPath)) { - console.log(`⚠️ 目标文件已存在: ${newPath}`) - return false - } - - fs.renameSync(oldPath, newPath) - console.log(`✅ 重命名成功: ${path.basename(oldPath)} -> ${path.basename(newPath)}`) - return true - } catch (error) { - console.error(`❌ 重命名失败: ${oldPath} -> ${newPath}`, error) - return false - } -} - -/** - * 列出 dist 目录中的所有文件 / List all files in dist directory - */ -function listDistFiles(): string[] { - try { - const files = fs.readdirSync(DIST_DIR, { recursive: true }) - return files - .filter( - (file) => typeof file === 'string' && !fs.statSync(path.join(DIST_DIR, file)).isDirectory() - ) - .map((file) => file.toString()) - } catch (error) { - console.error('❌ 无法读取 dist 目录:', error) - return [] - } -} - -/** - * 处理 Windows 构建产物 / Handle Windows build artifacts - */ -function handleWindowsArtifacts(version: string, productName: string, arch: string): number { - let renamedCount = 0 - const files = listDistFiles() - - // 查找 Windows 安装程序 / Find Windows installer - const setupPattern = /\.exe$/i - const setupFiles = files.filter((file) => setupPattern.test(file)) - - for (const file of setupFiles) { - const oldPath = path.join(DIST_DIR, file) - const expectedName = `${productName}-${version}-${arch}-setup.exe` - const newPath = path.join(DIST_DIR, expectedName) - - if (path.basename(file) !== expectedName) { - if (renameFile(oldPath, newPath)) { - renamedCount++ - } - } else { - console.log(`✅ Windows 安装程序已是正确名称: ${file}`) - renamedCount++ - } - } - - // 更新 latest.yml 文件中的文件引用 / Update file references in latest.yml - const latestYmlPath = path.join(DIST_DIR, 'latest.yml') - if (fs.existsSync(latestYmlPath)) { - try { - let yamlContent = fs.readFileSync(latestYmlPath, 'utf8') - let updated = false - - // 更新 EXE 文件引用 / Update EXE file references - const oldExeName = `${productName}-${version}-setup.exe` - const newExeName = `${productName}-${version}-${arch}-setup.exe` - if (yamlContent.includes(oldExeName)) { - yamlContent = yamlContent.replace(new RegExp(oldExeName, 'g'), newExeName) - updated = true - console.log(`✅ 更新 YAML 中的 EXE 文件引用: ${oldExeName} -> ${newExeName}`) - } - - if (updated) { - fs.writeFileSync(latestYmlPath, yamlContent, 'utf8') - console.log(`✅ 已更新 latest.yml 文件`) - renamedCount++ - } - } catch (error) { - console.error(`❌ 更新 latest.yml 文件失败:`, error) - } - } - - return renamedCount -} - -/** - * 处理 macOS 构建产物 / Handle macOS build artifacts - */ -function handleMacOSArtifacts(version: string, productName: string, arch: string): number { - let renamedCount = 0 - const files = listDistFiles() - - // 查找 macOS DMG 文件 / Find macOS DMG files - const dmgPattern = /\.dmg$/i - const dmgFiles = files.filter((file) => dmgPattern.test(file)) - - for (const file of dmgFiles) { - const oldPath = path.join(DIST_DIR, file) - const expectedName = `${productName}-${version}-${arch}.dmg` - const newPath = path.join(DIST_DIR, expectedName) - - if (path.basename(file) !== expectedName) { - if (renameFile(oldPath, newPath)) { - renamedCount++ - } - } else { - console.log(`✅ macOS DMG 文件已是正确名称: ${file}`) - renamedCount++ - } - } - - // 查找 macOS ZIP 文件 / Find macOS ZIP files - const zipPattern = /\.zip$/i - const zipFiles = files.filter((file) => zipPattern.test(file)) - - for (const file of zipFiles) { - const oldPath = path.join(DIST_DIR, file) - const expectedName = `${productName}-${version}-${arch}.zip` - const newPath = path.join(DIST_DIR, expectedName) - - if (path.basename(file) !== expectedName) { - if (renameFile(oldPath, newPath)) { - renamedCount++ - } - } else { - console.log(`✅ macOS ZIP 文件已是正确名称: ${file}`) - renamedCount++ - } - } - - // 查找 macOS blockmap 文件 / Find macOS blockmap files - const blockmapPattern = /\.blockmap$/i - const blockmapFiles = files.filter((file) => blockmapPattern.test(file)) - - for (const file of blockmapFiles) { - const oldPath = path.join(DIST_DIR, file) - let expectedName = '' - - if (file.includes('.dmg.blockmap')) { - expectedName = `${productName}-${version}-${arch}.dmg.blockmap` - } else if (file.includes('.zip.blockmap')) { - expectedName = `${productName}-${version}-${arch}.zip.blockmap` - } else { - continue // 跳过不匹配的 blockmap 文件 - } - - const newPath = path.join(DIST_DIR, expectedName) - - if (path.basename(file) !== expectedName) { - if (renameFile(oldPath, newPath)) { - renamedCount++ - } - } else { - console.log(`✅ macOS blockmap 文件已是正确名称: ${file}`) - renamedCount++ - } - } - - // 更新 latest-mac.yml 文件中的文件引用 / Update file references in latest-mac.yml - const latestMacYmlPath = path.join(DIST_DIR, 'latest-mac.yml') - if (fs.existsSync(latestMacYmlPath)) { - try { - let yamlContent = fs.readFileSync(latestMacYmlPath, 'utf8') - let updated = false - - // 更新 ZIP 文件引用 / Update ZIP file references - const oldZipName = `${productName}-${version}-mac.zip` - const newZipName = `${productName}-${version}-${arch}.zip` - if (yamlContent.includes(oldZipName)) { - yamlContent = yamlContent.replace(new RegExp(oldZipName, 'g'), newZipName) - updated = true - console.log(`✅ 更新 YAML 中的 ZIP 文件引用: ${oldZipName} -> ${newZipName}`) - } - - // 更新 DMG 文件引用 / Update DMG file references - const oldDmgName = `${productName}-${version}.dmg` - const newDmgName = `${productName}-${version}-${arch}.dmg` - if (yamlContent.includes(oldDmgName)) { - yamlContent = yamlContent.replace(new RegExp(oldDmgName, 'g'), newDmgName) - updated = true - console.log(`✅ 更新 YAML 中的 DMG 文件引用: ${oldDmgName} -> ${newDmgName}`) - } - - if (updated) { - fs.writeFileSync(latestMacYmlPath, yamlContent, 'utf8') - console.log(`✅ 已更新 latest-mac.yml 文件`) - renamedCount++ - } - } catch (error) { - console.error(`❌ 更新 latest-mac.yml 文件失败:`, error) - } - } - - return renamedCount -} - -/** - * 处理 Linux 构建产物 / Handle Linux build artifacts - */ - -function handleLinuxArtifacts(version: string, productName: string, arch: string): number { - let renamedCount = 0 - const files = listDistFiles() - - // 查找 Linux AppImage 文件 / Find Linux AppImage files - const appImagePattern = /\.AppImage$/i - const appImageFiles = files.filter((file) => appImagePattern.test(file)) - - for (const file of appImageFiles) { - const oldPath = path.join(DIST_DIR, file) - - // 检测实际文件名中的架构标识 / Detect architecture identifier in actual filename - let targetArch = arch - if (file.includes('x86_64') && arch === 'x64') { - // 如果文件名包含 x86_64 而矩阵配置是 x64,转换为 amd64 - targetArch = 'amd64' - console.log(`🔄 检测到 x86_64 架构,转换为 amd64`) - } - - const expectedName = `${productName}-${version}-${targetArch}.AppImage` - const newPath = path.join(DIST_DIR, expectedName) - - if (path.basename(file) !== expectedName) { - if (renameFile(oldPath, newPath)) { - renamedCount++ - } - } else { - console.log(`✅ Linux AppImage 文件已是正确名称: ${file}`) - renamedCount++ - } - } - - // 查找 Linux DEB 文件 / Find Linux DEB files - const debPattern = /\.deb$/i - const debFiles = files.filter((file) => debPattern.test(file)) - - for (const file of debFiles) { - const oldPath = path.join(DIST_DIR, file) - - // 检测实际文件名中的架构标识 / Detect architecture identifier in actual filename - let targetArch = arch - if (file.includes('amd64') && arch === 'x64') { - // 如果文件名包含 amd64 而矩阵配置是 x64,保持 amd64 - targetArch = 'amd64' - console.log(`🔄 检测到 amd64 架构,保持 amd64`) - } - - const expectedName = `${productName}-${version}-${targetArch}.deb` - const newPath = path.join(DIST_DIR, expectedName) - - if (path.basename(file) !== expectedName) { - if (renameFile(oldPath, newPath)) { - renamedCount++ - } - } else { - console.log(`✅ Linux DEB 文件已是正确名称: ${file}`) - renamedCount++ - } - } - - // 更新 latest-linux.yml 文件中的文件引用 / Update file references in latest-linux.yml - const latestLinuxYmlPath = path.join(DIST_DIR, 'latest-linux.yml') - if (fs.existsSync(latestLinuxYmlPath)) { - try { - let yamlContent = fs.readFileSync(latestLinuxYmlPath, 'utf8') - let updated = false - - // 确定目标架构名称 / Determine target architecture name - let targetArch = arch - if (yamlContent.includes('x86_64') && arch === 'x64') { - targetArch = 'amd64' - console.log(`🔄 YAML 文件中检测到 x86_64,转换为 amd64`) - } - - // 更新 AppImage 文件引用 / Update AppImage file references - const oldAppImageName = `${productName}-${version}.AppImage` - const newAppImageName = `${productName}-${version}-${targetArch}.AppImage` - if (yamlContent.includes(oldAppImageName)) { - yamlContent = yamlContent.replace(new RegExp(oldAppImageName, 'g'), newAppImageName) - updated = true - console.log(`✅ 更新 YAML 中的 AppImage 文件引用: ${oldAppImageName} -> ${newAppImageName}`) - } - - // 处理可能存在的 x86_64 AppImage 引用 / Handle possible x86_64 AppImage references - const oldAppImageNameX86 = `${productName}-${version}-x86_64.AppImage` - if (yamlContent.includes(oldAppImageNameX86) && targetArch === 'amd64') { - yamlContent = yamlContent.replace(new RegExp(oldAppImageNameX86, 'g'), newAppImageName) - updated = true - console.log( - `✅ 更新 YAML 中的 x86_64 AppImage 文件引用: ${oldAppImageNameX86} -> ${newAppImageName}` - ) - } - - // 更新 DEB 文件引用 / Update DEB file references - const oldDebName = `${productName}-${version}.deb` - const newDebName = `${productName}-${version}-${targetArch}.deb` - if (yamlContent.includes(oldDebName)) { - yamlContent = yamlContent.replace(new RegExp(oldDebName, 'g'), newDebName) - updated = true - console.log(`✅ 更新 YAML 中的 DEB 文件引用: ${oldDebName} -> ${newDebName}`) - } - - if (updated) { - fs.writeFileSync(latestLinuxYmlPath, yamlContent, 'utf8') - console.log(`✅ 已更新 latest-linux.yml 文件`) - renamedCount++ - } - } catch (error) { - console.error(`❌ 更新 latest-linux.yml 文件失败:`, error) - } - } - - return renamedCount -} - -/** - * 主函数 / Main function - */ -async function main(): Promise { - console.log('🔄 开始重命名构建产物...') - console.log('🔄 Starting to rename build artifacts...') - - // 检查 dist 目录是否存在 / Check if dist directory exists - if (!fileExists(DIST_DIR)) { - console.error('❌ dist 目录不存在,请先运行构建命令') - process.exit(1) - } - - // 获取项目信息 / Get project info - const { version, productName } = getPackageInfo() - const { platform, arch } = getPlatformInfo() - - console.log(`📦 产品名称: ${productName}`) - console.log(`🏷️ 版本号: ${version}`) - console.log(`💻 平台: ${platform}`) - console.log(`🏗️ 架构: ${arch}`) - - // 列出当前 dist 目录中的文件 / List current files in dist directory - const distFiles = listDistFiles() - console.log(`📁 dist 目录中的文件 (${distFiles.length} 个):`) - distFiles.forEach((file) => console.log(` - ${file}`)) - - let totalRenamed = 0 - - // 根据平台处理构建产物 / Handle build artifacts based on platform - switch (platform) { - case 'win': - case 'windows': - totalRenamed += handleWindowsArtifacts(version, productName, arch) - break - - case 'mac': - case 'macos': - case 'darwin': - totalRenamed += handleMacOSArtifacts(version, productName, arch) - break - - case 'linux': - totalRenamed += handleLinuxArtifacts(version, productName, arch) - break - - default: - console.log(`⚠️ 未知平台: ${platform},跳过重命名`) - break - } - - // 输出结果 / Output results - console.log(`\n📊 重命名完成统计:`) - console.log(`📊 Rename completion statistics:`) - console.log(`✅ 成功重命名文件数: ${totalRenamed}`) - console.log(`✅ Successfully renamed files: ${totalRenamed}`) - - if (totalRenamed === 0) { - console.log('⚠️ 没有文件需要重命名或重命名失败') - console.log('⚠️ No files need to be renamed or rename failed') - } - - console.log('🎉 构建产物重命名完成!') - console.log('🎉 Build artifacts rename completed!') -} - -// 运行主函数 / Run main function -main().catch((error) => { - console.error('❌ 重命名过程中出现错误:', error) - process.exit(1) -}) diff --git a/scripts/upload-assets.js b/scripts/upload-assets.js new file mode 100644 index 00000000..de54fc51 --- /dev/null +++ b/scripts/upload-assets.js @@ -0,0 +1,426 @@ +#!/usr/bin/env node + +const https = require('https') +const fs = require('fs') +const path = require('path') +const { URL } = require('url') + +/** + * GitCode 资产上传脚本 + * 功能: + * 1. 并发上传文件到 GitCode + * 2. 检查文件是否已存在,避免重复上传 + * 3. 支持断点续传和错误重试 + */ + +class GitCodeUploader { + constructor(options) { + this.accessToken = options.accessToken + this.owner = options.owner + this.repo = options.repo + this.tag = options.tag + this.concurrency = options.concurrency || 3 + this.retryAttempts = options.retryAttempts || 3 + this.baseUrl = 'https://api.gitcode.com/api/v5' + } + + /** + * HTTP 请求工具方法 + */ + async httpRequest(url, options = {}) { + return new Promise((resolve, reject) => { + const urlObj = new URL(url) + const requestOptions = { + hostname: urlObj.hostname, + port: urlObj.port || 443, + path: urlObj.pathname + urlObj.search, + method: options.method || 'GET', + headers: options.headers || {}, + ...options.httpsOptions + } + + const req = https.request(requestOptions, (res) => { + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + const result = { + statusCode: res.statusCode, + headers: res.headers, + data: data + } + + try { + if (data && res.headers['content-type']?.includes('application/json')) { + result.json = JSON.parse(data) + } + } catch (e) { + // JSON 解析失败,保持原始数据 + } + + resolve(result) + }) + }) + + req.on('error', reject) + + if (options.body) { + if (options.body instanceof Buffer || typeof options.body === 'string') { + req.write(options.body) + } else { + req.write(JSON.stringify(options.body)) + } + } + + req.end() + }) + } + + /** + * 获取现有的 release 信息和资产列表 + */ + async getExistingAssets() { + const url = `${this.baseUrl}/repos/${this.owner}/${this.repo}/releases?access_token=${this.accessToken}` + + try { + const response = await this.httpRequest(url) + + if (response.statusCode === 200 && response.json && Array.isArray(response.json)) { + // 从 releases 数组中找到匹配的 tag + const targetRelease = response.json.find((release) => release.tag_name === this.tag) + + if (targetRelease) { + const assets = targetRelease.assets || [] + const assetNames = new Set(assets.map((asset) => asset.name)) + console.log(`✓ 找到现有 release ${this.tag},包含 ${assets.length} 个资产`) + + // GitCode releases API 使用 tag_name 作为标识符 + const releaseId = targetRelease.tag_name + console.log(` 使用标识符: ${releaseId}`) + + if (assets.length > 0) { + console.log(` 现有资产:`) + assets.slice(0, 3).forEach((asset) => { + console.log(` - ${asset.name} (${asset.type})`) + }) + if (assets.length > 3) { + console.log(` ... 以及其他 ${assets.length - 3} 个文件`) + } + } + + return { releaseId: releaseId, existingAssets: assetNames } + } else { + console.log(`✗ Release ${this.tag} 不存在`) + return { releaseId: null, existingAssets: new Set() } + } + } else { + throw new Error(`获取 releases 列表失败: ${response.statusCode} ${response.data}`) + } + } catch (error) { + console.error('获取现有资产失败:', error.message) + throw error + } + } + + /** + * 获取上传 URL + */ + async getUploadUrl(releaseId, fileName) { + const url = `${this.baseUrl}/repos/${this.owner}/${this.repo}/releases/${releaseId}/upload_url?access_token=${this.accessToken}&file_name=${encodeURIComponent(fileName)}` + + try { + const response = await this.httpRequest(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + + if (response.statusCode === 200 && response.json) { + return response.json + } else { + throw new Error(`获取上传 URL 失败: ${response.statusCode} ${response.data}`) + } + } catch (error) { + console.error(`获取 ${fileName} 上传 URL 失败:`, error.message) + throw error + } + } + + /** + * 上传文件到 GitCode 对象存储 + */ + async uploadFileToStorage(uploadInfo, filePath) { + const fileName = path.basename(filePath) + const fileBuffer = fs.readFileSync(filePath) + const fileSize = fileBuffer.length + + const uploadUrl = uploadInfo.url + + console.log(uploadInfo.url) + console.log(uploadInfo.headers) + + try { + const response = await this.httpRequest(uploadUrl, { + method: 'PUT', + headers: { ...uploadInfo.headers, 'Content-Length': fileSize }, + body: fileBuffer + }) + + if (response.statusCode === 200) { + console.log(`✓ ${fileName} 上传成功 (${fileSize} bytes)`) + return true + } else { + throw new Error(`上传失败: ${response.statusCode} ${response.data}`) + } + } catch (error) { + console.error(`上传 ${fileName} 到存储失败:`, error.message) + throw error + } + } + + /** + * 上传单个文件(带重试) + */ + async uploadSingleFile(releaseId, filePath, existingAssets) { + const fileName = path.basename(filePath) + + // 检查文件是否已存在 + if (existingAssets.has(fileName)) { + console.log(`⚠ ${fileName} 已存在,跳过上传`) + return { success: true, skipped: true } + } + + if (!fs.existsSync(filePath)) { + console.log(`⚠ ${fileName} 文件不存在,跳过`) + return { success: false, error: 'File not found' } + } + + const fileStats = fs.statSync(filePath) + const fileSize = fileStats.size + + for (let attempt = 1; attempt <= this.retryAttempts; attempt++) { + try { + console.log( + `⏳ 上传 ${fileName} (${fileSize} bytes) - 尝试 ${attempt}/${this.retryAttempts}` + ) + + // 获取上传 URL + const uploadInfo = await this.getUploadUrl(releaseId, fileName) + + // 上传到对象存储 + await this.uploadFileToStorage(uploadInfo, filePath) + + return { success: true, skipped: false } + } catch (error) { + console.error( + `上传 ${fileName} 失败 (尝试 ${attempt}/${this.retryAttempts}):`, + error.message + ) + + if (attempt === this.retryAttempts) { + return { success: false, error: error.message } + } + + // 等待后重试 + await new Promise((resolve) => setTimeout(resolve, 1000 * attempt)) + } + } + } + + /** + * 并发上传多个文件 + */ + async uploadFiles(filePaths) { + console.log(`开始上传 ${filePaths.length} 个文件 (并发数: ${this.concurrency})`) + + // 获取现有资产列表 + const { releaseId, existingAssets } = await this.getExistingAssets() + + if (!releaseId) { + throw new Error(`Release ${this.tag} 不存在,无法上传资产`) + } + + // 过滤出需要上传的文件 + const filesToUpload = filePaths.filter((filePath) => { + const fileName = path.basename(filePath) + return !existingAssets.has(fileName) && fs.existsSync(filePath) + }) + + console.log(`需要上传 ${filesToUpload.length} 个新文件`) + + if (filesToUpload.length === 0) { + console.log('所有文件都已存在,无需上传') + return { + total: filePaths.length, + success: filePaths.length, + failed: 0, + skipped: filePaths.length + } + } + + // 并发上传 + const results = [] + const semaphore = new Array(this.concurrency).fill(null) + + const uploadPromises = filesToUpload.map(async (filePath) => { + // 等待信号量 + await new Promise((resolve) => { + const checkSemaphore = () => { + const index = semaphore.indexOf(null) + if (index !== -1) { + semaphore[index] = filePath + resolve() + } else { + setTimeout(checkSemaphore, 100) + } + } + checkSemaphore() + }) + + try { + const result = await this.uploadSingleFile(releaseId, filePath, existingAssets) + result.filePath = filePath + results.push(result) + } finally { + // 释放信号量 + const index = semaphore.indexOf(filePath) + if (index !== -1) { + semaphore[index] = null + } + } + }) + + await Promise.all(uploadPromises) + + // 统计结果 + const stats = { + total: filePaths.length, + success: results.filter((r) => r.success).length, + failed: results.filter((r) => !r.success).length, + skipped: + results.filter((r) => r.skipped || existingAssets.has(path.basename(r.filePath))).length + + (filePaths.length - filesToUpload.length) + } + + console.log(`\n上传完成:`) + console.log(` 总计: ${stats.total}`) + console.log(` 成功: ${stats.success}`) + console.log(` 失败: ${stats.failed}`) + console.log(` 跳过: ${stats.skipped}`) + + // 输出失败的文件 + const failedFiles = results.filter((r) => !r.success) + if (failedFiles.length > 0) { + console.log('\n失败的文件:') + failedFiles.forEach((result) => { + console.log(` - ${path.basename(result.filePath)}: ${result.error}`) + }) + } + + return stats + } +} + +// 命令行接口 +async function main() { + const args = process.argv.slice(2) + + if (args.length === 0 || args.includes('--help') || args.includes('-h')) { + console.log(` +GitCode 资产上传工具 + +用法: node upload-assets.js [选项] <文件路径...> + +选项: + --token GitCode access token (必需) + --owner 仓库所有者 (必需) + --repo 仓库名称 (必需) + --tag 发布标签 (必需) + --concurrency 并发数量 (默认: 3) + --retry 重试次数 (默认: 3) + --help, -h 显示帮助信息 + +示例: + node upload-assets.js --token xxx --owner mkdir700 --repo EchoPlayer --tag v1.0.0 file1.zip file2.deb + +环境变量: + GITCODE_ACCESS_TOKEN GitCode access token + GITCODE_OWNER 仓库所有者 + GITCODE_REPO 仓库名称 + GITCODE_TAG 发布标签 +`) + process.exit(0) + } + + // 解析命令行参数 + const options = { + accessToken: process.env.GITCODE_ACCESS_TOKEN, + owner: process.env.GITCODE_OWNER, + repo: process.env.GITCODE_REPO, + tag: process.env.GITCODE_TAG, + concurrency: 3, + retryAttempts: 3 + } + + const filePaths = [] + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + + if (arg === '--token' && i + 1 < args.length) { + options.accessToken = args[++i] + } else if (arg === '--owner' && i + 1 < args.length) { + options.owner = args[++i] + } else if (arg === '--repo' && i + 1 < args.length) { + options.repo = args[++i] + } else if (arg === '--tag' && i + 1 < args.length) { + options.tag = args[++i] + } else if (arg === '--concurrency' && i + 1 < args.length) { + options.concurrency = parseInt(args[++i]) + } else if (arg === '--retry' && i + 1 < args.length) { + options.retryAttempts = parseInt(args[++i]) + } else if (!arg.startsWith('--')) { + filePaths.push(arg) + } + } + + // 验证必需参数 + const required = ['accessToken', 'owner', 'repo', 'tag'] + const missing = required.filter((key) => !options[key]) + + if (missing.length > 0) { + console.error(`错误: 缺少必需参数: ${missing.join(', ')}`) + process.exit(1) + } + + if (filePaths.length === 0) { + console.error('错误: 未指定要上传的文件') + process.exit(1) + } + + try { + const uploader = new GitCodeUploader(options) + const stats = await uploader.uploadFiles(filePaths) + + if (stats.failed > 0) { + process.exit(1) + } + } catch (error) { + console.error('上传失败:', error.message) + process.exit(1) + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + main().catch((error) => { + console.error('未处理的错误:', error) + process.exit(1) + }) +} + +module.exports = GitCodeUploader diff --git a/scripts/version-manager.ts b/scripts/version-manager.ts deleted file mode 100644 index 841be712..00000000 --- a/scripts/version-manager.ts +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env node - -import * as fs from 'fs' -import * as path from 'path' -import { fileURLToPath } from 'url' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const PACKAGE_JSON_PATH = path.join(__dirname, '..', 'package.json') - -/** - * Version types and their meanings: - * - dev: Development version (for active development) - * - test: Test version (for internal testing) - * - alpha: Alpha version (early preview, may have bugs) - * - beta: Beta version (feature complete, testing phase) - * - stable: Stable version (production ready) - */ - -type VersionType = 'dev' | 'test' | 'alpha' | 'beta' | 'stable' -type IncrementType = 'major' | 'minor' | 'patch' - -interface PackageJson { - version: string - [key: string]: unknown -} - -interface ParsedVersion { - major: number - minor: number - patch: number - prerelease: string | null -} - -function readPackageJson(): PackageJson { - const content = fs.readFileSync(PACKAGE_JSON_PATH, 'utf8') - return JSON.parse(content) as PackageJson -} - -function writePackageJson(packageData: PackageJson): void { - fs.writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(packageData, null, 2) + '\n') -} - -function parseVersion(version: string): ParsedVersion { - const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/) - if (!match) { - throw new Error(`Invalid version format: ${version}`) - } - - const [, major, minor, patch, prerelease] = match - return { - major: parseInt(major, 10), - minor: parseInt(minor, 10), - patch: parseInt(patch, 10), - prerelease: prerelease || null - } -} - -function formatVersion(versionObj: ParsedVersion): string { - const base = `${versionObj.major}.${versionObj.minor}.${versionObj.patch}` - return versionObj.prerelease ? `${base}-${versionObj.prerelease}` : base -} - -function detectVersionType(version: string): VersionType { - if (!version) return 'stable' - - if (version.includes('dev')) return 'dev' - if (version.includes('test')) return 'test' - if (version.includes('alpha')) return 'alpha' - if (version.includes('beta')) return 'beta' - return 'stable' -} - -function incrementVersion( - currentVersion: string, - type: IncrementType, - versionType: VersionType = 'stable' -): string { - const parsed = parseVersion(currentVersion) - - switch (type) { - case 'major': { - parsed.major++ - parsed.minor = 0 - parsed.patch = 0 - break - } - case 'minor': { - parsed.minor++ - parsed.patch = 0 - break - } - case 'patch': { - parsed.patch++ - break - } - default: { - throw new Error(`Invalid increment type: ${type}`) - } - } - - // Set prerelease based on version type - if (versionType === 'stable') { - parsed.prerelease = null - } else if (versionType === 'beta') { - parsed.prerelease = 'beta.1' - } else if (versionType === 'alpha') { - parsed.prerelease = 'alpha.1' - } else if (versionType === 'dev') { - parsed.prerelease = 'dev.1' - } else if (versionType === 'test') { - parsed.prerelease = 'test.1' - } - - return formatVersion(parsed) -} - -function incrementPrerelease(currentVersion: string): string { - const parsed = parseVersion(currentVersion) - - if (!parsed.prerelease) { - throw new Error('Cannot increment prerelease on stable version') - } - - const match = parsed.prerelease.match(/^(.+)\.(\d+)$/) - if (!match) { - throw new Error(`Invalid prerelease format: ${parsed.prerelease}`) - } - - const [, type, number] = match - parsed.prerelease = `${type}.${parseInt(number, 10) + 1}` - - return formatVersion(parsed) -} - -function main(): void { - const args = process.argv.slice(2) - const command = args[0] - - if (!command) { - console.log(` -Usage: node version-manager.js [options] - -Commands: - current Show current version and type - set Set specific version (e.g., 1.0.0, 1.0.0-beta.1) - major [type] Increment major version (type: stable|beta|alpha|dev|test) - minor [type] Increment minor version (type: stable|beta|alpha|dev|test) - patch [type] Increment patch version (type: stable|beta|alpha|dev|test) - prerelease Increment prerelease number (e.g., beta.1 -> beta.2) - -Examples: - node version-manager.js current - node version-manager.js set 1.0.0-beta.1 - node version-manager.js minor beta - node version-manager.js prerelease - `) - return - } - - const packageData = readPackageJson() - const currentVersion = packageData.version - const currentType = detectVersionType(currentVersion) - - try { - switch (command) { - case 'current': { - console.log(`Current version: ${currentVersion}`) - console.log(`Version type: ${currentType}`) - break - } - - case 'set': { - const newVersion = args[1] - if (!newVersion) { - console.error('Please provide a version number') - process.exit(1) - } - packageData.version = newVersion - writePackageJson(packageData) - console.log(`Version updated to: ${newVersion}`) - console.log(`Version type: ${detectVersionType(newVersion)}`) - break - } - - case 'major': - case 'minor': - case 'patch': { - const versionType = (args[1] as VersionType) || 'stable' - const incrementedVersion = incrementVersion(currentVersion, command, versionType) - packageData.version = incrementedVersion - writePackageJson(packageData) - console.log(`Version updated from ${currentVersion} to ${incrementedVersion}`) - console.log(`Version type: ${detectVersionType(incrementedVersion)}`) - break - } - - case 'prerelease': { - const prereleaseVersion = incrementPrerelease(currentVersion) - packageData.version = prereleaseVersion - writePackageJson(packageData) - console.log(`Version updated from ${currentVersion} to ${prereleaseVersion}`) - console.log(`Version type: ${detectVersionType(prereleaseVersion)}`) - break - } - - default: { - console.error(`Unknown command: ${command}`) - process.exit(1) - } - } - } catch (error) { - console.error(`Error: ${(error as Error).message}`) - process.exit(1) - } -} - -// Always run main function when script is executed directly -main() - -export { - detectVersionType, - formatVersion, - incrementPrerelease, - type IncrementType, - incrementVersion, - type PackageJson, - type ParsedVersion, - parseVersion, - type VersionType -} diff --git a/src/main/__tests__/DictionaryService.test.ts b/src/main/__tests__/DictionaryService.test.ts new file mode 100644 index 00000000..17c12c16 --- /dev/null +++ b/src/main/__tests__/DictionaryService.test.ts @@ -0,0 +1,723 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock logger service - 简化版本,专注于核心功能测试 +vi.mock('@logger', () => ({ + loggerService: { + withContext: vi.fn(() => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn() + })) + } +})) + +import DictionaryService from '../services/DictionaryService' + +// Mock fetch globally +const mockFetch = vi.fn() +global.fetch = mockFetch as any + +describe('DictionaryService', () => { + let dictionaryService: DictionaryService + const mockEvent = {} as Electron.IpcMainInvokeEvent + + beforeEach(() => { + vi.clearAllMocks() + dictionaryService = new DictionaryService() + }) + + describe('queryEudic - 核心功能测试', () => { + describe('✅ 成功场景', () => { + it('应该成功查询单词并返回完整数据 - hello 示例', async () => { + // 模拟欧陆词典 hello 的真实 HTML 响应 + const mockHtmlResponse = ` + + + + + + + +
+
+ +
+
+
    +
  1. int. 喂;哈罗
  2. +
  3. n. 表示问候, 惊奇或唤起注意时的用语
  4. +
+
+
+
+
Hello, how are you?
+
你好,你好吗?
+
+ + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockHtmlResponse) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'hello') + + // 验证返回结果 + expect(result.success).toBe(true) + expect(result.data).toBeDefined() + expect(result.data!.word).toBe('hello') + expect(result.data!.pronunciations).toBeDefined() + expect(result.data!.pronunciations!.length).toBeGreaterThan(0) + expect(result.data!.pronunciations![0].phonetic).toBe("/hə'ləʊ/") + expect(result.data!.definitions).toHaveLength(2) + expect(result.data!.definitions[0]).toEqual({ + partOfSpeech: 'int.', + meaning: '喂;哈罗' + }) + expect(result.data!.definitions[1]).toEqual({ + partOfSpeech: 'n.', + meaning: '表示问候, 惊奇或唤起注意时的用语' + }) + + // 验证API调用 + expect(mockFetch).toHaveBeenCalledWith( + 'https://dict.eudic.net/dicts/MiniDictSearch2?word=hello&context=hello', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'User-Agent': expect.stringContaining('Mozilla') + }) + }) + ) + }) + + it('应该成功解析简单格式的词典响应 - program 示例', async () => { + const mockSimpleHtmlResponse = ` + + +
/ˈproʊɡræm/
+
+ n. 程序,节目 +
+ + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockSimpleHtmlResponse) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'program', 'computer program') + + expect(result.success).toBe(true) + expect(result.data!.word).toBe('program') + // 由于这个测试用例的HTML结构简单,没有完整的发音信息,所以跳过phonetic检查 + expect(result.data!.definitions).toHaveLength(1) + expect(result.data!.definitions[0]).toEqual({ + partOfSpeech: 'n.', + meaning: '程序,节目' + }) + + // 验证URL编码处理 + expect(mockFetch).toHaveBeenCalledWith( + 'https://dict.eudic.net/dicts/MiniDictSearch2?word=program&context=computer+program', + expect.any(Object) + ) + }) + + it('应该使用备用解析策略处理复杂HTML结构', async () => { + const mockComplexHtmlResponse = ` + + +
/test/
+
+
    +
  • v. 测试,检验
  • +
  • n. 测试,试验
  • +
  • adj. 测试的
  • +
+
+ + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockComplexHtmlResponse) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'test') + + expect(result.success).toBe(true) + expect(result.data!.definitions).toHaveLength(3) + expect(result.data!.definitions[0].partOfSpeech).toBe('v.') + expect(result.data!.definitions[0].meaning).toBe('测试,检验') + }) + + it('应该正确解析词性在标签中的释义格式 - need 示例', async () => { + const mockNeedHtmlResponse = ` + + + + + + +
+
    +
  1. v. 需要;必须
  2. +
  3. modal v. 必须
  4. +
  5. n. 需要,需求
  6. +
  7. 责任,必要
  8. +
  9. 需要的东西
  10. +
  11. 贫穷;困窘
  12. +
+
+ 时 态: + needed,needing,needs
+
+
+ + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockNeedHtmlResponse) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'need') + + expect(result.success).toBe(true) + expect(result.data!.word).toBe('need') + expect(result.data!.definitions).toHaveLength(6) + + // 验证带词性的释义 + expect(result.data!.definitions[0]).toEqual({ + partOfSpeech: 'v.', + meaning: '需要;必须' + }) + expect(result.data!.definitions[1]).toEqual({ + partOfSpeech: 'modal v.', + meaning: '必须' + }) + expect(result.data!.definitions[2]).toEqual({ + partOfSpeech: 'n.', + meaning: '需要,需求' + }) + + // 验证不带词性的释义 + expect(result.data!.definitions[3]).toEqual({ + meaning: '责任,必要' + }) + expect(result.data!.definitions[4]).toEqual({ + meaning: '需要的东西' + }) + expect(result.data!.definitions[5]).toEqual({ + meaning: '贫穷;困窘' + }) + }) + + it('应该正确处理混合词性格式(标签和纯文本)', async () => { + const mockMixedFormatHtml = ` + + +
+
    +
  1. adj. 快速的
  2. +
  3. adv. 快速地
  4. +
  5. n. 快速
  6. +
  7. 迅速的动作
  8. +
+
+ + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockMixedFormatHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'fast') + + expect(result.success).toBe(true) + expect(result.data!.definitions).toHaveLength(4) + + expect(result.data!.definitions[0]).toEqual({ + partOfSpeech: 'adj.', + meaning: '快速的' + }) + expect(result.data!.definitions[1]).toEqual({ + partOfSpeech: 'adv.', + meaning: '快速地' + }) + expect(result.data!.definitions[2]).toEqual({ + partOfSpeech: 'n.', + meaning: '快速' + }) + expect(result.data!.definitions[3]).toEqual({ + meaning: '迅速的动作' + }) + }) + + it('应该正确处理复杂词性格式(多词组合)', async () => { + const mockComplexPartOfSpeechHtml = ` + + +
+
    +
  1. modal v. 应该,必须
  2. +
  3. aux. v. 帮助动词
  4. +
  5. prep. phr. 介词短语
  6. +
+
+ + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockComplexPartOfSpeechHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'should') + + expect(result.success).toBe(true) + expect(result.data!.definitions).toHaveLength(3) + + expect(result.data!.definitions[0]).toEqual({ + partOfSpeech: 'modal v.', + meaning: '应该,必须' + }) + expect(result.data!.definitions[1]).toEqual({ + partOfSpeech: 'aux. v.', + meaning: '帮助动词' + }) + expect(result.data!.definitions[2]).toEqual({ + partOfSpeech: 'prep. phr.', + meaning: '介词短语' + }) + }) + + it('应该正确解析例句和翻译', async () => { + const mockHtmlWithExamplesAndTranslations = ` + +
+
  • v. 学习
  • +
    +
    I learn English every day.
    +
    Learning is fun.
    +
    我每天学习英语。
    +
    学习很有趣。
    + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockHtmlWithExamplesAndTranslations) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'learn') + + expect(result.success).toBe(true) + expect(result.data!.examples).toEqual(['I learn English every day.', 'Learning is fun.']) + expect(result.data!.translations).toEqual(['我每天学习英语。', '学习很有趣。']) + }) + + it('应该正确处理没有区分英美音的发音信息 - crystal 示例', async () => { + const mockCrystalHtmlResponse = ` + + + + + + +
    + + +
    您是否要查找:crystal
    +
    +
    +
      +
    1. n. 水晶;晶体
    2. +
    3. adj. 水晶的;透明的
    4. +
    +
    + + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockCrystalHtmlResponse) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'crystal') + + expect(result.success).toBe(true) + expect(result.data!.word).toBe('crystal') + expect(result.data!.pronunciations).toBeDefined() + expect(result.data!.pronunciations!.length).toBe(1) + + // 验证发音信息 - 类型应该为 null(未知) + const pronunciation = result.data!.pronunciations![0] + expect(pronunciation.type).toBe(null) // 未知发音类型 + expect(pronunciation.phonetic).toBe("/'krɪstl/") + expect(pronunciation.voiceParams).toBe('langid=en&txt=QYNY3J5c3RhbHM%3d') // 应该有语音参数 + + // 注意:音频URL可能为undefined,因为parseVoiceParams可能无法解析所有必要的参数 + // 在这个测试案例中,voicename参数缺失,所以audioUrl会是undefined + // 这是正常的,因为需要langid、voicename和txt三个参数才能构建音频URL + + expect(result.data!.definitions).toHaveLength(2) + expect(result.data!.definitions[0]).toEqual({ + partOfSpeech: 'n.', + meaning: '水晶;晶体' + }) + expect(result.data!.definitions[1]).toEqual({ + partOfSpeech: 'adj.', + meaning: '水晶的;透明的' + }) + }) + + it('应该正确处理没有发音信息的普通音标', async () => { + const mockSimplePhoneticHtml = ` + + + /ˈsɪmpəl/ +
    +
  • adj. 简单的
  • +
    + + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockSimplePhoneticHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'simple') + + expect(result.success).toBe(true) + expect(result.data!.pronunciations).toBeDefined() + expect(result.data!.pronunciations!.length).toBe(1) + + // 验证发音信息 - 类型应该为 null(未知),没有音频 + const pronunciation = result.data!.pronunciations![0] + expect(pronunciation.type).toBe(null) // 未知发音类型 + expect(pronunciation.phonetic).toBe('/ˈsɪmpəl/') + expect(pronunciation.audioUrl).toBeUndefined() // 没有音频URL + expect(pronunciation.voiceParams).toBeUndefined() + }) + }) + + describe('❌ 错误处理 - 健壮性测试', () => { + it('应该处理HTTP错误', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found' + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'nonexistent') + + expect(result.success).toBe(false) + expect(result.error).toBe('HTTP 404: Not Found') + expect(result.data).toBeUndefined() + }) + + it('应该处理网络错误', async () => { + const networkError = new Error('Network connection failed') + mockFetch.mockRejectedValue(networkError) + + const result = await dictionaryService.queryEudic(mockEvent, 'hello') + + expect(result.success).toBe(false) + expect(result.error).toBe('Network connection failed') + }) + + it('应该处理非Error类型的异常', async () => { + mockFetch.mockRejectedValue('String error') + + const result = await dictionaryService.queryEudic(mockEvent, 'hello') + + expect(result.success).toBe(false) + expect(result.error).toBe('网络错误') + }) + + it('应该处理无法解析出释义的情况', async () => { + const mockEmptyHtmlResponse = ` + + +
    No useful content here
    + + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockEmptyHtmlResponse) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'unknown') + + expect(result.success).toBe(false) + expect(result.error).toBe('未能从HTML中解析出任何释义') + }) + }) + + describe('🛡️ HTML解析健壮性', () => { + it('应该处理带有特殊字符的内容', async () => { + const mockSpecialCharHtml = ` + + +
    /ˈspɛʃəl/
    +
    +
      +
    1. adj. 特殊的;特别的;"专门的
    2. +
    +
    + + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockSpecialCharHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'special') + + expect(result.success).toBe(true) + expect(result.data!.definitions[0].meaning).toContain('特殊的;特别的;"专门的') + }) + + it('应该正确处理音标格式变化', async () => { + const mockPhoneticVariations = ` + + + UK /juːˈnaɪtɪd/ +
    +
  • adj. 联合的,统一的
  • +
    + + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockPhoneticVariations) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'united') + + expect(result.success).toBe(true) + // 由于这个测试用例的HTML结构简单,没有完整的发音信息,所以跳过phonetic检查 + }) + + it('应该限制备用解析策略的结果数量', async () => { + const mockManyItemsHtml = ` + + +
      +
    • 第一个中文释义
    • +
    • 第二个中文释义
    • +
    • 第三个中文释义
    • +
    • 第四个中文释义
    • +
    • 第五个中文释义
    • +
    • 第六个中文释义
    • +
    • 第七个中文释义
    • +
    + + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockManyItemsHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'many') + + expect(result.success).toBe(true) + expect(result.data!.definitions.length).toBe(5) // 应该被限制为最多5个 + }) + + it('应该忽略过长或过短的无关内容', async () => { + const mockNoisyHtml = ` + + +
      +
    • a
    • +
    • 这是一个正常长度的中文释义
    • +
    • ${'很'.repeat(250)}
    • +
    • 另一个正常的释义
    • +
    + + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockNoisyHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'noisy') + + expect(result.success).toBe(true) + expect(result.data!.definitions.length).toBe(2) + expect( + result.data!.definitions.every( + (def) => def.meaning.length >= 3 && def.meaning.length < 200 + ) + ).toBe(true) + }) + }) + + describe('⚙️ 参数处理', () => { + it('应该正确处理URL编码', async () => { + const mockHtmlResponse = ` + +
    +
  • 测试内容
  • +
    + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockHtmlResponse) + }) + + await dictionaryService.queryEudic(mockEvent, 'hello world', 'test context') + + const expectedUrl = + 'https://dict.eudic.net/dicts/MiniDictSearch2?word=hello+world&context=test+context' + expect(mockFetch).toHaveBeenCalledWith(expectedUrl, expect.any(Object)) + }) + + it('应该在context为空时使用word作为默认context', async () => { + const mockHtmlResponse = ` + +
    +
  • 测试内容
  • +
    + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockHtmlResponse) + }) + + await dictionaryService.queryEudic(mockEvent, 'test') + + const expectedUrl = 'https://dict.eudic.net/dicts/MiniDictSearch2?word=test&context=test' + expect(mockFetch).toHaveBeenCalledWith(expectedUrl, expect.any(Object)) + }) + }) + + describe('🔄 边界情况和压力测试', () => { + it('应该处理空字符串查询', async () => { + const mockHtmlResponse = `
    Empty query
    ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockHtmlResponse) + }) + + const result = await dictionaryService.queryEudic(mockEvent, '') + + expect(result.success).toBe(false) + expect(result.error).toBe('未能从HTML中解析出任何释义') + }) + + it('应该处理大量HTML内容', async () => { + const largeMockHtml = ` + +
    /lærdʒ/
    +
    +
  • adj. 大的
  • +
    + ${'
    无关内容
    '.repeat(1000)} + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(largeMockHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'large') + + expect(result.success).toBe(true) + expect(result.data!.definitions[0].meaning).toBe('大的') + }) + + it('应该处理畸形HTML', async () => { + const malformedHtml = ` + +
    /test/
    +
    +
  • adj. 测试的 +
  • + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(malformedHtml) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'malformed') + + // 应该能处理畸形HTML而不抛出异常 + expect(result.success).toBe(true) + }) + + it('应该处理没有例句和翻译的情况', async () => { + const mockHtmlWithoutExamplesAndTranslations = ` + +
    +
  • n. 单词
  • +
    + + ` + + mockFetch.mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockHtmlWithoutExamplesAndTranslations) + }) + + const result = await dictionaryService.queryEudic(mockEvent, 'word') + + expect(result.success).toBe(true) + expect(result.data!.examples).toBeUndefined() + expect(result.data!.translations).toBeUndefined() + }) + }) + }) +}) diff --git a/src/main/__tests__/ipc.database.test.ts b/src/main/__tests__/ipc.database.test.ts index 1ac4f324..4793ad42 100644 --- a/src/main/__tests__/ipc.database.test.ts +++ b/src/main/__tests__/ipc.database.test.ts @@ -166,7 +166,17 @@ vi.mock('../services/FFmpegService', () => ({ getVideoInfo: vi.fn(), transcodeVideo: vi.fn(), cancelTranscode: vi.fn(), - getFFmpegPath: vi.fn() + getFFmpegPath: vi.fn(), + getDownloadService: vi.fn(() => ({ + checkFFmpegExists: vi.fn(), + getFFmpegVersion: vi.fn(), + downloadFFmpeg: vi.fn(), + getDownloadProgress: vi.fn(), + cancelDownload: vi.fn(), + removeFFmpeg: vi.fn(), + getAllSupportedVersions: vi.fn(), + cleanupTempFiles: vi.fn() + })) })) })) diff --git a/src/main/db/dao/__tests__/VideoLibraryDAO.test.ts b/src/main/db/dao/__tests__/VideoLibraryDAO.test.ts index 877ef810..4d8f6300 100644 --- a/src/main/db/dao/__tests__/VideoLibraryDAO.test.ts +++ b/src/main/db/dao/__tests__/VideoLibraryDAO.test.ts @@ -12,13 +12,17 @@ describe('VideoLibraryDAO', () => { let dao: VideoLibraryDAO let mockKysely: any + // Fixed timestamps to avoid race conditions between Date.now() calls + const FIXED_NOW = 1758017326806 + const FIXED_FIRST_PLAYED_AT = FIXED_NOW - 86400000 // 1 day ago + // Mock data for insertion (uses boolean, will be converted to number by schema) const mockVideoRecordFromDBForInsert = { fileId: 'test-file-id-1', currentTime: 120.5, duration: 3600, - playedAt: Date.now(), - firstPlayedAt: Date.now() - 86400000, // 1 day ago + playedAt: FIXED_NOW, + firstPlayedAt: FIXED_FIRST_PLAYED_AT, playCount: 3, isFinished: false, // Boolean for insert isFavorite: true, // Boolean for insert @@ -30,8 +34,8 @@ describe('VideoLibraryDAO', () => { fileId: 'test-file-id-1', currentTime: 120.5, duration: 3600, - playedAt: Date.now(), - firstPlayedAt: Date.now() - 86400000, // 1 day ago + playedAt: FIXED_NOW, + firstPlayedAt: FIXED_FIRST_PLAYED_AT, playCount: 3, isFinished: 0, // Number from DB (0 for false) isFavorite: 1, // Number from DB (1 for true) @@ -43,8 +47,8 @@ describe('VideoLibraryDAO', () => { fileId: 'test-file-id-1', currentTime: 120.5, duration: 3600, - playedAt: Date.now(), - firstPlayedAt: Date.now() - 86400000, // 1 day ago + playedAt: FIXED_NOW, + firstPlayedAt: FIXED_FIRST_PLAYED_AT, playCount: 3, isFinished: false, // Converted to boolean isFavorite: true, // Converted to boolean @@ -155,7 +159,8 @@ describe('VideoLibraryDAO', () => { describe('getRecentlyPlayed', () => { it('应该获取最近播放的视频(默认限制)', async () => { - const now = Date.now() + // Use fixed timestamps to avoid race conditions + const now = FIXED_NOW // Mock database returns number format const mockDBResults = [ { id: 1, ...mockVideoRecordFromDBFromDB, playedAt: now }, @@ -262,7 +267,7 @@ describe('VideoLibraryDAO', () => { it('应该更新播放进度', async () => { const mockResult = { numUpdatedRows: 1 } mockKysely.execute.mockResolvedValue(mockResult) - const now = Date.now() + const now = FIXED_NOW vi.spyOn(Date, 'now').mockReturnValue(now) const result = await dao.updatePlayProgress(1, 150.5) @@ -279,7 +284,7 @@ describe('VideoLibraryDAO', () => { it('应该更新播放进度并设置完成状态', async () => { const mockResult = { numUpdatedRows: 1 } mockKysely.execute.mockResolvedValue(mockResult) - const now = Date.now() + const now = FIXED_NOW vi.spyOn(Date, 'now').mockReturnValue(now) const result = await dao.updatePlayProgress(1, 3600, true) @@ -295,7 +300,7 @@ describe('VideoLibraryDAO', () => { it('应该更新播放进度但不改变完成状态', async () => { const mockResult = { numUpdatedRows: 1 } mockKysely.execute.mockResolvedValue(mockResult) - const now = Date.now() + const now = FIXED_NOW vi.spyOn(Date, 'now').mockReturnValue(now) const result = await dao.updatePlayProgress(1, 150.5, undefined) diff --git a/src/main/db/migrations/20250829175200_init.js b/src/main/db/migrations/20250829175200_init.js deleted file mode 100644 index f2026c2f..00000000 --- a/src/main/db/migrations/20250829175200_init.js +++ /dev/null @@ -1,130 +0,0 @@ -const { sql } = require('kysely') - -/** - * Apply the initial database schema migration. - * - * Creates the tables `files`, `videoLibrary`, and `subtitleLibrary` (if not exists) - * and their associated indices. Designed to be run as the migration "up" step. - */ -async function up(db) { - // 创建文件表 - await db.schema - .createTable('files') - .ifNotExists() - .addColumn('id', 'text', (col) => col.primaryKey().notNull()) - .addColumn('name', 'text', (col) => col.notNull()) - .addColumn('origin_name', 'text', (col) => col.notNull()) - .addColumn('path', 'text', (col) => col.notNull().unique()) - .addColumn('size', 'integer', (col) => col.notNull()) - .addColumn('ext', 'text', (col) => col.notNull()) - .addColumn('type', 'text', (col) => col.notNull()) - .addColumn('created_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) - .execute() - - // 创建文件表索引 - await db.schema.createIndex('idx_files_name').ifNotExists().on('files').column('name').execute() - await db.schema.createIndex('idx_files_type').ifNotExists().on('files').column('type').execute() - await db.schema - .createIndex('idx_files_created_at') - .ifNotExists() - .on('files') - .column('created_at') - .execute() - await db.schema.createIndex('idx_files_ext').ifNotExists().on('files').column('ext').execute() - - // 创建视频库表 - await db.schema - .createTable('videoLibrary') - .ifNotExists() - .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement()) - .addColumn('fileId', 'text', (col) => col.notNull()) - .addColumn('currentTime', 'real', (col) => col.notNull().defaultTo(0)) - .addColumn('duration', 'real', (col) => col.notNull().defaultTo(0)) - .addColumn('playedAt', 'integer', (col) => col.notNull()) - .addColumn('firstPlayedAt', 'integer', (col) => col.notNull()) - .addColumn('playCount', 'integer', (col) => col.notNull().defaultTo(0)) - .addColumn('isFinished', 'integer', (col) => col.notNull().defaultTo(0)) - .addColumn('isFavorite', 'integer', (col) => col.notNull().defaultTo(0)) - .addColumn('thumbnailPath', 'text') - .execute() - - // 创建视频库表索引 - await db.schema - .createIndex('idx_videoLibrary_fileId_playedAt') - .ifNotExists() - .on('videoLibrary') - .columns(['fileId', 'playedAt']) - .execute() - await db.schema - .createIndex('idx_videoLibrary_playedAt') - .ifNotExists() - .on('videoLibrary') - .column('playedAt') - .execute() - await db.schema - .createIndex('idx_videoLibrary_playCount') - .ifNotExists() - .on('videoLibrary') - .column('playCount') - .execute() - await db.schema - .createIndex('idx_videoLibrary_isFavorite') - .ifNotExists() - .on('videoLibrary') - .column('isFavorite') - .execute() - - // 创建字幕库表 - await db.schema - .createTable('subtitleLibrary') - .ifNotExists() - .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement()) - .addColumn('videoId', 'integer', (col) => col.notNull()) - .addColumn('filePath', 'text', (col) => col.notNull()) - .addColumn('created_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) - .execute() - - // 创建字幕库表索引 - await db.schema - .createIndex('idx_subtitleLibrary_videoId_filePath') - .ifNotExists() - .on('subtitleLibrary') - .columns(['videoId', 'filePath']) - .execute() - await db.schema - .createIndex('idx_subtitleLibrary_created_at') - .ifNotExists() - .on('subtitleLibrary') - .column('created_at') - .execute() -} - -/** - * Reverts the migration by removing created indices and tables. - * - * Drops the migration's indices (using `ifExists`) in a safe order, then drops - * the tables `subtitleLibrary`, `videoLibrary`, and `files` (also using `ifExists`). - * The operation is idempotent and intended to fully revert the schema changes made by `up`. - * - * @returns {Promise} Resolves when all drop statements have completed. - */ -async function down(db) { - // 删除所有索引 - await db.schema.dropIndex('idx_subtitleLibrary_created_at').ifExists().execute() - await db.schema.dropIndex('idx_subtitleLibrary_videoId_filePath').ifExists().execute() - await db.schema.dropIndex('idx_videoLibrary_isFavorite').ifExists().execute() - await db.schema.dropIndex('idx_videoLibrary_playCount').ifExists().execute() - await db.schema.dropIndex('idx_videoLibrary_playedAt').ifExists().execute() - await db.schema.dropIndex('idx_videoLibrary_fileId_playedAt').ifExists().execute() - await db.schema.dropIndex('idx_files_ext').ifExists().execute() - await db.schema.dropIndex('idx_files_created_at').ifExists().execute() - await db.schema.dropIndex('idx_files_type').ifExists().execute() - await db.schema.dropIndex('idx_files_name').ifExists().execute() - - // 删除所有表 - await db.schema.dropTable('subtitleLibrary').ifExists().execute() - await db.schema.dropTable('videoLibrary').ifExists().execute() - await db.schema.dropTable('files').ifExists().execute() -} - -module.exports = { up, down } diff --git a/src/main/db/migrations/20250831062000_add_player_settings.js b/src/main/db/migrations/20250831062000_add_player_settings.js deleted file mode 100644 index 70a33df5..00000000 --- a/src/main/db/migrations/20250831062000_add_player_settings.js +++ /dev/null @@ -1,66 +0,0 @@ -const { sql } = require('kysely') - -/** - * Migration: Add player settings table for per-video configuration - * - * Creates the `playerSettings` table to store individual player configurations - * for each video in the library, replacing the global player settings approach. - */ -async function up(db) { - // 创建播放器设置表 - await db.schema - .createTable('playerSettings') - .ifNotExists() - .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement()) - .addColumn('videoId', 'integer', (col) => - col.notNull().references('videoLibrary.id').onDelete('cascade') - ) - .addColumn('playbackRate', 'real', (col) => col.notNull().defaultTo(1.0)) - .addColumn('volume', 'real', (col) => col.notNull().defaultTo(1.0)) - .addColumn('muted', 'integer', (col) => col.notNull().defaultTo(0)) - .addColumn('loopSettings', 'text') // JSON: {enabled, count, mode, remainingCount} - .addColumn('autoPauseSettings', 'text') // JSON: {enabled, pauseOnSubtitleEnd, resumeEnabled, resumeDelay} - .addColumn('subtitleOverlaySettings', 'text') // JSON: subtitleOverlay完整配置 - .addColumn('created_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) - .addColumn('updated_at', 'integer', (col) => col.notNull().defaultTo(sql`(unixepoch())`)) - .execute() - - // 创建索引 - await db.schema - .createIndex('idx_playerSettings_videoId') - .ifNotExists() - .on('playerSettings') - .column('videoId') - .execute() - - await db.schema - .createIndex('idx_playerSettings_updated_at') - .ifNotExists() - .on('playerSettings') - .column('updated_at') - .execute() - - // 创建唯一约束确保每个视频只有一个设置记录 - await db.schema - .createIndex('idx_playerSettings_videoId_unique') - .ifNotExists() - .on('playerSettings') - .column('videoId') - .unique() - .execute() -} - -/** - * Reverts the migration by removing the playerSettings table and its indices. - */ -async function down(db) { - // 删除索引 - await db.schema.dropIndex('idx_playerSettings_videoId_unique').ifExists().execute() - await db.schema.dropIndex('idx_playerSettings_updated_at').ifExists().execute() - await db.schema.dropIndex('idx_playerSettings_videoId').ifExists().execute() - - // 删除表(外键约束会自动删除) - await db.schema.dropTable('playerSettings').ifExists().execute() -} - -module.exports = { up, down } diff --git a/src/main/db/schemas/player-settings.ts b/src/main/db/schemas/player-settings.ts index 323d5476..1096d954 100644 --- a/src/main/db/schemas/player-settings.ts +++ b/src/main/db/schemas/player-settings.ts @@ -2,35 +2,13 @@ import { z } from 'zod' import { BooleanToSqlSchema, + JsonStringSchema, PositiveIntegerSchema, SqlBooleanSchema, SqlTimestampSchema, TimestampToDateSchema } from './transforms' -/** - * PlayerSettings 表的 Zod Schema 定义 - */ - -/** - * JSON 字符串验证器 - * 验证 JSON 字符串格式并允许 null - */ -const JsonStringSchema = z - .string() - .refine( - (str) => { - try { - JSON.parse(str) - return true - } catch { - return false - } - }, - { message: 'Invalid JSON string' } - ) - .nullable() - /** * 播放速度验证器 (0.25 - 3.0) */ @@ -48,6 +26,7 @@ const VolumeSchema = z.number().min(0).max(1) export const PlayerSettingsInsertSchema = z.object({ videoId: PositiveIntegerSchema, playbackRate: PlaybackRateSchema.default(1.0), + favoriteRates: JsonStringSchema.default(JSON.stringify([])), volume: VolumeSchema.default(1.0), muted: BooleanToSqlSchema.default(false), loopSettings: JsonStringSchema.optional(), @@ -60,6 +39,7 @@ export const PlayerSettingsInsertSchema = z.object({ */ export const PlayerSettingsUpdateSchema = z.object({ playbackRate: PlaybackRateSchema.optional(), + favoriteRates: JsonStringSchema.optional(), volume: VolumeSchema.optional(), muted: BooleanToSqlSchema.optional(), loopSettings: JsonStringSchema.optional(), @@ -76,6 +56,7 @@ export const PlayerSettingsSelectSchema = z.object({ id: PositiveIntegerSchema, videoId: PositiveIntegerSchema, playbackRate: PlaybackRateSchema, + favoriteRates: JsonStringSchema, volume: VolumeSchema, muted: SqlBooleanSchema, loopSettings: z.string().nullable(), diff --git a/src/main/db/schemas/transforms.ts b/src/main/db/schemas/transforms.ts index d2b044fc..bcb4b1fd 100644 --- a/src/main/db/schemas/transforms.ts +++ b/src/main/db/schemas/transforms.ts @@ -209,3 +209,26 @@ export function withDataTransforms>(schema: z.ZodS fromSelect: (data: unknown) => schema.parse(data) } } + +/** + * PlayerSettings 表的 Zod Schema 定义 + */ + +/** + * JSON 字符串验证器 + * 验证 JSON 字符串格式并允许 null + */ +export const JsonStringSchema = z + .string() + .refine( + (str) => { + try { + JSON.parse(str) + return true + } catch { + return false + } + }, + { message: 'Invalid JSON string' } + ) + .nullable() diff --git a/src/main/index.ts b/src/main/index.ts index d7202111..5ceff48d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,12 +15,21 @@ import { isDev, isLinux, isWin, isWSL } from './constant' import { registerIpc } from './ipc' import { configManager } from './services/ConfigManager' import { registerShortcuts } from './services/ShortcutService' +import { sentryService } from './services/SentryService' import { TrayService } from './services/TrayService' import { windowService } from './services/WindowService' import { initDatabase } from './db/init' const logger = loggerService.withContext('MainEntry') +// 初始化 Sentry(必须在 app.whenReady() 之前) +// 同步初始化,确保在 ready 事件前完成 +try { + sentryService.init() +} catch (error) { + logger.warn('Failed to initialize Sentry:', { error }) +} + /** * Disable hardware acceleration if setting is enabled or in WSL environment */ diff --git a/src/main/ipc.ts b/src/main/ipc.ts index c9e4753c..26ff54ef 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -24,6 +24,7 @@ import DictionaryService from './services/DictionaryService' import FFmpegService from './services/FFmpegService' import FileStorage from './services/FileStorage' import { loggerService } from './services/LoggerService' +import MediaParserService from './services/MediaParserService' import NotificationService from './services/NotificationService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { themeService } from './services/ThemeService' @@ -36,6 +37,7 @@ const logger = loggerService.withContext('IPC') const fileManager = new FileStorage() const dictionaryService = new DictionaryService() const ffmpegService = new FFmpegService() +const mediaParserService = new MediaParserService() /** * Register all IPC handlers used by the main process. @@ -382,6 +384,43 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { win && win.webContents.toggleDevTools() }) + // 全屏相关 IPC 处理器 / Fullscreen-related IPC handlers + ipcMain.handle(IpcChannel.Window_IsFullScreen, (e) => { + const win = BrowserWindow.fromWebContents(e.sender) + return win ? win.isFullScreen() : false + }) + + ipcMain.handle(IpcChannel.Window_EnterFullScreen, (e) => { + const win = BrowserWindow.fromWebContents(e.sender) + if (win && !win.isFullScreen()) { + win.setFullScreen(true) + // 通知渲染进程全屏状态已改变 + win.webContents.send(IpcChannel.FullscreenStatusChanged, true) + logger.info('Window entered fullscreen') + } + }) + + ipcMain.handle(IpcChannel.Window_ExitFullScreen, (e) => { + const win = BrowserWindow.fromWebContents(e.sender) + if (win && win.isFullScreen()) { + win.setFullScreen(false) + // 通知渲染进程全屏状态已改变 + win.webContents.send(IpcChannel.FullscreenStatusChanged, false) + logger.info('Window exited fullscreen') + } + }) + + ipcMain.handle(IpcChannel.Window_ToggleFullScreen, (e) => { + const win = BrowserWindow.fromWebContents(e.sender) + if (win) { + const isCurrentlyFullscreen = win.isFullScreen() + win.setFullScreen(!isCurrentlyFullscreen) + // 通知渲染进程全屏状态已改变 + win.webContents.send(IpcChannel.FullscreenStatusChanged, !isCurrentlyFullscreen) + logger.info(`Window fullscreen toggled to: ${!isCurrentlyFullscreen}`) + } + }) + // file ipcMain.handle(IpcChannel.File_Open, fileManager.open.bind(fileManager)) ipcMain.handle(IpcChannel.File_OpenPath, fileManager.openPath.bind(fileManager)) @@ -423,23 +462,100 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.Ffmpeg_GetVersion, async () => { return await ffmpegService.getFFmpegVersion() }) - ipcMain.handle(IpcChannel.Ffmpeg_Download, async (_, onProgress?: (progress: number) => void) => { - return await ffmpegService.downloadFFmpeg(onProgress) - }) ipcMain.handle(IpcChannel.Ffmpeg_GetVideoInfo, async (_, inputPath: string) => { return await ffmpegService.getVideoInfo(inputPath) }) + ipcMain.handle(IpcChannel.Ffmpeg_GetPath, async () => { + return ffmpegService.getFFmpegPath() + }) + ipcMain.handle(IpcChannel.Ffmpeg_Warmup, async () => { + return await ffmpegService.warmupFFmpeg() + }) + ipcMain.handle(IpcChannel.Ffmpeg_GetWarmupStatus, async () => { + return FFmpegService.getWarmupStatus() + }) + ipcMain.handle(IpcChannel.Ffmpeg_GetInfo, async () => { + return ffmpegService.getFFmpegInfo() + }) + ipcMain.handle(IpcChannel.Ffmpeg_AutoDetectAndDownload, async () => { + return await ffmpegService.autoDetectAndDownload() + }) + + // FFmpeg 下载服务 + const ffmpegDownloadService = ffmpegService.getDownloadService() + ipcMain.handle( + IpcChannel.FfmpegDownload_CheckExists, + async (_, platform?: string, arch?: string) => { + return ffmpegDownloadService.checkFFmpegExists(platform as any, arch as any) + } + ) ipcMain.handle( - IpcChannel.Ffmpeg_Transcode, - async (_, inputPath: string, outputPath: string, options: any) => { - return await ffmpegService.transcodeVideo(inputPath, outputPath, options) + IpcChannel.FfmpegDownload_GetVersion, + async (_, platform?: string, arch?: string) => { + return ffmpegDownloadService.getFFmpegVersion(platform as any, arch as any) } ) - ipcMain.handle(IpcChannel.Ffmpeg_CancelTranscode, () => { - return ffmpegService.cancelTranscode() + ipcMain.handle( + IpcChannel.FfmpegDownload_Download, + async (_, platform?: string, arch?: string) => { + return await ffmpegDownloadService.downloadFFmpeg(platform as any, arch as any) + } + ) + ipcMain.handle( + IpcChannel.FfmpegDownload_GetProgress, + async (_, platform?: string, arch?: string) => { + return ffmpegDownloadService.getDownloadProgress(platform as any, arch as any) + } + ) + ipcMain.handle(IpcChannel.FfmpegDownload_Cancel, async (_, platform?: string, arch?: string) => { + return ffmpegDownloadService.cancelDownload(platform as any, arch as any) }) - ipcMain.handle(IpcChannel.Ffmpeg_GetPath, async () => { - return ffmpegService.getFFmpegPath() + ipcMain.handle(IpcChannel.FfmpegDownload_Remove, async (_, platform?: string, arch?: string) => { + return ffmpegDownloadService.removeFFmpeg(platform as any, arch as any) + }) + ipcMain.handle(IpcChannel.FfmpegDownload_GetAllVersions, async () => { + return ffmpegDownloadService.getAllSupportedVersions() + }) + ipcMain.handle(IpcChannel.FfmpegDownload_CleanupTemp, async () => { + return ffmpegDownloadService.cleanupTempFiles() + }) + + // MediaParser (Remotion) + ipcMain.handle(IpcChannel.MediaInfo_CheckExists, async () => { + return await mediaParserService.checkExists() + }) + ipcMain.handle(IpcChannel.MediaInfo_GetVersion, async () => { + return await mediaParserService.getVersion() + }) + ipcMain.handle(IpcChannel.MediaInfo_GetVideoInfo, async (_, inputPath: string) => { + return await mediaParserService.getVideoInfo(inputPath) + }) + ipcMain.handle( + IpcChannel.MediaInfo_GetVideoInfoWithStrategy, + async ( + _, + inputPath: string, + strategy: + | 'remotion-first' + | 'ffmpeg-first' + | 'remotion-only' + | 'ffmpeg-only' = 'remotion-first', + timeoutMs: number = 10000 + ) => { + return await mediaParserService.getVideoInfoWithStrategy(inputPath, strategy, timeoutMs) + } + ) + + // 文件系统相关 IPC 处理程序 / File system-related IPC handlers + ipcMain.handle(IpcChannel.Fs_CheckFileExists, async (_, filePath: string) => { + try { + const exists = fs.existsSync(filePath) + logger.debug('检查文件存在性', { filePath, exists }) + return exists + } catch (error) { + logger.error('检查文件存在性时出错', { filePath, error }) + return false + } }) // shortcuts diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index 8085eaad..5b52979f 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -79,7 +79,7 @@ export default class AppUpdater { try { logger.info('get pre release version from github', channel) const responses = await fetch( - 'https://api.github.com/repos/mkdir700/EchoPlayer/releases?per_page=8', + 'https://api.github.com/repos/mkdir700/EchoPlayer/releases?per_page=20', { headers: { Accept: 'application/vnd.github+json', @@ -89,12 +89,25 @@ export default class AppUpdater { } ) const data = (await responses.json()) as GithubReleaseInfo[] - const release: GithubReleaseInfo | undefined = data.find((item: GithubReleaseInfo) => { + + // 过滤出匹配渠道的预发布版本 + const matchingReleases = data.filter((item: GithubReleaseInfo) => { return item.prerelease && item.tag_name.includes(`-${channel}.`) }) - logger.info('release info', release) - return release ? release : null + if (matchingReleases.length === 0) { + logger.info('No matching pre-release found for channel:', channel) + return null + } + + // 按发布时间排序,获取最新的版本 + const release = matchingReleases.sort( + (a, b) => + new Date(b.published_at || '').getTime() - new Date(a.published_at || '').getTime() + )[0] + + logger.info('Latest release info for channel', channel, ':', release) + return release } catch (error) { logger.error('Failed to get latest not draft version from github:', error) return null @@ -162,29 +175,45 @@ export default class AppUpdater { release: GithubReleaseInfo | null = null ) { logger.info('Setting feed URL - testPlan:', testPlan) + + // 获取IP地址归属地 + const ipCountry = await this._getIpCountry() + logger.info('Detected IP country:', ipCountry) + const isChinaUser = ipCountry.toLowerCase() === 'cn' + if (channel === UpgradeChannel.LATEST) { this.autoUpdater.channel = UpgradeChannel.LATEST - this.autoUpdater.setFeedURL(FeedUrl.GITHUB_LATEST) - logger.info('Using GitHub latest releases for test plan') + if (isChinaUser) { + this.autoUpdater.setFeedURL(FeedUrl.PRODUCTION) + logger.info('Using production releases for CN user') + } else { + this.autoUpdater.setFeedURL(FeedUrl.GITHUB_LATEST) + logger.info('Using GitHub latest releases for test plan') + } return } if (testPlan && release) { - const preReleaseUrl = `https://github.com/mkdir700/EchoPlayer/releases/download/${release.tag_name}` - if (preReleaseUrl) { - this.autoUpdater.setFeedURL(preReleaseUrl) - // Keep channel as 'latest' because GitHub releases only have latest-mac.yml, not alpha-mac.yml + if (isChinaUser) { + // 为中国用户使用对应的预发布渠道 + const chineseFeedUrl = channel === UpgradeChannel.ALPHA ? FeedUrl.CN_ALPHA : FeedUrl.CN_BETA + this.autoUpdater.setFeedURL(chineseFeedUrl) + this.autoUpdater.channel = channel + logger.info(`Using Chinese pre-release URL: ${chineseFeedUrl} with channel: ${channel}`) + } else { + const preReleaseUrl = `https://github.com/mkdir700/EchoPlayer/releases/download/${release.tag_name}` + if (preReleaseUrl) { + this.autoUpdater.setFeedURL(preReleaseUrl) + this.autoUpdater.channel = channel + logger.info(`Using pre-release URL: ${preReleaseUrl} with channel: ${channel}`) + return + } + + // if no prerelease url, use lowest prerelease version to avoid error + logger.warn('No prerelease URL found, falling back to lowest prerelease version') + this.autoUpdater.setFeedURL(FeedUrl.PRERELEASE_LOWEST) this.autoUpdater.channel = UpgradeChannel.LATEST - logger.info( - `Using pre-release URL: ${preReleaseUrl} with channel: ${UpgradeChannel.LATEST}` - ) - return } - - // if no prerelease url, use lowest prerelease version to avoid error - logger.warn('No prerelease URL found, falling back to lowest prerelease version') - this.autoUpdater.setFeedURL(FeedUrl.PRERELEASE_LOWEST) - this.autoUpdater.channel = UpgradeChannel.LATEST return } @@ -193,7 +222,6 @@ export default class AppUpdater { this.autoUpdater.setFeedURL(FeedUrl.PRODUCTION) logger.info('Using production feed URL') - const ipCountry = await this._getIpCountry() logger.info('Detected IP country:', ipCountry) if (ipCountry.toLowerCase() !== 'cn') { this.autoUpdater.setFeedURL(FeedUrl.GITHUB_LATEST) @@ -277,21 +305,13 @@ export default class AppUpdater { if (!this.releaseInfo) { return } - // const locale = locales[configManager.getLanguage()] - // const { update: updateLocale } = locale.translation - - let detail = this.formatReleaseNotes(this.releaseInfo.releaseNotes) - if (detail === '') { - detail = 'No release notes' - } dialog .showMessageBox({ type: 'info', title: 'Update available', icon, - message: 'A new version is available. Do you want to download it now?', - detail, + message: `A new version (${this.releaseInfo.version}) is available. Do you want to install it now?\n\nYou can view the release notes in Settings > About.`, buttons: ['Later', 'Install'], defaultId: 1, cancelId: 0 @@ -305,18 +325,6 @@ export default class AppUpdater { } }) } - - private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string { - if (!releaseNotes) { - return '' - } - - if (typeof releaseNotes === 'string') { - return releaseNotes - } - - return releaseNotes.map((note) => note.note).join('\n') - } } interface GithubReleaseInfo { id: number @@ -338,7 +346,3 @@ interface GithubReleaseInfo { created_at: string }> } -interface ReleaseNoteInfo { - readonly version: string - readonly note: string | null -} diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index df0852e1..8798e76c 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -5,6 +5,29 @@ import { ThemeMode } from '@types' import { app } from 'electron' import { Conf } from 'electron-conf/main' +// 根据应用版本动态设置测试相关的默认值 +function getVersionBasedDefaults() { + const version = app.getVersion() + + // 检查版本是否包含 alpha, beta 等标识 + if (version.includes('alpha')) { + return { + testChannel: UpgradeChannel.ALPHA, + testPlan: true + } + } else if (version.includes('beta')) { + return { + testChannel: UpgradeChannel.BETA, + testPlan: true + } + } else { + return { + testChannel: UpgradeChannel.LATEST, + testPlan: false + } + } +} + export enum ConfigKeys { Language = 'language', Theme = 'theme', @@ -19,6 +42,23 @@ export enum ConfigKeys { DisableHardwareAcceleration = 'disableHardwareAcceleration' } +// 获取基于版本的动态默认值 +const versionBasedDefaults = getVersionBasedDefaults() + +const defaultValues: Record = { + [ConfigKeys.Language]: defaultLanguage, + [ConfigKeys.Theme]: ThemeMode.system, + [ConfigKeys.LaunchToTray]: false, + [ConfigKeys.Tray]: true, + [ConfigKeys.TrayOnClose]: true, + [ConfigKeys.Shortcuts]: [], + [ConfigKeys.AutoUpdate]: true, + [ConfigKeys.TestChannel]: versionBasedDefaults.testChannel, + [ConfigKeys.TestPlan]: versionBasedDefaults.testPlan, + [ConfigKeys.SpellCheckLanguages]: [] as string[], + [ConfigKeys.DisableHardwareAcceleration]: false +} + export class ConfigManager { private store: Conf private subscribers: Map void>> = new Map() @@ -28,7 +68,7 @@ export class ConfigManager { } getTheme(): ThemeMode { - return this.get(ConfigKeys.Theme, ThemeMode.system) + return this.get(ConfigKeys.Theme, defaultValues[ConfigKeys.Theme]) } setTheme(theme: ThemeMode) { @@ -38,7 +78,7 @@ export class ConfigManager { getLanguage(): LanguageVarious { const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() - : defaultLanguage + : defaultValues[ConfigKeys.Language] return this.get(ConfigKeys.Language, locale) as LanguageVarious } @@ -58,7 +98,7 @@ export class ConfigManager { } getAutoUpdate(): boolean { - return this.get(ConfigKeys.AutoUpdate, true) + return this.get(ConfigKeys.AutoUpdate, defaultValues[ConfigKeys.AutoUpdate]) } setAutoUpdate(value: boolean) { @@ -66,7 +106,7 @@ export class ConfigManager { } getTestChannel(): UpgradeChannel { - return this.get(ConfigKeys.TestChannel) + return this.get(ConfigKeys.TestChannel, defaultValues[ConfigKeys.TestChannel]) } setTestChannel(value: UpgradeChannel) { @@ -74,7 +114,7 @@ export class ConfigManager { } getTestPlan(): boolean { - return this.get(ConfigKeys.TestPlan, false) + return this.get(ConfigKeys.TestPlan, defaultValues[ConfigKeys.TestPlan]) } setTestPlan(value: boolean) { @@ -82,7 +122,7 @@ export class ConfigManager { } getLaunchToTray(): boolean { - return !!this.get(ConfigKeys.LaunchToTray, false) + return !!this.get(ConfigKeys.LaunchToTray, defaultValues[ConfigKeys.LaunchToTray]) } setLaunchToTray(value: boolean) { @@ -90,7 +130,7 @@ export class ConfigManager { } getTray(): boolean { - return !!this.get(ConfigKeys.Tray, true) + return !!this.get(ConfigKeys.Tray, defaultValues[ConfigKeys.Tray]) } setTray(value: boolean) { @@ -98,7 +138,7 @@ export class ConfigManager { } getTrayOnClose(): boolean { - return !!this.get(ConfigKeys.TrayOnClose, true) + return !!this.get(ConfigKeys.TrayOnClose, defaultValues[ConfigKeys.TrayOnClose]) } setTrayOnClose(value: boolean) { @@ -106,7 +146,10 @@ export class ConfigManager { } getDisableHardwareAcceleration(): boolean { - return this.get(ConfigKeys.DisableHardwareAcceleration, false) + return this.get( + ConfigKeys.DisableHardwareAcceleration, + defaultValues[ConfigKeys.DisableHardwareAcceleration] + ) } setDisableHardwareAcceleration(value: boolean) { @@ -163,7 +206,7 @@ export class ConfigManager { * @param defaultValue 默认值 */ get(key: string, defaultValue?: T) { - return this.store.get(key, defaultValue) as T + return this.store.get(key, defaultValue ? defaultValue : defaultValues[key]) as T } } diff --git a/src/main/services/DictionaryService.ts b/src/main/services/DictionaryService.ts index fa67cad2..9a42f14d 100644 --- a/src/main/services/DictionaryService.ts +++ b/src/main/services/DictionaryService.ts @@ -1,5 +1,10 @@ import { loggerService } from '@logger' -import { DictionaryDefinition, DictionaryResponse, DictionaryResult } from '@types' +import { + DictionaryDefinition, + DictionaryResponse, + DictionaryResult, + PronunciationInfo +} from '@types' const logger = loggerService.withContext('DictionaryService') @@ -73,16 +78,12 @@ class DictionaryService { try { const definitions: DictionaryDefinition[] = [] - // 解析音标 - 匹配 class="phonetic" 的内容 - let phonetic = '' - const phoneticMatch = html.match(/<[^>]*class[^>]*phonetic[^>]*>([^<]+)<\/[^>]*>/i) - if (phoneticMatch) { - phonetic = phoneticMatch[1].trim() - } + // 解析真人发音信息 + const pronunciations = this.parsePronunciations(html) // 解析释义 - 主要目标是 FCChild 中的内容 const fcChildMatch = html.match( - /]*id="FCChild"[^>]*class="expDiv"[^>]*>([\s\S]*?)<\/div>/i + /]*id="FCchild"[^>]*class="expDiv"[^>]*>([\s\S]*?)<\/div>/i ) if (fcChildMatch) { @@ -103,7 +104,7 @@ class DictionaryService { logger.debug('欧陆词典正则解析结果:', { word, - phonetic: phonetic || '未找到', + pronunciations: pronunciations.length, definitions: definitions.length, definitionsDetail: definitions, examples: examples.length, @@ -112,7 +113,7 @@ class DictionaryService { return { word, - phonetic: phonetic || undefined, + pronunciations: pronunciations.length > 0 ? pronunciations : undefined, definitions, examples: examples.length > 0 ? examples : undefined, translations: translations.length > 0 ? translations : undefined @@ -131,35 +132,69 @@ class DictionaryService { definitions: DictionaryDefinition[] ): void => { // 方法1: 解析列表格式 (