Skip to content

Commit 1ac33d9

Browse files
committed
feat: simplified progressbar looks
1 parent f3cc16f commit 1ac33d9

7 files changed

Lines changed: 205 additions & 120 deletions

File tree

main.js

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ function createBrowserView() {
7979
function createWindow() {
8080
mainWindow = new BrowserWindow({
8181
width: 1000,
82-
height: 900,
82+
height: 850,
8383
minWidth: 900,
84-
minHeight: 900,
84+
minHeight: 850,
8585
show: true,
8686
backgroundColor: '#1a1a2e',
8787
paintWhenInitiallyHidden: false,
@@ -125,26 +125,24 @@ ipcMain.handle('start-voting', async (event, params) => {
125125
const { ids, outputDir, folderStructure, includeCompanyName } = params;
126126
stopRequested = false;
127127
const automation = require('./src/automation/main_flow');
128+
const { calculateProgress } = require('./src/automation/utils');
128129

129-
const sendLog = (msg) => {
130+
const sendLog = (msg, type = '') => {
130131
if (!mainWindow || mainWindow.isDestroyed()) return;
131-
mainWindow.webContents.send('log', String(msg));
132+
mainWindow.webContents.send('log', String(msg), type);
132133
};
133134

135+
let maxPercent = 0;
134136
const sendProgress = (progress) => {
135137
if (!mainWindow || mainWindow.isDestroyed()) return;
136138
mainWindow.webContents.send('progress', JSON.parse(JSON.stringify(progress)));
137139

138-
const { id, screenshot } = progress;
139-
if (!id || id.total <= 0) return;
140-
141-
const hasScreenshot = screenshot && screenshot.total > 0;
142-
const base = id.current - (hasScreenshot ? 1 : 0);
143-
const screenshotProgress = hasScreenshot ? (screenshot.current / screenshot.total) : 0;
144-
const percent = Math.floor(((base + screenshotProgress) / id.total) * 100);
145-
const safePercent = Math.min(100, Math.max(0, isNaN(percent) ? 0 : percent));
140+
const currentPercent = calculateProgress(progress);
141+
if (currentPercent > maxPercent) {
142+
maxPercent = currentPercent;
143+
}
146144

147-
mainWindow.setTitle(`(${safePercent}%) 股東會投票幫手`);
145+
mainWindow.setTitle(`(${maxPercent}%) 股東會投票幫手`);
148146
};
149147

150148
try {

preload.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
99
openExternal: (url) => ipcRenderer.invoke('open-external', url),
1010
openAbout: () => ipcRenderer.invoke('open-about'),
1111
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
12-
onLog: (callback) => ipcRenderer.on('log', (_event, value) => {
13-
// Only pass simple string value
14-
callback(String(value));
12+
onLog: (callback) => ipcRenderer.on('log', (_event, msg, type) => {
13+
callback(String(msg), type);
1514
}),
1615
onProgress: (callback) => ipcRenderer.on('progress', (_event, value) => {
1716
// Deep copy to ensure serializable data only

src/automation/main_flow.js

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ const logout = require('./logout');
66
const CONSTANTS = require('../constants');
77
const { randomDelay, waitForNavigation, isMaintenanceTime, isScreenshotExists } = require('./utils');
88

9-
async function processCompany(webContents, id, company, context, sendLog, sendProgress, isStopRequested) {
10-
const { pendingCodes, outputDir, folderStructure, includeCompanyName, i, idsLength, totalVotes, totalShots } = context;
9+
async function processCompany(webContents, id, company, context, sendLog, emitProgress, isStopRequested) {
10+
const { pendingCodes, outputDir, folderStructure, includeCompanyName, i, idsLength, sessionStats } = context;
1111
const { code } = company;
1212

1313
if (isScreenshotExists(id, code, outputDir, folderStructure)) {
1414
sendLog(`[清單] ${code} 已有截圖,跳過。`);
15-
if (pendingCodes.includes(code)) context.currentVote++;
15+
if (pendingCodes.includes(code)) {
16+
context.currentVote++;
17+
sessionStats.voted++;
18+
}
1619
context.currentShot++;
17-
sendProgress({
18-
id: { current: i + 1, total: idsLength },
19-
vote: { current: context.currentVote, total: totalVotes },
20-
screenshot: { current: context.currentShot, total: totalShots },
21-
});
20+
sessionStats.screenshoted++;
21+
emitProgress();
2222
return;
2323
}
2424

@@ -35,12 +35,8 @@ async function processCompany(webContents, id, company, context, sendLog, sendPr
3535
sendLog(`[投票] ${code} 成功。`);
3636

3737
context.currentVote++;
38-
if (context.sessionStats) context.sessionStats.voted++;
39-
sendProgress({
40-
id: { current: i + 1, total: idsLength },
41-
vote: { current: context.currentVote, total: totalVotes },
42-
screenshot: { current: context.currentShot, total: totalShots },
43-
});
38+
sessionStats.voted++;
39+
emitProgress();
4440

4541
sendLog('[導航] 返回查詢...');
4642
const waitGo = waitForNavigation(webContents);
@@ -63,11 +59,8 @@ async function processCompany(webContents, id, company, context, sendLog, sendPr
6359
sendLog(`[導航] 已投過,在查詢頁...`);
6460
if (pendingCodes.includes(code)) {
6561
context.currentVote++;
66-
sendProgress({
67-
id: { current: i + 1, total: idsLength },
68-
vote: { current: context.currentVote, total: totalVotes },
69-
screenshot: { current: context.currentShot, total: totalShots },
70-
});
62+
sessionStats.voted++;
63+
emitProgress();
7164
}
7265
}
7366

@@ -77,12 +70,8 @@ async function processCompany(webContents, id, company, context, sendLog, sendPr
7770
sendLog(`[截圖] 已存: ${path.basename(screenshotPath)}`);
7871

7972
context.currentShot++;
80-
if (context.sessionStats) context.sessionStats.screenshoted++;
81-
sendProgress({
82-
id: { current: i + 1, total: idsLength },
83-
vote: { current: context.currentVote, total: totalVotes },
84-
screenshot: { current: context.currentShot, total: totalShots },
85-
});
73+
sessionStats.screenshoted++;
74+
emitProgress();
8675

8776
} catch (procError) {
8877
if (isStopRequested()) return;
@@ -94,8 +83,41 @@ async function processId(webContents, id, i, ids, sendLog, sendProgress, isStopR
9483
const { outputDir, folderStructure, includeCompanyName } = config;
9584
const maskedId = `${id.substring(0, 4)}****${id.substring(8)}`;
9685

86+
const context = {
87+
pendingCodes: [],
88+
outputDir,
89+
folderStructure,
90+
includeCompanyName,
91+
i,
92+
idsLength: ids.length,
93+
totalVotes: 0,
94+
totalShots: 0,
95+
currentVote: 0,
96+
currentShot: 0,
97+
sessionStats,
98+
};
99+
100+
const emitProgress = (status = 'processing') => {
101+
sendProgress({
102+
id: { current: i + 1, total: ids.length },
103+
vote: {
104+
current: context.currentVote,
105+
total: context.totalVotes,
106+
globalCurrent: sessionStats.voted,
107+
globalTotal: sessionStats.totalVotes,
108+
},
109+
screenshot: {
110+
current: context.currentShot,
111+
total: context.totalShots,
112+
globalCurrent: sessionStats.screenshoted,
113+
globalTotal: sessionStats.totalShots,
114+
},
115+
status,
116+
});
117+
};
118+
97119
sendLog(`[系統] 處理: ${maskedId}`);
98-
sendProgress({ id: { current: i, total: ids.length }, vote: { current: 0, total: 0 }, screenshot: { current: 0, total: 0 } });
120+
emitProgress('initializing');
99121

100122
try {
101123
sendLog('[系統] 清空 Session...');
@@ -105,6 +127,7 @@ async function processId(webContents, id, i, ids, sendLog, sendProgress, isStopR
105127
const loggedIn = await login.execute(webContents, id, sendLog);
106128
if (!loggedIn) {
107129
sendLog(`[登入] ${maskedId} 失敗,跳過。`, 'error');
130+
emitProgress('finished');
108131
return;
109132
}
110133

@@ -115,31 +138,20 @@ async function processId(webContents, id, i, ids, sendLog, sendProgress, isStopR
115138
const votedNeedScreenshot = companies.filter(c => c.status === 'voted' && !isScreenshotExists(id, c.code, outputDir, folderStructure));
116139
const targetCompanies = [...pendingCompanies, ...votedNeedScreenshot];
117140

118-
const context = {
119-
pendingCodes: pendingCompanies.map(c => c.code),
120-
outputDir,
121-
folderStructure,
122-
includeCompanyName,
123-
i,
124-
idsLength: ids.length,
125-
totalVotes: pendingCompanies.length,
126-
totalShots: targetCompanies.length,
127-
currentVote: 0,
128-
currentShot: 0,
129-
sessionStats,
130-
};
141+
context.pendingCodes = pendingCompanies.map(c => c.code);
142+
context.totalVotes = pendingCompanies.length;
143+
context.totalShots = targetCompanies.length;
144+
145+
sessionStats.totalVotes += context.totalVotes;
146+
sessionStats.totalShots += context.totalShots;
131147

132148
sendLog(`[清單] 需投 ${context.totalVotes},需截 ${votedNeedScreenshot.length}。`);
133-
sendProgress({
134-
id: { current: i + 1, total: ids.length },
135-
vote: { current: context.currentVote, total: context.totalVotes },
136-
screenshot: { current: context.currentShot, total: context.totalShots },
137-
});
149+
emitProgress();
138150

139151
for (const company of targetCompanies) {
140152
if (isStopRequested()) break;
141153

142-
await processCompany(webContents, id, company, context, sendLog, sendProgress, isStopRequested);
154+
await processCompany(webContents, id, company, context, sendLog, emitProgress, isStopRequested);
143155

144156
if (!isStopRequested()) await voting.navigateBackToList(webContents, sendLog);
145157
}
@@ -159,8 +171,10 @@ async function processId(webContents, id, i, ids, sendLog, sendProgress, isStopR
159171
await randomDelay(1000, 2000);
160172

161173
sendLog(`[系統] ${maskedId} 結束。`, 'info');
174+
emitProgress('finished');
162175
} catch (error) {
163176
sendLog(`[系統] ${maskedId} 錯誤: ${error.message}`, 'error');
177+
emitProgress('finished');
164178
}
165179
}
166180

@@ -170,7 +184,7 @@ async function run(webContents, ids, sendLog, sendProgress, isStopRequested, out
170184
return { voted: 0, screenshoted: 0 };
171185
}
172186

173-
const sessionStats = { voted: 0, screenshoted: 0 };
187+
const sessionStats = { voted: 0, screenshoted: 0, totalVotes: 0, totalShots: 0 };
174188
const config = { outputDir, folderStructure, includeCompanyName };
175189

176190
for (let i = 0; i < ids.length; i++) {

src/automation/utils.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,46 @@ function isScreenshotExists(nationalId, code, outputDir, folderStructure = 'by_i
103103
return files.some(f => f.startsWith(prefix));
104104
}
105105

106+
/**
107+
* Calculates unified progress percentage (0-100).
108+
*
109+
* @param {object} data Progress data from sendProgress
110+
* @returns {number} 0-100 percentage
111+
*/
112+
function calculateProgress(data) {
113+
const { id, vote, screenshot, status } = data;
114+
if (!id || id.total <= 0) return 0;
115+
116+
const base = Math.max(0, id.current - 1);
117+
let accountProgress = 0;
118+
119+
if (status === 'finished') {
120+
accountProgress = 1;
121+
} else if (status === 'initializing') {
122+
accountProgress = 0;
123+
} else {
124+
const hasVote = vote && vote.total > 0;
125+
const hasShot = screenshot && screenshot.total > 0;
126+
127+
if (hasVote && hasShot) {
128+
accountProgress = (vote.current / vote.total * 0.5) + (screenshot.current / screenshot.total * 0.5);
129+
} else if (hasVote) {
130+
accountProgress = vote.current / vote.total;
131+
} else if (hasShot) {
132+
accountProgress = screenshot.current / screenshot.total;
133+
}
134+
}
135+
136+
const percent = Math.floor(((base + accountProgress) / id.total) * 100);
137+
return Math.min(100, Math.max(0, isNaN(percent) ? 0 : percent));
138+
}
139+
106140
module.exports = {
107141
delay,
108142
randomDelay,
109143
waitForNavigation,
110144
safeExecute,
111145
isMaintenanceTime,
112146
isScreenshotExists,
147+
calculateProgress,
113148
};

src/renderer/index.css

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,31 @@ textarea:focus {
265265
border-color: var(--primary);
266266
}
267267

268+
.select-input {
269+
width: 100%;
270+
padding: 8px 12px;
271+
border: 1px solid rgba(255, 255, 255, 0.1);
272+
border-radius: 8px;
273+
background-color: rgba(0, 0, 0, 0.2);
274+
color: #fff;
275+
font-size: 0.9rem;
276+
font-family: 'Noto Sans TC', sans-serif;
277+
cursor: pointer;
278+
}
279+
280+
.select-input option {
281+
background-color: var(--bg-sidebar);
282+
color: #fff;
283+
}
284+
285+
.label-with-info {
286+
font-size: 0.9rem;
287+
color: var(--text-muted);
288+
display: flex;
289+
align-items: center;
290+
gap: 6px;
291+
}
292+
268293
.path-selector {
269294
display: flex;
270295
gap: 8px;
@@ -388,9 +413,9 @@ textarea:focus {
388413

389414
.progress-bar-container {
390415
width: 100%;
391-
height: 4px;
416+
height: 12px;
392417
background: rgba(255, 255, 255, 0.05);
393-
border-radius: 2px;
418+
border-radius: 6px;
394419
overflow: hidden;
395420
}
396421

@@ -401,6 +426,12 @@ textarea:focus {
401426
transition: width 0.3s ease-in-out;
402427
}
403428

429+
#progress-status-text {
430+
font-size: 0.75rem;
431+
color: var(--text-muted);
432+
margin-left: 6px;
433+
}
434+
404435
.log-section {
405436
flex: 1;
406437
display: flex;

0 commit comments

Comments
 (0)