Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f646f287e6 | ||
|
|
603a4f051d | ||
|
|
e2bfa334c0 | ||
|
|
e8769fefb0 | ||
|
|
b88411d229 | ||
|
|
b6c67b23f6 | ||
|
|
cab86fe71c | ||
|
|
129899ed61 | ||
|
|
1f821b67dd | ||
|
|
6f1c6ce59e | ||
|
|
e5d36da904 | ||
|
|
9c3fd2c26f | ||
|
|
894eb9774b | ||
|
|
73af211bef | ||
|
|
4b561cef38 | ||
|
|
761adf2476 | ||
|
|
182a1b9c1e | ||
|
|
59aee07913 | ||
|
|
3d39c9751f | ||
|
|
2bf7af9b25 | ||
|
|
fe55ac1a79 | ||
|
|
c1d1a1b912 | ||
|
|
9d377dd464 | ||
|
|
be00f5b2a4 | ||
|
|
2d69ce81a4 | ||
|
|
4a9ee1406b | ||
|
|
aed6842319 | ||
|
|
e7957d09ec | ||
|
|
5057d18e09 | ||
|
|
3c1f52b06b | ||
|
|
f061b6b61e | ||
|
|
43cabf65e9 | ||
|
|
da3643bc79 | ||
|
|
976cb82bf9 | ||
|
|
14f80309af | ||
|
|
8acd761189 | ||
|
|
20c6ae6543 | ||
|
|
66f2211d68 | ||
|
|
ac183b4e3c | ||
|
|
877f088136 | ||
|
|
5ee08a3e62 | ||
|
|
137a07cf4e | ||
|
|
21a5adbdf2 | ||
|
|
81f52f86e5 | ||
|
|
c8147cafe3 | ||
|
|
7074f698e5 | ||
|
|
b254bb6dba | ||
|
|
1caca0ea9a | ||
|
|
12ec142a77 | ||
|
|
0136da410d | ||
|
|
4971a5fe8a | ||
|
|
d2806164a2 | ||
|
|
e332ec4ddc | ||
|
|
3b74712f57 | ||
|
|
475e4127c2 | ||
|
|
3041307aa2 | ||
|
|
e2ab8dce60 | ||
|
|
c1806e5321 | ||
|
|
92494966c6 | ||
|
|
f04680b8c2 | ||
|
|
a5b94411fe | ||
|
|
71ae4a4425 | ||
|
|
e8413aefae | ||
|
|
b229225b6e | ||
|
|
5497ed6245 | ||
|
|
155bf652f2 | ||
|
|
86bd77d995 | ||
|
|
b38b356c22 | ||
|
|
5020605590 | ||
|
|
b15c3d8ce5 | ||
|
|
a51655697d | ||
|
|
f5c26f5f02 | ||
|
|
3bc9d76f6e | ||
|
|
5751d73d5b | ||
|
|
50bd9f1659 | ||
|
|
e7627e1132 | ||
|
|
d7b131d795 | ||
|
|
74dbb223ce | ||
|
|
11a931f865 | ||
|
|
79a78d4322 | ||
|
|
a1c44baf65 | ||
|
|
86e6b93476 | ||
|
|
dbcd77963f | ||
|
|
58a4d2da36 | ||
|
|
db16d5d051 | ||
|
|
41f40b6e71 | ||
|
|
1ee717888f | ||
|
|
c7815eb81a | ||
|
|
3946b5ef2f | ||
|
|
ef99eb1f59 | ||
|
|
31607bd947 | ||
|
|
452132ce16 | ||
|
|
f84f3ef02a | ||
|
|
6c3f070797 | ||
|
|
62a339e5b9 | ||
|
|
f1fd40176f | ||
|
|
e5d8629ba3 | ||
|
|
3375f94848 | ||
|
|
9f7206abfe | ||
|
|
334c7426c2 | ||
|
|
bd7f4a433c | ||
|
|
27287238aa | ||
|
|
486767fca9 | ||
|
|
e22fe7679a | ||
|
|
7c2e526c49 | ||
|
|
2f5776d1e0 | ||
|
|
7901fc7e82 | ||
|
|
612800bef2 | ||
|
|
41b58a6f70 | ||
|
|
1a8e26a847 | ||
|
|
0bcddddc37 | ||
|
|
99163865e2 | ||
|
|
40113a2b53 | ||
|
|
75c8b19f31 | ||
|
|
2a28b0db85 | ||
|
|
ca4b851ba4 | ||
|
|
0742dac057 | ||
|
|
9d2e35c322 | ||
|
|
526bd994d8 | ||
|
|
37c1f22aee | ||
|
|
10a97b1f4a | ||
|
|
11eaaf0f88 | ||
|
|
512b9d1175 | ||
|
|
a1307bdd0b | ||
|
|
06132a0704 | ||
|
|
a7e4eb865b | ||
|
|
5246ab8369 | ||
|
|
baa887da2e | ||
|
|
be40f087ad | ||
|
|
ad62bd07aa | ||
|
|
365b9c0660 | ||
|
|
a1f5e0480c | ||
|
|
00a7950e6a | ||
|
|
598ed9c964 | ||
|
|
2b3edab4eb | ||
|
|
d0bdfce803 | ||
|
|
17a4568492 | ||
|
|
840a432b7f | ||
|
|
cc17e29fbe | ||
|
|
dd5f0a729b | ||
|
|
9d72caee44 | ||
|
|
661165bd65 | ||
|
|
e8cd89d7e4 | ||
|
|
4caf8efa72 | ||
|
|
ed707df837 | ||
|
|
0821aea72b | ||
|
|
4dfac6bb34 | ||
|
|
8ad41eebe2 | ||
|
|
7c64d255b8 | ||
|
|
bad839934d | ||
|
|
be06106f66 | ||
|
|
b6dc09a7e3 | ||
|
|
edbc572bf1 | ||
|
|
370a165572 | ||
|
|
6b0f9d0798 | ||
|
|
bc78f8a3ef | ||
|
|
297916c35b | ||
|
|
e2b0aacf05 | ||
|
|
d9204049fe |
37
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Bug Report
|
||||
description: Report a problem or regression
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: input
|
||||
id: app_version
|
||||
attributes:
|
||||
label: App version
|
||||
description: The VidBee version (e.g., 1.2.3)
|
||||
placeholder: 1.2.3
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os_version
|
||||
attributes:
|
||||
label: OS version
|
||||
description: Your operating system and version (e.g., macOS 14.2, Windows 11 23H2)
|
||||
placeholder: macOS 14.2
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Observed behavior
|
||||
placeholder: What actually happened
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs or screenshots
|
||||
description: Paste relevant logs or add screenshots
|
||||
placeholder: Attach files or paste logs here
|
||||
validations:
|
||||
required: false
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea or improvement
|
||||
title: "[Feature]: "
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem to solve
|
||||
description: What problem are you trying to solve?
|
||||
placeholder: I want to...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe the feature or change you want
|
||||
placeholder: It would be great if...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Other solutions or workarounds you considered
|
||||
placeholder: I tried...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: extra
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots
|
||||
placeholder: Links, screenshots, or related issues
|
||||
validations:
|
||||
required: false
|
||||
268
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
upload_artifacts:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
description: 'Whether to upload build artifacts'
|
||||
secrets:
|
||||
MAC_CERT_P12_BASE64:
|
||||
required: false
|
||||
MAC_CERT_P12_PASSWORD:
|
||||
required: false
|
||||
APPLE_API_KEY_ID:
|
||||
required: false
|
||||
APPLE_API_ISSUER:
|
||||
required: false
|
||||
APPLE_API_KEY_P8_BASE64:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: windows
|
||||
os: windows-latest
|
||||
build_script: pnpm run build:win
|
||||
ytdlp_asset: yt-dlp.exe
|
||||
ytdlp_output: yt-dlp.exe
|
||||
ffmpeg_url: https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-win64-gpl.zip
|
||||
ffmpeg_inner_path: ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe
|
||||
ffprobe_inner_path: ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe
|
||||
- platform: macos
|
||||
os: macos-latest
|
||||
build_script: pnpm run build:mac
|
||||
ytdlp_asset: yt-dlp_macos
|
||||
ytdlp_output: yt-dlp_macos
|
||||
ffmpeg_arm_url: https://github.com/eko5624/mpv-mac/releases/download/2026-01-12/ffmpeg-arm64-96e8f3b8cc.zip
|
||||
ffmpeg_x86_url: https://github.com/eko5624/mpv-mac/releases/download/2026-01-12/ffmpeg-x86_64-96e8f3b8cc.zip
|
||||
ffmpeg_inner_path: ffmpeg/ffmpeg
|
||||
ffprobe_inner_path: ffmpeg/ffprobe
|
||||
- platform: linux
|
||||
os: ubuntu-latest
|
||||
build_script: pnpm run build:linux
|
||||
ytdlp_asset: yt-dlp
|
||||
ytdlp_output: yt-dlp_linux
|
||||
ffmpeg_url: https://github.com/yt-dlp/FFmpeg-Builds/releases/latest/download/ffmpeg-master-latest-linux64-gpl.tar.xz
|
||||
ffmpeg_inner_path: ffmpeg-master-latest-linux64-gpl/bin/ffmpeg
|
||||
ffprobe_inner_path: ffmpeg-master-latest-linux64-gpl/bin/ffprobe
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download ffmpeg binary (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ffmpegUrl = '${{ matrix.ffmpeg_url }}'
|
||||
Invoke-WebRequest -Uri $ffmpegUrl -OutFile ffmpeg.zip
|
||||
Expand-Archive ffmpeg.zip -DestinationPath ffmpeg -Force
|
||||
$source = Join-Path 'ffmpeg' '${{ matrix.ffmpeg_inner_path }}'
|
||||
$ffprobeSource = Join-Path 'ffmpeg' '${{ matrix.ffprobe_inner_path }}'
|
||||
$destinationDir = Join-Path 'resources' 'ffmpeg'
|
||||
New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null
|
||||
Copy-Item -Path $source -Destination (Join-Path $destinationDir 'ffmpeg.exe') -Force
|
||||
Copy-Item -Path $ffprobeSource -Destination (Join-Path $destinationDir 'ffprobe.exe') -Force
|
||||
Remove-Item ffmpeg.zip -Force
|
||||
Remove-Item ffmpeg -Recurse -Force
|
||||
|
||||
- name: Download ffmpeg binary (macOS)
|
||||
if: matrix.platform == 'macos'
|
||||
shell: bash
|
||||
env:
|
||||
FFMPEG_OUTPUT: ffmpeg
|
||||
FFPROBE_OUTPUT: ffprobe
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_arm_url }}" -o ffmpeg-arm.zip
|
||||
unzip -q ffmpeg-arm.zip -d ffmpeg-arm
|
||||
|
||||
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_x86_url }}" -o ffmpeg-x86.zip
|
||||
unzip -q ffmpeg-x86.zip -d ffmpeg-x86
|
||||
|
||||
arm_bin="ffmpeg-arm/${{ matrix.ffmpeg_inner_path }}"
|
||||
x86_bin="ffmpeg-x86/${{ matrix.ffmpeg_inner_path }}"
|
||||
arm_probe="ffmpeg-arm/${{ matrix.ffprobe_inner_path }}"
|
||||
x86_probe="ffmpeg-x86/${{ matrix.ffprobe_inner_path }}"
|
||||
|
||||
if [[ ! -f "$arm_bin" ]]; then
|
||||
arm_bin="$(find ffmpeg-arm -type f -name ffmpeg -print -quit)"
|
||||
fi
|
||||
if [[ ! -f "$x86_bin" ]]; then
|
||||
x86_bin="$(find ffmpeg-x86 -type f -name ffmpeg -print -quit)"
|
||||
fi
|
||||
if [[ ! -f "$arm_probe" ]]; then
|
||||
arm_probe="$(find ffmpeg-arm -type f -name ffprobe -print -quit)"
|
||||
fi
|
||||
if [[ ! -f "$x86_probe" ]]; then
|
||||
x86_probe="$(find ffmpeg-x86 -type f -name ffprobe -print -quit)"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$arm_bin" ]]; then
|
||||
echo "::error::Missing arm64 ffmpeg binary at $arm_bin"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$x86_bin" ]]; then
|
||||
echo "::error::Missing x86_64 ffmpeg binary at $x86_bin"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$arm_probe" ]]; then
|
||||
echo "::error::Missing arm64 ffprobe binary at $arm_probe"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$x86_probe" ]]; then
|
||||
echo "::error::Missing x86_64 ffprobe binary at $x86_probe"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p resources/ffmpeg
|
||||
lipo -create "$arm_bin" "$x86_bin" -output "resources/ffmpeg/$FFMPEG_OUTPUT"
|
||||
lipo -create "$arm_probe" "$x86_probe" -output "resources/ffmpeg/$FFPROBE_OUTPUT"
|
||||
chmod +x "resources/ffmpeg/$FFMPEG_OUTPUT" "resources/ffmpeg/$FFPROBE_OUTPUT"
|
||||
rm -rf ffmpeg-arm ffmpeg-x86 ffmpeg-arm.zip ffmpeg-x86.zip
|
||||
|
||||
- name: Download ffmpeg binary (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_url }}" -o ffmpeg.tar.xz
|
||||
if ! tar -tf ffmpeg.tar.xz >/dev/null 2>&1; then
|
||||
echo "::error::Downloaded ffmpeg archive is not a valid tar.xz"
|
||||
exit 1
|
||||
fi
|
||||
mkdir ffmpeg
|
||||
tar -xf ffmpeg.tar.xz -C ffmpeg
|
||||
mkdir -p resources/ffmpeg
|
||||
cp "ffmpeg/${{ matrix.ffmpeg_inner_path }}" "resources/ffmpeg/ffmpeg"
|
||||
cp "ffmpeg/${{ matrix.ffprobe_inner_path }}" "resources/ffmpeg/ffprobe"
|
||||
chmod +x "resources/ffmpeg/ffmpeg" "resources/ffmpeg/ffprobe"
|
||||
rm -rf ffmpeg.tar.xz ffmpeg
|
||||
|
||||
- name: Download yt-dlp binary
|
||||
shell: bash
|
||||
run: |
|
||||
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp_asset }}" -o "resources/${{ matrix.ytdlp_output }}"
|
||||
if [[ "${{ matrix.platform }}" == "linux" ]] || [[ "${{ matrix.platform }}" == "macos" ]]; then
|
||||
chmod +x "resources/${{ matrix.ytdlp_output }}"
|
||||
fi
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check && pnpm run typecheck
|
||||
|
||||
- name: Setup macOS signing
|
||||
if: matrix.platform == 'macos'
|
||||
shell: bash
|
||||
env:
|
||||
MAC_CERT_P12_BASE64: ${{ secrets.MAC_CERT_P12_BASE64 }}
|
||||
MAC_CERT_P12_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
|
||||
APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "SIGNING_AVAILABLE=false" >> "$GITHUB_ENV"
|
||||
|
||||
# Check if all required secrets are present
|
||||
if [[ -z "$MAC_CERT_P12_BASE64" ]] || [[ -z "$MAC_CERT_P12_PASSWORD" ]] || \
|
||||
[[ -z "$APPLE_API_KEY_ID" ]] || [[ -z "$APPLE_API_ISSUER" ]] || \
|
||||
[[ -z "$APPLE_API_KEY_P8_BASE64" ]]; then
|
||||
echo "::notice::macOS signing secrets not available, skipping code signing setup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CERT_PATH="$RUNNER_TEMP/mac_cert.p12"
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
|
||||
API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
|
||||
|
||||
echo "$MAC_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
|
||||
echo "$APPLE_API_KEY_P8_BASE64" | base64 --decode > "$API_KEY_PATH"
|
||||
|
||||
security create-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||
security unlock-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
|
||||
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MAC_CERT_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild
|
||||
security list-keychain -d user -s "$KEYCHAIN_PATH"
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
|
||||
|
||||
echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
|
||||
echo "CSC_KEY_PASSWORD=$MAC_CERT_P12_PASSWORD" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_KEY=$API_KEY_PATH" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
|
||||
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV"
|
||||
echo "SIGNING_AVAILABLE=true" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build application
|
||||
run: ${{ matrix.build_script }}
|
||||
|
||||
- name: Verify macOS codesign and notarization
|
||||
if: matrix.platform == 'macos' && env.SIGNING_AVAILABLE == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apps_found=0
|
||||
while IFS= read -r app; do
|
||||
apps_found=1
|
||||
echo "Verifying codesign for $app"
|
||||
codesign --verify --deep --strict --verbose=2 "$app"
|
||||
spctl -a -t exec -vv "$app"
|
||||
echo "Validating notarization ticket for $app"
|
||||
xcrun stapler validate "$app"
|
||||
done < <(find dist -type d -name "*.app" -prune -print)
|
||||
|
||||
if [[ "$apps_found" -eq 0 ]]; then
|
||||
echo "::error::No .app bundles found in dist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dmgs_found=0
|
||||
while IFS= read -r dmg; do
|
||||
dmgs_found=1
|
||||
echo "Submitting DMG for notarization: $dmg"
|
||||
xcrun notarytool submit "$dmg" --key "$APPLE_API_KEY" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER" --wait
|
||||
echo "Stapling notarization ticket for $dmg"
|
||||
xcrun stapler staple "$dmg"
|
||||
echo "Validating notarization ticket for $dmg"
|
||||
xcrun stapler validate "$dmg"
|
||||
done < <(find dist -type f -name "*.dmg" -print)
|
||||
|
||||
if [[ "$dmgs_found" -eq 0 ]]; then
|
||||
echo "::notice::No DMG artifacts found to validate"
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
if: inputs.upload_artifacts == true
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-${{ matrix.os }}
|
||||
path: |
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
dist/*.dmg
|
||||
dist/*.AppImage
|
||||
dist/*.snap
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
retention-days: 1
|
||||
36
.github/workflows/ci.yml
vendored
@@ -1,37 +1,11 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check
|
||||
|
||||
- name: Build check
|
||||
run: pnpm run build
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
upload_artifacts: true
|
||||
|
||||
84
.github/workflows/extension-publish.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Publish Extension
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- extension/package.json
|
||||
|
||||
jobs:
|
||||
detect-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.version_check.outputs.changed }}
|
||||
current_version: ${{ steps.version_check.outputs.current_version }}
|
||||
previous_version: ${{ steps.version_check.outputs.previous_version }}
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if extension version changed
|
||||
id: version_check
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
before_sha="${{ github.event.before }}"
|
||||
current_version=$(node -e "const fs = require('fs'); const v = JSON.parse(fs.readFileSync('extension/package.json', 'utf8')).version; process.stdout.write(v);")
|
||||
previous_version=""
|
||||
if git cat-file -e "${before_sha}:extension/package.json" 2>/dev/null; then
|
||||
previous_version=$(git show "${before_sha}:extension/package.json" | node -e "let data=''; process.stdin.on('data', d => data += d); process.stdin.on('end', () => { const v = JSON.parse(data).version; process.stdout.write(v); });")
|
||||
fi
|
||||
|
||||
echo "current_version=${current_version}" >> "$GITHUB_OUTPUT"
|
||||
echo "previous_version=${previous_version}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [[ -n "${previous_version}" && "${current_version}" == "${previous_version}" ]]; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
submit:
|
||||
needs: detect-version
|
||||
if: needs.detect-version.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: extension
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: extension/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
pnpm zip
|
||||
|
||||
- name: Submit to stores
|
||||
run: |
|
||||
pnpm wxt submit \
|
||||
--chrome-zip .output/*-chrome.zip
|
||||
env:
|
||||
CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
|
||||
CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
CHROME_PUBLISH_TARGET: "default"
|
||||
CHROME_SKIP_SUBMIT_REVIEW: false
|
||||
96
.github/workflows/release.yml
vendored
@@ -7,85 +7,45 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check
|
||||
|
||||
- name: Build Electron app
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
|
||||
pnpm run build:linux
|
||||
elif [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
pnpm run build:mac
|
||||
elif [ "${{ matrix.os }}" == "windows-latest" ]; then
|
||||
pnpm run build:win
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-${{ matrix.os }}
|
||||
path: dist/
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
upload_artifacts: true
|
||||
secrets: inherit
|
||||
|
||||
release:
|
||||
needs: build
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: dist-*
|
||||
merge-multiple: true
|
||||
path: dist/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
dist-ubuntu-latest/*.AppImage
|
||||
dist-ubuntu-latest/*.snap
|
||||
dist-ubuntu-latest/*.deb
|
||||
dist-ubuntu-latest/*.rpm
|
||||
dist-ubuntu-latest/*.tar.gz
|
||||
dist-ubuntu-latest/*.yml
|
||||
dist-ubuntu-latest/*.blockmap
|
||||
dist-macos-latest/*.dmg
|
||||
dist-macos-latest/*.zip
|
||||
dist-macos-latest/*.yml
|
||||
dist-macos-latest/*.blockmap
|
||||
dist-windows-latest/*.exe
|
||||
dist-windows-latest/*.zip
|
||||
dist-windows-latest/*.yml
|
||||
dist-windows-latest/*.blockmap
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
dist/*.dmg
|
||||
dist/*.AppImage
|
||||
dist/*.snap
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
- name: Notify Cloudflare Pages
|
||||
env:
|
||||
CLOUDFLARE_WEBHOOK_URL: ${{ secrets.CLOUDFLARE_WEBHOOK_URL }}
|
||||
run: |
|
||||
curl -X POST "$CLOUDFLARE_WEBHOOK_URL"
|
||||
|
||||
25
.github/workflows/translator.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: 'translator'
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
discussion:
|
||||
types: [created, edited]
|
||||
discussion_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: lizheming/github-translate-action@c55aac477e98562d4faed9f77c54ab8306ae6ebf
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
IS_MODIFY_TITLE: true
|
||||
6
.gitignore
vendored
@@ -1,7 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
/dist
|
||||
out
|
||||
.conductor/
|
||||
.wxt
|
||||
.output
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
*.log*
|
||||
|
||||
|
||||
5
.vscode/settings.json
vendored
@@ -25,5 +25,8 @@
|
||||
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"i18n-ally.localesPaths": ["src/renderer/src/locales"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
1. use pnpm instead of npm
|
||||
2. use pnpm run check after tasks to check code
|
||||
3. Support i18n, only translate the English version of en.json
|
||||
3. Support i18n. When writing business logic, initially only translate the English version of en.json
|
||||
4. use English for comments&console
|
||||
5. Follow the ✅ KISS (Keep It Simple, Stupid) & ✅ YAGNI (You Aren't Gonna Need It) principles
|
||||
6. Use Conventional Commits format for commit messages: `type(scope): subject`. Common types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert. PR titles should also follow this format.
|
||||
7. If there is an error when running `pnpm run check:i18n`, please complete the missing corresponding translation files and fields. Ensure the translation is done into the corresponding language, rather than directly copying the English version.
|
||||
|
||||
78
CONTRIBUTING.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Contributing to VidBee
|
||||
|
||||
Thank you for taking the time to improve VidBee. These notes keep the project maintainable and easy to review.
|
||||
|
||||
## Getting Ready
|
||||
- Use Node.js 18+ and pnpm 8+.
|
||||
- Install dependencies with `pnpm install`.
|
||||
- Run `pnpm dev` to test changes locally.
|
||||
|
||||
## Tech Stack
|
||||
- Runtime: Electron 38, electron-vite, electron-builder.
|
||||
- Frontend: React 19, React Router, Jotai, React Hook Form, Tailwind CSS 4, shadcn/ui, Lucide icons.
|
||||
- Tooling: TypeScript 5, pnpm, Biome, dayjs, electron-log, electron-store, electron-updater, i18next, next-themes.
|
||||
|
||||
## Local Development
|
||||
- Use `pnpm install` to pull dependencies after cloning.
|
||||
- Start the Electron and Vite development environment with `pnpm dev`; hot module replacement is already configured.
|
||||
- Preview the production build locally with `pnpm start`.
|
||||
|
||||
## Useful Scripts
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `pnpm run typecheck` | Type-check the main and renderer projects. |
|
||||
| `pnpm build` | Run type checks and produce production bundles. |
|
||||
| `pnpm build:win` / `pnpm build:mac` / `pnpm build:linux` | Create platform-specific distributables. |
|
||||
| `pnpm build:unpack` | Produce unpacked output directories for inspection. |
|
||||
| `pnpm run check` | Format and lint the codebase with Biome. |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
src/
|
||||
|-- main/ # Electron main process, IPC services, configuration
|
||||
|-- preload/ # Context bridge and preload helpers
|
||||
`-- renderer/
|
||||
|-- src/
|
||||
| |-- pages/ # Application routes (Home, Settings, Playlist, etc.)
|
||||
| |-- components/ # UI components, download views, shared controls
|
||||
| |-- data/ # Static datasets such as popularSites.ts
|
||||
| |-- hooks/ # Custom hooks and global atoms
|
||||
| |-- lib/ # Utilities shared across the renderer
|
||||
| `-- assets/ # Global styles and icons
|
||||
`-- index.html
|
||||
```
|
||||
|
||||
## Internationalization
|
||||
- i18next drives localization with English (`en`) and Simplified Chinese (`zh-CN`) namespaces.
|
||||
- Only update strings in `src/renderer/src/locales/en.json`; maintainers handle the other locales.
|
||||
- Keep copy edits focused and avoid removing translation keys without discussion.
|
||||
|
||||
## Configuration and Storage
|
||||
- Persistent settings are stored with `electron-store` and exposed through IPC helpers.
|
||||
- User-facing preferences such as download paths and themes live in `src/main/settings.ts` and related services.
|
||||
- Logs are recorded with `electron-log` to simplify troubleshooting.
|
||||
|
||||
## Packaging
|
||||
- Build production bundles with `pnpm build`.
|
||||
- Create platform-specific artifacts with `pnpm build:win`, `pnpm build:mac`, or `pnpm build:linux`.
|
||||
- Use `pnpm build:unpack` to generate unpacked directories under `dist/` for manual inspection.
|
||||
- Bundle platform binaries of `yt-dlp` and `ffmpeg/ffprobe` under `resources/ffmpeg/` (or set `YTDLP_PATH`/`FFMPEG_PATH`) before packaging so merges and audio extraction work out of the box.
|
||||
|
||||
## Working on Changes
|
||||
- Keep each pull request focused on a single problem or feature.
|
||||
- Run `pnpm run check` before committing to ensure formatting and linting stay consistent.
|
||||
- Write comments and console messages in English only.
|
||||
- When updating copy in the app, adjust strings in `src/renderer/src/locales/en.json`; other locale files are handled by maintainers.
|
||||
|
||||
## Opening Issues
|
||||
- Search existing issues to avoid duplicates.
|
||||
- Describe the problem clearly with steps to reproduce, expected behaviour, and screenshots or logs when useful.
|
||||
|
||||
## Submitting Pull Requests
|
||||
- Explain the motivation and impact of the change in the description.
|
||||
- Mention any user facing updates or migrations.
|
||||
- Confirm that `pnpm run check` passes and note any follow-up work that is out of scope.
|
||||
|
||||
We appreciate every contribution that keeps VidBee simple and reliable.
|
||||
146
README.md
@@ -1,97 +1,85 @@
|
||||
# VidBee
|
||||
|
||||
<div align="center">
|
||||
<img src="build/icon.png" alt="VidBee icon" width="120" />
|
||||
<h3>A minimal Electron downloader for video and audio</h3>
|
||||
<p>Built with Electron, React, TypeScript, Tailwind CSS, and shadcn/ui.</p>
|
||||
<a href="https://github.com/nexmoe/VidBee">
|
||||
<img src="build/icon.png" alt="Logo" width="80" height="80">
|
||||
</a>
|
||||
|
||||
<h3>VidBee</h3>
|
||||
<p>
|
||||
<a href="https://github.com/nexmoe/VidBee/stargazers"><img src="https://img.shields.io/github/stars/nexmoe/VidBee?color=ffcb47&labelColor=black&logo=github&label=Stars" /></a>
|
||||
<a href="https://github.com/nexmoe/VidBee/graphs/contributors"><img src="https://img.shields.io/github/contributors/nexmoe/VidBee?ogo=github&label=Contributors&labelColor=black" /></a>
|
||||
<a href="https://github.com/nexmoe/VidBee/releases"><img src="https://img.shields.io/github/downloads/nexmoe/VidBee/total?color=369eff&labelColor=black&logo=github&label=Downloads" /></a>
|
||||
<a href="https://github.com/nexmoe/VidBee/releases/latest"><img src="https://img.shields.io/github/v/release/nexmoe/VidBee?color=369eff&labelColor=black&logo=github&label=Latest%20Release" /></a>
|
||||
<a href="https://x.com/intent/follow?screen_name=nexmoex"><img src="https://img.shields.io/badge/Follow-blue?color=1d9bf0&logo=x&labelColor=black" /></a>
|
||||
<a href="https://deepwiki.com/nexmoe/VidBee"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://github.com/nexmoe/VidBee/releases/latest" target="_blank"><img src="screenshots/main-interface.png" alt="VidBee Desktop" width="46%"/></a>
|
||||
<a href="https://github.com/nexmoe/VidBee/releases/latest" target="_blank"><img src="screenshots/download-queue.png" alt="VidBee Download Queue" width="46%"/></a>
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## Features
|
||||
- Download single videos, audio-only tracks, or entire playlists through a unified flow.
|
||||
- Queue multiple jobs with progress tracking, pause, resume, retry, and download history.
|
||||
- Detect platform-friendly formats automatically and store files in custom locations.
|
||||
- Quick actions for popular sites plus manual URL entry for anything yt-dlp supports.
|
||||
- Theme toggle (system, light, dark) and localized interface in English and Simplified Chinese.
|
||||
- Desktop-native touches such as tray integration, update checks, and persistent settings.
|
||||
VidBee is a modern, open-source video downloader that lets you download videos and audios from 1000+ websites worldwide. Built with Electron and powered by yt-dlp, VidBee offers a clean, intuitive interface with powerful features for all your downloading needs, including RSS auto-download automation that automatically subscribes to feeds and downloads new videos from your favorite creators in the background.
|
||||
|
||||
## Tech Stack
|
||||
- **Runtime:** Electron 38, electron-vite, electron-builder.
|
||||
- **Frontend:** React 19, React Router, Jotai, React Hook Form, Tailwind CSS 4, shadcn/ui, Lucide icons.
|
||||
- **Tooling:** TypeScript 5, pnpm, Biome, dayjs, electron-log, electron-store, electron-updater, i18next, next-themes.
|
||||
## 👋🏻 Getting Started
|
||||
|
||||
## Getting Started
|
||||
### Prerequisites
|
||||
- Node.js 18 or newer
|
||||
- pnpm 8 or newer
|
||||
VidBee is currently under active development, and feedback is welcome for any [issue](https://github.com/nexmoe/VidBee/issues) encountered.
|
||||
|
||||
### Install dependencies
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
[📥 Download VidBee](https://vidbee.org/download/) | [📚 Documentation](https://docs.vidbee.org)
|
||||
|
||||
### Run the app in development
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> **Star Us**, You will receive all release notifications from GitHub without any delay ~
|
||||
|
||||
The Electron app and Vite dev server launch together with hot module replacement.
|
||||
<a href="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats?repo_id=1081230042" target="_blank" style="display: block" align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=1081230042&image_size=auto&color_scheme=dark" width="655" height="auto">
|
||||
<img alt="Performance Stats of nexmoe/VidBee - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=1081230042&image_size=auto&color_scheme=light" width="655" height="auto">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Useful Scripts
|
||||
| Command | Description |
|
||||
| --- | --- |
|
||||
| `pnpm dev` | Run the Electron and Vite development environment. |
|
||||
| `pnpm start` | Preview the production build locally. |
|
||||
| `pnpm run typecheck` | Type-check the main and renderer projects. |
|
||||
| `pnpm build` | Run type checks and produce production bundles. |
|
||||
| `pnpm build:win` / `pnpm build:mac` / `pnpm build:linux` | Create platform-specific distributables. |
|
||||
| `pnpm build:unpack` | Produce unpacked output directories. |
|
||||
| `pnpm run check` | Format and lint the codebase with Biome. |
|
||||
<!-- Made with [OSS Insight](https://ossinsight.io/) -->
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
src/
|
||||
├─ main/ # Electron main process, IPC services, configuration
|
||||
├─ preload/ # Context bridge and preload helpers
|
||||
└─ renderer/
|
||||
├─ src/
|
||||
│ ├─ pages/ # Application routes (Home, Settings, Playlist, etc.)
|
||||
│ ├─ components/ # UI components, download views, shared controls
|
||||
│ ├─ data/ # Static datasets such as popularSites.ts
|
||||
│ ├─ hooks/ # Custom hooks and global atoms
|
||||
│ ├─ lib/ # Utilities shared across the renderer
|
||||
│ └─ assets/ # Global styles and icons
|
||||
└─ index.html
|
||||
```
|
||||
## ✨ Features
|
||||
|
||||
## Internationalization
|
||||
The renderer uses i18next with English (`en`) and Simplified Chinese (`zh-CN`) namespaces. Update strings in `src/renderer/src/locales/en.json`; other locales are maintained separately.
|
||||
### 🌍 Global Video Download Support
|
||||
|
||||
## Configuration and Storage
|
||||
- Persistent settings are stored with `electron-store` and exposed through IPC helpers.
|
||||
- User-facing preferences such as download paths and themes live in `src/main/settings.ts` and related services.
|
||||
- Logs are recorded with `electron-log` to simplify troubleshooting.
|
||||
Download videos from almost any website worldwide through the powerful yt-dlp engine. Support for 1000+ sites including YouTube, TikTok, Instagram, Twitter, and many more.
|
||||
|
||||
## Packaging
|
||||
Run one of the following commands after a successful build:
|
||||

|
||||
|
||||
```bash
|
||||
pnpm build:win
|
||||
pnpm build:mac
|
||||
pnpm build:linux
|
||||
```
|
||||
### 🎨 Best-in-class UI Experience
|
||||
|
||||
Artifacts are generated under `dist/`. Use `pnpm build:unpack` to create unpacked directories for manual inspection.
|
||||
Modern, clean interface with intuitive operations. One-click pause/resume/retry, real-time progress tracking, and comprehensive download queue management.
|
||||
|
||||
## Contributing
|
||||
Issues and pull requests are welcome. Keep changes focused, document user facing updates, and run `pnpm run check` before opening a PR.
|
||||

|
||||
|
||||
## License
|
||||
This project is distributed under the MIT License. See `LICENSE` for details.
|
||||
### 📡 RSS Auto Download
|
||||
|
||||
## Thanks
|
||||
- [Electron](https://www.electronjs.org/)
|
||||
- [React](https://react.dev/)
|
||||
- [Vite](https://vitejs.dev/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [shadcn/ui](https://ui.shadcn.com/)
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||
Automatically subscribe to RSS feeds and auto-download new videos in the background from your favorite creators across YouTube, TikTok, and more. Set up RSS subscriptions once, and VidBee will automatically download new uploads without manual intervention, perfect for keeping up with your favorite channels and creators.
|
||||
|
||||
## 🌐 Supported Sites
|
||||
|
||||
VidBee supports 1000+ video and audio platforms through yt-dlp. For the complete list of supported sites, visit [https://vidbee.org/supported-sites/](https://vidbee.org/supported-sites/)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
You are welcome to join the open source community to build together. For more details, check out:
|
||||
|
||||
- [Contributing Guide](./CONTRIBUTING.md)
|
||||
- [DeepWiki Documentation](https://deepwiki.com/nexmoe/VidBee)
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is distributed under the MIT License. See [`LICENSE`](LICENSE) for details.
|
||||
|
||||
## 🙏 Thanks
|
||||
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - The powerful video downloader engine
|
||||
- [FFmpeg](https://ffmpeg.org/) - The multimedia framework for video and audio processing
|
||||
- [Electron](https://www.electronjs.org/) - Build cross-platform desktop apps
|
||||
- [React](https://react.dev/) - The UI library
|
||||
- [Vite](https://vitejs.dev/) - Next generation frontend tooling
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - Beautifully designed components
|
||||
|
||||
14
biome.json
@@ -39,7 +39,19 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"]
|
||||
"includes": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.json",
|
||||
"!docs",
|
||||
"!monkey/dist",
|
||||
"!dist",
|
||||
"!out",
|
||||
"!build"
|
||||
],
|
||||
"experimentalScannerIgnores": ["docs/**", "monkey/dist/**", "dist/**", "out/**", "build/**"]
|
||||
},
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
|
||||
63
build/after-pack.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const BINARIES = [
|
||||
'yt-dlp_macos',
|
||||
path.join('ffmpeg', 'ffmpeg'),
|
||||
path.join('ffmpeg', 'ffprobe'),
|
||||
'deno'
|
||||
]
|
||||
|
||||
const findAppBundle = (appOutDir) => {
|
||||
const entries = fs.readdirSync(appOutDir)
|
||||
const app = entries.find((entry) => entry.endsWith('.app'))
|
||||
return app ? path.join(appOutDir, app) : null
|
||||
}
|
||||
|
||||
const resolveSigningIdentity = () =>
|
||||
process.env.CSC_NAME || process.env.APPLE_SIGNING_IDENTITY || '-'
|
||||
|
||||
const signBinary = (targetPath, entitlementsPath) => {
|
||||
const identity = resolveSigningIdentity()
|
||||
const args = ['--force', '--sign', identity, '--entitlements', entitlementsPath]
|
||||
|
||||
if (identity !== '-') {
|
||||
args.push('--options', 'runtime', '--timestamp')
|
||||
}
|
||||
|
||||
args.push(targetPath)
|
||||
execFileSync('codesign', args, { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
exports.default = async function afterPack(context) {
|
||||
if (context.electronPlatformName !== 'darwin') {
|
||||
return
|
||||
}
|
||||
|
||||
const appBundle = findAppBundle(context.appOutDir)
|
||||
if (!appBundle) {
|
||||
console.warn('afterPack: No .app bundle found, skipping tool signing.')
|
||||
return
|
||||
}
|
||||
|
||||
const resourcesPath = path.join(
|
||||
appBundle,
|
||||
'Contents',
|
||||
'Resources',
|
||||
'app.asar.unpacked',
|
||||
'resources'
|
||||
)
|
||||
|
||||
const entitlementsPath = path.resolve(__dirname, 'entitlements.mac.plist')
|
||||
|
||||
for (const binary of BINARIES) {
|
||||
const targetPath = path.join(resourcesPath, binary)
|
||||
if (!fs.existsSync(targetPath)) {
|
||||
console.warn(`afterPack: Missing ${binary}, skipping.`)
|
||||
continue
|
||||
}
|
||||
console.log(`afterPack: Signing ${binary} with entitlements.`)
|
||||
signBinary(targetPath, entitlementsPath)
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
BIN
build/icon.icns
6
conductor.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scripts": {
|
||||
"setup": "rm -rf resources && cp -r $CONDUCTOR_ROOT_PATH/resources resources && pnpm install",
|
||||
"run": "pnpm run dev"
|
||||
}
|
||||
}
|
||||
26
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# deps
|
||||
/node_modules
|
||||
|
||||
# generated content
|
||||
.source
|
||||
|
||||
# test & build
|
||||
/coverage
|
||||
/.next/
|
||||
/out/
|
||||
/build
|
||||
*.tsbuildinfo
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
/.pnp
|
||||
.pnp.js
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# others
|
||||
.env*.local
|
||||
.vercel
|
||||
next-env.d.ts
|
||||
45
docs/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# docs
|
||||
|
||||
This is a Next.js application generated with
|
||||
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
|
||||
|
||||
Run development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000 with your browser to see the result.
|
||||
|
||||
## Explore
|
||||
|
||||
In the project, you can see:
|
||||
|
||||
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
|
||||
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
|
||||
|
||||
| Route | Description |
|
||||
| ------------------------- | ------------------------------------------------------ |
|
||||
| `app/(home)` | The route group for your landing page and other pages. |
|
||||
| `app/docs` | The documentation layout and pages. |
|
||||
| `app/api/search/route.ts` | The Route Handler for search. |
|
||||
|
||||
### Fumadocs MDX
|
||||
|
||||
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
|
||||
|
||||
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js and Fumadocs, take a look at the following
|
||||
resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
|
||||
features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
|
||||
41
docs/biome.config.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": [
|
||||
"**",
|
||||
"!node_modules",
|
||||
"!.next",
|
||||
"!dist",
|
||||
"!build",
|
||||
"!.source"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
},
|
||||
"domains": {
|
||||
"next": "recommended",
|
||||
"react": "recommended"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
docs/content/cookies.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Cookies
|
||||
description: Signed-in downloads, restricted content, and cookie setup
|
||||
---
|
||||
|
||||
Cookies reuse your browser's signed-in session so VidBee can download content that requires login or verification, such as subscriber-only items, age-gated pages, or private links.
|
||||
|
||||
## When you need cookies
|
||||
|
||||
- Content that requires an account to view or download.
|
||||
- Age-gated or region-locked pages.
|
||||
- Private links or lists visible only to signed-in users.
|
||||
|
||||
## Two ways to use cookies in VidBee
|
||||
|
||||
### Option 1: Read cookies from your browser
|
||||
|
||||
In **Settings**, select your browser. VidBee will try to detect the browser profile path automatically. You can also enter the profile path manually.
|
||||
|
||||

|
||||
|
||||
**Supported browsers (platform dependent):**
|
||||
|
||||
- **Windows: Firefox only. Other browsers cannot be used for cookie reading.**
|
||||
- macOS: All browsers supported.
|
||||
- Linux: All browsers supported.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open VidBee → Settings → Cookies.
|
||||
2. Select a browser and confirm the profile path.
|
||||
3. Go back and start the download again.
|
||||
|
||||
If detection fails or the path is invalid, manually choose the actual browser profile directory.
|
||||
On Windows, if you are not using Firefox, switch to the cookies file method.
|
||||
|
||||
### Option 2: Import a cookies file
|
||||
|
||||
You can import a **Netscape-formatted** cookies file. This is useful when reading the browser profile is not possible.
|
||||
|
||||

|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Export a Netscape cookies file using a browser extension.
|
||||
2. Open VidBee → Settings → Cookies file, and select the exported file.
|
||||
3. Click Clear to disable it.
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Prefer browser-based cookies when possible, because it's easier to maintain.
|
||||
- If browser reading fails, switch to a cookies file.
|
||||
- Cookies files expire. Re-export when your account state changes.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Profile path is invalid**: Ensure the folder exists and points to the real browser profile directory.
|
||||
- **Still asks for login**: Confirm your browser is signed in and re-save the setting.
|
||||
- **Browser read failed**: Close the browser and retry, or switch to a cookies file.
|
||||
|
||||
## Privacy and security
|
||||
|
||||
Cookies are equivalent to your login session. Keep them private and never share or upload them. If you suspect leakage, sign out and change your password.
|
||||
36
docs/content/faq.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: FAQ
|
||||
description: Common questions and troubleshooting for VidBee
|
||||
---
|
||||
|
||||
## What if a download fails or shows an error?
|
||||
|
||||
- Confirm the link is valid and opens in your browser.
|
||||
- Update VidBee to the latest version and retry.
|
||||
- If the content requires login or age verification, configure cookies.
|
||||
|
||||
## Why does the same link sometimes work and sometimes fail?
|
||||
|
||||
Some sites frequently change page structure or limit request rates. Suggested steps:
|
||||
|
||||
- Make sure VidBee is up to date.
|
||||
- Reduce the number of concurrent downloads.
|
||||
- Use cookies to reuse your signed-in session when needed.
|
||||
|
||||
## Which sites are supported?
|
||||
|
||||
VidBee uses the yt-dlp extractor system and supports many sites. Try the download first; if it fails, submit feedback with the link and error details.
|
||||
|
||||
## Why is the download speed slow?
|
||||
|
||||
- Check your network and proxy settings.
|
||||
- Avoid starting too many tasks at once.
|
||||
- Some sites limit bandwidth on their side.
|
||||
|
||||
## macOS says “file is damaged”
|
||||
|
||||
Remove the quarantine attribute and retry:
|
||||
|
||||
```bash
|
||||
xattr -rd com.apple.quarantine /Applications/VidBee.app/
|
||||
```
|
||||
20
docs/content/index.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Introduction
|
||||
description: VidBee desktop downloader documentation and FAQ
|
||||
---
|
||||
|
||||
VidBee is a desktop downloader built with Electron and powered by yt-dlp. It provides a clean interface and queue management for downloading video and audio.
|
||||
|
||||
These docs focus on real-world usage and settings, especially signed-in downloads and cookie configuration.
|
||||
|
||||
## Start here
|
||||
|
||||
- [vidbee:// Protocol](./protocol.mdx): Quick download using URL protocol.
|
||||
- [Cookies](./cookies.mdx): Configure signed-in sessions and restricted content.
|
||||
- [FAQ](./faq.mdx): Common questions and troubleshooting.
|
||||
|
||||
## Quick links
|
||||
|
||||
- [VidBee website](https://vidbee.org/)
|
||||
- [Supported sites](https://vidbee.org/supported-sites/)
|
||||
- [Features](https://vidbee.org/features/)
|
||||
4
docs/content/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "VidBee Docs",
|
||||
"pages": ["index", "protocol", "cookies", "faq"]
|
||||
}
|
||||
149
docs/content/protocol.mdx
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: vidbee:// Protocol
|
||||
description: Quick download using vidbee:// URL protocol
|
||||
---
|
||||
|
||||
VidBee registers a custom URL protocol (`vidbee://`) that allows you to trigger downloads directly from web browsers, browser extensions, or userscripts.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The `vidbee://` protocol can be used to open VidBee and automatically start downloading videos.
|
||||
|
||||
### Protocol Format
|
||||
|
||||
```
|
||||
vidbee://download?url=<encoded-video-url>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `url` (required): The video URL to download, must be URL-encoded
|
||||
|
||||
### Example
|
||||
|
||||
To download a YouTube video:
|
||||
|
||||
```html
|
||||
<a href="vidbee://download?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ">
|
||||
Download with VidBee
|
||||
</a>
|
||||
```
|
||||
|
||||
Or in JavaScript:
|
||||
|
||||
```javascript
|
||||
const videoUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
|
||||
window.location.href = vidbeeUrl
|
||||
```
|
||||
|
||||
## Opening VidBee
|
||||
|
||||
To simply open the VidBee app without starting a download:
|
||||
|
||||
```
|
||||
vidbee://
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Browser Extension
|
||||
|
||||
The VidBee browser extension uses this protocol to send the current tab's URL to the desktop app:
|
||||
|
||||
```javascript
|
||||
const currentUrl = window.location.href
|
||||
const deepLink = `vidbee://download?url=${encodeURIComponent(currentUrl)}`
|
||||
window.location.href = deepLink
|
||||
```
|
||||
|
||||
### Userscript Integration
|
||||
|
||||
The VidBee userscript adds quick download buttons to supported video sites:
|
||||
|
||||
```javascript
|
||||
// Single click triggers download via protocol
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
|
||||
window.location.href = vidbeeUrl
|
||||
```
|
||||
|
||||
### Web Pages
|
||||
|
||||
You can add direct download links to your web pages:
|
||||
|
||||
```html
|
||||
<!-- Simple link -->
|
||||
<a href="vidbee://download?url=https%3A%2F%2Fexample.com%2Fvideo">
|
||||
Download with VidBee
|
||||
</a>
|
||||
|
||||
<!-- Button with JavaScript -->
|
||||
<button onclick="openInVidBee('https://example.com/video')">
|
||||
Quick Download
|
||||
</button>
|
||||
|
||||
<script>
|
||||
function openInVidBee(url) {
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(url)}`
|
||||
window.location.href = vidbeeUrl
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Playlist Support
|
||||
|
||||
To download an entire playlist:
|
||||
|
||||
```
|
||||
vidbee://download?url=<encoded-playlist-url>&type=playlist
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `url` (required): The playlist URL, must be URL-encoded
|
||||
- `type`: Set to `playlist` to download all videos in the playlist
|
||||
|
||||
### Example
|
||||
|
||||
```javascript
|
||||
const playlistUrl = 'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf'
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(playlistUrl)}&type=playlist`
|
||||
window.location.href = vidbeeUrl
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Protocol Registration**: VidBee registers as the handler for the `vidbee://` protocol during installation
|
||||
2. **URL Parsing**: When a `vidbee://download?url=...` link is clicked, the OS launches VidBee
|
||||
3. **Queue Processing**: VidBee extracts the video URL and adds it to the download queue
|
||||
4. **Auto-start**: The download begins automatically if the app is configured for auto-download
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
The `vidbee://` protocol works across all major browsers:
|
||||
- Chrome/Edge/Brave
|
||||
- Firefox
|
||||
- Safari
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Only URLs starting with `vidbee://` will trigger the app
|
||||
- The app validates the URL format before processing
|
||||
- Malformed URLs are ignored with a warning in the logs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Protocol Not Working
|
||||
|
||||
If clicking `vidbee://` links doesn't open VidBee:
|
||||
|
||||
1. **Check installation**: Ensure VidBee is properly installed
|
||||
2. **Reinstall**: Try reinstalling VidBee to re-register the protocol
|
||||
3. **OS permissions**: On macOS, check System Settings > Privacy & Security for any blocks
|
||||
4. **Browser settings**: Some browsers may require you to allow the protocol on first use
|
||||
|
||||
### App Opens But Doesn't Download
|
||||
|
||||
If VidBee opens but the download doesn't start:
|
||||
|
||||
1. **Check URL encoding**: Ensure the video URL is properly encoded with `encodeURIComponent()`
|
||||
2. **Check logs**: Open the app and check the developer console for errors
|
||||
3. **Supported sites**: Verify the URL is from a [supported site](https://vidbee.org/supported-sites/)
|
||||
63
docs/content/zh/cookies.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Cookie 使用说明
|
||||
description: 登录下载、受限内容与 Cookie 配置
|
||||
---
|
||||
|
||||
Cookie 用于复用浏览器的登录态,帮助 VidBee 下载需要登录或验证的内容,例如订阅内容、年龄限制或私密链接。
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 需要账号登录才能观看或下载的内容。
|
||||
- 年龄限制或地区限制页面。
|
||||
- 仅对登录用户可见的私密链接或列表。
|
||||
|
||||
## VidBee 支持的两种方式
|
||||
|
||||
### 方式一:读取浏览器 Cookie
|
||||
|
||||
在 **设置** 中选择你的浏览器,VidBee 会尝试自动识别浏览器配置文件路径。你也可以手动填写配置文件路径。
|
||||
|
||||

|
||||
|
||||
**支持的浏览器(按平台差异显示):**
|
||||
|
||||
- **Windows:仅支持 Firefox。其他浏览器无法读取 Cookie。**
|
||||
- macOS:全部支持。
|
||||
- Linux:全部支持。
|
||||
|
||||
**使用步骤:**
|
||||
|
||||
1. 打开 VidBee → 设置 → Cookie。
|
||||
2. 选择浏览器并确认配置文件路径。
|
||||
3. 返回下载页面,重新开始下载任务。
|
||||
|
||||
如果识别失败或路径无效,请手动选择实际的浏览器配置文件目录。
|
||||
在 Windows 上,如果不是 Firefox,请改用 cookies 文件方式。
|
||||
|
||||
### 方式二:导入 Cookies 文件
|
||||
|
||||
你也可以导入 **Netscape 格式** 的 cookies 文件。这个方式适合在不方便读取浏览器配置文件时使用。
|
||||
|
||||

|
||||
|
||||
**使用步骤:**
|
||||
|
||||
1. 使用浏览器扩展导出 Netscape cookies 文件。
|
||||
2. 打开 VidBee → 设置 → Cookies 文件,选择导出的文件。
|
||||
3. 如需停用,可点击“清除”。
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 优先使用浏览器读取方式,维护成本更低。
|
||||
- 如果浏览器读取失败,再切换到 cookies 文件方式。
|
||||
- cookies 文件会过期,账号状态变化时需要重新导出。
|
||||
|
||||
## 常见问题
|
||||
|
||||
- **提示配置文件路径无效**:请确认路径存在,并指向实际的浏览器配置文件目录。
|
||||
- **下载仍提示需要登录**:确认当前浏览器已登录对应账号,并重新保存设置。
|
||||
- **浏览器读取失败**:关闭浏览器后重试,或改用 cookies 文件方式。
|
||||
|
||||
## 隐私与安全
|
||||
|
||||
Cookie 等同于登录态,请妥善保管,不要共享或上传。若怀疑泄露,请及时在网站上退出登录并更新密码。
|
||||
36
docs/content/zh/faq.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: 常见问题 (FAQ)
|
||||
description: VidBee 使用过程中的常见问题与建议
|
||||
---
|
||||
|
||||
## 下载失败或报错怎么办?
|
||||
|
||||
- 先确认链接是否有效,并尝试在浏览器中打开。
|
||||
- 升级 VidBee 到最新版本后重试。
|
||||
- 如果是登录或年龄限制内容,请配置 Cookie。
|
||||
|
||||
## 为什么同一个链接有时可以、有时不行?
|
||||
|
||||
部分站点会频繁更新页面结构或限制请求频率。建议:
|
||||
|
||||
- 确保 VidBee 为最新版本。
|
||||
- 适当降低同时下载任务数量。
|
||||
- 必要时使用 Cookie 复用登录态。
|
||||
|
||||
## 支持哪些网站?
|
||||
|
||||
VidBee 基于 yt-dlp 解析器体系,覆盖大量站点。可以先尝试下载;若失败,请提交反馈并附上链接与错误提示。
|
||||
|
||||
## 下载速度慢怎么办?
|
||||
|
||||
- 检查本地网络与代理设置。
|
||||
- 避免同时发起过多任务。
|
||||
- 某些站点本身带宽较低,速度受限。
|
||||
|
||||
## macOS 提示“文件已损坏”
|
||||
|
||||
请执行以下命令移除隔离标记后重试:
|
||||
|
||||
```bash
|
||||
xattr -rd com.apple.quarantine /Applications/VidBee.app/
|
||||
```
|
||||
20
docs/content/zh/index.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: 简介
|
||||
description: VidBee 桌面下载器使用说明与常见问题
|
||||
---
|
||||
|
||||
VidBee 是一款基于 Electron 的桌面下载器,内置 yt-dlp 引擎,提供清爽的界面与队列管理能力,用于下载视频与音频内容。
|
||||
|
||||
这份文档聚焦 VidBee 的实际使用场景与设置说明,尤其是登录下载与 Cookie 相关配置。
|
||||
|
||||
## 从这里开始
|
||||
|
||||
- [vidbee:// 协议](./protocol.mdx):使用 URL 协议快速下载。
|
||||
- [Cookie 使用](./cookies.mdx):登录态与限制内容的下载配置。
|
||||
- [常见问题](./faq.mdx):常见问题与排查思路。
|
||||
|
||||
## 快捷链接
|
||||
|
||||
- [VidBee 官网](https://vidbee.org/)
|
||||
- [支持站点](https://vidbee.org/supported-sites/)
|
||||
- [功能介绍](https://vidbee.org/features/)
|
||||
4
docs/content/zh/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "VidBee 文档",
|
||||
"pages": ["index", "protocol", "cookies", "faq"]
|
||||
}
|
||||
149
docs/content/zh/protocol.mdx
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: vidbee:// 协议
|
||||
description: 使用 vidbee:// URL 协议快速下载
|
||||
---
|
||||
|
||||
VidBee 注册了自定义 URL 协议(`vidbee://`),允许您直接从网页浏览器、浏览器扩展或用户脚本触发下载。
|
||||
|
||||
## 基本用法
|
||||
|
||||
`vidbee://` 协议可用于打开 VidBee 并自动开始下载视频。
|
||||
|
||||
### 协议格式
|
||||
|
||||
```
|
||||
vidbee://download?url=<编码后的视频URL>
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `url`(必需):要下载的视频 URL,必须经过 URL 编码
|
||||
|
||||
### 示例
|
||||
|
||||
下载 YouTube 视频:
|
||||
|
||||
```html
|
||||
<a href="vidbee://download?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ">
|
||||
使用 VidBee 下载
|
||||
</a>
|
||||
```
|
||||
|
||||
或使用 JavaScript:
|
||||
|
||||
```javascript
|
||||
const videoUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
|
||||
window.location.href = vidbeeUrl
|
||||
```
|
||||
|
||||
## 打开 VidBee
|
||||
|
||||
仅打开 VidBee 应用而不开始下载:
|
||||
|
||||
```
|
||||
vidbee://
|
||||
```
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 浏览器扩展
|
||||
|
||||
VidBee 浏览器扩展使用此协议将当前标签页的 URL 发送到桌面应用:
|
||||
|
||||
```javascript
|
||||
const currentUrl = window.location.href
|
||||
const deepLink = `vidbee://download?url=${encodeURIComponent(currentUrl)}`
|
||||
window.location.href = deepLink
|
||||
```
|
||||
|
||||
### 用户脚本集成
|
||||
|
||||
VidBee 用户脚本在支持的视频网站上添加快速下载按钮:
|
||||
|
||||
```javascript
|
||||
// 单击通过协议触发下载
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
|
||||
window.location.href = vidbeeUrl
|
||||
```
|
||||
|
||||
### 网页集成
|
||||
|
||||
您可以在网页中添加直接下载链接:
|
||||
|
||||
```html
|
||||
<!-- 简单链接 -->
|
||||
<a href="vidbee://download?url=https%3A%2F%2Fexample.com%2Fvideo">
|
||||
使用 VidBee 下载
|
||||
</a>
|
||||
|
||||
<!-- 带 JavaScript 的按钮 -->
|
||||
<button onclick="openInVidBee('https://example.com/video')">
|
||||
快速下载
|
||||
</button>
|
||||
|
||||
<script>
|
||||
function openInVidBee(url) {
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(url)}`
|
||||
window.location.href = vidbeeUrl
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 播放列表支持
|
||||
|
||||
下载整个播放列表:
|
||||
|
||||
```
|
||||
vidbee://download?url=<编码后的播放列表URL>&type=playlist
|
||||
```
|
||||
|
||||
**参数:**
|
||||
- `url`(必需):播放列表 URL,必须经过 URL 编码
|
||||
- `type`:设置为 `playlist` 以下载播放列表中的所有视频
|
||||
|
||||
### 示例
|
||||
|
||||
```javascript
|
||||
const playlistUrl = 'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf'
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(playlistUrl)}&type=playlist`
|
||||
window.location.href = vidbeeUrl
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **协议注册**:VidBee 在安装过程中注册为 `vidbee://` 协议的处理程序
|
||||
2. **URL 解析**:当点击 `vidbee://download?url=...` 链接时,操作系统会启动 VidBee
|
||||
3. **队列处理**:VidBee 提取视频 URL 并将其添加到下载队列
|
||||
4. **自动开始**:如果应用配置为自动下载,下载会自动开始
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
`vidbee://` 协议适用于所有主流浏览器:
|
||||
- Chrome/Edge/Brave
|
||||
- Firefox
|
||||
- Safari
|
||||
|
||||
## 安全说明
|
||||
|
||||
- 仅以 `vidbee://` 开头的 URL 会触发应用
|
||||
- 应用在处理前会验证 URL 格式
|
||||
- 格式错误的 URL 将被忽略,并在日志中显示警告
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 协议不工作
|
||||
|
||||
如果点击 `vidbee://` 链接无法打开 VidBee:
|
||||
|
||||
1. **检查安装**:确保 VidBee 已正确安装
|
||||
2. **重新安装**:尝试重新安装 VidBee 以重新注册协议
|
||||
3. **操作系统权限**:在 macOS 上,检查系统设置 > 隐私与安全性 是否有任何阻止
|
||||
4. **浏览器设置**:某些浏览器可能需要您在首次使用时允许该协议
|
||||
|
||||
### 应用打开但未下载
|
||||
|
||||
如果 VidBee 打开但下载未开始:
|
||||
|
||||
1. **检查 URL 编码**:确保视频 URL 使用 `encodeURIComponent()` 正确编码
|
||||
2. **检查日志**:打开应用并检查开发者控制台是否有错误
|
||||
3. **支持的网站**:验证 URL 是否来自[支持的网站](https://vidbee.org/supported-sites/)
|
||||
16
docs/next.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createMDX } from 'fumadocs-mdx/next';
|
||||
|
||||
const withMDX = createMDX();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
// Only use static export for production builds, not dev mode
|
||||
output: process.env.NODE_ENV === 'production' ? 'export' : undefined,
|
||||
reactStrictMode: true,
|
||||
// Use trailing slashes to avoid conflicts with route handlers that have file extensions
|
||||
trailingSlash: true,
|
||||
// Note: rewrites are not supported with static export
|
||||
// The /llms.mdx route will be pre-rendered as static files
|
||||
};
|
||||
|
||||
export default withMDX(config);
|
||||
36
docs/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "next build && node scripts/post-export.js",
|
||||
"dev": "next dev",
|
||||
"start": "next start",
|
||||
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"lint": "biome check --config-path biome.config.json",
|
||||
"format": "biome format --write --config-path biome.config.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"fumadocs-core": "16.4.7",
|
||||
"fumadocs-mdx": "14.2.5",
|
||||
"fumadocs-ui": "16.4.7",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.0.5",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"@biomejs/biome": "^2.3.11"
|
||||
}
|
||||
}
|
||||
3989
docs/pnpm-lock.yaml
generated
Normal file
5
docs/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
101
docs/public/ICONS.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# VidBee Documentation Icons
|
||||
|
||||
This directory contains the VidBee logo in various sizes for use in the documentation site.
|
||||
|
||||
## Source
|
||||
|
||||
All icons are generated from the original VidBee application icon located at:
|
||||
`/Users/air15/Documents/GitHub/VidBee/build/icon.png`
|
||||
|
||||
## Available Sizes
|
||||
|
||||
| Filename | Size | Use Case |
|
||||
|----------|------|----------|
|
||||
| `icon.png` | 512×512 | Default/Original icon |
|
||||
| `icon-16.png` | 16×16 | Browser favicon (small) |
|
||||
| `icon-32.png` | 32×32 | Browser favicon (standard) |
|
||||
| `icon-48.png` | 48×48 | Browser favicon (large) |
|
||||
| `icon-64.png` | 64×64 | Small UI elements |
|
||||
| `icon-128.png` | 128×128 | Medium UI elements |
|
||||
| `icon-192.png` | 192×192 | PWA icon (Android) |
|
||||
| `icon-256.png` | 256×256 | Large UI elements |
|
||||
| `icon-512.png` | 512×512 | PWA splash screen, high-res displays |
|
||||
| `apple-touch-icon.png` | 180×180 | iOS/macOS home screen icon |
|
||||
| `favicon.png` | 32×32 | Standard favicon |
|
||||
|
||||
## Usage in Next.js
|
||||
|
||||
### In `app/layout.tsx` or `app/favicon.ico`:
|
||||
|
||||
```tsx
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'VidBee Documentation',
|
||||
description: 'Official VidBee documentation',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icon-16.png', sizes: '16x16', type: 'image/png' },
|
||||
{ url: '/icon-32.png', sizes: '32x32', type: 'image/png' },
|
||||
{ url: '/icon-48.png', sizes: '48x48', type: 'image/png' },
|
||||
],
|
||||
apple: [
|
||||
{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### For PWA (Progressive Web App):
|
||||
|
||||
Add to your `manifest.json` or `site.webmanifest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Regeneration
|
||||
|
||||
If you need to regenerate these icons from the source:
|
||||
|
||||
```bash
|
||||
cd docs/public
|
||||
SOURCE="/Users/air15/Documents/GitHub/VidBee/build/icon.png"
|
||||
|
||||
# Copy original
|
||||
cp $SOURCE icon-original.png
|
||||
|
||||
# Generate sizes
|
||||
sips -z 16 16 icon-original.png --out icon-16.png
|
||||
sips -z 32 32 icon-original.png --out icon-32.png
|
||||
sips -z 48 48 icon-original.png --out icon-48.png
|
||||
sips -z 64 64 icon-original.png --out icon-64.png
|
||||
sips -z 128 128 icon-original.png --out icon-128.png
|
||||
sips -z 192 192 icon-original.png --out icon-192.png
|
||||
sips -z 256 256 icon-original.png --out icon-256.png
|
||||
sips -z 180 180 icon-original.png --out apple-touch-icon.png
|
||||
|
||||
# Create standard copies
|
||||
cp icon-original.png icon-512.png
|
||||
cp icon-original.png icon.png
|
||||
cp icon-32.png favicon.png
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All icons maintain the VidBee bee/honeycomb theme
|
||||
- Icons use PNG format with transparency (RGBA)
|
||||
- Generated using macOS `sips` tool for quality consistency
|
||||
BIN
docs/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
docs/public/browser-cookies.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/public/cookies-file.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/public/icon-128.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
docs/public/icon-16.png
Normal file
|
After Width: | Height: | Size: 924 B |
BIN
docs/public/icon-192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/public/icon-256.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/public/icon-32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/public/icon-48.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
docs/public/icon-64.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
docs/public/icon-original.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/public/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
46
docs/scripts/post-export.js
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Post-export script to copy English content to root directory
|
||||
* This allows the default language (en) to be accessible without language prefix
|
||||
*/
|
||||
|
||||
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const outDir = join(__dirname, '../out');
|
||||
const enDir = join(outDir, 'en');
|
||||
|
||||
console.log('Copying English content to root directory...');
|
||||
|
||||
if (!existsSync(enDir)) {
|
||||
console.error('Error: /en directory not found in output');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get all items in /en directory
|
||||
const fs = await import('node:fs/promises');
|
||||
const items = await fs.readdir(enDir);
|
||||
|
||||
// Copy each item to root, excluding already existing root items
|
||||
for (const item of items) {
|
||||
const source = join(enDir, item);
|
||||
const dest = join(outDir, item);
|
||||
|
||||
// Skip if item already exists at root (like _next, api, etc.)
|
||||
if (existsSync(dest)) {
|
||||
console.log(`Skipping ${item} (already exists at root)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
cpSync(source, dest, { recursive: true });
|
||||
console.log(`Copied ${item}`);
|
||||
} catch (error) {
|
||||
console.error(`Error copying ${item}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('English content copied to root directory successfully!');
|
||||
22
docs/source.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineConfig, defineDocs, frontmatterSchema, metaSchema } from 'fumadocs-mdx/config';
|
||||
|
||||
// You can customise Zod schemas for frontmatter and `meta.json` here
|
||||
// see https://fumadocs.dev/docs/mdx/collections
|
||||
export const docs = defineDocs({
|
||||
dir: 'content',
|
||||
docs: {
|
||||
schema: frontmatterSchema,
|
||||
postprocess: {
|
||||
includeProcessedMarkdown: true,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
schema: metaSchema,
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
// MDX options
|
||||
},
|
||||
});
|
||||
73
docs/src/app/[lang]/(docs)/[[...slug]]/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import type { Metadata } from 'next';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { GitHubEditButton, LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
|
||||
|
||||
export default async function Page(props: PageProps<'/[lang]/[[...slug]]'>) {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
const gitConfig = {
|
||||
user: 'nexmoe',
|
||||
repo: 'VidBee',
|
||||
branch: 'main',
|
||||
};
|
||||
|
||||
const githubFilePath = `docs/content/${page.path}`;
|
||||
const githubBlobUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/${githubFilePath}`;
|
||||
const githubEditUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/edit/${gitConfig.branch}/${githubFilePath}`;
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
|
||||
<div className="flex flex-row gap-2 items-center border-b pb-6">
|
||||
<GitHubEditButton href={githubEditUrl} />
|
||||
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
|
||||
<ViewOptions
|
||||
markdownUrl={`${page.url}.mdx`}
|
||||
githubUrl={githubBlobUrl}
|
||||
/>
|
||||
</div>
|
||||
<DocsBody>
|
||||
<MDX
|
||||
components={getMDXComponents({
|
||||
// this allows you to link to other pages with relative file paths
|
||||
a: createRelativeLink(source, page),
|
||||
})}
|
||||
/>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
props: PageProps<'/[lang]/[[...slug]]'>,
|
||||
): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
const baseUrl = 'https://docs.vidbee.org';
|
||||
const canonicalUrl = `${baseUrl}${page.url}`;
|
||||
|
||||
return {
|
||||
title: `${page.data.title} | VidBee Docs`,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: getPageImage(page).url,
|
||||
},
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
20
docs/src/app/[lang]/(docs)/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
params: Promise<{ lang: string; slug?: string[] }>;
|
||||
}) {
|
||||
const resolvedParams = await params;
|
||||
const locale = resolvedParams.lang;
|
||||
return (
|
||||
<DocsLayout tree={source.getPageTree(locale)} {...baseOptions(locale)}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
39
docs/src/app/[lang]/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { defineI18nUI } from 'fumadocs-ui/i18n';
|
||||
import { i18n, isLocale } from '@/lib/i18n';
|
||||
|
||||
const { provider } = defineI18nUI(i18n, {
|
||||
translations: {
|
||||
en: {
|
||||
displayName: 'English',
|
||||
},
|
||||
zh: {
|
||||
displayName: '中文',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
params: Promise<{ lang: string }>;
|
||||
}) {
|
||||
const { lang } = await params;
|
||||
const locale = isLocale(lang) ? lang : i18n.defaultLanguage;
|
||||
|
||||
return (
|
||||
<RootProvider
|
||||
i18n={provider(locale)}
|
||||
search={{
|
||||
options: {
|
||||
type: 'static',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RootProvider>
|
||||
);
|
||||
}
|
||||
14
docs/src/app/api/search/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
// statically cached for static export
|
||||
export const revalidate = false;
|
||||
|
||||
// Configure language support for both English and Chinese
|
||||
export const { staticGET: GET } = createFromSource(source, {
|
||||
localeMap: {
|
||||
en: { language: 'english' },
|
||||
// Chinese is not natively supported by Orama, use English tokenizer for zh
|
||||
zh: { language: 'english' },
|
||||
},
|
||||
});
|
||||
3
docs/src/app/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
39
docs/src/app/layout.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import './global.css';
|
||||
import { Inter } from 'next/font/google';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import { i18n, isLocale } from '@/lib/i18n';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.png', sizes: '32x32', type: 'image/png' },
|
||||
{ url: '/icon-16.png', sizes: '16x16', type: 'image/png' },
|
||||
{ url: '/icon-32.png', sizes: '32x32', type: 'image/png' },
|
||||
{ url: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||
],
|
||||
apple: '/apple-touch-icon.png',
|
||||
},
|
||||
};
|
||||
|
||||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
params?: Promise<{ lang?: string }>;
|
||||
}) {
|
||||
const resolvedParams = params ? await params : undefined;
|
||||
const lang = isLocale(resolvedParams?.lang) ? resolvedParams.lang : i18n.defaultLanguage;
|
||||
return (
|
||||
<html lang={lang} className={inter.className} suppressHydrationWarning>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
34
docs/src/app/sitemap.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
const baseUrl = 'https://docs.vidbee.org';
|
||||
|
||||
function buildPath(segments: string[]): string {
|
||||
if (segments.length === 0) {
|
||||
return '/';
|
||||
}
|
||||
return `/${segments.join('/')}/`;
|
||||
}
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const params = source.generateParams();
|
||||
|
||||
return params.map(({ lang, slug }) => {
|
||||
const segments: string[] = [];
|
||||
|
||||
if (lang && lang !== i18n.defaultLanguage) {
|
||||
segments.push(lang);
|
||||
}
|
||||
|
||||
if (slug && slug.length > 0) {
|
||||
segments.push(...slug);
|
||||
}
|
||||
|
||||
return {
|
||||
url: `${baseUrl}${buildPath(segments)}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
250
docs/src/components/ai/page-actions.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Check, ChevronDown, Copy, ExternalLinkIcon, MessageCircleIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
export function LLMCopyButton({
|
||||
/**
|
||||
* A URL to fetch the raw Markdown/MDX content of page
|
||||
*/
|
||||
markdownUrl,
|
||||
}: {
|
||||
markdownUrl: string;
|
||||
}) {
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [checked, onClick] = useCopyButton(async () => {
|
||||
const cached = cache.get(markdownUrl);
|
||||
if (cached) return navigator.clipboard.writeText(cached);
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'text/plain': fetch(markdownUrl).then(async (res) => {
|
||||
const content = await res.text();
|
||||
cache.set(markdownUrl, content);
|
||||
|
||||
return content;
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
className: 'gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{checked ? <Check /> : <Copy />}
|
||||
Copy Markdown
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ViewOptions({
|
||||
markdownUrl,
|
||||
githubUrl,
|
||||
}: {
|
||||
/**
|
||||
* A URL to the raw Markdown/MDX content of page
|
||||
*/
|
||||
markdownUrl: string;
|
||||
|
||||
/**
|
||||
* Source file URL on GitHub
|
||||
*/
|
||||
githubUrl: string;
|
||||
}) {
|
||||
const items = useMemo(() => {
|
||||
const fullMarkdownUrl =
|
||||
typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
|
||||
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Open in GitHub',
|
||||
href: githubUrl,
|
||||
icon: (
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
|
||||
<title>GitHub</title>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Open in Scira AI',
|
||||
href: `https://scira.ai/?${new URLSearchParams({
|
||||
q,
|
||||
})}`,
|
||||
icon: (
|
||||
<svg
|
||||
width="910"
|
||||
height="934"
|
||||
viewBox="0 0 910 934"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Scira AI</title>
|
||||
<path
|
||||
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
|
||||
stroke="currentColor"
|
||||
strokeWidth="30"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Open in ChatGPT',
|
||||
href: `https://chatgpt.com/?${new URLSearchParams({
|
||||
hints: 'search',
|
||||
q,
|
||||
})}`,
|
||||
icon: (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>OpenAI</title>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Open in Claude',
|
||||
href: `https://claude.ai/new?${new URLSearchParams({
|
||||
q,
|
||||
})}`,
|
||||
icon: (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Anthropic</title>
|
||||
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Open in T3 Chat',
|
||||
href: `https://t3.chat/new?${new URLSearchParams({
|
||||
q,
|
||||
})}`,
|
||||
icon: <MessageCircleIcon />,
|
||||
},
|
||||
];
|
||||
}, [githubUrl, markdownUrl]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
className: 'gap-2',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
Open
|
||||
<ChevronDown className="size-3.5 text-fd-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex flex-col">
|
||||
{items.map((item) => (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4"
|
||||
>
|
||||
{item.icon}
|
||||
{item.title}
|
||||
<ExternalLinkIcon className="text-fd-muted-foreground size-3.5 ms-auto" />
|
||||
</a>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitHubEditButton({ href }: { href: string }) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
className: 'gap-2',
|
||||
})
|
||||
)}
|
||||
>
|
||||
<ExternalLinkIcon className="size-3.5 text-fd-muted-foreground" />
|
||||
Edit on GitHub
|
||||
</a>
|
||||
)
|
||||
}
|
||||
1
docs/src/lib/cn.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { twMerge as cn } from 'tailwind-merge';
|
||||
34
docs/src/lib/i18n.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
|
||||
export const i18n = defineI18n({
|
||||
languages: ['en', 'zh'],
|
||||
defaultLanguage: 'en',
|
||||
// Hide locale prefix for default language (en) so English content appears at root
|
||||
hideLocale: 'default-locale',
|
||||
parser: 'dir'
|
||||
});
|
||||
|
||||
export type Locale = (typeof i18n.languages)[number];
|
||||
|
||||
const localeSet = new Set(i18n.languages);
|
||||
|
||||
export function isLocale(value?: string): value is Locale {
|
||||
return Boolean(value && localeSet.has(value as Locale));
|
||||
}
|
||||
|
||||
export function resolveLocaleFromSlug(slug?: string[]): Locale {
|
||||
if (slug && slug.length > 0 && isLocale(slug[0])) {
|
||||
return slug[0];
|
||||
}
|
||||
return i18n.defaultLanguage;
|
||||
}
|
||||
|
||||
export function stripLocaleFromSlug(slug?: string[]): string[] {
|
||||
if (!slug || slug.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (isLocale(slug[0])) {
|
||||
return slug.slice(1);
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
11
docs/src/lib/layout.shared.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
|
||||
export function baseOptions(_locale: string): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
title: 'VidBee',
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
}
|
||||
31
docs/src/lib/source.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { docs } from 'fumadocs-mdx:collections/server';
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
|
||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
baseUrl: '/',
|
||||
i18n,
|
||||
source: docs.toFumadocsSource(),
|
||||
plugins: [lucideIconsPlugin()],
|
||||
});
|
||||
|
||||
export function getPageImage(page: InferPageType<typeof source>) {
|
||||
const localePrefix =
|
||||
page.locale && page.locale !== i18n.defaultLanguage ? [page.locale] : [];
|
||||
const segments = [...localePrefix, ...page.slugs, 'image.png'];
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: `/og/docs/${segments.join('/')}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
const processed = await page.data.getText('processed');
|
||||
|
||||
return `# ${page.data.title}
|
||||
|
||||
${processed}`;
|
||||
}
|
||||
9
docs/src/mdx-components.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...defaultMdxComponents,
|
||||
...components,
|
||||
};
|
||||
}
|
||||
9
docs/src/middleware.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
|
||||
export default createI18nMiddleware(i18n);
|
||||
|
||||
export const config = {
|
||||
// Note: Middleware doesn't run in static export, but kept for development
|
||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
||||
};
|
||||
46
docs/tsconfig.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"fumadocs-mdx:collections/*": [
|
||||
".source/*"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
7
drizzle.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/main/lib/database/schema.ts',
|
||||
out: './resources/drizzle',
|
||||
dialect: 'sqlite'
|
||||
})
|
||||
@@ -2,13 +2,23 @@ appId: com.vidbee
|
||||
productName: VidBee
|
||||
directories:
|
||||
buildResources: build
|
||||
afterPack: build/after-pack.cjs
|
||||
protocols:
|
||||
- name: VidBee
|
||||
schemes:
|
||||
- vidbee
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!**/.context/**'
|
||||
- '!**/.github/**'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintcache,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
- '!extension/**'
|
||||
- '!monkey/**'
|
||||
- '!screenshots/**'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
@@ -19,16 +29,25 @@ nsis:
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
hardenedRuntime: true
|
||||
entitlements: build/entitlements.mac.plist
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
notarize: true
|
||||
artifactName: ${name}-${version}-${arch}.${ext}
|
||||
target:
|
||||
- target: zip
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
- target: dmg
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: yourname@example.com
|
||||
maintainer: nexmoex@gmail.com
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
@@ -38,3 +57,5 @@ publish:
|
||||
url: https://github.com/nexmoe/vidbee/releases/latest/download
|
||||
electronDownload:
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
electronLanguages:
|
||||
- en
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
29
extension/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
.env.*
|
||||
54
extension/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# VidBee Video Downloader Extension
|
||||
|
||||
VidBee Video Downloader is a lightweight browser companion for the VidBee desktop app. It detects the video on your current tab, shows the available formats, and hands the URL to VidBee so the download happens in the desktop app instead of your browser.
|
||||
|
||||
## Why install it?
|
||||
|
||||
- **One-click handoff:** Send the current video page to VidBee without copy-pasting links.
|
||||
- **See formats before downloading:** Preview resolutions, file sizes, and audio-only options in the popup.
|
||||
- **More reliable downloads:** VidBee handles large files and multi-format downloads better than most browsers.
|
||||
- **Works on 1,000+ sites:** The extension uses the same site support as the VidBee app.
|
||||
|
||||
## What it does
|
||||
|
||||
1. Reads the active tab URL when you open the popup.
|
||||
2. Asks the VidBee desktop app (running locally) to analyze the video.
|
||||
3. Displays the formats it finds and lets you open VidBee to download.
|
||||
|
||||
## Requirements
|
||||
|
||||
- VidBee desktop app installed.
|
||||
- VidBee running while you use the extension.
|
||||
|
||||
## How to use
|
||||
|
||||
1. Open a supported video page.
|
||||
2. Click the VidBee extension icon.
|
||||
3. Review available formats.
|
||||
4. Click **Download with VidBee** to start the download in the desktop app.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Build or package:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm zip
|
||||
```
|
||||
|
||||
Firefox builds:
|
||||
|
||||
```bash
|
||||
pnpm dev:firefox
|
||||
pnpm build:firefox
|
||||
pnpm zip:firefox
|
||||
```
|
||||
|
||||
## Notes on privacy
|
||||
|
||||
The extension only sends the current tab URL to the local VidBee app on `127.0.0.1` and stores temporary results in browser storage for faster reloads.
|
||||
163
extension/assets/content.css
Normal file
@@ -0,0 +1,163 @@
|
||||
.vidbee-download-container {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 9999;
|
||||
transition: all 0.2s ease;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.vidbee-download-container.vidbee-hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.vidbee-download-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
font-size: 0;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.6;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.vidbee-download-button:hover {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.vidbee-download-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.vidbee-download-button svg {
|
||||
flex-shrink: 0;
|
||||
stroke: currentColor;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.vidbee-tooltip {
|
||||
position: absolute;
|
||||
right: calc(100% + 8px);
|
||||
top: 50%;
|
||||
padding: 6px 10px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
transform: translateY(-50%) translateX(4px);
|
||||
z-index: 10000;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.vidbee-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-left-color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.vidbee-download-button:hover .vidbee-tooltip {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
}
|
||||
|
||||
.vidbee-close-button {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
background: rgba(255, 77, 77, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
font-size: 0;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.vidbee-download-container:hover .vidbee-close-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.vidbee-close-button:hover {
|
||||
background: rgba(255, 77, 77, 1);
|
||||
transform: scale(1.15);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.vidbee-close-button:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.vidbee-close-button svg {
|
||||
flex-shrink: 0;
|
||||
stroke: currentColor;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.vidbee-close-button .vidbee-tooltip {
|
||||
right: calc(100% + 6px);
|
||||
top: 50%;
|
||||
left: auto;
|
||||
transform: translateY(-50%) translateX(4px);
|
||||
}
|
||||
|
||||
.vidbee-close-button .vidbee-tooltip::after {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
right: auto;
|
||||
transform: translateY(-50%);
|
||||
border-left-color: rgba(0, 0, 0, 0.9);
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
.vidbee-download-container:hover .vidbee-close-button:hover .vidbee-tooltip {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
}
|
||||
203
extension/entrypoints/background.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
type VideoFormat = {
|
||||
format_id?: string
|
||||
ext?: string
|
||||
format_note?: string
|
||||
resolution?: string
|
||||
width?: number
|
||||
height?: number
|
||||
fps?: number
|
||||
vcodec?: string
|
||||
acodec?: string
|
||||
filesize?: number
|
||||
filesize_approx?: number
|
||||
tbr?: number
|
||||
}
|
||||
|
||||
type VideoInfo = {
|
||||
title?: string
|
||||
thumbnail?: string
|
||||
duration?: number
|
||||
formats?: VideoFormat[]
|
||||
}
|
||||
|
||||
type VideoInfoCacheEntry = {
|
||||
url: string
|
||||
status: 'pending' | 'ready' | 'error'
|
||||
fetchedAt: number
|
||||
info?: VideoInfo
|
||||
error?: string
|
||||
}
|
||||
|
||||
const PORT_RANGE_START = 27100
|
||||
const PORT_RANGE_END = 27120
|
||||
const STATUS_TIMEOUT_MS = 800
|
||||
const INFO_TIMEOUT_MS = 60000
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000
|
||||
|
||||
const pendingRequests = new Map<string, Promise<void>>()
|
||||
const defaultIconPaths = {
|
||||
16: 'icon/16.png',
|
||||
32: 'icon/32.png',
|
||||
48: 'icon/48.png',
|
||||
128: 'icon/128.png'
|
||||
}
|
||||
const loadingIconPaths = {
|
||||
16: 'icon/icon-loading-16.png',
|
||||
32: 'icon/icon-loading-32.png',
|
||||
48: 'icon/icon-loading-48.png',
|
||||
128: 'icon/icon-loading-128.png'
|
||||
}
|
||||
const successIconPaths = {
|
||||
16: 'icon/icon-success-16.png',
|
||||
32: 'icon/icon-success-32.png',
|
||||
48: 'icon/icon-success-48.png',
|
||||
128: 'icon/icon-success-128.png'
|
||||
}
|
||||
|
||||
const setActionIcon = (status: 'default' | 'loading' | 'success', tabId?: number): void => {
|
||||
const paths =
|
||||
status === 'loading'
|
||||
? loadingIconPaths
|
||||
: status === 'success'
|
||||
? successIconPaths
|
||||
: defaultIconPaths
|
||||
const options = tabId ? { path: paths, tabId } : { path: paths }
|
||||
void browser.action.setIcon(options)
|
||||
}
|
||||
|
||||
const fetchJson = async <T>(url: string, timeoutMs: number): Promise<T> => {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort('timeout'), timeoutMs)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal })
|
||||
const data = (await response.json().catch(() => null)) as (T & { error?: string }) | null
|
||||
if (!response.ok) {
|
||||
const message = data && typeof data === 'object' && 'error' in data ? data.error : null
|
||||
const details = data && typeof data === 'object' && 'details' in data ? data.details : null
|
||||
const combined = [message, details].filter(Boolean).join('\n\n')
|
||||
throw new Error(combined || `Request failed: ${response.status}`)
|
||||
}
|
||||
return data as T
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
throw new Error('Request timed out.')
|
||||
}
|
||||
if (error instanceof Error && error.message.includes('signal is aborted')) {
|
||||
throw new Error('Request timed out.')
|
||||
}
|
||||
if (error instanceof Error && error.message.includes('Failed to fetch')) {
|
||||
throw new Error('VidBee app not responding on this port.')
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
const findAvailablePort = async (): Promise<number | null> => {
|
||||
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port += 1) {
|
||||
const baseUrl = `http://127.0.0.1:${port}`
|
||||
try {
|
||||
await fetchJson<{ ok: boolean }>(`${baseUrl}/status`, STATUS_TIMEOUT_MS)
|
||||
return port
|
||||
} catch {
|
||||
// Keep scanning.
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const requestVideoInfo = async (targetUrl: string): Promise<VideoInfo> => {
|
||||
const port = await findAvailablePort()
|
||||
if (!port) {
|
||||
throw new Error('VidBee app not found on localhost.')
|
||||
}
|
||||
|
||||
const baseUrl = `http://127.0.0.1:${port}`
|
||||
const tokenResponse = await fetchJson<{ token?: string }>(`${baseUrl}/token`, STATUS_TIMEOUT_MS)
|
||||
if (!tokenResponse.token) {
|
||||
throw new Error('Failed to acquire token from VidBee.')
|
||||
}
|
||||
|
||||
return fetchJson<VideoInfo>(
|
||||
`${baseUrl}/video-info?url=${encodeURIComponent(targetUrl)}&token=${encodeURIComponent(
|
||||
tokenResponse.token
|
||||
)}`,
|
||||
INFO_TIMEOUT_MS
|
||||
)
|
||||
}
|
||||
|
||||
const getCacheMap = async (): Promise<Record<string, VideoInfoCacheEntry>> => {
|
||||
const data = await browser.storage.local.get('videoInfoCacheByUrl')
|
||||
const map = data.videoInfoCacheByUrl as Record<string, VideoInfoCacheEntry> | undefined
|
||||
if (!map) return {}
|
||||
return map
|
||||
}
|
||||
|
||||
const pruneCache = (map: Record<string, VideoInfoCacheEntry>): void => {
|
||||
const now = Date.now()
|
||||
for (const [key, entry] of Object.entries(map)) {
|
||||
if (now - entry.fetchedAt > CACHE_TTL_MS) {
|
||||
delete map[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadCache = async (url: string): Promise<VideoInfoCacheEntry | null> => {
|
||||
const map = await getCacheMap()
|
||||
pruneCache(map)
|
||||
const cache = map[url]
|
||||
if (!cache) return null
|
||||
return cache
|
||||
}
|
||||
|
||||
const saveCacheEntry = async (cache: VideoInfoCacheEntry): Promise<void> => {
|
||||
const map = await getCacheMap()
|
||||
pruneCache(map)
|
||||
map[cache.url] = cache
|
||||
await browser.storage.local.set({ videoInfoCacheByUrl: map })
|
||||
}
|
||||
|
||||
const fetchAndCache = async (url: string, tabId?: number): Promise<void> => {
|
||||
if (pendingRequests.has(url)) {
|
||||
return pendingRequests.get(url) as Promise<void>
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
const existing = await loadCache(url)
|
||||
if (existing?.status === 'ready') {
|
||||
setActionIcon('success', tabId)
|
||||
return
|
||||
}
|
||||
|
||||
setActionIcon('loading', tabId)
|
||||
await saveCacheEntry({ url, status: 'pending', fetchedAt: Date.now() })
|
||||
|
||||
try {
|
||||
const info = await requestVideoInfo(url)
|
||||
await saveCacheEntry({ url, status: 'ready', fetchedAt: Date.now(), info })
|
||||
setActionIcon('success', tabId)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to fetch video info.'
|
||||
await saveCacheEntry({ url, status: 'error', fetchedAt: Date.now(), error: message })
|
||||
setActionIcon('default', tabId)
|
||||
}
|
||||
})()
|
||||
|
||||
pendingRequests.set(url, task)
|
||||
try {
|
||||
await task
|
||||
} finally {
|
||||
pendingRequests.delete(url)
|
||||
}
|
||||
}
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onMessage.addListener((message: { type?: string; url?: string }, sender) => {
|
||||
if (message.type !== 'video-info:fetch' || !message.url) {
|
||||
return
|
||||
}
|
||||
void fetchAndCache(message.url, sender.tab?.id)
|
||||
})
|
||||
})
|
||||
317
extension/entrypoints/popup/App.css
Normal file
@@ -0,0 +1,317 @@
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #111111;
|
||||
--fg-secondary: #757575;
|
||||
--border: #f0f0f0;
|
||||
--accent: #000000;
|
||||
--error: #e00000;
|
||||
--success: #00c853;
|
||||
--warning: #ffd600;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 360px;
|
||||
min-height: 200px;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--fg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--border);
|
||||
}
|
||||
.status-dot.loading { background-color: var(--warning); box-shadow: 0 0 4px var(--warning); }
|
||||
.status-dot.ok { background-color: var(--success); }
|
||||
.status-dot.error { background-color: var(--error); }
|
||||
|
||||
.video-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 90px;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.video-details h2 {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
font-size: 12px;
|
||||
color: var(--fg-secondary);
|
||||
margin: 0 0 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 90px;
|
||||
height: 50px;
|
||||
background: var(--border);
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.formats-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.format-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Sticky headers for long lists */
|
||||
.sticky-title {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg);
|
||||
padding: 4px 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--fg-secondary);
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--border);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.format-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.format-table th {
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
color: var(--fg-secondary);
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.format-table td {
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--fg);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.format-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.col-id { width: 50px; color: var(--fg-secondary); }
|
||||
.col-ext { width: 50px; }
|
||||
.col-size { width: 60px; text-align: right; }
|
||||
.format-table th.col-size { text-align: right; }
|
||||
|
||||
.empty-state {
|
||||
font-size: 12px;
|
||||
color: var(--fg-secondary);
|
||||
padding: 12px 0;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
font-size: 12px;
|
||||
color: var(--error);
|
||||
line-height: 1.5;
|
||||
background: rgba(224, 0, 0, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
background: var(--fg);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.primary-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 12px;
|
||||
color: var(--fg-secondary);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.action-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--fg-secondary);
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
border: 1px solid var(--fg);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2.5px solid var(--border);
|
||||
border-top-color: var(--fg);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--fg-secondary);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
473
extension/entrypoints/popup/App.tsx
Normal file
@@ -0,0 +1,473 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import './App.css'
|
||||
|
||||
type VideoFormat = {
|
||||
format_id?: string
|
||||
ext?: string
|
||||
format_note?: string
|
||||
resolution?: string
|
||||
width?: number
|
||||
height?: number
|
||||
fps?: number
|
||||
vcodec?: string
|
||||
acodec?: string
|
||||
filesize?: number
|
||||
filesize_approx?: number
|
||||
tbr?: number
|
||||
}
|
||||
|
||||
type VideoInfo = {
|
||||
title?: string
|
||||
thumbnail?: string
|
||||
duration?: number
|
||||
formats?: VideoFormat[]
|
||||
}
|
||||
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000
|
||||
|
||||
const isValidHttpUrl = (value?: string): boolean => {
|
||||
if (!value) return false
|
||||
return value.startsWith('http://') || value.startsWith('https://')
|
||||
}
|
||||
|
||||
const formatDuration = (value?: number): string => {
|
||||
if (!value || value <= 0) return 'Unknown'
|
||||
const totalSeconds = Math.round(value)
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
const paddedMinutes = hours > 0 ? String(minutes).padStart(2, '0') : String(minutes)
|
||||
const paddedSeconds = String(seconds).padStart(2, '0')
|
||||
return hours > 0 ? `${hours}:${paddedMinutes}:${paddedSeconds}` : `${minutes}:${paddedSeconds}`
|
||||
}
|
||||
|
||||
const formatBytes = (value?: number): string => {
|
||||
if (!value || value <= 0) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let size = value
|
||||
let unitIndex = 0
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex += 1
|
||||
}
|
||||
return `${size.toFixed(size >= 100 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const isVideoFormat = (format: VideoFormat): boolean => {
|
||||
if (format.vcodec && format.vcodec !== 'none') {
|
||||
return true
|
||||
}
|
||||
return Boolean(format.resolution || format.width || format.height)
|
||||
}
|
||||
|
||||
const isAudioFormat = (format: VideoFormat): boolean => {
|
||||
return Boolean(format.acodec && format.acodec !== 'none' && !isVideoFormat(format))
|
||||
}
|
||||
|
||||
type VideoInfoCacheEntry = {
|
||||
url: string
|
||||
status: 'pending' | 'ready' | 'error'
|
||||
fetchedAt: number
|
||||
info?: VideoInfo
|
||||
error?: string
|
||||
}
|
||||
|
||||
type VideoGroup = {
|
||||
label: string
|
||||
height: number
|
||||
formats: VideoFormat[]
|
||||
}
|
||||
|
||||
const loadCachedInfo = async (url: string): Promise<VideoInfoCacheEntry | null> => {
|
||||
const data = await browser.storage.local.get('videoInfoCacheByUrl')
|
||||
const map = data.videoInfoCacheByUrl as Record<string, VideoInfoCacheEntry> | undefined
|
||||
if (!map) return null
|
||||
const cached = map[url]
|
||||
if (!cached) return null
|
||||
if (Date.now() - cached.fetchedAt > CACHE_TTL_MS) return null
|
||||
return cached
|
||||
}
|
||||
|
||||
const sanitizeError = (error: string): string => {
|
||||
const message = error.toLowerCase()
|
||||
if (
|
||||
message.includes('localhost') ||
|
||||
message.includes('fetch') ||
|
||||
message.includes('network') ||
|
||||
message.includes('connect') ||
|
||||
message.includes('failed to request')
|
||||
) {
|
||||
return 'Client connection failed'
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [info, setInfo] = useState<VideoInfo | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentUrl, setCurrentUrl] = useState<string>('')
|
||||
const [retryTrigger, setRetryTrigger] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
const targetState = { url: '' }
|
||||
|
||||
const handleStorageChange = (
|
||||
changes: Record<string, browser.storage.StorageChange>,
|
||||
areaName: string
|
||||
) => {
|
||||
if (!active || areaName !== 'local') return
|
||||
const change = changes.videoInfoCacheByUrl
|
||||
if (!change?.newValue) return
|
||||
|
||||
const map = change.newValue as Record<string, VideoInfoCacheEntry>
|
||||
const next = map[targetState.url]
|
||||
if (!next) return
|
||||
|
||||
if (next.status === 'ready' && next.info) {
|
||||
setInfo(next.info)
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
} else if (next.status === 'error' && next.error) {
|
||||
setError(sanitizeError(next.error))
|
||||
setInfo(null)
|
||||
setLoading(false)
|
||||
} else if (next.status === 'pending') {
|
||||
setLoading(true)
|
||||
}
|
||||
}
|
||||
|
||||
browser.storage.onChanged.addListener(handleStorageChange)
|
||||
|
||||
const loadInfo = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setInfo(null)
|
||||
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true })
|
||||
if (!isValidHttpUrl(tab?.url)) {
|
||||
setError('Please open a valid video page first.')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const targetUrl = tab.url as string
|
||||
targetState.url = targetUrl
|
||||
setCurrentUrl(targetUrl)
|
||||
|
||||
const cached = await loadCachedInfo(targetUrl)
|
||||
const shouldBypassCache = retryTrigger > 0
|
||||
if (cached && !shouldBypassCache) {
|
||||
if (cached.status === 'ready' && cached.info) {
|
||||
setInfo(cached.info)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
if (cached.status === 'error' && cached.error) {
|
||||
setError(sanitizeError(cached.error))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'video-info:fetch',
|
||||
url: targetUrl
|
||||
})
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to request video info.'
|
||||
setError(sanitizeError(message))
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const latest = await loadCachedInfo(targetUrl)
|
||||
if (latest && latest.status === 'ready' && latest.info) {
|
||||
setInfo(latest.info)
|
||||
setError(null)
|
||||
setLoading(false)
|
||||
} else if (latest && latest.status === 'error' && latest.error) {
|
||||
setError(sanitizeError(latest.error))
|
||||
setInfo(null)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void loadInfo()
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
browser.storage.onChanged.removeListener(handleStorageChange)
|
||||
}
|
||||
}, [retryTrigger])
|
||||
|
||||
const formats = useMemo(() => info?.formats ?? [], [info])
|
||||
const groupedFormats = useMemo(() => {
|
||||
const video: VideoFormat[] = []
|
||||
const audio: VideoFormat[] = []
|
||||
const other: VideoFormat[] = []
|
||||
|
||||
for (const format of formats) {
|
||||
if (isVideoFormat(format)) {
|
||||
video.push(format)
|
||||
} else if (isAudioFormat(format)) {
|
||||
audio.push(format)
|
||||
} else {
|
||||
other.push(format)
|
||||
}
|
||||
}
|
||||
|
||||
return { video, audio, other }
|
||||
}, [formats])
|
||||
|
||||
const groupedVideoFormats = useMemo(() => {
|
||||
const raw = groupedFormats.video
|
||||
if (!raw.length) return []
|
||||
|
||||
const groups: Record<number, VideoFormat[]> = {}
|
||||
const noHeight: VideoFormat[] = []
|
||||
|
||||
for (const f of raw) {
|
||||
const h = f.height || f.resolution?.match(/x(\d+)/)?.[1]
|
||||
const heightVal = h ? Number(h) : 0
|
||||
|
||||
if (heightVal > 0) {
|
||||
if (!groups[heightVal]) groups[heightVal] = []
|
||||
groups[heightVal].push(f)
|
||||
} else {
|
||||
noHeight.push(f)
|
||||
}
|
||||
}
|
||||
|
||||
const sortedLabels = Object.keys(groups)
|
||||
.map(Number)
|
||||
.sort((a, b) => b - a)
|
||||
|
||||
const result: VideoGroup[] = sortedLabels.map((h) => ({
|
||||
label: `${h}p`,
|
||||
height: h,
|
||||
formats: groups[h].sort((a, b) => {
|
||||
const sa = a.filesize || a.filesize_approx || 0
|
||||
const sb = b.filesize || b.filesize_approx || 0
|
||||
return sb - sa
|
||||
})
|
||||
}))
|
||||
|
||||
if (noHeight.length > 0) {
|
||||
result.push({
|
||||
label: 'Other',
|
||||
height: 0,
|
||||
formats: noHeight
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}, [groupedFormats.video])
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const clientLaunchDelayMs = 2000
|
||||
|
||||
const openClientApp = async () => {
|
||||
window.location.href = 'vidbee://'
|
||||
await wait(clientLaunchDelayMs)
|
||||
}
|
||||
|
||||
const handleOpenClient = () => {
|
||||
if (!currentUrl) return
|
||||
const deepLink = `vidbee://download?url=${encodeURIComponent(currentUrl)}`
|
||||
window.location.href = deepLink
|
||||
}
|
||||
|
||||
const handleOpenClientAndRetry = async () => {
|
||||
await openClientApp()
|
||||
setRetryTrigger((count) => count + 1)
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
setRetryTrigger((count) => count + 1)
|
||||
}
|
||||
|
||||
const isInvalidPageError = error === 'Please open a valid video page first.'
|
||||
const isClientConnectionError = Boolean(error?.includes('Client connection failed'))
|
||||
const errorTitle = isInvalidPageError
|
||||
? 'Open a video page'
|
||||
: isClientConnectionError
|
||||
? 'Connect the VidBee app'
|
||||
: 'Something went wrong'
|
||||
const errorDescription = isInvalidPageError
|
||||
? 'Navigate to a supported video page, then try again.'
|
||||
: isClientConnectionError
|
||||
? 'The extension needs the VidBee desktop app to be running.'
|
||||
: 'Try again in a moment.'
|
||||
|
||||
const renderStatus = () => {
|
||||
if (loading)
|
||||
return (
|
||||
<span className="status-indicator">
|
||||
<div className="status-dot loading" /> Working
|
||||
</span>
|
||||
)
|
||||
if (error)
|
||||
return (
|
||||
<span className="status-indicator">
|
||||
<div className="status-dot error" /> Error
|
||||
</span>
|
||||
)
|
||||
if (info)
|
||||
return (
|
||||
<span className="status-indicator">
|
||||
<div className="status-dot ok" /> Ready
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<span className="status-indicator">
|
||||
<div className="status-dot" /> Idle
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header>
|
||||
<h1>VidBee</h1>
|
||||
{renderStatus()}
|
||||
</header>
|
||||
{loading && (
|
||||
<div className="loading-container">
|
||||
<div className="spinner" />
|
||||
<div className="loading-text">Analyzing video...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && error && (
|
||||
<div className="error-container">
|
||||
<div className="error-header">
|
||||
<h2 className="error-title">{errorTitle}</h2>
|
||||
<p className="error-description">{errorDescription}</p>
|
||||
</div>
|
||||
<div className="error-banner">{error}</div>
|
||||
{isClientConnectionError ? (
|
||||
<div className="action-grid">
|
||||
<div className="action-card">
|
||||
<p className="action-title">Client installed</p>
|
||||
<p className="action-text">
|
||||
Start VidBee and keep it running, then we will retry automatically.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={handleOpenClientAndRetry}
|
||||
>
|
||||
Open Client
|
||||
</button>
|
||||
</div>
|
||||
<div className="action-card">
|
||||
<p className="action-title">Need the app?</p>
|
||||
<p className="action-text">
|
||||
Download VidBee once, install it, then come back here to try again.
|
||||
</p>
|
||||
<a
|
||||
href="https://vidbee.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button"
|
||||
>
|
||||
Download VidBee
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="action-card">
|
||||
<p className="action-title">Try again</p>
|
||||
<p className="action-text">
|
||||
{isInvalidPageError
|
||||
? 'Open a supported video page, then retry.'
|
||||
: 'Retry after a moment.'}
|
||||
</p>
|
||||
<button type="button" className="secondary-button" onClick={handleRetry}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && info && (
|
||||
<>
|
||||
<section className="video-info">
|
||||
<div className="video-details">
|
||||
<h2>{info.title || 'Untitled video'}</h2>
|
||||
<div className="meta-row">
|
||||
<span>{formatDuration(info.duration)}</span>
|
||||
<span>•</span>
|
||||
<span>{formats.length} formats</span>
|
||||
</div>
|
||||
</div>
|
||||
{info.thumbnail && <img className="thumbnail" src={info.thumbnail} alt="" />}
|
||||
</section>
|
||||
|
||||
<button type="button" className="primary-button" onClick={handleOpenClient}>
|
||||
Download with VidBee
|
||||
</button>
|
||||
|
||||
<section className="formats-section">
|
||||
{groupedVideoFormats.map((group) => (
|
||||
<div className="format-group" key={group.label}>
|
||||
<div className="group-title sticky-title">{group.label}</div>
|
||||
<table className="format-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="col-id">ID</th>
|
||||
<th className="col-ext">Ext</th>
|
||||
<th className="col-size">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{group.formats.map((f) => (
|
||||
<tr key={`vg-${group.label}-${f.format_id ?? f.ext ?? 'video'}`}>
|
||||
<td className="col-id">{f.format_id || '-'}</td>
|
||||
<td className="col-ext">{f.ext || '-'}</td>
|
||||
<td className="col-size">{formatBytes(f.filesize || f.filesize_approx)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{groupedVideoFormats.length === 0 && groupedFormats.audio.length === 0 && (
|
||||
<div className="empty-state">No compatible formats.</div>
|
||||
)}
|
||||
|
||||
{groupedFormats.audio.length > 0 && (
|
||||
<div className="format-group">
|
||||
<div className="group-title">Audio Only</div>
|
||||
<table className="format-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="col-id">ID</th>
|
||||
<th className="col-ext">Ext</th>
|
||||
<th className="col-size">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupedFormats.audio.map((f) => (
|
||||
<tr key={`a-${f.format_id ?? f.ext ?? 'audio'}`}>
|
||||
<td className="col-id">{f.format_id || '-'}</td>
|
||||
<td className="col-ext">{f.ext || '-'}</td>
|
||||
<td className="col-size">{formatBytes(f.filesize || f.filesize_approx)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
13
extension/entrypoints/popup/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Popup Title</title>
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
13
extension/entrypoints/popup/main.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
||||
if (root) {
|
||||
ReactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
)
|
||||
}
|
||||
28
extension/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "vidbee-extension",
|
||||
"description": "manifest.json description",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"zip": "wxt zip",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@wxt-dev/module-react": "^1.1.5",
|
||||
"typescript": "^5.9.3",
|
||||
"wxt": "^0.20.6"
|
||||
}
|
||||
}
|
||||
3541
extension/pnpm-lock.yaml
generated
Normal file
14
extension/public/_locales/en/messages.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extensionName": {
|
||||
"message": "VidBee Video Downloader"
|
||||
},
|
||||
"extensionDescription": {
|
||||
"message": "Download videos from over 1,000 websites with VidBee."
|
||||
},
|
||||
"downloadWithVidBee": {
|
||||
"message": "Download with VidBee"
|
||||
},
|
||||
"hideButton": {
|
||||
"message": "Hide"
|
||||
}
|
||||
}
|
||||
BIN
extension/public/icon/128.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
extension/public/icon/16.png
Normal file
|
After Width: | Height: | Size: 924 B |
BIN
extension/public/icon/32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
extension/public/icon/48.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
extension/public/icon/icon-loading-128.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
extension/public/icon/icon-loading-16.png
Normal file
|
After Width: | Height: | Size: 744 B |
BIN
extension/public/icon/icon-loading-32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
extension/public/icon/icon-loading-48.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
extension/public/icon/icon-success-128.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
extension/public/icon/icon-success-16.png
Normal file
|
After Width: | Height: | Size: 751 B |
BIN
extension/public/icon/icon-success-32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
extension/public/icon/icon-success-48.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
7
extension/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
13
extension/wxt.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'wxt'
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-react'],
|
||||
manifest: {
|
||||
name: '__MSG_extensionName__',
|
||||
description: '__MSG_extensionDescription__',
|
||||
default_locale: 'en',
|
||||
host_permissions: ['http://127.0.0.1/*'],
|
||||
permissions: ['activeTab', 'storage']
|
||||
}
|
||||
})
|
||||
23
monkey/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
156
monkey/dist/vidbee-quick-download.user.js
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
// ==UserScript==
|
||||
// @name vidbee-quick-download
|
||||
// @namespace vidbee
|
||||
// @version 0.0.1
|
||||
// @icon https://vidbee.org/favicon.svg
|
||||
// @match https://www.youtube.com/*
|
||||
// @match https://youtube.com/*
|
||||
// @match https://music.youtube.com/*
|
||||
// @match https://www.bilibili.com/*
|
||||
// @match https://bilibili.com/*
|
||||
// @match https://www.tiktok.com/*
|
||||
// @match https://tiktok.com/*
|
||||
// @match https://vimeo.com/*
|
||||
// @match https://www.vimeo.com/*
|
||||
// @match https://www.dailymotion.com/*
|
||||
// @match https://dailymotion.com/*
|
||||
// @match https://www.twitch.tv/*
|
||||
// @match https://twitch.tv/*
|
||||
// @match https://twitter.com/*
|
||||
// @match https://www.twitter.com/*
|
||||
// @match https://x.com/*
|
||||
// @match https://www.x.com/*
|
||||
// @match https://www.instagram.com/*
|
||||
// @match https://instagram.com/*
|
||||
// @match https://www.facebook.com/*
|
||||
// @match https://facebook.com/*
|
||||
// @match https://fb.com/*
|
||||
// @match https://www.fb.com/*
|
||||
// @match https://www.reddit.com/*
|
||||
// @match https://reddit.com/*
|
||||
// @match https://soundcloud.com/*
|
||||
// @match https://www.soundcloud.com/*
|
||||
// @match https://www.nicovideo.jp/*
|
||||
// @match https://nicovideo.jp/*
|
||||
// @match https://kick.com/*
|
||||
// @match https://www.kick.com/*
|
||||
// @match https://bandcamp.com/*
|
||||
// @match https://*.bandcamp.com/*
|
||||
// @match https://www.mixcloud.com/*
|
||||
// @match https://mixcloud.com/*
|
||||
// @grant GM_addStyle
|
||||
// ==/UserScript==
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):(document.head||document.documentElement).appendChild(document.createElement("style")).append(t);})(e));};
|
||||
|
||||
const styleCss = '.vidbee-download-container{position:fixed;bottom:16px;right:16px;z-index:9999;transition:all .2s ease;overflow:visible}.vidbee-download-container.vidbee-hidden{opacity:0;pointer-events:none;transform:scale(0)}.vidbee-download-button{position:relative;display:flex;align-items:center;justify-content:center;width:36px;height:36px;padding:0;background:#00000080;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fffc;border:1px solid rgba(255,255,255,.1);border-radius:50%;box-shadow:0 2px 8px #0003;cursor:pointer;font-size:0;transition:all .2s ease;opacity:.6;overflow:visible}.vidbee-download-button:hover{opacity:1;background:#000000b3;border-color:#fff3;box-shadow:0 4px 12px #0000004d;transform:scale(1.1)}.vidbee-download-button:active{transform:scale(.95)}.vidbee-download-button svg{flex-shrink:0;stroke:currentColor;width:16px;height:16px}.vidbee-tooltip{position:absolute;right:calc(100% + 8px);top:50%;padding:6px 10px;background:#000000e6;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;font-size:12px;font-weight:500;white-space:nowrap;border-radius:4px;box-shadow:0 2px 8px #0000004d;opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease;transform:translateY(-50%) translate(4px);z-index:10000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;line-height:1}.vidbee-tooltip:after{content:"";position:absolute;left:100%;top:50%;transform:translateY(-50%);border:4px solid transparent;border-left-color:#000000e6}.vidbee-download-button:hover .vidbee-tooltip{opacity:1;transform:translateY(-50%) translate(0)}.vidbee-close-button{position:absolute;top:-6px;right:-6px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;background:#ff4d4de6;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:50%;box-shadow:0 2px 4px #0000004d;cursor:pointer;font-size:0;transition:all .2s ease;z-index:1;opacity:0;pointer-events:none;overflow:visible}.vidbee-download-container:hover .vidbee-close-button{opacity:1;pointer-events:auto}.vidbee-close-button:hover{background:#ff4d4d;transform:scale(1.15);box-shadow:0 3px 6px #0006}.vidbee-close-button:active{transform:scale(.9)}.vidbee-close-button svg{flex-shrink:0;stroke:currentColor;width:10px;height:10px}.vidbee-close-button .vidbee-tooltip{right:calc(100% + 6px);top:50%;left:auto;transform:translateY(-50%) translate(4px)}.vidbee-close-button .vidbee-tooltip:after{left:100%;top:50%;right:auto;transform:translateY(-50%);border-left-color:#000000e6;border-top-color:transparent}.vidbee-download-container:hover .vidbee-close-button:hover .vidbee-tooltip{opacity:1;transform:translateY(-50%) translate(0)}';
|
||||
importCSS(styleCss);
|
||||
function getVideoUrl() {
|
||||
return window.location.href;
|
||||
}
|
||||
function hideButtonTemporarily() {
|
||||
const container = document.getElementById("vidbee-download-btn");
|
||||
if (container) {
|
||||
container.classList.add("vidbee-hidden");
|
||||
setTimeout(() => {
|
||||
if (container) {
|
||||
container.classList.remove("vidbee-hidden");
|
||||
}
|
||||
}, 5e3);
|
||||
}
|
||||
}
|
||||
function createVidBeeButton() {
|
||||
if (document.getElementById("vidbee-download-btn")) {
|
||||
return;
|
||||
}
|
||||
const videoUrl = getVideoUrl();
|
||||
if (!videoUrl) {
|
||||
return;
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
container.id = "vidbee-download-btn";
|
||||
container.className = "vidbee-download-container";
|
||||
const button = document.createElement("button");
|
||||
button.className = "vidbee-download-button";
|
||||
button.setAttribute("aria-label", "Download with VidBee");
|
||||
button.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
<span class="vidbee-tooltip">Download with VidBee</span>
|
||||
`;
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.className = "vidbee-close-button";
|
||||
closeButton.setAttribute("aria-label", "Hide button");
|
||||
closeButton.innerHTML = `
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
<span class="vidbee-tooltip">Hide</span>
|
||||
`;
|
||||
closeButton.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
hideButtonTemporarily();
|
||||
});
|
||||
let clickTimer = null;
|
||||
let clickCount = 0;
|
||||
button.addEventListener("click", () => {
|
||||
clickCount++;
|
||||
if (clickCount === 1) {
|
||||
clickTimer = window.setTimeout(() => {
|
||||
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`;
|
||||
window.location.href = vidbeeUrl;
|
||||
clickCount = 0;
|
||||
}, 300);
|
||||
} else if (clickCount === 2) {
|
||||
if (clickTimer !== null) {
|
||||
clearTimeout(clickTimer);
|
||||
}
|
||||
clickCount = 0;
|
||||
hideButtonTemporarily();
|
||||
}
|
||||
});
|
||||
container.appendChild(button);
|
||||
container.appendChild(closeButton);
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
function init() {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", createVidBeeButton);
|
||||
} else {
|
||||
createVidBeeButton();
|
||||
}
|
||||
let lastUrl = location.href;
|
||||
let urlCheckTimer = null;
|
||||
const checkUrlChange = () => {
|
||||
const currentUrl = location.href;
|
||||
if (currentUrl !== lastUrl) {
|
||||
lastUrl = currentUrl;
|
||||
const oldButton = document.getElementById("vidbee-download-btn");
|
||||
if (oldButton) {
|
||||
oldButton.remove();
|
||||
}
|
||||
const hostname = window.location.hostname;
|
||||
const delay = hostname.includes("bilibili.com") ? 800 : 500;
|
||||
setTimeout(createVidBeeButton, delay);
|
||||
}
|
||||
};
|
||||
new MutationObserver(() => {
|
||||
if (urlCheckTimer !== null) {
|
||||
clearTimeout(urlCheckTimer);
|
||||
}
|
||||
urlCheckTimer = window.setTimeout(checkUrlChange, 100);
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
window.addEventListener("popstate", () => {
|
||||
setTimeout(checkUrlChange, 300);
|
||||
});
|
||||
}
|
||||
init();
|
||||
|
||||
})();
|
||||
16
monkey/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "vidbee-quick-download",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.3",
|
||||
"vite-plugin-monkey": "^7.1.1"
|
||||
}
|
||||
}
|
||||
943
monkey/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,943 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.1.3
|
||||
version: 7.3.0
|
||||
vite-plugin-monkey:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.8(postcss@8.5.6)(vite@7.3.0)
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.54.0':
|
||||
resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.54.0':
|
||||
resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.54.0':
|
||||
resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
|
||||
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.54.0':
|
||||
resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.54.0':
|
||||
resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.54.0':
|
||||
resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
acorn-walk@8.3.4:
|
||||
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
|
||||
bundle-name@4.1.0:
|
||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
cuint@0.2.2:
|
||||
resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==}
|
||||
|
||||
default-browser-id@5.0.1:
|
||||
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
default-browser@5.4.0:
|
||||
resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
define-lazy-prop@3.0.0:
|
||||
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@6.0.1:
|
||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
esbuild@0.27.2:
|
||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
htmlparser2@10.0.0:
|
||||
resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
|
||||
|
||||
import-meta-resolve@4.2.0:
|
||||
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
|
||||
|
||||
is-docker@3.0.0:
|
||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
hasBin: true
|
||||
|
||||
is-inside-container@1.0.0:
|
||||
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||
engines: {node: '>=14.16'}
|
||||
hasBin: true
|
||||
|
||||
is-wsl@3.1.0:
|
||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
mime@2.5.2:
|
||||
resolution: {integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
hasBin: true
|
||||
|
||||
minimatch@3.0.8:
|
||||
resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==}
|
||||
|
||||
mrmime@2.0.1:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
open@10.2.0:
|
||||
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@4.0.3:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
postcss-url@10.1.3:
|
||||
resolution: {integrity: sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
postcss: ^8.0.0
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
rollup@4.54.0:
|
||||
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
run-applescript@7.1.0:
|
||||
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
systemjs@6.15.1:
|
||||
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
vite-plugin-monkey@7.1.8:
|
||||
resolution: {integrity: sha512-FGz1jlHcodt+L120UcHLiN0jXqQZdZHxLuHcGQg8Zjas6BulxLM1oiGUg3M95HbjYVFH6ygrOJoZ5YCYu1purw==}
|
||||
peerDependencies:
|
||||
vite: ^6.0.0 || ^7.0.0
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
vite@7.3.0:
|
||||
resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^20.19.0 || >=22.12.0
|
||||
jiti: '>=1.21.0'
|
||||
less: ^4.0.0
|
||||
lightningcss: ^1.21.0
|
||||
sass: ^1.70.0
|
||||
sass-embedded: ^1.70.0
|
||||
stylus: '>=0.54.8'
|
||||
sugarss: ^5.0.0
|
||||
terser: ^5.16.0
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
jiti:
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wsl-utils@0.1.0:
|
||||
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
xxhashjs@0.2.2:
|
||||
resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.54.0':
|
||||
optional: true
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
acorn-walk@8.3.4:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
bundle-name@4.1.0:
|
||||
dependencies:
|
||||
run-applescript: 7.1.0
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
cuint@0.2.2: {}
|
||||
|
||||
default-browser-id@5.0.1: {}
|
||||
|
||||
default-browser@5.4.0:
|
||||
dependencies:
|
||||
bundle-name: 4.1.0
|
||||
default-browser-id: 5.0.1
|
||||
|
||||
define-lazy-prop@3.0.0: {}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domhandler@5.0.3:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domutils@3.2.2:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
|
||||
esbuild@0.27.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.2
|
||||
'@esbuild/android-arm': 0.27.2
|
||||
'@esbuild/android-arm64': 0.27.2
|
||||
'@esbuild/android-x64': 0.27.2
|
||||
'@esbuild/darwin-arm64': 0.27.2
|
||||
'@esbuild/darwin-x64': 0.27.2
|
||||
'@esbuild/freebsd-arm64': 0.27.2
|
||||
'@esbuild/freebsd-x64': 0.27.2
|
||||
'@esbuild/linux-arm': 0.27.2
|
||||
'@esbuild/linux-arm64': 0.27.2
|
||||
'@esbuild/linux-ia32': 0.27.2
|
||||
'@esbuild/linux-loong64': 0.27.2
|
||||
'@esbuild/linux-mips64el': 0.27.2
|
||||
'@esbuild/linux-ppc64': 0.27.2
|
||||
'@esbuild/linux-riscv64': 0.27.2
|
||||
'@esbuild/linux-s390x': 0.27.2
|
||||
'@esbuild/linux-x64': 0.27.2
|
||||
'@esbuild/netbsd-arm64': 0.27.2
|
||||
'@esbuild/netbsd-x64': 0.27.2
|
||||
'@esbuild/openbsd-arm64': 0.27.2
|
||||
'@esbuild/openbsd-x64': 0.27.2
|
||||
'@esbuild/openharmony-arm64': 0.27.2
|
||||
'@esbuild/sunos-x64': 0.27.2
|
||||
'@esbuild/win32-arm64': 0.27.2
|
||||
'@esbuild/win32-ia32': 0.27.2
|
||||
'@esbuild/win32-x64': 0.27.2
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
htmlparser2@10.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
entities: 6.0.1
|
||||
|
||||
import-meta-resolve@4.2.0: {}
|
||||
|
||||
is-docker@3.0.0: {}
|
||||
|
||||
is-inside-container@1.0.0:
|
||||
dependencies:
|
||||
is-docker: 3.0.0
|
||||
|
||||
is-wsl@3.1.0:
|
||||
dependencies:
|
||||
is-inside-container: 1.0.0
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
semver: 6.3.1
|
||||
|
||||
mime@2.5.2: {}
|
||||
|
||||
minimatch@3.0.8:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
open@10.2.0:
|
||||
dependencies:
|
||||
default-browser: 5.4.0
|
||||
define-lazy-prop: 3.0.0
|
||||
is-inside-container: 1.0.0
|
||||
wsl-utils: 0.1.0
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
postcss-url@10.1.3(postcss@8.5.6):
|
||||
dependencies:
|
||||
make-dir: 3.1.0
|
||||
mime: 2.5.2
|
||||
minimatch: 3.0.8
|
||||
postcss: 8.5.6
|
||||
xxhashjs: 0.2.2
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
rollup@4.54.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.54.0
|
||||
'@rollup/rollup-android-arm64': 4.54.0
|
||||
'@rollup/rollup-darwin-arm64': 4.54.0
|
||||
'@rollup/rollup-darwin-x64': 4.54.0
|
||||
'@rollup/rollup-freebsd-arm64': 4.54.0
|
||||
'@rollup/rollup-freebsd-x64': 4.54.0
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.54.0
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.54.0
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-arm64-musl': 4.54.0
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.54.0
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-x64-gnu': 4.54.0
|
||||
'@rollup/rollup-linux-x64-musl': 4.54.0
|
||||
'@rollup/rollup-openharmony-arm64': 4.54.0
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.54.0
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.54.0
|
||||
'@rollup/rollup-win32-x64-gnu': 4.54.0
|
||||
'@rollup/rollup-win32-x64-msvc': 4.54.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
run-applescript@7.1.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
systemjs@6.15.1: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
vite-plugin-monkey@7.1.8(postcss@8.5.6)(vite@7.3.0):
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
acorn-walk: 8.3.4
|
||||
cross-spawn: 7.0.6
|
||||
htmlparser2: 10.0.0
|
||||
import-meta-resolve: 4.2.0
|
||||
magic-string: 0.30.21
|
||||
mrmime: 2.0.1
|
||||
open: 10.2.0
|
||||
picocolors: 1.1.1
|
||||
postcss-url: 10.1.3(postcss@8.5.6)
|
||||
systemjs: 6.15.1
|
||||
optionalDependencies:
|
||||
vite: 7.3.0
|
||||
transitivePeerDependencies:
|
||||
- postcss
|
||||
|
||||
vite@7.3.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.54.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wsl-utils@0.1.0:
|
||||
dependencies:
|
||||
is-wsl: 3.1.0
|
||||
|
||||
xxhashjs@0.2.2:
|
||||
dependencies:
|
||||
cuint: 0.2.2
|
||||