@@ -2,6 +2,8 @@ const { app, BrowserWindow, BrowserView, ipcMain, shell, Notification, Menu } =
22const path = require ( 'path' ) ;
33const fs = require ( 'fs' ) ;
44const pkg = require ( './package.json' ) ;
5+ const CONSTANTS = require ( './src/constants' ) ;
6+
57const APP_VERSION = pkg . version ;
68
79let mainWindow ;
@@ -14,20 +16,58 @@ app.userAgentFallback = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/5
1416function getConfig ( ) {
1517 const CONFIG_PATH = path . join ( app . getPath ( 'userData' ) , 'config.json' ) ;
1618 const defaultConfig = { outputDir : '' , ids : '' , folderStructure : 'by_id' , includeCompanyName : false } ;
19+ if ( ! fs . existsSync ( CONFIG_PATH ) ) return defaultConfig ;
20+
1721 try {
18- if ( fs . existsSync ( CONFIG_PATH ) ) {
19- return { ...defaultConfig , ...JSON . parse ( fs . readFileSync ( CONFIG_PATH , 'utf8' ) ) } ;
20- }
21- } catch ( e ) { }
22- return defaultConfig ;
22+ const data = fs . readFileSync ( CONFIG_PATH , 'utf8' ) ;
23+ return { ...defaultConfig , ...JSON . parse ( data ) } ;
24+ } catch ( e ) {
25+ console . error ( 'Failed to read config:' , e ) ;
26+ return defaultConfig ;
27+ }
2328}
2429
2530function saveConfig ( config ) {
2631 const CONFIG_PATH = path . join ( app . getPath ( 'userData' ) , 'config.json' ) ;
2732 try {
2833 fs . writeFileSync ( CONFIG_PATH , JSON . stringify ( config , null , 2 ) ) ;
2934 return true ;
30- } catch ( e ) { return false ; }
35+ } catch ( e ) {
36+ console . error ( 'Failed to save config:' , e ) ;
37+ return false ;
38+ }
39+ }
40+
41+ const handleDevToolsShortcut = ( targetWebContents ) => ( event , input ) => {
42+ if ( input . type !== 'keyDown' ) return ;
43+
44+ const isF12 = input . key === 'F12' ;
45+ const isCtrlShiftI = input . key . toLowerCase ( ) === 'i' && ( input . control || input . meta ) && input . shift ;
46+ if ( isF12 || isCtrlShiftI ) {
47+ targetWebContents . toggleDevTools ( ) ;
48+ event . preventDefault ( ) ;
49+ }
50+ } ;
51+
52+ function createBrowserView ( ) {
53+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return ;
54+
55+ browserView = new BrowserView ( {
56+ webPreferences : { nodeIntegration : false , contextIsolation : true } ,
57+ } ) ;
58+
59+ mainWindow . setBrowserView ( browserView ) ;
60+
61+ const updateBounds = ( ) => {
62+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return ;
63+ const { width, height } = mainWindow . getContentBounds ( ) ;
64+ browserView . setBounds ( { x : 450 , y : 0 , width : width - 450 , height : height } ) ;
65+ } ;
66+
67+ updateBounds ( ) ;
68+ mainWindow . on ( 'resize' , updateBounds ) ;
69+ browserView . webContents . on ( 'before-input-event' , handleDevToolsShortcut ( browserView . webContents ) ) ;
70+ browserView . webContents . loadURL ( CONSTANTS . URLS . LOGIN ) ;
3171}
3272
3373function createWindow ( ) {
@@ -48,40 +88,9 @@ function createWindow() {
4888 } ) ;
4989
5090 mainWindow . loadFile ( path . join ( __dirname , 'src/renderer/index.html' ) ) ;
51-
52- const handleDevToolsShortcut = ( targetWebContents ) => ( event , input ) => {
53- if ( input . type === 'keyDown' ) {
54- const isF12 = input . key === 'F12' ;
55- const isCtrlShiftI = input . key . toLowerCase ( ) === 'i' && ( input . control || input . meta ) && input . shift ;
56- if ( isF12 || isCtrlShiftI ) {
57- targetWebContents . toggleDevTools ( ) ;
58- event . preventDefault ( ) ;
59- }
60- }
61- } ;
62-
6391 mainWindow . webContents . on ( 'before-input-event' , handleDevToolsShortcut ( mainWindow . webContents ) ) ;
6492
65- setTimeout ( ( ) => {
66- if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return ;
67- const CONSTANTS = require ( './src/constants' ) ;
68-
69- browserView = new BrowserView ( {
70- webPreferences : { nodeIntegration : false , contextIsolation : true } ,
71- } ) ;
72-
73- mainWindow . setBrowserView ( browserView ) ;
74- const updateBounds = ( ) => {
75- if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return ;
76- const { width, height } = mainWindow . getContentBounds ( ) ;
77- browserView . setBounds ( { x : 450 , y : 0 , width : width - 450 , height : height } ) ;
78- } ;
79-
80- updateBounds ( ) ;
81- mainWindow . on ( 'resize' , updateBounds ) ;
82- browserView . webContents . on ( 'before-input-event' , handleDevToolsShortcut ( browserView . webContents ) ) ;
83- browserView . webContents . loadURL ( CONSTANTS . URLS . LOGIN ) ;
84- } , 400 ) ;
93+ setTimeout ( createBrowserView , 400 ) ;
8594
8695 app . on ( 'select-client-certificate' , ( event , webContents , url , list , callback ) => {
8796 event . preventDefault ( ) ;
@@ -102,44 +111,73 @@ app.on('window-all-closed', () => {
102111 if ( process . platform !== 'darwin' ) app . quit ( ) ;
103112} ) ;
104113
105- ipcMain . handle ( 'get-app-version' , ( ) => {
106- return APP_VERSION ;
107- } ) ;
114+ ipcMain . handle ( 'get-app-version' , ( ) => APP_VERSION ) ;
108115
109- ipcMain . handle ( 'start-voting' , async ( event , { ids, outputDir, folderStructure, includeCompanyName } ) => {
116+ ipcMain . handle ( 'start-voting' , async ( event , params ) => {
117+ const { ids, outputDir, folderStructure, includeCompanyName } = params ;
110118 stopRequested = false ;
111119 const automation = require ( './src/automation/main_flow' ) ;
120+
121+ const sendLog = ( msg ) => {
122+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return ;
123+ mainWindow . webContents . send ( 'log' , String ( msg ) ) ;
124+ } ;
125+
126+ const sendProgress = ( progress ) => {
127+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return ;
128+ mainWindow . webContents . send ( 'progress' , JSON . parse ( JSON . stringify ( progress ) ) ) ;
129+
130+ const { id, screenshot } = progress ;
131+ if ( ! id || id . total <= 0 ) return ;
132+
133+ const base = id . current - ( screenshot && screenshot . total > 0 ? 1 : 0 ) ;
134+ const screenshotProgress = screenshot ? screenshot . current / screenshot . total : 0 ;
135+ const percent = Math . floor ( ( ( base + screenshotProgress ) / id . total ) * 100 ) ;
136+ const safePercent = Math . min ( 100 , Math . max ( 0 , percent ) ) ;
137+
138+ mainWindow . setTitle ( `(${ safePercent } %) 股東會投票幫手` ) ;
139+ } ;
140+
112141 try {
113- const stats = await automation . run ( browserView . webContents , ids , ( msg ) => {
114- if ( mainWindow && ! mainWindow . isDestroyed ( ) ) mainWindow . webContents . send ( 'log' , String ( msg ) ) ;
115- } , ( progress ) => {
116- if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
117- mainWindow . webContents . send ( 'progress' , JSON . parse ( JSON . stringify ( progress ) ) ) ;
118- const { id, screenshot } = progress ;
119- let percent = 0 ;
120- if ( id && id . total > 0 ) {
121- let base = id . current - ( screenshot && screenshot . total > 0 ? 1 : 0 ) ;
122- percent = Math . floor ( ( ( base + ( screenshot ? screenshot . current / screenshot . total : 0 ) ) / id . total ) * 100 ) ;
123- }
124- mainWindow . setTitle ( `(${ Math . min ( 100 , Math . max ( 0 , percent ) ) } %) 股東會投票幫手` ) ;
125- }
126- } , ( ) => stopRequested , outputDir , folderStructure , includeCompanyName ) ;
127-
128- if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
129- mainWindow . setTitle ( '股東會投票幫手' ) ;
130- if ( stats ) mainWindow . webContents . send ( 'log' , `[系統] 完成。累計投票: ${ stats . voted } ,累計截圖: ${ stats . screenshoted } ` ) ;
131- if ( ! mainWindow . isFocused ( ) && Notification . isSupported ( ) ) {
132- new Notification ( { title : '投票完成' , body : '所有作業已結束。' , icon : path . join ( __dirname , 'assets/icons/icon.png' ) } ) . show ( ) ;
133- }
142+ const stats = await automation . run (
143+ browserView . webContents ,
144+ ids ,
145+ sendLog ,
146+ sendProgress ,
147+ ( ) => stopRequested ,
148+ outputDir ,
149+ folderStructure ,
150+ includeCompanyName
151+ ) ;
152+
153+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return { success : true } ;
154+
155+ mainWindow . setTitle ( '股東會投票幫手' ) ;
156+ if ( stats ) {
157+ sendLog ( `[系統] 完成。累計投票: ${ stats . voted } ,累計截圖: ${ stats . screenshoted } ` ) ;
134158 }
159+
160+ if ( ! mainWindow . isFocused ( ) && Notification . isSupported ( ) ) {
161+ new Notification ( {
162+ title : '投票完成' ,
163+ body : '所有作業已結束。' ,
164+ icon : path . join ( __dirname , 'assets/icons/icon.png' ) ,
165+ } ) . show ( ) ;
166+ }
167+
135168 return { success : true } ;
136169 } catch ( error ) {
137- if ( mainWindow && ! mainWindow . isDestroyed ( ) ) {
138- mainWindow . setTitle ( '股東會投票幫手' ) ;
139- if ( ! mainWindow . isFocused ( ) && Notification . isSupported ( ) ) {
140- new Notification ( { title : '投票錯誤' , body : error . message , icon : path . join ( __dirname , 'assets/icons/icon.png' ) } ) . show ( ) ;
141- }
170+ if ( ! mainWindow || mainWindow . isDestroyed ( ) ) return { success : false , error : error . message } ;
171+
172+ mainWindow . setTitle ( '股東會投票幫手' ) ;
173+ if ( ! mainWindow . isFocused ( ) && Notification . isSupported ( ) ) {
174+ new Notification ( {
175+ title : '投票錯誤' ,
176+ body : error . message ,
177+ icon : path . join ( __dirname , 'assets/icons/icon.png' ) ,
178+ } ) . show ( ) ;
142179 }
180+
143181 return { success : false , error : error . message } ;
144182 }
145183} ) ;
@@ -152,6 +190,7 @@ ipcMain.handle('select-directory', async () => {
152190
153191ipcMain . handle ( 'get-config' , async ( ) => getConfig ( ) ) ;
154192ipcMain . handle ( 'save-config' , async ( event , config ) => saveConfig ( config ) ) ;
193+
155194ipcMain . handle ( 'open-about' , async ( ) => {
156195 const aboutWindow = new BrowserWindow ( {
157196 width : 400 ,
@@ -174,4 +213,7 @@ ipcMain.handle('open-external', async (event, url) => {
174213 return { success : true } ;
175214} ) ;
176215
177- ipcMain . handle ( 'stop-voting' , ( ) => { stopRequested = true ; return { success : true } ; } ) ;
216+ ipcMain . handle ( 'stop-voting' , ( ) => {
217+ stopRequested = true ;
218+ return { success : true } ;
219+ } ) ;
0 commit comments