Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a28b0db85 | ||
|
|
ca4b851ba4 | ||
|
|
0742dac057 | ||
|
|
9d2e35c322 | ||
|
|
526bd994d8 | ||
|
|
37c1f22aee | ||
|
|
10a97b1f4a | ||
|
|
11eaaf0f88 | ||
|
|
512b9d1175 | ||
|
|
a1307bdd0b | ||
|
|
06132a0704 | ||
|
|
a7e4eb865b | ||
|
|
5246ab8369 | ||
|
|
baa887da2e | ||
|
|
be40f087ad | ||
|
|
ad62bd07aa | ||
|
|
365b9c0660 | ||
|
|
a1f5e0480c | ||
|
|
00a7950e6a | ||
|
|
598ed9c964 | ||
|
|
2b3edab4eb | ||
|
|
d0bdfce803 | ||
|
|
17a4568492 |
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -6,7 +6,19 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: windows-latest
|
||||
build-script: pnpm run build:win
|
||||
ytdlp-asset: yt-dlp.exe
|
||||
ytdlp-output: yt-dlp.exe
|
||||
- os: ubuntu-latest
|
||||
build-script: pnpm run build:linux
|
||||
ytdlp-asset: yt-dlp
|
||||
ytdlp-output: yt-dlp_linux
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -25,11 +37,16 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Type check
|
||||
run: pnpm run typecheck
|
||||
- name: Download yt-dlp binary
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp-asset }}" -o "resources/${{ matrix.ytdlp-output }}"
|
||||
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
chmod +x "resources/${{ matrix.ytdlp-output }}"
|
||||
fi
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check
|
||||
|
||||
- name: Build check
|
||||
run: pnpm run build
|
||||
- name: Build application
|
||||
run: ${{ matrix.build-script }}
|
||||
|
||||
46
.github/workflows/release.yml
vendored
46
.github/workflows/release.yml
vendored
@@ -11,7 +11,19 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest]
|
||||
include:
|
||||
- os: windows-latest
|
||||
build_script: pnpm run build:win
|
||||
ytdlp_asset: yt-dlp.exe
|
||||
ytdlp_output: yt-dlp.exe
|
||||
- os: macos-latest
|
||||
build_script: pnpm run build:mac
|
||||
ytdlp_asset: yt-dlp_macos
|
||||
ytdlp_output: yt-dlp_macos
|
||||
- os: ubuntu-latest
|
||||
build_script: pnpm run build:linux
|
||||
ytdlp_asset: yt-dlp
|
||||
ytdlp_output: yt-dlp_linux
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
@@ -30,38 +42,24 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download yt-dlp binaries
|
||||
- name: Download yt-dlp binary
|
||||
shell: bash
|
||||
run: |
|
||||
# Download yt-dlp.exe for Windows
|
||||
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe -o resources/yt-dlp.exe
|
||||
|
||||
# Download yt-dlp for macOS (for cross-platform builds)
|
||||
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos -o resources/yt-dlp_macos
|
||||
chmod +x resources/yt-dlp_macos
|
||||
|
||||
# Download yt-dlp for Linux (for cross-platform builds)
|
||||
# curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o resources/yt-dlp_linux
|
||||
# chmod +x resources/yt-dlp_linux
|
||||
curl -L "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp_asset }}" -o "resources/${{ matrix.ytdlp_output }}"
|
||||
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
chmod +x "resources/${{ matrix.ytdlp_output }}"
|
||||
fi
|
||||
|
||||
- name: Lint and format check
|
||||
run: pnpm run check
|
||||
|
||||
# - name: build-linux
|
||||
# if: matrix.os == 'ubuntu-latest'
|
||||
# run: pnpm run build:linux
|
||||
|
||||
- name: build-mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: pnpm run build:mac
|
||||
|
||||
- name: build-win
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pnpm run build:win
|
||||
- name: Build application
|
||||
run: ${{ matrix.build_script }}
|
||||
|
||||
- name: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
|
||||
11
README.md
11
README.md
@@ -16,7 +16,6 @@
|
||||
- **Multi-format Support** - Videos, audio tracks, playlists to meet all download needs
|
||||
- Localized interface support in many languages
|
||||
|
||||
|
||||
### 🎨 Best-in-class UI Experience
|
||||
|
||||
- **Modern Design** - Clean and beautiful interface
|
||||
@@ -33,6 +32,16 @@
|
||||
- **Linux**: Download `vidbee-x.x.x.AppImage`
|
||||
3. **Install and run** the application
|
||||
|
||||
### 🍎 macOS Installation Notes
|
||||
|
||||
After downloading and installing VidBee on macOS, you may encounter a "file is damaged" error when trying to run the application. This is due to macOS security restrictions on applications downloaded from the internet.
|
||||
|
||||
```bash
|
||||
xattr -rd com.apple.quarantine /Applications/VidBee.app/
|
||||
```
|
||||
|
||||
This command removes the quarantine attribute that macOS applies to applications downloaded from the internet, allowing VidBee to run properly without the "file is damaged" error.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||

|
||||
|
||||
@@ -21,14 +21,17 @@ nsis:
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
artifactName: ${name}-${version}-${arch}.${ext}
|
||||
target:
|
||||
- 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}
|
||||
|
||||
@@ -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>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vidbee",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.2",
|
||||
"description": "A modern Electron application for downloading videos and audios",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "VidBee",
|
||||
@@ -16,7 +16,7 @@
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:unpack": "pnpm run build && electron-builder --dir",
|
||||
"build:win": "node scripts/check-ytdlp.js win && pnpm run build && electron-builder --win",
|
||||
"build:mac": "node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac",
|
||||
"build:mac": "node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac --x64 --arm64",
|
||||
"build:linux": "node scripts/check-ytdlp.js linux && pnpm run build && electron-builder --linux",
|
||||
"release": "pnpm run check && bumpp"
|
||||
},
|
||||
@@ -47,6 +47,7 @@
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-store": "^11.0.2",
|
||||
"electron-updater": "^6.3.9",
|
||||
"flag-icons": "^7.5.0",
|
||||
"i18next": "^25.5.3",
|
||||
"jotai": "^2.15.0",
|
||||
"lucide-react": "^0.544.0",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -86,6 +86,9 @@ importers:
|
||||
electron-updater:
|
||||
specifier: ^6.3.9
|
||||
version: 6.6.2
|
||||
flag-icons:
|
||||
specifier: ^7.5.0
|
||||
version: 7.5.0
|
||||
i18next:
|
||||
specifier: ^25.5.3
|
||||
version: 25.6.0(typescript@5.9.3)
|
||||
@@ -2056,6 +2059,9 @@ packages:
|
||||
filelist@1.0.4:
|
||||
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
|
||||
|
||||
flag-icons@7.5.0:
|
||||
resolution: {integrity: sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -5350,6 +5356,8 @@ snapshots:
|
||||
dependencies:
|
||||
minimatch: 5.1.6
|
||||
|
||||
flag-icons@7.5.0: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
@@ -90,6 +90,11 @@ export const buildDownloadArgs = (
|
||||
args.push('--cookies-from-browser', settings.browserForCookies)
|
||||
}
|
||||
|
||||
const cookiesPath = settings.cookiesPath?.trim()
|
||||
if (cookiesPath) {
|
||||
args.push('--cookies', cookiesPath)
|
||||
}
|
||||
|
||||
if (settings.proxy) {
|
||||
args.push('--proxy', settings.proxy)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from 'node:path'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow, shell } from 'electron'
|
||||
import { app, BrowserWindow, type BrowserWindowConstructorOptions, shell } from 'electron'
|
||||
import log from 'electron-log/main'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import appIcon from '../../build/icon.png?asset'
|
||||
@@ -10,6 +10,7 @@ import { downloadEngine } from './lib/download-engine'
|
||||
import { ytdlpManager } from './lib/ytdlp-manager'
|
||||
import { settingsManager } from './settings'
|
||||
import { createTray, destroyTray } from './tray'
|
||||
import { applyDockVisibility } from './utils/dock'
|
||||
|
||||
// Initialize electron-log for main process
|
||||
log.initialize()
|
||||
@@ -21,17 +22,16 @@ let mainWindow: BrowserWindow | null = null
|
||||
let isQuitting = false
|
||||
|
||||
export function createWindow(): void {
|
||||
// Create the browser window
|
||||
mainWindow = new BrowserWindow({
|
||||
const isMac = process.platform === 'darwin'
|
||||
const isWindows = process.platform === 'win32'
|
||||
|
||||
const windowOptions: BrowserWindowConstructorOptions = {
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
titleBarStyle: 'hidden', // Hide title bar on macOS
|
||||
autoHideMenuBar: true,
|
||||
icon: appIcon, // Set application icon
|
||||
frame: false,
|
||||
vibrancy: 'fullscreen-ui', // on MacOS
|
||||
backgroundMaterial: 'acrylic', // on Windows 11
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
@@ -39,7 +39,20 @@ export function createWindow(): void {
|
||||
nodeIntegration: false,
|
||||
webSecurity: false // Allow drag regions to work
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isMac) {
|
||||
windowOptions.titleBarStyle = 'hidden'
|
||||
windowOptions.trafficLightPosition = { x: 12.5, y: 10 }
|
||||
windowOptions.vibrancy = 'fullscreen-ui'
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
windowOptions.backgroundMaterial = 'acrylic'
|
||||
}
|
||||
|
||||
// Create the browser window
|
||||
mainWindow = new BrowserWindow(windowOptions)
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
const closeToTray = settingsManager.get('closeToTray')
|
||||
@@ -94,12 +107,12 @@ function setupDownloadEvents(): void {
|
||||
|
||||
function initAutoUpdater(): void {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log('Skipping auto-updater initialization in development mode')
|
||||
log.info('Skipping auto-updater initialization in development mode')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Initializing auto-updater...')
|
||||
log.info('Initializing auto-updater...')
|
||||
|
||||
log.transports.file.level = 'info'
|
||||
autoUpdater.logger = log
|
||||
@@ -108,31 +121,26 @@ function initAutoUpdater(): void {
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
log.info('Update available:', info.version)
|
||||
console.log('Update available:', info.version)
|
||||
mainWindow?.webContents.send('update:available', info)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
log.info('Update not available:', info.version)
|
||||
console.log('Update not available:', info.version)
|
||||
mainWindow?.webContents.send('update:not-available', info)
|
||||
})
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
log.error('Update error:', err)
|
||||
console.error('Update error:', err)
|
||||
mainWindow?.webContents.send('update:error', err.message)
|
||||
})
|
||||
|
||||
autoUpdater.on('download-progress', (progressObj) => {
|
||||
log.info('Download progress:', progressObj.percent)
|
||||
console.log('Download progress:', progressObj.percent)
|
||||
mainWindow?.webContents.send('update:download-progress', progressObj)
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
log.info('Update downloaded:', info.version)
|
||||
console.log('Update downloaded:', info.version)
|
||||
mainWindow?.webContents.send('update:downloaded', info)
|
||||
|
||||
if (mainWindow) {
|
||||
@@ -146,15 +154,12 @@ function initAutoUpdater(): void {
|
||||
|
||||
if (settingsManager.get('autoUpdate')) {
|
||||
log.info('Auto-update is enabled, checking for updates...')
|
||||
console.log('Auto-update is enabled, checking for updates...')
|
||||
void autoUpdater.checkForUpdatesAndNotify()
|
||||
}
|
||||
|
||||
log.info('Auto-updater initialized successfully')
|
||||
console.log('Auto-updater initialized successfully')
|
||||
} catch (error) {
|
||||
log.error('Failed to initialize auto-updater:', error)
|
||||
console.error('Failed to initialize auto-updater:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +188,8 @@ app.whenReady().then(async () => {
|
||||
log.error('Failed to initialize yt-dlp:', error)
|
||||
}
|
||||
|
||||
applyDockVisibility(settingsManager.get('hideDockIcon'))
|
||||
|
||||
createWindow()
|
||||
|
||||
initAutoUpdater()
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
DownloadItem,
|
||||
DownloadOptions,
|
||||
PlaylistDownloadOptions,
|
||||
PlaylistDownloadResult,
|
||||
PlaylistInfo,
|
||||
VideoInfo
|
||||
} from '../../../shared/types'
|
||||
@@ -45,7 +46,7 @@ class DownloadService extends IpcService {
|
||||
async startPlaylistDownload(
|
||||
_context: IpcContext,
|
||||
options: PlaylistDownloadOptions
|
||||
): Promise<string[]> {
|
||||
): Promise<PlaylistDownloadResult> {
|
||||
return downloadEngine.startPlaylistDownload(options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { execFile, execSync } from 'node:child_process'
|
||||
import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
@@ -40,7 +40,20 @@ class FileSystemService extends IpcService {
|
||||
|
||||
@IpcMethod()
|
||||
getDefaultDownloadPath(_context: IpcContext): string {
|
||||
return `${os.homedir()}/Downloads`
|
||||
const fallbackPath = path.join(os.homedir(), 'Downloads')
|
||||
|
||||
if (process.platform === 'linux' || process.platform === 'freebsd') {
|
||||
try {
|
||||
const xdgPath = execSync('xdg-user-dir DOWNLOAD', { encoding: 'utf8' }).trim()
|
||||
if (xdgPath) {
|
||||
return xdgPath
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Unable to resolve XDG download directory, falling back to default:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackPath
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
|
||||
import type { AppSettings } from '../../../shared/types'
|
||||
import { settingsManager } from '../../settings'
|
||||
import { updateTrayMenu } from '../../tray'
|
||||
import { applyDockVisibility } from '../../utils/dock'
|
||||
|
||||
class SettingsService extends IpcService {
|
||||
static readonly groupName = 'settings'
|
||||
@@ -18,6 +19,10 @@ class SettingsService extends IpcService {
|
||||
if (key === 'language') {
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
if (key === 'hideDockIcon') {
|
||||
applyDockVisibility(value as AppSettings['hideDockIcon'])
|
||||
}
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
@@ -32,11 +37,16 @@ class SettingsService extends IpcService {
|
||||
if (settings.language) {
|
||||
updateTrayMenu()
|
||||
}
|
||||
|
||||
if (typeof settings.hideDockIcon === 'boolean') {
|
||||
applyDockVisibility(settings.hideDockIcon)
|
||||
}
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
reset(_context: IpcContext): void {
|
||||
settingsManager.reset()
|
||||
applyDockVisibility(settingsManager.get('hideDockIcon'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,33 @@ import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { settingsManager } from '../../settings'
|
||||
|
||||
const isNewerVersion = (latest: string, current: string): boolean => {
|
||||
const toSegments = (version: string) =>
|
||||
version.split(/[.-]/).map((segment) => {
|
||||
const parsed = Number.parseInt(segment, 10)
|
||||
return Number.isNaN(parsed) ? 0 : parsed
|
||||
})
|
||||
|
||||
const latestSegments = toSegments(latest)
|
||||
const currentSegments = toSegments(current)
|
||||
const maxLength = Math.max(latestSegments.length, currentSegments.length)
|
||||
|
||||
for (let index = 0; index < maxLength; index += 1) {
|
||||
const latestValue = latestSegments[index] ?? 0
|
||||
const currentValue = currentSegments[index] ?? 0
|
||||
|
||||
if (latestValue > currentValue) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (latestValue < currentValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
class UpdateService extends IpcService {
|
||||
static readonly groupName = 'update'
|
||||
|
||||
@@ -11,11 +38,20 @@ class UpdateService extends IpcService {
|
||||
_context: IpcContext
|
||||
): Promise<{ available: boolean; version?: string; error?: string }> {
|
||||
try {
|
||||
// In production, use checkForUpdatesAndNotify for automatic notifications
|
||||
const result = await autoUpdater.checkForUpdatesAndNotify()
|
||||
const currentVersion = app.getVersion()
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
const latestVersion = result?.updateInfo?.version
|
||||
|
||||
if (latestVersion && isNewerVersion(latestVersion, currentVersion)) {
|
||||
return {
|
||||
available: true,
|
||||
version: latestVersion
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
available: result !== null,
|
||||
version: result?.updateInfo?.version
|
||||
available: false,
|
||||
version: latestVersion ?? currentVersion
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
DownloadOptions,
|
||||
DownloadProgress,
|
||||
PlaylistDownloadOptions,
|
||||
PlaylistDownloadResult,
|
||||
PlaylistInfo,
|
||||
VideoFormat,
|
||||
VideoInfo
|
||||
@@ -61,6 +62,11 @@ class DownloadEngine extends EventEmitter {
|
||||
args.push('--cookies-from-browser', settings.browserForCookies)
|
||||
}
|
||||
|
||||
const cookiesPath = settings.cookiesPath?.trim()
|
||||
if (cookiesPath) {
|
||||
args.push('--cookies', cookiesPath)
|
||||
}
|
||||
|
||||
// Add config file if configured
|
||||
if (settings.configPath) {
|
||||
args.push('--config-location', `"${settings.configPath}"`)
|
||||
@@ -115,7 +121,7 @@ class DownloadEngine extends EventEmitter {
|
||||
const ytdlp = ytdlpManager.getInstance()
|
||||
const settings = settingsManager.getAll()
|
||||
|
||||
const args = ['-j', '--flat-playlist', '--no-warnings']
|
||||
const args = ['-J', '--flat-playlist', '--no-warnings']
|
||||
|
||||
// Add encoding support for proper handling of non-ASCII characters
|
||||
args.push('--encoding', 'utf-8')
|
||||
@@ -130,8 +136,57 @@ class DownloadEngine extends EventEmitter {
|
||||
args.push('--cookies-from-browser', settings.browserForCookies)
|
||||
}
|
||||
|
||||
const cookiesPath = settings.cookiesPath?.trim()
|
||||
if (cookiesPath) {
|
||||
args.push('--cookies', cookiesPath)
|
||||
}
|
||||
|
||||
// Add config file if configured
|
||||
if (settings.configPath) {
|
||||
args.push('--config-location', `"${settings.configPath}"`)
|
||||
}
|
||||
|
||||
args.push(url)
|
||||
|
||||
type RawPlaylistEntry = {
|
||||
id?: string
|
||||
title?: string
|
||||
url?: string
|
||||
webpage_url?: string
|
||||
original_url?: string
|
||||
ie_key?: string
|
||||
}
|
||||
|
||||
const resolveEntryUrl = (entry: RawPlaylistEntry): string => {
|
||||
if (entry.url && typeof entry.url === 'string' && entry.url.startsWith('http')) {
|
||||
return entry.url
|
||||
}
|
||||
if (entry.webpage_url && typeof entry.webpage_url === 'string') {
|
||||
return entry.webpage_url
|
||||
}
|
||||
if (entry.original_url && typeof entry.original_url === 'string') {
|
||||
return entry.original_url
|
||||
}
|
||||
if (entry.url && typeof entry.url === 'string') {
|
||||
if (entry.ie_key && typeof entry.ie_key === 'string') {
|
||||
const extractor = entry.ie_key.toLowerCase()
|
||||
if (extractor.includes('youtube')) {
|
||||
return `https://www.youtube.com/watch?v=${entry.url}`
|
||||
}
|
||||
if (extractor.includes('youtubemusic')) {
|
||||
return `https://music.youtube.com/watch?v=${entry.url}`
|
||||
}
|
||||
}
|
||||
if (entry.url.startsWith('https://') || entry.url.startsWith('http://')) {
|
||||
return entry.url
|
||||
}
|
||||
}
|
||||
if (entry.id && typeof entry.id === 'string') {
|
||||
return entry.id
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = ytdlp.exec(args)
|
||||
let stdout = ''
|
||||
@@ -148,51 +203,107 @@ class DownloadEngine extends EventEmitter {
|
||||
process.on('close', (code) => {
|
||||
if (code === 0 && stdout) {
|
||||
try {
|
||||
const lines = stdout.trim().split('\n')
|
||||
const entries = lines.map((line) => JSON.parse(line))
|
||||
const playlistEntry = entries[0]
|
||||
const parsed = JSON.parse(stdout) as {
|
||||
id?: string
|
||||
title?: string
|
||||
entries?: RawPlaylistEntry[]
|
||||
}
|
||||
const rawEntries = Array.isArray(parsed.entries) ? parsed.entries : []
|
||||
const entries = rawEntries
|
||||
.map((entry, index) => {
|
||||
const resolvedUrl = resolveEntryUrl(entry)
|
||||
return {
|
||||
id: entry.id || `${index}`,
|
||||
title: entry.title || `Entry ${index + 1}`,
|
||||
url: resolvedUrl,
|
||||
index: index + 1
|
||||
}
|
||||
})
|
||||
.filter((entry) => entry.url)
|
||||
|
||||
scopedLoggers.download.info(
|
||||
'Successfully retrieved playlist info for:',
|
||||
url,
|
||||
'entries:',
|
||||
entries.length
|
||||
)
|
||||
resolve({
|
||||
id: playlistEntry.id || '',
|
||||
title: playlistEntry.title || 'Playlist',
|
||||
entries: entries.map((entry) => ({
|
||||
id: entry.id || '',
|
||||
title: entry.title || 'Unknown',
|
||||
url: entry.url || entry.webpage_url || ''
|
||||
})),
|
||||
id: parsed.id || url,
|
||||
title: parsed.title || 'Playlist',
|
||||
entries,
|
||||
entryCount: entries.length
|
||||
})
|
||||
} catch (error) {
|
||||
scopedLoggers.download.error('Failed to parse playlist info for:', url, error)
|
||||
reject(new Error(`Failed to parse playlist info: ${error}`))
|
||||
}
|
||||
} else {
|
||||
scopedLoggers.download.error(
|
||||
'Failed to fetch playlist info for:',
|
||||
url,
|
||||
'Exit code:',
|
||||
code,
|
||||
'Error:',
|
||||
stderr
|
||||
)
|
||||
reject(new Error(stderr || 'Failed to fetch playlist info'))
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', (error) => {
|
||||
scopedLoggers.download.error('yt-dlp process error while fetching playlist info:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async startPlaylistDownload(options: PlaylistDownloadOptions): Promise<string[]> {
|
||||
async startPlaylistDownload(options: PlaylistDownloadOptions): Promise<PlaylistDownloadResult> {
|
||||
const playlistInfo = await this.getPlaylistInfo(options.url)
|
||||
const downloadIds: string[] = []
|
||||
const downloadEntries: PlaylistDownloadResult['entries'] = []
|
||||
const groupId = `playlist_group_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`
|
||||
|
||||
// Calculate the range of entries to download
|
||||
const startIndex = (options.startIndex || 1) - 1 // Convert to 0-based index
|
||||
const endIndex = options.endIndex ? options.endIndex - 1 : playlistInfo.entries.length - 1
|
||||
const entriesToDownload = playlistInfo.entries.slice(startIndex, endIndex + 1)
|
||||
const totalEntries = playlistInfo.entries.length
|
||||
if (totalEntries === 0) {
|
||||
scopedLoggers.download.warn('Playlist has no entries:', options.url)
|
||||
return {
|
||||
groupId,
|
||||
playlistId: playlistInfo.id,
|
||||
playlistTitle: playlistInfo.title,
|
||||
type: options.type,
|
||||
totalCount: 0,
|
||||
startIndex: 0,
|
||||
endIndex: 0,
|
||||
entries: []
|
||||
}
|
||||
}
|
||||
|
||||
const requestedStart = Math.max((options.startIndex ?? 1) - 1, 0)
|
||||
const requestedEnd = options.endIndex
|
||||
? Math.min(options.endIndex - 1, totalEntries - 1)
|
||||
: totalEntries - 1
|
||||
const rangeStart = Math.min(requestedStart, requestedEnd)
|
||||
const rangeEnd = Math.max(requestedStart, requestedEnd)
|
||||
const rawEntries = playlistInfo.entries.slice(rangeStart, rangeEnd + 1)
|
||||
const settings = settingsManager.getAll()
|
||||
|
||||
const selectedEntries = rawEntries.filter((entry) => {
|
||||
if (!entry.url) {
|
||||
scopedLoggers.download.warn('Skipping playlist entry with missing URL:', entry)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const selectionSize = selectedEntries.length
|
||||
|
||||
scopedLoggers.download.info(
|
||||
`Starting playlist download: ${entriesToDownload.length} videos from "${playlistInfo.title}"`
|
||||
`Starting playlist download: ${selectionSize} items from "${playlistInfo.title}"`
|
||||
)
|
||||
|
||||
// Create download items for each video in the playlist
|
||||
for (const entry of entriesToDownload) {
|
||||
const downloadId = `playlist_${Date.now()}_${Math.random().toString(36).substring(7)}`
|
||||
downloadIds.push(downloadId)
|
||||
for (const entry of selectedEntries) {
|
||||
const downloadId = `${groupId}_${Math.random().toString(36).substring(2, 10)}`
|
||||
|
||||
const downloadOptions: DownloadOptions = {
|
||||
url: entry.url,
|
||||
@@ -202,6 +313,13 @@ class DownloadEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
const createdAt = Date.now()
|
||||
downloadEntries.push({
|
||||
downloadId,
|
||||
entryId: entry.id,
|
||||
title: entry.title,
|
||||
url: entry.url,
|
||||
index: entry.index
|
||||
})
|
||||
|
||||
// Add to queue
|
||||
this.queue.add(downloadId, downloadOptions, {
|
||||
@@ -211,17 +329,35 @@ class DownloadEngine extends EventEmitter {
|
||||
type: options.type,
|
||||
status: 'pending',
|
||||
progress: { percent: 0 },
|
||||
createdAt
|
||||
createdAt,
|
||||
playlistId: groupId,
|
||||
playlistTitle: playlistInfo.title,
|
||||
playlistIndex: entry.index,
|
||||
playlistSize: selectionSize
|
||||
})
|
||||
|
||||
this.upsertHistoryEntry(downloadId, downloadOptions, {
|
||||
title: entry.title,
|
||||
status: 'pending',
|
||||
downloadedAt: createdAt
|
||||
downloadedAt: createdAt,
|
||||
downloadPath: settings.downloadPath,
|
||||
playlistId: groupId,
|
||||
playlistTitle: playlistInfo.title,
|
||||
playlistIndex: entry.index,
|
||||
playlistSize: selectionSize
|
||||
})
|
||||
}
|
||||
|
||||
return downloadIds
|
||||
return {
|
||||
groupId,
|
||||
playlistId: playlistInfo.id,
|
||||
playlistTitle: playlistInfo.title,
|
||||
type: options.type,
|
||||
totalCount: selectionSize,
|
||||
startIndex: selectedEntries[0]?.index ?? rangeStart + 1,
|
||||
endIndex: selectedEntries[selectedEntries.length - 1]?.index ?? rangeEnd + 1,
|
||||
entries: downloadEntries
|
||||
}
|
||||
}
|
||||
|
||||
startDownload(id: string, options: DownloadOptions): void {
|
||||
@@ -597,6 +733,18 @@ class DownloadEngine extends EventEmitter {
|
||||
if (updates.tags !== undefined) {
|
||||
historyUpdates.tags = updates.tags
|
||||
}
|
||||
if (updates.playlistId !== undefined) {
|
||||
historyUpdates.playlistId = updates.playlistId
|
||||
}
|
||||
if (updates.playlistTitle !== undefined) {
|
||||
historyUpdates.playlistTitle = updates.playlistTitle
|
||||
}
|
||||
if (updates.playlistIndex !== undefined) {
|
||||
historyUpdates.playlistIndex = updates.playlistIndex
|
||||
}
|
||||
if (updates.playlistSize !== undefined) {
|
||||
historyUpdates.playlistSize = updates.playlistSize
|
||||
}
|
||||
if (updates.status !== undefined) {
|
||||
historyUpdates.status = updates.status
|
||||
}
|
||||
@@ -641,7 +789,11 @@ class DownloadEngine extends EventEmitter {
|
||||
channel: completedDownload?.item.channel,
|
||||
uploader: completedDownload?.item.uploader,
|
||||
viewCount: completedDownload?.item.viewCount,
|
||||
tags: completedDownload?.item.tags
|
||||
tags: completedDownload?.item.tags,
|
||||
playlistId: completedDownload?.item.playlistId,
|
||||
playlistTitle: completedDownload?.item.playlistTitle,
|
||||
playlistIndex: completedDownload?.item.playlistIndex,
|
||||
playlistSize: completedDownload?.item.playlistSize
|
||||
})
|
||||
}
|
||||
|
||||
@@ -673,7 +825,11 @@ class DownloadEngine extends EventEmitter {
|
||||
viewCount: updates.viewCount,
|
||||
tags: updates.tags,
|
||||
// Download-specific format info
|
||||
selectedFormat: updates.selectedFormat
|
||||
selectedFormat: updates.selectedFormat,
|
||||
playlistId: updates.playlistId,
|
||||
playlistTitle: updates.playlistTitle,
|
||||
playlistIndex: updates.playlistIndex,
|
||||
playlistSize: updates.playlistSize
|
||||
}
|
||||
|
||||
const merged: DownloadHistoryItem = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeLanguageCode } from '@shared/languages'
|
||||
import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron'
|
||||
import appIcon from '../../resources/icon.png?asset'
|
||||
import trayIcon from '../../resources/tray-icon.png?asset'
|
||||
@@ -9,7 +10,7 @@ let tray: Tray | null = null
|
||||
* Get translated text based on current language setting
|
||||
*/
|
||||
function t(key: 'showHome' | 'quit'): string {
|
||||
const language = settingsManager.get('language') || 'en'
|
||||
const language = normalizeLanguageCode(settingsManager.get('language'))
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
@@ -17,12 +18,12 @@ function t(key: 'showHome' | 'quit'): string {
|
||||
quit: 'Quit'
|
||||
},
|
||||
zh: {
|
||||
showHome: '打开首页',
|
||||
quit: '关闭应用'
|
||||
showHome: '显示主页',
|
||||
quit: '退出应用'
|
||||
}
|
||||
}
|
||||
|
||||
const displayLang = language.startsWith('en') ? 'en' : 'zh'
|
||||
const displayLang = language.startsWith('zh') ? 'zh' : 'en'
|
||||
return translations[displayLang][key]
|
||||
}
|
||||
|
||||
|
||||
16
src/main/utils/dock.ts
Normal file
16
src/main/utils/dock.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
/**
|
||||
* Apply Dock visibility preference on macOS.
|
||||
*/
|
||||
export function applyDockVisibility(hideDockIcon: boolean): void {
|
||||
if (process.platform !== 'darwin' || !app.dock) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hideDockIcon) {
|
||||
app.dock.hide()
|
||||
} else {
|
||||
app.dock.show()
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,27 @@ import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||
import { Sidebar } from '@renderer/components/ui/sidebar'
|
||||
import { Toaster } from '@renderer/components/ui/sonner'
|
||||
import { TitleBar } from '@renderer/components/ui/title-bar'
|
||||
import { useAtom } from 'jotai'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ipcServices } from './lib/ipc'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { ipcEvents, ipcServices } from './lib/ipc'
|
||||
import { About } from './pages/About'
|
||||
import { Home } from './pages/Home'
|
||||
import { Settings } from './pages/Settings'
|
||||
import { SupportedSites } from './pages/SupportedSites'
|
||||
import { settingsAtom } from './store/settings'
|
||||
|
||||
type Page = 'home' | 'settings' | 'about' | 'sites'
|
||||
|
||||
function AppContent() {
|
||||
const [currentPage, setCurrentPage] = useState<Page>('home')
|
||||
const [platform, setPlatform] = useState<string>('')
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const { t } = useTranslation()
|
||||
const autoUpdateEnabled = settings.autoUpdate
|
||||
const updateDownloadInProgressRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Get platform info to determine if we should show title bar
|
||||
@@ -31,6 +39,130 @@ function AppContent() {
|
||||
getPlatform()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window?.api) {
|
||||
return
|
||||
}
|
||||
|
||||
const showRestartPrompt = () => {
|
||||
toast.info(t('about.notifications.restartToUpdate'), {
|
||||
action: {
|
||||
label: t('about.notifications.restartNowAction'),
|
||||
onClick: () => {
|
||||
void ipcServices.update.quitAndInstall()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetDownloadState = () => {
|
||||
if (updateDownloadInProgressRef.current) {
|
||||
updateDownloadInProgressRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
const startUpdateDownload = async () => {
|
||||
if (updateDownloadInProgressRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
updateDownloadInProgressRef.current = true
|
||||
toast.info(t('about.notifications.downloadStarted'))
|
||||
|
||||
try {
|
||||
const result = await ipcServices.update.downloadUpdate()
|
||||
if (!result.success) {
|
||||
throw new Error(result.error ?? 'Unknown error')
|
||||
}
|
||||
} catch (error) {
|
||||
updateDownloadInProgressRef.current = false
|
||||
const message =
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: t('about.notifications.unknownErrorFallback')
|
||||
toast.error(t('about.notifications.updateError', { error: message }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateAvailable = (rawInfo: unknown) => {
|
||||
const info = (rawInfo ?? {}) as { version?: string }
|
||||
const versionLabel = info.version ?? ''
|
||||
toast.success(t('about.notifications.updateAvailable', { version: versionLabel }))
|
||||
|
||||
if (autoUpdateEnabled) {
|
||||
void startUpdateDownload()
|
||||
} else {
|
||||
toast(t('about.notifications.downloadUpdate', { version: versionLabel }), {
|
||||
action: {
|
||||
label: t('about.notifications.manualDownloadAction'),
|
||||
onClick: () => {
|
||||
void startUpdateDownload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateDownloaded = (rawInfo: unknown) => {
|
||||
const info = (rawInfo ?? {}) as { version?: string }
|
||||
resetDownloadState()
|
||||
|
||||
const versionLabel = info?.version ?? ''
|
||||
const downloadedMessage = versionLabel
|
||||
? t('about.notifications.updateDownloadedVersion', { version: versionLabel })
|
||||
: t('about.notifications.updateDownloaded')
|
||||
toast.success(downloadedMessage)
|
||||
|
||||
showRestartPrompt()
|
||||
}
|
||||
|
||||
const handleUpdateError = (rawMessage: unknown) => {
|
||||
const message = typeof rawMessage === 'string' ? rawMessage : ''
|
||||
resetDownloadState()
|
||||
|
||||
const errorMessage = message || t('about.notifications.unknownErrorFallback')
|
||||
toast.error(t('about.notifications.updateError', { error: errorMessage }))
|
||||
}
|
||||
|
||||
const handleDownloadProgress = (rawProgress: unknown) => {
|
||||
const progress = (rawProgress ?? {}) as { percent?: number }
|
||||
if (typeof progress?.percent === 'number') {
|
||||
console.info('Update download progress:', progress.percent.toFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateNotification = (rawPayload: unknown) => {
|
||||
const payload = (rawPayload ?? {}) as { body?: string; version?: string }
|
||||
const versionLabel = payload.version ?? ''
|
||||
const downloadedMessage = versionLabel
|
||||
? t('about.notifications.updateDownloadedVersion', { version: versionLabel })
|
||||
: t('about.notifications.updateDownloaded')
|
||||
|
||||
toast.info(payload?.body ?? downloadedMessage, {
|
||||
action: {
|
||||
label: t('about.notifications.restartNowAction'),
|
||||
onClick: () => {
|
||||
void ipcServices.update.quitAndInstall()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ipcEvents.on('update:available', handleUpdateAvailable)
|
||||
ipcEvents.on('update:downloaded', handleUpdateDownloaded)
|
||||
ipcEvents.on('update:error', handleUpdateError)
|
||||
ipcEvents.on('update:download-progress', handleDownloadProgress)
|
||||
ipcEvents.on('update:show-notification', handleUpdateNotification)
|
||||
|
||||
return () => {
|
||||
ipcEvents.removeListener('update:available', handleUpdateAvailable)
|
||||
ipcEvents.removeListener('update:downloaded', handleUpdateDownloaded)
|
||||
ipcEvents.removeListener('update:error', handleUpdateError)
|
||||
ipcEvents.removeListener('update:download-progress', handleDownloadProgress)
|
||||
ipcEvents.removeListener('update:show-notification', handleUpdateNotification)
|
||||
}
|
||||
}, [autoUpdateEnabled, t])
|
||||
|
||||
const renderPage = () => {
|
||||
switch (currentPage) {
|
||||
case 'home':
|
||||
@@ -53,8 +185,8 @@ function AppContent() {
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex flex-col flex-1 min-h-0 overflow-hidden bg-background">
|
||||
{/* Custom Title Bar - Hide on macOS */}
|
||||
{platform !== 'darwin' && <TitleBar />}
|
||||
{/* Custom Title Bar */}
|
||||
<TitleBar platform={platform} />
|
||||
|
||||
<ScrollArea
|
||||
className="flex-1 w-full overflow-y-auto overflow-x-hidden"
|
||||
|
||||
@@ -214,6 +214,28 @@ export function DownloadItem({ download }: DownloadItemProps) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{download.playlistId && (
|
||||
<div className="flex w-full flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-blue-500/10 text-blue-700 dark:text-blue-200"
|
||||
>
|
||||
{t('playlist.badgeLabel')}
|
||||
</Badge>
|
||||
<span className="truncate">
|
||||
{download.playlistTitle || t('playlist.untitled')}
|
||||
{download.playlistIndex !== undefined &&
|
||||
download.playlistSize !== undefined && (
|
||||
<span className="ml-1 text-muted-foreground/80">
|
||||
{t('playlist.positionLabel', {
|
||||
index: download.playlistIndex,
|
||||
total: download.playlistSize
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full min-w-0 flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
{timestamp ? (
|
||||
<span className="truncate max-w-[120px]">{formatDate(timestamp)}</span>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { DownloadRecord } from '../../store/downloads'
|
||||
import { DownloadItem } from './DownloadItem'
|
||||
|
||||
interface PlaylistDownloadGroupProps {
|
||||
groupId: string
|
||||
title: string
|
||||
records: DownloadRecord[]
|
||||
totalCount: number
|
||||
}
|
||||
|
||||
export function PlaylistDownloadGroup({
|
||||
groupId,
|
||||
title,
|
||||
records,
|
||||
totalCount
|
||||
}: PlaylistDownloadGroupProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const completedCount = records.filter((record) => record.status === 'completed').length
|
||||
const errorCount = records.filter((record) => record.status === 'error').length
|
||||
const activeCount = records.filter((record) =>
|
||||
['downloading', 'processing', 'pending'].includes(record.status)
|
||||
).length
|
||||
|
||||
const displayTitle = title || t('playlist.untitled')
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-md border border-border/60 bg-muted/20 p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold text-foreground">{displayTitle}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('playlist.groupSummary', { completed: completedCount, total: totalCount })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
|
||||
{activeCount > 0 && <span>{t('playlist.groupActive', { count: activeCount })}</span>}
|
||||
{errorCount > 0 && (
|
||||
<span className="text-destructive">
|
||||
{t('playlist.groupErrors', { count: errorCount })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{records.map((record) => (
|
||||
<div
|
||||
key={`${groupId}:${record.entryType}:${record.id}`}
|
||||
className="border-l border-border/50 pl-3"
|
||||
>
|
||||
<DownloadItem download={record} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { History as HistoryIcon } from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHistorySync } from '../../hooks/use-history-sync'
|
||||
import type { DownloadRecord } from '../../store/downloads'
|
||||
import { clearCompletedAtom, downloadStatsAtom, downloadsArrayAtom } from '../../store/downloads'
|
||||
import { DownloadItem } from './DownloadItem'
|
||||
import { PlaylistDownloadGroup } from './PlaylistDownloadGroup'
|
||||
|
||||
type StatusFilter = 'all' | 'active' | 'completed' | 'error'
|
||||
|
||||
@@ -46,6 +49,56 @@ export function UnifiedDownloadHistory() {
|
||||
{ key: 'error', label: t('download.error'), count: downloadStats.error }
|
||||
]
|
||||
|
||||
const groupedView = useMemo(() => {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{ id: string; title: string; totalCount: number; records: DownloadRecord[] }
|
||||
>()
|
||||
const order: Array<{ type: 'group'; id: string } | { type: 'single'; record: DownloadRecord }> =
|
||||
[]
|
||||
|
||||
for (const record of filteredRecords) {
|
||||
if (record.playlistId) {
|
||||
let group = groups.get(record.playlistId)
|
||||
if (!group) {
|
||||
group = {
|
||||
id: record.playlistId,
|
||||
title: record.playlistTitle || record.title,
|
||||
totalCount: record.playlistSize || 0,
|
||||
records: []
|
||||
}
|
||||
groups.set(record.playlistId, group)
|
||||
order.push({ type: 'group', id: record.playlistId })
|
||||
}
|
||||
group.records.push(record)
|
||||
if (!group.title && record.playlistTitle) {
|
||||
group.title = record.playlistTitle
|
||||
}
|
||||
if (!group.totalCount && record.playlistSize) {
|
||||
group.totalCount = record.playlistSize
|
||||
}
|
||||
} else {
|
||||
order.push({ type: 'single', record })
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of groups.values()) {
|
||||
group.records.sort((a, b) => {
|
||||
const aIndex = a.playlistIndex ?? Number.MAX_SAFE_INTEGER
|
||||
const bIndex = b.playlistIndex ?? Number.MAX_SAFE_INTEGER
|
||||
if (aIndex !== bIndex) {
|
||||
return aIndex - bIndex
|
||||
}
|
||||
return b.createdAt - a.createdAt
|
||||
})
|
||||
if (!group.totalCount) {
|
||||
group.totalCount = group.records.length
|
||||
}
|
||||
}
|
||||
|
||||
return { order, groups }
|
||||
}, [filteredRecords])
|
||||
|
||||
const hasCompletedActive = allRecords.some(
|
||||
(item) => item.entryType === 'active' && item.status === 'completed'
|
||||
)
|
||||
@@ -89,7 +142,14 @@ export function UnifiedDownloadHistory() {
|
||||
onClick={() => setStatusFilter(filter.key)}
|
||||
>
|
||||
<span>{filter.label}</span>
|
||||
<span className="ml-1 text-xs opacity-70">({filter.count})</span>
|
||||
<span
|
||||
className={cn(
|
||||
'ml-1 min-w-5 rounded-full px-1 text-xs font-medium text-neutral-900',
|
||||
isActive ? ' bg-neutral-100' : ' bg-neutral-200'
|
||||
)}
|
||||
>
|
||||
{filter.count}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
@@ -102,10 +162,32 @@ export function UnifiedDownloadHistory() {
|
||||
<p className="text-sm font-medium">{t('download.noItems')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 sm:space-y-4 overflow-hidden w-full">
|
||||
{filteredRecords.map((record) => (
|
||||
<DownloadItem key={`${record.entryType}:${record.id}`} download={record} />
|
||||
))}
|
||||
<div className="space-y-3 sm:space-y-4 overflow-hidden w-full">
|
||||
{groupedView.order.map((item) => {
|
||||
if (item.type === 'single') {
|
||||
return (
|
||||
<DownloadItem
|
||||
key={`${item.record.entryType}:${item.record.id}`}
|
||||
download={item.record}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const group = groupedView.groups.get(item.id)
|
||||
if (!group) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<PlaylistDownloadGroup
|
||||
key={`group:${group.id}`}
|
||||
groupId={group.id}
|
||||
title={group.title}
|
||||
totalCount={group.totalCount}
|
||||
records={group.records}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
94
src/renderer/src/components/playlist/PlaylistPreviewCard.tsx
Normal file
94
src/renderer/src/components/playlist/PlaylistPreviewCard.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@renderer/components/ui/card'
|
||||
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||
import type { PlaylistEntry, PlaylistInfo } from '@shared/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface PlaylistPreviewCardProps {
|
||||
playlist: PlaylistInfo
|
||||
entries: PlaylistEntry[]
|
||||
onClear?: () => void
|
||||
}
|
||||
|
||||
export function PlaylistPreviewCard({ playlist, entries, onClear }: PlaylistPreviewCardProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const totalCount = playlist.entryCount
|
||||
const selectedCount = entries.length
|
||||
const firstIndex = entries[0]?.index ?? null
|
||||
const lastIndex = entries[entries.length - 1]?.index ?? firstIndex ?? null
|
||||
|
||||
return (
|
||||
<Card className="border border-border/60 bg-background/80 shadow-sm overflow-hidden">
|
||||
<CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<CardTitle
|
||||
className="truncate text-base font-semibold sm:text-lg wrap-break-word"
|
||||
title={playlist.title}
|
||||
>
|
||||
{playlist.title || t('playlist.untitled')}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs text-muted-foreground sm:text-sm">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-3 text-xs text-muted-foreground sm:text-sm">
|
||||
<span className="truncate">{t('playlist.totalVideos', { count: totalCount })}</span>
|
||||
{firstIndex !== null && lastIndex !== null ? (
|
||||
<span className="truncate">
|
||||
{t('playlist.selectedRange', {
|
||||
start: firstIndex,
|
||||
end: lastIndex
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<span className="truncate">{t('playlist.noRangeSelected')}</span>
|
||||
)}
|
||||
<span className="truncate">
|
||||
{t('playlist.showingCount', { count: selectedCount })}
|
||||
</span>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</div>
|
||||
{onClear && (
|
||||
<Button variant="ghost" size="sm" onClick={onClear} className="shrink-0">
|
||||
{t('playlist.clearPreview')}
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="rounded-md border border-border/60 bg-muted/20">
|
||||
<ScrollArea className="max-h-64 w-full pr-1 overflow-y-auto overflow-x-hidden">
|
||||
<ol className="w-full min-w-0 divide-y divide-border/60 text-sm leading-snug">
|
||||
{entries.length === 0 ? (
|
||||
<li className="px-4 py-6 text-center text-xs text-muted-foreground">
|
||||
{t('playlist.noEntriesInRange')}
|
||||
</li>
|
||||
) : (
|
||||
entries.map((entry) => (
|
||||
<li
|
||||
key={`${entry.index}-${entry.id}`}
|
||||
className="flex items-start gap-3 px-4 py-2 min-w-0 w-full max-w-full overflow-hidden"
|
||||
>
|
||||
<span className="w-12 shrink-0 text-xs font-semibold text-muted-foreground text-center">
|
||||
#{entry.index}
|
||||
</span>
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-sm overflow-hidden wrap-break-word"
|
||||
title={entry.title}
|
||||
>
|
||||
{entry.title}
|
||||
</span>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ol>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@renderer/components/ui/dropdown-menu'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'
|
||||
import { saveSettingAtom } from '@renderer/store/settings'
|
||||
import { type LanguageCode, languageList, normalizeLanguageCode } from '@shared/languages'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
@@ -40,10 +41,7 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const saveSetting = useSetAtom(saveSettingAtom)
|
||||
|
||||
const languageOptions = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '中文' }
|
||||
]
|
||||
const languageOptions = languageList
|
||||
|
||||
const navigationItems: NavigationItem[] = [
|
||||
{
|
||||
@@ -83,11 +81,11 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
|
||||
}
|
||||
]
|
||||
|
||||
const activeLanguageCode = (i18n.language ?? 'en').split('-')[0]
|
||||
const activeLanguageCode = normalizeLanguageCode(i18n.language)
|
||||
const currentLanguage =
|
||||
languageOptions.find((option) => option.value === activeLanguageCode) ?? languageOptions[0]
|
||||
|
||||
const handleLanguageChange = async (value: string) => {
|
||||
const handleLanguageChange = async (value: LanguageCode) => {
|
||||
if (activeLanguageCode === value) {
|
||||
return
|
||||
}
|
||||
@@ -167,9 +165,13 @@ export function Sidebar({ currentPage, onPageChange }: SidebarProps) {
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
onClick={() => void handleLanguageChange(option.value)}
|
||||
className={isActive ? 'font-semibold' : undefined}
|
||||
className={isActive ? 'font-semibold bg-muted focus:bg-muted' : undefined}
|
||||
aria-current={isActive}
|
||||
>
|
||||
{option.label}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`${option.flag} rounded-xs text-base`} aria-hidden="true" />
|
||||
<span lang={option.hreflang}>{option.name}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -7,11 +7,15 @@ import IconFluentSubtract20Regular from '~icons/fluent/subtract-20-regular'
|
||||
import { ipcEvents, ipcServices } from '../../lib/ipc'
|
||||
import '../../assets/title-bar.css'
|
||||
|
||||
export function TitleBar() {
|
||||
interface TitleBarProps {
|
||||
platform?: string
|
||||
}
|
||||
|
||||
export function TitleBar({ platform }: TitleBarProps) {
|
||||
const [isMaximized, setIsMaximized] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// 监听窗口最大化状态变化
|
||||
// Listen for window maximize state changes
|
||||
const handleMaximized = () => {
|
||||
setIsMaximized(true)
|
||||
}
|
||||
@@ -41,8 +45,17 @@ export function TitleBar() {
|
||||
ipcServices.window.close()
|
||||
}
|
||||
|
||||
const isMac = platform === 'darwin'
|
||||
const containerClass = `flex drag-region bg-background select-none ${
|
||||
isMac ? 'h-10 items-center px-4' : 'justify-end pt-4 px-5'
|
||||
}`
|
||||
|
||||
if (isMac) {
|
||||
return <div className={containerClass} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex drag-region justify-end bg-background pt-4 px-5 select-none">
|
||||
<div className={containerClass}>
|
||||
{/* Window controls */}
|
||||
<div className="flex items-center gap-1 no-drag">
|
||||
<Button
|
||||
|
||||
@@ -1,98 +1,24 @@
|
||||
export interface PopularSite {
|
||||
id: string
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const popularSites: PopularSite[] = [
|
||||
{
|
||||
id: 'youtube',
|
||||
label: 'YouTube',
|
||||
description: 'Long-form and livestream video from creators worldwide.'
|
||||
},
|
||||
{
|
||||
id: 'youtubemusic',
|
||||
label: 'YouTube Music',
|
||||
description: 'Official music videos, albums, and live performances.'
|
||||
},
|
||||
{
|
||||
id: 'tiktok',
|
||||
label: 'TikTok',
|
||||
description: 'Short-form mobile videos, effects, and live streams.'
|
||||
},
|
||||
{
|
||||
id: 'facebook',
|
||||
label: 'Facebook',
|
||||
description: 'Feed, Watch, and Reels videos from public pages.'
|
||||
},
|
||||
{
|
||||
id: 'instagram',
|
||||
label: 'Instagram',
|
||||
description: 'Feed, Stories, Reels, and Highlights content.'
|
||||
},
|
||||
{
|
||||
id: 'twitter',
|
||||
label: 'X (Twitter)',
|
||||
description: 'Timeline posts, Spaces recordings, and broadcasts.'
|
||||
},
|
||||
{
|
||||
id: 'soundcloud',
|
||||
label: 'SoundCloud',
|
||||
description: 'Music tracks, playlists, and DJ sets.'
|
||||
},
|
||||
{
|
||||
id: 'reddit',
|
||||
label: 'Reddit',
|
||||
description: 'Embedded clips and hosted videos from communities.'
|
||||
},
|
||||
{
|
||||
id: 'vimeo',
|
||||
label: 'Vimeo',
|
||||
description: 'High-quality creator and business video hosting.'
|
||||
},
|
||||
{
|
||||
id: 'dailymotion',
|
||||
label: 'Dailymotion',
|
||||
description: 'Global news, sports, and entertainment clips.'
|
||||
},
|
||||
{
|
||||
id: 'twitch',
|
||||
label: 'Twitch',
|
||||
description: 'Gaming, music, and IRL live streams and VODs.'
|
||||
},
|
||||
{
|
||||
id: 'linkedin',
|
||||
label: 'LinkedIn',
|
||||
description: 'Professional talks, webinars, and learning videos.'
|
||||
},
|
||||
{
|
||||
id: 'pinterest',
|
||||
label: 'Pinterest',
|
||||
description: 'Idea pins, how-to reels, and lifestyle inspiration videos.'
|
||||
},
|
||||
{
|
||||
id: 'tumblr',
|
||||
label: 'Tumblr',
|
||||
description: 'Creative short-form media and fan edits.'
|
||||
},
|
||||
{
|
||||
id: 'mixcloud',
|
||||
label: 'Mixcloud',
|
||||
description: 'DJ mixes, radio shows, and long-form audio.'
|
||||
},
|
||||
{
|
||||
id: 'niconico',
|
||||
label: 'Niconico',
|
||||
description: 'Japanese animation, music, and live broadcast archive.'
|
||||
},
|
||||
{
|
||||
id: 'kick',
|
||||
label: 'Kick',
|
||||
description: 'Creator live streams and replays on the Kick platform.'
|
||||
},
|
||||
{
|
||||
id: 'bandcamp',
|
||||
label: 'Bandcamp',
|
||||
description: 'Independent artist albums and community releases.'
|
||||
}
|
||||
{ id: 'youtube' },
|
||||
{ id: 'youtubemusic' },
|
||||
{ id: 'tiktok' },
|
||||
{ id: 'facebook' },
|
||||
{ id: 'instagram' },
|
||||
{ id: 'twitter' },
|
||||
{ id: 'soundcloud' },
|
||||
{ id: 'reddit' },
|
||||
{ id: 'vimeo' },
|
||||
{ id: 'dailymotion' },
|
||||
{ id: 'twitch' },
|
||||
{ id: 'linkedin' },
|
||||
{ id: 'pinterest' },
|
||||
{ id: 'tumblr' },
|
||||
{ id: 'mixcloud' },
|
||||
{ id: 'niconico' },
|
||||
{ id: 'kick' },
|
||||
{ id: 'bandcamp' }
|
||||
]
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
import { defaultLanguageCode, supportedLanguageCodes } from '@shared/languages'
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import en from './locales/en.json'
|
||||
import zh from './locales/zh.json'
|
||||
|
||||
type TranslationDictionary = typeof en
|
||||
|
||||
const localeModules = import.meta.glob<{ default: TranslationDictionary }>('./locales/*.json', {
|
||||
eager: true
|
||||
})
|
||||
|
||||
const translations = Object.fromEntries(
|
||||
Object.entries(localeModules).map(([path, module]) => {
|
||||
const code = path.replace('./locales/', '').replace('.json', '')
|
||||
return [code, module.default]
|
||||
})
|
||||
) as Record<string, TranslationDictionary>
|
||||
|
||||
const resources = Object.fromEntries(
|
||||
supportedLanguageCodes.map((code) => [
|
||||
code,
|
||||
{
|
||||
translation: translations[code] ?? en
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
zh: { translation: zh }
|
||||
},
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
resources,
|
||||
lng: defaultLanguageCode,
|
||||
fallbackLng: defaultLanguageCode,
|
||||
supportedLngs: supportedLanguageCodes,
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
|
||||
@@ -14,18 +14,28 @@
|
||||
"betaProgramDescription": "Receive early builds and upcoming features before everyone else.",
|
||||
"betaProgramTitle": "Preview channel",
|
||||
"description": "VidBee is a free, open-source downloader built with Electron and powered by yt-dlp.",
|
||||
"followAuthorActions": {
|
||||
"follow": "Follow @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "Stay updated with the latest VidBee news and updates.",
|
||||
"followAuthorSupport": "Follow the developer on X (Twitter) to get the latest updates and news about VidBee.",
|
||||
"followAuthorTitle": "Follow the Developer",
|
||||
"here": "here",
|
||||
"homepage": "Homepage",
|
||||
"notifications": {
|
||||
"checkingUpdates": "Looking for updates...",
|
||||
"updateAvailable": "Update available: {{version}}",
|
||||
"noUpdatesAvailable": "You're using the latest version",
|
||||
"updateError": "Failed to check for updates: {{error}}",
|
||||
"downloadUpdate": "Download and install update {{version}}?",
|
||||
"downloadStarted": "Download started...",
|
||||
"downloadError": "Failed to download update",
|
||||
"downloadStarted": "Download started...",
|
||||
"downloadUpdate": "Download and install update {{version}}?",
|
||||
"manualDownloadAction": "Download now",
|
||||
"noUpdatesAvailable": "You're using the latest version",
|
||||
"restartToUpdate": "Restart now to install update?",
|
||||
"restartNowAction": "Restart now",
|
||||
"updateAvailable": "Update available: {{version}}",
|
||||
"updateDownloaded": "Update downloaded, restart to install",
|
||||
"restartToUpdate": "Restart now to install update?"
|
||||
"updateDownloadedVersion": "Update {{version}} downloaded, restart to install",
|
||||
"updateError": "Failed to check for updates: {{error}}",
|
||||
"unknownErrorFallback": "Unknown error"
|
||||
},
|
||||
"preferencesDescription": "Tune update settings without leaving this page.",
|
||||
"preferencesTitle": "Quick Toggles",
|
||||
@@ -45,25 +55,24 @@
|
||||
},
|
||||
"resourcesDescription": "Useful links to learn more about VidBee and stay connected.",
|
||||
"resourcesTitle": "Resources",
|
||||
"shareActions": {
|
||||
"copy": "Copy link",
|
||||
"facebook": "Share on Facebook",
|
||||
"twitter": "Share on X (Twitter)"
|
||||
},
|
||||
"shareDescription": "Share VidBee with your community in one click.",
|
||||
"shareSupport": "Recommend VidBee to your friends to support our growth and updates.",
|
||||
"shareTitle": "Spread the word",
|
||||
"shareDescription": "Share VidBee with your community in one click.",
|
||||
"followAuthorActions": {
|
||||
"follow": "Follow @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "Stay updated with the latest VidBee news and updates.",
|
||||
"followAuthorSupport": "Follow the developer on X (Twitter) to get the latest updates and news about VidBee.",
|
||||
"followAuthorTitle": "Follow the Developer",
|
||||
"shareActions": {
|
||||
"twitter": "Share on X (Twitter)",
|
||||
"facebook": "Share on Facebook",
|
||||
"copy": "Copy link"
|
||||
},
|
||||
"sourceCode": "Source Code is available",
|
||||
"tagline": "An AI-friendly download helper for every creator",
|
||||
"title": "About",
|
||||
"version": "Version",
|
||||
"versionLabel": "v{{version}}"
|
||||
"versionLabel": "v{{version}}",
|
||||
"latestVersionBadge": "Latest: v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "New version available",
|
||||
"uptodate": "You're up to date",
|
||||
"error": "Unable to fetch the latest version"
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "Close app when download finishes",
|
||||
@@ -97,7 +106,7 @@
|
||||
"worst": "Worst"
|
||||
},
|
||||
"download": {
|
||||
"active": "active",
|
||||
"active": "Active",
|
||||
"all": "All",
|
||||
"audio": "Audio",
|
||||
"back": "Back",
|
||||
@@ -166,8 +175,8 @@
|
||||
"clearCancelled": "Clear Cancelled",
|
||||
"clearCompleted": "Clear Completed",
|
||||
"clearErrors": "Clear Errors",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"copyUrl": "Copy URL",
|
||||
"openInBrowser": "Click to open in browser",
|
||||
"date": "Date",
|
||||
"description": "View and manage your download history",
|
||||
"duration": "Duration",
|
||||
@@ -180,11 +189,11 @@
|
||||
},
|
||||
"noHistory": "No download history yet",
|
||||
"noHistoryDescription": "Your completed downloads will appear here",
|
||||
"openDownloadFolder": "Open Download Folder",
|
||||
"openFile": "Open File",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"openFileLocation": "Open File Location",
|
||||
"openFolder": "Open Folder",
|
||||
"openDownloadFolder": "Open Download Folder",
|
||||
"openInBrowser": "Click to open in browser",
|
||||
"removeItem": "Remove Item",
|
||||
"stats": {
|
||||
"cancelled": "Cancelled",
|
||||
@@ -209,7 +218,6 @@
|
||||
},
|
||||
"notifications": {
|
||||
"copyFailed": "Failed to copy to clipboard",
|
||||
"videoCopied": "Video copied to clipboard",
|
||||
"downloadCompleted": "Download completed",
|
||||
"downloadFailed": "Download failed",
|
||||
"downloadStarted": "Download started",
|
||||
@@ -218,9 +226,12 @@
|
||||
"openFolderFailed": "Failed to open folder",
|
||||
"removeFailed": "Failed to remove item",
|
||||
"settingsSaved": "Settings saved",
|
||||
"urlCopied": "URL copied to clipboard"
|
||||
"urlCopied": "URL copied to clipboard",
|
||||
"videoCopied": "Video copied to clipboard"
|
||||
},
|
||||
"playlist": {
|
||||
"badgeLabel": "Playlist",
|
||||
"clearPreview": "Clear preview",
|
||||
"comingSoon": "Playlist download feature coming soon!",
|
||||
"completed": "Playlist downloaded",
|
||||
"description": "Download all videos from a YouTube playlist or channel",
|
||||
@@ -235,12 +246,27 @@
|
||||
"filenameFormat": "Filename format for playlists",
|
||||
"folderFormat": "Folder name format for playlists",
|
||||
"foundVideos": "Found {{count}} videos in playlist",
|
||||
"groupActive": "{{count}} active",
|
||||
"groupErrors": "{{count}} failed",
|
||||
"groupSummary": "{{completed}} / {{total}} completed",
|
||||
"linkLabel": "Playlist URL",
|
||||
"noEntries": "No videos were found in this playlist",
|
||||
"noEntriesInRange": "No videos in the selected range",
|
||||
"noRangeSelected": "No end set - full playlist selected",
|
||||
"playlistUrlDescription": "Download all videos from a playlist in bulk",
|
||||
"positionLabel": "Item {{index}} of {{total}}",
|
||||
"previewButton": "Preview playlist",
|
||||
"previewFailed": "Failed to preview playlist",
|
||||
"previewSummary": "Preview playlist items before downloading.",
|
||||
"previewRequired": "Preview the playlist before downloading.",
|
||||
"range": "Range (Optional)",
|
||||
"resetToDefault": "Reset to default",
|
||||
"selectedRange": "Range: {{start}}-{{end}}",
|
||||
"showingCount": "Showing {{count}} videos",
|
||||
"startIndex": "Start (1)",
|
||||
"title": "Download Playlist"
|
||||
"title": "Download Playlist",
|
||||
"totalVideos": "Total videos: {{count}}",
|
||||
"untitled": "Untitled playlist"
|
||||
},
|
||||
"settings": {
|
||||
"aboutTab": "About",
|
||||
@@ -248,23 +274,44 @@
|
||||
"app": "App Settings",
|
||||
"audio": "Audio Preferences",
|
||||
"browserForCookies": "Select browser to use cookies from",
|
||||
"browserForCookiesDescription": "Browser to extract cookies from for authentication",
|
||||
"cookiesFile": "Cookies file",
|
||||
"cookiesFileDescription": "Netscape formatted cookies file to load for authentication",
|
||||
"clearCookiesFile": "Clear",
|
||||
"cookiesHelpTitle": "Using cookies",
|
||||
"cookiesHelpBrowser": "Pick your browser above to reuse its signed-in session automatically.",
|
||||
"cookiesHelpFile": "Export a Netscape cookies file (see the yt-dlp FAQ) and select it here when needed.",
|
||||
"cookiesHelpFaq": "Open yt-dlp cookies FAQ",
|
||||
"openLinkError": "Failed to open link",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"configFile": "Use configuration file",
|
||||
"configFileDescription": "Custom configuration file for yt-dlp",
|
||||
"dark": "Dark",
|
||||
"description": "Configure your download preferences and application settings",
|
||||
"directorySelectError": "Failed to select directory",
|
||||
"downloadPath": "Download location",
|
||||
"downloadPathDescription": "Choose where to save downloaded files",
|
||||
"fileSelectError": "Failed to select file",
|
||||
"general": "General",
|
||||
"language": "Language",
|
||||
"languageOptions": {
|
||||
"chinese": "Chinese (Simplified)",
|
||||
"english": "English"
|
||||
},
|
||||
"light": "Light",
|
||||
"hideDockIcon": "Hide Dock icon",
|
||||
"hideDockIconDescription": "Remove VidBee from the macOS Dock. Use the menu bar or tray icon to reopen the app.",
|
||||
"maxConcurrentDownloads": "Maximum number of active downloads",
|
||||
"maxConcurrentDownloadsDescription": "Maximum number of simultaneous downloads",
|
||||
"none": "None",
|
||||
"oneClickDownload": "One-Click Download",
|
||||
"oneClickDownloadDescription": "Enable one-click download with default settings",
|
||||
"oneClickDownloadType": "Default download type",
|
||||
"oneClickDownloadTypeDescription": "Choose the default download type for one-click downloads. Quality uses the preset below.",
|
||||
"oneClickQuality": "Preferred quality",
|
||||
"oneClickQualityDescription": "Select the quality preset used for one-click downloads",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "Auto",
|
||||
"bad": "Bad",
|
||||
@@ -274,11 +321,15 @@
|
||||
"worst": "Worst"
|
||||
},
|
||||
"proxy": "Proxy",
|
||||
"proxyDescription": "Proxy server for network requests",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "Select config file",
|
||||
"selectPath": "Select",
|
||||
"showMoreFormats": "Show more format options",
|
||||
"showMoreFormatsDescription": "Display additional format options in the interface",
|
||||
"system": "System",
|
||||
"theme": "Theme",
|
||||
"themeDescription": "Choose a light, dark, or system theme for VidBee",
|
||||
"title": "Settings",
|
||||
"tray": {
|
||||
"quit": "Quit",
|
||||
@@ -294,6 +345,80 @@
|
||||
"pageDescription": "VidBee uses yt-dlp under the hood to reach hundreds of sources.",
|
||||
"pageIntro": "Here are the mainstream services people download from most frequently.",
|
||||
"pageTitle": "Supported Sites",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "Independent artist albums and community releases.",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "Global news, sports, and entertainment clips.",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "Feed, Watch, and Reels videos from public pages.",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "Feed, Stories, Reels, and Highlights content.",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Creator live streams and replays on the Kick platform.",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "Professional talks, webinars, and learning videos.",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "DJ mixes, radio shows, and long-form audio.",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "Japanese animation, music, and live broadcast archive.",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "Idea pins, how-to reels, and lifestyle inspiration videos.",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "Embedded clips and hosted videos from communities.",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "Music tracks, playlists, and DJ sets.",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "Short-form mobile videos, effects, and live streams.",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "Creative short-form media and fan edits.",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "Gaming, music, and IRL live streams and VODs.",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "Timeline posts, Spaces recordings, and broadcasts.",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "High-quality creator and business video hosting.",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "Long-form and livestream video from creators worldwide.",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "Official music videos, albums, and live performances.",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "Main platforms",
|
||||
"viewAll": "View all supported sites"
|
||||
}
|
||||
|
||||
394
src/renderer/src/locales/fr.json
Normal file
394
src/renderer/src/locales/fr.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"about": {
|
||||
"actions": {
|
||||
"checkUpdates": "Vérifier les mises à jour",
|
||||
"email": "Email",
|
||||
"feedback": "Commentaires",
|
||||
"openRepo": "Ouvrir le dépôt GitHub",
|
||||
"view": "Voir",
|
||||
"visit": "Visiter"
|
||||
},
|
||||
"appName": "VidBee",
|
||||
"autoUpdateDescription": "Télécharger et installer automatiquement les nouvelles versions en arrière-plan.",
|
||||
"autoUpdateTitle": "Mises à jour automatiques",
|
||||
"betaProgramDescription": "Recevez les versions préliminaires et les prochaines fonctionnalités avant tout le monde.",
|
||||
"betaProgramTitle": "Canal de prévisualisation",
|
||||
"description": "VidBee est un téléchargeur gratuit et open-source construit avec Electron et alimenté par yt-dlp.",
|
||||
"followAuthorActions": {
|
||||
"follow": "Suivre @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "Restez à jour avec les dernières nouvelles et mises à jour de VidBee.",
|
||||
"followAuthorSupport": "Suivez le développeur sur X (Twitter) pour obtenir les dernières mises à jour et nouvelles sur VidBee.",
|
||||
"followAuthorTitle": "Suivre le Développeur",
|
||||
"here": "ici",
|
||||
"homepage": "Page d'accueil",
|
||||
"notifications": {
|
||||
"checkingUpdates": "Recherche de mises à jour...",
|
||||
"downloadError": "Échec du téléchargement de la mise à jour",
|
||||
"downloadStarted": "Téléchargement démarré...",
|
||||
"downloadUpdate": "Télécharger et installer la mise à jour {{version}} ?",
|
||||
"noUpdatesAvailable": "Vous utilisez la dernière version",
|
||||
"restartToUpdate": "Redémarrer maintenant pour installer la mise à jour ?",
|
||||
"updateAvailable": "Mise à jour disponible : {{version}}",
|
||||
"updateDownloaded": "Mise à jour téléchargée, redémarrez pour installer",
|
||||
"updateError": "Échec de la vérification des mises à jour : {{error}}"
|
||||
},
|
||||
"preferencesDescription": "Ajustez les paramètres de mise à jour sans quitter cette page.",
|
||||
"preferencesTitle": "Basculements Rapides",
|
||||
"resources": {
|
||||
"changelog": "Notes de version",
|
||||
"changelogDescription": "Suivez ce qui a changé dans chaque version.",
|
||||
"contact": "Support par email",
|
||||
"contactDescription": "Contactez directement pour de l'aide ou collaboration.",
|
||||
"documentation": "Centre d'aide",
|
||||
"documentationDescription": "Guides, FAQ et flux de travail courants.",
|
||||
"feedback": "Commentaires et problèmes",
|
||||
"feedbackDescription": "Partagez des idées ou signalez des problèmes sur GitHub.",
|
||||
"license": "Licence",
|
||||
"licenseDescription": "Consultez les termes de la licence open-source.",
|
||||
"website": "Site web officiel",
|
||||
"websiteDescription": "Points forts du produit, feuille de route et actualités de la communauté."
|
||||
},
|
||||
"resourcesDescription": "Liens utiles pour en savoir plus sur VidBee et rester connecté.",
|
||||
"resourcesTitle": "Ressources",
|
||||
"shareActions": {
|
||||
"copy": "Copier le lien",
|
||||
"facebook": "Partager sur Facebook",
|
||||
"twitter": "Partager sur X (Twitter)"
|
||||
},
|
||||
"shareDescription": "Partagez VidBee avec votre communauté en un clic.",
|
||||
"shareSupport": "Recommandez VidBee à vos amis pour soutenir notre croissance et nos mises à jour.",
|
||||
"shareTitle": "Faites passer le mot",
|
||||
"sourceCode": "Le code source est disponible",
|
||||
"title": "À propos",
|
||||
"version": "Version",
|
||||
"versionLabel": "v{{version}}",
|
||||
"latestVersionBadge": "Dernière : v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "Nouvelle version disponible",
|
||||
"uptodate": "Vous êtes à jour",
|
||||
"error": "Impossible de récupérer la dernière version"
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "Fermer l'application quand le téléchargement se termine",
|
||||
"currentLocation": "Emplacement de téléchargement actuel - ",
|
||||
"downloadLocation": "Emplacement de téléchargement",
|
||||
"downloadSubs": "Télécharger les sous-titres si disponibles",
|
||||
"end": "Fin",
|
||||
"endHint": "Si laissé vide, sera téléchargé jusqu'à la fin",
|
||||
"endPlaceholder": "10:00",
|
||||
"selectLocation": "Sélectionner l'Emplacement de Téléchargement",
|
||||
"start": "Début",
|
||||
"startHint": "Si laissé vide, commencera du début",
|
||||
"startPlaceholder": "00:00",
|
||||
"subtitles": "Sous-titres",
|
||||
"timeRange": "Télécharger une plage de temps spécifique",
|
||||
"title": "Options Avancées"
|
||||
},
|
||||
"app": {
|
||||
"description": "Télécharger des vidéos et audios depuis des centaines de sites",
|
||||
"title": "VidBee"
|
||||
},
|
||||
"audioExtract": {
|
||||
"bad": "Mauvais",
|
||||
"best": "Meilleur",
|
||||
"extract": "Extraire",
|
||||
"good": "Bon",
|
||||
"normal": "Normal",
|
||||
"selectFormat": "Sélectionner le Format",
|
||||
"selectQuality": "Sélectionner la Qualité",
|
||||
"title": "Extraire l'Audio",
|
||||
"worst": "Pire"
|
||||
},
|
||||
"download": {
|
||||
"active": "Actif",
|
||||
"all": "Tout",
|
||||
"audio": "Audio",
|
||||
"back": "Retour",
|
||||
"cancel": "Annuler",
|
||||
"cancelled": "Annulé",
|
||||
"clearCompleted": "Effacer les Terminés",
|
||||
"clearDownloads": "Effacer les Téléchargements",
|
||||
"completed": "Terminé",
|
||||
"downloadAudio": "Télécharger l'Audio",
|
||||
"downloadBtn": "Télécharger",
|
||||
"downloadPending": "En attente",
|
||||
"downloadQueue": "File de Téléchargement",
|
||||
"downloadVideo": "Télécharger la Vidéo",
|
||||
"downloading": "Téléchargement en cours...",
|
||||
"enterUrl": "Entrer l'URL de la Vidéo",
|
||||
"enterUrlDescription": "Collez ou tapez une URL de vidéo. ",
|
||||
"error": "Erreur",
|
||||
"fetch": "Récupérer",
|
||||
"fetchingVideoInfo": "Récupération des informations vidéo...",
|
||||
"history": "Historique",
|
||||
"imageLoadError": "Échec du chargement de l'image",
|
||||
"imagePlaceholder": "Aucune image disponible",
|
||||
"infoUnavailable": "Téléchargement en Un Clic (Info indisponible)",
|
||||
"loading": "Chargement",
|
||||
"moreOptions": "Plus d'options",
|
||||
"noActiveDownloads": "Aucun téléchargement actif",
|
||||
"noAudio": "Pas d'Audio",
|
||||
"noHistory": "Aucun historique de téléchargement",
|
||||
"noItems": "Aucun élément trouvé",
|
||||
"oneClickDownload": "Téléchargement en Un Clic",
|
||||
"oneClickDownloadDescription": "Télécharger directement avec les paramètres par défaut sans confirmation",
|
||||
"oneClickDownloadNow": "Télécharger Maintenant",
|
||||
"oneClickDownloadStarted": "Téléchargement démarré avec les paramètres par défaut",
|
||||
"paste": "Coller",
|
||||
"pastePlaylistUrl": "Cliquez pour coller le lien de la playlist depuis le presse-papiers [Ctrl + V]",
|
||||
"pasteUrl": "Cliquez pour coller l'URL de la vidéo ou l'ID [Ctrl + V]",
|
||||
"preparing": "Préparation...",
|
||||
"processing": "Traitement",
|
||||
"progress": "Progrès",
|
||||
"selectAudioFormat": "Sélectionner le Format Audio",
|
||||
"selectFormat": "Sélectionner le Format",
|
||||
"selectVideoFormat": "Sélectionner le Format Vidéo",
|
||||
"singleVideo": "Vidéo Unique",
|
||||
"speed": "Vitesse",
|
||||
"title": "Titre",
|
||||
"total": "Total",
|
||||
"unknownQuality": "Qualité inconnue",
|
||||
"unknownSize": "Taille inconnue",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=...",
|
||||
"video": "Vidéo",
|
||||
"videoInfo": "Informations Vidéo",
|
||||
"videoInfoUpdated": "Informations vidéo mises à jour"
|
||||
},
|
||||
"errors": {
|
||||
"clickToCopy": "Cliquez pour copier les détails",
|
||||
"clipboardEmpty": "Le presse-papiers est vide",
|
||||
"downloadFailed": "Échec du téléchargement",
|
||||
"downloadNecessaryFilesFailed": "Échec du téléchargement des fichiers nécessaires. Veuillez vérifier votre réseau et réessayer",
|
||||
"emptyUrl": "Veuillez entrer une URL",
|
||||
"errorDetails": "Détails de l'Erreur",
|
||||
"fetchInfoFailed": "Échec de la récupération des informations vidéo",
|
||||
"networkError": "Une erreur s'est produite. Vérifiez votre réseau et utilisez une URL correcte",
|
||||
"pasteFromClipboard": "Échec du collage depuis le presse-papiers"
|
||||
},
|
||||
"history": {
|
||||
"clearCancelled": "Effacer les Annulés",
|
||||
"clearCompleted": "Effacer les Terminés",
|
||||
"clearErrors": "Effacer les Erreurs",
|
||||
"copyToClipboard": "Copier dans le presse-papiers",
|
||||
"copyUrl": "Copier l'URL",
|
||||
"date": "Date",
|
||||
"description": "Voir et gérer votre historique de téléchargements",
|
||||
"duration": "Durée",
|
||||
"fileSize": "Taille du Fichier",
|
||||
"filters": {
|
||||
"all": "Tout",
|
||||
"cancelled": "Annulé",
|
||||
"completed": "Terminé",
|
||||
"errors": "Erreurs"
|
||||
},
|
||||
"noHistory": "Aucun historique de téléchargement encore",
|
||||
"noHistoryDescription": "Vos téléchargements terminés apparaîtront ici",
|
||||
"openDownloadFolder": "Ouvrir le Dossier de Téléchargement",
|
||||
"openFile": "Ouvrir le Fichier",
|
||||
"openFileLocation": "Ouvrir l'Emplacement du Fichier",
|
||||
"openFolder": "Ouvrir le Dossier",
|
||||
"openInBrowser": "Cliquez pour ouvrir dans le navigateur",
|
||||
"removeItem": "Supprimer l'Élément",
|
||||
"stats": {
|
||||
"cancelled": "Annulé",
|
||||
"completed": "Terminé",
|
||||
"errors": "Erreurs",
|
||||
"total": "Total"
|
||||
},
|
||||
"status": {
|
||||
"cancelled": "Annulé",
|
||||
"completed": "Terminé",
|
||||
"error": "Erreur"
|
||||
},
|
||||
"title": "Historique de Téléchargement"
|
||||
},
|
||||
"menu": {
|
||||
"about": "À propos",
|
||||
"download": "Télécharger",
|
||||
"playlist": "Télécharger la Playlist",
|
||||
"preferences": "Préférences",
|
||||
"supportedSites": "Sites Supportés",
|
||||
"theme": "Thème :"
|
||||
},
|
||||
"notifications": {
|
||||
"copyFailed": "Échec de la copie dans le presse-papiers",
|
||||
"downloadCompleted": "Téléchargement terminé",
|
||||
"downloadFailed": "Échec du téléchargement",
|
||||
"downloadStarted": "Téléchargement démarré",
|
||||
"itemRemoved": "Élément supprimé",
|
||||
"openFileFailed": "Échec de l'ouverture du fichier",
|
||||
"openFolderFailed": "Échec de l'ouverture du dossier",
|
||||
"removeFailed": "Échec de la suppression de l'élément",
|
||||
"settingsSaved": "Paramètres sauvegardés",
|
||||
"urlCopied": "URL copiée dans le presse-papiers",
|
||||
"videoCopied": "Vidéo copiée dans le presse-papiers"
|
||||
},
|
||||
"playlist": {
|
||||
"comingSoon": "La fonctionnalité de téléchargement de playlist arrive bientôt !",
|
||||
"completed": "Playlist téléchargée",
|
||||
"description": "Télécharger toutes les vidéos d'une playlist ou chaîne YouTube",
|
||||
"downloadFailed": "Échec du démarrage du téléchargement de playlist",
|
||||
"downloadPlaylist": "Télécharger la Playlist",
|
||||
"downloadStarted": "Démarré le téléchargement de {{count}} vidéos de la playlist",
|
||||
"downloadType": "Type de Téléchargement",
|
||||
"downloading": "Téléchargement de la playlist :",
|
||||
"endIndex": "Fin",
|
||||
"enterPlaylistUrl": "Entrer l'URL de la Playlist",
|
||||
"fetchFailed": "Échec de la récupération des informations de playlist",
|
||||
"filenameFormat": "Format de nom de fichier pour les playlists",
|
||||
"folderFormat": "Format de nom de dossier pour les playlists",
|
||||
"foundVideos": "Trouvé {{count}} vidéos dans la playlist",
|
||||
"linkLabel": "URL de la Playlist",
|
||||
"playlistUrlDescription": "Télécharger toutes les vidéos d'une playlist en lot",
|
||||
"range": "Plage (Optionnel)",
|
||||
"resetToDefault": "Réinitialiser par défaut",
|
||||
"startIndex": "Début (1)",
|
||||
"title": "Télécharger la Playlist"
|
||||
},
|
||||
"settings": {
|
||||
"aboutTab": "À propos",
|
||||
"advanced": "Avancé",
|
||||
"app": "Paramètres de l'App",
|
||||
"audio": "Préférences Audio",
|
||||
"browserForCookies": "Sélectionner le navigateur pour utiliser les cookies",
|
||||
"browserForCookiesDescription": "Navigateur pour extraire les cookies pour l'authentification",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"configFile": "Utiliser le fichier de configuration",
|
||||
"configFileDescription": "Fichier de configuration personnalisé pour yt-dlp",
|
||||
"dark": "Sombre",
|
||||
"description": "Configurez vos préférences de téléchargement et paramètres de l'application",
|
||||
"directorySelectError": "Échec de la sélection du répertoire",
|
||||
"downloadPath": "Emplacement de téléchargement",
|
||||
"downloadPathDescription": "Choisissez où sauvegarder les fichiers téléchargés",
|
||||
"fileSelectError": "Échec de la sélection du fichier",
|
||||
"general": "Général",
|
||||
"language": "Langue",
|
||||
"light": "Clair",
|
||||
"maxConcurrentDownloads": "Nombre maximum de téléchargements actifs",
|
||||
"maxConcurrentDownloadsDescription": "Nombre maximum de téléchargements simultanés",
|
||||
"none": "Aucun",
|
||||
"oneClickDownload": "Téléchargement en Un Clic",
|
||||
"oneClickDownloadDescription": "Activer le téléchargement en un clic avec les paramètres par défaut",
|
||||
"oneClickDownloadType": "Type de téléchargement par défaut",
|
||||
"oneClickDownloadTypeDescription": "Choisissez le type de téléchargement par défaut pour les téléchargements en un clic. La qualité utilise le préréglage ci-dessous.",
|
||||
"oneClickQuality": "Qualité préférée",
|
||||
"oneClickQualityDescription": "Sélectionnez le préréglage de qualité utilisé pour les téléchargements en un clic",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "Auto",
|
||||
"bad": "Mauvais",
|
||||
"best": "Meilleur",
|
||||
"good": "Bon",
|
||||
"normal": "Normal",
|
||||
"worst": "Pire"
|
||||
},
|
||||
"proxy": "Proxy",
|
||||
"proxyDescription": "Serveur proxy pour les requêtes réseau",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "Sélectionner le fichier de configuration",
|
||||
"selectPath": "Sélectionner",
|
||||
"showMoreFormats": "Afficher plus d'options de format",
|
||||
"showMoreFormatsDescription": "Afficher des options de format supplémentaires dans l'interface",
|
||||
"system": "Système",
|
||||
"theme": "Thème",
|
||||
"themeDescription": "Choisissez un thème clair, sombre ou système pour VidBee",
|
||||
"title": "Paramètres",
|
||||
"tray": {
|
||||
"quit": "Quitter",
|
||||
"showHome": "Afficher l'Accueil"
|
||||
},
|
||||
"video": "Préférences Vidéo"
|
||||
},
|
||||
"sites": {
|
||||
"homeInlineDescription": "Supporte {{sites}} et plus.",
|
||||
"moreDescription": "La liste complète yt-dlp est mise à jour constamment par la communauté.",
|
||||
"moreTitle": "Besoin d'un autre site ?",
|
||||
"openFullList": "Ouvrir la liste complète des sites supportés",
|
||||
"pageDescription": "VidBee utilise yt-dlp en arrière-plan pour atteindre des centaines de sources.",
|
||||
"pageIntro": "Voici les services principaux que les gens téléchargent le plus souvent.",
|
||||
"pageTitle": "Sites Supportés",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "Albums d'artistes indépendants et sorties communautaires.",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "Actualités mondiales, sports et clips de divertissement.",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "Vidéos de flux, Watch et Reels des pages publiques.",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "Contenu de flux, Stories, Reels et Highlights.",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Streams en direct et replays de créateurs sur la plateforme Kick.",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "Conférences professionnelles, webinaires et vidéos d'apprentissage.",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "Mixes DJ, émissions radio et audio long format.",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "Animation japonaise, musique et archives de diffusion en direct.",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "Épingles d'idées, Reels de tutoriels et vidéos d'inspiration lifestyle.",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "Clips intégrés et vidéos hébergées des communautés.",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "Pistes musicales, playlists et sets DJ.",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "Vidéos courtes mobiles, effets et streams en direct.",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "Médias courts créatifs et montages de fans.",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "Streams en direct de gaming, musique et IRL et VOD.",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "Publications de timeline, enregistrements Spaces et diffusions.",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "Hébergement vidéo de haute qualité pour créateurs et entreprises.",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "Vidéo long format et livestream de créateurs du monde entier.",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "Vidéos musicales officielles, albums et performances live.",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "Plateformes principales",
|
||||
"viewAll": "Voir tous les sites supportés"
|
||||
}
|
||||
}
|
||||
394
src/renderer/src/locales/it.json
Normal file
394
src/renderer/src/locales/it.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"about": {
|
||||
"actions": {
|
||||
"checkUpdates": "Controlla aggiornamenti",
|
||||
"email": "Email",
|
||||
"feedback": "Feedback",
|
||||
"openRepo": "Apri repository GitHub",
|
||||
"view": "Visualizza",
|
||||
"visit": "Visita"
|
||||
},
|
||||
"appName": "VidBee",
|
||||
"autoUpdateDescription": "Scarica e installa automaticamente le nuove versioni in background.",
|
||||
"autoUpdateTitle": "Aggiornamenti automatici",
|
||||
"betaProgramDescription": "Ricevi build anticipate e prossime funzionalità prima di tutti.",
|
||||
"betaProgramTitle": "Canale anteprima",
|
||||
"description": "VidBee è un downloader gratuito e open-source costruito con Electron e alimentato da yt-dlp.",
|
||||
"followAuthorActions": {
|
||||
"follow": "Segui @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "Rimani aggiornato con le ultime notizie e aggiornamenti di VidBee.",
|
||||
"followAuthorSupport": "Segui lo sviluppatore su X (Twitter) per ottenere gli ultimi aggiornamenti e notizie su VidBee.",
|
||||
"followAuthorTitle": "Segui lo Sviluppatore",
|
||||
"here": "qui",
|
||||
"homepage": "Homepage",
|
||||
"notifications": {
|
||||
"checkingUpdates": "Ricerca aggiornamenti...",
|
||||
"downloadError": "Errore nel download dell'aggiornamento",
|
||||
"downloadStarted": "Download iniziato...",
|
||||
"downloadUpdate": "Scarica e installa l'aggiornamento {{version}}?",
|
||||
"noUpdatesAvailable": "Stai usando l'ultima versione",
|
||||
"restartToUpdate": "Riavvia ora per installare l'aggiornamento?",
|
||||
"updateAvailable": "Aggiornamento disponibile: {{version}}",
|
||||
"updateDownloaded": "Aggiornamento scaricato, riavvia per installare",
|
||||
"updateError": "Errore nel controllo degli aggiornamenti: {{error}}"
|
||||
},
|
||||
"preferencesDescription": "Regola le impostazioni di aggiornamento senza lasciare questa pagina.",
|
||||
"preferencesTitle": "Toggle Rapidi",
|
||||
"resources": {
|
||||
"changelog": "Note di rilascio",
|
||||
"changelogDescription": "Tieni traccia di cosa è cambiato in ogni versione.",
|
||||
"contact": "Supporto email",
|
||||
"contactDescription": "Contatta direttamente per aiuto o collaborazione.",
|
||||
"documentation": "Centro assistenza",
|
||||
"documentationDescription": "Guide, FAQ e flussi di lavoro comuni.",
|
||||
"feedback": "Feedback e problemi",
|
||||
"feedbackDescription": "Condividi idee o segnala problemi su GitHub.",
|
||||
"license": "Licenza",
|
||||
"licenseDescription": "Rivedi i termini della licenza open-source.",
|
||||
"website": "Sito web ufficiale",
|
||||
"websiteDescription": "Punti salienti del prodotto, roadmap e notizie della comunità."
|
||||
},
|
||||
"resourcesDescription": "Link utili per saperne di più su VidBee e rimanere connessi.",
|
||||
"resourcesTitle": "Risorse",
|
||||
"shareActions": {
|
||||
"copy": "Copia link",
|
||||
"facebook": "Condividi su Facebook",
|
||||
"twitter": "Condividi su X (Twitter)"
|
||||
},
|
||||
"shareDescription": "Condividi VidBee con la tua comunità in un clic.",
|
||||
"shareSupport": "Raccomanda VidBee ai tuoi amici per sostenere la nostra crescita e aggiornamenti.",
|
||||
"shareTitle": "Passa parola",
|
||||
"sourceCode": "Il codice sorgente è disponibile",
|
||||
"title": "Informazioni",
|
||||
"version": "Versione",
|
||||
"versionLabel": "v{{version}}",
|
||||
"latestVersionBadge": "Ultima: v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "Nuova versione disponibile",
|
||||
"uptodate": "Sei aggiornato",
|
||||
"error": "Impossibile recuperare l'ultima versione"
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "Chiudi app quando il download finisce",
|
||||
"currentLocation": "Posizione download attuale - ",
|
||||
"downloadLocation": "Posizione download",
|
||||
"downloadSubs": "Scarica sottotitoli se disponibili",
|
||||
"end": "Fine",
|
||||
"endHint": "Se lasciato vuoto, verrà scaricato fino alla fine",
|
||||
"endPlaceholder": "10:00",
|
||||
"selectLocation": "Seleziona Posizione Download",
|
||||
"start": "Inizio",
|
||||
"startHint": "Se lasciato vuoto, inizierà dall'inizio",
|
||||
"startPlaceholder": "00:00",
|
||||
"subtitles": "Sottotitoli",
|
||||
"timeRange": "Scarica intervallo di tempo specifico",
|
||||
"title": "Opzioni Avanzate"
|
||||
},
|
||||
"app": {
|
||||
"description": "Scarica video e audio da centinaia di siti",
|
||||
"title": "VidBee"
|
||||
},
|
||||
"audioExtract": {
|
||||
"bad": "Cattivo",
|
||||
"best": "Migliore",
|
||||
"extract": "Estrai",
|
||||
"good": "Buono",
|
||||
"normal": "Normale",
|
||||
"selectFormat": "Seleziona Formato",
|
||||
"selectQuality": "Seleziona Qualità",
|
||||
"title": "Estrai Audio",
|
||||
"worst": "Peggiore"
|
||||
},
|
||||
"download": {
|
||||
"active": "Attivo",
|
||||
"all": "Tutto",
|
||||
"audio": "Audio",
|
||||
"back": "Indietro",
|
||||
"cancel": "Annulla",
|
||||
"cancelled": "Annullato",
|
||||
"clearCompleted": "Cancella Completati",
|
||||
"clearDownloads": "Cancella Download",
|
||||
"completed": "Completato",
|
||||
"downloadAudio": "Scarica Audio",
|
||||
"downloadBtn": "Scarica",
|
||||
"downloadPending": "In attesa",
|
||||
"downloadQueue": "Coda Download",
|
||||
"downloadVideo": "Scarica Video",
|
||||
"downloading": "Scaricando...",
|
||||
"enterUrl": "Inserisci URL Video",
|
||||
"enterUrlDescription": "Incolla o digita un URL video. ",
|
||||
"error": "Errore",
|
||||
"fetch": "Recupera",
|
||||
"fetchingVideoInfo": "Recupero informazioni video...",
|
||||
"history": "Cronologia",
|
||||
"imageLoadError": "Errore nel caricamento dell'immagine",
|
||||
"imagePlaceholder": "Nessuna immagine disponibile",
|
||||
"infoUnavailable": "Download con Un Clic (Info non disponibile)",
|
||||
"loading": "Caricamento",
|
||||
"moreOptions": "Più opzioni",
|
||||
"noActiveDownloads": "Nessun download attivo",
|
||||
"noAudio": "Nessun Audio",
|
||||
"noHistory": "Nessuna cronologia download",
|
||||
"noItems": "Nessun elemento trovato",
|
||||
"oneClickDownload": "Download con Un Clic",
|
||||
"oneClickDownloadDescription": "Scarica direttamente con impostazioni predefinite senza conferma",
|
||||
"oneClickDownloadNow": "Scarica Ora",
|
||||
"oneClickDownloadStarted": "Download iniziato con impostazioni predefinite",
|
||||
"paste": "Incolla",
|
||||
"pastePlaylistUrl": "Clicca per incollare link playlist dagli appunti [Ctrl + V]",
|
||||
"pasteUrl": "Clicca per incollare URL video o ID [Ctrl + V]",
|
||||
"preparing": "Preparazione...",
|
||||
"processing": "Elaborazione",
|
||||
"progress": "Progresso",
|
||||
"selectAudioFormat": "Seleziona Formato Audio",
|
||||
"selectFormat": "Seleziona Formato",
|
||||
"selectVideoFormat": "Seleziona Formato Video",
|
||||
"singleVideo": "Video Singolo",
|
||||
"speed": "Velocità",
|
||||
"title": "Titolo",
|
||||
"total": "Totale",
|
||||
"unknownQuality": "Qualità sconosciuta",
|
||||
"unknownSize": "Dimensione sconosciuta",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=...",
|
||||
"video": "Video",
|
||||
"videoInfo": "Informazioni Video",
|
||||
"videoInfoUpdated": "Informazioni video aggiornate"
|
||||
},
|
||||
"errors": {
|
||||
"clickToCopy": "Clicca per copiare i dettagli",
|
||||
"clipboardEmpty": "Gli appunti sono vuoti",
|
||||
"downloadFailed": "Download fallito",
|
||||
"downloadNecessaryFilesFailed": "Errore nel download dei file necessari. Controlla la tua rete e riprova",
|
||||
"emptyUrl": "Inserisci un URL",
|
||||
"errorDetails": "Dettagli Errore",
|
||||
"fetchInfoFailed": "Errore nel recupero delle informazioni video",
|
||||
"networkError": "Si è verificato un errore. Controlla la tua rete e usa un URL corretto",
|
||||
"pasteFromClipboard": "Errore nell'incollare dagli appunti"
|
||||
},
|
||||
"history": {
|
||||
"clearCancelled": "Cancella Annullati",
|
||||
"clearCompleted": "Cancella Completati",
|
||||
"clearErrors": "Cancella Errori",
|
||||
"copyToClipboard": "Copia negli appunti",
|
||||
"copyUrl": "Copia URL",
|
||||
"date": "Data",
|
||||
"description": "Visualizza e gestisci la tua cronologia download",
|
||||
"duration": "Durata",
|
||||
"fileSize": "Dimensione File",
|
||||
"filters": {
|
||||
"all": "Tutto",
|
||||
"cancelled": "Annullato",
|
||||
"completed": "Completato",
|
||||
"errors": "Errori"
|
||||
},
|
||||
"noHistory": "Nessuna cronologia download ancora",
|
||||
"noHistoryDescription": "I tuoi download completati appariranno qui",
|
||||
"openDownloadFolder": "Apri Cartella Download",
|
||||
"openFile": "Apri File",
|
||||
"openFileLocation": "Apri Posizione File",
|
||||
"openFolder": "Apri Cartella",
|
||||
"openInBrowser": "Clicca per aprire nel browser",
|
||||
"removeItem": "Rimuovi Elemento",
|
||||
"stats": {
|
||||
"cancelled": "Annullato",
|
||||
"completed": "Completato",
|
||||
"errors": "Errori",
|
||||
"total": "Totale"
|
||||
},
|
||||
"status": {
|
||||
"cancelled": "Annullato",
|
||||
"completed": "Completato",
|
||||
"error": "Errore"
|
||||
},
|
||||
"title": "Cronologia Download"
|
||||
},
|
||||
"menu": {
|
||||
"about": "Informazioni",
|
||||
"download": "Scarica",
|
||||
"playlist": "Scarica Playlist",
|
||||
"preferences": "Preferenze",
|
||||
"supportedSites": "Siti Supportati",
|
||||
"theme": "Tema:"
|
||||
},
|
||||
"notifications": {
|
||||
"copyFailed": "Errore nella copia negli appunti",
|
||||
"downloadCompleted": "Download completato",
|
||||
"downloadFailed": "Download fallito",
|
||||
"downloadStarted": "Download iniziato",
|
||||
"itemRemoved": "Elemento rimosso",
|
||||
"openFileFailed": "Errore nell'apertura del file",
|
||||
"openFolderFailed": "Errore nell'apertura della cartella",
|
||||
"removeFailed": "Errore nella rimozione dell'elemento",
|
||||
"settingsSaved": "Impostazioni salvate",
|
||||
"urlCopied": "URL copiato negli appunti",
|
||||
"videoCopied": "Video copiato negli appunti"
|
||||
},
|
||||
"playlist": {
|
||||
"comingSoon": "La funzionalità di download playlist arriverà presto!",
|
||||
"completed": "Playlist scaricata",
|
||||
"description": "Scarica tutti i video da una playlist o canale YouTube",
|
||||
"downloadFailed": "Errore nell'avvio del download playlist",
|
||||
"downloadPlaylist": "Scarica Playlist",
|
||||
"downloadStarted": "Iniziato download di {{count}} video dalla playlist",
|
||||
"downloadType": "Tipo Download",
|
||||
"downloading": "Scaricando playlist:",
|
||||
"endIndex": "Fine",
|
||||
"enterPlaylistUrl": "Inserisci URL Playlist",
|
||||
"fetchFailed": "Errore nel recupero delle informazioni playlist",
|
||||
"filenameFormat": "Formato nome file per playlist",
|
||||
"folderFormat": "Formato nome cartella per playlist",
|
||||
"foundVideos": "Trovati {{count}} video nella playlist",
|
||||
"linkLabel": "URL Playlist",
|
||||
"playlistUrlDescription": "Scarica tutti i video da una playlist in blocco",
|
||||
"range": "Intervallo (Opzionale)",
|
||||
"resetToDefault": "Ripristina predefinito",
|
||||
"startIndex": "Inizio (1)",
|
||||
"title": "Scarica Playlist"
|
||||
},
|
||||
"settings": {
|
||||
"aboutTab": "Informazioni",
|
||||
"advanced": "Avanzato",
|
||||
"app": "Impostazioni App",
|
||||
"audio": "Preferenze Audio",
|
||||
"browserForCookies": "Seleziona browser per usare i cookie",
|
||||
"browserForCookiesDescription": "Browser per estrarre i cookie per l'autenticazione",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"configFile": "Usa file di configurazione",
|
||||
"configFileDescription": "File di configurazione personalizzato per yt-dlp",
|
||||
"dark": "Scuro",
|
||||
"description": "Configura le tue preferenze di download e impostazioni dell'app",
|
||||
"directorySelectError": "Errore nella selezione della directory",
|
||||
"downloadPath": "Posizione download",
|
||||
"downloadPathDescription": "Scegli dove salvare i file scaricati",
|
||||
"fileSelectError": "Errore nella selezione del file",
|
||||
"general": "Generale",
|
||||
"language": "Lingua",
|
||||
"light": "Chiaro",
|
||||
"maxConcurrentDownloads": "Numero massimo di download attivi",
|
||||
"maxConcurrentDownloadsDescription": "Numero massimo di download simultanei",
|
||||
"none": "Nessuno",
|
||||
"oneClickDownload": "Download con Un Clic",
|
||||
"oneClickDownloadDescription": "Abilita download con un clic con impostazioni predefinite",
|
||||
"oneClickDownloadType": "Tipo download predefinito",
|
||||
"oneClickDownloadTypeDescription": "Scegli il tipo di download predefinito per i download con un clic. La qualità usa il preset qui sotto.",
|
||||
"oneClickQuality": "Qualità preferita",
|
||||
"oneClickQualityDescription": "Seleziona il preset di qualità utilizzato per i download con un clic",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "Auto",
|
||||
"bad": "Cattivo",
|
||||
"best": "Migliore",
|
||||
"good": "Buono",
|
||||
"normal": "Normale",
|
||||
"worst": "Peggiore"
|
||||
},
|
||||
"proxy": "Proxy",
|
||||
"proxyDescription": "Server proxy per le richieste di rete",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "Seleziona file di configurazione",
|
||||
"selectPath": "Seleziona",
|
||||
"showMoreFormats": "Mostra più opzioni formato",
|
||||
"showMoreFormatsDescription": "Visualizza opzioni di formato aggiuntive nell'interfaccia",
|
||||
"system": "Sistema",
|
||||
"theme": "Tema",
|
||||
"themeDescription": "Scegli un tema chiaro, scuro o sistema per VidBee",
|
||||
"title": "Impostazioni",
|
||||
"tray": {
|
||||
"quit": "Esci",
|
||||
"showHome": "Mostra Home"
|
||||
},
|
||||
"video": "Preferenze Video"
|
||||
},
|
||||
"sites": {
|
||||
"homeInlineDescription": "Supporta {{sites}} e altro.",
|
||||
"moreDescription": "La lista completa yt-dlp viene aggiornata costantemente dalla comunità.",
|
||||
"moreTitle": "Hai bisogno di un altro sito?",
|
||||
"openFullList": "Apri lista completa siti supportati",
|
||||
"pageDescription": "VidBee usa yt-dlp sotto il cofano per raggiungere centinaia di fonti.",
|
||||
"pageIntro": "Ecco i servizi principali da cui le persone scaricano più spesso.",
|
||||
"pageTitle": "Siti Supportati",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "Album di artisti indipendenti e uscite della comunità.",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "Notizie globali, sport e clip di intrattenimento.",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "Video di feed, Watch e Reels da pagine pubbliche.",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "Contenuto di feed, Stories, Reels e Highlights.",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Stream live e replay di creatori sulla piattaforma Kick.",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "Talk professionali, webinar e video di apprendimento.",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "Mix DJ, programmi radio e audio long-form.",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "Animazione giapponese, musica e archivio di trasmissioni live.",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "Pin di idee, Reels tutorial e video di ispirazione lifestyle.",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "Clip incorporati e video ospitati dalle comunità.",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "Traccia musicali, playlist e set DJ.",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "Video brevi mobili, effetti e stream live.",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "Media brevi creativi e montaggi di fan.",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "Stream live di gaming, musica e IRL e VOD.",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "Post di timeline, registrazioni Spaces e trasmissioni.",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "Hosting video di alta qualità per creatori e aziende.",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "Video long-form e livestream da creatori di tutto il mondo.",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "Video musicali ufficiali, album e performance live.",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "Piattaforme principali",
|
||||
"viewAll": "Visualizza tutti i siti supportati"
|
||||
}
|
||||
}
|
||||
394
src/renderer/src/locales/ja.json
Normal file
394
src/renderer/src/locales/ja.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"about": {
|
||||
"actions": {
|
||||
"checkUpdates": "アップデートを確認",
|
||||
"email": "メール",
|
||||
"feedback": "フィードバック",
|
||||
"openRepo": "GitHubリポジトリを開く",
|
||||
"view": "表示",
|
||||
"visit": "訪問"
|
||||
},
|
||||
"appName": "VidBee",
|
||||
"autoUpdateDescription": "バックグラウンドで新しいリリースを自動的にダウンロードしてインストールします。",
|
||||
"autoUpdateTitle": "自動アップデート",
|
||||
"betaProgramDescription": "他の人より先に早期ビルドと今後の機能を受け取ります。",
|
||||
"betaProgramTitle": "プレビューチャンネル",
|
||||
"description": "VidBeeはElectronで構築され、yt-dlpによって動力を得る無料のオープンソースダウンローダーです。",
|
||||
"followAuthorActions": {
|
||||
"follow": "@nexmoexをフォロー"
|
||||
},
|
||||
"followAuthorDescription": "VidBeeの最新ニュースとアップデートを入手してください。",
|
||||
"followAuthorSupport": "X (Twitter)で開発者をフォローして、VidBeeの最新アップデートとニュースを入手してください。",
|
||||
"followAuthorTitle": "開発者をフォロー",
|
||||
"here": "ここ",
|
||||
"homepage": "ホームページ",
|
||||
"notifications": {
|
||||
"checkingUpdates": "アップデートを検索中...",
|
||||
"downloadError": "アップデートのダウンロードに失敗",
|
||||
"downloadStarted": "ダウンロード開始...",
|
||||
"downloadUpdate": "アップデート{{version}}をダウンロードしてインストールしますか?",
|
||||
"noUpdatesAvailable": "最新バージョンを使用しています",
|
||||
"restartToUpdate": "今すぐ再起動してアップデートをインストールしますか?",
|
||||
"updateAvailable": "利用可能なアップデート:{{version}}",
|
||||
"updateDownloaded": "アップデートがダウンロードされました。再起動してインストールしてください",
|
||||
"updateError": "アップデートの確認に失敗:{{error}}"
|
||||
},
|
||||
"preferencesDescription": "このページを離れることなくアップデート設定を調整します。",
|
||||
"preferencesTitle": "クイックトグル",
|
||||
"resources": {
|
||||
"changelog": "リリースノート",
|
||||
"changelogDescription": "各バージョンで何が変更されたかを追跡します。",
|
||||
"contact": "メールサポート",
|
||||
"contactDescription": "ヘルプやコラボレーションのために直接連絡してください。",
|
||||
"documentation": "ヘルプセンター",
|
||||
"documentationDescription": "ガイド、FAQ、一般的なワークフロー。",
|
||||
"feedback": "フィードバックと問題",
|
||||
"feedbackDescription": "GitHubでアイデアを共有したり問題を報告したりしてください。",
|
||||
"license": "ライセンス",
|
||||
"licenseDescription": "オープンソースライセンス条項を確認してください。",
|
||||
"website": "公式ウェブサイト",
|
||||
"websiteDescription": "製品のハイライト、ロードマップ、コミュニティニュース。"
|
||||
},
|
||||
"resourcesDescription": "VidBeeについてもっと学び、つながりを保つための有用なリンク。",
|
||||
"resourcesTitle": "リソース",
|
||||
"shareActions": {
|
||||
"copy": "リンクをコピー",
|
||||
"facebook": "Facebookで共有",
|
||||
"twitter": "X (Twitter)で共有"
|
||||
},
|
||||
"shareDescription": "ワンクリックでコミュニティとVidBeeを共有してください。",
|
||||
"shareSupport": "私たちの成長とアップデートをサポートするために、友達にVidBeeを推奨してください。",
|
||||
"shareTitle": "口コミを広める",
|
||||
"sourceCode": "ソースコードが利用可能",
|
||||
"title": "について",
|
||||
"version": "バージョン",
|
||||
"versionLabel": "v{{version}}",
|
||||
"latestVersionBadge": "最新: v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "新しいバージョンが利用可能",
|
||||
"uptodate": "最新バージョンを使用中",
|
||||
"error": "最新バージョンを取得できません"
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "ダウンロード完了時にアプリを閉じる",
|
||||
"currentLocation": "現在のダウンロード場所 - ",
|
||||
"downloadLocation": "ダウンロード場所",
|
||||
"downloadSubs": "利用可能な場合は字幕をダウンロード",
|
||||
"end": "終了",
|
||||
"endHint": "空のままにすると最後までダウンロードされます",
|
||||
"endPlaceholder": "10:00",
|
||||
"selectLocation": "ダウンロード場所を選択",
|
||||
"start": "開始",
|
||||
"startHint": "空のままにすると最初から開始されます",
|
||||
"startPlaceholder": "00:00",
|
||||
"subtitles": "字幕",
|
||||
"timeRange": "特定の時間範囲をダウンロード",
|
||||
"title": "高度なオプション"
|
||||
},
|
||||
"app": {
|
||||
"description": "数百のサイトからビデオとオーディオをダウンロード",
|
||||
"title": "VidBee"
|
||||
},
|
||||
"audioExtract": {
|
||||
"bad": "悪い",
|
||||
"best": "最高",
|
||||
"extract": "抽出",
|
||||
"good": "良い",
|
||||
"normal": "通常",
|
||||
"selectFormat": "フォーマットを選択",
|
||||
"selectQuality": "品質を選択",
|
||||
"title": "オーディオを抽出",
|
||||
"worst": "最悪"
|
||||
},
|
||||
"download": {
|
||||
"active": "アクティブ",
|
||||
"all": "すべて",
|
||||
"audio": "オーディオ",
|
||||
"back": "戻る",
|
||||
"cancel": "キャンセル",
|
||||
"cancelled": "キャンセル済み",
|
||||
"clearCompleted": "完了をクリア",
|
||||
"clearDownloads": "ダウンロードをクリア",
|
||||
"completed": "完了",
|
||||
"downloadAudio": "オーディオをダウンロード",
|
||||
"downloadBtn": "ダウンロード",
|
||||
"downloadPending": "保留中",
|
||||
"downloadQueue": "ダウンロードキュー",
|
||||
"downloadVideo": "ビデオをダウンロード",
|
||||
"downloading": "ダウンロード中...",
|
||||
"enterUrl": "ビデオURLを入力",
|
||||
"enterUrlDescription": "ビデオURLを貼り付けまたは入力してください。 ",
|
||||
"error": "エラー",
|
||||
"fetch": "取得",
|
||||
"fetchingVideoInfo": "ビデオ情報を取得中...",
|
||||
"history": "履歴",
|
||||
"imageLoadError": "画像の読み込みに失敗",
|
||||
"imagePlaceholder": "利用可能な画像なし",
|
||||
"infoUnavailable": "ワンクリックダウンロード(情報利用不可)",
|
||||
"loading": "読み込み中",
|
||||
"moreOptions": "その他のオプション",
|
||||
"noActiveDownloads": "アクティブなダウンロードなし",
|
||||
"noAudio": "オーディオなし",
|
||||
"noHistory": "ダウンロード履歴なし",
|
||||
"noItems": "アイテムが見つかりません",
|
||||
"oneClickDownload": "ワンクリックダウンロード",
|
||||
"oneClickDownloadDescription": "確認なしでデフォルト設定で直接ダウンロード",
|
||||
"oneClickDownloadNow": "今すぐダウンロード",
|
||||
"oneClickDownloadStarted": "デフォルト設定でダウンロード開始",
|
||||
"paste": "貼り付け",
|
||||
"pastePlaylistUrl": "クリップボードからプレイリストリンクを貼り付け [Ctrl + V]",
|
||||
"pasteUrl": "ビデオURLまたはIDを貼り付け [Ctrl + V]",
|
||||
"preparing": "準備中...",
|
||||
"processing": "処理中",
|
||||
"progress": "進行状況",
|
||||
"selectAudioFormat": "オーディオフォーマットを選択",
|
||||
"selectFormat": "フォーマットを選択",
|
||||
"selectVideoFormat": "ビデオフォーマットを選択",
|
||||
"singleVideo": "単一ビデオ",
|
||||
"speed": "速度",
|
||||
"title": "タイトル",
|
||||
"total": "合計",
|
||||
"unknownQuality": "不明な品質",
|
||||
"unknownSize": "不明なサイズ",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=...",
|
||||
"video": "ビデオ",
|
||||
"videoInfo": "ビデオ情報",
|
||||
"videoInfoUpdated": "ビデオ情報が更新されました"
|
||||
},
|
||||
"errors": {
|
||||
"clickToCopy": "詳細をコピーするにはクリック",
|
||||
"clipboardEmpty": "クリップボードが空です",
|
||||
"downloadFailed": "ダウンロードに失敗",
|
||||
"downloadNecessaryFilesFailed": "必要なファイルのダウンロードに失敗しました。ネットワークを確認して再試行してください",
|
||||
"emptyUrl": "URLを入力してください",
|
||||
"errorDetails": "エラーの詳細",
|
||||
"fetchInfoFailed": "ビデオ情報の取得に失敗",
|
||||
"networkError": "エラーが発生しました。ネットワークを確認し、正しいURLを使用してください",
|
||||
"pasteFromClipboard": "クリップボードからの貼り付けに失敗"
|
||||
},
|
||||
"history": {
|
||||
"clearCancelled": "キャンセル済みをクリア",
|
||||
"clearCompleted": "完了をクリア",
|
||||
"clearErrors": "エラーをクリア",
|
||||
"copyToClipboard": "クリップボードにコピー",
|
||||
"copyUrl": "URLをコピー",
|
||||
"date": "日付",
|
||||
"description": "ダウンロード履歴を表示および管理",
|
||||
"duration": "期間",
|
||||
"fileSize": "ファイルサイズ",
|
||||
"filters": {
|
||||
"all": "すべて",
|
||||
"cancelled": "キャンセル済み",
|
||||
"completed": "完了",
|
||||
"errors": "エラー"
|
||||
},
|
||||
"noHistory": "ダウンロード履歴はまだありません",
|
||||
"noHistoryDescription": "完了したダウンロードがここに表示されます",
|
||||
"openDownloadFolder": "ダウンロードフォルダを開く",
|
||||
"openFile": "ファイルを開く",
|
||||
"openFileLocation": "ファイルの場所を開く",
|
||||
"openFolder": "フォルダを開く",
|
||||
"openInBrowser": "ブラウザで開くにはクリック",
|
||||
"removeItem": "アイテムを削除",
|
||||
"stats": {
|
||||
"cancelled": "キャンセル済み",
|
||||
"completed": "完了",
|
||||
"errors": "エラー",
|
||||
"total": "合計"
|
||||
},
|
||||
"status": {
|
||||
"cancelled": "キャンセル済み",
|
||||
"completed": "完了",
|
||||
"error": "エラー"
|
||||
},
|
||||
"title": "ダウンロード履歴"
|
||||
},
|
||||
"menu": {
|
||||
"about": "について",
|
||||
"download": "ダウンロード",
|
||||
"playlist": "プレイリストをダウンロード",
|
||||
"preferences": "設定",
|
||||
"supportedSites": "サポートされているサイト",
|
||||
"theme": "テーマ:"
|
||||
},
|
||||
"notifications": {
|
||||
"copyFailed": "クリップボードへのコピーに失敗",
|
||||
"downloadCompleted": "ダウンロード完了",
|
||||
"downloadFailed": "ダウンロードに失敗",
|
||||
"downloadStarted": "ダウンロード開始",
|
||||
"itemRemoved": "アイテムが削除されました",
|
||||
"openFileFailed": "ファイルの開封に失敗",
|
||||
"openFolderFailed": "フォルダの開封に失敗",
|
||||
"removeFailed": "アイテムの削除に失敗",
|
||||
"settingsSaved": "設定が保存されました",
|
||||
"urlCopied": "URLがクリップボードにコピーされました",
|
||||
"videoCopied": "ビデオがクリップボードにコピーされました"
|
||||
},
|
||||
"playlist": {
|
||||
"comingSoon": "プレイリストダウンロード機能がまもなく登場します!",
|
||||
"completed": "プレイリストがダウンロードされました",
|
||||
"description": "YouTubeプレイリストまたはチャンネルからすべてのビデオをダウンロード",
|
||||
"downloadFailed": "プレイリストダウンロードの開始に失敗",
|
||||
"downloadPlaylist": "プレイリストをダウンロード",
|
||||
"downloadStarted": "プレイリストから{{count}}個のビデオのダウンロードを開始",
|
||||
"downloadType": "ダウンロードタイプ",
|
||||
"downloading": "プレイリストをダウンロード中:",
|
||||
"endIndex": "終了",
|
||||
"enterPlaylistUrl": "プレイリストURLを入力",
|
||||
"fetchFailed": "プレイリスト情報の取得に失敗",
|
||||
"filenameFormat": "プレイリスト用ファイル名フォーマット",
|
||||
"folderFormat": "プレイリスト用フォルダ名フォーマット",
|
||||
"foundVideos": "プレイリストで{{count}}個のビデオを発見",
|
||||
"linkLabel": "プレイリストURL",
|
||||
"playlistUrlDescription": "プレイリストからすべてのビデオを一括ダウンロード",
|
||||
"range": "範囲(オプション)",
|
||||
"resetToDefault": "デフォルトにリセット",
|
||||
"startIndex": "開始(1)",
|
||||
"title": "プレイリストをダウンロード"
|
||||
},
|
||||
"settings": {
|
||||
"aboutTab": "について",
|
||||
"advanced": "高度",
|
||||
"app": "アプリ設定",
|
||||
"audio": "オーディオ設定",
|
||||
"browserForCookies": "Cookieに使用するブラウザを選択",
|
||||
"browserForCookiesDescription": "認証用のCookieを抽出するブラウザ",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"configFile": "設定ファイルを使用",
|
||||
"configFileDescription": "yt-dlp用のカスタム設定ファイル",
|
||||
"dark": "ダーク",
|
||||
"description": "ダウンロード設定とアプリ設定を構成",
|
||||
"directorySelectError": "ディレクトリの選択に失敗",
|
||||
"downloadPath": "ダウンロード場所",
|
||||
"downloadPathDescription": "ダウンロードファイルの保存場所を選択",
|
||||
"fileSelectError": "ファイルの選択に失敗",
|
||||
"general": "一般",
|
||||
"language": "言語",
|
||||
"light": "ライト",
|
||||
"maxConcurrentDownloads": "最大アクティブダウンロード数",
|
||||
"maxConcurrentDownloadsDescription": "最大同時ダウンロード数",
|
||||
"none": "なし",
|
||||
"oneClickDownload": "ワンクリックダウンロード",
|
||||
"oneClickDownloadDescription": "デフォルト設定でワンクリックダウンロードを有効化",
|
||||
"oneClickDownloadType": "デフォルトダウンロードタイプ",
|
||||
"oneClickDownloadTypeDescription": "ワンクリックダウンロードのデフォルトダウンロードタイプを選択。品質は下のプリセットを使用。",
|
||||
"oneClickQuality": "優先品質",
|
||||
"oneClickQualityDescription": "ワンクリックダウンロードに使用される品質プリセットを選択",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "自動",
|
||||
"bad": "悪い",
|
||||
"best": "最高",
|
||||
"good": "良い",
|
||||
"normal": "通常",
|
||||
"worst": "最悪"
|
||||
},
|
||||
"proxy": "プロキシ",
|
||||
"proxyDescription": "ネットワークリクエスト用のプロキシサーバー",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "設定ファイルを選択",
|
||||
"selectPath": "選択",
|
||||
"showMoreFormats": "より多くのフォーマットオプションを表示",
|
||||
"showMoreFormatsDescription": "インターフェースに追加のフォーマットオプションを表示",
|
||||
"system": "システム",
|
||||
"theme": "テーマ",
|
||||
"themeDescription": "VidBeeのライト、ダーク、またはシステムテーマを選択",
|
||||
"title": "設定",
|
||||
"tray": {
|
||||
"quit": "終了",
|
||||
"showHome": "ホームを表示"
|
||||
},
|
||||
"video": "ビデオ設定"
|
||||
},
|
||||
"sites": {
|
||||
"homeInlineDescription": "{{sites}}およびその他のサイトをサポートしています。",
|
||||
"moreDescription": "完全なyt-dlpリストはコミュニティによって継続的に更新されています。",
|
||||
"moreTitle": "他のサイトが必要ですか?",
|
||||
"openFullList": "サポートされているすべてのサイトリストを開く",
|
||||
"pageDescription": "VidBeeは数百のソースに到達するためにyt-dlpをバックグラウンドで使用します。",
|
||||
"pageIntro": "人々が最も頻繁にダウンロードする主要サービスです。",
|
||||
"pageTitle": "サポートされているサイト",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "インディペンデントアーティストのアルバムとコミュニティリリース。",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "グローバルニュース、スポーツ、エンターテイメントクリップ。",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "パブリックページからのフィード、Watch、Reels動画。",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "フィード、Stories、Reels、ハイライトコンテンツ。",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Kickプラットフォームでのクリエイターライブストリームとリプレイ。",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "プロフェッショナルトーク、ウェビナー、学習動画。",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "DJミックス、ラジオ番組、ロングフォームオーディオ。",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "日本のアニメーション、音楽、ライブ放送アーカイブ。",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "アイデアピン、ハウツーReels、ライフスタイルインスピレーション動画。",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "コミュニティからの埋め込みクリップとホスト動画。",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "音楽トラック、プレイリスト、DJセット。",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "ショートフォームモバイル動画、エフェクト、ライブストリーム。",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "クリエイティブショートフォームメディアとファン編集。",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "ゲーミング、音楽、IRLライブストリームとVOD。",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "タイムラインポスト、Spaces録音、放送。",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "クリエイターとビジネス向け高品質動画ホスティング。",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "世界中のクリエイターからのロングフォームとライブストリーム動画。",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "公式ミュージックビデオ、アルバム、ライブパフォーマンス。",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "主要プラットフォーム",
|
||||
"viewAll": "サポートされているすべてのサイトを表示"
|
||||
}
|
||||
}
|
||||
394
src/renderer/src/locales/ko.json
Normal file
394
src/renderer/src/locales/ko.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"about": {
|
||||
"actions": {
|
||||
"checkUpdates": "업데이트 확인",
|
||||
"email": "이메일",
|
||||
"feedback": "피드백",
|
||||
"openRepo": "GitHub 저장소 열기",
|
||||
"view": "보기",
|
||||
"visit": "방문"
|
||||
},
|
||||
"appName": "VidBee",
|
||||
"autoUpdateDescription": "백그라운드에서 새 릴리스를 자동으로 다운로드하고 설치합니다.",
|
||||
"autoUpdateTitle": "자동 업데이트",
|
||||
"betaProgramDescription": "다른 사람들보다 먼저 초기 빌드와 다가오는 기능을 받으세요.",
|
||||
"betaProgramTitle": "미리보기 채널",
|
||||
"description": "VidBee는 Electron으로 구축되고 yt-dlp로 구동되는 무료 오픈소스 다운로더입니다.",
|
||||
"followAuthorActions": {
|
||||
"follow": "@nexmoex 팔로우"
|
||||
},
|
||||
"followAuthorDescription": "VidBee의 최신 뉴스와 업데이트를 받아보세요.",
|
||||
"followAuthorSupport": "X (Twitter)에서 개발자를 팔로우하여 VidBee의 최신 업데이트와 뉴스를 받아보세요.",
|
||||
"followAuthorTitle": "개발자 팔로우",
|
||||
"here": "여기",
|
||||
"homepage": "홈페이지",
|
||||
"notifications": {
|
||||
"checkingUpdates": "업데이트 검색 중...",
|
||||
"downloadError": "업데이트 다운로드 실패",
|
||||
"downloadStarted": "다운로드 시작...",
|
||||
"downloadUpdate": "업데이트 {{version}}을(를) 다운로드하고 설치하시겠습니까?",
|
||||
"noUpdatesAvailable": "최신 버전을 사용 중입니다",
|
||||
"restartToUpdate": "지금 재시작하여 업데이트를 설치하시겠습니까?",
|
||||
"updateAvailable": "사용 가능한 업데이트: {{version}}",
|
||||
"updateDownloaded": "업데이트가 다운로드되었습니다. 재시작하여 설치하세요",
|
||||
"updateError": "업데이트 확인 실패: {{error}}"
|
||||
},
|
||||
"preferencesDescription": "이 페이지를 떠나지 않고 업데이트 설정을 조정하세요.",
|
||||
"preferencesTitle": "빠른 토글",
|
||||
"resources": {
|
||||
"changelog": "릴리스 노트",
|
||||
"changelogDescription": "각 버전에서 변경된 내용을 확인하세요.",
|
||||
"contact": "이메일 지원",
|
||||
"contactDescription": "도움이나 협업을 위해 직접 연락하세요.",
|
||||
"documentation": "도움말 센터",
|
||||
"documentationDescription": "가이드, FAQ 및 일반적인 워크플로우.",
|
||||
"feedback": "피드백 및 문제",
|
||||
"feedbackDescription": "GitHub에서 아이디어를 공유하거나 문제를 신고하세요.",
|
||||
"license": "라이선스",
|
||||
"licenseDescription": "오픈소스 라이선스 조건을 검토하세요.",
|
||||
"website": "공식 웹사이트",
|
||||
"websiteDescription": "제품 하이라이트, 로드맵 및 커뮤니티 뉴스."
|
||||
},
|
||||
"resourcesDescription": "VidBee에 대해 더 알아보고 연결을 유지하는 유용한 링크입니다.",
|
||||
"resourcesTitle": "리소스",
|
||||
"shareActions": {
|
||||
"copy": "링크 복사",
|
||||
"facebook": "Facebook에서 공유",
|
||||
"twitter": "X (Twitter)에서 공유"
|
||||
},
|
||||
"shareDescription": "한 번의 클릭으로 커뮤니티와 VidBee를 공유하세요.",
|
||||
"shareSupport": "우리의 성장과 업데이트를 지원하기 위해 친구들에게 VidBee를 추천하세요.",
|
||||
"shareTitle": "소문을 퍼뜨리세요",
|
||||
"sourceCode": "소스 코드 사용 가능",
|
||||
"title": "정보",
|
||||
"version": "버전",
|
||||
"versionLabel": "v{{version}}",
|
||||
"latestVersionBadge": "최신: v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "새 버전 사용 가능",
|
||||
"uptodate": "최신 버전 사용 중",
|
||||
"error": "최신 버전을 가져올 수 없음"
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "다운로드 완료 시 앱 닫기",
|
||||
"currentLocation": "현재 다운로드 위치 - ",
|
||||
"downloadLocation": "다운로드 위치",
|
||||
"downloadSubs": "사용 가능한 경우 자막 다운로드",
|
||||
"end": "끝",
|
||||
"endHint": "비워두면 끝까지 다운로드됩니다",
|
||||
"endPlaceholder": "10:00",
|
||||
"selectLocation": "다운로드 위치 선택",
|
||||
"start": "시작",
|
||||
"startHint": "비워두면 처음부터 시작됩니다",
|
||||
"startPlaceholder": "00:00",
|
||||
"subtitles": "자막",
|
||||
"timeRange": "특정 시간 범위 다운로드",
|
||||
"title": "고급 옵션"
|
||||
},
|
||||
"app": {
|
||||
"description": "수백 개의 사이트에서 비디오와 오디오 다운로드",
|
||||
"title": "VidBee"
|
||||
},
|
||||
"audioExtract": {
|
||||
"bad": "나쁨",
|
||||
"best": "최고",
|
||||
"extract": "추출",
|
||||
"good": "좋음",
|
||||
"normal": "보통",
|
||||
"selectFormat": "형식 선택",
|
||||
"selectQuality": "품질 선택",
|
||||
"title": "오디오 추출",
|
||||
"worst": "최악"
|
||||
},
|
||||
"download": {
|
||||
"active": "활성",
|
||||
"all": "모두",
|
||||
"audio": "오디오",
|
||||
"back": "뒤로",
|
||||
"cancel": "취소",
|
||||
"cancelled": "취소됨",
|
||||
"clearCompleted": "완료된 항목 지우기",
|
||||
"clearDownloads": "다운로드 지우기",
|
||||
"completed": "완료",
|
||||
"downloadAudio": "오디오 다운로드",
|
||||
"downloadBtn": "다운로드",
|
||||
"downloadPending": "대기 중",
|
||||
"downloadQueue": "다운로드 큐",
|
||||
"downloadVideo": "비디오 다운로드",
|
||||
"downloading": "다운로드 중...",
|
||||
"enterUrl": "비디오 URL 입력",
|
||||
"enterUrlDescription": "비디오 URL을 붙여넣거나 입력하세요. ",
|
||||
"error": "오류",
|
||||
"fetch": "가져오기",
|
||||
"fetchingVideoInfo": "비디오 정보 가져오는 중...",
|
||||
"history": "기록",
|
||||
"imageLoadError": "이미지 로드 실패",
|
||||
"imagePlaceholder": "사용 가능한 이미지 없음",
|
||||
"infoUnavailable": "원클릭 다운로드 (정보 사용 불가)",
|
||||
"loading": "로딩 중",
|
||||
"moreOptions": "더 많은 옵션",
|
||||
"noActiveDownloads": "활성 다운로드 없음",
|
||||
"noAudio": "오디오 없음",
|
||||
"noHistory": "다운로드 기록 없음",
|
||||
"noItems": "항목을 찾을 수 없음",
|
||||
"oneClickDownload": "원클릭 다운로드",
|
||||
"oneClickDownloadDescription": "확인 없이 기본 설정으로 직접 다운로드",
|
||||
"oneClickDownloadNow": "지금 다운로드",
|
||||
"oneClickDownloadStarted": "기본 설정으로 다운로드 시작됨",
|
||||
"paste": "붙여넣기",
|
||||
"pastePlaylistUrl": "클립보드에서 재생목록 링크 붙여넣기 [Ctrl + V]",
|
||||
"pasteUrl": "비디오 URL 또는 ID 붙여넣기 [Ctrl + V]",
|
||||
"preparing": "준비 중...",
|
||||
"processing": "처리 중",
|
||||
"progress": "진행률",
|
||||
"selectAudioFormat": "오디오 형식 선택",
|
||||
"selectFormat": "형식 선택",
|
||||
"selectVideoFormat": "비디오 형식 선택",
|
||||
"singleVideo": "단일 비디오",
|
||||
"speed": "속도",
|
||||
"title": "제목",
|
||||
"total": "총계",
|
||||
"unknownQuality": "알 수 없는 품질",
|
||||
"unknownSize": "알 수 없는 크기",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=...",
|
||||
"video": "비디오",
|
||||
"videoInfo": "비디오 정보",
|
||||
"videoInfoUpdated": "비디오 정보 업데이트됨"
|
||||
},
|
||||
"errors": {
|
||||
"clickToCopy": "세부 정보 복사하려면 클릭",
|
||||
"clipboardEmpty": "클립보드가 비어있음",
|
||||
"downloadFailed": "다운로드 실패",
|
||||
"downloadNecessaryFilesFailed": "필수 파일 다운로드 실패. 네트워크를 확인하고 다시 시도하세요",
|
||||
"emptyUrl": "URL을 입력하세요",
|
||||
"errorDetails": "오류 세부 정보",
|
||||
"fetchInfoFailed": "비디오 정보 가져오기 실패",
|
||||
"networkError": "오류가 발생했습니다. 네트워크를 확인하고 올바른 URL을 사용하세요",
|
||||
"pasteFromClipboard": "클립보드에서 붙여넣기 실패"
|
||||
},
|
||||
"history": {
|
||||
"clearCancelled": "취소된 항목 지우기",
|
||||
"clearCompleted": "완료된 항목 지우기",
|
||||
"clearErrors": "오류 지우기",
|
||||
"copyToClipboard": "클립보드에 복사",
|
||||
"copyUrl": "URL 복사",
|
||||
"date": "날짜",
|
||||
"description": "다운로드 기록 보기 및 관리",
|
||||
"duration": "지속 시간",
|
||||
"fileSize": "파일 크기",
|
||||
"filters": {
|
||||
"all": "모두",
|
||||
"cancelled": "취소됨",
|
||||
"completed": "완료",
|
||||
"errors": "오류"
|
||||
},
|
||||
"noHistory": "다운로드 기록이 아직 없습니다",
|
||||
"noHistoryDescription": "완료된 다운로드가 여기에 표시됩니다",
|
||||
"openDownloadFolder": "다운로드 폴더 열기",
|
||||
"openFile": "파일 열기",
|
||||
"openFileLocation": "파일 위치 열기",
|
||||
"openFolder": "폴더 열기",
|
||||
"openInBrowser": "브라우저에서 열려면 클릭",
|
||||
"removeItem": "항목 제거",
|
||||
"stats": {
|
||||
"cancelled": "취소됨",
|
||||
"completed": "완료",
|
||||
"errors": "오류",
|
||||
"total": "총계"
|
||||
},
|
||||
"status": {
|
||||
"cancelled": "취소됨",
|
||||
"completed": "완료",
|
||||
"error": "오류"
|
||||
},
|
||||
"title": "다운로드 기록"
|
||||
},
|
||||
"menu": {
|
||||
"about": "정보",
|
||||
"download": "다운로드",
|
||||
"playlist": "재생목록 다운로드",
|
||||
"preferences": "환경설정",
|
||||
"supportedSites": "지원되는 사이트",
|
||||
"theme": "테마:"
|
||||
},
|
||||
"notifications": {
|
||||
"copyFailed": "클립보드에 복사 실패",
|
||||
"downloadCompleted": "다운로드 완료",
|
||||
"downloadFailed": "다운로드 실패",
|
||||
"downloadStarted": "다운로드 시작됨",
|
||||
"itemRemoved": "항목 제거됨",
|
||||
"openFileFailed": "파일 열기 실패",
|
||||
"openFolderFailed": "폴더 열기 실패",
|
||||
"removeFailed": "항목 제거 실패",
|
||||
"settingsSaved": "설정 저장됨",
|
||||
"urlCopied": "URL이 클립보드에 복사됨",
|
||||
"videoCopied": "비디오가 클립보드에 복사됨"
|
||||
},
|
||||
"playlist": {
|
||||
"comingSoon": "재생목록 다운로드 기능이 곧 출시됩니다!",
|
||||
"completed": "재생목록 다운로드됨",
|
||||
"description": "YouTube 재생목록 또는 채널의 모든 비디오 다운로드",
|
||||
"downloadFailed": "재생목록 다운로드 시작 실패",
|
||||
"downloadPlaylist": "재생목록 다운로드",
|
||||
"downloadStarted": "재생목록에서 {{count}}개 비디오 다운로드 시작됨",
|
||||
"downloadType": "다운로드 유형",
|
||||
"downloading": "재생목록 다운로드 중:",
|
||||
"endIndex": "끝",
|
||||
"enterPlaylistUrl": "재생목록 URL 입력",
|
||||
"fetchFailed": "재생목록 정보 가져오기 실패",
|
||||
"filenameFormat": "재생목록용 파일명 형식",
|
||||
"folderFormat": "재생목록용 폴더명 형식",
|
||||
"foundVideos": "재생목록에서 {{count}}개 비디오 발견",
|
||||
"linkLabel": "재생목록 URL",
|
||||
"playlistUrlDescription": "재생목록의 모든 비디오를 일괄 다운로드",
|
||||
"range": "범위 (선택사항)",
|
||||
"resetToDefault": "기본값으로 재설정",
|
||||
"startIndex": "시작 (1)",
|
||||
"title": "재생목록 다운로드"
|
||||
},
|
||||
"settings": {
|
||||
"aboutTab": "정보",
|
||||
"advanced": "고급",
|
||||
"app": "앱 설정",
|
||||
"audio": "오디오 환경설정",
|
||||
"browserForCookies": "쿠키를 사용할 브라우저 선택",
|
||||
"browserForCookiesDescription": "인증을 위한 쿠키 추출 브라우저",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"configFile": "설정 파일 사용",
|
||||
"configFileDescription": "yt-dlp용 사용자 정의 설정 파일",
|
||||
"dark": "다크",
|
||||
"description": "다운로드 환경설정 및 앱 설정 구성",
|
||||
"directorySelectError": "디렉토리 선택 실패",
|
||||
"downloadPath": "다운로드 위치",
|
||||
"downloadPathDescription": "다운로드 파일을 저장할 위치 선택",
|
||||
"fileSelectError": "파일 선택 실패",
|
||||
"general": "일반",
|
||||
"language": "언어",
|
||||
"light": "라이트",
|
||||
"maxConcurrentDownloads": "최대 활성 다운로드 수",
|
||||
"maxConcurrentDownloadsDescription": "최대 동시 다운로드 수",
|
||||
"none": "없음",
|
||||
"oneClickDownload": "원클릭 다운로드",
|
||||
"oneClickDownloadDescription": "기본 설정으로 원클릭 다운로드 활성화",
|
||||
"oneClickDownloadType": "기본 다운로드 유형",
|
||||
"oneClickDownloadTypeDescription": "원클릭 다운로드의 기본 다운로드 유형을 선택하세요. 품질은 아래 사전 설정을 사용합니다.",
|
||||
"oneClickQuality": "선호 품질",
|
||||
"oneClickQualityDescription": "원클릭 다운로드에 사용되는 품질 사전 설정을 선택하세요",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "자동",
|
||||
"bad": "나쁨",
|
||||
"best": "최고",
|
||||
"good": "좋음",
|
||||
"normal": "보통",
|
||||
"worst": "최악"
|
||||
},
|
||||
"proxy": "프록시",
|
||||
"proxyDescription": "네트워크 요청용 프록시 서버",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "설정 파일 선택",
|
||||
"selectPath": "선택",
|
||||
"showMoreFormats": "더 많은 형식 옵션 표시",
|
||||
"showMoreFormatsDescription": "인터페이스에 추가 형식 옵션 표시",
|
||||
"system": "시스템",
|
||||
"theme": "테마",
|
||||
"themeDescription": "VidBee용 라이트, 다크 또는 시스템 테마 선택",
|
||||
"title": "설정",
|
||||
"tray": {
|
||||
"quit": "종료",
|
||||
"showHome": "홈 표시"
|
||||
},
|
||||
"video": "비디오 환경설정"
|
||||
},
|
||||
"sites": {
|
||||
"homeInlineDescription": "{{sites}} 및 더 많은 사이트를 지원합니다.",
|
||||
"moreDescription": "완전한 yt-dlp 목록은 커뮤니티에 의해 지속적으로 업데이트됩니다.",
|
||||
"moreTitle": "다른 사이트가 필요하신가요?",
|
||||
"openFullList": "지원되는 모든 사이트 목록 열기",
|
||||
"pageDescription": "VidBee는 수백 개의 소스에 도달하기 위해 yt-dlp를 백그라운드에서 사용합니다.",
|
||||
"pageIntro": "사람들이 가장 자주 다운로드하는 주요 서비스들입니다.",
|
||||
"pageTitle": "지원되는 사이트",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "인디 아티스트 앨범 및 커뮤니티 릴리스.",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "글로벌 뉴스, 스포츠 및 엔터테인먼트 클립.",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "공개 페이지의 피드, Watch 및 Reels 비디오.",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "피드, Stories, Reels 및 Highlights 콘텐츠.",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Kick 플랫폼의 크리에이터 라이브 스트림 및 리플레이.",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "전문 강연, 웨비나 및 학습 비디오.",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "DJ 믹스, 라디오 쇼 및 롱폼 오디오.",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "일본 애니메이션, 음악 및 라이브 방송 아카이브.",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "아이디어 핀, 하우투 Reels 및 라이프스타일 영감 비디오.",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "커뮤니티의 임베드 클립 및 호스팅 비디오.",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "음악 트랙, 플레이리스트 및 DJ 세트.",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "짧은 모바일 비디오, 효과 및 라이브 스트림.",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "창의적인 짧은 미디어 및 팬 편집.",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "게임, 음악 및 IRL 라이브 스트림 및 VOD.",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "타임라인 포스트, Spaces 녹음 및 방송.",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "크리에이터 및 비즈니스용 고품질 비디오 호스팅.",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "전 세계 크리에이터의 롱폼 및 라이브스트림 비디오.",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "공식 뮤직 비디오, 앨범 및 라이브 공연.",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "주요 플랫폼",
|
||||
"viewAll": "지원되는 모든 사이트 보기"
|
||||
}
|
||||
}
|
||||
394
src/renderer/src/locales/pt.json
Normal file
394
src/renderer/src/locales/pt.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"about": {
|
||||
"actions": {
|
||||
"checkUpdates": "Verificar atualizações",
|
||||
"email": "Email",
|
||||
"feedback": "Feedback",
|
||||
"openRepo": "Abrir repositório GitHub",
|
||||
"view": "Ver",
|
||||
"visit": "Visitar"
|
||||
},
|
||||
"appName": "VidBee",
|
||||
"autoUpdateDescription": "Baixar e instalar novas versões automaticamente em segundo plano.",
|
||||
"autoUpdateTitle": "Atualizações automáticas",
|
||||
"betaProgramDescription": "Receba builds antecipados e próximos recursos antes de todos.",
|
||||
"betaProgramTitle": "Canal de visualização",
|
||||
"description": "VidBee é um baixador gratuito e de código aberto construído com Electron e alimentado por yt-dlp.",
|
||||
"followAuthorActions": {
|
||||
"follow": "Seguir @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "Mantenha-se atualizado com as últimas notícias e atualizações do VidBee.",
|
||||
"followAuthorSupport": "Siga o desenvolvedor no X (Twitter) para obter as últimas atualizações e notícias sobre VidBee.",
|
||||
"followAuthorTitle": "Seguir o Desenvolvedor",
|
||||
"here": "aqui",
|
||||
"homepage": "Página inicial",
|
||||
"notifications": {
|
||||
"checkingUpdates": "Procurando atualizações...",
|
||||
"downloadError": "Falha ao baixar atualização",
|
||||
"downloadStarted": "Download iniciado...",
|
||||
"downloadUpdate": "Baixar e instalar atualização {{version}}?",
|
||||
"noUpdatesAvailable": "Você está usando a versão mais recente",
|
||||
"restartToUpdate": "Reiniciar agora para instalar atualização?",
|
||||
"updateAvailable": "Atualização disponível: {{version}}",
|
||||
"updateDownloaded": "Atualização baixada, reinicie para instalar",
|
||||
"updateError": "Falha ao verificar atualizações: {{error}}"
|
||||
},
|
||||
"preferencesDescription": "Ajuste configurações de atualização sem sair desta página.",
|
||||
"preferencesTitle": "Alternâncias Rápidas",
|
||||
"resources": {
|
||||
"changelog": "Notas da versão",
|
||||
"changelogDescription": "Acompanhe o que mudou em cada versão.",
|
||||
"contact": "Suporte por email",
|
||||
"contactDescription": "Entre em contato diretamente para ajuda ou colaboração.",
|
||||
"documentation": "Central de ajuda",
|
||||
"documentationDescription": "Guias, FAQs e fluxos de trabalho comuns.",
|
||||
"feedback": "Feedback e problemas",
|
||||
"feedbackDescription": "Compartilhe ideias ou reporte problemas no GitHub.",
|
||||
"license": "Licença",
|
||||
"licenseDescription": "Revise os termos da licença de código aberto.",
|
||||
"website": "Site oficial",
|
||||
"websiteDescription": "Destaques do produto, roadmap e notícias da comunidade."
|
||||
},
|
||||
"resourcesDescription": "Links úteis para aprender mais sobre VidBee e manter-se conectado.",
|
||||
"resourcesTitle": "Recursos",
|
||||
"shareActions": {
|
||||
"copy": "Copiar link",
|
||||
"facebook": "Compartilhar no Facebook",
|
||||
"twitter": "Compartilhar no X (Twitter)"
|
||||
},
|
||||
"shareDescription": "Compartilhe VidBee com sua comunidade em um clique.",
|
||||
"shareSupport": "Recomende VidBee aos seus amigos para apoiar nosso crescimento e atualizações.",
|
||||
"shareTitle": "Espalhe a palavra",
|
||||
"sourceCode": "Código fonte disponível",
|
||||
"title": "Sobre",
|
||||
"version": "Versão",
|
||||
"versionLabel": "v{{version}}",
|
||||
"latestVersionBadge": "Mais recente: v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "Nova versão disponível",
|
||||
"uptodate": "Você está atualizado",
|
||||
"error": "Não foi possível obter a versão mais recente"
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "Fechar aplicativo quando download terminar",
|
||||
"currentLocation": "Local de download atual - ",
|
||||
"downloadLocation": "Local de download",
|
||||
"downloadSubs": "Baixar legendas se disponíveis",
|
||||
"end": "Fim",
|
||||
"endHint": "Se deixado vazio, será baixado até o final",
|
||||
"endPlaceholder": "10:00",
|
||||
"selectLocation": "Selecionar Local de Download",
|
||||
"start": "Início",
|
||||
"startHint": "Se deixado vazio, começará do início",
|
||||
"startPlaceholder": "00:00",
|
||||
"subtitles": "Legendas",
|
||||
"timeRange": "Baixar intervalo de tempo específico",
|
||||
"title": "Opções Avançadas"
|
||||
},
|
||||
"app": {
|
||||
"description": "Baixar vídeos e áudios de centenas de sites",
|
||||
"title": "VidBee"
|
||||
},
|
||||
"audioExtract": {
|
||||
"bad": "Ruim",
|
||||
"best": "Melhor",
|
||||
"extract": "Extrair",
|
||||
"good": "Bom",
|
||||
"normal": "Normal",
|
||||
"selectFormat": "Selecionar Formato",
|
||||
"selectQuality": "Selecionar Qualidade",
|
||||
"title": "Extrair Áudio",
|
||||
"worst": "Pior"
|
||||
},
|
||||
"download": {
|
||||
"active": "Ativo",
|
||||
"all": "Todos",
|
||||
"audio": "Áudio",
|
||||
"back": "Voltar",
|
||||
"cancel": "Cancelar",
|
||||
"cancelled": "Cancelado",
|
||||
"clearCompleted": "Limpar Concluídos",
|
||||
"clearDownloads": "Limpar Downloads",
|
||||
"completed": "Concluído",
|
||||
"downloadAudio": "Baixar Áudio",
|
||||
"downloadBtn": "Baixar",
|
||||
"downloadPending": "Pendente",
|
||||
"downloadQueue": "Fila de Download",
|
||||
"downloadVideo": "Baixar Vídeo",
|
||||
"downloading": "Baixando...",
|
||||
"enterUrl": "Inserir URL do Vídeo",
|
||||
"enterUrlDescription": "Cole ou digite uma URL de vídeo. ",
|
||||
"error": "Erro",
|
||||
"fetch": "Buscar",
|
||||
"fetchingVideoInfo": "Buscando informações do vídeo...",
|
||||
"history": "Histórico",
|
||||
"imageLoadError": "Falha ao carregar imagem",
|
||||
"imagePlaceholder": "Nenhuma imagem disponível",
|
||||
"infoUnavailable": "Download de Um Clique (Info indisponível)",
|
||||
"loading": "Carregando",
|
||||
"moreOptions": "Mais opções",
|
||||
"noActiveDownloads": "Nenhum download ativo",
|
||||
"noAudio": "Sem Áudio",
|
||||
"noHistory": "Nenhum histórico de download",
|
||||
"noItems": "Nenhum item encontrado",
|
||||
"oneClickDownload": "Download de Um Clique",
|
||||
"oneClickDownloadDescription": "Baixar diretamente com configurações padrão sem confirmação",
|
||||
"oneClickDownloadNow": "Baixar Agora",
|
||||
"oneClickDownloadStarted": "Download iniciado com configurações padrão",
|
||||
"paste": "Colar",
|
||||
"pastePlaylistUrl": "Clique para colar link da playlist da área de transferência [Ctrl + V]",
|
||||
"pasteUrl": "Clique para colar URL do vídeo ou ID [Ctrl + V]",
|
||||
"preparing": "Preparando...",
|
||||
"processing": "Processando",
|
||||
"progress": "Progresso",
|
||||
"selectAudioFormat": "Selecionar Formato de Áudio",
|
||||
"selectFormat": "Selecionar Formato",
|
||||
"selectVideoFormat": "Selecionar Formato de Vídeo",
|
||||
"singleVideo": "Vídeo Único",
|
||||
"speed": "Velocidade",
|
||||
"title": "Título",
|
||||
"total": "Total",
|
||||
"unknownQuality": "Qualidade desconhecida",
|
||||
"unknownSize": "Tamanho desconhecido",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=...",
|
||||
"video": "Vídeo",
|
||||
"videoInfo": "Informações do Vídeo",
|
||||
"videoInfoUpdated": "Informações do vídeo atualizadas"
|
||||
},
|
||||
"errors": {
|
||||
"clickToCopy": "Clique para copiar detalhes",
|
||||
"clipboardEmpty": "Área de transferência vazia",
|
||||
"downloadFailed": "Download falhou",
|
||||
"downloadNecessaryFilesFailed": "Falha ao baixar arquivos necessários. Verifique sua rede e tente novamente",
|
||||
"emptyUrl": "Por favor, insira uma URL",
|
||||
"errorDetails": "Detalhes do Erro",
|
||||
"fetchInfoFailed": "Falha ao buscar informações do vídeo",
|
||||
"networkError": "Algum erro ocorreu. Verifique sua rede e use uma URL correta",
|
||||
"pasteFromClipboard": "Falha ao colar da área de transferência"
|
||||
},
|
||||
"history": {
|
||||
"clearCancelled": "Limpar Cancelados",
|
||||
"clearCompleted": "Limpar Concluídos",
|
||||
"clearErrors": "Limpar Erros",
|
||||
"copyToClipboard": "Copiar para área de transferência",
|
||||
"copyUrl": "Copiar URL",
|
||||
"date": "Data",
|
||||
"description": "Ver e gerenciar seu histórico de downloads",
|
||||
"duration": "Duração",
|
||||
"fileSize": "Tamanho do Arquivo",
|
||||
"filters": {
|
||||
"all": "Todos",
|
||||
"cancelled": "Cancelado",
|
||||
"completed": "Concluído",
|
||||
"errors": "Erros"
|
||||
},
|
||||
"noHistory": "Nenhum histórico de download ainda",
|
||||
"noHistoryDescription": "Seus downloads concluídos aparecerão aqui",
|
||||
"openDownloadFolder": "Abrir Pasta de Downloads",
|
||||
"openFile": "Abrir Arquivo",
|
||||
"openFileLocation": "Abrir Localização do Arquivo",
|
||||
"openFolder": "Abrir Pasta",
|
||||
"openInBrowser": "Clique para abrir no navegador",
|
||||
"removeItem": "Remover Item",
|
||||
"stats": {
|
||||
"cancelled": "Cancelado",
|
||||
"completed": "Concluído",
|
||||
"errors": "Erros",
|
||||
"total": "Total"
|
||||
},
|
||||
"status": {
|
||||
"cancelled": "Cancelado",
|
||||
"completed": "Concluído",
|
||||
"error": "Erro"
|
||||
},
|
||||
"title": "Histórico de Downloads"
|
||||
},
|
||||
"menu": {
|
||||
"about": "Sobre",
|
||||
"download": "Download",
|
||||
"playlist": "Baixar Playlist",
|
||||
"preferences": "Preferências",
|
||||
"supportedSites": "Sites Suportados",
|
||||
"theme": "Tema:"
|
||||
},
|
||||
"notifications": {
|
||||
"copyFailed": "Falha ao copiar para área de transferência",
|
||||
"downloadCompleted": "Download concluído",
|
||||
"downloadFailed": "Download falhou",
|
||||
"downloadStarted": "Download iniciado",
|
||||
"itemRemoved": "Item removido",
|
||||
"openFileFailed": "Falha ao abrir arquivo",
|
||||
"openFolderFailed": "Falha ao abrir pasta",
|
||||
"removeFailed": "Falha ao remover item",
|
||||
"settingsSaved": "Configurações salvas",
|
||||
"urlCopied": "URL copiada para área de transferência",
|
||||
"videoCopied": "Vídeo copiado para área de transferência"
|
||||
},
|
||||
"playlist": {
|
||||
"comingSoon": "Recurso de download de playlist em breve!",
|
||||
"completed": "Playlist baixada",
|
||||
"description": "Baixar todos os vídeos de uma playlist ou canal do YouTube",
|
||||
"downloadFailed": "Falha ao iniciar download da playlist",
|
||||
"downloadPlaylist": "Baixar Playlist",
|
||||
"downloadStarted": "Iniciado download de {{count}} vídeos da playlist",
|
||||
"downloadType": "Tipo de Download",
|
||||
"downloading": "Baixando playlist:",
|
||||
"endIndex": "Fim",
|
||||
"enterPlaylistUrl": "Inserir URL da Playlist",
|
||||
"fetchFailed": "Falha ao buscar informações da playlist",
|
||||
"filenameFormat": "Formato de nome de arquivo para playlists",
|
||||
"folderFormat": "Formato de nome de pasta para playlists",
|
||||
"foundVideos": "Encontrados {{count}} vídeos na playlist",
|
||||
"linkLabel": "URL da Playlist",
|
||||
"playlistUrlDescription": "Baixar todos os vídeos de uma playlist em lote",
|
||||
"range": "Intervalo (Opcional)",
|
||||
"resetToDefault": "Redefinir para padrão",
|
||||
"startIndex": "Início (1)",
|
||||
"title": "Baixar Playlist"
|
||||
},
|
||||
"settings": {
|
||||
"aboutTab": "Sobre",
|
||||
"advanced": "Avançado",
|
||||
"app": "Configurações do App",
|
||||
"audio": "Preferências de Áudio",
|
||||
"browserForCookies": "Selecionar navegador para usar cookies",
|
||||
"browserForCookiesDescription": "Navegador para extrair cookies para autenticação",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"configFile": "Usar arquivo de configuração",
|
||||
"configFileDescription": "Arquivo de configuração personalizado para yt-dlp",
|
||||
"dark": "Escuro",
|
||||
"description": "Configure suas preferências de download e configurações do aplicativo",
|
||||
"directorySelectError": "Falha ao selecionar diretório",
|
||||
"downloadPath": "Local de download",
|
||||
"downloadPathDescription": "Escolha onde salvar os arquivos baixados",
|
||||
"fileSelectError": "Falha ao selecionar arquivo",
|
||||
"general": "Geral",
|
||||
"language": "Idioma",
|
||||
"light": "Claro",
|
||||
"maxConcurrentDownloads": "Número máximo de downloads ativos",
|
||||
"maxConcurrentDownloadsDescription": "Número máximo de downloads simultâneos",
|
||||
"none": "Nenhum",
|
||||
"oneClickDownload": "Download de Um Clique",
|
||||
"oneClickDownloadDescription": "Habilitar download de um clique com configurações padrão",
|
||||
"oneClickDownloadType": "Tipo de download padrão",
|
||||
"oneClickDownloadTypeDescription": "Escolha o tipo de download padrão para downloads de um clique. A qualidade usa o preset abaixo.",
|
||||
"oneClickQuality": "Qualidade preferida",
|
||||
"oneClickQualityDescription": "Selecione o preset de qualidade usado para downloads de um clique",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "Automático",
|
||||
"bad": "Ruim",
|
||||
"best": "Melhor",
|
||||
"good": "Bom",
|
||||
"normal": "Normal",
|
||||
"worst": "Pior"
|
||||
},
|
||||
"proxy": "Proxy",
|
||||
"proxyDescription": "Servidor proxy para requisições de rede",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "Selecionar arquivo de configuração",
|
||||
"selectPath": "Selecionar",
|
||||
"showMoreFormats": "Mostrar mais opções de formato",
|
||||
"showMoreFormatsDescription": "Exibir opções de formato adicionais na interface",
|
||||
"system": "Sistema",
|
||||
"theme": "Tema",
|
||||
"themeDescription": "Escolha um tema claro, escuro ou sistema para VidBee",
|
||||
"title": "Configurações",
|
||||
"tray": {
|
||||
"quit": "Sair",
|
||||
"showHome": "Mostrar Início"
|
||||
},
|
||||
"video": "Preferências de Vídeo"
|
||||
},
|
||||
"sites": {
|
||||
"homeInlineDescription": "Suporta {{sites}} e mais.",
|
||||
"moreDescription": "A lista completa do yt-dlp é atualizada constantemente pela comunidade.",
|
||||
"moreTitle": "Precisa de outro site?",
|
||||
"openFullList": "Abrir lista completa de sites suportados",
|
||||
"pageDescription": "VidBee usa yt-dlp nos bastidores para alcançar centenas de fontes.",
|
||||
"pageIntro": "Aqui estão os serviços principais que as pessoas mais baixam.",
|
||||
"pageTitle": "Sites Suportados",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "Álbuns de artistas independentes e lançamentos da comunidade.",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "Notícias globais, esportes e clipes de entretenimento.",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "Vídeos de feed, Watch e Reels de páginas públicas.",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "Conteúdo de feed, Stories, Reels e Highlights.",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Streams ao vivo e replays de criadores na plataforma Kick.",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "Palestras profissionais, webinars e vídeos de aprendizado.",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "Mixes de DJ, programas de rádio e áudio long-form.",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "Animação japonesa, música e arquivo de transmissão ao vivo.",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "Pins de ideias, Reels de tutoriais e vídeos de inspiração lifestyle.",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "Clipes incorporados e vídeos hospedados das comunidades.",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "Faixas musicais, playlists e sets de DJ.",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "Vídeos curtos móveis, efeitos e streams ao vivo.",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "Mídia curta criativa e edições de fãs.",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "Streams ao vivo de gaming, música e IRL e VOD.",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "Posts de timeline, gravações Spaces e transmissões.",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "Hospedagem de vídeo de alta qualidade para criadores e empresas.",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "Vídeo long-form e livestream de criadores em todo o mundo.",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "Vídeos musicais oficiais, álbuns e performances ao vivo.",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "Plataformas principais",
|
||||
"viewAll": "Ver todos os sites suportados"
|
||||
}
|
||||
}
|
||||
418
src/renderer/src/locales/zh-TW.json
Normal file
418
src/renderer/src/locales/zh-TW.json
Normal file
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"about": {
|
||||
"actions": {
|
||||
"checkUpdates": "檢查更新",
|
||||
"email": "電子郵件",
|
||||
"feedback": "意見回饋",
|
||||
"openRepo": "開啟 GitHub 儲存庫",
|
||||
"view": "檢視",
|
||||
"visit": "造訪"
|
||||
},
|
||||
"appName": "VidBee",
|
||||
"autoUpdateDescription": "在背景自動下載並安裝新版本。",
|
||||
"autoUpdateTitle": "自動更新",
|
||||
"betaProgramDescription": "搶先獲得預覽版和即將發布的新功能。",
|
||||
"betaProgramTitle": "預覽通道",
|
||||
"description": "VidBee 是一個基於 Node.js 和 Electron 構建的自由開源應用,使用 yt-dlp 來完成下載。",
|
||||
"followAuthorActions": {
|
||||
"follow": "關注 @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "獲取 VidBee 的最新消息和更新。",
|
||||
"followAuthorSupport": "在 X (Twitter) 上關注開發者,獲取 VidBee 的最新更新和消息。",
|
||||
"followAuthorTitle": "關注開發者",
|
||||
"here": "此處",
|
||||
"homepage": "首頁",
|
||||
"latestVersionBadge": "最新版本:v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "有新版本可用",
|
||||
"error": "無法取得最新版本",
|
||||
"uptodate": "您已是最新版本"
|
||||
},
|
||||
"notifications": {
|
||||
"checkingUpdates": "正在搜尋更新...",
|
||||
"downloadError": "下載更新失敗",
|
||||
"downloadStarted": "開始下載...",
|
||||
"downloadUpdate": "下載並安裝更新 {{version}}?",
|
||||
"noUpdatesAvailable": "您正在使用最新版本",
|
||||
"restartToUpdate": "立即重新啟動以安裝更新?",
|
||||
"updateAvailable": "發現新版本:{{version}}",
|
||||
"updateDownloaded": "更新已下載,重新啟動以安裝",
|
||||
"updateError": "檢查更新失敗:{{error}}"
|
||||
},
|
||||
"preferencesDescription": "無需離開此頁即可調整更新設定。",
|
||||
"preferencesTitle": "快速切換",
|
||||
"resources": {
|
||||
"changelog": "發行說明",
|
||||
"changelogDescription": "了解每個版本的變更內容。",
|
||||
"contact": "電子郵件支援",
|
||||
"contactDescription": "直接聯繫我們以獲取協助或開展合作。",
|
||||
"documentation": "說明中心",
|
||||
"documentationDescription": "指南、常見問題和常見流程。",
|
||||
"feedback": "意見回饋與問題",
|
||||
"feedbackDescription": "在 GitHub 上分享想法或回報問題。",
|
||||
"license": "授權條款",
|
||||
"licenseDescription": "查閱開源授權條款。",
|
||||
"website": "官方網站",
|
||||
"websiteDescription": "產品亮點、路線圖與社群動態。"
|
||||
},
|
||||
"resourcesDescription": "了解 VidBee 並保持關注的實用連結。",
|
||||
"resourcesTitle": "資源",
|
||||
"shareActions": {
|
||||
"copy": "複製連結",
|
||||
"facebook": "在 Facebook 上分享",
|
||||
"twitter": "在 X (Twitter) 上分享"
|
||||
},
|
||||
"shareDescription": "一鍵與您的社群分享 VidBee。",
|
||||
"shareSupport": "向您的朋友推薦 VidBee 以支援我們的成長和更新。",
|
||||
"shareTitle": "廣為宣傳",
|
||||
"sourceCode": "原始碼已開放",
|
||||
"title": "關於",
|
||||
"version": "版本",
|
||||
"versionLabel": "v{{version}}"
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "下載完成後關閉應用程式",
|
||||
"currentLocation": "目前下載位置 - ",
|
||||
"downloadLocation": "下載位置",
|
||||
"downloadSubs": "若有字幕則下載",
|
||||
"end": "結束",
|
||||
"endHint": "如果留空,將下載到結尾",
|
||||
"endPlaceholder": "10:00",
|
||||
"selectLocation": "選擇下載位置",
|
||||
"start": "開始",
|
||||
"startHint": "如果留空,將從開頭開始",
|
||||
"startPlaceholder": "00:00",
|
||||
"subtitles": "字幕",
|
||||
"timeRange": "下載指定時間範圍",
|
||||
"title": "進階選項"
|
||||
},
|
||||
"app": {
|
||||
"description": "從數百個網站下載影片和音訊",
|
||||
"title": "VidBee"
|
||||
},
|
||||
"audioExtract": {
|
||||
"bad": "較差",
|
||||
"best": "最佳",
|
||||
"extract": "擷取",
|
||||
"good": "良好",
|
||||
"normal": "標準",
|
||||
"selectFormat": "選擇格式",
|
||||
"selectQuality": "選擇品質",
|
||||
"title": "擷取音訊",
|
||||
"worst": "最差"
|
||||
},
|
||||
"download": {
|
||||
"active": "進行中",
|
||||
"all": "全部",
|
||||
"audio": "音訊",
|
||||
"back": "返回",
|
||||
"cancel": "取消",
|
||||
"cancelled": "已取消",
|
||||
"clearCompleted": "清除已完成",
|
||||
"clearDownloads": "清除下載",
|
||||
"completed": "已完成",
|
||||
"downloadAudio": "下載音訊",
|
||||
"downloadBtn": "下載",
|
||||
"downloadPending": "待處理",
|
||||
"downloadQueue": "下載佇列",
|
||||
"downloadVideo": "下載影片",
|
||||
"downloading": "正在下載...",
|
||||
"enterUrl": "輸入影片連結",
|
||||
"enterUrlDescription": "貼上或輸入一個影片連結。",
|
||||
"error": "錯誤",
|
||||
"fetch": "取得",
|
||||
"fetchingVideoInfo": "正在取得影片資訊...",
|
||||
"history": "歷史",
|
||||
"imageLoadError": "圖片載入失敗",
|
||||
"imagePlaceholder": "暫無圖片",
|
||||
"infoUnavailable": "一鍵下載(資訊不可用)",
|
||||
"loading": "載入中",
|
||||
"moreOptions": "更多選項",
|
||||
"noActiveDownloads": "暫無進行中的下載",
|
||||
"noAudio": "無音訊",
|
||||
"noHistory": "暫無下載歷史",
|
||||
"noItems": "未找到項目",
|
||||
"oneClickDownload": "一鍵下載",
|
||||
"oneClickDownloadDescription": "使用預設設定直接下載,無需確認",
|
||||
"oneClickDownloadNow": "立即下載",
|
||||
"oneClickDownloadStarted": "已使用預設設定開始下載",
|
||||
"paste": "貼上",
|
||||
"pastePlaylistUrl": "點擊從剪貼簿貼上播放清單連結 [Ctrl + V]",
|
||||
"pasteUrl": "點擊貼上影片連結或 ID [Ctrl + V]",
|
||||
"preparing": "正在準備...",
|
||||
"processing": "處理中",
|
||||
"progress": "進度",
|
||||
"selectAudioFormat": "選擇音訊格式",
|
||||
"selectFormat": "選擇格式",
|
||||
"selectVideoFormat": "選擇影片格式",
|
||||
"singleVideo": "單個影片",
|
||||
"speed": "速度",
|
||||
"title": "標題",
|
||||
"total": "總計",
|
||||
"unknownQuality": "未知品質",
|
||||
"unknownSize": "未知大小",
|
||||
"urlPlaceholder": "https://www.youtube.com/watch?v=...",
|
||||
"video": "影片",
|
||||
"videoInfo": "影片資訊",
|
||||
"videoInfoUpdated": "影片資訊已更新"
|
||||
},
|
||||
"errors": {
|
||||
"clickToCopy": "點擊複製詳情",
|
||||
"clipboardEmpty": "剪貼簿為空",
|
||||
"downloadFailed": "下載失敗",
|
||||
"downloadNecessaryFilesFailed": "必要檔案下載失敗。請檢查網路後再試",
|
||||
"emptyUrl": "請輸入連結",
|
||||
"errorDetails": "錯誤詳情",
|
||||
"fetchInfoFailed": "取得影片資訊失敗",
|
||||
"networkError": "發生錯誤。請檢查網路並確認連結正確",
|
||||
"pasteFromClipboard": "從剪貼簿貼上失敗"
|
||||
},
|
||||
"history": {
|
||||
"clearCancelled": "清除已取消",
|
||||
"clearCompleted": "清除已完成",
|
||||
"clearErrors": "清除錯誤",
|
||||
"copyToClipboard": "複製到剪貼簿",
|
||||
"copyUrl": "複製連結",
|
||||
"date": "日期",
|
||||
"description": "檢視並管理下載歷史",
|
||||
"duration": "時長",
|
||||
"fileSize": "檔案大小",
|
||||
"filters": {
|
||||
"all": "全部",
|
||||
"cancelled": "已取消",
|
||||
"completed": "已完成",
|
||||
"errors": "錯誤"
|
||||
},
|
||||
"noHistory": "暫無下載歷史",
|
||||
"noHistoryDescription": "完成的下載會顯示在這裡",
|
||||
"openDownloadFolder": "開啟下載資料夾",
|
||||
"openFile": "開啟檔案",
|
||||
"openFileLocation": "開啟檔案位置",
|
||||
"openFolder": "開啟資料夾",
|
||||
"openInBrowser": "點擊在瀏覽器中開啟",
|
||||
"removeItem": "移除項目",
|
||||
"stats": {
|
||||
"cancelled": "已取消",
|
||||
"completed": "已完成",
|
||||
"errors": "錯誤",
|
||||
"total": "總計"
|
||||
},
|
||||
"status": {
|
||||
"cancelled": "已取消",
|
||||
"completed": "已完成",
|
||||
"error": "錯誤"
|
||||
},
|
||||
"title": "下載歷史"
|
||||
},
|
||||
"menu": {
|
||||
"about": "關於",
|
||||
"download": "下載",
|
||||
"playlist": "下載播放清單",
|
||||
"preferences": "偏好設定",
|
||||
"supportedSites": "支援的網站",
|
||||
"theme": "主題:"
|
||||
},
|
||||
"notifications": {
|
||||
"copyFailed": "複製到剪貼簿失敗",
|
||||
"downloadCompleted": "下載完成",
|
||||
"downloadFailed": "下載失敗",
|
||||
"downloadStarted": "下載已開始",
|
||||
"itemRemoved": "項目已移除",
|
||||
"openFileFailed": "開啟檔案失敗",
|
||||
"openFolderFailed": "開啟資料夾失敗",
|
||||
"removeFailed": "移除項目失敗",
|
||||
"settingsSaved": "設定已儲存",
|
||||
"urlCopied": "連結已複製到剪貼簿",
|
||||
"videoCopied": "影片已複製到剪貼簿"
|
||||
},
|
||||
"playlist": {
|
||||
"clearPreview": "清晰預覽",
|
||||
"comingSoon": "播放清單下載功能即將推出!",
|
||||
"completed": "播放清單已下載",
|
||||
"description": "下載 YouTube 播放清單或頻道中的全部影片",
|
||||
"downloadFailed": "啟動播放清單下載失敗",
|
||||
"downloadPlaylist": "下載播放清單",
|
||||
"downloadStarted": "已開始下載播放清單中的 {{count}} 個影片",
|
||||
"downloadType": "下載類型",
|
||||
"downloading": "正在下載播放清單:",
|
||||
"endIndex": "結束",
|
||||
"enterPlaylistUrl": "輸入播放清單連結",
|
||||
"fetchFailed": "取得播放清單資訊失敗",
|
||||
"filenameFormat": "播放清單檔案名稱格式",
|
||||
"folderFormat": "播放清單資料夾命名格式",
|
||||
"foundVideos": "在播放清單中找到 {{count}} 個影片",
|
||||
"groupActive": "{{count}} 個活躍",
|
||||
"groupErrors": "{{count}} 失敗",
|
||||
"groupSummary": "{{已完成}} / {{總計}}已完成",
|
||||
"linkLabel": "播放清單連結",
|
||||
"noEntries": "在此播放列表中找不到視頻",
|
||||
"noEntriesInRange": "所選範圍內沒有視頻",
|
||||
"noRangeSelected": "沒有結束設置 - 已選擇完整播放列表",
|
||||
"playlistUrlDescription": "批量下載播放清單中的所有影片",
|
||||
"positionLabel": "第 {{index}} 項,共 {{total}} 項",
|
||||
"previewButton": "預覽播放列表",
|
||||
"previewFailed": "預覽播放列表失敗",
|
||||
"previewRequired": "下載前預覽播放列表。",
|
||||
"previewSummary": "下載前預覽播放列表項目。",
|
||||
"range": "範圍(可選)",
|
||||
"resetToDefault": "恢復預設",
|
||||
"selectedRange": "範圍:{{開始}}-{{結束}}",
|
||||
"showingCount": "顯示 {{count}} 個視頻",
|
||||
"startIndex": "開始(1)",
|
||||
"title": "下載播放清單",
|
||||
"totalVideos": "視頻總數:{{count}}",
|
||||
"untitled": "無標題播放列表"
|
||||
},
|
||||
"settings": {
|
||||
"aboutTab": "關於",
|
||||
"advanced": "進階",
|
||||
"app": "應用程式設定",
|
||||
"audio": "音訊偏好",
|
||||
"browserForCookies": "選擇用於讀取 Cookie 的瀏覽器",
|
||||
"browserForCookiesDescription": "用於身份驗證的瀏覽器 Cookie 提取",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"clearCookiesFile": "清除",
|
||||
"configFile": "使用設定檔",
|
||||
"configFileDescription": "yt-dlp 的自訂設定檔",
|
||||
"cookiesFile": "餅乾文件",
|
||||
"cookiesFileDescription": "要加載以進行身份驗證的 Netscape 格式的 cookie 文件",
|
||||
"cookiesHelpBrowser": "選擇上面的瀏覽器以自動重用其登錄會話。",
|
||||
"cookiesHelpFaq": "打開 yt-dlp cookies 常見問題解答",
|
||||
"cookiesHelpFile": "導出 Netscape cookies 文件(請參閱 yt-dlp FAQ)並在需要時在此處選擇它。",
|
||||
"cookiesHelpTitle": "使用cookie",
|
||||
"dark": "深色",
|
||||
"description": "設定下載偏好和應用程式設定",
|
||||
"directorySelectError": "選擇目錄失敗",
|
||||
"downloadPath": "下載位置",
|
||||
"downloadPathDescription": "選擇儲存下載檔案的位置",
|
||||
"fileSelectError": "選擇檔案失敗",
|
||||
"general": "一般",
|
||||
"language": "語言",
|
||||
"light": "淺色",
|
||||
"maxConcurrentDownloads": "最大活動下載數",
|
||||
"maxConcurrentDownloadsDescription": "最大同時下載數量",
|
||||
"none": "無",
|
||||
"oneClickDownload": "一鍵下載",
|
||||
"oneClickDownloadDescription": "啟用使用預設設定的一鍵下載",
|
||||
"oneClickDownloadType": "預設下載類型",
|
||||
"oneClickDownloadTypeDescription": "選擇一鍵下載的預設下載類型。品質使用下面的預設。",
|
||||
"oneClickQuality": "首選品質",
|
||||
"oneClickQualityDescription": "選擇用於一鍵下載的品質預設",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "自動",
|
||||
"bad": "較差",
|
||||
"best": "最佳",
|
||||
"good": "良好",
|
||||
"normal": "標準",
|
||||
"worst": "最差"
|
||||
},
|
||||
"openLinkError": "無法打開鏈接",
|
||||
"proxy": "代理伺服器",
|
||||
"proxyDescription": "網路請求的代理伺服器",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "選擇設定檔",
|
||||
"selectPath": "選擇",
|
||||
"showMoreFormats": "顯示更多格式選項",
|
||||
"showMoreFormatsDescription": "在介面中顯示額外的格式選項",
|
||||
"system": "系統",
|
||||
"theme": "主題",
|
||||
"themeDescription": "為 VidBee 選擇淺色、深色或系統主題",
|
||||
"title": "設定",
|
||||
"tray": {
|
||||
"quit": "結束",
|
||||
"showHome": "顯示首頁"
|
||||
},
|
||||
"video": "影片偏好"
|
||||
},
|
||||
"sites": {
|
||||
"homeInlineDescription": "支援 {{sites}} 等更多網站。",
|
||||
"moreDescription": "完整的 yt-dlp 清單由社群持續更新。",
|
||||
"moreTitle": "需要其他網站?",
|
||||
"openFullList": "開啟全部支援網站清單",
|
||||
"pageDescription": "VidBee 使用 yt-dlp 覆蓋數百個資源。",
|
||||
"pageIntro": "以下是大家最常下載的主流服務。",
|
||||
"pageTitle": "支援的網站",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "獨立藝術家專輯和社群發布。",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "全球新聞、體育和娛樂片段。",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "來自公開頁面的動態、觀看和 Reels 影片。",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "動態、故事、Reels 和精選內容。",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Kick 平台上的創作者直播和回放。",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "專業演講、網路研討會和學習影片。",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "DJ 混音、廣播節目和長音訊。",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "日本動畫、音樂和直播檔案。",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "創意圖釘、教學 Reels 和生活方式靈感影片。",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "來自社群的嵌入片段和託管影片。",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "音樂曲目、播放清單和 DJ 套裝。",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "短影片、特效和直播。",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "創意短影片和粉絲編輯。",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "遊戲、音樂和 IRL 直播和 VOD。",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "時間軸貼文、Spaces 錄音和廣播。",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "高品質創作者和商業影片託管。",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "來自全球創作者的長影片和直播。",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "官方音樂影片、專輯和現場表演。",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "主流平台",
|
||||
"viewAll": "檢視全部支援的網站"
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,24 @@
|
||||
"betaProgramDescription": "抢先获得预览版和即将发布的新功能。",
|
||||
"betaProgramTitle": "预览通道",
|
||||
"description": "这是一个基于 Node.js 和 Electron 构建的自由开源应用,使用 yt-dlp 来完成下载。",
|
||||
"followAuthorActions": {
|
||||
"follow": "关注 @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "获取 VidBee 的最新消息和更新。",
|
||||
"followAuthorSupport": "在 X (Twitter) 上关注开发者,获取 VidBee 的最新更新和消息。",
|
||||
"followAuthorTitle": "关注开发者",
|
||||
"here": "此处",
|
||||
"homepage": "主页",
|
||||
"notifications": {
|
||||
"checkingUpdates": "正在查找更新...",
|
||||
"updateAvailable": "发现新版本: {{version}}",
|
||||
"noUpdatesAvailable": "您正在使用最新版本",
|
||||
"updateError": "检查更新失败: {{error}}",
|
||||
"downloadUpdate": "下载并安装更新 {{version}}?",
|
||||
"downloadStarted": "开始下载...",
|
||||
"downloadError": "下载更新失败",
|
||||
"downloadStarted": "开始下载...",
|
||||
"downloadUpdate": "下载并安装更新 {{version}}?",
|
||||
"noUpdatesAvailable": "您正在使用最新版本",
|
||||
"restartToUpdate": "立即重启以安装更新?",
|
||||
"updateAvailable": "发现新版本: {{version}}",
|
||||
"updateDownloaded": "更新已下载,重启以安装",
|
||||
"restartToUpdate": "立即重启以安装更新?"
|
||||
"updateError": "检查更新失败: {{error}}"
|
||||
},
|
||||
"preferencesDescription": "无需离开此页即可调整更新设置。",
|
||||
"preferencesTitle": "快速切换",
|
||||
@@ -43,19 +49,18 @@
|
||||
"website": "官方网站",
|
||||
"websiteDescription": "产品亮点、路线图与社区动态。"
|
||||
},
|
||||
"followAuthorActions": {
|
||||
"follow": "关注 @nexmoex"
|
||||
},
|
||||
"followAuthorDescription": "获取 VidBee 的最新消息和更新。",
|
||||
"followAuthorSupport": "在 X (Twitter) 上关注开发者,获取 VidBee 的最新更新和消息。",
|
||||
"followAuthorTitle": "关注开发者",
|
||||
"resourcesDescription": "了解 VidBee 并保持关注的实用链接。",
|
||||
"resourcesTitle": "资源",
|
||||
"sourceCode": "源代码已开放",
|
||||
"tagline": "面向每位创作者的 AI 友好下载助手",
|
||||
"title": "关于",
|
||||
"version": "版本",
|
||||
"versionLabel": "v{{version}}"
|
||||
"versionLabel": "v{{version}}",
|
||||
"latestVersionBadge": "最新版本:v{{version}}",
|
||||
"latestVersionStatus": {
|
||||
"available": "有新版本可用",
|
||||
"uptodate": "您已是最新版本",
|
||||
"error": "无法获取最新版本"
|
||||
}
|
||||
},
|
||||
"advancedOptions": {
|
||||
"closeWhenDone": "下载完成后关闭应用",
|
||||
@@ -159,7 +164,6 @@
|
||||
"clearCompleted": "清除已完成",
|
||||
"clearErrors": "清除错误",
|
||||
"copyUrl": "复制链接",
|
||||
"openInBrowser": "点击在浏览器中打开",
|
||||
"date": "日期",
|
||||
"description": "查看并管理下载历史",
|
||||
"duration": "时长",
|
||||
@@ -172,10 +176,11 @@
|
||||
},
|
||||
"noHistory": "暂无下载历史",
|
||||
"noHistoryDescription": "完成的下载会显示在这里",
|
||||
"openDownloadFolder": "打开下载文件夹",
|
||||
"openFile": "打开文件",
|
||||
"openFileLocation": "打开文件位置",
|
||||
"openFolder": "打开文件夹",
|
||||
"openDownloadFolder": "打开下载文件夹",
|
||||
"openInBrowser": "点击在浏览器中打开",
|
||||
"removeItem": "移除项目",
|
||||
"stats": {
|
||||
"cancelled": "已取消",
|
||||
@@ -238,34 +243,52 @@
|
||||
"app": "应用设置",
|
||||
"audio": "音频偏好",
|
||||
"browserForCookies": "选择用于读取 Cookie 的浏览器",
|
||||
"browserForCookiesDescription": "用于身份验证的浏览器 Cookie 提取",
|
||||
"browserOptions": {
|
||||
"brave": "Brave",
|
||||
"chrome": "Chrome",
|
||||
"edge": "Edge",
|
||||
"firefox": "Firefox",
|
||||
"safari": "Safari"
|
||||
},
|
||||
"configFile": "使用配置文件",
|
||||
"configFileDescription": "yt-dlp 的自定义配置文件",
|
||||
"dark": "深色",
|
||||
"description": "配置下载偏好和应用设置",
|
||||
"directorySelectError": "选择目录失败",
|
||||
"downloadPath": "下载位置",
|
||||
"downloadPathDescription": "选择保存下载文件的位置",
|
||||
"fileSelectError": "选择文件失败",
|
||||
"general": "通用",
|
||||
"language": "语言",
|
||||
"languageOptions": {
|
||||
"chinese": "简体中文",
|
||||
"english": "英语"
|
||||
},
|
||||
"light": "浅色",
|
||||
"maxConcurrentDownloads": "最大活动下载数",
|
||||
"maxConcurrentDownloadsDescription": "最大同时下载数量",
|
||||
"none": "无",
|
||||
"oneClickAudioForVideo": "视频默认音频",
|
||||
"oneClickAudioFormat": "默认音频格式",
|
||||
"oneClickDownload": "一键下载",
|
||||
"oneClickDownloadDescription": "启用使用默认设置的一键下载",
|
||||
"oneClickDownloadType": "默认下载类型",
|
||||
"oneClickVideoFormat": "默认视频格式",
|
||||
"preferredAudioQuality": "首选音频质量",
|
||||
"preferredVideoCodec": "首选视频编码",
|
||||
"preferredVideoQuality": "首选视频质量",
|
||||
"oneClickDownloadTypeDescription": "选择一键下载的默认下载类型。质量使用下面的预设。",
|
||||
"oneClickQuality": "首选质量",
|
||||
"oneClickQualityDescription": "选择用于一键下载的质量预设",
|
||||
"oneClickQualityOptions": {
|
||||
"auto": "自动",
|
||||
"bad": "较差",
|
||||
"best": "最佳",
|
||||
"good": "良好",
|
||||
"normal": "标准",
|
||||
"worst": "最差"
|
||||
},
|
||||
"proxy": "代理",
|
||||
"proxyDescription": "网络请求的代理服务器",
|
||||
"proxyPlaceholder": "http://proxy:port",
|
||||
"selectConfigFile": "选择配置文件",
|
||||
"selectPath": "选择",
|
||||
"showMoreFormats": "显示更多格式选项",
|
||||
"showMoreFormatsDescription": "在界面中显示额外的格式选项",
|
||||
"system": "系统",
|
||||
"theme": "主题",
|
||||
"themeDescription": "为 VidBee 选择浅色、深色或系统主题",
|
||||
"title": "设置",
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
@@ -281,6 +304,80 @@
|
||||
"pageDescription": "VidBee 使用 yt-dlp 覆盖数百个资源。",
|
||||
"pageIntro": "以下是大家最常下载的主流服务。",
|
||||
"pageTitle": "支持的网站",
|
||||
"popular": {
|
||||
"bandcamp": {
|
||||
"description": "独立艺术家专辑和社区发布。",
|
||||
"label": "Bandcamp"
|
||||
},
|
||||
"dailymotion": {
|
||||
"description": "全球新闻、体育和娱乐片段。",
|
||||
"label": "Dailymotion"
|
||||
},
|
||||
"facebook": {
|
||||
"description": "来自公共页面的动态、观看和 Reels 视频。",
|
||||
"label": "Facebook"
|
||||
},
|
||||
"instagram": {
|
||||
"description": "动态、故事、Reels 和精选内容。",
|
||||
"label": "Instagram"
|
||||
},
|
||||
"kick": {
|
||||
"description": "Kick 平台上的创作者直播和回放。",
|
||||
"label": "Kick"
|
||||
},
|
||||
"linkedin": {
|
||||
"description": "专业演讲、网络研讨会和学习视频。",
|
||||
"label": "LinkedIn"
|
||||
},
|
||||
"mixcloud": {
|
||||
"description": "DJ 混音、广播节目和长音频。",
|
||||
"label": "Mixcloud"
|
||||
},
|
||||
"niconico": {
|
||||
"description": "日本动画、音乐和直播档案。",
|
||||
"label": "Niconico"
|
||||
},
|
||||
"pinterest": {
|
||||
"description": "创意图钉、教程 Reels 和生活方式灵感视频。",
|
||||
"label": "Pinterest"
|
||||
},
|
||||
"reddit": {
|
||||
"description": "来自社区的嵌入片段和托管视频。",
|
||||
"label": "Reddit"
|
||||
},
|
||||
"soundcloud": {
|
||||
"description": "音乐曲目、播放列表和 DJ 套装。",
|
||||
"label": "SoundCloud"
|
||||
},
|
||||
"tiktok": {
|
||||
"description": "短视频、特效和直播。",
|
||||
"label": "TikTok"
|
||||
},
|
||||
"tumblr": {
|
||||
"description": "创意短视频和粉丝编辑。",
|
||||
"label": "Tumblr"
|
||||
},
|
||||
"twitch": {
|
||||
"description": "游戏、音乐和 IRL 直播和 VOD。",
|
||||
"label": "Twitch"
|
||||
},
|
||||
"twitter": {
|
||||
"description": "时间线帖子、Spaces 录音和广播。",
|
||||
"label": "X (Twitter)"
|
||||
},
|
||||
"vimeo": {
|
||||
"description": "高质量创作者和商业视频托管。",
|
||||
"label": "Vimeo"
|
||||
},
|
||||
"youtube": {
|
||||
"description": "来自全球创作者的长视频和直播。",
|
||||
"label": "YouTube"
|
||||
},
|
||||
"youtubemusic": {
|
||||
"description": "官方音乐视频、专辑和现场表演。",
|
||||
"label": "YouTube Music"
|
||||
}
|
||||
},
|
||||
"popularSection": "主流平台",
|
||||
"viewAll": "查看全部支持的网站"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import './assets/main.css'
|
||||
import './assets/global.css'
|
||||
import 'flag-icons/css/flag-icons.min.css'
|
||||
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
@@ -36,12 +36,19 @@ interface AboutResource {
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
type LatestVersionState =
|
||||
| { status: 'available'; version: string }
|
||||
| { status: 'uptodate'; version: string }
|
||||
| { status: 'error'; error?: string }
|
||||
| null
|
||||
|
||||
export function About() {
|
||||
const { t } = useTranslation()
|
||||
const [settings, _setSettings] = useAtom(settingsAtom)
|
||||
const [appVersion, setAppVersion] = useState<string>('—')
|
||||
const [latestVersionState, setLatestVersionState] = useState<LatestVersionState>(null)
|
||||
const saveSetting = useSetAtom(saveSettingAtom)
|
||||
const shareTargetUrl = 'https://github.com/nexmoe/VidBee'
|
||||
const shareTargetUrl = 'https://vidbee.org'
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true
|
||||
@@ -79,20 +86,35 @@ export function About() {
|
||||
|
||||
if (result.available) {
|
||||
toast.success(t('about.notifications.updateAvailable', { version: result.version }))
|
||||
setLatestVersionState({
|
||||
status: 'available',
|
||||
version: result.version ?? ''
|
||||
})
|
||||
} else if (result.error) {
|
||||
toast.error(t('about.notifications.updateError', { error: result.error }))
|
||||
setLatestVersionState({
|
||||
status: 'error',
|
||||
error: result.error
|
||||
})
|
||||
} else {
|
||||
toast.success(t('about.notifications.noUpdatesAvailable'))
|
||||
setLatestVersionState({
|
||||
status: 'uptodate',
|
||||
version: result.version ?? appVersion
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check for updates:', error)
|
||||
toast.error(t('about.notifications.updateError', { error: 'Unknown error' }))
|
||||
setLatestVersionState({
|
||||
status: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const shareLinks = useMemo(() => {
|
||||
const encodedUrl = encodeURIComponent(shareTargetUrl)
|
||||
const encodedText = encodeURIComponent(t('about.tagline'))
|
||||
const encodedText = encodeURIComponent(t('about.description'))
|
||||
|
||||
return {
|
||||
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`,
|
||||
@@ -126,8 +148,30 @@ export function About() {
|
||||
}
|
||||
}
|
||||
|
||||
const latestVersionBadgeText =
|
||||
latestVersionState && latestVersionState.status !== 'error' && latestVersionState.version
|
||||
? t('about.latestVersionBadge', { version: latestVersionState.version })
|
||||
: null
|
||||
const latestVersionStatusKey = latestVersionState
|
||||
? `about.latestVersionStatus.${latestVersionState.status}`
|
||||
: null
|
||||
const latestVersionStatusClass =
|
||||
latestVersionState?.status === 'available'
|
||||
? 'text-primary'
|
||||
: latestVersionState?.status === 'error'
|
||||
? 'text-destructive'
|
||||
: 'text-muted-foreground'
|
||||
const latestVersionStatusText = latestVersionStatusKey ? t(latestVersionStatusKey) : null
|
||||
|
||||
const aboutResources = useMemo<AboutResource[]>(
|
||||
() => [
|
||||
{
|
||||
icon: LinkIcon,
|
||||
label: t('about.resources.website'),
|
||||
description: t('about.resources.websiteDescription'),
|
||||
actionLabel: t('about.actions.visit'),
|
||||
href: 'https://vidbee.org/'
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
label: t('about.resources.changelog'),
|
||||
@@ -163,11 +207,6 @@ export function About() {
|
||||
return (
|
||||
<div className="h-full bg-background">
|
||||
<div className="container mx-auto max-w-5xl p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('about.title')}</h1>
|
||||
<p className="text-muted-foreground">{t('about.description')}</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
@@ -176,11 +215,25 @@ export function About() {
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold leading-tight">{t('about.appName')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('about.tagline')}</p>
|
||||
<p className="text-sm text-muted-foreground">{t('about.description')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{t('about.versionLabel', { version: appVersion })}
|
||||
</Badge>
|
||||
{latestVersionState ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{latestVersionBadgeText ? (
|
||||
<Badge variant="outline">{latestVersionBadgeText}</Badge>
|
||||
) : null}
|
||||
{latestVersionStatusText ? (
|
||||
<span className={`text-sm ${latestVersionStatusClass}`}>
|
||||
{latestVersionStatusText}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{t('about.versionLabel', { version: appVersion })}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -15,15 +15,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@renderer/components/ui/select'
|
||||
import { Tabs, TabsContent } from '@renderer/components/ui/tabs'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'
|
||||
import { popularSites } from '@renderer/data/popularSites'
|
||||
import type { AppSettings, OneClickQualityPreset } from '@shared/types'
|
||||
import type { AppSettings, OneClickQualityPreset, PlaylistInfo } from '@shared/types'
|
||||
import { useAtom, useSetAtom } from 'jotai'
|
||||
import { AlertCircle, Download, Loader2, Search } from 'lucide-react'
|
||||
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
||||
import { AlertCircle, Download, ListVideo, Loader2, Search } from 'lucide-react'
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { UnifiedDownloadHistory } from '../components/download/UnifiedDownloadHistory'
|
||||
import { PlaylistPreviewCard } from '../components/playlist/PlaylistPreviewCard'
|
||||
import { VideoInfoCard } from '../components/video/VideoInfoCard'
|
||||
import { ipcEvents, ipcServices } from '../lib/ipc'
|
||||
import {
|
||||
@@ -141,17 +142,48 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const inlinePreviewSites = popularSites
|
||||
.slice(0, 3)
|
||||
.map((site) => site.label)
|
||||
.map((site) => t(`sites.popular.${site.id}.label`))
|
||||
.join(', ')
|
||||
|
||||
// Playlist states
|
||||
const playlistUrlId = useId()
|
||||
const downloadTypeId = useId()
|
||||
const [playlistUrl, setPlaylistUrl] = useState('')
|
||||
const [playlistLoading, setPlaylistLoading] = useState(false)
|
||||
const [downloadType, setDownloadType] = useState<'video' | 'audio'>('video')
|
||||
const [startIndex, setStartIndex] = useState('1')
|
||||
const [endIndex, setEndIndex] = useState('')
|
||||
const [playlistInfo, setPlaylistInfo] = useState<PlaylistInfo | null>(null)
|
||||
const [playlistPreviewLoading, setPlaylistPreviewLoading] = useState(false)
|
||||
const [playlistDownloadLoading, setPlaylistDownloadLoading] = useState(false)
|
||||
const [playlistPreviewError, setPlaylistPreviewError] = useState<string | null>(null)
|
||||
const playlistBusy = playlistPreviewLoading || playlistDownloadLoading
|
||||
|
||||
const computePlaylistRange = useCallback(
|
||||
(info: PlaylistInfo) => {
|
||||
const parsedStart = Math.max(parseInt(startIndex, 10) || 1, 1)
|
||||
const rawEnd = endIndex ? Math.max(parseInt(endIndex, 10), parsedStart) : undefined
|
||||
const start = info.entryCount > 0 ? Math.min(parsedStart, info.entryCount) : parsedStart
|
||||
const endValue =
|
||||
rawEnd !== undefined
|
||||
? info.entryCount > 0
|
||||
? Math.min(rawEnd, info.entryCount)
|
||||
: rawEnd
|
||||
: undefined
|
||||
return { start, end: endValue }
|
||||
},
|
||||
[startIndex, endIndex]
|
||||
)
|
||||
|
||||
const selectedPlaylistEntries = useMemo(() => {
|
||||
if (!playlistInfo) {
|
||||
return []
|
||||
}
|
||||
const range = computePlaylistRange(playlistInfo)
|
||||
const previewEnd = range.end ?? playlistInfo.entryCount
|
||||
return playlistInfo.entries.filter(
|
||||
(entry) => entry.index >= range.start && entry.index <= previewEnd
|
||||
)
|
||||
}, [playlistInfo, computePlaylistRange])
|
||||
|
||||
const syncHistoryItem = useCallback(
|
||||
async (id: string) => {
|
||||
@@ -366,70 +398,131 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
|
||||
// Playlist handlers
|
||||
const handlePastePlaylistUrl = useCallback(async () => {
|
||||
if (playlistBusy) return
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (!text.trim()) {
|
||||
toast.error(t('errors.clipboardEmpty'))
|
||||
return
|
||||
}
|
||||
setPlaylistUrl(text.trim())
|
||||
const trimmed = text.trim()
|
||||
setPlaylistUrl(trimmed)
|
||||
setPlaylistInfo(null)
|
||||
setPlaylistPreviewError(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to paste URL:', error)
|
||||
toast.error(t('errors.pasteFromClipboard'))
|
||||
}
|
||||
}, [t])
|
||||
}, [playlistBusy, t])
|
||||
|
||||
const handleDownloadPlaylist = useCallback(async () => {
|
||||
const handleClearPlaylistPreview = useCallback(() => {
|
||||
setPlaylistInfo(null)
|
||||
setPlaylistPreviewError(null)
|
||||
}, [])
|
||||
|
||||
const handlePreviewPlaylist = useCallback(async () => {
|
||||
if (!playlistUrl.trim()) {
|
||||
toast.error(t('errors.emptyUrl'))
|
||||
return
|
||||
}
|
||||
|
||||
setPlaylistLoading(true)
|
||||
setPlaylistPreviewError(null)
|
||||
setPlaylistPreviewLoading(true)
|
||||
try {
|
||||
// Get playlist info first to show user what will be downloaded
|
||||
const playlistInfo = await ipcServices.download.getPlaylistInfo(playlistUrl)
|
||||
const trimmedUrl = playlistUrl.trim()
|
||||
const info = await ipcServices.download.getPlaylistInfo(trimmedUrl)
|
||||
setPlaylistInfo(info)
|
||||
if (info.entryCount === 0) {
|
||||
toast.error(t('playlist.noEntries'))
|
||||
return
|
||||
}
|
||||
toast.success(t('playlist.foundVideos', { count: info.entryCount }))
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch playlist info:', error)
|
||||
const message =
|
||||
error instanceof Error && error.message ? error.message : t('playlist.previewFailed')
|
||||
setPlaylistPreviewError(message)
|
||||
setPlaylistInfo(null)
|
||||
toast.error(t('playlist.previewFailed'))
|
||||
} finally {
|
||||
setPlaylistPreviewLoading(false)
|
||||
}
|
||||
}, [playlistUrl, t])
|
||||
|
||||
toast.success(t('playlist.foundVideos', { count: playlistInfo.entryCount }))
|
||||
const handleDownloadPlaylist = useCallback(async () => {
|
||||
const trimmedUrl = playlistUrl.trim()
|
||||
if (!trimmedUrl) {
|
||||
toast.error(t('errors.emptyUrl'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!playlistInfo) {
|
||||
toast.error(t('playlist.previewRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
setPlaylistPreviewError(null)
|
||||
setPlaylistDownloadLoading(true)
|
||||
try {
|
||||
const info = playlistInfo
|
||||
setPlaylistInfo(info)
|
||||
|
||||
if (info.entryCount === 0) {
|
||||
toast.error(t('playlist.noEntries'))
|
||||
return
|
||||
}
|
||||
|
||||
const range = computePlaylistRange(info)
|
||||
const previewEnd = range.end ?? info.entryCount
|
||||
|
||||
if (previewEnd < range.start || previewEnd === 0) {
|
||||
toast.error(t('playlist.noEntriesInRange'))
|
||||
return
|
||||
}
|
||||
|
||||
// Build format preference based on settings
|
||||
const format =
|
||||
downloadType === 'video'
|
||||
? buildVideoFormatPreference(settings)
|
||||
: buildAudioFormatPreference(settings)
|
||||
|
||||
// Start playlist download
|
||||
const downloadIds = await ipcServices.download.startPlaylistDownload({
|
||||
url: playlistUrl.trim(),
|
||||
const result = await ipcServices.download.startPlaylistDownload({
|
||||
url: trimmedUrl,
|
||||
type: downloadType,
|
||||
format,
|
||||
startIndex: parseInt(startIndex, 10) || 1,
|
||||
endIndex: endIndex ? parseInt(endIndex, 10) : undefined
|
||||
startIndex: range.start,
|
||||
endIndex: range.end
|
||||
})
|
||||
|
||||
// Add all downloads to the renderer state
|
||||
for (const id of downloadIds) {
|
||||
if (result.totalCount === 0) {
|
||||
toast.error(t('playlist.noEntriesInRange'))
|
||||
return
|
||||
}
|
||||
|
||||
const baseCreatedAt = Date.now()
|
||||
result.entries.forEach((entry, index) => {
|
||||
const downloadItem = {
|
||||
id,
|
||||
url: playlistUrl.trim(),
|
||||
title: t('download.fetchingVideoInfo'),
|
||||
id: entry.downloadId,
|
||||
url: entry.url,
|
||||
title: entry.title || t('download.fetchingVideoInfo'),
|
||||
type: downloadType,
|
||||
status: 'pending' as const,
|
||||
progress: { percent: 0 },
|
||||
createdAt: Date.now()
|
||||
createdAt: baseCreatedAt + index,
|
||||
playlistId: result.groupId,
|
||||
playlistTitle: result.playlistTitle,
|
||||
playlistIndex: entry.index,
|
||||
playlistSize: result.totalCount
|
||||
}
|
||||
addDownload(downloadItem)
|
||||
}
|
||||
})
|
||||
|
||||
toast.success(t('playlist.downloadStarted', { count: downloadIds.length }))
|
||||
setPlaylistUrl('') // Clear the URL after starting download
|
||||
toast.success(t('playlist.downloadStarted', { count: result.totalCount }))
|
||||
} catch (error) {
|
||||
console.error('Failed to start playlist download:', error)
|
||||
toast.error(t('playlist.downloadFailed'))
|
||||
} finally {
|
||||
setPlaylistLoading(false)
|
||||
setPlaylistDownloadLoading(false)
|
||||
}
|
||||
}, [playlistUrl, downloadType, startIndex, endIndex, settings, addDownload, t])
|
||||
}, [playlistUrl, playlistInfo, computePlaylistRange, downloadType, settings, addDownload, t])
|
||||
|
||||
// Auto-focus input on mount
|
||||
useEffect(() => {
|
||||
@@ -442,7 +535,7 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
style={{ maxWidth: '100%' }}
|
||||
>
|
||||
<Tabs defaultValue="single" className="w-full">
|
||||
{/* <TabsList className="grid w-full grid-cols-2">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="single" className="flex items-center gap-2">
|
||||
<Download className="h-4 w-4" />
|
||||
{t('download.singleVideo')}
|
||||
@@ -451,7 +544,7 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
<ListVideo className="h-4 w-4" />
|
||||
{t('playlist.title')}
|
||||
</TabsTrigger>
|
||||
</TabsList> */}
|
||||
</TabsList>
|
||||
|
||||
{/* Single Video Download Tab */}
|
||||
<TabsContent value="single" className="space-y-6">
|
||||
@@ -580,14 +673,18 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
id={playlistUrlId}
|
||||
placeholder="https://www.youtube.com/playlist?list=..."
|
||||
value={playlistUrl}
|
||||
onChange={(e) => setPlaylistUrl(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setPlaylistUrl(e.target.value)
|
||||
setPlaylistInfo(null)
|
||||
setPlaylistPreviewError(null)
|
||||
}}
|
||||
className="flex-1"
|
||||
disabled={playlistLoading}
|
||||
disabled={playlistBusy}
|
||||
/>
|
||||
<Button
|
||||
onClick={handlePastePlaylistUrl}
|
||||
variant="outline"
|
||||
disabled={playlistLoading}
|
||||
disabled={playlistBusy}
|
||||
>
|
||||
{t('download.paste')}
|
||||
</Button>
|
||||
@@ -600,7 +697,7 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
<Select
|
||||
value={downloadType}
|
||||
onValueChange={(v) => setDownloadType(v as 'video' | 'audio')}
|
||||
disabled={playlistLoading}
|
||||
disabled={playlistBusy}
|
||||
>
|
||||
<SelectTrigger id={downloadTypeId}>
|
||||
<SelectValue />
|
||||
@@ -621,7 +718,7 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
value={startIndex}
|
||||
onChange={(e) => setStartIndex(e.target.value)}
|
||||
min="1"
|
||||
disabled={playlistLoading}
|
||||
disabled={playlistBusy}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
@@ -629,29 +726,68 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
||||
value={endIndex}
|
||||
onChange={(e) => setEndIndex(e.target.value)}
|
||||
min="1"
|
||||
disabled={playlistLoading}
|
||||
disabled={playlistBusy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleDownloadPlaylist}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={playlistLoading || !playlistUrl.trim()}
|
||||
>
|
||||
{playlistLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
{t('download.loading')}
|
||||
</>
|
||||
) : (
|
||||
t('playlist.downloadPlaylist')
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
onClick={handlePreviewPlaylist}
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
disabled={playlistBusy || !playlistUrl.trim()}
|
||||
>
|
||||
{playlistPreviewLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
{t('download.loading')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="mr-2 h-5 w-5" />
|
||||
{t('playlist.previewButton')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{playlistInfo && (
|
||||
<Button
|
||||
onClick={handleDownloadPlaylist}
|
||||
className="w-full sm:flex-1"
|
||||
size="lg"
|
||||
disabled={playlistDownloadLoading || !playlistUrl.trim()}
|
||||
>
|
||||
{playlistDownloadLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
{t('download.loading')}
|
||||
</>
|
||||
) : (
|
||||
t('playlist.downloadPlaylist')
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{playlistUrl.trim() && !playlistInfo && !playlistPreviewError && !playlistBusy && (
|
||||
<p className="text-xs text-muted-foreground">{t('playlist.previewRequired')}</p>
|
||||
)}
|
||||
|
||||
{playlistPreviewError && (
|
||||
<div className="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{playlistPreviewError}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{playlistInfo && (
|
||||
<PlaylistPreviewCard
|
||||
playlist={playlistInfo}
|
||||
entries={selectedPlaylistEntries}
|
||||
onClear={handleClearPlaylistPreview}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/u
|
||||
import type { OneClickQualityPreset } from '@shared/types'
|
||||
import { useAtom, useSetAtom } from 'jotai'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { loadSettingsAtom, saveSettingAtom, settingsAtom } from '../store/settings'
|
||||
@@ -32,11 +32,26 @@ export function Settings() {
|
||||
const [settings, _setSettings] = useAtom(settingsAtom)
|
||||
const loadSettings = useSetAtom(loadSettingsAtom)
|
||||
const saveSetting = useSetAtom(saveSettingAtom)
|
||||
const [platform, setPlatform] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
}, [loadSettings])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlatform = async () => {
|
||||
try {
|
||||
const { ipcServices } = await import('../lib/ipc')
|
||||
const platformInfo = await ipcServices.app.getPlatform()
|
||||
setPlatform(platformInfo)
|
||||
} catch (error) {
|
||||
console.error('Failed to get platform info:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchPlatform()
|
||||
}, [])
|
||||
|
||||
const handleSettingChange = async (
|
||||
key: keyof typeof settings,
|
||||
value: (typeof settings)[keyof typeof settings]
|
||||
@@ -54,7 +69,7 @@ export function Settings() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select directory:', error)
|
||||
toast.error('Failed to select directory')
|
||||
toast.error(t('settings.directorySelectError'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +82,32 @@ export function Settings() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select file:', error)
|
||||
toast.error('Failed to select file')
|
||||
toast.error(t('settings.fileSelectError'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectCookiesFile = async () => {
|
||||
try {
|
||||
const { ipcServices } = await import('../lib/ipc')
|
||||
const path = await ipcServices.fs.selectFile()
|
||||
if (path) {
|
||||
await handleSettingChange('cookiesPath', path)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to select cookies file:', error)
|
||||
toast.error(t('settings.fileSelectError'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenCookiesFaq = async () => {
|
||||
try {
|
||||
const { ipcServices } = await import('../lib/ipc')
|
||||
await ipcServices.fs.openExternal(
|
||||
'https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp'
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to open cookies FAQ:', error)
|
||||
toast.error(t('settings.openLinkError'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +140,7 @@ export function Settings() {
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.downloadPath')}</ItemTitle>
|
||||
<ItemDescription>Choose where to save downloaded files</ItemDescription>
|
||||
<ItemDescription>{t('settings.downloadPathDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<div className="flex gap-2 w-full max-w-md">
|
||||
@@ -115,9 +155,7 @@ export function Settings() {
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.theme')}</ItemTitle>
|
||||
<ItemDescription>
|
||||
Choose a light, dark, or system theme for VidBee
|
||||
</ItemDescription>
|
||||
<ItemDescription>{t('settings.themeDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select
|
||||
@@ -160,8 +198,7 @@ export function Settings() {
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.oneClickDownloadType')}</ItemTitle>
|
||||
<ItemDescription>
|
||||
Choose the default download type for one-click downloads. Quality uses the
|
||||
preset below.
|
||||
{t('settings.oneClickDownloadTypeDescription')}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
@@ -185,9 +222,7 @@ export function Settings() {
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.oneClickQuality')}</ItemTitle>
|
||||
<ItemDescription>
|
||||
Select the quality preset used for one-click downloads
|
||||
</ItemDescription>
|
||||
<ItemDescription>{t('settings.oneClickQualityDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select
|
||||
@@ -230,9 +265,7 @@ export function Settings() {
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.showMoreFormats')}</ItemTitle>
|
||||
<ItemDescription>
|
||||
Display additional format options in the interface
|
||||
</ItemDescription>
|
||||
<ItemDescription>{t('settings.showMoreFormatsDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Switch
|
||||
@@ -245,11 +278,30 @@ export function Settings() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="advanced" className="space-y-4 mt-2">
|
||||
{platform === 'darwin' && (
|
||||
<ItemGroup>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.hideDockIcon')}</ItemTitle>
|
||||
<ItemDescription>{t('settings.hideDockIconDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Switch
|
||||
checked={settings.hideDockIcon}
|
||||
onCheckedChange={(value) => handleSettingChange('hideDockIcon', value)}
|
||||
/>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
)}
|
||||
|
||||
<ItemGroup>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.maxConcurrentDownloads')}</ItemTitle>
|
||||
<ItemDescription>Maximum number of simultaneous downloads</ItemDescription>
|
||||
<ItemDescription>
|
||||
{t('settings.maxConcurrentDownloadsDescription')}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select
|
||||
@@ -274,43 +326,14 @@ export function Settings() {
|
||||
|
||||
<ItemSeparator />
|
||||
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.browserForCookies')}</ItemTitle>
|
||||
<ItemDescription>
|
||||
Browser to extract cookies from for authentication
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select
|
||||
value={settings.browserForCookies}
|
||||
onValueChange={(value) => handleSettingChange('browserForCookies', value)}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{t('settings.none')}</SelectItem>
|
||||
<SelectItem value="chrome">Chrome</SelectItem>
|
||||
<SelectItem value="firefox">Firefox</SelectItem>
|
||||
<SelectItem value="edge">Edge</SelectItem>
|
||||
<SelectItem value="safari">Safari</SelectItem>
|
||||
<SelectItem value="brave">Brave</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
|
||||
<ItemSeparator />
|
||||
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.proxy')}</ItemTitle>
|
||||
<ItemDescription>Proxy server for network requests</ItemDescription>
|
||||
<ItemDescription>{t('settings.proxyDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Input
|
||||
placeholder="http://proxy:port"
|
||||
placeholder={t('settings.proxyPlaceholder')}
|
||||
value={settings.proxy}
|
||||
onChange={(e) => handleSettingChange('proxy', e.target.value)}
|
||||
className="w-64"
|
||||
@@ -323,7 +346,7 @@ export function Settings() {
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.configFile')}</ItemTitle>
|
||||
<ItemDescription>Custom configuration file for yt-dlp</ItemDescription>
|
||||
<ItemDescription>{t('settings.configFileDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<div className="flex gap-2 w-full max-w-md">
|
||||
@@ -333,6 +356,76 @@ export function Settings() {
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.browserForCookies')}</ItemTitle>
|
||||
<ItemDescription>{t('settings.browserForCookiesDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Select
|
||||
value={settings.browserForCookies}
|
||||
onValueChange={(value) => handleSettingChange('browserForCookies', value)}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{t('settings.none')}</SelectItem>
|
||||
<SelectItem value="chrome">{t('settings.browserOptions.chrome')}</SelectItem>
|
||||
<SelectItem value="firefox">
|
||||
{t('settings.browserOptions.firefox')}
|
||||
</SelectItem>
|
||||
<SelectItem value="edge">{t('settings.browserOptions.edge')}</SelectItem>
|
||||
<SelectItem value="safari">{t('settings.browserOptions.safari')}</SelectItem>
|
||||
<SelectItem value="brave">{t('settings.browserOptions.brave')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
|
||||
<ItemSeparator />
|
||||
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.cookiesFile')}</ItemTitle>
|
||||
<ItemDescription>{t('settings.cookiesFileDescription')}</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<div className="flex gap-2 w-full max-w-md">
|
||||
<Input value={settings.cookiesPath ?? ''} readOnly className="flex-1" />
|
||||
<Button onClick={handleSelectCookiesFile}>{t('settings.selectPath')}</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => void handleSettingChange('cookiesPath', '')}
|
||||
disabled={!settings.cookiesPath}
|
||||
>
|
||||
{t('settings.clearCookiesFile')}
|
||||
</Button>
|
||||
</div>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
|
||||
<ItemSeparator />
|
||||
|
||||
<Item variant="muted">
|
||||
<ItemContent>
|
||||
<ItemTitle>{t('settings.cookiesHelpTitle')}</ItemTitle>
|
||||
<ItemDescription>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>{t('settings.cookiesHelpBrowser')}</li>
|
||||
<li>{t('settings.cookiesHelpFile')}</li>
|
||||
</ul>
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Button variant="link" className="px-0" onClick={handleOpenCookiesFaq}>
|
||||
{t('settings.cookiesHelpFaq')}
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -29,14 +29,22 @@ export function SupportedSites() {
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-lg font-semibold px-6">{t('sites.popularSection')}</h2>
|
||||
<ul className="grid gap-3 sm:grid-cols-2">
|
||||
{popularSites.map((site) => (
|
||||
<li key={site.id} className="rounded-md border border-border px-6 py-5">
|
||||
<p className="text-sm font-medium">{site.label}</p>
|
||||
{site.description ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{site.description}</p>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
{popularSites.map((site) => {
|
||||
const labelKey = `sites.popular.${site.id}.label`
|
||||
const descriptionKey = `sites.popular.${site.id}.description`
|
||||
const label = t(labelKey)
|
||||
const description = t(descriptionKey)
|
||||
const hasDescription = description !== descriptionKey
|
||||
|
||||
return (
|
||||
<li key={site.id} className="rounded-md border border-border px-6 py-5">
|
||||
<p className="text-sm font-medium">{label}</p>
|
||||
{hasDescription ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ const toHistoryRecord = (item: DownloadHistoryItem): DownloadRecord => ({
|
||||
viewCount: item.viewCount,
|
||||
tags: item.tags,
|
||||
selectedFormat: item.selectedFormat,
|
||||
playlistId: item.playlistId,
|
||||
playlistTitle: item.playlistTitle,
|
||||
playlistIndex: item.playlistIndex,
|
||||
playlistSize: item.playlistSize,
|
||||
entryType: 'history',
|
||||
downloadedAt: item.downloadedAt
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { atom } from 'jotai'
|
||||
import { normalizeLanguageCode } from '../../../shared/languages'
|
||||
import type { AppSettings } from '../../../shared/types'
|
||||
import { defaultSettings } from '../../../shared/types'
|
||||
import i18n from '../i18n'
|
||||
import { ipcServices } from '../lib/ipc'
|
||||
|
||||
// Settings atom
|
||||
@@ -10,7 +12,18 @@ export const settingsAtom = atom<AppSettings>(defaultSettings)
|
||||
export const loadSettingsAtom = atom(null, async (_get, set) => {
|
||||
try {
|
||||
const settings = await ipcServices.settings.getAll()
|
||||
set(settingsAtom, settings)
|
||||
const savedLanguage = normalizeLanguageCode(settings.language)
|
||||
const currentLanguage = normalizeLanguageCode(i18n.language)
|
||||
|
||||
if (currentLanguage !== savedLanguage) {
|
||||
try {
|
||||
await i18n.changeLanguage(savedLanguage)
|
||||
} catch (error) {
|
||||
console.error('Failed to apply saved language:', error)
|
||||
}
|
||||
}
|
||||
|
||||
set(settingsAtom, { ...settings, language: savedLanguage })
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error)
|
||||
}
|
||||
|
||||
105
src/shared/languages.ts
Normal file
105
src/shared/languages.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
export interface LanguageDefinition {
|
||||
flag: string
|
||||
name: string
|
||||
hreflang: string
|
||||
}
|
||||
|
||||
export const languages = {
|
||||
en: {
|
||||
flag: 'fi fi-us',
|
||||
name: 'English',
|
||||
hreflang: 'en'
|
||||
},
|
||||
es: {
|
||||
flag: 'fi fi-es',
|
||||
name: 'Español',
|
||||
hreflang: 'es'
|
||||
},
|
||||
ar: {
|
||||
flag: 'fi fi-sa',
|
||||
name: 'العربية',
|
||||
hreflang: 'ar'
|
||||
},
|
||||
id: {
|
||||
flag: 'fi fi-id',
|
||||
name: 'Bahasa Indonesia',
|
||||
hreflang: 'id'
|
||||
},
|
||||
pt: {
|
||||
flag: 'fi fi-br',
|
||||
name: 'Português',
|
||||
hreflang: 'pt-BR'
|
||||
},
|
||||
fr: {
|
||||
flag: 'fi fi-fr',
|
||||
name: 'Français',
|
||||
hreflang: 'fr'
|
||||
},
|
||||
it: {
|
||||
flag: 'fi fi-it',
|
||||
name: 'Italiano',
|
||||
hreflang: 'it'
|
||||
},
|
||||
zh: {
|
||||
flag: 'fi fi-cn',
|
||||
name: '中文',
|
||||
hreflang: 'zh-CN'
|
||||
},
|
||||
'zh-TW': {
|
||||
flag: 'fi fi-tw',
|
||||
name: '繁體中文',
|
||||
hreflang: 'zh-TW'
|
||||
},
|
||||
ko: {
|
||||
flag: 'fi fi-kr',
|
||||
name: '한국어',
|
||||
hreflang: 'ko'
|
||||
},
|
||||
ja: {
|
||||
flag: 'fi fi-jp',
|
||||
name: '日本語',
|
||||
hreflang: 'ja'
|
||||
},
|
||||
ru: {
|
||||
flag: 'fi fi-ru',
|
||||
name: 'Русский',
|
||||
hreflang: 'ru'
|
||||
},
|
||||
de: {
|
||||
flag: 'fi fi-de',
|
||||
name: 'Deutsch',
|
||||
hreflang: 'de'
|
||||
}
|
||||
} as const satisfies Record<string, LanguageDefinition>
|
||||
|
||||
export type LanguageCode = keyof typeof languages
|
||||
|
||||
export const defaultLanguageCode: LanguageCode = 'en'
|
||||
|
||||
export const languageList = Object.entries(languages).map(([code, definition]) => ({
|
||||
value: code as LanguageCode,
|
||||
...definition
|
||||
}))
|
||||
|
||||
export const supportedLanguageCodes = languageList.map((language) => language.value)
|
||||
|
||||
export function normalizeLanguageCode(code: string | null | undefined): LanguageCode {
|
||||
if (!code) {
|
||||
return defaultLanguageCode
|
||||
}
|
||||
|
||||
const normalizedInput = code.toLowerCase()
|
||||
const directMatch = supportedLanguageCodes.find(
|
||||
(languageCode) => languageCode.toLowerCase() === normalizedInput
|
||||
)
|
||||
if (directMatch) {
|
||||
return directMatch
|
||||
}
|
||||
|
||||
const base = normalizedInput.split('-')[0] ?? ''
|
||||
const baseMatch = supportedLanguageCodes.find(
|
||||
(languageCode) => languageCode.split('-')[0]?.toLowerCase() === base
|
||||
)
|
||||
|
||||
return baseMatch ?? defaultLanguageCode
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { LanguageCode } from '../languages'
|
||||
import { defaultLanguageCode } from '../languages'
|
||||
|
||||
// Download related types
|
||||
export interface VideoFormat {
|
||||
format_id: string
|
||||
@@ -73,6 +76,11 @@ export interface DownloadItem {
|
||||
tags?: string[]
|
||||
// Download-specific format info
|
||||
selectedFormat?: VideoFormat
|
||||
// Playlist context (optional)
|
||||
playlistId?: string
|
||||
playlistTitle?: string
|
||||
playlistIndex?: number
|
||||
playlistSize?: number
|
||||
}
|
||||
|
||||
export interface DownloadHistoryItem {
|
||||
@@ -100,6 +108,11 @@ export interface DownloadHistoryItem {
|
||||
tags?: string[]
|
||||
// Download-specific format info
|
||||
selectedFormat?: VideoFormat
|
||||
// Playlist context (optional)
|
||||
playlistId?: string
|
||||
playlistTitle?: string
|
||||
playlistIndex?: number
|
||||
playlistSize?: number
|
||||
}
|
||||
|
||||
export interface DownloadOptions {
|
||||
@@ -114,14 +127,17 @@ export interface DownloadOptions {
|
||||
downloadSubs?: boolean
|
||||
}
|
||||
|
||||
export interface PlaylistEntry {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface PlaylistInfo {
|
||||
id: string
|
||||
title: string
|
||||
entries: Array<{
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
}>
|
||||
entries: PlaylistEntry[]
|
||||
entryCount: number
|
||||
}
|
||||
|
||||
@@ -135,6 +151,25 @@ export interface PlaylistDownloadOptions {
|
||||
folderFormat?: string
|
||||
}
|
||||
|
||||
export interface PlaylistDownloadEntry {
|
||||
downloadId: string
|
||||
entryId: string
|
||||
title: string
|
||||
url: string
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface PlaylistDownloadResult {
|
||||
groupId: string
|
||||
playlistId: string
|
||||
playlistTitle: string
|
||||
type: 'video' | 'audio'
|
||||
totalCount: number
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
entries: PlaylistDownloadEntry[]
|
||||
}
|
||||
|
||||
// Settings types
|
||||
export type OneClickQualityPreset = 'auto' | 'best' | 'good' | 'normal' | 'bad' | 'worst'
|
||||
|
||||
@@ -143,15 +178,17 @@ export interface AppSettings {
|
||||
showMoreFormats: boolean
|
||||
maxConcurrentDownloads: number
|
||||
browserForCookies: string
|
||||
cookiesPath: string
|
||||
proxy: string
|
||||
configPath: string
|
||||
betaProgram: boolean
|
||||
language: string
|
||||
language: LanguageCode
|
||||
theme: string
|
||||
oneClickDownload: boolean
|
||||
oneClickDownloadType: 'video' | 'audio'
|
||||
oneClickQuality: OneClickQualityPreset
|
||||
closeToTray: boolean
|
||||
hideDockIcon: boolean
|
||||
autoUpdate: boolean
|
||||
}
|
||||
|
||||
@@ -160,14 +197,16 @@ export const defaultSettings: AppSettings = {
|
||||
showMoreFormats: false,
|
||||
maxConcurrentDownloads: 5,
|
||||
browserForCookies: 'none',
|
||||
cookiesPath: '',
|
||||
proxy: '',
|
||||
configPath: '',
|
||||
betaProgram: false,
|
||||
language: 'en',
|
||||
language: defaultLanguageCode,
|
||||
theme: 'system',
|
||||
oneClickDownload: false,
|
||||
oneClickDownloadType: 'video',
|
||||
oneClickQuality: 'auto',
|
||||
closeToTray: false,
|
||||
hideDockIcon: false,
|
||||
autoUpdate: true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user