1+
12// main.js (ESM)
2- import { app , BrowserWindow , BrowserView } from 'electron' ;
3+ import { app , BrowserWindow , BrowserView , dialog } from 'electron' ;
34import fs from 'node:fs' ;
45import path from 'node:path' ;
56
6- // Improve compatibility with some enterprise GPUs/VMs
7+ // Disable GPU to avoid blank windows on some enterprise GPUs/VMs
78app . disableHardwareAcceleration ( ) ;
89
9- let mainWindow ;
10- let topView ;
11- let bottomView ;
12- let config ;
13- let cfgPathInUse = null ;
10+ let mainWindow ; let topView ; let bottomView ; let config ; let cfgPathInUse = null ;
1411let defaultUA = { top : null , bottom : null } ;
12+ let logFile = null ;
13+
14+ function getPortableDir ( ) {
15+ // electron-builder portable sets this to the real folder containing the EXE
16+ return process . env . PORTABLE_EXECUTABLE_DIR || null ;
17+ }
18+
19+ function getExeDir ( ) {
20+ try { return path . dirname ( app . getPath ( 'exe' ) ) ; } catch { return process . cwd ( ) ; }
21+ }
22+
23+ function getLogPath ( ) {
24+ const base = getPortableDir ( ) || getExeDir ( ) || process . cwd ( ) ;
25+ return path . join ( base , 'two-sites-viewer.log' ) ;
26+ }
1527
16- // ---------- Config loading ----------
28+ function log ( ...args ) {
29+ try {
30+ const line = `[${ new Date ( ) . toISOString ( ) } ] ` + args . map ( a => ( typeof a === 'string' ? a : JSON . stringify ( a ) ) ) . join ( ' ' ) + '
31+ ' ;
32+ if ( ! logFile ) logFile = getLogPath ( ) ;
33+ fs . appendFileSync ( logFile , line ) ;
34+ } catch { }
35+ // Also try console for dev runs
36+ try { console . log ( ...args ) ; } catch { }
37+ }
1738
1839function normalizeConfig ( cfg ) {
1940 return {
@@ -28,74 +49,75 @@ function normalizeConfig(cfg) {
2849}
2950
3051function candidatesForConfig ( ) {
31- const portableDir = process . env . PORTABLE_EXECUTABLE_DIR ; // electron-builder portable
32- const exeDir = path . dirname ( app . getPath ( 'exe' ) ) ;
52+ const portableDir = getPortableDir ( ) ;
53+ const exeDir = getExeDir ( ) ;
3354 const cwd = process . cwd ( ) ;
34- const userData = app . getPath ( 'userData' ) ; // optional override location
35- const resources = process . resourcesPath ; // baked default (if you ship one inside asar)
36-
55+ const userData = app . getPath ( 'userData' ) ;
56+ const resources = process . resourcesPath ;
3757 const list = [ ] ;
3858 if ( portableDir ) list . push ( path . join ( portableDir , 'config.json' ) ) ;
3959 list . push (
4060 path . join ( exeDir , 'config.json' ) ,
4161 path . join ( cwd , 'config.json' ) ,
4262 path . join ( userData , 'config.json' ) ,
43- path . join ( resources , 'config.json' ) // final fallback if bundled
63+ path . join ( resources , 'config.json' )
4464 ) ;
4565 return list ;
4666}
4767
68+ function ensureConfigExistsAt ( targetPath ) {
69+ try {
70+ if ( ! fs . existsSync ( targetPath ) ) {
71+ fs . writeFileSync ( targetPath , JSON . stringify ( {
72+ topUrl : 'https://example.com' ,
73+ bottomUrl : 'https://example.com' ,
74+ dividerRatio : 0.55 ,
75+ minWidth : 1200 ,
76+ minHeight : 800 ,
77+ userAgent : '' ,
78+ lockToInitialOrigin : false
79+ } , null , 2 ) ) ;
80+ log ( 'Created default config at' , targetPath ) ;
81+ }
82+ } catch ( e ) { log ( 'Failed to create default config at' , targetPath , e . message ) ; }
83+ }
84+
4885function findConfigPath ( ) {
49- for ( const p of candidatesForConfig ( ) ) {
86+ const candidates = candidatesForConfig ( ) ;
87+ log ( 'Config path candidates:' ) ;
88+ for ( const p of candidates ) { log ( ' -' , p , fs . existsSync ( p ) ? '[exists]' : '[missing]' ) ; }
89+ for ( const p of candidates ) {
5090 try { if ( fs . existsSync ( p ) ) return p ; } catch { }
5191 }
52- // If nothing exists, prefer PORTABLE_EXECUTABLE_DIR (if present) for future writes
53- if ( process . env . PORTABLE_EXECUTABLE_DIR ) {
54- return path . join ( process . env . PORTABLE_EXECUTABLE_DIR , 'config.json' ) ;
55- }
56- return path . join ( path . dirname ( app . getPath ( 'exe' ) ) , 'config.json' ) ;
92+ const preferred = ( getPortableDir ( ) || getExeDir ( ) || process . cwd ( ) ) ;
93+ const fallback = path . join ( preferred , 'config.json' ) ;
94+ ensureConfigExistsAt ( fallback ) ;
95+ return fallback ;
5796}
5897
5998function loadConfig ( ) {
6099 const p = cfgPathInUse || findConfigPath ( ) ;
61100 cfgPathInUse = p ;
62101 try {
63- if ( fs . existsSync ( p ) ) {
64- const raw = fs . readFileSync ( p , 'utf-8' ) ;
65- const parsed = JSON . parse ( raw ) ;
66- return normalizeConfig ( parsed ) ;
67- }
68- } catch { }
69- // Fallback defaults — you can put your AWACS URLs here as a baked default if you want
70- return normalizeConfig ( { } ) ;
71- }
72-
73- // ---------- Navigation guard ----------
74-
75- function getOrigin ( u ) {
76- try {
77- const x = new URL ( u ) ;
78- return `${ x . protocol } //${ x . host } ` ;
79- } catch {
80- return '' ;
102+ const raw = fs . readFileSync ( p , 'utf-8' ) ;
103+ const parsed = JSON . parse ( raw ) ;
104+ return normalizeConfig ( parsed ) ;
105+ } catch ( e ) {
106+ log ( 'Failed to read/parse config at' , p , e . message ) ;
107+ // Keep defaults if parse fails
108+ return normalizeConfig ( { } ) ;
81109 }
82110}
83111
112+ function getOrigin ( u ) { try { const x = new URL ( u ) ; return `${ x . protocol } //${ x . host } ` ; } catch { return '' ; } }
113+
84114function guardNavigation ( view , initialUrl ) {
85115 if ( ! config . lockToInitialOrigin ) return ;
86116 const origin = getOrigin ( initialUrl ) ;
87-
88- view . webContents . on ( 'will-navigate' , ( e , url ) => {
89- if ( getOrigin ( url ) !== origin ) e . preventDefault ( ) ;
90- } ) ;
91- view . webContents . setWindowOpenHandler ( ( d ) => {
92- if ( getOrigin ( d . url ) !== origin ) return { action : 'deny' } ;
93- return { action : 'allow' } ;
94- } ) ;
117+ view . webContents . on ( 'will-navigate' , ( e , url ) => { if ( getOrigin ( url ) !== origin ) e . preventDefault ( ) ; } ) ;
118+ view . webContents . setWindowOpenHandler ( ( d ) => { return ( getOrigin ( d . url ) !== origin ) ? { action : 'deny' } : { action : 'allow' } ; } ) ;
95119}
96120
97- // ---------- Layout ----------
98-
99121function layout ( ) {
100122 if ( ! mainWindow || ! topView || ! bottomView ) return ;
101123 const [ w , h ] = mainWindow . getContentSize ( ) ;
@@ -106,8 +128,6 @@ function layout() {
106128 bottomView . setAutoResize ( { width : true , height : true } ) ;
107129}
108130
109- // ---------- Live apply changes ----------
110-
111131function applyConfigChanges ( next ) {
112132 const urlsChanged = next . topUrl !== config . topUrl || next . bottomUrl !== config . bottomUrl ;
113133 const ratioChanged = next . dividerRatio !== config . dividerRatio ;
@@ -129,7 +149,6 @@ function applyConfigChanges(next) {
129149 }
130150
131151 if ( lockChanged ) {
132- // Re-apply guards (handlers are additive; we only tighten rules when enabled)
133152 guardNavigation ( topView , config . topUrl ) ;
134153 guardNavigation ( bottomView , config . bottomUrl ) ;
135154 }
@@ -141,7 +160,6 @@ function applyConfigChanges(next) {
141160 } else if ( uaChanged && topView ) {
142161 topView . webContents . reload ( ) ;
143162 }
144-
145163 if ( bottomView && bottomView . webContents . getURL ( ) !== config . bottomUrl ) {
146164 bottomView . webContents . loadURL ( config . bottomUrl , loadOpts ) . catch ( ( ) => { } ) ;
147165 } else if ( uaChanged && bottomView ) {
@@ -154,42 +172,30 @@ function applyConfigChanges(next) {
154172
155173function watchConfigFile ( ) {
156174 if ( ! cfgPathInUse ) return ;
157-
158- const tryApply = debounce ( ( ) => {
159- try {
160- const next = loadConfig ( ) ;
161- applyConfigChanges ( next ) ;
162- } catch {
163- // ignore transient parse errors while saving
164- }
175+ const applyLater = debounce ( ( ) => {
176+ const next = loadConfig ( ) ;
177+ log ( 'Config change detected. Applying...' ) ;
178+ applyConfigChanges ( next ) ;
165179 } , 300 ) ;
166180
167181 try {
168- const watcher = fs . watch ( cfgPathInUse , { persistent : false } , tryApply ) ;
169- watcher . on ( 'error' , ( ) => {
182+ const w = fs . watch ( cfgPathInUse , { persistent : false } , applyLater ) ;
183+ w . on ( 'error' , ( ) => {
170184 try { fs . unwatchFile ( cfgPathInUse ) ; } catch { }
171- try { fs . watchFile ( cfgPathInUse , { interval : 500 } , tryApply ) ; } catch { }
185+ try { fs . watchFile ( cfgPathInUse , { interval : 500 } , applyLater ) ; } catch { }
172186 } ) ;
173187 } catch {
174- try { fs . watchFile ( cfgPathInUse , { interval : 500 } , tryApply ) ; } catch { }
188+ try { fs . watchFile ( cfgPathInUse , { interval : 500 } , applyLater ) ; } catch { }
175189 }
176190}
177191
178- function debounce ( fn , ms = 250 ) {
179- let t ;
180- return ( ...args ) => {
181- clearTimeout ( t ) ;
182- t = setTimeout ( ( ) => fn ( ...args ) , ms ) ;
183- } ;
184- }
185-
186- // ---------- Create window ----------
192+ function debounce ( fn , ms = 250 ) { let t ; return ( ...args ) => { clearTimeout ( t ) ; t = setTimeout ( ( ) => fn ( ...args ) , ms ) ; } ; }
187193
188194function createWindow ( ) {
189195 config = loadConfig ( ) ;
190- console . log ( '[TwoSites] Using config at:' , cfgPathInUse ) ;
191- console . log ( '[TwoSites] Top URL:' , config . topUrl ) ;
192- console . log ( '[TwoSites] Bottom URL:' , config . bottomUrl ) ;
196+ log ( '[TwoSites] Using config at:' , cfgPathInUse ) ;
197+ log ( '[TwoSites] Top URL:' , config . topUrl ) ;
198+ log ( '[TwoSites] Bottom URL:' , config . bottomUrl ) ;
193199
194200 mainWindow = new BrowserWindow ( {
195201 width : 1200 ,
@@ -198,8 +204,8 @@ function createWindow() {
198204 minWidth : config . minWidth ,
199205 minHeight : config . minHeight ,
200206 backgroundColor : '#111111' ,
201- show : true , // show immediately
202- autoHideMenuBar : true , // no address bar/menu
207+ show : true ,
208+ autoHideMenuBar : true ,
203209 webPreferences : {
204210 nodeIntegration : false ,
205211 contextIsolation : true ,
@@ -208,7 +214,7 @@ function createWindow() {
208214 } ,
209215 } ) ;
210216
211- // Safety net : ensure it shows even if pages hang
217+ // Safety: ensure visible even if loads hang
212218 setTimeout ( ( ) => { try { mainWindow . show ( ) ; } catch { } } , 1500 ) ;
213219
214220 topView = new BrowserView ( { webPreferences : { contextIsolation : true , sandbox : true } } ) ;
@@ -217,17 +223,10 @@ function createWindow() {
217223 mainWindow . setBrowserView ( topView ) ;
218224 mainWindow . addBrowserView ( bottomView ) ;
219225
220- // capture default UA
221- try {
222- defaultUA . top = topView . webContents . getUserAgent ?. ( ) || null ;
223- defaultUA . bottom = bottomView . webContents . getUserAgent ?. ( ) || null ;
224- } catch { }
225-
226+ try { defaultUA . top = topView . webContents . getUserAgent ?. ( ) || null ; } catch { }
227+ try { defaultUA . bottom = bottomView . webContents . getUserAgent ?. ( ) || null ; } catch { }
226228 if ( config . userAgent ) {
227- try {
228- topView . webContents . setUserAgent ( config . userAgent ) ;
229- bottomView . webContents . setUserAgent ( config . userAgent ) ;
230- } catch { }
229+ try { topView . webContents . setUserAgent ( config . userAgent ) ; bottomView . webContents . setUserAgent ( config . userAgent ) ; } catch { }
231230 }
232231
233232 const loadOpts = config . userAgent ? { userAgent : config . userAgent } : undefined ;
@@ -239,26 +238,33 @@ function createWindow() {
239238 mainWindow . on ( 'resize' , layout ) ;
240239 mainWindow . once ( 'ready-to-show' , ( ) => { layout ( ) ; try { mainWindow . show ( ) ; } catch { } } ) ;
241240
242- // diagnostics
243241 for ( const wc of [ topView . webContents , bottomView . webContents ] ) {
244- wc . on ( 'did-fail-load' , ( _e , code , desc , validatedURL ) => {
245- console . error ( 'did-fail-load' , code , desc , validatedURL ) ;
246- } ) ;
247- wc . on ( 'crashed' , ( ) => console . error ( 'webContents crashed' ) ) ;
242+ wc . on ( 'did-fail-load' , ( _e , code , desc , validatedURL ) => { log ( 'did-fail-load' , code , desc , validatedURL ) ; } ) ;
243+ wc . on ( 'crashed' , ( ) => log ( 'webContents crashed' ) ) ;
248244 }
249245
250246 watchConfigFile ( ) ;
251- }
252247
253- // ---------- App lifecycle ----------
248+ // If the app is still showing example.com and not your URLs, show a one-time dialog
249+ if ( ! config . topUrl || config . topUrl . includes ( 'example.com' ) ) {
250+ setTimeout ( ( ) => {
251+ try {
252+ dialog . showMessageBox ( {
253+ type : 'warning' ,
254+ title : 'Two Sites Viewer' ,
255+ message : 'No valid config.json was found. A default config may have been created.' ,
256+ detail : `Config path used: ${ cfgPathInUse }
257+
258+ Edit this file and save; the app will auto-reload.` ,
259+ } ) ;
260+ } catch { }
261+ } , 800 ) ;
262+ }
263+ }
254264
255265app . whenReady ( ) . then ( ( ) => {
256266 createWindow ( ) ;
257- app . on ( 'activate' , ( ) => {
258- if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( ) ;
259- } ) ;
267+ app . on ( 'activate' , ( ) => { if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( ) ; } ) ;
260268} ) ;
261269
262- app . on ( 'window-all-closed' , ( ) => {
263- if ( process . platform !== 'darwin' ) app . quit ( ) ;
264- } ) ;
270+ app . on ( 'window-all-closed' , ( ) => { if ( process . platform !== 'darwin' ) app . quit ( ) ; } ) ;
0 commit comments