Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aandrew-me
GitHub Repository: aandrew-me/ytDownloader
Path: blob/main/main.js
448 views
1
const {
2
app,
3
BrowserWindow,
4
dialog,
5
ipcMain,
6
shell,
7
Tray,
8
Menu,
9
clipboard,
10
} = require("electron");
11
const {autoUpdater} = require("electron-updater");
12
const fs = require("fs").promises;
13
const {existsSync, readFileSync} = require("fs");
14
const path = require("path");
15
const DownloadHistory = require("./src/history");
16
17
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
18
autoUpdater.autoDownload = false;
19
20
const USER_DATA_PATH = app.getPath("userData");
21
const CONFIG_FILE_PATH = path.join(USER_DATA_PATH, "ytdownloader.json");
22
23
const appState = {
24
/** @type {BrowserWindow | null} */
25
mainWindow: null,
26
/** @type {BrowserWindow | null} */
27
secondaryWindow: null,
28
/** @type {Tray | null} */
29
tray: null,
30
isQuitting: false,
31
indexPageIsOpen: true,
32
trayEnabled: false,
33
loadedLanguage: {},
34
config: {},
35
downloadHistory: new DownloadHistory(),
36
autoUpdateEnabled: false,
37
};
38
39
const gotTheLock = app.requestSingleInstanceLock();
40
41
if (!gotTheLock) {
42
app.quit();
43
} else {
44
app.on("second-instance", () => {
45
if (appState.mainWindow) {
46
if (appState.mainWindow.isMinimized())
47
appState.mainWindow.restore();
48
appState.mainWindow.show();
49
appState.mainWindow.focus();
50
}
51
});
52
}
53
54
app.whenReady().then(async () => {
55
await initialize();
56
57
app.on("activate", () => {
58
if (BrowserWindow.getAllWindows().length === 0) {
59
createWindow();
60
}
61
});
62
});
63
64
app.on("before-quit", async () => {
65
appState.isQuitting = true;
66
try {
67
// Save the final config state before exiting.
68
await saveConfiguration();
69
} catch (error) {
70
console.error("Failed to save configuration during quit:", error);
71
}
72
});
73
74
app.on("window-all-closed", () => {
75
if (process.platform !== "darwin") {
76
app.quit();
77
}
78
});
79
80
/**
81
* Initializes the application by loading config, translations,
82
* and setting up handlers.
83
*/
84
async function initialize() {
85
await loadConfiguration();
86
await loadTranslations();
87
88
registerIpcHandlers();
89
registerAutoUpdaterEvents();
90
91
createWindow();
92
93
if (process.platform === "win32") {
94
app.setAppUserModelId(app.name);
95
}
96
}
97
98
function createWindow() {
99
const bounds = appState.config.bounds || {};
100
101
appState.mainWindow = new BrowserWindow({
102
...bounds,
103
minWidth: 800,
104
minHeight: 600,
105
autoHideMenuBar: true,
106
show: false,
107
icon: path.join(__dirname, "/assets/images/icon.png"),
108
webPreferences: {
109
nodeIntegration: true,
110
contextIsolation: false,
111
spellcheck: false,
112
},
113
});
114
115
appState.mainWindow.loadFile("html/index.html");
116
117
appState.mainWindow.once("ready-to-show", () => {
118
if (appState.config.isMaximized) {
119
appState.mainWindow.maximize();
120
}
121
appState.mainWindow.show();
122
});
123
124
const saveBounds = () => {
125
if (appState.mainWindow && !appState.mainWindow.isMaximized()) {
126
appState.config.bounds = appState.mainWindow.getBounds();
127
}
128
};
129
130
appState.mainWindow.on("resize", saveBounds);
131
appState.mainWindow.on("move", saveBounds);
132
133
appState.mainWindow.on("maximize", () => {
134
appState.config.isMaximized = true;
135
});
136
137
appState.mainWindow.on("unmaximize", () => {
138
appState.config.isMaximized = false;
139
});
140
141
appState.mainWindow.on("close", (event) => {
142
if (!appState.isQuitting && appState.trayEnabled) {
143
event.preventDefault();
144
appState.mainWindow.hide();
145
if (app.dock) app.dock.hide();
146
}
147
});
148
}
149
150
/**
151
* @param {string} file The HTML file to load.
152
*/
153
function createSecondaryWindow(file) {
154
if (appState.secondaryWindow) {
155
appState.secondaryWindow.focus();
156
return;
157
}
158
159
appState.secondaryWindow = new BrowserWindow({
160
parent: appState.mainWindow,
161
modal: true,
162
show: false,
163
webPreferences: {
164
nodeIntegration: true,
165
contextIsolation: false,
166
},
167
width: 1000,
168
height: 800,
169
});
170
171
// appState.secondaryWindow.webContents.openDevTools();
172
appState.secondaryWindow.loadFile(file);
173
appState.secondaryWindow.setMenu(null);
174
appState.secondaryWindow.once("ready-to-show", () => {
175
appState.secondaryWindow.show();
176
});
177
178
appState.secondaryWindow.on("closed", () => {
179
appState.secondaryWindow = null;
180
});
181
}
182
183
/**
184
* Creates the system tray icon
185
*/
186
function createTray() {
187
if (appState.tray) return;
188
189
let iconPath;
190
if (process.platform === "win32") {
191
iconPath = path.join(__dirname, "resources/icon.ico");
192
} else if (process.platform === "darwin") {
193
iconPath = path.join(__dirname, "resources/icons/16x16.png");
194
} else {
195
iconPath = path.join(__dirname, "resources/icons/256x256.png");
196
}
197
198
appState.tray = new Tray(iconPath);
199
200
const contextMenu = Menu.buildFromTemplate([
201
{
202
label: i18n("openApp"),
203
click: () => {
204
appState.mainWindow?.show();
205
if (app.dock) app.dock.show();
206
},
207
},
208
{
209
label: i18n("pasteVideoLink"),
210
click: async () => {
211
const text = clipboard.readText();
212
appState.mainWindow?.show();
213
if (app.dock) app.dock.show();
214
if (appState.indexPageIsOpen) {
215
appState.mainWindow.webContents.send("link", text);
216
} else {
217
await appState.mainWindow.loadFile("html/index.html");
218
appState.indexPageIsOpen = true;
219
appState.mainWindow.webContents.once(
220
"did-finish-load",
221
() => {
222
appState.mainWindow.webContents.send("link", text);
223
}
224
);
225
}
226
},
227
},
228
{
229
label: i18n("downloadPlaylistButton"),
230
click: () => {
231
appState.indexPageIsOpen = false;
232
appState.mainWindow?.loadFile("html/playlist.html");
233
appState.mainWindow?.show();
234
if (app.dock) app.dock.show();
235
},
236
},
237
{
238
label: i18n("quit"),
239
click: () => {
240
app.quit();
241
},
242
},
243
]);
244
245
appState.tray.setToolTip("ytDownloader");
246
appState.tray.setContextMenu(contextMenu);
247
appState.tray.on("click", () => {
248
appState.mainWindow?.show();
249
250
if (app.dock) app.dock.show();
251
});
252
}
253
254
function registerIpcHandlers() {
255
ipcMain.on("autoUpdate", (_event, status) => {
256
appState.autoUpdateEnabled = status;
257
258
if (status) {
259
autoUpdater.checkForUpdates();
260
}
261
});
262
263
ipcMain.on("reload", () => {
264
appState.mainWindow?.reload();
265
appState.secondaryWindow?.reload();
266
});
267
268
ipcMain.on("get-version", (event) => {
269
event.sender.send("version", app.getVersion());
270
});
271
272
ipcMain.on("show-file", async (_event, fullPath) => {
273
try {
274
await fs.stat(fullPath);
275
shell.showItemInFolder(fullPath);
276
} catch (error) {}
277
});
278
279
ipcMain.handle("show-file", async (_event, fullPath) => {
280
try {
281
await fs.stat(fullPath);
282
shell.showItemInFolder(fullPath);
283
284
return {success: true};
285
} catch (error) {
286
return {success: false, error: error.message};
287
}
288
});
289
290
ipcMain.handle("open-folder", async (_event, folderPath) => {
291
try {
292
await fs.stat(folderPath);
293
const result = await shell.openPath(folderPath);
294
if (result) {
295
return {success: false, error: result};
296
} else {
297
return {success: true};
298
}
299
} catch (error) {
300
return {success: false, error: error.message};
301
}
302
});
303
304
ipcMain.on("load-win", (_event, file) => {
305
appState.indexPageIsOpen = file.includes("index.html");
306
appState.mainWindow?.loadFile(file);
307
});
308
309
ipcMain.on("load-page", (_event, file) => {
310
createSecondaryWindow(file);
311
});
312
313
ipcMain.on("close-secondary", () => {
314
appState.secondaryWindow?.close();
315
});
316
317
ipcMain.on("quit", () => {
318
app.quit();
319
});
320
321
ipcMain.on("select-location-main", async () => {
322
if (!appState.mainWindow) return;
323
const {canceled, filePaths} = await dialog.showOpenDialog(
324
appState.mainWindow,
325
{properties: ["openDirectory"]}
326
);
327
if (!canceled && filePaths.length > 0) {
328
appState.mainWindow.webContents.send("downloadPath", filePaths);
329
}
330
});
331
332
ipcMain.on("select-location-secondary", async () => {
333
if (!appState.secondaryWindow) return;
334
const {canceled, filePaths} = await dialog.showOpenDialog(
335
appState.secondaryWindow,
336
{properties: ["openDirectory"]}
337
);
338
if (!canceled && filePaths.length > 0) {
339
appState.secondaryWindow.webContents.send(
340
"downloadPath",
341
filePaths
342
);
343
}
344
});
345
346
ipcMain.on("get-directory", async () => {
347
if (!appState.mainWindow) return;
348
const {canceled, filePaths} = await dialog.showOpenDialog(
349
appState.mainWindow,
350
{properties: ["openDirectory"]}
351
);
352
if (!canceled && filePaths.length > 0) {
353
appState.mainWindow.webContents.send("directory-path", filePaths);
354
}
355
});
356
357
ipcMain.on("select-config", async () => {
358
if (!appState.secondaryWindow) return;
359
const {canceled, filePaths} = await dialog.showOpenDialog(
360
appState.secondaryWindow,
361
{properties: ["openFile"]}
362
);
363
if (!canceled && filePaths.length > 0) {
364
appState.secondaryWindow.webContents.send("configPath", filePaths);
365
}
366
});
367
368
ipcMain.on("useTray", (_event, enabled) => {
369
appState.trayEnabled = enabled;
370
if (enabled) createTray();
371
else {
372
appState.tray?.destroy();
373
appState.tray = null;
374
}
375
});
376
377
ipcMain.on("progress", (_event, percentage) => {
378
if (appState.mainWindow) appState.mainWindow.setProgressBar(percentage);
379
});
380
381
ipcMain.on("error_dialog", async (_event, message) => {
382
const {response} = await dialog.showMessageBox(appState.mainWindow, {
383
type: "error",
384
title: "Error",
385
message: message,
386
buttons: ["Ok", i18n("clickToCopy")],
387
});
388
if (response === 1) clipboard.writeText(message);
389
});
390
391
ipcMain.handle("get-system-locale", async (_event) => {
392
return app.getSystemLocale();
393
});
394
395
ipcMain.handle("get-translation", (_event, locale) => {
396
const fallbackFile = path.join(__dirname, "translations", "en.json");
397
const localeFile = path.join(
398
__dirname,
399
"translations",
400
`${locale}.json`
401
);
402
403
const fallbackData = JSON.parse(readFileSync(fallbackFile, "utf8"));
404
405
let localeData = {};
406
if (locale !== "en" && existsSync(localeFile)) {
407
try {
408
localeData = JSON.parse(readFileSync(localeFile, "utf8"));
409
} catch (e) {
410
console.error(`Could not parse ${localeFile}`, e);
411
}
412
}
413
414
const mergedTranslations = {...fallbackData, ...localeData};
415
416
return mergedTranslations;
417
});
418
419
ipcMain.handle("get-download-history", () =>
420
appState.downloadHistory.getHistory()
421
);
422
ipcMain.handle("add-to-history", (_, info) =>
423
appState.downloadHistory.addDownload(info)
424
);
425
ipcMain.handle("get-download-stats", () =>
426
appState.downloadHistory.getStats()
427
);
428
ipcMain.handle("delete-history-item", (_, id) =>
429
appState.downloadHistory.removeHistoryItem(id)
430
);
431
ipcMain.handle("clear-all-history", async () => {
432
await appState.downloadHistory.clearHistory();
433
return true;
434
});
435
ipcMain.handle("export-history-json", () =>
436
appState.downloadHistory.exportAsJSON()
437
);
438
ipcMain.handle("export-history-csv", () =>
439
appState.downloadHistory.exportAsCSV()
440
);
441
}
442
443
function registerAutoUpdaterEvents() {
444
autoUpdater.on("update-available", async (info) => {
445
const dialogOpts = {
446
type: "info",
447
buttons: [i18n("update"), i18n("no")],
448
title: "Update Available",
449
message: i18n("updateAvailablePrompt"),
450
detail:
451
info.releaseNotes?.toString().replace(/<[^>]*>?/gm, "") ||
452
"No details available.",
453
};
454
const {response} = await dialog.showMessageBox(
455
appState.mainWindow,
456
dialogOpts
457
);
458
if (response === 0) {
459
autoUpdater.downloadUpdate();
460
}
461
});
462
463
autoUpdater.on("update-downloaded", async () => {
464
appState.mainWindow.webContents.send("update-downloaded", "");
465
const dialogOpts = {
466
type: "info",
467
buttons: [i18n("restart"), i18n("later")],
468
title: "Update Ready",
469
message: i18n("installAndRestartPrompt"),
470
};
471
const {response} = await dialog.showMessageBox(
472
appState.mainWindow,
473
dialogOpts
474
);
475
if (response === 0) {
476
autoUpdater.quitAndInstall();
477
}
478
});
479
480
autoUpdater.on("download-progress", async (info) => {
481
appState.mainWindow.webContents.send("download-progress", info.percent);
482
});
483
484
autoUpdater.on("error", (error) => {
485
console.error("Auto-update error:", error);
486
dialog.showErrorBox("Update Error", i18n("updateError"));
487
});
488
}
489
490
/**
491
* @param {string} phrase The key to translate.
492
* @returns {string} The translated string or the key itself.
493
*/
494
function i18n(phrase) {
495
return appState.loadedLanguage[phrase] || phrase;
496
}
497
498
/**
499
* Loads the configuration from the config file.
500
*/
501
async function loadConfiguration() {
502
try {
503
const fileContent = await fs.readFile(CONFIG_FILE_PATH, "utf8");
504
appState.config = JSON.parse(fileContent);
505
} catch (error) {
506
console.log(
507
"Could not load config file, using defaults.",
508
error.message
509
);
510
appState.config = {
511
bounds: {width: 1024, height: 768},
512
isMaximized: false,
513
};
514
}
515
}
516
517
async function saveConfiguration() {
518
try {
519
await fs.writeFile(CONFIG_FILE_PATH, JSON.stringify(appState.config));
520
} catch (error) {
521
console.error("Failed to save configuration:", error);
522
}
523
}
524
525
async function loadTranslations() {
526
const locale = app.getSystemLocale();
527
console.log({locale});
528
const defaultLangPath = path.join(__dirname, "translations", "en.json");
529
let langPath = path.join(__dirname, "translations", `${locale}.json`);
530
531
try {
532
await fs.access(langPath);
533
} catch {
534
langPath = defaultLangPath;
535
}
536
537
try {
538
const fileContent = await fs.readFile(langPath, "utf8");
539
appState.loadedLanguage = JSON.parse(fileContent);
540
} catch (error) {
541
console.error("Failed to load translation file:", error);
542
appState.loadedLanguage = {};
543
}
544
}
545
546