Skip to content

Commit afa9a03

Browse files
committed
feat: Implement initial application structure, integrate new UI components, and add custom Electron window controls.
1 parent defaf09 commit afa9a03

21 files changed

Lines changed: 1352 additions & 112 deletions

dist-electron/main.js

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,146 @@
1-
import { app, ipcMain, BrowserWindow } from "electron";
1+
import { app, BrowserWindow, ipcMain, dialog } from "electron";
22
import path from "path";
33
import { fileURLToPath } from "url";
4+
import fs from "fs/promises";
45
const __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
5-
let mainWindow;
6+
let mainWindow = null;
67
function createWindow() {
78
mainWindow = new BrowserWindow({
8-
width: 1024,
9-
height: 768,
9+
width: 1100,
10+
height: 750,
11+
minWidth: 800,
12+
minHeight: 600,
1013
frame: false,
11-
// MAGIC HAPPENS HERE: Removes default OS window frame
12-
titleBarStyle: "hidden",
13-
// Extra clean up for macOS
14+
backgroundColor: "#0a0a0b",
1415
webPreferences: {
1516
preload: path.join(__dirname$1, "preload.js"),
16-
// Security bridge (compiled from preload.ts)
1717
contextIsolation: true,
18-
nodeIntegration: false
18+
nodeIntegration: false,
19+
sandbox: false
1920
}
2021
});
22+
mainWindow.on("maximize", () => {
23+
mainWindow?.webContents.send("window-maximized-changed", true);
24+
});
25+
mainWindow.on("unmaximize", () => {
26+
mainWindow?.webContents.send("window-maximized-changed", false);
27+
});
28+
mainWindow.on("closed", () => {
29+
mainWindow = null;
30+
});
2131
if (process.env.VITE_DEV_SERVER_URL) {
2232
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
33+
mainWindow.webContents.openDevTools({ mode: "detach" });
2334
} else {
2435
mainWindow.loadFile(path.join(__dirname$1, "../dist/index.html"));
2536
}
2637
}
2738
app.whenReady().then(createWindow);
39+
app.on("window-all-closed", () => {
40+
if (process.platform !== "darwin") {
41+
app.quit();
42+
}
43+
});
44+
app.on("activate", () => {
45+
if (BrowserWindow.getAllWindows().length === 0) {
46+
createWindow();
47+
}
48+
});
2849
ipcMain.on("window-minimize", () => mainWindow?.minimize());
2950
ipcMain.on("window-maximize", () => {
3051
if (mainWindow?.isMaximized()) {
31-
mainWindow?.unmaximize();
52+
mainWindow.unmaximize();
3253
} else {
3354
mainWindow?.maximize();
3455
}
3556
});
3657
ipcMain.on("window-close", () => mainWindow?.close());
58+
ipcMain.handle("open-file-dialog", async () => {
59+
if (!mainWindow) return [];
60+
const result = await dialog.showOpenDialog(mainWindow, {
61+
properties: ["openFile", "multiSelections"],
62+
filters: [
63+
{
64+
name: "Supported Files",
65+
extensions: [
66+
"png",
67+
"jpg",
68+
"jpeg",
69+
"gif",
70+
"webp",
71+
"bmp",
72+
"avif",
73+
"tiff",
74+
"tif",
75+
"svg",
76+
"ico",
77+
"jxl",
78+
"pdf",
79+
"epub",
80+
"xps",
81+
"cbz",
82+
"mobi",
83+
"fb2",
84+
"docx",
85+
"txt",
86+
"rtf",
87+
"odt",
88+
"mp4",
89+
"mkv",
90+
"avi",
91+
"mov",
92+
"webm",
93+
"3gp",
94+
"flv",
95+
"wmv",
96+
"mp3",
97+
"wav",
98+
"aac",
99+
"ogg",
100+
"flac",
101+
"wma",
102+
"m4a"
103+
]
104+
},
105+
{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "webp", "bmp", "avif", "tiff", "tif", "svg", "ico", "jxl"] },
106+
{ name: "Documents", extensions: ["pdf", "epub", "xps", "cbz", "mobi", "fb2", "docx", "txt", "rtf", "odt"] },
107+
{ name: "Video", extensions: ["mp4", "mkv", "avi", "mov", "webm", "3gp", "flv", "wmv"] },
108+
{ name: "Audio", extensions: ["mp3", "wav", "aac", "ogg", "flac", "wma", "m4a"] },
109+
{ name: "All Files", extensions: ["*"] }
110+
]
111+
});
112+
if (result.canceled) return [];
113+
const fileInfos = await Promise.all(
114+
result.filePaths.map(async (filePath) => {
115+
const stat = await fs.stat(filePath);
116+
return {
117+
path: filePath,
118+
name: path.basename(filePath),
119+
ext: path.extname(filePath).slice(1).toLowerCase(),
120+
size: stat.size
121+
};
122+
})
123+
);
124+
return fileInfos;
125+
});
126+
ipcMain.handle("select-output-dir", async () => {
127+
if (!mainWindow) return null;
128+
const result = await dialog.showOpenDialog(mainWindow, {
129+
properties: ["openDirectory", "createDirectory"]
130+
});
131+
if (result.canceled) return null;
132+
return result.filePaths[0];
133+
});
134+
ipcMain.handle("get-file-info", async (_event, filePath) => {
135+
try {
136+
const stat = await fs.stat(filePath);
137+
return {
138+
path: filePath,
139+
name: path.basename(filePath),
140+
ext: path.extname(filePath).slice(1).toLowerCase(),
141+
size: stat.size
142+
};
143+
} catch {
144+
return null;
145+
}
146+
});

dist-electron/preload.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import { contextBridge, ipcRenderer } from "electron";
22
contextBridge.exposeInMainWorld("electronAPI", {
3+
// Window controls
34
minimize: () => ipcRenderer.send("window-minimize"),
45
maximize: () => ipcRenderer.send("window-maximize"),
5-
close: () => ipcRenderer.send("window-close")
6+
close: () => ipcRenderer.send("window-close"),
7+
// Window state listener
8+
onMaximizeChange: (callback) => {
9+
const handler = (_event, isMaximized) => callback(isMaximized);
10+
ipcRenderer.on("window-maximized-changed", handler);
11+
return () => ipcRenderer.removeListener("window-maximized-changed", handler);
12+
},
13+
// File operations
14+
openFileDialog: () => ipcRenderer.invoke("open-file-dialog"),
15+
selectOutputDir: () => ipcRenderer.invoke("select-output-dir"),
16+
getFileInfo: (filePath) => ipcRenderer.invoke("get-file-info", filePath)
617
});

electron/main.ts

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,138 @@
1-
import { app, BrowserWindow, ipcMain } from 'electron'
1+
import { app, BrowserWindow, ipcMain, dialog } from 'electron'
22
import path from 'path'
33
import { fileURLToPath } from 'url'
4+
import fs from 'fs/promises'
45

56
const __dirname = path.dirname(fileURLToPath(import.meta.url))
67

7-
let mainWindow: BrowserWindow | null
8+
let mainWindow: BrowserWindow | null = null
89

910
function createWindow() {
1011
mainWindow = new BrowserWindow({
11-
width: 1024,
12-
height: 768,
13-
frame: false, // MAGIC HAPPENS HERE: Removes default OS window frame
14-
titleBarStyle: 'hidden', // Extra clean up for macOS
12+
width: 1100,
13+
height: 750,
14+
minWidth: 800,
15+
minHeight: 600,
16+
frame: false,
17+
backgroundColor: '#0a0a0b',
1518
webPreferences: {
16-
preload: path.join(__dirname, 'preload.js'), // Security bridge (compiled from preload.ts)
19+
preload: path.join(__dirname, 'preload.js'),
1720
contextIsolation: true,
1821
nodeIntegration: false,
22+
sandbox: false,
1923
},
2024
})
2125

22-
// Load Vite Dev Server in development, else load built index.html
26+
// Forward maximize/unmaximize state changes to renderer
27+
mainWindow.on('maximize', () => {
28+
mainWindow?.webContents.send('window-maximized-changed', true)
29+
})
30+
mainWindow.on('unmaximize', () => {
31+
mainWindow?.webContents.send('window-maximized-changed', false)
32+
})
33+
34+
// Clean up reference when window is closed
35+
mainWindow.on('closed', () => {
36+
mainWindow = null
37+
})
38+
2339
if (process.env.VITE_DEV_SERVER_URL) {
2440
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
41+
mainWindow.webContents.openDevTools({ mode: 'detach' })
2542
} else {
2643
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
2744
}
2845
}
2946

3047
app.whenReady().then(createWindow)
3148

32-
// Handle IPC messages from React UI
49+
// Quit when all windows are closed (except on macOS)
50+
app.on('window-all-closed', () => {
51+
if (process.platform !== 'darwin') {
52+
app.quit()
53+
}
54+
})
55+
56+
// Re-create window on macOS when dock icon is clicked
57+
app.on('activate', () => {
58+
if (BrowserWindow.getAllWindows().length === 0) {
59+
createWindow()
60+
}
61+
})
62+
63+
// Window controls
3364
ipcMain.on('window-minimize', () => mainWindow?.minimize())
3465
ipcMain.on('window-maximize', () => {
3566
if (mainWindow?.isMaximized()) {
36-
mainWindow?.unmaximize()
67+
mainWindow.unmaximize()
3768
} else {
3869
mainWindow?.maximize()
3970
}
4071
})
4172
ipcMain.on('window-close', () => mainWindow?.close())
73+
74+
// File dialog
75+
ipcMain.handle('open-file-dialog', async () => {
76+
if (!mainWindow) return []
77+
78+
const result = await dialog.showOpenDialog(mainWindow, {
79+
properties: ['openFile', 'multiSelections'],
80+
filters: [
81+
{
82+
name: 'Supported Files',
83+
extensions: [
84+
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'avif', 'tiff', 'tif', 'svg', 'ico', 'jxl',
85+
'pdf', 'epub', 'xps', 'cbz', 'mobi', 'fb2', 'docx', 'txt', 'rtf', 'odt',
86+
'mp4', 'mkv', 'avi', 'mov', 'webm', '3gp', 'flv', 'wmv',
87+
'mp3', 'wav', 'aac', 'ogg', 'flac', 'wma', 'm4a',
88+
],
89+
},
90+
{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'avif', 'tiff', 'tif', 'svg', 'ico', 'jxl'] },
91+
{ name: 'Documents', extensions: ['pdf', 'epub', 'xps', 'cbz', 'mobi', 'fb2', 'docx', 'txt', 'rtf', 'odt'] },
92+
{ name: 'Video', extensions: ['mp4', 'mkv', 'avi', 'mov', 'webm', '3gp', 'flv', 'wmv'] },
93+
{ name: 'Audio', extensions: ['mp3', 'wav', 'aac', 'ogg', 'flac', 'wma', 'm4a'] },
94+
{ name: 'All Files', extensions: ['*'] },
95+
],
96+
})
97+
98+
if (result.canceled) return []
99+
100+
const fileInfos = await Promise.all(
101+
result.filePaths.map(async (filePath) => {
102+
const stat = await fs.stat(filePath)
103+
return {
104+
path: filePath,
105+
name: path.basename(filePath),
106+
ext: path.extname(filePath).slice(1).toLowerCase(),
107+
size: stat.size,
108+
}
109+
})
110+
)
111+
return fileInfos
112+
})
113+
114+
// Save dialog for output directory
115+
ipcMain.handle('select-output-dir', async () => {
116+
if (!mainWindow) return null
117+
118+
const result = await dialog.showOpenDialog(mainWindow, {
119+
properties: ['openDirectory', 'createDirectory'],
120+
})
121+
if (result.canceled) return null
122+
return result.filePaths[0]
123+
})
124+
125+
// Get file info (for drag & drop)
126+
ipcMain.handle('get-file-info', async (_event, filePath: string) => {
127+
try {
128+
const stat = await fs.stat(filePath)
129+
return {
130+
path: filePath,
131+
name: path.basename(filePath),
132+
ext: path.extname(filePath).slice(1).toLowerCase(),
133+
size: stat.size,
134+
}
135+
} catch {
136+
return null
137+
}
138+
})

electron/preload.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
import { contextBridge, ipcRenderer } from 'electron'
22

3-
// Expose protected methods that allow the renderer process to use ipcRenderer
43
contextBridge.exposeInMainWorld('electronAPI', {
4+
// Window controls
55
minimize: () => ipcRenderer.send('window-minimize'),
66
maximize: () => ipcRenderer.send('window-maximize'),
77
close: () => ipcRenderer.send('window-close'),
8+
9+
// Window state listener
10+
onMaximizeChange: (callback: (isMaximized: boolean) => void) => {
11+
const handler = (_event: Electron.IpcRendererEvent, isMaximized: boolean) => callback(isMaximized)
12+
ipcRenderer.on('window-maximized-changed', handler)
13+
// Return cleanup function
14+
return () => ipcRenderer.removeListener('window-maximized-changed', handler)
15+
},
16+
17+
// File operations
18+
openFileDialog: () => ipcRenderer.invoke('open-file-dialog'),
19+
selectOutputDir: () => ipcRenderer.invoke('select-output-dir'),
20+
getFileInfo: (filePath: string) => ipcRenderer.invoke('get-file-info', filePath),
821
})

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
5+
<link rel="icon" type="image/x-icon" href="/resources/logo.ico" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>openconvert-vite</title>
7+
<title>OpenConvert</title>
88
</head>
99
<body>
1010
<div id="root"></div>

0 commit comments

Comments
 (0)