Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e22fe7679a | ||
|
|
7c2e526c49 | ||
|
|
2f5776d1e0 | ||
|
|
7901fc7e82 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vidbee",
|
"name": "vidbee",
|
||||||
"version": "0.3.4",
|
"version": "0.3.5",
|
||||||
"description": "A modern Electron application for downloading videos and audios",
|
"description": "A modern Electron application for downloading videos and audios",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "VidBee",
|
"author": "VidBee",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type {
|
|||||||
} from '../../shared/types'
|
} from '../../shared/types'
|
||||||
|
|
||||||
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
||||||
auto: null,
|
|
||||||
best: null,
|
best: null,
|
||||||
good: 1080,
|
good: 1080,
|
||||||
normal: 720,
|
normal: 720,
|
||||||
@@ -15,7 +14,6 @@ const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const qualityPresetToAudioAbr: Record<OneClickQualityPreset, number | null> = {
|
const qualityPresetToAudioAbr: Record<OneClickQualityPreset, number | null> = {
|
||||||
auto: null,
|
|
||||||
best: 320,
|
best: 320,
|
||||||
good: 256,
|
good: 256,
|
||||||
normal: 192,
|
normal: 192,
|
||||||
@@ -187,7 +185,7 @@ export const resolveSelectedFormat = (
|
|||||||
return directMatch
|
return directMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
const preset = settings.oneClickQuality ?? 'auto'
|
const preset = settings.oneClickQuality ?? 'best'
|
||||||
|
|
||||||
if (options.type === 'video') {
|
if (options.type === 'video') {
|
||||||
const videoFormats = formats.filter(
|
const videoFormats = formats.filter(
|
||||||
|
|||||||
@@ -229,6 +229,25 @@ class FileSystemService extends IpcService {
|
|||||||
.replace(/'/g, ''')
|
.replace(/'/g, ''')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IpcMethod()
|
||||||
|
async fileExists(_context: IpcContext, filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!filePath) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedPath = this.sanitizePath(filePath)
|
||||||
|
const normalizedPath = path.normalize(sanitizedPath)
|
||||||
|
|
||||||
|
const stats = await fs.stat(normalizedPath).catch(() => null)
|
||||||
|
|
||||||
|
return stats?.isFile() ?? false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check file existence:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@IpcMethod()
|
@IpcMethod()
|
||||||
async deleteFile(_context: IpcContext, filePath: string): Promise<boolean> {
|
async deleteFile(_context: IpcContext, filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -153,7 +153,12 @@ function AppContent() {
|
|||||||
const renderPage = () => {
|
const renderPage = () => {
|
||||||
switch (currentPage) {
|
switch (currentPage) {
|
||||||
case 'home':
|
case 'home':
|
||||||
return <Home onOpenSupportedSites={() => setCurrentPage('sites')} />
|
return (
|
||||||
|
<Home
|
||||||
|
onOpenSupportedSites={() => setCurrentPage('sites')}
|
||||||
|
onOpenSettings={() => setCurrentPage('settings')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return <Settings />
|
return <Settings />
|
||||||
case 'about':
|
case 'about':
|
||||||
@@ -161,7 +166,12 @@ function AppContent() {
|
|||||||
case 'sites':
|
case 'sites':
|
||||||
return <SupportedSites />
|
return <SupportedSites />
|
||||||
default:
|
default:
|
||||||
return <Home onOpenSupportedSites={() => setCurrentPage('sites')} />
|
return (
|
||||||
|
<Home
|
||||||
|
onOpenSupportedSites={() => setCurrentPage('sites')}
|
||||||
|
onOpenSettings={() => setCurrentPage('settings')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,32 +60,32 @@
|
|||||||
--card-foreground: oklch(0.8853 0 0);
|
--card-foreground: oklch(0.8853 0 0);
|
||||||
--popover: oklch(0 0 0);
|
--popover: oklch(0 0 0);
|
||||||
--popover-foreground: oklch(0.9328 0.0025 228.7857);
|
--popover-foreground: oklch(0.9328 0.0025 228.7857);
|
||||||
--primary: oklch(0.6692 0.1607 245.0110);
|
--primary: oklch(0.8223 0.1704 79.8747);
|
||||||
--primary-foreground: oklch(1.0000 0 0);
|
--primary-foreground: oklch(1.0000 0 0);
|
||||||
--secondary: oklch(0.9622 0.0035 219.5331);
|
--secondary: oklch(0.9622 0.0035 219.5331);
|
||||||
--secondary-foreground: oklch(0.1884 0.0128 248.5103);
|
--secondary-foreground: oklch(0.1884 0.0128 248.5103);
|
||||||
--muted: oklch(0.2090 0 0);
|
--muted: oklch(0.3485 0 0);
|
||||||
--muted-foreground: oklch(0.5637 0.0078 247.9662);
|
--muted-foreground: oklch(0.5637 0.0078 247.9662);
|
||||||
--accent: oklch(0.1928 0.0331 242.5459);
|
--accent: oklch(0.1928 0.0331 242.5459);
|
||||||
--accent-foreground: oklch(0.6692 0.1607 245.0110);
|
--accent-foreground: oklch(0.8223 0.1704 79.8747);
|
||||||
--destructive: oklch(0.6188 0.2376 25.7658);
|
--destructive: oklch(0.6188 0.2376 25.7658);
|
||||||
--destructive-foreground: oklch(1.0000 0 0);
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
--border: oklch(0.2674 0.0047 248.0045);
|
--border: oklch(0.2674 0.0047 248.0045);
|
||||||
--input: oklch(0.3020 0.0288 244.8244);
|
--input: oklch(0.3020 0.0288 244.8244);
|
||||||
--ring: oklch(0.6818 0.1584 243.3540);
|
--ring: oklch(0.8223 0.1704 79.8747);
|
||||||
--chart-1: oklch(0.6723 0.1606 244.9955);
|
--chart-1: oklch(0.8223 0.1704 79.8747);
|
||||||
--chart-2: oklch(0.6907 0.1554 160.3454);
|
--chart-2: oklch(0.6907 0.1554 160.3454);
|
||||||
--chart-3: oklch(0.8214 0.1600 82.5337);
|
--chart-3: oklch(0.8214 0.1600 82.5337);
|
||||||
--chart-4: oklch(0.7064 0.1822 151.7125);
|
--chart-4: oklch(0.7064 0.1822 151.7125);
|
||||||
--chart-5: oklch(0.5919 0.2186 10.5826);
|
--chart-5: oklch(0.5919 0.2186 10.5826);
|
||||||
--sidebar: oklch(0.2097 0.0080 274.5332);
|
--sidebar: oklch(0.2097 0.0080 274.5332);
|
||||||
--sidebar-foreground: oklch(0.8853 0 0);
|
--sidebar-foreground: oklch(0.8853 0 0);
|
||||||
--sidebar-primary: oklch(0.6818 0.1584 243.3540);
|
--sidebar-primary: oklch(0.8223 0.1704 79.8747);
|
||||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
--sidebar-accent: oklch(0.1928 0.0331 242.5459);
|
--sidebar-accent: oklch(0.1928 0.0331 242.5459);
|
||||||
--sidebar-accent-foreground: oklch(0.6692 0.1607 245.0110);
|
--sidebar-accent-foreground: oklch(0.8223 0.1704 79.8747);
|
||||||
--sidebar-border: oklch(0.3795 0.0220 240.5943);
|
--sidebar-border: oklch(0.3795 0.0220 240.5943);
|
||||||
--sidebar-ring: oklch(0.6818 0.1584 243.3540);
|
--sidebar-ring: oklch(0.8223 0.1704 79.8747);
|
||||||
--font-sans: Open Sans, sans-serif;
|
--font-sans: Open Sans, sans-serif;
|
||||||
--font-serif: Georgia, serif;
|
--font-serif: Georgia, serif;
|
||||||
--font-mono: Menlo, monospace;
|
--font-mono: Menlo, monospace;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Progress } from '@renderer/components/ui/progress'
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { AlertCircle, CheckCircle2, Copy, FolderOpen, Loader2, Play, Trash2, X } from 'lucide-react'
|
import { AlertCircle, CheckCircle2, Copy, FolderOpen, Loader2, Play, Trash2, X } from 'lucide-react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useCachedThumbnail } from '../../hooks/use-cached-thumbnail'
|
import { useCachedThumbnail } from '../../hooks/use-cached-thumbnail'
|
||||||
@@ -67,6 +68,30 @@ export function DownloadItem({ download }: DownloadItemProps) {
|
|||||||
? actionsContainerBaseClass
|
? actionsContainerBaseClass
|
||||||
: `${actionsContainerBaseClass} sm:opacity-0 sm:group-hover:opacity-100`
|
: `${actionsContainerBaseClass} sm:opacity-0 sm:group-hover:opacity-100`
|
||||||
|
|
||||||
|
// Track if the file exists
|
||||||
|
const [fileExists, setFileExists] = useState(false)
|
||||||
|
|
||||||
|
// Check if file exists when download data changes
|
||||||
|
useEffect(() => {
|
||||||
|
const checkFileExists = async () => {
|
||||||
|
if (!download.title || !download.downloadPath || !download.format) {
|
||||||
|
setFileExists(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = generateFilePath(download.downloadPath, download.title, download.format)
|
||||||
|
const exists = await ipcServices.fs.fileExists(filePath)
|
||||||
|
setFileExists(exists)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check file existence:', error)
|
||||||
|
setFileExists(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkFileExists()
|
||||||
|
}, [download.title, download.downloadPath, download.format])
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
if (isHistory) return
|
if (isHistory) return
|
||||||
try {
|
try {
|
||||||
@@ -93,18 +118,31 @@ export function DownloadItem({ download }: DownloadItemProps) {
|
|||||||
toast.error(t('notifications.openFolderFailed'))
|
toast.error(t('notifications.openFolderFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Check if copy to clipboard is available
|
||||||
|
const canCopyToClipboard = () => {
|
||||||
|
return !!(download.title && download.downloadPath && download.format && fileExists)
|
||||||
|
}
|
||||||
|
|
||||||
// need title, downloadPath, format
|
// need title, downloadPath, format
|
||||||
const handleCopyToClipboard = async () => {
|
const handleCopyToClipboard = async () => {
|
||||||
if (!download.title || !download.downloadPath || !download.format) {
|
if (!canCopyToClipboard()) {
|
||||||
|
toast.error(t('notifications.copyFailed'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard: these values are guaranteed to exist after canCopyToClipboard() check
|
||||||
|
const downloadPath = download.downloadPath
|
||||||
|
const format = download.format
|
||||||
|
const title = download.title
|
||||||
|
|
||||||
|
if (!downloadPath || !format || !title) {
|
||||||
toast.error(t('notifications.copyFailed'))
|
toast.error(t('notifications.copyFailed'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate file path using downloadPath + title + ext
|
// Generate file path using downloadPath + title + ext
|
||||||
const downloadPath = download.downloadPath
|
const filePath = generateFilePath(downloadPath, title, format)
|
||||||
const format = download.format
|
|
||||||
const filePath = generateFilePath(downloadPath, download.title, format)
|
|
||||||
|
|
||||||
const success = await ipcServices.fs.copyFileToClipboard(filePath)
|
const success = await ipcServices.fs.copyFileToClipboard(filePath)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -320,7 +358,7 @@ export function DownloadItem({ download }: DownloadItemProps) {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8 shrink-0"
|
||||||
onClick={handleCopyToClipboard}
|
onClick={handleCopyToClipboard}
|
||||||
disabled={!download.title || !download.downloadPath || !download.format}
|
disabled={!canCopyToClipboard()}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -373,6 +411,7 @@ export function DownloadItem({ download }: DownloadItemProps) {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8 shrink-0"
|
||||||
onClick={handleCopyToClipboard}
|
onClick={handleCopyToClipboard}
|
||||||
|
disabled={!canCopyToClipboard()}
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button } from '@renderer/components/ui/button'
|
import { Button } from '@renderer/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@renderer/components/ui/card'
|
import { CardContent, CardHeader } from '@renderer/components/ui/card'
|
||||||
import { cn } from '@renderer/lib/utils'
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
import { History as HistoryIcon } from 'lucide-react'
|
import { History as HistoryIcon } from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useHistorySync } from '../../hooks/use-history-sync'
|
import { useHistorySync } from '../../hooks/use-history-sync'
|
||||||
import type { DownloadRecord } from '../../store/downloads'
|
import type { DownloadRecord } from '../../store/downloads'
|
||||||
import { clearCompletedAtom, downloadStatsAtom, downloadsArrayAtom } from '../../store/downloads'
|
import { downloadStatsAtom, downloadsArrayAtom } from '../../store/downloads'
|
||||||
import { DownloadItem } from './DownloadItem'
|
import { DownloadItem } from './DownloadItem'
|
||||||
import { PlaylistDownloadGroup } from './PlaylistDownloadGroup'
|
import { PlaylistDownloadGroup } from './PlaylistDownloadGroup'
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@ export function UnifiedDownloadHistory() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const allRecords = useAtomValue(downloadsArrayAtom)
|
const allRecords = useAtomValue(downloadsArrayAtom)
|
||||||
const downloadStats = useAtomValue(downloadStatsAtom)
|
const downloadStats = useAtomValue(downloadStatsAtom)
|
||||||
const clearCompleted = useSetAtom(clearCompletedAtom)
|
|
||||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
|
||||||
|
|
||||||
useHistorySync()
|
useHistorySync()
|
||||||
@@ -99,33 +98,9 @@ export function UnifiedDownloadHistory() {
|
|||||||
return { order, groups }
|
return { order, groups }
|
||||||
}, [filteredRecords])
|
}, [filteredRecords])
|
||||||
|
|
||||||
const hasCompletedActive = allRecords.some(
|
|
||||||
(item) => item.entryType === 'active' && item.status === 'completed'
|
|
||||||
)
|
|
||||||
const handleClearCompleted = () => {
|
|
||||||
clearCompleted()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border border-border/60 bg-background max-w-full shadow-sm backdrop-blur-sm">
|
<div className="space-y-4">
|
||||||
<CardHeader className="gap-4">
|
<CardHeader className="gap-4 p-0">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<CardTitle>{t('download.downloadQueue')}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
||||||
{hasCompletedActive && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-8 border border-border/60 px-3"
|
|
||||||
onClick={handleClearCompleted}
|
|
||||||
>
|
|
||||||
{t('download.clearCompleted')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||||
{filters.map((filter) => {
|
{filters.map((filter) => {
|
||||||
const isActive = statusFilter === filter.key
|
const isActive = statusFilter === filter.key
|
||||||
@@ -155,7 +130,7 @@ export function UnifiedDownloadHistory() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 overflow-hidden w-full">
|
<CardContent className="space-y-3 p-0 overflow-hidden w-full">
|
||||||
{filteredRecords.length === 0 ? (
|
{filteredRecords.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border/60 px-6 py-10 text-center text-muted-foreground">
|
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border/60 px-6 py-10 text-center text-muted-foreground">
|
||||||
<HistoryIcon className="h-10 w-10 opacity-50" />
|
<HistoryIcon className="h-10 w-10 opacity-50" />
|
||||||
@@ -191,6 +166,6 @@ export function UnifiedDownloadHistory() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import type { OneClickQualityPreset, VideoFormat } from '../../../../shared/types'
|
import type { OneClickQualityPreset, VideoFormat } from '../../../../shared/types'
|
||||||
|
|
||||||
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
||||||
auto: null,
|
|
||||||
best: null,
|
best: null,
|
||||||
good: 1080,
|
good: 1080,
|
||||||
normal: 720,
|
normal: 720,
|
||||||
|
|||||||
@@ -139,8 +139,10 @@
|
|||||||
"noAudio": "No Audio",
|
"noAudio": "No Audio",
|
||||||
"noHistory": "No download history",
|
"noHistory": "No download history",
|
||||||
"noItems": "No items found",
|
"noItems": "No items found",
|
||||||
|
"goToSettings": "Go to Settings",
|
||||||
"oneClickDownload": "One-Click Download",
|
"oneClickDownload": "One-Click Download",
|
||||||
"oneClickDownloadDescription": "Download directly with default settings without confirmation",
|
"oneClickDownloadDescription": "Download directly with default settings without confirmation",
|
||||||
|
"oneClickDownloadEnabled": "One-Click Download is enabled. Downloads will start directly with default settings.",
|
||||||
"oneClickDownloadNow": "Download Now",
|
"oneClickDownloadNow": "Download Now",
|
||||||
"oneClickDownloadStarted": "Download started with default settings",
|
"oneClickDownloadStarted": "Download started with default settings",
|
||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
|
|||||||
@@ -319,49 +319,49 @@ export function About() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{t('about.followAuthorTitle')}</CardTitle>
|
|
||||||
<CardDescription>{t('about.followAuthorDescription')}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
||||||
<p className="text-sm text-muted-foreground md:max-w-md">
|
|
||||||
{t('about.followAuthorSupport')}
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => openShareUrl('https://x.com/nexmoex')}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<Twitter className="h-4 w-4" />
|
|
||||||
{t('about.followAuthorActions.follow')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('about.shareTitle')}</CardTitle>
|
<CardTitle>{t('about.shareTitle')}</CardTitle>
|
||||||
<CardDescription>{t('about.shareDescription')}</CardDescription>
|
<CardDescription>{t('about.shareDescription')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-sm text-muted-foreground md:max-w-md">{t('about.shareSupport')}</p>
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
<div className="flex flex-wrap gap-2">
|
<p className="text-sm text-muted-foreground md:max-w-md">{t('about.shareSupport')}</p>
|
||||||
<Button variant="outline" size="sm" onClick={handleShareTwitter} className="gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Twitter className="h-4 w-4" />
|
<Button variant="outline" size="sm" onClick={handleShareTwitter} className="gap-2">
|
||||||
{t('about.shareActions.twitter')}
|
<Twitter className="h-4 w-4" />
|
||||||
</Button>
|
{t('about.shareActions.twitter')}
|
||||||
<Button variant="outline" size="sm" onClick={handleShareFacebook} className="gap-2">
|
</Button>
|
||||||
<Facebook className="h-4 w-4" />
|
<Button variant="outline" size="sm" onClick={handleShareFacebook} className="gap-2">
|
||||||
{t('about.shareActions.facebook')}
|
<Facebook className="h-4 w-4" />
|
||||||
</Button>
|
{t('about.shareActions.facebook')}
|
||||||
<Button variant="secondary" size="sm" onClick={handleCopyShareLink} className="gap-2">
|
</Button>
|
||||||
<LinkIcon className="h-4 w-4" />
|
<Button
|
||||||
{t('about.shareActions.copy')}
|
variant="secondary"
|
||||||
</Button>
|
size="sm"
|
||||||
|
onClick={handleCopyShareLink}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<LinkIcon className="h-4 w-4" />
|
||||||
|
{t('about.shareActions.copy')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
<p className="text-sm text-muted-foreground md:max-w-md">
|
||||||
|
{t('about.followAuthorSupport')}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => openShareUrl('https://x.com/nexmoex')}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Twitter className="h-4 w-4" />
|
||||||
|
{t('about.followAuthorActions.follow')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/u
|
|||||||
import { popularSites } from '@renderer/data/popularSites'
|
import { popularSites } from '@renderer/data/popularSites'
|
||||||
import type { AppSettings, OneClickQualityPreset, PlaylistInfo } from '@shared/types'
|
import type { AppSettings, OneClickQualityPreset, PlaylistInfo } from '@shared/types'
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
import { AlertCircle, Download, ListVideo, Loader2, Search } from 'lucide-react'
|
import { AlertCircle, Download, Loader2, Search } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
} from '../store/video'
|
} from '../store/video'
|
||||||
|
|
||||||
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
||||||
auto: null,
|
|
||||||
best: null,
|
best: null,
|
||||||
good: 1080,
|
good: 1080,
|
||||||
normal: 720,
|
normal: 720,
|
||||||
@@ -51,7 +50,6 @@ const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const qualityPresetToAudioAbr: Record<OneClickQualityPreset, number | null> = {
|
const qualityPresetToAudioAbr: Record<OneClickQualityPreset, number | null> = {
|
||||||
auto: null,
|
|
||||||
best: 320,
|
best: 320,
|
||||||
good: 256,
|
good: 256,
|
||||||
normal: 192,
|
normal: 192,
|
||||||
@@ -72,7 +70,7 @@ const dedupe = (candidates: Array<string | undefined>): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getQualityPreset = (settings: AppSettings): OneClickQualityPreset =>
|
const getQualityPreset = (settings: AppSettings): OneClickQualityPreset =>
|
||||||
settings.oneClickQuality ?? 'auto'
|
settings.oneClickQuality ?? 'best'
|
||||||
|
|
||||||
const buildAudioSelectors = (preset: OneClickQualityPreset): string[] => {
|
const buildAudioSelectors = (preset: OneClickQualityPreset): string[] => {
|
||||||
if (preset === 'worst') {
|
if (preset === 'worst') {
|
||||||
@@ -126,9 +124,10 @@ const buildAudioFormatPreference = (settings: AppSettings): string => {
|
|||||||
|
|
||||||
interface HomeProps {
|
interface HomeProps {
|
||||||
onOpenSupportedSites?: () => void
|
onOpenSupportedSites?: () => void
|
||||||
|
onOpenSettings?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Home({ onOpenSupportedSites }: HomeProps) {
|
export function Home({ onOpenSupportedSites, onOpenSettings }: HomeProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [videoInfo, _setVideoInfo] = useAtom(currentVideoInfoAtom)
|
const [videoInfo, _setVideoInfo] = useAtom(currentVideoInfoAtom)
|
||||||
const [loading] = useAtom(videoInfoLoadingAtom)
|
const [loading] = useAtom(videoInfoLoadingAtom)
|
||||||
@@ -540,11 +539,9 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
|||||||
<Tabs defaultValue="single" className="w-full">
|
<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">
|
<TabsTrigger value="single" className="flex items-center gap-2">
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
{t('download.singleVideo')}
|
{t('download.singleVideo')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="playlist" className="flex items-center gap-2">
|
<TabsTrigger value="playlist" className="flex items-center gap-2">
|
||||||
<ListVideo className="h-4 w-4" />
|
|
||||||
{t('playlist.title')}
|
{t('playlist.title')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -624,18 +621,20 @@ export function Home({ onOpenSupportedSites }: HomeProps) {
|
|||||||
|
|
||||||
{/* One-Click Download Info */}
|
{/* One-Click Download Info */}
|
||||||
{settings.oneClickDownload && (
|
{settings.oneClickDownload && (
|
||||||
<div className="rounded-lg bg-blue-50 dark:bg-blue-900/10 p-4">
|
<div className="flex items-center justify-between gap-2 rounded-lg bg-card px-4 py-3 text-sm text-muted-foreground">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<Download className="h-5 w-5 text-blue-600 mt-0.5 dark:text-blue-400" />
|
<span>{t('download.oneClickDownloadEnabled')}</span>
|
||||||
<div className="flex-1 space-y-1">
|
|
||||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
|
||||||
{t('download.oneClickDownload')}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-blue-700">
|
|
||||||
{t('download.oneClickDownloadDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{onOpenSettings && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
className="h-auto px-2 py-0 text-xs"
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
>
|
||||||
|
{t('download.goToSettings')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -235,9 +235,6 @@ export function Settings() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="auto">
|
|
||||||
{t('settings.oneClickQualityOptions.auto')}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="best">
|
<SelectItem value="best">
|
||||||
{t('settings.oneClickQualityOptions.best')}
|
{t('settings.oneClickQualityOptions.best')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export interface PlaylistDownloadResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Settings types
|
// Settings types
|
||||||
export type OneClickQualityPreset = 'auto' | 'best' | 'good' | 'normal' | 'bad' | 'worst'
|
export type OneClickQualityPreset = 'best' | 'good' | 'normal' | 'bad' | 'worst'
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
downloadPath: string
|
downloadPath: string
|
||||||
@@ -206,7 +206,7 @@ export const defaultSettings: AppSettings = {
|
|||||||
theme: 'system',
|
theme: 'system',
|
||||||
oneClickDownload: false,
|
oneClickDownload: false,
|
||||||
oneClickDownloadType: 'video',
|
oneClickDownloadType: 'video',
|
||||||
oneClickQuality: 'auto',
|
oneClickQuality: 'best',
|
||||||
closeToTray: false,
|
closeToTray: false,
|
||||||
hideDockIcon: false,
|
hideDockIcon: false,
|
||||||
autoUpdate: true
|
autoUpdate: true
|
||||||
|
|||||||
Reference in New Issue
Block a user