feat: switch to electron-updater for auto-updates
Replaces the custom server-poll updater (which depended on a placeholder serverUrl 'https://your-server.com/api' and never fired in practice) with electron-updater. Now a banner appears automatically when a new release is published — auto-download in background, in-place install via quitAndInstall, delta updates via blockmap. Changes: - Add electron-updater dep, build.publish (provider: generic, placeholder URL) - Rewrite UpdaterManager around autoUpdater events (available/progress/ downloaded/error), forwarded to renderer via window.webContents.send - Drop hardcoded APP_VERSION constant; main uses app.getVersion(), renderer fetches via new GET_APP_VERSION IPC channel - IPC: drop DOWNLOAD_UPDATE (autoDownload handles it), add INSTALL_UPDATE + GET_APP_VERSION - VersionInfo reshaped (currentVersion field, no downloadUrl/mandatory); add UpdateProgress and UpdateCheckResult types - UpdateNotification: 3-phase UI (downloading with progress bar, ready with restart-and-install, hidden); App.tsx tracks phase state TODO before first real release: - Replace build.publish.url placeholder with the actual generic host - Bump version, run package:win, upload latest.yml + .exe + .blockmap to host Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
106
package-lock.json
generated
106
package-lock.json
generated
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "media-center",
|
"name": "media-center",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "media-center",
|
"name": "media-center",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
|
"electron-updater": "^6.8.3",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
@@ -2197,7 +2198,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/assert-plus": {
|
"node_modules/assert-plus": {
|
||||||
@@ -3135,7 +3135,6 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -3698,6 +3697,82 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-updater": {
|
||||||
|
"version": "6.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
|
||||||
|
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
|
||||||
|
"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.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"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/node_modules/@types/node": {
|
"node_modules/electron/node_modules/@types/node": {
|
||||||
"version": "18.19.130",
|
"version": "18.19.130",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
|
||||||
@@ -4334,7 +4409,6 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
@@ -4709,7 +4783,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -4796,7 +4869,6 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lazystream": {
|
"node_modules/lazystream": {
|
||||||
@@ -4903,6 +4975,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"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.flatten": {
|
"node_modules/lodash.flatten": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||||
@@ -4911,6 +4989,13 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"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/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
@@ -5118,7 +5203,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
@@ -6251,6 +6335,12 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
|
"electron-updater": "^6.8.3",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
@@ -53,6 +54,10 @@
|
|||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
|
"publish": {
|
||||||
|
"provider": "generic",
|
||||||
|
"url": "https://your-update-server.example.com/media-center/"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"node_modules/**/*",
|
"node_modules/**/*",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async function initializeApp() {
|
|||||||
proxyManager = new ProxyManager();
|
proxyManager = new ProxyManager();
|
||||||
configManager = new ConfigManager(userDataPath);
|
configManager = new ConfigManager(userDataPath);
|
||||||
databaseManager = new DatabaseManager(userDataPath);
|
databaseManager = new DatabaseManager(userDataPath);
|
||||||
updaterManager = new UpdaterManager(configManager);
|
updaterManager = new UpdaterManager(() => mainWindow);
|
||||||
tabManager = new TabManager();
|
tabManager = new TabManager();
|
||||||
|
|
||||||
// Load initial configuration
|
// Load initial configuration
|
||||||
@@ -69,11 +69,7 @@ async function initializeApp() {
|
|||||||
|
|
||||||
// Check for updates
|
// Check for updates
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updaterManager.checkForUpdates().then((versionInfo) => {
|
updaterManager.checkForUpdates();
|
||||||
if (versionInfo.updateAvailable && mainWindow) {
|
|
||||||
mainWindow.webContents.send('update-available', versionInfo);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,8 +165,12 @@ function setupIpcHandlers() {
|
|||||||
return await updaterManager.checkForUpdates();
|
return await updaterManager.checkForUpdates();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.DOWNLOAD_UPDATE, async (_, downloadUrl: string) => {
|
ipcMain.handle(IPC_CHANNELS.INSTALL_UPDATE, () => {
|
||||||
return await updaterManager.downloadUpdate(downloadUrl);
|
updaterManager.installUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.GET_APP_VERSION, () => {
|
||||||
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Settings handlers
|
// Settings handlers
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const IPC_CHANNELS = {
|
|||||||
START_PROXY: 'proxy:start',
|
START_PROXY: 'proxy:start',
|
||||||
STOP_PROXY: 'proxy:stop',
|
STOP_PROXY: 'proxy:stop',
|
||||||
CHECK_VERSION: 'version:check',
|
CHECK_VERSION: 'version:check',
|
||||||
DOWNLOAD_UPDATE: 'version:download',
|
INSTALL_UPDATE: 'version:install',
|
||||||
|
GET_APP_VERSION: 'app:version',
|
||||||
GET_SETTINGS: 'settings:get',
|
GET_SETTINGS: 'settings:get',
|
||||||
SAVE_SETTINGS: 'settings:save',
|
SAVE_SETTINGS: 'settings:save',
|
||||||
};
|
};
|
||||||
@@ -53,20 +54,29 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
startProxy: () => ipcRenderer.invoke(IPC_CHANNELS.START_PROXY),
|
startProxy: () => ipcRenderer.invoke(IPC_CHANNELS.START_PROXY),
|
||||||
stopProxy: () => ipcRenderer.invoke(IPC_CHANNELS.STOP_PROXY),
|
stopProxy: () => ipcRenderer.invoke(IPC_CHANNELS.STOP_PROXY),
|
||||||
|
|
||||||
// Version
|
// Version / Updates
|
||||||
checkVersion: () => ipcRenderer.invoke(IPC_CHANNELS.CHECK_VERSION),
|
checkVersion: () => ipcRenderer.invoke(IPC_CHANNELS.CHECK_VERSION),
|
||||||
downloadUpdate: (downloadUrl: string) =>
|
installUpdate: () => ipcRenderer.invoke(IPC_CHANNELS.INSTALL_UPDATE),
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.DOWNLOAD_UPDATE, downloadUrl),
|
getAppVersion: () => ipcRenderer.invoke(IPC_CHANNELS.GET_APP_VERSION),
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
getSettings: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SETTINGS),
|
getSettings: () => ipcRenderer.invoke(IPC_CHANNELS.GET_SETTINGS),
|
||||||
saveSettings: (settings: any) =>
|
saveSettings: (settings: any) =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.SAVE_SETTINGS, settings),
|
ipcRenderer.invoke(IPC_CHANNELS.SAVE_SETTINGS, settings),
|
||||||
|
|
||||||
// Event listeners
|
// Update event listeners
|
||||||
onUpdateAvailable: (callback: (versionInfo: any) => void) => {
|
onUpdateAvailable: (callback: (versionInfo: any) => void) => {
|
||||||
ipcRenderer.on('update-available', (_, versionInfo) => callback(versionInfo));
|
ipcRenderer.on('update-available', (_, versionInfo) => callback(versionInfo));
|
||||||
},
|
},
|
||||||
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
|
ipcRenderer.on('update-download-progress', (_, progress) => callback(progress));
|
||||||
|
},
|
||||||
|
onUpdateDownloaded: (callback: (info: any) => void) => {
|
||||||
|
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||||
|
},
|
||||||
|
onUpdateError: (callback: (error: any) => void) => {
|
||||||
|
ipcRenderer.on('update-error', (_, error) => callback(error));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Type declaration for TypeScript
|
// Type declaration for TypeScript
|
||||||
@@ -87,11 +97,15 @@ declare global {
|
|||||||
getProxyStatus: () => Promise<any>;
|
getProxyStatus: () => Promise<any>;
|
||||||
startProxy: () => Promise<any>;
|
startProxy: () => Promise<any>;
|
||||||
stopProxy: () => Promise<any>;
|
stopProxy: () => Promise<any>;
|
||||||
checkVersion: () => Promise<any>;
|
checkVersion: () => Promise<{ updateAvailable: boolean; latestVersion: string }>;
|
||||||
downloadUpdate: (downloadUrl: string) => Promise<string>;
|
installUpdate: () => Promise<void>;
|
||||||
|
getAppVersion: () => Promise<string>;
|
||||||
getSettings: () => Promise<any>;
|
getSettings: () => Promise<any>;
|
||||||
saveSettings: (settings: any) => Promise<void>;
|
saveSettings: (settings: any) => Promise<void>;
|
||||||
onUpdateAvailable: (callback: (versionInfo: any) => void) => void;
|
onUpdateAvailable: (callback: (versionInfo: any) => void) => void;
|
||||||
|
onDownloadProgress: (callback: (progress: any) => void) => void;
|
||||||
|
onUpdateDownloaded: (callback: (info: any) => void) => void;
|
||||||
|
onUpdateError: (callback: (error: any) => void) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,71 @@
|
|||||||
import axios from 'axios';
|
import { app, BrowserWindow } from 'electron';
|
||||||
import * as fs from 'fs';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import * as path from 'path';
|
import type { UpdateCheckResult } from '../shared/types';
|
||||||
import { app, shell } from 'electron';
|
|
||||||
import { VersionInfo } from '../shared/types';
|
|
||||||
import { APP_VERSION } from '../shared/constants';
|
|
||||||
import { ConfigManager } from './config';
|
|
||||||
|
|
||||||
export class UpdaterManager {
|
export class UpdaterManager {
|
||||||
private configManager: ConfigManager;
|
private getWindow: () => BrowserWindow | null;
|
||||||
|
|
||||||
constructor(configManager: ConfigManager) {
|
constructor(getWindow: () => BrowserWindow | null) {
|
||||||
this.configManager = configManager;
|
this.getWindow = getWindow;
|
||||||
|
|
||||||
|
autoUpdater.autoDownload = true;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
|
this.wireEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkForUpdates(): Promise<VersionInfo> {
|
private send(channel: string, payload?: unknown) {
|
||||||
try {
|
const win = this.getWindow();
|
||||||
const settings = await this.configManager.getConfig();
|
if (win && !win.isDestroyed()) {
|
||||||
const serverUrl = (settings as any).serverUrl || 'https://your-server.com/api';
|
win.webContents.send(channel, payload);
|
||||||
const endpoint = `${serverUrl}/version/check`;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios.get<VersionInfo>(endpoint, {
|
private wireEvents() {
|
||||||
params: {
|
autoUpdater.on('update-available', (info) => {
|
||||||
currentVersion: APP_VERSION,
|
this.send('update-available', {
|
||||||
platform: process.platform,
|
latestVersion: info.version,
|
||||||
},
|
currentVersion: app.getVersion(),
|
||||||
timeout: 10000,
|
releaseDate: info.releaseDate ?? '',
|
||||||
|
changelog: typeof info.releaseNotes === 'string' ? info.releaseNotes : '',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
autoUpdater.on('download-progress', (progress) => {
|
||||||
} catch (error: any) {
|
this.send('update-download-progress', {
|
||||||
console.error('Error checking for updates:', error.message);
|
percent: progress.percent,
|
||||||
// Return no update available if check fails
|
bytesPerSecond: progress.bytesPerSecond,
|
||||||
return {
|
transferred: progress.transferred,
|
||||||
latestVersion: APP_VERSION,
|
total: progress.total,
|
||||||
updateAvailable: false,
|
});
|
||||||
downloadUrl: '',
|
|
||||||
changelog: '',
|
|
||||||
releaseDate: '',
|
|
||||||
mandatory: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadUpdate(downloadUrl: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const downloadsPath = app.getPath('downloads');
|
|
||||||
const fileName = path.basename(new URL(downloadUrl).pathname);
|
|
||||||
const filePath = path.join(downloadsPath, fileName);
|
|
||||||
|
|
||||||
const response = await axios.get(downloadUrl, {
|
|
||||||
responseType: 'stream',
|
|
||||||
timeout: 300000, // 5 minutes
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const writer = fs.createWriteStream(filePath);
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
|
this.send('update-downloaded', { latestVersion: info.version });
|
||||||
response.data.pipe(writer);
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
autoUpdater.on('error', (err) => {
|
||||||
writer.on('finish', () => resolve(filePath));
|
console.error('[updater] error:', err.message);
|
||||||
writer.on('error', reject);
|
this.send('update-error', { message: err.message });
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error downloading update:', error.message);
|
|
||||||
throw new Error(`Failed to download update: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async installUpdate(installerPath: string): Promise<void> {
|
async checkForUpdates(): Promise<UpdateCheckResult> {
|
||||||
|
const currentVersion = app.getVersion();
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
return { updateAvailable: false, latestVersion: currentVersion };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// Open the installer
|
const result = await autoUpdater.checkForUpdates();
|
||||||
await shell.openPath(installerPath);
|
const latestVersion = result?.updateInfo?.version ?? currentVersion;
|
||||||
|
return { updateAvailable: latestVersion !== currentVersion, latestVersion };
|
||||||
// Close the app after a short delay
|
} catch (err: any) {
|
||||||
setTimeout(() => {
|
console.error('[updater] check failed:', err.message);
|
||||||
app.quit();
|
return { updateAvailable: false, latestVersion: currentVersion };
|
||||||
}, 1000);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error installing update:', error.message);
|
|
||||||
throw new Error(`Failed to install update: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compareVersions(v1: string, v2: string): number {
|
installUpdate(): void {
|
||||||
const parts1 = v1.split('.').map(Number);
|
autoUpdater.quitAndInstall();
|
||||||
const parts2 = v2.split('.').map(Number);
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
||||||
const part1 = parts1[i] || 0;
|
|
||||||
const part2 = parts2[i] || 0;
|
|
||||||
|
|
||||||
if (part1 > part2) return 1;
|
|
||||||
if (part1 < part2) return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,25 +6,38 @@ import Bookmarks from './pages/Bookmarks';
|
|||||||
import ActiveTabs from './pages/ActiveTabs';
|
import ActiveTabs from './pages/ActiveTabs';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
import UpdateNotification from './components/UpdateNotification';
|
import UpdateNotification from './components/UpdateNotification';
|
||||||
import { VersionInfo } from '../shared/types';
|
import { VersionInfo, UpdateProgress } from '../shared/types';
|
||||||
|
|
||||||
|
type UpdatePhase = 'idle' | 'downloading' | 'ready';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [updateInfo, setUpdateInfo] = useState<VersionInfo | null>(null);
|
const [phase, setPhase] = useState<UpdatePhase>('idle');
|
||||||
|
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
|
||||||
|
const [progress, setProgress] = useState<UpdateProgress | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Listen for update notifications
|
window.electronAPI.onUpdateAvailable((info) => {
|
||||||
window.electronAPI.onUpdateAvailable((versionInfo) => {
|
setVersionInfo(info);
|
||||||
setUpdateInfo(versionInfo);
|
setPhase('downloading');
|
||||||
|
});
|
||||||
|
window.electronAPI.onDownloadProgress((p) => {
|
||||||
|
setProgress(p);
|
||||||
|
});
|
||||||
|
window.electronAPI.onUpdateDownloaded(() => {
|
||||||
|
setPhase('ready');
|
||||||
|
setProgress(null);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Layout>
|
<Layout>
|
||||||
{updateInfo && updateInfo.updateAvailable && (
|
{phase !== 'idle' && versionInfo && (
|
||||||
<UpdateNotification
|
<UpdateNotification
|
||||||
versionInfo={updateInfo}
|
phase={phase}
|
||||||
onClose={() => setUpdateInfo(null)}
|
versionInfo={versionInfo}
|
||||||
|
progress={progress}
|
||||||
|
onClose={() => setPhase('idle')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -1,55 +1,66 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { VersionInfo } from '../../shared/types';
|
import { VersionInfo, UpdateProgress } from '../../shared/types';
|
||||||
import '../styles/UpdateNotification.css';
|
import '../styles/UpdateNotification.css';
|
||||||
|
|
||||||
interface UpdateNotificationProps {
|
interface UpdateNotificationProps {
|
||||||
|
phase: 'downloading' | 'ready';
|
||||||
versionInfo: VersionInfo;
|
versionInfo: VersionInfo;
|
||||||
|
progress: UpdateProgress | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} Б`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} КБ`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`;
|
||||||
|
}
|
||||||
|
|
||||||
const UpdateNotification: React.FC<UpdateNotificationProps> = ({
|
const UpdateNotification: React.FC<UpdateNotificationProps> = ({
|
||||||
|
phase,
|
||||||
versionInfo,
|
versionInfo,
|
||||||
|
progress,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const handleInstall = () => {
|
||||||
|
window.electronAPI.installUpdate();
|
||||||
const handleUpdate = async () => {
|
|
||||||
setIsDownloading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const installerPath = await window.electronAPI.downloadUpdate(
|
|
||||||
versionInfo.downloadUrl
|
|
||||||
);
|
|
||||||
alert(`Обновление загружено: ${installerPath}\nПриложение будет закрыто для установки.`);
|
|
||||||
// App will close automatically after opening installer
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error downloading update:', error);
|
|
||||||
alert('Ошибка при загрузке обновления');
|
|
||||||
setIsDownloading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="update-notification">
|
<div className="update-notification">
|
||||||
<div className="update-content">
|
<div className="update-content">
|
||||||
<h3>Доступно обновление!</h3>
|
<h3>{phase === 'ready' ? 'Обновление готово' : 'Доступно обновление'}</h3>
|
||||||
<p className="version">
|
<p className="version">
|
||||||
Версия <strong>{versionInfo.latestVersion}</strong>
|
Версия <strong>{versionInfo.latestVersion}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
{versionInfo.changelog && (
|
||||||
<div className="changelog">
|
<div className="changelog">
|
||||||
<h4>Что нового:</h4>
|
<h4>Что нового:</h4>
|
||||||
<pre>{versionInfo.changelog}</pre>
|
<pre>{versionInfo.changelog}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{phase === 'downloading' && (
|
||||||
|
<div className="update-progress">
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div
|
||||||
|
className="progress-fill"
|
||||||
|
style={{ width: `${progress?.percent ?? 0}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="progress-label">
|
||||||
|
{progress
|
||||||
|
? `${Math.round(progress.percent)}% — ${formatBytes(progress.transferred)} / ${formatBytes(progress.total)}`
|
||||||
|
: 'Подготовка загрузки…'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="update-actions">
|
<div className="update-actions">
|
||||||
<button
|
{phase === 'ready' && (
|
||||||
className="button-primary"
|
<button className="button-primary" onClick={handleInstall}>
|
||||||
onClick={handleUpdate}
|
Перезапустить и установить
|
||||||
disabled={isDownloading}
|
|
||||||
>
|
|
||||||
{isDownloading ? 'Загрузка...' : 'Обновить сейчас'}
|
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
<button className="button-secondary" onClick={onClose}>
|
<button className="button-secondary" onClick={onClose}>
|
||||||
Напомнить позже
|
{phase === 'ready' ? 'Позже' : 'Скрыть'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,6 +59,30 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-progress {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--accent);
|
||||||
|
transition: width 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.update-actions {
|
.update-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export const APP_VERSION = '1.0.1';
|
|
||||||
|
|
||||||
export const DEFAULT_CONFIG_SERVER_URL = 'https://your-server.com/api';
|
export const DEFAULT_CONFIG_SERVER_URL = 'https://your-server.com/api';
|
||||||
|
|
||||||
export const DEFAULT_PROXY_PORT = 10808;
|
export const DEFAULT_PROXY_PORT = 10808;
|
||||||
|
|||||||
@@ -59,11 +59,21 @@ export interface ActiveTab {
|
|||||||
// Version Check Types
|
// Version Check Types
|
||||||
export interface VersionInfo {
|
export interface VersionInfo {
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
updateAvailable: boolean;
|
currentVersion: string;
|
||||||
downloadUrl: string;
|
|
||||||
changelog: string;
|
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
mandatory: boolean;
|
changelog: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProgress {
|
||||||
|
percent: number;
|
||||||
|
bytesPerSecond: number;
|
||||||
|
transferred: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateCheckResult {
|
||||||
|
updateAvailable: boolean;
|
||||||
|
latestVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy Types
|
// Proxy Types
|
||||||
@@ -113,7 +123,8 @@ export enum IPC_CHANNELS {
|
|||||||
|
|
||||||
// Version
|
// Version
|
||||||
CHECK_VERSION = 'version:check',
|
CHECK_VERSION = 'version:check',
|
||||||
DOWNLOAD_UPDATE = 'version:download',
|
INSTALL_UPDATE = 'version:install',
|
||||||
|
GET_APP_VERSION = 'app:version',
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
GET_SETTINGS = 'settings:get',
|
GET_SETTINGS = 'settings:get',
|
||||||
|
|||||||
Reference in New Issue
Block a user