Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8acd761189 | ||
|
|
20c6ae6543 | ||
|
|
66f2211d68 | ||
|
|
ac183b4e3c | ||
|
|
877f088136 | ||
|
|
5ee08a3e62 | ||
|
|
137a07cf4e | ||
|
|
21a5adbdf2 | ||
|
|
81f52f86e5 | ||
|
|
c8147cafe3 | ||
|
|
7074f698e5 | ||
|
|
b254bb6dba | ||
|
|
1caca0ea9a | ||
|
|
12ec142a77 |
91
.github/workflows/build.yml
vendored
91
.github/workflows/build.yml
vendored
@@ -8,6 +8,17 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
description: 'Whether to upload build artifacts'
|
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:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -68,6 +79,8 @@ jobs:
|
|||||||
$source = Join-Path 'ffmpeg' '${{ matrix.ffmpeg_inner_path }}'
|
$source = Join-Path 'ffmpeg' '${{ matrix.ffmpeg_inner_path }}'
|
||||||
$destination = Join-Path 'resources' '${{ matrix.ffmpeg_output }}'
|
$destination = Join-Path 'resources' '${{ matrix.ffmpeg_output }}'
|
||||||
Copy-Item -Path $source -Destination $destination -Force
|
Copy-Item -Path $source -Destination $destination -Force
|
||||||
|
Remove-Item ffmpeg.zip -Force
|
||||||
|
Remove-Item ffmpeg -Recurse -Force
|
||||||
|
|
||||||
- name: Download ffmpeg binary (macOS)
|
- name: Download ffmpeg binary (macOS)
|
||||||
if: matrix.platform == 'macos'
|
if: matrix.platform == 'macos'
|
||||||
@@ -112,6 +125,7 @@ jobs:
|
|||||||
tar -xf ffmpeg.tar.xz -C ffmpeg
|
tar -xf ffmpeg.tar.xz -C ffmpeg
|
||||||
cp "ffmpeg/${{ matrix.ffmpeg_inner_path }}" "resources/${{ matrix.ffmpeg_output }}"
|
cp "ffmpeg/${{ matrix.ffmpeg_inner_path }}" "resources/${{ matrix.ffmpeg_output }}"
|
||||||
chmod +x "resources/${{ matrix.ffmpeg_output }}"
|
chmod +x "resources/${{ matrix.ffmpeg_output }}"
|
||||||
|
rm -rf ffmpeg.tar.xz ffmpeg
|
||||||
|
|
||||||
- name: Download yt-dlp binary
|
- name: Download yt-dlp binary
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -124,9 +138,86 @@ jobs:
|
|||||||
- name: Lint and format check
|
- name: Lint and format check
|
||||||
run: pnpm run check && pnpm run typecheck
|
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
|
- name: Build application
|
||||||
run: ${{ matrix.build_script }}
|
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
|
- name: Upload build artifacts
|
||||||
if: inputs.upload_artifacts == true
|
if: inputs.upload_artifacts == true
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -10,6 +10,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
with:
|
with:
|
||||||
upload_artifacts: true
|
upload_artifacts: true
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build]
|
needs: [build]
|
||||||
@@ -42,3 +43,9 @@ jobs:
|
|||||||
dist/*.blockmap
|
dist/*.blockmap
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Notify Cloudflare Pages
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_WEBHOOK_URL: ${{ secrets.CLOUDFLARE_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
curl -X POST "$CLOUDFLARE_WEBHOOK_URL"
|
||||||
|
|||||||
58
build/after-pack.cjs
Normal file
58
build/after-pack.cjs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const { execFileSync } = require('node:child_process')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
const BINARIES = ['yt-dlp_macos', 'ffmpeg_macos', '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/>
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -2,17 +2,23 @@ appId: com.vidbee
|
|||||||
productName: VidBee
|
productName: VidBee
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
|
afterPack: build/after-pack.cjs
|
||||||
protocols:
|
protocols:
|
||||||
- name: VidBee
|
- name: VidBee
|
||||||
schemes:
|
schemes:
|
||||||
- vidbee
|
- vidbee
|
||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!**/.vscode/*'
|
||||||
|
- '!**/.context/**'
|
||||||
|
- '!**/.github/**'
|
||||||
- '!src/*'
|
- '!src/*'
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
- '!{.eslintcache,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md}'
|
- '!{.eslintcache,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md}'
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
- '!extension/**'
|
||||||
|
- '!monkey/**'
|
||||||
|
- '!screenshots/**'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
@@ -23,9 +29,10 @@ nsis:
|
|||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
mac:
|
mac:
|
||||||
identity: null
|
hardenedRuntime: true
|
||||||
|
entitlements: build/entitlements.mac.plist
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
notarize: false
|
notarize: true
|
||||||
artifactName: ${name}-${version}-${arch}.${ext}
|
artifactName: ${name}-${version}-${arch}.${ext}
|
||||||
target:
|
target:
|
||||||
- target: zip
|
- target: zip
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vidbee",
|
"name": "vidbee",
|
||||||
"version": "1.1.6",
|
"version": "1.1.10",
|
||||||
"description": "A modern Electron application for downloading videos and audios",
|
"description": "A modern Electron application for downloading videos and audios",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "VidBee",
|
"author": "VidBee",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
const fs = require('node:fs')
|
const fs = require('node:fs')
|
||||||
const path = require('node:path')
|
const path = require('node:path')
|
||||||
const os = require('node:os')
|
const os = require('node:os')
|
||||||
const { execSync } = require('node:child_process')
|
const { execSync, spawnSync } = require('node:child_process')
|
||||||
const https = require('node:https')
|
const https = require('node:https')
|
||||||
const http = require('node:http')
|
const http = require('node:http')
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const PLATFORM_CONFIG = {
|
|||||||
extract: 'unzip',
|
extract: 'unzip',
|
||||||
release: {
|
release: {
|
||||||
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
|
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
|
||||||
assetPattern: /win64.*gpl.*\.zip$/i,
|
assetPattern: /ffmpeg-master-latest-win64-gpl\.zip$/i,
|
||||||
binaryName: 'ffmpeg.exe'
|
binaryName: 'ffmpeg.exe'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ const PLATFORM_CONFIG = {
|
|||||||
extract: 'tar',
|
extract: 'tar',
|
||||||
release: {
|
release: {
|
||||||
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
|
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
|
||||||
assetPattern: /linux64.*gpl.*\.tar\.xz$/i,
|
assetPattern: /ffmpeg-master-latest-linux64-gpl\.tar\.xz$/i,
|
||||||
binaryName: 'ffmpeg'
|
binaryName: 'ffmpeg'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,24 +125,43 @@ function downloadFile(url, dest) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const protocol = url.startsWith('https') ? https : http
|
const protocol = url.startsWith('https') ? https : http
|
||||||
const file = fs.createWriteStream(dest)
|
const file = fs.createWriteStream(dest)
|
||||||
|
let downloadedBytes = 0
|
||||||
|
|
||||||
const request = protocol.get(url, { headers: getDownloadHeaders(url) }, (response) => {
|
const request = protocol.get(url, { headers: getDownloadHeaders(url) }, (response) => {
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||||
// Handle redirect
|
// Handle redirect
|
||||||
file.close()
|
file.close()
|
||||||
safeUnlink(dest)
|
safeUnlink(dest)
|
||||||
return downloadFile(response.headers.location, dest).then(resolve).catch(reject)
|
const redirectUrl = response.headers.location
|
||||||
|
if (!redirectUrl) {
|
||||||
|
return reject(new Error(`Redirect without location for ${url}`))
|
||||||
|
}
|
||||||
|
log(`Redirected to ${redirectUrl}`, 'info')
|
||||||
|
return downloadFile(redirectUrl, dest).then(resolve).catch(reject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentLength = response.headers['content-length']
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
file.close()
|
file.close()
|
||||||
safeUnlink(dest)
|
safeUnlink(dest)
|
||||||
return reject(new Error(`Failed to download: ${response.statusCode}`))
|
return reject(
|
||||||
|
new Error(
|
||||||
|
`Failed to download ${url}: ${response.statusCode} (length: ${contentLength || 'unknown'})`
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
downloadedBytes += chunk.length
|
||||||
|
})
|
||||||
|
|
||||||
response.pipe(file)
|
response.pipe(file)
|
||||||
file.on('finish', () => {
|
file.on('finish', () => {
|
||||||
file.close()
|
file.close()
|
||||||
|
log(
|
||||||
|
`Downloaded ${formatBytes(downloadedBytes)} from ${url}`,
|
||||||
|
downloadedBytes ? 'success' : 'warn'
|
||||||
|
)
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -154,6 +173,7 @@ function downloadFile(url, dest) {
|
|||||||
request.on('error', (err) => {
|
request.on('error', (err) => {
|
||||||
file.close()
|
file.close()
|
||||||
safeUnlink(dest)
|
safeUnlink(dest)
|
||||||
|
log(`Download error for ${url}: ${err.message}`, 'error')
|
||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -163,6 +183,7 @@ async function downloadFileWithRetry(url, dest, retries = 3, delayMs = 2000) {
|
|||||||
let lastError
|
let lastError
|
||||||
for (let attempt = 1; attempt <= retries; attempt += 1) {
|
for (let attempt = 1; attempt <= retries; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
|
log(`Downloading ${url} (attempt ${attempt}/${retries})...`, 'download')
|
||||||
await downloadFile(url, dest)
|
await downloadFile(url, dest)
|
||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -170,7 +191,7 @@ async function downloadFileWithRetry(url, dest, retries = 3, delayMs = 2000) {
|
|||||||
safeUnlink(dest)
|
safeUnlink(dest)
|
||||||
if (attempt < retries) {
|
if (attempt < retries) {
|
||||||
const backoff = delayMs * attempt
|
const backoff = delayMs * attempt
|
||||||
log(`Download failed (attempt ${attempt}/${retries}): ${error.message}`, 'warn')
|
log(`Download failed for ${url} (attempt ${attempt}/${retries}): ${error.message}`, 'warn')
|
||||||
await new Promise((resolve) => setTimeout(resolve, backoff))
|
await new Promise((resolve) => setTimeout(resolve, backoff))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,6 +319,37 @@ function fileExists(filePath) {
|
|||||||
return fs.existsSync(filePath)
|
return fs.existsSync(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (!bytes || bytes <= 0) {
|
||||||
|
return 'unknown size'
|
||||||
|
}
|
||||||
|
if (bytes >= 1024 * 1024) {
|
||||||
|
return `${Math.round(bytes / (1024 * 1024))} MB`
|
||||||
|
}
|
||||||
|
return `${Math.round(bytes / 1024)} KB`
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBinary(filePath, args, label) {
|
||||||
|
const result = spawnSync(filePath, args, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 8000,
|
||||||
|
windowsHide: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return { ok: false, message: result.error.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim()
|
||||||
|
return { ok: false, message: output || `exit code ${result.status}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim()
|
||||||
|
const firstLine = output.split(/\r?\n/).find((line) => line.trim())
|
||||||
|
return { ok: true, message: firstLine ? firstLine.trim() : `${label} version check ok` }
|
||||||
|
}
|
||||||
|
|
||||||
function getDenoAssetName(platform, arch) {
|
function getDenoAssetName(platform, arch) {
|
||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
if (arch === 'arm64') {
|
if (arch === 'arm64') {
|
||||||
@@ -330,6 +382,10 @@ async function downloadYtDlp(config) {
|
|||||||
const outputPath = path.join(RESOURCES_DIR, output)
|
const outputPath = path.join(RESOURCES_DIR, output)
|
||||||
|
|
||||||
if (fileExists(outputPath)) {
|
if (fileExists(outputPath)) {
|
||||||
|
const validation = checkBinary(outputPath, ['--version'], 'yt-dlp')
|
||||||
|
if (!validation.ok) {
|
||||||
|
log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
|
||||||
|
}
|
||||||
log(`${output} already exists, skipping download`, 'info')
|
log(`${output} already exists, skipping download`, 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -342,6 +398,11 @@ async function downloadYtDlp(config) {
|
|||||||
await downloadFileWithRetry(url, tempPath)
|
await downloadFileWithRetry(url, tempPath)
|
||||||
fs.renameSync(tempPath, outputPath)
|
fs.renameSync(tempPath, outputPath)
|
||||||
setExecutable(outputPath)
|
setExecutable(outputPath)
|
||||||
|
const validation = checkBinary(outputPath, ['--version'], 'yt-dlp')
|
||||||
|
if (!validation.ok) {
|
||||||
|
safeUnlink(outputPath)
|
||||||
|
throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
|
||||||
|
}
|
||||||
log(`Downloaded ${output} successfully`, 'success')
|
log(`Downloaded ${output} successfully`, 'success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (fs.existsSync(tempPath)) {
|
if (fs.existsSync(tempPath)) {
|
||||||
@@ -356,6 +417,10 @@ async function downloadFfmpegWindows(config) {
|
|||||||
const outputPath = path.join(RESOURCES_DIR, output)
|
const outputPath = path.join(RESOURCES_DIR, output)
|
||||||
|
|
||||||
if (fileExists(outputPath)) {
|
if (fileExists(outputPath)) {
|
||||||
|
const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
|
||||||
|
if (!validation.ok) {
|
||||||
|
log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
|
||||||
|
}
|
||||||
log(`${output} already exists, skipping download`, 'info')
|
log(`${output} already exists, skipping download`, 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -392,6 +457,11 @@ async function downloadFfmpegWindows(config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.copyFileSync(sourcePath, outputPath)
|
fs.copyFileSync(sourcePath, outputPath)
|
||||||
|
const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
|
||||||
|
if (!validation.ok) {
|
||||||
|
safeUnlink(outputPath)
|
||||||
|
throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
|
||||||
|
}
|
||||||
log(`Downloaded ${output} successfully`, 'success')
|
log(`Downloaded ${output} successfully`, 'success')
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -416,6 +486,10 @@ async function downloadFfmpegMac(config) {
|
|||||||
const outputPath = path.join(RESOURCES_DIR, output)
|
const outputPath = path.join(RESOURCES_DIR, output)
|
||||||
|
|
||||||
if (fileExists(outputPath)) {
|
if (fileExists(outputPath)) {
|
||||||
|
const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
|
||||||
|
if (!validation.ok) {
|
||||||
|
log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
|
||||||
|
}
|
||||||
log(`${output} already exists, skipping download`, 'info')
|
log(`${output} already exists, skipping download`, 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -448,6 +522,11 @@ async function downloadFfmpegMac(config) {
|
|||||||
|
|
||||||
fs.copyFileSync(sourcePath, outputPath)
|
fs.copyFileSync(sourcePath, outputPath)
|
||||||
setExecutable(outputPath)
|
setExecutable(outputPath)
|
||||||
|
const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
|
||||||
|
if (!validation.ok) {
|
||||||
|
safeUnlink(outputPath)
|
||||||
|
throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
|
||||||
|
}
|
||||||
log(`Downloaded ${output} successfully`, 'success')
|
log(`Downloaded ${output} successfully`, 'success')
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -465,6 +544,10 @@ async function downloadFfmpegLinux(config) {
|
|||||||
const outputPath = path.join(RESOURCES_DIR, output)
|
const outputPath = path.join(RESOURCES_DIR, output)
|
||||||
|
|
||||||
if (fileExists(outputPath)) {
|
if (fileExists(outputPath)) {
|
||||||
|
const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
|
||||||
|
if (!validation.ok) {
|
||||||
|
log(`Existing ${output} failed version check: ${validation.message}`, 'warn')
|
||||||
|
}
|
||||||
log(`${output} already exists, skipping download`, 'info')
|
log(`${output} already exists, skipping download`, 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -502,6 +585,11 @@ async function downloadFfmpegLinux(config) {
|
|||||||
|
|
||||||
fs.copyFileSync(sourcePath, outputPath)
|
fs.copyFileSync(sourcePath, outputPath)
|
||||||
setExecutable(outputPath)
|
setExecutable(outputPath)
|
||||||
|
const validation = checkBinary(outputPath, ['-version'], 'ffmpeg')
|
||||||
|
if (!validation.ok) {
|
||||||
|
safeUnlink(outputPath)
|
||||||
|
throw new Error(`Downloaded ${output} failed version check: ${validation.message}`)
|
||||||
|
}
|
||||||
log(`Downloaded ${output} successfully`, 'success')
|
log(`Downloaded ${output} successfully`, 'success')
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -528,6 +616,10 @@ async function downloadDenoRuntime() {
|
|||||||
const outputPath = path.join(RESOURCES_DIR, outputName)
|
const outputPath = path.join(RESOURCES_DIR, outputName)
|
||||||
|
|
||||||
if (fileExists(outputPath)) {
|
if (fileExists(outputPath)) {
|
||||||
|
const validation = checkBinary(outputPath, ['--version'], 'deno')
|
||||||
|
if (!validation.ok) {
|
||||||
|
log(`Existing ${outputName} failed version check: ${validation.message}`, 'warn')
|
||||||
|
}
|
||||||
log(`${outputName} already exists, skipping download`, 'info')
|
log(`${outputName} already exists, skipping download`, 'info')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -549,6 +641,11 @@ async function downloadDenoRuntime() {
|
|||||||
|
|
||||||
fs.copyFileSync(sourcePath, outputPath)
|
fs.copyFileSync(sourcePath, outputPath)
|
||||||
setExecutable(outputPath)
|
setExecutable(outputPath)
|
||||||
|
const validation = checkBinary(outputPath, ['--version'], 'deno')
|
||||||
|
if (!validation.ok) {
|
||||||
|
safeUnlink(outputPath)
|
||||||
|
throw new Error(`Downloaded ${outputName} failed version check: ${validation.message}`)
|
||||||
|
}
|
||||||
log(`Downloaded ${outputName} successfully`, 'success')
|
log(`Downloaded ${outputName} successfully`, 'success')
|
||||||
|
|
||||||
fs.unlinkSync(tempZip)
|
fs.unlinkSync(tempZip)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const buildDownloadArgs = (
|
|||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
jsRuntimeArgs: string[] = []
|
jsRuntimeArgs: string[] = []
|
||||||
): string[] => {
|
): string[] => {
|
||||||
const args: string[] = ['--no-playlist', '--embed-chapters', '--no-mtime']
|
const args: string[] = ['--no-playlist', '--no-mtime']
|
||||||
|
|
||||||
// Add encoding support for proper handling of non-ASCII characters
|
// Add encoding support for proper handling of non-ASCII characters
|
||||||
args.push('--encoding', 'utf-8')
|
args.push('--encoding', 'utf-8')
|
||||||
@@ -95,11 +95,26 @@ export const buildDownloadArgs = (
|
|||||||
args.push('--download-sections', `*${start}-${end || ''}`)
|
args.push('--download-sections', `*${start}-${end || ''}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const embedSubs = settings.embedSubs
|
||||||
|
const embedMetadata = settings.embedMetadata
|
||||||
|
const embedChapters = settings.embedChapters
|
||||||
|
|
||||||
// Subtitles
|
// Subtitles
|
||||||
if (options.downloadSubs) {
|
if (options.downloadSubs || embedSubs) {
|
||||||
args.push('--write-subs', '--sub-langs', 'all')
|
args.push('--sub-langs', 'all')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.downloadSubs) {
|
||||||
|
args.push('--write-subs')
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(embedSubs ? '--embed-subs' : '--no-embed-subs')
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
args.push(settings.embedThumbnail ? '--embed-thumbnail' : '--no-embed-thumbnail')
|
||||||
|
}
|
||||||
|
args.push(embedMetadata ? '--embed-metadata' : '--no-embed-metadata')
|
||||||
|
args.push(embedChapters ? '--embed-chapters' : '--no-embed-chapters')
|
||||||
|
|
||||||
// Output path with proper encoding handling
|
// Output path with proper encoding handling
|
||||||
const baseDownloadPath = options.customDownloadPath?.trim() || downloadPath
|
const baseDownloadPath = options.customDownloadPath?.trim() || downloadPath
|
||||||
const filenameTemplate = sanitizeFilenameTemplate(
|
const filenameTemplate = sanitizeFilenameTemplate(
|
||||||
|
|||||||
@@ -364,12 +364,6 @@ function initAutoUpdater(): void {
|
|||||||
autoUpdater.on('update-downloaded', (info) => {
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
log.info('Update downloaded:', info.version)
|
log.info('Update downloaded:', info.version)
|
||||||
mainWindow?.webContents.send('update:downloaded', info)
|
mainWindow?.webContents.send('update:downloaded', info)
|
||||||
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('update:show-notification', {
|
|
||||||
version: info.version
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
log.info('Auto-updater initialized successfully')
|
log.info('Auto-updater initialized successfully')
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function AppContent() {
|
|||||||
const loadSettings = useSetAtom(loadSettingsAtom)
|
const loadSettings = useSetAtom(loadSettingsAtom)
|
||||||
const setUpdateReady = useSetAtom(updateReadyAtom)
|
const setUpdateReady = useSetAtom(updateReadyAtom)
|
||||||
const setUpdateAvailable = useSetAtom(updateAvailableAtom)
|
const setUpdateAvailable = useSetAtom(updateAvailableAtom)
|
||||||
const { t } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const updateDownloadInProgressRef = useRef(false)
|
const updateDownloadInProgressRef = useRef(false)
|
||||||
const analyticsScriptRef = useRef<HTMLScriptElement | null>(null)
|
const analyticsScriptRef = useRef<HTMLScriptElement | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -197,14 +197,27 @@ function AppContent() {
|
|||||||
available: true,
|
available: true,
|
||||||
version: info.version
|
version: info.version
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const versionLabel = info?.version ?? ''
|
||||||
|
const downloadedMessage = versionLabel
|
||||||
|
? i18n.t('about.notifications.updateDownloadedVersion', { version: versionLabel })
|
||||||
|
: i18n.t('about.notifications.updateDownloaded')
|
||||||
|
toast.info(downloadedMessage, {
|
||||||
|
action: {
|
||||||
|
label: i18n.t('about.notifications.restartNowAction'),
|
||||||
|
onClick: () => {
|
||||||
|
void ipcServices.update.quitAndInstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateError = (rawMessage: unknown) => {
|
const handleUpdateError = (rawMessage: unknown) => {
|
||||||
const message = typeof rawMessage === 'string' ? rawMessage : ''
|
const message = typeof rawMessage === 'string' ? rawMessage : ''
|
||||||
resetDownloadState()
|
resetDownloadState()
|
||||||
|
|
||||||
const errorMessage = message || t('about.notifications.unknownErrorFallback')
|
const errorMessage = message || i18n.t('about.notifications.unknownErrorFallback')
|
||||||
toast.error(t('about.notifications.updateError', { error: errorMessage }))
|
toast.error(i18n.t('about.notifications.updateError', { error: errorMessage }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownloadProgress = (rawProgress: unknown) => {
|
const handleDownloadProgress = (rawProgress: unknown) => {
|
||||||
@@ -214,39 +227,20 @@ function AppContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdateNotification = (rawPayload: unknown) => {
|
|
||||||
const payload = (rawPayload ?? {}) as { version?: string }
|
|
||||||
const versionLabel = payload?.version ?? ''
|
|
||||||
const downloadedMessage = versionLabel
|
|
||||||
? t('about.notifications.updateDownloadedVersion', { version: versionLabel })
|
|
||||||
: t('about.notifications.updateDownloaded')
|
|
||||||
|
|
||||||
toast.info(downloadedMessage, {
|
|
||||||
action: {
|
|
||||||
label: t('about.notifications.restartNowAction'),
|
|
||||||
onClick: () => {
|
|
||||||
void ipcServices.update.quitAndInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only listen to update events that should be shown globally
|
// Only listen to update events that should be shown globally
|
||||||
// update:available shows a visual indicator in the sidebar
|
// update:available shows a visual indicator in the sidebar
|
||||||
ipcEvents.on('update:available', handleUpdateAvailable)
|
ipcEvents.on('update:available', handleUpdateAvailable)
|
||||||
ipcEvents.on('update:downloaded', handleUpdateDownloaded)
|
ipcEvents.on('update:downloaded', handleUpdateDownloaded)
|
||||||
ipcEvents.on('update:error', handleUpdateError)
|
ipcEvents.on('update:error', handleUpdateError)
|
||||||
ipcEvents.on('update:download-progress', handleDownloadProgress)
|
ipcEvents.on('update:download-progress', handleDownloadProgress)
|
||||||
ipcEvents.on('update:show-notification', handleUpdateNotification)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ipcEvents.removeListener('update:available', handleUpdateAvailable)
|
ipcEvents.removeListener('update:available', handleUpdateAvailable)
|
||||||
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
|
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
|
||||||
ipcEvents.removeListener('update:error', handleUpdateError)
|
ipcEvents.removeListener('update:error', handleUpdateError)
|
||||||
ipcEvents.removeListener('update:download-progress', handleDownloadProgress)
|
ipcEvents.removeListener('update:download-progress', handleDownloadProgress)
|
||||||
ipcEvents.removeListener('update:show-notification', handleUpdateNotification)
|
|
||||||
}
|
}
|
||||||
}, [setUpdateAvailable, setUpdateReady, t])
|
}, [i18n, setUpdateAvailable, setUpdateReady])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row h-screen">
|
<div className="flex flex-row h-screen">
|
||||||
|
|||||||
@@ -400,6 +400,10 @@ export function DownloadItem({ download, isSelected = false, onToggleSelect }: D
|
|||||||
|
|
||||||
const statusIcon = getStatusIcon()
|
const statusIcon = getStatusIcon()
|
||||||
const statusText = getStatusText()
|
const statusText = getStatusText()
|
||||||
|
const progressInfo = download.progress
|
||||||
|
const showInlineProgress = Boolean(
|
||||||
|
progressInfo && download.status !== 'completed' && download.status !== 'error'
|
||||||
|
)
|
||||||
const sourceDisplay =
|
const sourceDisplay =
|
||||||
download.uploader && download.channel && download.uploader !== download.channel
|
download.uploader && download.channel && download.uploader !== download.channel
|
||||||
? `${download.uploader} • ${download.channel}`
|
? `${download.uploader} • ${download.channel}`
|
||||||
@@ -725,6 +729,24 @@ export function DownloadItem({ download, isSelected = false, onToggleSelect }: D
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
{showInlineProgress && (
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="font-medium shrink-0">
|
||||||
|
{(progressInfo?.percent ?? 0).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
{progressInfo?.downloaded && progressInfo?.total && (
|
||||||
|
<span className="truncate max-w-[120px]">
|
||||||
|
{progressInfo.downloaded} / {progressInfo.total}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{progressInfo?.currentSpeed && (
|
||||||
|
<span className="truncate max-w-[80px]">{progressInfo.currentSpeed}</span>
|
||||||
|
)}
|
||||||
|
{progressInfo?.eta && (
|
||||||
|
<span className="truncate max-w-[80px]">ETA: {progressInfo.eta}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Timestamp */}
|
{/* Timestamp */}
|
||||||
{timestamp && (
|
{timestamp && (
|
||||||
<span className="truncate shrink-0">{formatDateShort(timestamp)}</span>
|
<span className="truncate shrink-0">{formatDateShort(timestamp)}</span>
|
||||||
@@ -898,26 +920,8 @@ export function DownloadItem({ download, isSelected = false, onToggleSelect }: D
|
|||||||
|
|
||||||
{/* Progress */}
|
{/* Progress */}
|
||||||
{download.progress && download.status !== 'completed' && download.status !== 'error' && (
|
{download.progress && download.status !== 'completed' && download.status !== 'error' && (
|
||||||
<div className="space-y-1 bg-background/60 w-full overflow-hidden">
|
<div className="bg-background/60 w-full overflow-hidden">
|
||||||
<Progress value={download.progress.percent} className="h-1 w-full" />
|
<Progress value={download.progress.percent} className="h-1 w-full" />
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 text-[11px] text-muted-foreground w-full">
|
|
||||||
<span className="font-medium shrink-0">
|
|
||||||
{download.progress.percent.toFixed(1)}%
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap items-center gap-2 min-w-0 flex-1">
|
|
||||||
{download.progress.downloaded && download.progress.total && (
|
|
||||||
<span className="truncate max-w-[100px]">
|
|
||||||
{download.progress.downloaded} / {download.progress.total}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{download.progress.currentSpeed && (
|
|
||||||
<span className="truncate max-w-[80px]">{download.progress.currentSpeed}</span>
|
|
||||||
)}
|
|
||||||
{download.progress.eta && (
|
|
||||||
<span className="truncate max-w-[80px]">ETA: {download.progress.eta}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"currentLocation": "Current download location - ",
|
"currentLocation": "Current download location - ",
|
||||||
"downloadLocation": "Download location",
|
"downloadLocation": "Download location",
|
||||||
"downloadSubs": "Download subtitles if available",
|
"downloadSubs": "Download subtitles if available",
|
||||||
|
"downloadSubsHint": "Save subtitles as separate files when available",
|
||||||
"end": "End",
|
"end": "End",
|
||||||
"endHint": "If kept empty, it will be downloaded to the end",
|
"endHint": "If kept empty, it will be downloaded to the end",
|
||||||
"endPlaceholder": "10:00",
|
"endPlaceholder": "10:00",
|
||||||
@@ -420,6 +421,14 @@
|
|||||||
"launchAtLoginUnsupported": "Auto launch is only available on macOS and Windows.",
|
"launchAtLoginUnsupported": "Auto launch is only available on macOS and Windows.",
|
||||||
"enableAnalytics": "Help improve VidBee",
|
"enableAnalytics": "Help improve VidBee",
|
||||||
"enableAnalyticsDescription": "Share anonymous usage data to help us understand how the app is used and prioritize improvements.",
|
"enableAnalyticsDescription": "Share anonymous usage data to help us understand how the app is used and prioritize improvements.",
|
||||||
|
"embedChapters": "Embed chapters",
|
||||||
|
"embedChaptersDescription": "Add chapter markers to the file when available",
|
||||||
|
"embedMetadata": "Embed metadata",
|
||||||
|
"embedMetadataDescription": "Write title, artist, and other metadata when available",
|
||||||
|
"embedSubs": "Embed subtitles",
|
||||||
|
"embedSubsDescription": "Embed subtitles into the video file (mp4, webm, mkv)",
|
||||||
|
"embedThumbnail": "Embed thumbnail",
|
||||||
|
"embedThumbnailDescription": "Add the thumbnail as cover art",
|
||||||
"maxConcurrentDownloads": "Maximum number of active downloads",
|
"maxConcurrentDownloads": "Maximum number of active downloads",
|
||||||
"maxConcurrentDownloadsDescription": "Maximum number of simultaneous downloads",
|
"maxConcurrentDownloadsDescription": "Maximum number of simultaneous downloads",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
|
|||||||
@@ -39,9 +39,10 @@ type LatestVersionState =
|
|||||||
| null
|
| null
|
||||||
|
|
||||||
export function About() {
|
export function About() {
|
||||||
const { t } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const [settings, _setSettings] = useAtom(settingsAtom)
|
const [settings, _setSettings] = useAtom(settingsAtom)
|
||||||
const [updateReady] = useAtom(updateReadyAtom)
|
const [updateReady] = useAtom(updateReadyAtom)
|
||||||
|
const [updateAvailableState] = useAtom(updateAvailableAtom)
|
||||||
const setUpdateAvailable = useSetAtom(updateAvailableAtom)
|
const setUpdateAvailable = useSetAtom(updateAvailableAtom)
|
||||||
const [appVersion, setAppVersion] = useState<string>('—')
|
const [appVersion, setAppVersion] = useState<string>('—')
|
||||||
const [latestVersionState, setLatestVersionState] = useState<LatestVersionState>(null)
|
const [latestVersionState, setLatestVersionState] = useState<LatestVersionState>(null)
|
||||||
@@ -70,6 +71,17 @@ export function About() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!updateAvailableState.available) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLatestVersionState({
|
||||||
|
status: 'available',
|
||||||
|
version: updateAvailableState.version ?? ''
|
||||||
|
})
|
||||||
|
}, [updateAvailableState.available, updateAvailableState.version])
|
||||||
|
|
||||||
// Listen for update events only in About page
|
// Listen for update events only in About page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!window?.api) {
|
if (!window?.api) {
|
||||||
@@ -81,7 +93,7 @@ export function About() {
|
|||||||
const versionLabel = info.version ?? ''
|
const versionLabel = info.version ?? ''
|
||||||
|
|
||||||
// Update will be downloaded automatically because autoDownload is enabled in main process
|
// Update will be downloaded automatically because autoDownload is enabled in main process
|
||||||
toast.success(t('about.notifications.updateAvailable', { version: versionLabel }))
|
toast.success(i18n.t('about.notifications.updateAvailable', { version: versionLabel }))
|
||||||
setLatestVersionState({
|
setLatestVersionState({
|
||||||
status: 'available',
|
status: 'available',
|
||||||
version: versionLabel
|
version: versionLabel
|
||||||
@@ -115,7 +127,7 @@ export function About() {
|
|||||||
ipcEvents.removeListener('update:download-progress', handleUpdateDownloadProgress)
|
ipcEvents.removeListener('update:download-progress', handleUpdateDownloadProgress)
|
||||||
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
|
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
|
||||||
}
|
}
|
||||||
}, [setUpdateAvailable, t])
|
}, [i18n, setUpdateAvailable])
|
||||||
|
|
||||||
const handleSettingChange = async (
|
const handleSettingChange = async (
|
||||||
key: keyof typeof settings,
|
key: keyof typeof settings,
|
||||||
@@ -264,6 +276,8 @@ export function About() {
|
|||||||
? 'text-destructive'
|
? 'text-destructive'
|
||||||
: 'text-muted-foreground'
|
: 'text-muted-foreground'
|
||||||
const latestVersionStatusText = latestVersionStatusKey ? t(latestVersionStatusKey) : null
|
const latestVersionStatusText = latestVersionStatusKey ? t(latestVersionStatusKey) : null
|
||||||
|
const shouldShowCheckUpdates =
|
||||||
|
!updateAvailableState.available && latestVersionState?.status !== 'available'
|
||||||
|
|
||||||
const handleXFeedback = useCallback(() => {
|
const handleXFeedback = useCallback(() => {
|
||||||
const versionText = appVersion !== '—' ? ` VidBee v${appVersion}` : ' VidBee'
|
const versionText = appVersion !== '—' ? ` VidBee v${appVersion}` : ' VidBee'
|
||||||
@@ -379,10 +393,12 @@ export function About() {
|
|||||||
{t('about.actions.goToDownload')}
|
{t('about.actions.goToDownload')}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Button onClick={handleCheckForUpdates} size="sm" className="gap-2">
|
{shouldShowCheckUpdates ? (
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
<Button onClick={handleCheckForUpdates} size="sm" className="gap-2">
|
||||||
{t('about.actions.checkUpdates')}
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
</Button>
|
{t('about.actions.checkUpdates')}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{t('about.description')}</p>
|
<p className="text-sm text-muted-foreground">{t('about.description')}</p>
|
||||||
|
|||||||
@@ -519,6 +519,94 @@ export function Settings() {
|
|||||||
</Item>
|
</Item>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Item variant="muted">
|
||||||
|
<ItemContent>
|
||||||
|
<ItemTitle>{t('settings.embedSubs')}</ItemTitle>
|
||||||
|
<ItemDescription>{t('settings.embedSubsDescription')}</ItemDescription>
|
||||||
|
</ItemContent>
|
||||||
|
<ItemActions>
|
||||||
|
<Switch
|
||||||
|
checked={settings.embedSubs ?? false}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
try {
|
||||||
|
handleSettingChange('embedSubs', value)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[Settings] Error toggling embedSubs:', error)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ItemActions>
|
||||||
|
</Item>
|
||||||
|
|
||||||
|
<ItemSeparator />
|
||||||
|
|
||||||
|
{platform !== 'darwin' && (
|
||||||
|
<>
|
||||||
|
<Item variant="muted">
|
||||||
|
<ItemContent>
|
||||||
|
<ItemTitle>{t('settings.embedThumbnail')}</ItemTitle>
|
||||||
|
<ItemDescription>{t('settings.embedThumbnailDescription')}</ItemDescription>
|
||||||
|
</ItemContent>
|
||||||
|
<ItemActions>
|
||||||
|
<Switch
|
||||||
|
checked={settings.embedThumbnail ?? false}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
try {
|
||||||
|
handleSettingChange('embedThumbnail', value)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[Settings] Error toggling embedThumbnail:', error)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ItemActions>
|
||||||
|
</Item>
|
||||||
|
|
||||||
|
<ItemSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Item variant="muted">
|
||||||
|
<ItemContent>
|
||||||
|
<ItemTitle>{t('settings.embedMetadata')}</ItemTitle>
|
||||||
|
<ItemDescription>{t('settings.embedMetadataDescription')}</ItemDescription>
|
||||||
|
</ItemContent>
|
||||||
|
<ItemActions>
|
||||||
|
<Switch
|
||||||
|
checked={settings.embedMetadata ?? false}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
try {
|
||||||
|
handleSettingChange('embedMetadata', value)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[Settings] Error toggling embedMetadata:', error)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ItemActions>
|
||||||
|
</Item>
|
||||||
|
|
||||||
|
<ItemSeparator />
|
||||||
|
|
||||||
|
<Item variant="muted">
|
||||||
|
<ItemContent>
|
||||||
|
<ItemTitle>{t('settings.embedChapters')}</ItemTitle>
|
||||||
|
<ItemDescription>{t('settings.embedChaptersDescription')}</ItemDescription>
|
||||||
|
</ItemContent>
|
||||||
|
<ItemActions>
|
||||||
|
<Switch
|
||||||
|
checked={settings.embedChapters ?? true}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
try {
|
||||||
|
handleSettingChange('embedChapters', value)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[Settings] Error toggling embedChapters:', error)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ItemActions>
|
||||||
|
</Item>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Item variant="muted">
|
<Item variant="muted">
|
||||||
<ItemContent>
|
<ItemContent>
|
||||||
|
|||||||
@@ -271,6 +271,10 @@ export interface AppSettings {
|
|||||||
autoUpdate: boolean
|
autoUpdate: boolean
|
||||||
subscriptionOnlyLatestDefault: boolean
|
subscriptionOnlyLatestDefault: boolean
|
||||||
enableAnalytics: boolean
|
enableAnalytics: boolean
|
||||||
|
embedSubs: boolean
|
||||||
|
embedThumbnail: boolean
|
||||||
|
embedMetadata: boolean
|
||||||
|
embedChapters: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE = '%(uploader)s/%(title)s.%(ext)s'
|
export const DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE = '%(uploader)s/%(title)s.%(ext)s'
|
||||||
@@ -294,5 +298,9 @@ export const defaultSettings: AppSettings = {
|
|||||||
launchAtLogin: false,
|
launchAtLogin: false,
|
||||||
autoUpdate: true,
|
autoUpdate: true,
|
||||||
subscriptionOnlyLatestDefault: true,
|
subscriptionOnlyLatestDefault: true,
|
||||||
enableAnalytics: true
|
enableAnalytics: true,
|
||||||
|
embedSubs: true,
|
||||||
|
embedThumbnail: true,
|
||||||
|
embedMetadata: true,
|
||||||
|
embedChapters: true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user