From 7c2e526c490602b010d6e6b16090192d393d47f4 Mon Sep 17 00:00:00 2001 From: Nexmoe <16796652+nexmoe@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:44:28 +0800 Subject: [PATCH] feat(ui): add one-click texts, file existence checks, clipboard copy (#15) * feat(ui): add one-click texts, file existence checks, clipboard copy * refactor(ui): remove unused clearCompleted and adjust card bg --- src/main/ipc/services/file-system-service.ts | 19 +++++ src/renderer/src/App.tsx | 14 +++- .../src/components/download/DownloadItem.tsx | 49 ++++++++++-- .../download/UnifiedDownloadHistory.tsx | 39 ++-------- src/renderer/src/locales/en.json | 2 + src/renderer/src/pages/About.tsx | 76 +++++++++---------- src/renderer/src/pages/Home.tsx | 31 ++++---- 7 files changed, 138 insertions(+), 92 deletions(-) diff --git a/src/main/ipc/services/file-system-service.ts b/src/main/ipc/services/file-system-service.ts index 408ac83..2f47464 100644 --- a/src/main/ipc/services/file-system-service.ts +++ b/src/main/ipc/services/file-system-service.ts @@ -229,6 +229,25 @@ class FileSystemService extends IpcService { .replace(/'/g, ''') } + @IpcMethod() + async fileExists(_context: IpcContext, filePath: string): Promise { + 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() async deleteFile(_context: IpcContext, filePath: string): Promise { try { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c0aba93..8bd1c5d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -153,7 +153,12 @@ function AppContent() { const renderPage = () => { switch (currentPage) { case 'home': - return setCurrentPage('sites')} /> + return ( + setCurrentPage('sites')} + onOpenSettings={() => setCurrentPage('settings')} + /> + ) case 'settings': return case 'about': @@ -161,7 +166,12 @@ function AppContent() { case 'sites': return default: - return setCurrentPage('sites')} /> + return ( + setCurrentPage('sites')} + onOpenSettings={() => setCurrentPage('settings')} + /> + ) } } diff --git a/src/renderer/src/components/download/DownloadItem.tsx b/src/renderer/src/components/download/DownloadItem.tsx index 399810c..2b4fc30 100644 --- a/src/renderer/src/components/download/DownloadItem.tsx +++ b/src/renderer/src/components/download/DownloadItem.tsx @@ -5,6 +5,7 @@ import { Progress } from '@renderer/components/ui/progress' import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip' import { useAtomValue, useSetAtom } from 'jotai' import { AlertCircle, CheckCircle2, Copy, FolderOpen, Loader2, Play, Trash2, X } from 'lucide-react' +import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { useCachedThumbnail } from '../../hooks/use-cached-thumbnail' @@ -67,6 +68,30 @@ export function DownloadItem({ download }: DownloadItemProps) { ? actionsContainerBaseClass : `${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 () => { if (isHistory) return try { @@ -93,18 +118,31 @@ export function DownloadItem({ download }: DownloadItemProps) { 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 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')) return } try { // Generate file path using downloadPath + title + ext - const downloadPath = download.downloadPath - const format = download.format - const filePath = generateFilePath(downloadPath, download.title, format) + const filePath = generateFilePath(downloadPath, title, format) const success = await ipcServices.fs.copyFileToClipboard(filePath) if (!success) { @@ -320,7 +358,7 @@ export function DownloadItem({ download }: DownloadItemProps) { size="icon" className="h-8 w-8 shrink-0" onClick={handleCopyToClipboard} - disabled={!download.title || !download.downloadPath || !download.format} + disabled={!canCopyToClipboard()} > @@ -373,6 +411,7 @@ export function DownloadItem({ download }: DownloadItemProps) { size="icon" className="h-8 w-8 shrink-0" onClick={handleCopyToClipboard} + disabled={!canCopyToClipboard()} > diff --git a/src/renderer/src/components/download/UnifiedDownloadHistory.tsx b/src/renderer/src/components/download/UnifiedDownloadHistory.tsx index 28274af..2f0dc59 100644 --- a/src/renderer/src/components/download/UnifiedDownloadHistory.tsx +++ b/src/renderer/src/components/download/UnifiedDownloadHistory.tsx @@ -1,13 +1,13 @@ 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 { useAtomValue, useSetAtom } from 'jotai' +import { useAtomValue } 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 { downloadStatsAtom, downloadsArrayAtom } from '../../store/downloads' import { DownloadItem } from './DownloadItem' import { PlaylistDownloadGroup } from './PlaylistDownloadGroup' @@ -17,7 +17,6 @@ export function UnifiedDownloadHistory() { const { t } = useTranslation() const allRecords = useAtomValue(downloadsArrayAtom) const downloadStats = useAtomValue(downloadStatsAtom) - const clearCompleted = useSetAtom(clearCompletedAtom) const [statusFilter, setStatusFilter] = useState('all') useHistorySync() @@ -99,33 +98,9 @@ export function UnifiedDownloadHistory() { return { order, groups } }, [filteredRecords]) - const hasCompletedActive = allRecords.some( - (item) => item.entryType === 'active' && item.status === 'completed' - ) - const handleClearCompleted = () => { - clearCompleted() - } - return ( - - -
-
- {t('download.downloadQueue')} -
-
- {hasCompletedActive && ( - - )} -
-
+
+
{filters.map((filter) => { const isActive = statusFilter === filter.key @@ -155,7 +130,7 @@ export function UnifiedDownloadHistory() { })}
- + {filteredRecords.length === 0 ? (
@@ -191,6 +166,6 @@ export function UnifiedDownloadHistory() {
)}
- +
) } diff --git a/src/renderer/src/locales/en.json b/src/renderer/src/locales/en.json index b811731..e45800e 100644 --- a/src/renderer/src/locales/en.json +++ b/src/renderer/src/locales/en.json @@ -139,8 +139,10 @@ "noAudio": "No Audio", "noHistory": "No download history", "noItems": "No items found", + "goToSettings": "Go to Settings", "oneClickDownload": "One-Click Download", "oneClickDownloadDescription": "Download directly with default settings without confirmation", + "oneClickDownloadEnabled": "One-Click Download is enabled. Downloads will start directly with default settings.", "oneClickDownloadNow": "Download Now", "oneClickDownloadStarted": "Download started with default settings", "paste": "Paste", diff --git a/src/renderer/src/pages/About.tsx b/src/renderer/src/pages/About.tsx index 3ec15eb..fbc8eec 100644 --- a/src/renderer/src/pages/About.tsx +++ b/src/renderer/src/pages/About.tsx @@ -319,49 +319,49 @@ export function About() {
- - - {t('about.followAuthorTitle')} - {t('about.followAuthorDescription')} - - -

- {t('about.followAuthorSupport')} -

-
- -
-
-
- {t('about.shareTitle')} {t('about.shareDescription')} - -

{t('about.shareSupport')}

-
- - - + +
+

{t('about.shareSupport')}

+
+ + + +
+
+
+

+ {t('about.followAuthorSupport')} +

+
+ +
diff --git a/src/renderer/src/pages/Home.tsx b/src/renderer/src/pages/Home.tsx index 7c3bc96..36c4a97 100644 --- a/src/renderer/src/pages/Home.tsx +++ b/src/renderer/src/pages/Home.tsx @@ -19,7 +19,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/u import { popularSites } from '@renderer/data/popularSites' import type { AppSettings, OneClickQualityPreset, PlaylistInfo } from '@shared/types' 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 { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -124,9 +124,10 @@ const buildAudioFormatPreference = (settings: AppSettings): string => { interface HomeProps { onOpenSupportedSites?: () => void + onOpenSettings?: () => void } -export function Home({ onOpenSupportedSites }: HomeProps) { +export function Home({ onOpenSupportedSites, onOpenSettings }: HomeProps) { const { t } = useTranslation() const [videoInfo, _setVideoInfo] = useAtom(currentVideoInfoAtom) const [loading] = useAtom(videoInfoLoadingAtom) @@ -538,11 +539,9 @@ export function Home({ onOpenSupportedSites }: HomeProps) { - {t('download.singleVideo')} - {t('playlist.title')} @@ -622,18 +621,20 @@ export function Home({ onOpenSupportedSites }: HomeProps) { {/* One-Click Download Info */} {settings.oneClickDownload && ( -
-
- -
-

- {t('download.oneClickDownload')} -

-

- {t('download.oneClickDownloadDescription')} -

-
+
+
+ {t('download.oneClickDownloadEnabled')}
+ {onOpenSettings && ( + + )}
)}