@glassnote/client 2.4.9 → 2.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/main.js +732 -725
  2. package/package.json +2 -1
  3. package/versionReport.js +156 -0
package/main.js CHANGED
@@ -1,725 +1,732 @@
1
- const {
2
- app,
3
- BrowserWindow,
4
- Notification,
5
- Tray,
6
- Menu,
7
- nativeImage,
8
- screen,
9
- } = require('electron');
10
- const packageJson = require('./package.json');
11
- const LocalServer = require('./localserver');
12
- const userData = require('./userData');
13
- const { ipcMain } = require('electron');
14
- const path = require('path');
15
- const fs = require('fs');
16
- const AutoStart = require('./autoStart');
17
- const AutoUpdate = require('./autoupdate');
18
- const NpmUpdate = require('./npmUpdate');
19
- const npmMode = require('./npmMode');
20
- const AutoUpdateRenderer = require('./autoupdate_renderer');
21
- const { baselineRendererDir, rendererVersionFrom: getRendererVersionFromPath } = require('./rendererPaths');
22
- const { createTailUtility } = require('./logUtilities');
23
-
24
- const logRenderer = packageJson.logRenderer !== undefined ? packageJson.logRenderer : true;
25
-
26
- // Sin empaquetar, electron llamaría a la app "Electron" en la bandeja, el menú de macOS
27
- // y las notificaciones. Instalado por npm eso es siempre el caso, así que se nombra a
28
- // mano. Los datos de usuario no dependen de esto (userData.js calcula su propia ruta).
29
- app.setName('glassnote');
30
-
31
- let mainWindow;
32
- let localServer = null;
33
- let tray = null;
34
- let lockCheckerInterval = null;
35
- let lockCheckerTimeout = null;
36
- let isCleaningUp = false;
37
-
38
-
39
- // Inicializar y pasar datos de usuario
40
- const uuid = userData.getUUID();
41
-
42
- // Configure logging - usar el directorio personalizado en lugar de Electron
43
- const logsDir = path.join(userData.getUserDataDir(), 'logs');
44
- if (!fs.existsSync(logsDir)) {
45
- fs.mkdirSync(logsDir, { recursive: true });
46
- }
47
-
48
- // Clean up old logs (older than 30 days)
49
- function cleanupOldLogs() {
50
- try {
51
- const files = fs.readdirSync(logsDir);
52
- const now = Date.now();
53
- const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
54
-
55
- files.forEach((file) => {
56
- if (file.endsWith('.log')) {
57
- const filePath = path.join(logsDir, file);
58
- const stats = fs.statSync(filePath);
59
- if (stats.mtimeMs < thirtyDaysAgo) {
60
- fs.unlinkSync(filePath);
61
- }
62
- }
63
- });
64
- } catch (error) {
65
- console.error('Error cleaning up old logs:', error);
66
- }
67
- }
68
-
69
- // Create log file for current session
70
- const currentDate = new Date().toISOString().split('T')[0];
71
- const logFilePath = path.join(logsDir, `glassnote-${currentDate}.log`);
72
-
73
- // Override console.log to write to file
74
- const originalConsoleLog = console.log;
75
- console.log = function (...args) {
76
- const timestamp = new Date().toISOString();
77
- const message = args
78
- .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
79
- .join(' ');
80
-
81
- const logMessage = `[${timestamp}] ${message}\n`;
82
-
83
-
84
- // Write to file
85
- try {
86
- fs.appendFileSync(logFilePath, logMessage, 'utf8');
87
- } catch (error) {
88
- originalConsoleLog.apply(console, ['Error writing to log file:', error]);
89
- }
90
-
91
- // Call original console.log
92
- originalConsoleLog.apply(console, args);
93
- };
94
- console.warn('====== APPLICATION START ======');
95
- let initialLock = Date.now().toString();
96
-
97
- // Override console.error as well
98
- const originalConsoleError = console.error;
99
- console.error = function (...args) {
100
- const timestamp = new Date().toISOString();
101
- const message = args
102
- .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
103
- .join(' ');
104
-
105
- const logMessage = `[${timestamp}] ERROR: ${message}\n`;
106
-
107
- // Write to file
108
- try {
109
- fs.appendFileSync(logFilePath, logMessage, 'utf8');
110
- } catch (error) {
111
- originalConsoleError.apply(console, ['Error writing to log file:', error]);
112
- }
113
-
114
- // Call original console.error
115
- originalConsoleError.apply(console, args);
116
- };
117
-
118
- // Clean up old logs on startup
119
- cleanupOldLogs();
120
-
121
- // Create tail utility in logs directory for debugging
122
- createTailUtility(logsDir);
123
-
124
- app.whenReady().then(() => {
125
- // macOS: esto es una app de bandeja, se maneja desde la barra de menu. Instalada por
126
- // npm no hay .app propio, asi que el Dock mostraba el icono por defecto de Electron y
127
- // una entrada que no sirve para nada. Se saca del Dock, pero antes se le pone el icono
128
- // de glassnote: si algo lo vuelve a mostrar (un dialogo modal lo hace), que al menos
129
- // no aparezca como "Electron".
130
- if (process.platform === 'darwin' && app.dock) {
131
- try {
132
- const dockIcon = nativeImage.createFromPath(
133
- path.join(__dirname, 'images', 'icon-512.png')
134
- );
135
- if (!dockIcon.isEmpty()) app.dock.setIcon(dockIcon);
136
- } catch (error) {
137
- console.warn('[dock] no se pudo poner el icono:', error.message);
138
- }
139
- app.dock.hide();
140
- }
141
-
142
- // Configure auto-start - this will handle shortcut creation
143
- //
144
- // Corriendo por `npx` no se registra nada: la app vive en la caché de npx, que se
145
- // limpia sola. Una entrada de arranque automático apuntando ahí queda rota sin que
146
- // nadie se entere. Quien quiera que arranque solo, instala: `npx glassnote install`.
147
- if (npmMode.isEphemeral()) {
148
- console.log('[autostart] modo npx: no se registra el arranque automático');
149
- } else {
150
- try {
151
- const autoStart = new AutoStart();
152
- const autoStartResult = autoStart.configureAutoStart();
153
- if (!autoStartResult) {
154
- console.log('Auto-start configuration may have failed, but application will continue running normally');
155
- }
156
- } catch (error) {
157
- console.error('Auto-start configuration failed, but application will continue running normally:', error.message);
158
- }
159
- }
160
-
161
- // Configure auto-update
162
- //
163
- // Instalado por npm no hay instalador que bajar: actualizar es `npm install -g` y
164
- // reabrir. El updater de S3 (.exe/.dmg) queda solo para los builds nativos, que
165
- // están en deprecación — ver README, sección «Instalación».
166
- const autoUpdate = npmMode.isNpmMode() ? new NpmUpdate() : new AutoUpdate();
167
- autoUpdate.start();
168
-
169
- // Create system tray with platform-appropriate icon
170
- let iconPath;
171
- if (process.platform === 'win32') {
172
- iconPath = path.join(__dirname, 'images/icon.ico');
173
- } else if (process.platform === 'darwin') {
174
- // macOS uses smaller tray icons - use icon-16.png for proper sizing
175
- iconPath = path.join(__dirname, 'images/icon-16.png');
176
- } else {
177
- iconPath = path.join(__dirname, 'images/icon.png');
178
- }
179
- tray = new Tray(iconPath);
180
- const contextMenu = Menu.buildFromTemplate([
181
- {
182
- label: `glassnote v${packageJson.version}`,
183
- enabled: false,
184
- },
185
- { type: 'separator' },
186
- {
187
- label: 'Config Menu',
188
- click: () => {
189
- // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
190
- if (mainWindow && !mainWindow.isDestroyed()) {
191
- mainWindow.webContents.send('show-config-menu', 'review');
192
- }
193
- }
194
- }
195
- ]);
196
- tray.setToolTip(`GlassNote v${packageJson.version}`);
197
- tray.setContextMenu(contextMenu);
198
-
199
- // Agregar funcionalidad de click izquierdo para mostrar el Config Menu
200
- tray.on('click', () => {
201
- // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
202
- if (mainWindow && !mainWindow.isDestroyed()) {
203
- mainWindow.webContents.send('show-config-menu', 'review');
204
- }
205
- });
206
-
207
- // Platform-specific window configuration
208
- // On macOS: Use screen dimensions instead of fullscreen to avoid virtual desktop behavior
209
- // On Windows: Keep original fullscreen behavior
210
- let windowOptions = {
211
- transparent: true,
212
- frame: false,
213
- alwaysOnTop: true,
214
- skipTaskbar: true,
215
- resizable: false,
216
- x: 0,
217
- y: 0,
218
- acceptFirstMouse: true,
219
- webPreferences: {
220
- nodeIntegration: false,
221
- contextIsolation: true,
222
- enableRemoteModule: false,
223
- preload: path.join(__dirname, 'preload.js'),
224
- sandbox: false, // Allow access to Node.js modules in preload
225
- webSecurity: false, // Allow connections to local WebSocket servers
226
- },
227
- };
228
-
229
- if (process.platform === 'darwin') {
230
- // macOS
231
- const primaryDisplay = screen.getPrimaryDisplay();
232
- const { width, height } = primaryDisplay.workAreaSize;
233
- windowOptions.width = width;
234
- windowOptions.height = height;
235
- // Explicitly set fullscreen to false for macOS
236
- windowOptions.fullscreen = false;
237
- // La sombra de la ventana la dibuja macOS alrededor de lo que NO es transparente,
238
- // asi que en una ventana transparente sin marco la pinta pegada a cada imagen: eso
239
- // es el borde negro que se ve alrededor de los mensajes. Una capa de overlay no
240
- // necesita sombra.
241
- windowOptions.hasShadow = false;
242
- } else {
243
- // Windows and other platforms
244
- windowOptions.width = 400;
245
- windowOptions.height = 300;
246
- windowOptions.fullscreen = true;
247
- }
248
-
249
- mainWindow = new BrowserWindow(windowOptions);
250
-
251
- // Interceptar console.log de la página renderizada
252
- if (logRenderer) {
253
- mainWindow.webContents.on(
254
- 'console-message',
255
- (event, level, message, line, sourceId) => {
256
- const levels = ['', 'INFO', 'WARNING', 'ERROR', 'DEBUG'];
257
- const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
258
- const logMessage = `[${timestamp}] [RENDERER ${levels[level]}] ${message} (${sourceId}:${line})`;
259
-
260
- // Write to both console and log file
261
- console.info(logMessage);
262
- // Also write directly to log file to ensure it's captured
263
- const fileLogMessage = `[${timestamp}] ${logMessage}\n`;
264
- try {
265
- fs.appendFileSync(logFilePath, fileLogMessage, 'utf8');
266
- } catch (error) {
267
- console.error('Error writing renderer log to file:', error);
268
- }
269
- }
270
- );
271
- }
272
-
273
- // Generic userData operations via IPC
274
- ipcMain.on('get-user-data', (event, { key, nestedKey }) => {
275
- try {
276
- const value = userData.get(key, nestedKey);
277
-
278
- // Serialize the value for IPC communication
279
- let serializableValue;
280
- if (value === undefined || value === null) {
281
- serializableValue = null;
282
- } else {
283
- // Convert to JSON string and back to ensure proper serialization
284
- serializableValue = JSON.parse(JSON.stringify(value));
285
- }
286
-
287
- event.reply('user-data-response', { key, nestedKey, value: serializableValue, success: true });
288
- } catch (error) {
289
- console.error('Error getting user data for key:', key, 'nestedKey:', nestedKey, error);
290
- event.reply('user-data-response', { key, nestedKey, value: null, success: false, error: error.message });
291
- }
292
- });
293
-
294
- // Direct userData operation for IPC serialization issues
295
- ipcMain.on('get-user-data-direct', (event, { key, nestedKey }) => {
296
- try {
297
- const value = userData.get(key, nestedKey);
298
-
299
- // Use a different approach for direct communication
300
- // Send the value as a JSON string to avoid IPC serialization issues
301
- const response = {
302
- key,
303
- nestedKey,
304
- value: value ? JSON.stringify(value) : null,
305
- success: true
306
- };
307
-
308
- event.reply('user-data-direct-response', response);
309
- } catch (error) {
310
- console.error('Error getting user data directly for key:', key, 'nestedKey:', nestedKey, error);
311
- event.reply('user-data-direct-response', { key, nestedKey, value: null, success: false, error: error.message });
312
- }
313
- });
314
-
315
- ipcMain.on('set-user-data', (event, { key, nestedKey, value }) => {
316
- try {
317
- const success = userData.set(key, nestedKey, value);
318
- event.reply('user-data-response', { key, nestedKey, value, success });
319
-
320
- // If setting servers, send updated list to renderer
321
- if (key === 'servers' && Array.isArray(value) && nestedKey === undefined) {
322
- if (mainWindow && !mainWindow.isDestroyed()) {
323
- mainWindow.webContents.send('servers-list', value);
324
- }
325
- }
326
- } catch (error) {
327
- console.error('Error setting user data for key:', key, 'nestedKey:', nestedKey, error);
328
- event.reply('user-data-response', { key, nestedKey, value, success: false, error: error.message });
329
- }
330
- });
331
-
332
- ipcMain.on('remove-user-data', (event, { key, nestedKey }) => {
333
- try {
334
- const success = userData.remove(key, nestedKey);
335
- event.reply('user-data-response', { key, nestedKey, value: null, success });
336
- } catch (error) {
337
- console.error('Error removing user data for key:', key, 'nestedKey:', nestedKey, error);
338
- event.reply('user-data-response', { key, nestedKey, value: null, success: false, error: error.message });
339
- }
340
- });
341
-
342
- // Initialize renderer auto-update after mainWindow is created
343
- const autoUpdateRenderer = new AutoUpdateRenderer((version) => {
344
- // Recargar la ventana para aplicar los cambios
345
- setTimeout(() => {
346
- reloadMainWindow();
347
- }, 1000);
348
- });
349
- autoUpdateRenderer.start();
350
-
351
- // Cargar el HTML del renderer (priorizar userData, fallback a app)
352
- loadRendererHtml(mainWindow, uuid);
353
-
354
- // Mouse events control
355
- mainWindow.setIgnoreMouseEvents(true, { forward: true });
356
- ipcMain.on('set-ignore-events-true', () => {
357
- mainWindow.setIgnoreMouseEvents(true, { forward: true });
358
- });
359
- ipcMain.on('set-ignore-events-false', () => {
360
- mainWindow.setIgnoreMouseEvents(false, { forward: true });
361
- });
362
-
363
- // Window visibility control
364
- ipcMain.on('show-window', () => {
365
- if (mainWindow && !mainWindow.isDestroyed()) {
366
- mainWindow.show();
367
- }
368
- });
369
-
370
- ipcMain.on('hide-window', () => {
371
- if (mainWindow && !mainWindow.isDestroyed()) {
372
- mainWindow.hide();
373
- }
374
- });
375
-
376
- mainWindow.setAlwaysOnTop(true, 'screen-saver');
377
- mainWindow.webContents.on('did-finish-load', () => {
378
- mainWindow.webContents.send('servers-list', userData.readData().servers);
379
- mainWindow.webContents.send('app-data', { version: packageJson.version });
380
- });
381
-
382
-
383
- // Handle external URL opening
384
- ipcMain.on('open-external', (event, url) => {
385
- const { shell } = require('electron');
386
- shell.openExternal(url);
387
- });
388
-
389
- ipcMain.on('open-external-browser', (event, url) => {
390
- const { shell } = require('electron');
391
- shell.openExternal(url);
392
- });
393
-
394
- // Handle registration window opening (larger window without menus)
395
- ipcMain.on('open-registration-window', (event, url) => {
396
- // Create a custom browser window for registration
397
- const registrationWindow = new BrowserWindow({
398
- width: 1200,
399
- height: 800,
400
- minWidth: 800,
401
- minHeight: 600,
402
- show: false,
403
- titleBarStyle: 'default',
404
- autoHideMenuBar: true,
405
- webPreferences: {
406
- nodeIntegration: false,
407
- contextIsolation: true,
408
- webSecurity: true
409
- }
410
- });
411
-
412
- // Remove menu bar
413
- registrationWindow.setMenuBarVisibility(false);
414
-
415
- // Load the URL
416
- registrationWindow.loadURL(url);
417
-
418
- // Show window when ready
419
- registrationWindow.once('ready-to-show', () => {
420
- registrationWindow.show();
421
- });
422
-
423
- // Handle window closed
424
- registrationWindow.on('closed', () => {
425
- // Window closed, no action needed
426
- });
427
- });
428
-
429
- mainWindow.on('ready-to-show', () => {
430
- mainWindow.show();
431
-
432
- // Use LocalServer singleton
433
- if (!localServer) {
434
- localServer = new LocalServer();
435
- localServer.start(mainWindow);
436
- } else {
437
- // Update the mainWindow reference if server already exists
438
- localServer.updateMainWindow(mainWindow);
439
- }
440
- });
441
-
442
- lockChecker();
443
- });
444
-
445
- app.on('window-all-closed', () => {
446
- // Don't quit when all windows are closed if we have a tray
447
- if (tray && process.platform !== 'darwin') {
448
- // Keep the app running with tray
449
- } else {
450
- cleanupAndQuit();
451
- }
452
- });
453
-
454
- // Clean up on quit
455
- app.on('before-quit', () => {
456
- if (!isCleaningUp) {
457
- // Clear intervals and timeouts
458
- if (lockCheckerInterval) {
459
- clearInterval(lockCheckerInterval);
460
- lockCheckerInterval = null;
461
- }
462
- if (lockCheckerTimeout) {
463
- clearTimeout(lockCheckerTimeout);
464
- lockCheckerTimeout = null;
465
- }
466
-
467
- // Destroy main window
468
- if (mainWindow && !mainWindow.isDestroyed()) {
469
- mainWindow.destroy();
470
- mainWindow = null;
471
- }
472
-
473
- // Stop local server
474
- if (localServer) {
475
- localServer.stop();
476
- localServer = null;
477
- }
478
-
479
- // Destroy tray
480
- if (tray) {
481
- tray.destroy();
482
- tray = null;
483
- }
484
- }
485
- });
486
-
487
- function lockChecker() {
488
- lockCheckerTimeout = setTimeout(() => {
489
- userData.set('lock', initialLock);
490
- }, 1000);
491
- lockCheckerInterval = setInterval(() => {
492
- // Leer el lock directamente del archivo, no del cache
493
- const currentData = userData.getLockFromFile();
494
- // Si currentData es undefined, significa que el archivo .glassnote fue borrado o corrompido
495
- // También verificar si el valor del lock cambió
496
- if (currentData === undefined || currentData.toString() != initialLock.toString()) {
497
- cleanupAndQuit();
498
- }
499
- }, 10000); // Check every 10 seconds
500
- }
501
-
502
- function cleanupAndQuit() {
503
- // Prevent multiple cleanup calls
504
- if (isCleaningUp) {
505
- return;
506
- }
507
- isCleaningUp = true;
508
-
509
-
510
- // Clear intervals and timeouts
511
- if (lockCheckerInterval) {
512
- clearInterval(lockCheckerInterval);
513
- lockCheckerInterval = null;
514
- }
515
- if (lockCheckerTimeout) {
516
- clearTimeout(lockCheckerTimeout);
517
- lockCheckerTimeout = null;
518
- }
519
-
520
- // Destroy main window
521
- if (mainWindow && !mainWindow.isDestroyed()) {
522
- mainWindow.destroy();
523
- mainWindow = null;
524
- }
525
-
526
- // Stop local server
527
- if (localServer) {
528
- localServer.stop();
529
- localServer = null;
530
- }
531
-
532
- // Destroy tray
533
- if (tray) {
534
- tray.destroy();
535
- tray = null;
536
- }
537
-
538
- // Force exit after a timeout to ensure process terminates
539
- setTimeout(() => {
540
- app.exit(0);
541
- // Double safety: exit process after another timeout
542
- setTimeout(() => {
543
- process.exit(0);
544
- }, 1000);
545
- }, 500);
546
- }
547
-
548
- // Handle process signals for clean shutdown
549
- process.on('SIGINT', () => {
550
- cleanupAndQuit();
551
- });
552
-
553
- process.on('SIGTERM', () => {
554
- cleanupAndQuit();
555
- });
556
-
557
- // Handle uncaught exceptions to prevent zombie processes
558
- process.on('uncaughtException', (error) => {
559
- console.error('Uncaught exception:', error);
560
- cleanupAndQuit();
561
- });
562
-
563
- process.on('unhandledRejection', (reason, promise) => {
564
- console.error('Unhandled promise rejection:', reason);
565
- cleanupAndQuit();
566
- });
567
-
568
- // Start lock checker after everything is initialized
569
-
570
-
571
-
572
- /**
573
- * Carga el HTML del renderer desde la fuente con versión SUPERIOR.
574
- *
575
- * Dos fuentes, y la division importa:
576
- * - userData: lo que bajo el auto-update de S3. Es el canal EN CALIENTE: una version
577
- * nueva del renderer llega ahi sin republicar el cliente ni tocar el binario.
578
- * - baseline: el renderer que viaja con la instalacion. Es lo que ve una instalacion
579
- * nueva antes de su primer update en caliente.
580
- *
581
- * Gana la version mas alta, sin importar de donde venga; empate, gana userData.
582
- */
583
- // baselineRendererDir vive en rendererPaths.js: lo comparte con el updater del renderer.
584
-
585
- function loadRendererHtml(window, uuid) {
586
- // Definir las dos fuentes a considerar con archivos HTML alternativos
587
- const sources = [
588
- {
589
- name: 'userData',
590
- dir: path.join(userData.getUserDataDir(), 'renderer'),
591
- htmlFiles: ['glassnote.html'], // userData siempre usa glassnote.html
592
- type: 'auto-update'
593
- },
594
- {
595
- name: 'baseline',
596
- dir: baselineRendererDir(),
597
- htmlFiles: ['glassnote.html', 'index.html'], // Intenta glassnote.html primero, luego index.html
598
- type: 'paquete'
599
- }
600
- ];
601
-
602
-
603
- // Obtener versiones y verificar archivos HTML para ambas fuentes
604
- const sourcesWithInfo = sources.map(source => {
605
- const version = getRendererVersionFromPath(source.dir);
606
-
607
- // Buscar el primer archivo HTML que exista
608
- let htmlPath = null;
609
- let htmlFile = null;
610
- for (const file of source.htmlFiles) {
611
- const potentialPath = path.join(source.dir, file);
612
- if (fs.existsSync(potentialPath)) {
613
- htmlPath = potentialPath;
614
- htmlFile = file;
615
- break;
616
- }
617
- }
618
-
619
- const htmlExists = htmlPath !== null;
620
-
621
- return {
622
- ...source,
623
- version,
624
- htmlPath,
625
- htmlFile,
626
- htmlExists
627
- };
628
- });
629
-
630
- // Log de información encontrada
631
- sourcesWithInfo.forEach(source => {
632
- if (source.htmlExists) {
633
- } else {
634
- console.log(` ${source.name}: versión ${source.version}, SIN ARCHIVO HTML`);
635
- }
636
- });
637
-
638
- // Comparar versiones para determinar cuál es superior
639
- const versionComparison = compareVersions(
640
- sourcesWithInfo[0].version,
641
- sourcesWithInfo[1].version
642
- );
643
-
644
- // Determinar fuente con versión superior
645
- let superiorSource;
646
- if (versionComparison > 0) {
647
- // userData tiene versión superior
648
- superiorSource = sourcesWithInfo[0];
649
- } else if (versionComparison < 0) {
650
- // glassnote-renderer/dist tiene versión superior
651
- superiorSource = sourcesWithInfo[1];
652
- } else {
653
- // Versiones iguales, priorizar userData (auto-update)
654
- superiorSource = sourcesWithInfo[0];
655
- }
656
-
657
- // Intentar cargar desde la fuente con versión superior
658
- if (superiorSource.htmlExists) {
659
- window.loadFile(superiorSource.htmlPath, {
660
- query: { uuid: uuid, version: packageJson.version }
661
- });
662
- return;
663
- }
664
-
665
- // Si la fuente con versión superior no tiene HTML, intentar la otra fuente
666
- const otherSource = superiorSource.name === 'userData' ? sourcesWithInfo[1] : sourcesWithInfo[0];
667
-
668
- if (otherSource.htmlExists) {
669
-
670
- window.loadFile(otherSource.htmlPath, {
671
- query: { uuid: uuid, version: packageJson.version }
672
- });
673
- return;
674
- }
675
-
676
- // Si ninguna fuente tiene HTML, error
677
- console.error('ERROR: No se encontró archivo HTML del renderer en ninguna fuente');
678
- console.error('userData buscó:', sourcesWithInfo[0].htmlFiles.join(', '));
679
- console.error(`baseline (${sourcesWithInfo[1].dir}) buscó:`, sourcesWithInfo[1].htmlFiles.join(', '));
680
-
681
- // Error fatal - no hay renderer disponible
682
- console.error('ERROR FATAL: No se puede cargar el renderer');
683
- // Intentar cargar una página de error o cerrar la aplicación
684
- window.loadURL('data:text/html,<h1>Error: No se pudo cargar el renderer</h1>');
685
- }
686
-
687
- /**
688
- * Compara dos versiones semánticas (formato: major.minor.patch)
689
- * Retorna 1 si version1 > version2
690
- * Retorna 0 si version1 == version2
691
- * Retorna -1 si version1 < version2
692
- */
693
- function compareVersions(version1, version2) {
694
- const v1Parts = version1.split('.').map((part) => parseInt(part) || 0);
695
- const v2Parts = version2.split('.').map((part) => parseInt(part) || 0);
696
-
697
- // Comparar partes principales (major, minor, patch)
698
- for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
699
- const v1Part = v1Parts[i] || 0;
700
- const v2Part = v2Parts[i] || 0;
701
-
702
- if (v1Part > v2Part) return 1;
703
- if (v1Part < v2Part) return -1;
704
- }
705
-
706
- return 0; // Las versiones son iguales
707
- }
708
-
709
- // getRendererVersionFromPath = rendererVersionFrom de rendererPaths.js (ver el require de arriba).
710
-
711
- /**
712
- * Recarga el main window después de una actualización del renderer
713
- */
714
- function reloadMainWindow() {
715
- if (mainWindow && !mainWindow.isDestroyed()) {
716
-
717
- // Update LocalServer with the new mainWindow reference
718
- if (localServer) {
719
- localServer.updateMainWindow(mainWindow);
720
- }
721
-
722
- loadRendererHtml(mainWindow, uuid);
723
- }
724
- }
725
-
1
+ const {
2
+ app,
3
+ BrowserWindow,
4
+ Notification,
5
+ Tray,
6
+ Menu,
7
+ nativeImage,
8
+ screen,
9
+ } = require('electron');
10
+ const packageJson = require('./package.json');
11
+ const LocalServer = require('./localserver');
12
+ const userData = require('./userData');
13
+ const { ipcMain } = require('electron');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const AutoStart = require('./autoStart');
17
+ const AutoUpdate = require('./autoupdate');
18
+ const NpmUpdate = require('./npmUpdate');
19
+ const VersionReport = require('./versionReport');
20
+ const npmMode = require('./npmMode');
21
+ const AutoUpdateRenderer = require('./autoupdate_renderer');
22
+ const { baselineRendererDir, rendererVersionFrom: getRendererVersionFromPath } = require('./rendererPaths');
23
+ const { createTailUtility } = require('./logUtilities');
24
+
25
+ const logRenderer = packageJson.logRenderer !== undefined ? packageJson.logRenderer : true;
26
+
27
+ // Sin empaquetar, electron llamaría a la app "Electron" en la bandeja, el menú de macOS
28
+ // y las notificaciones. Instalado por npm eso es siempre el caso, así que se nombra a
29
+ // mano. Los datos de usuario no dependen de esto (userData.js calcula su propia ruta).
30
+ app.setName('glassnote');
31
+
32
+ let mainWindow;
33
+ let localServer = null;
34
+ let tray = null;
35
+ let lockCheckerInterval = null;
36
+ let lockCheckerTimeout = null;
37
+ let isCleaningUp = false;
38
+
39
+
40
+ // Inicializar y pasar datos de usuario
41
+ const uuid = userData.getUUID();
42
+
43
+ // Configure logging - usar el directorio personalizado en lugar de Electron
44
+ const logsDir = path.join(userData.getUserDataDir(), 'logs');
45
+ if (!fs.existsSync(logsDir)) {
46
+ fs.mkdirSync(logsDir, { recursive: true });
47
+ }
48
+
49
+ // Clean up old logs (older than 30 days)
50
+ function cleanupOldLogs() {
51
+ try {
52
+ const files = fs.readdirSync(logsDir);
53
+ const now = Date.now();
54
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
55
+
56
+ files.forEach((file) => {
57
+ if (file.endsWith('.log')) {
58
+ const filePath = path.join(logsDir, file);
59
+ const stats = fs.statSync(filePath);
60
+ if (stats.mtimeMs < thirtyDaysAgo) {
61
+ fs.unlinkSync(filePath);
62
+ }
63
+ }
64
+ });
65
+ } catch (error) {
66
+ console.error('Error cleaning up old logs:', error);
67
+ }
68
+ }
69
+
70
+ // Create log file for current session
71
+ const currentDate = new Date().toISOString().split('T')[0];
72
+ const logFilePath = path.join(logsDir, `glassnote-${currentDate}.log`);
73
+
74
+ // Override console.log to write to file
75
+ const originalConsoleLog = console.log;
76
+ console.log = function (...args) {
77
+ const timestamp = new Date().toISOString();
78
+ const message = args
79
+ .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
80
+ .join(' ');
81
+
82
+ const logMessage = `[${timestamp}] ${message}\n`;
83
+
84
+
85
+ // Write to file
86
+ try {
87
+ fs.appendFileSync(logFilePath, logMessage, 'utf8');
88
+ } catch (error) {
89
+ originalConsoleLog.apply(console, ['Error writing to log file:', error]);
90
+ }
91
+
92
+ // Call original console.log
93
+ originalConsoleLog.apply(console, args);
94
+ };
95
+ console.warn('====== APPLICATION START ======');
96
+ let initialLock = Date.now().toString();
97
+
98
+ // Override console.error as well
99
+ const originalConsoleError = console.error;
100
+ console.error = function (...args) {
101
+ const timestamp = new Date().toISOString();
102
+ const message = args
103
+ .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
104
+ .join(' ');
105
+
106
+ const logMessage = `[${timestamp}] ERROR: ${message}\n`;
107
+
108
+ // Write to file
109
+ try {
110
+ fs.appendFileSync(logFilePath, logMessage, 'utf8');
111
+ } catch (error) {
112
+ originalConsoleError.apply(console, ['Error writing to log file:', error]);
113
+ }
114
+
115
+ // Call original console.error
116
+ originalConsoleError.apply(console, args);
117
+ };
118
+
119
+ // Clean up old logs on startup
120
+ cleanupOldLogs();
121
+
122
+ // Create tail utility in logs directory for debugging
123
+ createTailUtility(logsDir);
124
+
125
+ app.whenReady().then(() => {
126
+ // macOS: esto es una app de bandeja, se maneja desde la barra de menu. Instalada por
127
+ // npm no hay .app propio, asi que el Dock mostraba el icono por defecto de Electron y
128
+ // una entrada que no sirve para nada. Se saca del Dock, pero antes se le pone el icono
129
+ // de glassnote: si algo lo vuelve a mostrar (un dialogo modal lo hace), que al menos
130
+ // no aparezca como "Electron".
131
+ if (process.platform === 'darwin' && app.dock) {
132
+ try {
133
+ const dockIcon = nativeImage.createFromPath(
134
+ path.join(__dirname, 'images', 'icon-512.png')
135
+ );
136
+ if (!dockIcon.isEmpty()) app.dock.setIcon(dockIcon);
137
+ } catch (error) {
138
+ console.warn('[dock] no se pudo poner el icono:', error.message);
139
+ }
140
+ app.dock.hide();
141
+ }
142
+
143
+ // Configure auto-start - this will handle shortcut creation
144
+ //
145
+ // Corriendo por `npx` no se registra nada: la app vive en la caché de npx, que se
146
+ // limpia sola. Una entrada de arranque automático apuntando ahí queda rota sin que
147
+ // nadie se entere. Quien quiera que arranque solo, instala: `npx glassnote install`.
148
+ if (npmMode.isEphemeral()) {
149
+ console.log('[autostart] modo npx: no se registra el arranque automático');
150
+ } else {
151
+ try {
152
+ const autoStart = new AutoStart();
153
+ const autoStartResult = autoStart.configureAutoStart();
154
+ if (!autoStartResult) {
155
+ console.log('Auto-start configuration may have failed, but application will continue running normally');
156
+ }
157
+ } catch (error) {
158
+ console.error('Auto-start configuration failed, but application will continue running normally:', error.message);
159
+ }
160
+ }
161
+
162
+ // Configure auto-update
163
+ //
164
+ // Instalado por npm no hay instalador que bajar: actualizar es `npm install -g` y
165
+ // reabrir. El updater de S3 (.exe/.dmg) queda solo para los builds nativos, que
166
+ // están en deprecación — ver README, sección «Instalación».
167
+ const autoUpdate = npmMode.isNpmMode() ? new NpmUpdate() : new AutoUpdate();
168
+ autoUpdate.start();
169
+
170
+ // Le dice a la API qué versión corre este equipo, y trae de vuelta la que debería
171
+ // correr. Sin esto la API no sabía la versión de nadie: un cliente clavado se ve
172
+ // igual que uno al día, y así es como la auto-actualización pudo estar rota meses.
173
+ const versionReport = new VersionReport();
174
+ versionReport.start();
175
+
176
+ // Create system tray with platform-appropriate icon
177
+ let iconPath;
178
+ if (process.platform === 'win32') {
179
+ iconPath = path.join(__dirname, 'images/icon.ico');
180
+ } else if (process.platform === 'darwin') {
181
+ // macOS uses smaller tray icons - use icon-16.png for proper sizing
182
+ iconPath = path.join(__dirname, 'images/icon-16.png');
183
+ } else {
184
+ iconPath = path.join(__dirname, 'images/icon.png');
185
+ }
186
+ tray = new Tray(iconPath);
187
+ const contextMenu = Menu.buildFromTemplate([
188
+ {
189
+ label: `glassnote v${packageJson.version}`,
190
+ enabled: false,
191
+ },
192
+ { type: 'separator' },
193
+ {
194
+ label: 'Config Menu',
195
+ click: () => {
196
+ // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
197
+ if (mainWindow && !mainWindow.isDestroyed()) {
198
+ mainWindow.webContents.send('show-config-menu', 'review');
199
+ }
200
+ }
201
+ }
202
+ ]);
203
+ tray.setToolTip(`GlassNote v${packageJson.version}`);
204
+ tray.setContextMenu(contextMenu);
205
+
206
+ // Agregar funcionalidad de click izquierdo para mostrar el Config Menu
207
+ tray.on('click', () => {
208
+ // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
209
+ if (mainWindow && !mainWindow.isDestroyed()) {
210
+ mainWindow.webContents.send('show-config-menu', 'review');
211
+ }
212
+ });
213
+
214
+ // Platform-specific window configuration
215
+ // On macOS: Use screen dimensions instead of fullscreen to avoid virtual desktop behavior
216
+ // On Windows: Keep original fullscreen behavior
217
+ let windowOptions = {
218
+ transparent: true,
219
+ frame: false,
220
+ alwaysOnTop: true,
221
+ skipTaskbar: true,
222
+ resizable: false,
223
+ x: 0,
224
+ y: 0,
225
+ acceptFirstMouse: true,
226
+ webPreferences: {
227
+ nodeIntegration: false,
228
+ contextIsolation: true,
229
+ enableRemoteModule: false,
230
+ preload: path.join(__dirname, 'preload.js'),
231
+ sandbox: false, // Allow access to Node.js modules in preload
232
+ webSecurity: false, // Allow connections to local WebSocket servers
233
+ },
234
+ };
235
+
236
+ if (process.platform === 'darwin') {
237
+ // macOS
238
+ const primaryDisplay = screen.getPrimaryDisplay();
239
+ const { width, height } = primaryDisplay.workAreaSize;
240
+ windowOptions.width = width;
241
+ windowOptions.height = height;
242
+ // Explicitly set fullscreen to false for macOS
243
+ windowOptions.fullscreen = false;
244
+ // La sombra de la ventana la dibuja macOS alrededor de lo que NO es transparente,
245
+ // asi que en una ventana transparente sin marco la pinta pegada a cada imagen: eso
246
+ // es el borde negro que se ve alrededor de los mensajes. Una capa de overlay no
247
+ // necesita sombra.
248
+ windowOptions.hasShadow = false;
249
+ } else {
250
+ // Windows and other platforms
251
+ windowOptions.width = 400;
252
+ windowOptions.height = 300;
253
+ windowOptions.fullscreen = true;
254
+ }
255
+
256
+ mainWindow = new BrowserWindow(windowOptions);
257
+
258
+ // Interceptar console.log de la página renderizada
259
+ if (logRenderer) {
260
+ mainWindow.webContents.on(
261
+ 'console-message',
262
+ (event, level, message, line, sourceId) => {
263
+ const levels = ['', 'INFO', 'WARNING', 'ERROR', 'DEBUG'];
264
+ const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
265
+ const logMessage = `[${timestamp}] [RENDERER ${levels[level]}] ${message} (${sourceId}:${line})`;
266
+
267
+ // Write to both console and log file
268
+ console.info(logMessage);
269
+ // Also write directly to log file to ensure it's captured
270
+ const fileLogMessage = `[${timestamp}] ${logMessage}\n`;
271
+ try {
272
+ fs.appendFileSync(logFilePath, fileLogMessage, 'utf8');
273
+ } catch (error) {
274
+ console.error('Error writing renderer log to file:', error);
275
+ }
276
+ }
277
+ );
278
+ }
279
+
280
+ // Generic userData operations via IPC
281
+ ipcMain.on('get-user-data', (event, { key, nestedKey }) => {
282
+ try {
283
+ const value = userData.get(key, nestedKey);
284
+
285
+ // Serialize the value for IPC communication
286
+ let serializableValue;
287
+ if (value === undefined || value === null) {
288
+ serializableValue = null;
289
+ } else {
290
+ // Convert to JSON string and back to ensure proper serialization
291
+ serializableValue = JSON.parse(JSON.stringify(value));
292
+ }
293
+
294
+ event.reply('user-data-response', { key, nestedKey, value: serializableValue, success: true });
295
+ } catch (error) {
296
+ console.error('Error getting user data for key:', key, 'nestedKey:', nestedKey, error);
297
+ event.reply('user-data-response', { key, nestedKey, value: null, success: false, error: error.message });
298
+ }
299
+ });
300
+
301
+ // Direct userData operation for IPC serialization issues
302
+ ipcMain.on('get-user-data-direct', (event, { key, nestedKey }) => {
303
+ try {
304
+ const value = userData.get(key, nestedKey);
305
+
306
+ // Use a different approach for direct communication
307
+ // Send the value as a JSON string to avoid IPC serialization issues
308
+ const response = {
309
+ key,
310
+ nestedKey,
311
+ value: value ? JSON.stringify(value) : null,
312
+ success: true
313
+ };
314
+
315
+ event.reply('user-data-direct-response', response);
316
+ } catch (error) {
317
+ console.error('Error getting user data directly for key:', key, 'nestedKey:', nestedKey, error);
318
+ event.reply('user-data-direct-response', { key, nestedKey, value: null, success: false, error: error.message });
319
+ }
320
+ });
321
+
322
+ ipcMain.on('set-user-data', (event, { key, nestedKey, value }) => {
323
+ try {
324
+ const success = userData.set(key, nestedKey, value);
325
+ event.reply('user-data-response', { key, nestedKey, value, success });
326
+
327
+ // If setting servers, send updated list to renderer
328
+ if (key === 'servers' && Array.isArray(value) && nestedKey === undefined) {
329
+ if (mainWindow && !mainWindow.isDestroyed()) {
330
+ mainWindow.webContents.send('servers-list', value);
331
+ }
332
+ }
333
+ } catch (error) {
334
+ console.error('Error setting user data for key:', key, 'nestedKey:', nestedKey, error);
335
+ event.reply('user-data-response', { key, nestedKey, value, success: false, error: error.message });
336
+ }
337
+ });
338
+
339
+ ipcMain.on('remove-user-data', (event, { key, nestedKey }) => {
340
+ try {
341
+ const success = userData.remove(key, nestedKey);
342
+ event.reply('user-data-response', { key, nestedKey, value: null, success });
343
+ } catch (error) {
344
+ console.error('Error removing user data for key:', key, 'nestedKey:', nestedKey, error);
345
+ event.reply('user-data-response', { key, nestedKey, value: null, success: false, error: error.message });
346
+ }
347
+ });
348
+
349
+ // Initialize renderer auto-update after mainWindow is created
350
+ const autoUpdateRenderer = new AutoUpdateRenderer((version) => {
351
+ // Recargar la ventana para aplicar los cambios
352
+ setTimeout(() => {
353
+ reloadMainWindow();
354
+ }, 1000);
355
+ });
356
+ autoUpdateRenderer.start();
357
+
358
+ // Cargar el HTML del renderer (priorizar userData, fallback a app)
359
+ loadRendererHtml(mainWindow, uuid);
360
+
361
+ // Mouse events control
362
+ mainWindow.setIgnoreMouseEvents(true, { forward: true });
363
+ ipcMain.on('set-ignore-events-true', () => {
364
+ mainWindow.setIgnoreMouseEvents(true, { forward: true });
365
+ });
366
+ ipcMain.on('set-ignore-events-false', () => {
367
+ mainWindow.setIgnoreMouseEvents(false, { forward: true });
368
+ });
369
+
370
+ // Window visibility control
371
+ ipcMain.on('show-window', () => {
372
+ if (mainWindow && !mainWindow.isDestroyed()) {
373
+ mainWindow.show();
374
+ }
375
+ });
376
+
377
+ ipcMain.on('hide-window', () => {
378
+ if (mainWindow && !mainWindow.isDestroyed()) {
379
+ mainWindow.hide();
380
+ }
381
+ });
382
+
383
+ mainWindow.setAlwaysOnTop(true, 'screen-saver');
384
+ mainWindow.webContents.on('did-finish-load', () => {
385
+ mainWindow.webContents.send('servers-list', userData.readData().servers);
386
+ mainWindow.webContents.send('app-data', { version: packageJson.version });
387
+ });
388
+
389
+
390
+ // Handle external URL opening
391
+ ipcMain.on('open-external', (event, url) => {
392
+ const { shell } = require('electron');
393
+ shell.openExternal(url);
394
+ });
395
+
396
+ ipcMain.on('open-external-browser', (event, url) => {
397
+ const { shell } = require('electron');
398
+ shell.openExternal(url);
399
+ });
400
+
401
+ // Handle registration window opening (larger window without menus)
402
+ ipcMain.on('open-registration-window', (event, url) => {
403
+ // Create a custom browser window for registration
404
+ const registrationWindow = new BrowserWindow({
405
+ width: 1200,
406
+ height: 800,
407
+ minWidth: 800,
408
+ minHeight: 600,
409
+ show: false,
410
+ titleBarStyle: 'default',
411
+ autoHideMenuBar: true,
412
+ webPreferences: {
413
+ nodeIntegration: false,
414
+ contextIsolation: true,
415
+ webSecurity: true
416
+ }
417
+ });
418
+
419
+ // Remove menu bar
420
+ registrationWindow.setMenuBarVisibility(false);
421
+
422
+ // Load the URL
423
+ registrationWindow.loadURL(url);
424
+
425
+ // Show window when ready
426
+ registrationWindow.once('ready-to-show', () => {
427
+ registrationWindow.show();
428
+ });
429
+
430
+ // Handle window closed
431
+ registrationWindow.on('closed', () => {
432
+ // Window closed, no action needed
433
+ });
434
+ });
435
+
436
+ mainWindow.on('ready-to-show', () => {
437
+ mainWindow.show();
438
+
439
+ // Use LocalServer singleton
440
+ if (!localServer) {
441
+ localServer = new LocalServer();
442
+ localServer.start(mainWindow);
443
+ } else {
444
+ // Update the mainWindow reference if server already exists
445
+ localServer.updateMainWindow(mainWindow);
446
+ }
447
+ });
448
+
449
+ lockChecker();
450
+ });
451
+
452
+ app.on('window-all-closed', () => {
453
+ // Don't quit when all windows are closed if we have a tray
454
+ if (tray && process.platform !== 'darwin') {
455
+ // Keep the app running with tray
456
+ } else {
457
+ cleanupAndQuit();
458
+ }
459
+ });
460
+
461
+ // Clean up on quit
462
+ app.on('before-quit', () => {
463
+ if (!isCleaningUp) {
464
+ // Clear intervals and timeouts
465
+ if (lockCheckerInterval) {
466
+ clearInterval(lockCheckerInterval);
467
+ lockCheckerInterval = null;
468
+ }
469
+ if (lockCheckerTimeout) {
470
+ clearTimeout(lockCheckerTimeout);
471
+ lockCheckerTimeout = null;
472
+ }
473
+
474
+ // Destroy main window
475
+ if (mainWindow && !mainWindow.isDestroyed()) {
476
+ mainWindow.destroy();
477
+ mainWindow = null;
478
+ }
479
+
480
+ // Stop local server
481
+ if (localServer) {
482
+ localServer.stop();
483
+ localServer = null;
484
+ }
485
+
486
+ // Destroy tray
487
+ if (tray) {
488
+ tray.destroy();
489
+ tray = null;
490
+ }
491
+ }
492
+ });
493
+
494
+ function lockChecker() {
495
+ lockCheckerTimeout = setTimeout(() => {
496
+ userData.set('lock', initialLock);
497
+ }, 1000);
498
+ lockCheckerInterval = setInterval(() => {
499
+ // Leer el lock directamente del archivo, no del cache
500
+ const currentData = userData.getLockFromFile();
501
+ // Si currentData es undefined, significa que el archivo .glassnote fue borrado o corrompido
502
+ // También verificar si el valor del lock cambió
503
+ if (currentData === undefined || currentData.toString() != initialLock.toString()) {
504
+ cleanupAndQuit();
505
+ }
506
+ }, 10000); // Check every 10 seconds
507
+ }
508
+
509
+ function cleanupAndQuit() {
510
+ // Prevent multiple cleanup calls
511
+ if (isCleaningUp) {
512
+ return;
513
+ }
514
+ isCleaningUp = true;
515
+
516
+
517
+ // Clear intervals and timeouts
518
+ if (lockCheckerInterval) {
519
+ clearInterval(lockCheckerInterval);
520
+ lockCheckerInterval = null;
521
+ }
522
+ if (lockCheckerTimeout) {
523
+ clearTimeout(lockCheckerTimeout);
524
+ lockCheckerTimeout = null;
525
+ }
526
+
527
+ // Destroy main window
528
+ if (mainWindow && !mainWindow.isDestroyed()) {
529
+ mainWindow.destroy();
530
+ mainWindow = null;
531
+ }
532
+
533
+ // Stop local server
534
+ if (localServer) {
535
+ localServer.stop();
536
+ localServer = null;
537
+ }
538
+
539
+ // Destroy tray
540
+ if (tray) {
541
+ tray.destroy();
542
+ tray = null;
543
+ }
544
+
545
+ // Force exit after a timeout to ensure process terminates
546
+ setTimeout(() => {
547
+ app.exit(0);
548
+ // Double safety: exit process after another timeout
549
+ setTimeout(() => {
550
+ process.exit(0);
551
+ }, 1000);
552
+ }, 500);
553
+ }
554
+
555
+ // Handle process signals for clean shutdown
556
+ process.on('SIGINT', () => {
557
+ cleanupAndQuit();
558
+ });
559
+
560
+ process.on('SIGTERM', () => {
561
+ cleanupAndQuit();
562
+ });
563
+
564
+ // Handle uncaught exceptions to prevent zombie processes
565
+ process.on('uncaughtException', (error) => {
566
+ console.error('Uncaught exception:', error);
567
+ cleanupAndQuit();
568
+ });
569
+
570
+ process.on('unhandledRejection', (reason, promise) => {
571
+ console.error('Unhandled promise rejection:', reason);
572
+ cleanupAndQuit();
573
+ });
574
+
575
+ // Start lock checker after everything is initialized
576
+
577
+
578
+
579
+ /**
580
+ * Carga el HTML del renderer desde la fuente con versión SUPERIOR.
581
+ *
582
+ * Dos fuentes, y la division importa:
583
+ * - userData: lo que bajo el auto-update de S3. Es el canal EN CALIENTE: una version
584
+ * nueva del renderer llega ahi sin republicar el cliente ni tocar el binario.
585
+ * - baseline: el renderer que viaja con la instalacion. Es lo que ve una instalacion
586
+ * nueva antes de su primer update en caliente.
587
+ *
588
+ * Gana la version mas alta, sin importar de donde venga; empate, gana userData.
589
+ */
590
+ // baselineRendererDir vive en rendererPaths.js: lo comparte con el updater del renderer.
591
+
592
+ function loadRendererHtml(window, uuid) {
593
+ // Definir las dos fuentes a considerar con archivos HTML alternativos
594
+ const sources = [
595
+ {
596
+ name: 'userData',
597
+ dir: path.join(userData.getUserDataDir(), 'renderer'),
598
+ htmlFiles: ['glassnote.html'], // userData siempre usa glassnote.html
599
+ type: 'auto-update'
600
+ },
601
+ {
602
+ name: 'baseline',
603
+ dir: baselineRendererDir(),
604
+ htmlFiles: ['glassnote.html', 'index.html'], // Intenta glassnote.html primero, luego index.html
605
+ type: 'paquete'
606
+ }
607
+ ];
608
+
609
+
610
+ // Obtener versiones y verificar archivos HTML para ambas fuentes
611
+ const sourcesWithInfo = sources.map(source => {
612
+ const version = getRendererVersionFromPath(source.dir);
613
+
614
+ // Buscar el primer archivo HTML que exista
615
+ let htmlPath = null;
616
+ let htmlFile = null;
617
+ for (const file of source.htmlFiles) {
618
+ const potentialPath = path.join(source.dir, file);
619
+ if (fs.existsSync(potentialPath)) {
620
+ htmlPath = potentialPath;
621
+ htmlFile = file;
622
+ break;
623
+ }
624
+ }
625
+
626
+ const htmlExists = htmlPath !== null;
627
+
628
+ return {
629
+ ...source,
630
+ version,
631
+ htmlPath,
632
+ htmlFile,
633
+ htmlExists
634
+ };
635
+ });
636
+
637
+ // Log de información encontrada
638
+ sourcesWithInfo.forEach(source => {
639
+ if (source.htmlExists) {
640
+ } else {
641
+ console.log(` ${source.name}: versión ${source.version}, SIN ARCHIVO HTML`);
642
+ }
643
+ });
644
+
645
+ // Comparar versiones para determinar cuál es superior
646
+ const versionComparison = compareVersions(
647
+ sourcesWithInfo[0].version,
648
+ sourcesWithInfo[1].version
649
+ );
650
+
651
+ // Determinar fuente con versión superior
652
+ let superiorSource;
653
+ if (versionComparison > 0) {
654
+ // userData tiene versión superior
655
+ superiorSource = sourcesWithInfo[0];
656
+ } else if (versionComparison < 0) {
657
+ // glassnote-renderer/dist tiene versión superior
658
+ superiorSource = sourcesWithInfo[1];
659
+ } else {
660
+ // Versiones iguales, priorizar userData (auto-update)
661
+ superiorSource = sourcesWithInfo[0];
662
+ }
663
+
664
+ // Intentar cargar desde la fuente con versión superior
665
+ if (superiorSource.htmlExists) {
666
+ window.loadFile(superiorSource.htmlPath, {
667
+ query: { uuid: uuid, version: packageJson.version }
668
+ });
669
+ return;
670
+ }
671
+
672
+ // Si la fuente con versión superior no tiene HTML, intentar la otra fuente
673
+ const otherSource = superiorSource.name === 'userData' ? sourcesWithInfo[1] : sourcesWithInfo[0];
674
+
675
+ if (otherSource.htmlExists) {
676
+
677
+ window.loadFile(otherSource.htmlPath, {
678
+ query: { uuid: uuid, version: packageJson.version }
679
+ });
680
+ return;
681
+ }
682
+
683
+ // Si ninguna fuente tiene HTML, error
684
+ console.error('ERROR: No se encontró archivo HTML del renderer en ninguna fuente');
685
+ console.error('userData buscó:', sourcesWithInfo[0].htmlFiles.join(', '));
686
+ console.error(`baseline (${sourcesWithInfo[1].dir}) buscó:`, sourcesWithInfo[1].htmlFiles.join(', '));
687
+
688
+ // Error fatal - no hay renderer disponible
689
+ console.error('ERROR FATAL: No se puede cargar el renderer');
690
+ // Intentar cargar una página de error o cerrar la aplicación
691
+ window.loadURL('data:text/html,<h1>Error: No se pudo cargar el renderer</h1>');
692
+ }
693
+
694
+ /**
695
+ * Compara dos versiones semánticas (formato: major.minor.patch)
696
+ * Retorna 1 si version1 > version2
697
+ * Retorna 0 si version1 == version2
698
+ * Retorna -1 si version1 < version2
699
+ */
700
+ function compareVersions(version1, version2) {
701
+ const v1Parts = version1.split('.').map((part) => parseInt(part) || 0);
702
+ const v2Parts = version2.split('.').map((part) => parseInt(part) || 0);
703
+
704
+ // Comparar partes principales (major, minor, patch)
705
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
706
+ const v1Part = v1Parts[i] || 0;
707
+ const v2Part = v2Parts[i] || 0;
708
+
709
+ if (v1Part > v2Part) return 1;
710
+ if (v1Part < v2Part) return -1;
711
+ }
712
+
713
+ return 0; // Las versiones son iguales
714
+ }
715
+
716
+ // getRendererVersionFromPath = rendererVersionFrom de rendererPaths.js (ver el require de arriba).
717
+
718
+ /**
719
+ * Recarga el main window después de una actualización del renderer
720
+ */
721
+ function reloadMainWindow() {
722
+ if (mainWindow && !mainWindow.isDestroyed()) {
723
+
724
+ // Update LocalServer with the new mainWindow reference
725
+ if (localServer) {
726
+ localServer.updateMainWindow(mainWindow);
727
+ }
728
+
729
+ loadRendererHtml(mainWindow, uuid);
730
+ }
731
+ }
732
+