Skip to content

Commit 89a8018

Browse files
committed
RoboCopy Pro GUI
Intial RoboCopy Pro GUI
1 parent deee530 commit 89a8018

22 files changed

Lines changed: 8389 additions & 0 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
robocopy.log
4+
*.log
5+
chrome-profile/

electron/main.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
const { app, BrowserWindow, dialog, ipcMain, Notification } = require('electron');
2+
const path = require('path');
3+
const { spawn } = require('child_process');
4+
const fs = require('fs/promises');
5+
6+
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
7+
8+
async function getFolderSize(rootPath) {
9+
let total = 0;
10+
const stack = [rootPath];
11+
12+
while (stack.length > 0) {
13+
const current = stack.pop();
14+
let entries = [];
15+
try {
16+
entries = await fs.readdir(current, { withFileTypes: true });
17+
} catch {
18+
continue;
19+
}
20+
21+
for (const entry of entries) {
22+
const fullPath = path.join(current, entry.name);
23+
if (entry.isDirectory()) {
24+
stack.push(fullPath);
25+
} else if (entry.isFile()) {
26+
try {
27+
const stat = await fs.stat(fullPath);
28+
total += stat.size;
29+
} catch {
30+
// Ignore unreadable files.
31+
}
32+
}
33+
}
34+
}
35+
36+
return total;
37+
}
38+
39+
function createWindow() {
40+
const mainWindow = new BrowserWindow({
41+
width: 1260,
42+
height: 820,
43+
minWidth: 980,
44+
minHeight: 680,
45+
backgroundColor: '#0b1020',
46+
title: 'RoboCopy Pro',
47+
webPreferences: {
48+
contextIsolation: true,
49+
preload: path.join(__dirname, 'preload.js'),
50+
},
51+
});
52+
53+
if (isDev) {
54+
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
55+
mainWindow.webContents.openDevTools({ mode: 'detach' });
56+
} else {
57+
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
58+
}
59+
}
60+
61+
app.whenReady().then(() => {
62+
createWindow();
63+
64+
app.on('activate', () => {
65+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
66+
});
67+
});
68+
69+
app.on('window-all-closed', () => {
70+
if (process.platform !== 'darwin') app.quit();
71+
});
72+
73+
ipcMain.handle('robocopy:select-folder', async () => {
74+
const result = await dialog.showOpenDialog({
75+
properties: ['openDirectory'],
76+
});
77+
if (result.canceled || result.filePaths.length === 0) return null;
78+
return result.filePaths[0];
79+
});
80+
81+
ipcMain.handle('robocopy:folder-size', async (_event, folderPath) => {
82+
if (!folderPath) return 0;
83+
return getFolderSize(folderPath);
84+
});
85+
86+
ipcMain.on('robocopy:ui-progress', (event, value) => {
87+
const window = BrowserWindow.fromWebContents(event.sender);
88+
if (!window) return;
89+
if (typeof value !== 'number') return;
90+
window.setProgressBar(value);
91+
});
92+
93+
ipcMain.handle('robocopy:run', async (event, payload) => {
94+
const { args, command } = payload;
95+
const window = BrowserWindow.fromWebContents(event.sender);
96+
if (window) window.setProgressBar(0);
97+
98+
return new Promise((resolve, reject) => {
99+
try {
100+
if (window) {
101+
window.webContents.send('robocopy:log', `Command: robocopy ${args.join(' ')}`);
102+
}
103+
const proc = spawn('robocopy', args, { windowsHide: true });
104+
105+
proc.stdout.on('data', (data) => {
106+
const lines = data.toString().split(/\r?\n/).filter(Boolean);
107+
lines.forEach((line) => {
108+
window?.webContents.send('robocopy:log', line);
109+
const progressMatch = line.match(/(\d+(?:\.\d+)?)%/);
110+
if (progressMatch) {
111+
const progress = Number(progressMatch[1]);
112+
if (!Number.isNaN(progress)) {
113+
window?.webContents.send('robocopy:progress', progress);
114+
window?.setProgressBar(Math.min(1, Math.max(0, progress / 100)));
115+
}
116+
}
117+
});
118+
});
119+
120+
proc.stderr.on('data', (data) => {
121+
const lines = data.toString().split(/\r?\n/).filter(Boolean);
122+
lines.forEach((line) => window?.webContents.send('robocopy:log', line));
123+
});
124+
125+
proc.on('close', (code) => {
126+
const exitCode = Number.isFinite(code) ? code : 16;
127+
const codeMap = {
128+
0: 'No files copied.',
129+
1: 'Files copied successfully.',
130+
2: 'Extra files or directories detected.',
131+
3: 'Copied + extra files detected.',
132+
5: 'Some files copied, some mismatched.',
133+
6: 'Extra + mismatched files detected.',
134+
7: 'Copied + extra + mismatched.',
135+
8: 'Some files or directories could not be copied.',
136+
16: 'Serious error.',
137+
};
138+
const message = codeMap[exitCode] || 'Robocopy completed with a non-standard exit code.';
139+
window?.webContents.send('robocopy:log', `Robocopy exit code ${exitCode}: ${message}`);
140+
window?.webContents.send('robocopy:done', { code: exitCode, message });
141+
142+
if (window) window.setProgressBar(-1);
143+
144+
if (exitCode <= 7) {
145+
if (Notification.isSupported()) {
146+
new Notification({
147+
title: 'RoboCopy Pro',
148+
body: 'Transfer complete.',
149+
}).show();
150+
}
151+
resolve({ code: exitCode, message });
152+
} else {
153+
if (Notification.isSupported()) {
154+
new Notification({
155+
title: 'RoboCopy Pro',
156+
body: 'Transfer failed. Check logs for details.',
157+
}).show();
158+
}
159+
window?.webContents.send('robocopy:error', { code: exitCode, command });
160+
reject(new Error(`Robocopy exited with code ${exitCode}: ${message}`));
161+
}
162+
});
163+
} catch (error) {
164+
window?.webContents.send('robocopy:error', { message: error.message });
165+
if (window) window.setProgressBar(-1);
166+
reject(error);
167+
}
168+
});
169+
});

electron/preload.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { contextBridge, ipcRenderer } = require('electron');
2+
3+
contextBridge.exposeInMainWorld('electron', {
4+
selectFolder: () => ipcRenderer.invoke('robocopy:select-folder'),
5+
});
6+
7+
contextBridge.exposeInMainWorld('robocopy', {
8+
run: ({ args, command, onProgress }) => {
9+
const logListener = (_, line) => {
10+
if (line) onProgress?.(null, line);
11+
};
12+
const progressListener = (_, progress) => {
13+
if (Number.isFinite(progress)) onProgress?.(progress, null);
14+
};
15+
16+
ipcRenderer.on('robocopy:log', logListener);
17+
ipcRenderer.on('robocopy:progress', progressListener);
18+
19+
return ipcRenderer
20+
.invoke('robocopy:run', { args, command })
21+
.finally(() => {
22+
ipcRenderer.removeListener('robocopy:log', logListener);
23+
ipcRenderer.removeListener('robocopy:progress', progressListener);
24+
});
25+
},
26+
setProgress: (value) => ipcRenderer.send('robocopy:ui-progress', value),
27+
getFolderSize: (path) => ipcRenderer.invoke('robocopy:folder-size', path),
28+
});

index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<html lang="en" class="dark">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>RoboCopy Pro</title>
7+
<link rel="preconnect" href="https://fonts.googleapis.com" />
8+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9+
<link
10+
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Sora:wght@500;600;700;800&display=swap"
11+
rel="stylesheet"
12+
/>
13+
</head>
14+
<body>
15+
<div id="root"></div>
16+
<script type="module" src="/src/main.jsx"></script>
17+
</body>
18+
</html>

0 commit comments

Comments
 (0)