Skip to content

Commit 93c6d69

Browse files
committed
feat: Introduce multi-view application structure with sidebar navigation and enhanced Electron DevTools handling.
1 parent afa9a03 commit 93c6d69

21 files changed

Lines changed: 1527 additions & 332 deletions

dist-electron/main.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "path";
33
import { fileURLToPath } from "url";
44
import fs from "fs/promises";
55
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
6+
const isDev = !!process.env.VITE_DEV_SERVER_URL;
67
let mainWindow = null;
78
function createWindow() {
89
mainWindow = new BrowserWindow({
@@ -28,12 +29,27 @@ function createWindow() {
2829
mainWindow.on("closed", () => {
2930
mainWindow = null;
3031
});
31-
if (process.env.VITE_DEV_SERVER_URL) {
32+
console.log("[main] isDev:", isDev);
33+
console.log("[main] VITE_DEV_SERVER_URL:", process.env.VITE_DEV_SERVER_URL);
34+
if (isDev) {
3235
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
33-
mainWindow.webContents.openDevTools({ mode: "detach" });
36+
console.log("[main] Loaded dev URL, opening DevTools...");
37+
mainWindow?.webContents.openDevTools({ mode: "undocked" });
3438
} else {
3539
mainWindow.loadFile(path.join(__dirname$1, "../dist/index.html"));
3640
}
41+
mainWindow.once("ready-to-show", () => {
42+
console.log("[main] Window ready-to-show, opening DevTools...");
43+
mainWindow?.webContents.openDevTools({ mode: "undocked" });
44+
});
45+
ipcMain.on("toggle-dev-tools", () => {
46+
console.log("[main] toggle-dev-tools IPC received");
47+
if (mainWindow?.webContents.isDevToolsOpened()) {
48+
mainWindow.webContents.closeDevTools();
49+
} else {
50+
mainWindow?.webContents.openDevTools({ mode: "undocked" });
51+
}
52+
});
3753
}
3854
app.whenReady().then(createWindow);
3955
app.on("window-all-closed", () => {

dist-electron/preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
44
minimize: () => ipcRenderer.send("window-minimize"),
55
maximize: () => ipcRenderer.send("window-maximize"),
66
close: () => ipcRenderer.send("window-close"),
7+
toggleDevTools: () => ipcRenderer.send("toggle-dev-tools"),
78
// Window state listener
89
onMaximizeChange: (callback) => {
910
const handler = (_event, isMaximized) => callback(isMaximized);

electron/main.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url'
44
import fs from 'fs/promises'
55

66
const __dirname = path.dirname(fileURLToPath(import.meta.url))
7+
const isDev = !!process.env.VITE_DEV_SERVER_URL
78

89
let mainWindow: BrowserWindow | null = null
910

@@ -36,12 +37,32 @@ function createWindow() {
3637
mainWindow = null
3738
})
3839

39-
if (process.env.VITE_DEV_SERVER_URL) {
40-
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
41-
mainWindow.webContents.openDevTools({ mode: 'detach' })
40+
console.log('[main] isDev:', isDev)
41+
console.log('[main] VITE_DEV_SERVER_URL:', process.env.VITE_DEV_SERVER_URL)
42+
43+
if (isDev) {
44+
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL!)
45+
console.log('[main] Loaded dev URL, opening DevTools...')
46+
mainWindow?.webContents.openDevTools({ mode: 'undocked' })
4247
} else {
4348
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
4449
}
50+
51+
// Fallback: open DevTools after window is ready, unconditionally
52+
mainWindow.once('ready-to-show', () => {
53+
console.log('[main] Window ready-to-show, opening DevTools...')
54+
mainWindow?.webContents.openDevTools({ mode: 'undocked' })
55+
})
56+
57+
// Toggle DevTools via IPC from renderer
58+
ipcMain.on('toggle-dev-tools', () => {
59+
console.log('[main] toggle-dev-tools IPC received')
60+
if (mainWindow?.webContents.isDevToolsOpened()) {
61+
mainWindow.webContents.closeDevTools()
62+
} else {
63+
mainWindow?.webContents.openDevTools({ mode: 'undocked' })
64+
}
65+
})
4566
}
4667

4768
app.whenReady().then(createWindow)

electron/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
55
minimize: () => ipcRenderer.send('window-minimize'),
66
maximize: () => ipcRenderer.send('window-maximize'),
77
close: () => ipcRenderer.send('window-close'),
8+
toggleDevTools: () => ipcRenderer.send('toggle-dev-tools'),
89

910
// Window state listener
1011
onMaximizeChange: (callback: (isMaximized: boolean) => void) => {

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!doctype html>
2-
<html lang="en">
2+
<html lang="en" class="dark">
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/x-icon" href="/resources/logo.ico" />

src/App.tsx

Lines changed: 47 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -1,211 +1,60 @@
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'
52
import { TooltipProvider } from '@/components/ui/tooltip'
63
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+
}
1123

1224
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()
2934
}
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)
5435
}
36+
window.addEventListener('keydown', handleKeyDown)
37+
return () => window.removeEventListener('keydown', handleKeyDown)
5538
}, [])
5639

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-
10040
return (
10141
<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>
20958
</div>
21059
</div>
21160
</TooltipProvider>

0 commit comments

Comments
 (0)