From e9439a6153395c70e68d9a33f53868bccd7bdb5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:16:18 +0000 Subject: [PATCH 1/4] Initial plan From d311c2a9cb67c5eb655b0d26c61a0c157ba3795e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:20:15 +0000 Subject: [PATCH 2/4] Add auto-update functionality with electron-updater Co-authored-by: hellotaotao <1796860+hellotaotao@users.noreply.github.com> --- package-lock.json | 114 ++++++++++++++++++++++++++++++-- package.json | 13 +++- src/auto-updater.js | 154 ++++++++++++++++++++++++++++++++++++++++++++ src/main.js | 33 ++++++++++ src/views/main.css | 19 ++++++ src/views/main.html | 16 +++++ 6 files changed, 340 insertions(+), 9 deletions(-) create mode 100644 src/auto-updater.js diff --git a/package-lock.json b/package-lock.json index 971dab3..3b0f788 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,10 +7,12 @@ "": { "name": "whisp-line", "version": "1.0.77", - "license": "MIT", + "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { "auto-launch": "^5.0.6", + "electron-log": "^5.4.3", "electron-store": "^10.1.0", + "electron-updater": "^6.7.3", "groq-sdk": "^0.30.0", "koffi": "^2.13.0", "openai": "^4.57.0", @@ -1295,7 +1297,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/assert-plus": { @@ -2097,7 +2098,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2512,6 +2512,15 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-log": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", + "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/electron-publish": { "version": "26.0.11", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.0.11.tgz", @@ -2583,6 +2592,82 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/electron-updater": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz", + "integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.5.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", @@ -3162,7 +3247,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/groq-sdk": { @@ -3552,7 +3636,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3640,7 +3723,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/lodash": { @@ -3650,6 +3732,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -4725,7 +4820,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true, "license": "ISC" }, "node_modules/semver": { @@ -5193,6 +5287,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", diff --git a/package.json b/package.json index a66aa92..538bc15 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,13 @@ }, "dependencies": { "auto-launch": "^5.0.6", + "electron-log": "^5.4.3", "electron-store": "^10.1.0", + "electron-updater": "^6.7.3", "groq-sdk": "^0.30.0", + "koffi": "^2.13.0", "openai": "^4.57.0", - "uiohook-napi": "^1.5.4", - "koffi": "^2.13.0" + "uiohook-napi": "^1.5.4" }, "build": { "appId": "com.tao.whispline", @@ -44,6 +46,13 @@ "src/**/*", "assets/**/*" ], + "publish": [ + { + "provider": "github", + "owner": "hellotaotao", + "repo": "WhispLine" + } + ], "mac": { "icon": "assets/icon.icns", "category": "public.app-category.productivity", diff --git a/src/auto-updater.js b/src/auto-updater.js new file mode 100644 index 0000000..81c31bf --- /dev/null +++ b/src/auto-updater.js @@ -0,0 +1,154 @@ +const { autoUpdater } = require('electron-updater'); +const { dialog } = require('electron'); +const log = require('electron-log'); + +class AutoUpdaterService { + constructor(mainWindow) { + this.mainWindow = mainWindow; + this.updateCheckInProgress = false; + + // Configure logging + log.transports.file.level = 'info'; + autoUpdater.logger = log; + + // Configure auto-updater + autoUpdater.autoDownload = false; // Don't auto-download, ask user first + autoUpdater.autoInstallOnAppQuit = true; + + this.setupEventListeners(); + } + + setupEventListeners() { + // Checking for updates + autoUpdater.on('checking-for-update', () => { + log.info('Checking for updates...'); + this.sendStatusToWindow('Checking for updates...'); + }); + + // Update available + autoUpdater.on('update-available', (info) => { + log.info('Update available:', info.version); + this.updateCheckInProgress = false; + + dialog.showMessageBox(this.mainWindow, { + type: 'info', + title: 'Update Available', + message: `A new version ${info.version} is available!`, + detail: 'Would you like to download it now?', + buttons: ['Download', 'Later'], + defaultId: 0, + cancelId: 1 + }).then((result) => { + if (result.response === 0) { + autoUpdater.downloadUpdate(); + } + }); + }); + + // Update not available + autoUpdater.on('update-not-available', (info) => { + log.info('Update not available:', info.version); + this.updateCheckInProgress = false; + + // Only show dialog if user manually checked for updates + if (this.manualCheck) { + dialog.showMessageBox(this.mainWindow, { + type: 'info', + title: 'No Updates', + message: 'You are already running the latest version.', + detail: `Current version: ${info.version}`, + buttons: ['OK'] + }); + this.manualCheck = false; + } + }); + + // Error during update check + autoUpdater.on('error', (err) => { + log.error('Error in auto-updater:', err); + this.updateCheckInProgress = false; + + // Only show error if user manually checked for updates + if (this.manualCheck) { + dialog.showMessageBox(this.mainWindow, { + type: 'error', + title: 'Update Error', + message: 'Error checking for updates', + detail: err.message, + buttons: ['OK'] + }); + this.manualCheck = false; + } + }); + + // Download progress + autoUpdater.on('download-progress', (progressObj) => { + const message = `Downloading update: ${Math.round(progressObj.percent)}%`; + log.info(message); + this.sendStatusToWindow(message); + }); + + // Update downloaded + autoUpdater.on('update-downloaded', (info) => { + log.info('Update downloaded:', info.version); + + dialog.showMessageBox(this.mainWindow, { + type: 'info', + title: 'Update Ready', + message: `Version ${info.version} has been downloaded.`, + detail: 'The update will be installed when you quit the application. Would you like to restart now?', + buttons: ['Restart Now', 'Later'], + defaultId: 0, + cancelId: 1 + }).then((result) => { + if (result.response === 0) { + // Quit and install update + autoUpdater.quitAndInstall(); + } + }); + }); + } + + sendStatusToWindow(message) { + if (this.mainWindow && this.mainWindow.webContents) { + this.mainWindow.webContents.send('update-status', message); + } + } + + // Check for updates (manual check by user) + checkForUpdates() { + if (this.updateCheckInProgress) { + log.info('Update check already in progress'); + return; + } + + this.manualCheck = true; + this.updateCheckInProgress = true; + + log.info('Manually checking for updates...'); + autoUpdater.checkForUpdates().catch((err) => { + log.error('Failed to check for updates:', err); + this.updateCheckInProgress = false; + this.manualCheck = false; + }); + } + + // Check for updates silently (on app startup) + checkForUpdatesQuietly() { + if (this.updateCheckInProgress) { + log.info('Update check already in progress'); + return; + } + + this.manualCheck = false; + this.updateCheckInProgress = true; + + log.info('Checking for updates quietly...'); + autoUpdater.checkForUpdates().catch((err) => { + log.error('Failed to check for updates:', err); + this.updateCheckInProgress = false; + }); + } +} + +module.exports = AutoUpdaterService; diff --git a/src/main.js b/src/main.js index 0dd62ee..e0b9a9f 100644 --- a/src/main.js +++ b/src/main.js @@ -18,6 +18,7 @@ const { uIOhook, UiohookKey } = require("uiohook-napi"); const DatabaseManager = require("./database-manager"); const PermissionManager = require("./permission-manager"); const TranscriptionService = require("./services/transcription-service"); +const AutoUpdaterService = require("./auto-updater"); // Import platform-specific text inserters let windowsTextInserter = null; @@ -60,6 +61,9 @@ const db = new DatabaseManager(); const permissionManager = new PermissionManager(); const isDevelopment = process.env.NODE_ENV === 'development' || process.argv.includes('--dev'); +// Auto-updater service (initialized after main window is created) +let autoUpdaterService = null; + // Transcription service cache to avoid recreating clients let transcriptionServiceCache = new Map(); @@ -791,6 +795,17 @@ app.whenReady().then(async () => { { type: "separator" }, + { + label: "Check for Updates...", + click: () => { + if (autoUpdaterService) { + autoUpdaterService.checkForUpdates(); + } + } + }, + { + type: "separator" + }, { label: "Preferences...", accelerator: process.platform === "darwin" ? "Command+," : "Ctrl+,", @@ -855,6 +870,17 @@ app.whenReady().then(async () => { createTray(); await setupGlobalHotkeys(); + // Initialize auto-updater service (only in production) + if (!isDevelopment) { + autoUpdaterService = new AutoUpdaterService(mainWindow); + // Check for updates quietly on startup (after a short delay) + setTimeout(() => { + if (autoUpdaterService) { + autoUpdaterService.checkForUpdatesQuietly(); + } + }, 3000); + } + // Show main window on startup unless startMinimized is true const startMinimized = store.get("startMinimized", false); if (!startMinimized) { @@ -1274,6 +1300,13 @@ ipcMain.handle("get-app-version", () => { return app.getVersion(); }); +// Check for updates +ipcMain.handle("check-for-updates", () => { + if (autoUpdaterService) { + autoUpdaterService.checkForUpdates(); + } +}); + // Dictionary-related IPC handlers ipcMain.handle("get-dictionary", async (event) => { try { diff --git a/src/views/main.css b/src/views/main.css index 204c486..e3d0139 100644 --- a/src/views/main.css +++ b/src/views/main.css @@ -148,6 +148,25 @@ body { margin-top: 0; } +.check-update-btn { + background: none; + border: none; + color: var(--text-soft); + cursor: pointer; + padding: 2px; + margin-left: 4px; + border-radius: 4px; + display: inline-flex; + align-items: center; + opacity: 0.6; + transition: opacity 0.2s, background-color 0.2s; +} + +.check-update-btn:hover { + opacity: 1; + background-color: var(--bg-hover); +} + .welcome { margin-bottom: 40px; } diff --git a/src/views/main.html b/src/views/main.html index 26635aa..38d9fec 100644 --- a/src/views/main.html +++ b/src/views/main.html @@ -16,6 +16,9 @@
Basic
v-
+
@@ -160,6 +163,19 @@

How to use the Dictionary

const version = await ipcRenderer.invoke("get-app-version"); const el = document.getElementById('appVersion'); if (el) el.textContent = `v${version}`; + + // Show the check for updates button + const checkUpdateBtn = document.getElementById('checkUpdateBtn'); + if (checkUpdateBtn) { + checkUpdateBtn.style.display = 'inline-flex'; + checkUpdateBtn.addEventListener('click', async () => { + try { + await ipcRenderer.invoke("check-for-updates"); + } catch (e) { + console.error('Failed to check for updates', e); + } + }); + } } catch (e) { console.error('Failed to load app version', e); } From 43e4f656ffd5858264a55a7c526bfc87b72cc265 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:21:14 +0000 Subject: [PATCH 3/4] Update documentation with auto-update information Co-authored-by: hellotaotao <1796860+hellotaotao@users.noreply.github.com> --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 1f65473..40d3515 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A voice input method software built with Electron that allows you to dictate tex - **Cross-platform**: Works on macOS, Windows, and Linux - **Background Operation**: Runs silently in the system tray - **Customizable Settings**: Configure API key, microphone, and languages +- **Auto-Update**: Automatically checks for and installs updates (production builds only) ## Installation @@ -37,6 +38,16 @@ Access settings through the system tray menu or main window to configure: - Default microphone - Transcription language +## Auto-Update + +WhispLine includes automatic update functionality that: +- Checks for updates on app startup (production builds only) +- Notifies you when a new version is available +- Downloads and installs updates with your permission +- Can be manually triggered via the "Check for Updates" menu item or button in the main window + +**Note**: Auto-update only works in production builds. Development mode (using `npm run dev`) does not check for updates. + ## Development ```bash @@ -50,6 +61,22 @@ npm run dev npm run build ``` +## Publishing Releases + +To enable auto-update functionality for users: + +1. Build the app using `npm run build` +2. Create a new release on GitHub with a version tag (e.g., `v1.0.78`) +3. Upload the built artifacts from the `dist/` folder to the GitHub release +4. The auto-updater will automatically detect and download new releases for users + +The app uses GitHub Releases as the update server. Each new release should include: +- macOS: `.dmg` file +- Windows: `.exe` installer +- Linux: `.AppImage` file + +**Note**: For auto-update to work properly, ensure the `version` field in `package.json` matches the release tag on GitHub. + ## Console Character Encoding (Windows) On Windows, the console may display non-English characters as garbled text due to PowerShell/CMD output encoding settings. From 4af0ebf9ebe2cd9ce69f92bd0ca3ff4be1bff646 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:22:32 +0000 Subject: [PATCH 4/4] Address code review feedback: move inline styles to CSS Co-authored-by: hellotaotao <1796860+hellotaotao@users.noreply.github.com> --- src/auto-updater.js | 2 +- src/views/main.css | 8 ++++++++ src/views/main.html | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/auto-updater.js b/src/auto-updater.js index 81c31bf..37634b2 100644 --- a/src/auto-updater.js +++ b/src/auto-updater.js @@ -151,4 +151,4 @@ class AutoUpdaterService { } } -module.exports = AutoUpdaterService; +module.exports = AutoUpdaterService; \ No newline at end of file diff --git a/src/views/main.css b/src/views/main.css index e3d0139..0cb8913 100644 --- a/src/views/main.css +++ b/src/views/main.css @@ -162,6 +162,14 @@ body { transition: opacity 0.2s, background-color 0.2s; } +.check-update-btn--hidden { + display: none; +} + +.check-update-btn .material-icons { + font-size: 14px; +} + .check-update-btn:hover { opacity: 1; background-color: var(--bg-hover); diff --git a/src/views/main.html b/src/views/main.html index 38d9fec..7e594ab 100644 --- a/src/views/main.html +++ b/src/views/main.html @@ -16,8 +16,8 @@
Basic
v-
-
@@ -167,7 +167,7 @@

How to use the Dictionary

// Show the check for updates button const checkUpdateBtn = document.getElementById('checkUpdateBtn'); if (checkUpdateBtn) { - checkUpdateBtn.style.display = 'inline-flex'; + checkUpdateBtn.classList.remove('check-update-btn--hidden'); checkUpdateBtn.addEventListener('click', async () => { try { await ipcRenderer.invoke("check-for-updates");