|
1 | | -import { useState, useCallback } from 'react' |
2 | | -import { FolderOutput, Zap, Settings2, Image, FileText, Film, Music } from 'lucide-react' |
3 | | -import { Button } from '@/components/ui/button' |
4 | | -import { Separator } from '@/components/ui/separator' |
| 1 | +import { useState, useEffect } from 'react' |
5 | 2 | import { TooltipProvider } from '@/components/ui/tooltip' |
6 | 3 | import Titlebar from '@/components/Titlebar' |
7 | | -import DropZone from '@/components/DropZone' |
8 | | -import FileList from '@/components/FileList' |
9 | | -import type { ConvertFile } from '@/components/FileList' |
10 | | -import { getTargetFormats, getFileCategory } from '@/lib/formats' |
| 4 | +import AppSidebar from '@/components/Sidebar' |
| 5 | +import type { Tab } from '@/components/Sidebar' |
| 6 | +import ConvertView from '@/views/ConvertView' |
| 7 | +import PluginsView from '@/views/PluginsView' |
| 8 | +import HistoryView from '@/views/HistoryView' |
| 9 | +import SettingsView from '@/views/SettingsView' |
| 10 | + |
| 11 | +function renderView(activeTab: Tab) { |
| 12 | + switch (activeTab) { |
| 13 | + case 'convert': |
| 14 | + return <ConvertView /> |
| 15 | + case 'plugins': |
| 16 | + return <PluginsView /> |
| 17 | + case 'history': |
| 18 | + return <HistoryView /> |
| 19 | + case 'settings': |
| 20 | + return <SettingsView /> |
| 21 | + } |
| 22 | +} |
11 | 23 |
|
12 | 24 | function App() { |
13 | | - const [files, setFiles] = useState<ConvertFile[]>([]) |
14 | | - const [outputDir, setOutputDir] = useState<string | null>(null) |
15 | | - const [isConverting, setIsConverting] = useState(false) |
16 | | - |
17 | | - const handleFilesAdded = useCallback((newFiles: FileInfo[]) => { |
18 | | - const convertFiles: ConvertFile[] = newFiles.map((f) => { |
19 | | - const targets = getTargetFormats(f.ext) |
20 | | - return { |
21 | | - id: `${f.path}-${Date.now()}-${Math.random().toString(36).slice(2)}`, |
22 | | - path: f.path, |
23 | | - name: f.name, |
24 | | - ext: f.ext, |
25 | | - size: f.size, |
26 | | - targetFormat: targets[0] ?? 'png', |
27 | | - status: 'pending' as const, |
28 | | - progress: 0, |
| 25 | + const [activeTab, setActiveTab] = useState<Tab>('convert') |
| 26 | + const [isSidebarExpanded, setIsSidebarExpanded] = useState(true) |
| 27 | + |
| 28 | + // F12 / Ctrl+Shift+I to toggle DevTools (dev mode only) |
| 29 | + useEffect(() => { |
| 30 | + const handleKeyDown = (e: KeyboardEvent) => { |
| 31 | + if (e.key === 'F12' || (e.ctrlKey && e.shiftKey && e.key === 'I')) { |
| 32 | + e.preventDefault() |
| 33 | + window.electronAPI.toggleDevTools() |
29 | 34 | } |
30 | | - }) |
31 | | - setFiles((prev) => [...prev, ...convertFiles]) |
32 | | - }, []) |
33 | | - |
34 | | - const handleRemoveFile = useCallback((id: string) => { |
35 | | - setFiles((prev) => prev.filter((f) => f.id !== id)) |
36 | | - }, []) |
37 | | - |
38 | | - const handleTargetFormatChange = useCallback((id: string, format: string) => { |
39 | | - setFiles((prev) => |
40 | | - prev.map((f) => (f.id === id ? { ...f, targetFormat: format } : f)) |
41 | | - ) |
42 | | - }, []) |
43 | | - |
44 | | - const handleClearAll = useCallback(() => { |
45 | | - setFiles([]) |
46 | | - }, []) |
47 | | - |
48 | | - const handleSelectOutputDir = useCallback(async () => { |
49 | | - try { |
50 | | - const dir = await window.electronAPI.selectOutputDir() |
51 | | - if (dir) setOutputDir(dir) |
52 | | - } catch (err) { |
53 | | - console.error('Failed to select output directory:', err) |
54 | 35 | } |
| 36 | + window.addEventListener('keydown', handleKeyDown) |
| 37 | + return () => window.removeEventListener('keydown', handleKeyDown) |
55 | 38 | }, []) |
56 | 39 |
|
57 | | - const handleConvert = useCallback(async () => { |
58 | | - if (files.length === 0) return |
59 | | - setIsConverting(true) |
60 | | - |
61 | | - // Snapshot file IDs to avoid stale closure issues if files array changes during conversion |
62 | | - const fileIds = files.map((f) => f.id) |
63 | | - |
64 | | - try { |
65 | | - // Simulate conversion progress for each file |
66 | | - for (const fileId of fileIds) { |
67 | | - setFiles((prev) => |
68 | | - prev.map((f) => (f.id === fileId ? { ...f, status: 'converting' as const, progress: 0 } : f)) |
69 | | - ) |
70 | | - |
71 | | - // Simulate progress |
72 | | - for (let p = 0; p <= 100; p += 10) { |
73 | | - await new Promise((r) => setTimeout(r, 80)) |
74 | | - setFiles((prev) => |
75 | | - prev.map((f) => (f.id === fileId ? { ...f, progress: p } : f)) |
76 | | - ) |
77 | | - } |
78 | | - |
79 | | - setFiles((prev) => |
80 | | - prev.map((f) => (f.id === fileId ? { ...f, status: 'done' as const, progress: 100 } : f)) |
81 | | - ) |
82 | | - } |
83 | | - } catch (err) { |
84 | | - console.error('Conversion failed:', err) |
85 | | - } finally { |
86 | | - setIsConverting(false) |
87 | | - } |
88 | | - }, [files]) |
89 | | - |
90 | | - // Stats |
91 | | - const categoryCounts = files.reduce<Record<string, number>>((acc, f) => { |
92 | | - const cat = getFileCategory(f.ext) ?? 'other' |
93 | | - acc[cat] = (acc[cat] || 0) + 1 |
94 | | - return acc |
95 | | - }, {}) |
96 | | - |
97 | | - const pendingCount = files.filter((f) => f.status === 'pending').length |
98 | | - const doneCount = files.filter((f) => f.status === 'done').length |
99 | | - |
100 | 40 | return ( |
101 | 41 | <TooltipProvider> |
102 | | - <div className="flex flex-col h-screen bg-[#0a0a0b] text-white overflow-hidden"> |
103 | | - <Titlebar /> |
104 | | - |
105 | | - <div className="flex-1 flex flex-col overflow-hidden"> |
106 | | - {/* Main content area */} |
107 | | - <div className="flex-1 overflow-y-auto"> |
108 | | - <div className="max-w-3xl mx-auto px-6 py-8 flex flex-col gap-6"> |
109 | | - {/* Hero header */} |
110 | | - <div className="text-center mb-2"> |
111 | | - <h1 className="text-2xl font-bold tracking-tight text-white"> |
112 | | - Convert your files |
113 | | - </h1> |
114 | | - <p className="text-sm text-zinc-500 mt-1"> |
115 | | - Images, documents, video & audio — fast and private |
116 | | - </p> |
117 | | - </div> |
118 | | - |
119 | | - {/* Format category pills */} |
120 | | - <div className="flex items-center justify-center gap-2"> |
121 | | - {[ |
122 | | - { icon: Image, label: 'Images', color: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20' }, |
123 | | - { icon: FileText, label: 'Docs', color: 'text-blue-400 bg-blue-500/10 border-blue-500/20' }, |
124 | | - { icon: Film, label: 'Video', color: 'text-amber-400 bg-amber-500/10 border-amber-500/20' }, |
125 | | - { icon: Music, label: 'Audio', color: 'text-pink-400 bg-pink-500/10 border-pink-500/20' }, |
126 | | - ].map(({ icon: Icon, label, color }) => ( |
127 | | - <div |
128 | | - key={label} |
129 | | - className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium border ${color} transition-all`} |
130 | | - > |
131 | | - <Icon size={12} /> |
132 | | - {label} |
133 | | - </div> |
134 | | - ))} |
135 | | - </div> |
136 | | - |
137 | | - {/* Drop zone */} |
138 | | - <DropZone onFilesAdded={handleFilesAdded} /> |
139 | | - |
140 | | - {/* File list */} |
141 | | - <FileList |
142 | | - files={files} |
143 | | - onRemoveFile={handleRemoveFile} |
144 | | - onTargetFormatChange={handleTargetFormatChange} |
145 | | - onClearAll={handleClearAll} |
146 | | - /> |
147 | | - </div> |
148 | | - </div> |
149 | | - |
150 | | - {/* Bottom action bar */} |
151 | | - {files.length > 0 && ( |
152 | | - <> |
153 | | - <Separator className="bg-zinc-800/50" /> |
154 | | - <div className="flex-shrink-0 px-6 py-4 bg-[#0a0a0b]/95 backdrop-blur-xl border-t border-zinc-800/30"> |
155 | | - <div className="max-w-3xl mx-auto flex items-center justify-between"> |
156 | | - {/* Left: output dir + stats */} |
157 | | - <div className="flex items-center gap-4"> |
158 | | - <button |
159 | | - onClick={handleSelectOutputDir} |
160 | | - className="flex items-center gap-2 px-3 py-2 rounded-lg bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-xs text-zinc-400 hover:text-zinc-300 transition-all" |
161 | | - > |
162 | | - <FolderOutput size={14} /> |
163 | | - {outputDir ? ( |
164 | | - <span className="max-w-[180px] truncate">{outputDir}</span> |
165 | | - ) : ( |
166 | | - 'Output folder' |
167 | | - )} |
168 | | - </button> |
169 | | - |
170 | | - <div className="flex items-center gap-3 text-xs text-zinc-600"> |
171 | | - {Object.entries(categoryCounts).map(([cat, count]) => ( |
172 | | - <span key={cat} className="capitalize"> |
173 | | - {count} {cat} |
174 | | - </span> |
175 | | - ))} |
176 | | - {doneCount > 0 && ( |
177 | | - <span className="text-emerald-500"> |
178 | | - {doneCount}/{files.length} done |
179 | | - </span> |
180 | | - )} |
181 | | - </div> |
182 | | - </div> |
183 | | - |
184 | | - {/* Right: convert button */} |
185 | | - <div className="flex items-center gap-3"> |
186 | | - <Button |
187 | | - variant="ghost" |
188 | | - size="sm" |
189 | | - className="text-zinc-500 hover:text-zinc-300" |
190 | | - > |
191 | | - <Settings2 size={16} /> |
192 | | - </Button> |
193 | | - <Button |
194 | | - onClick={handleConvert} |
195 | | - disabled={pendingCount === 0 || isConverting} |
196 | | - className="bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-medium px-6 shadow-lg shadow-violet-500/20 disabled:opacity-40 disabled:shadow-none transition-all duration-200" |
197 | | - > |
198 | | - <Zap size={16} className="mr-2" /> |
199 | | - {isConverting |
200 | | - ? 'Converting...' |
201 | | - : `Convert ${pendingCount} ${pendingCount === 1 ? 'file' : 'files'}` |
202 | | - } |
203 | | - </Button> |
204 | | - </div> |
205 | | - </div> |
206 | | - </div> |
207 | | - </> |
208 | | - )} |
| 42 | + <div className="flex flex-col h-screen bg-[#0a0a0b] text-white overflow-hidden font-sans"> |
| 43 | + <Titlebar |
| 44 | + isSidebarExpanded={isSidebarExpanded} |
| 45 | + onToggleSidebar={() => setIsSidebarExpanded(!isSidebarExpanded)} |
| 46 | + /> |
| 47 | + |
| 48 | + <div className="flex-1 flex overflow-hidden"> |
| 49 | + <AppSidebar |
| 50 | + activeTab={activeTab} |
| 51 | + onTabChange={setActiveTab} |
| 52 | + isSidebarExpanded={isSidebarExpanded} |
| 53 | + /> |
| 54 | + |
| 55 | + <main className="flex-1 overflow-y-auto custom-scrollbar"> |
| 56 | + {renderView(activeTab)} |
| 57 | + </main> |
209 | 58 | </div> |
210 | 59 | </div> |
211 | 60 | </TooltipProvider> |
|
0 commit comments