Sync latest TrueGrowth updates
This commit is contained in:
8
apps/electron/build/entitlements.mac.inherit.plist
Normal file
8
apps/electron/build/entitlements.mac.inherit.plist
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
12
apps/electron/build/entitlements.mac.plist
Normal file
12
apps/electron/build/entitlements.mac.plist
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -7,14 +7,25 @@ import {
|
||||
shell,
|
||||
utilityProcess,
|
||||
} from 'electron';
|
||||
import electronUpdater from 'electron-updater';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const { autoUpdater } = electronUpdater;
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const brandName = 'TrueGrowth';
|
||||
const rendererUrl = process.env.VITE_DEV_SERVER_URL;
|
||||
const localApiPort = Number(process.env.TRUEGROWTH_LOCAL_API_PORT || 48177);
|
||||
const updateFeedUrl =
|
||||
process.env.TRUEGROWTH_UPDATE_FEED_URL ||
|
||||
'https://truegrowth.benchu.cloud/desktop/stable/';
|
||||
const updateCheckDelayMs = Number(
|
||||
process.env.TRUEGROWTH_UPDATE_CHECK_DELAY_MS || 15000
|
||||
);
|
||||
const updateCheckIntervalMs = Number(
|
||||
process.env.TRUEGROWTH_UPDATE_CHECK_INTERVAL_MS || 4 * 60 * 60 * 1000
|
||||
);
|
||||
const userDataPath =
|
||||
process.env.TRUEGROWTH_ELECTRON_USER_DATA ||
|
||||
path.join(app.getPath('appData'), brandName);
|
||||
@@ -28,6 +39,10 @@ const brandIconPath = resolveAppPath(
|
||||
|
||||
let mainWindow = null;
|
||||
let localApi = null;
|
||||
let updateCheckTimer = null;
|
||||
let updateCheckInFlight = null;
|
||||
let updateDownloadInFlight = null;
|
||||
let updateState = null;
|
||||
|
||||
function readAppVersion() {
|
||||
try {
|
||||
@@ -41,11 +56,282 @@ function readAppVersion() {
|
||||
}
|
||||
|
||||
const appVersion = readAppVersion();
|
||||
updateState = createInitialUpdateState();
|
||||
|
||||
function resolveAppPath(...segments) {
|
||||
return path.join(appRoot, ...segments);
|
||||
}
|
||||
|
||||
function createInitialUpdateState() {
|
||||
return {
|
||||
appVersion: appVersion || app.getVersion(),
|
||||
available: false,
|
||||
autoDownload: false,
|
||||
canCheck: true,
|
||||
canDownload: false,
|
||||
canInstall: false,
|
||||
channelUrl: updateFeedUrl,
|
||||
downloaded: false,
|
||||
downloading: false,
|
||||
error: '',
|
||||
feedReady: false,
|
||||
isPackaged: app.isPackaged,
|
||||
lastCheckedAt: null,
|
||||
notes: '',
|
||||
percent: 0,
|
||||
platform: process.platform,
|
||||
status: 'idle',
|
||||
updateVersion: null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeReleaseNotes(releaseNotes) {
|
||||
if (Array.isArray(releaseNotes)) {
|
||||
return releaseNotes
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
return String(item.note || item.notes || item.version || '');
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
if (typeof releaseNotes === 'string') {
|
||||
return releaseNotes;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function updateUpdaterState(patch) {
|
||||
updateState = {
|
||||
...updateState,
|
||||
...patch,
|
||||
appVersion: appVersion || app.getVersion(),
|
||||
channelUrl: updateFeedUrl,
|
||||
isPackaged: app.isPackaged,
|
||||
platform: process.platform,
|
||||
};
|
||||
broadcastUpdaterState();
|
||||
return updateState;
|
||||
}
|
||||
|
||||
function broadcastUpdaterState() {
|
||||
BrowserWindow.getAllWindows().forEach((window) => {
|
||||
if (!window.isDestroyed()) {
|
||||
window.webContents.send('truegrowth-update:state', updateState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function configureAutoUpdater() {
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.logger = console;
|
||||
autoUpdater.setFeedURL({
|
||||
provider: 'generic',
|
||||
url: updateFeedUrl,
|
||||
});
|
||||
updateUpdaterState({
|
||||
autoDownload: autoUpdater.autoDownload,
|
||||
feedReady: true,
|
||||
});
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
updateUpdaterState({
|
||||
canCheck: false,
|
||||
error: '',
|
||||
status: 'checking',
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
updateUpdaterState({
|
||||
available: false,
|
||||
canCheck: true,
|
||||
canDownload: false,
|
||||
canInstall: false,
|
||||
downloaded: false,
|
||||
downloading: false,
|
||||
error: '',
|
||||
lastCheckedAt: Date.now(),
|
||||
percent: 0,
|
||||
status: 'not-available',
|
||||
updateVersion: null,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
updateUpdaterState({
|
||||
available: true,
|
||||
canCheck: true,
|
||||
canDownload: true,
|
||||
canInstall: false,
|
||||
downloaded: false,
|
||||
downloading: false,
|
||||
error: '',
|
||||
lastCheckedAt: Date.now(),
|
||||
notes: normalizeReleaseNotes(info?.releaseNotes),
|
||||
percent: 0,
|
||||
status: 'available',
|
||||
updateVersion: info?.version || null,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
updateUpdaterState({
|
||||
available: true,
|
||||
canCheck: false,
|
||||
canDownload: false,
|
||||
canInstall: false,
|
||||
downloaded: false,
|
||||
downloading: true,
|
||||
error: '',
|
||||
percent: Math.max(0, Math.min(100, Number(progress?.percent || 0))),
|
||||
status: 'downloading',
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
updateDownloadInFlight = null;
|
||||
updateUpdaterState({
|
||||
available: true,
|
||||
canCheck: true,
|
||||
canDownload: false,
|
||||
canInstall: true,
|
||||
downloaded: true,
|
||||
downloading: false,
|
||||
error: '',
|
||||
notes: normalizeReleaseNotes(info?.releaseNotes) || updateState.notes,
|
||||
percent: 100,
|
||||
status: 'downloaded',
|
||||
updateVersion: info?.version || updateState.updateVersion,
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (error) => {
|
||||
updateCheckInFlight = null;
|
||||
updateDownloadInFlight = null;
|
||||
updateUpdaterState({
|
||||
canCheck: true,
|
||||
canDownload: Boolean(updateState.available && !updateState.downloaded),
|
||||
downloading: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: String(error || '更新检查失败'),
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function checkForDesktopUpdate({ manual = false } = {}) {
|
||||
if (!app.isPackaged && !process.env.TRUEGROWTH_ALLOW_DEV_UPDATES) {
|
||||
return updateUpdaterState({
|
||||
canCheck: true,
|
||||
error: manual ? '开发模式下不会连接正式更新通道。' : '',
|
||||
feedReady: true,
|
||||
status: manual ? 'dev-disabled' : 'idle',
|
||||
});
|
||||
}
|
||||
if (updateCheckInFlight) {
|
||||
return updateCheckInFlight;
|
||||
}
|
||||
updateCheckInFlight = autoUpdater
|
||||
.checkForUpdates()
|
||||
.then(() => updateState)
|
||||
.finally(() => {
|
||||
updateCheckInFlight = null;
|
||||
});
|
||||
return updateCheckInFlight;
|
||||
}
|
||||
|
||||
async function downloadDesktopUpdate() {
|
||||
if (!app.isPackaged && !process.env.TRUEGROWTH_ALLOW_DEV_UPDATES) {
|
||||
return updateUpdaterState({
|
||||
error: '开发模式下不会下载正式更新包。',
|
||||
status: 'dev-disabled',
|
||||
});
|
||||
}
|
||||
if (!updateState.available) {
|
||||
return updateUpdaterState({
|
||||
error: '当前没有可下载的新版本。',
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
if (updateDownloadInFlight) {
|
||||
return updateDownloadInFlight;
|
||||
}
|
||||
updateDownloadInFlight = autoUpdater
|
||||
.downloadUpdate()
|
||||
.then(() => updateState)
|
||||
.finally(() => {
|
||||
if (updateState.status !== 'downloaded') {
|
||||
updateDownloadInFlight = null;
|
||||
}
|
||||
});
|
||||
updateUpdaterState({
|
||||
canCheck: false,
|
||||
canDownload: false,
|
||||
downloading: true,
|
||||
error: '',
|
||||
status: 'downloading',
|
||||
});
|
||||
return updateDownloadInFlight;
|
||||
}
|
||||
|
||||
function installDesktopUpdate() {
|
||||
if (!updateState.downloaded) {
|
||||
return updateUpdaterState({
|
||||
error: '更新包尚未下载完成。',
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
updateUpdaterState({
|
||||
canCheck: false,
|
||||
canDownload: false,
|
||||
canInstall: false,
|
||||
status: 'installing',
|
||||
});
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
return updateState;
|
||||
}
|
||||
|
||||
function openDesktopUpdateChannel() {
|
||||
void shell.openExternal(updateFeedUrl);
|
||||
return updateState;
|
||||
}
|
||||
|
||||
function installUpdateIpc() {
|
||||
ipcMain.handle('truegrowth-update:get-state', () => updateState);
|
||||
ipcMain.handle('truegrowth-update:check', () =>
|
||||
checkForDesktopUpdate({ manual: true })
|
||||
);
|
||||
ipcMain.handle('truegrowth-update:download', () => downloadDesktopUpdate());
|
||||
ipcMain.handle('truegrowth-update:install', () => installDesktopUpdate());
|
||||
ipcMain.handle('truegrowth-update:open-channel', () =>
|
||||
openDesktopUpdateChannel()
|
||||
);
|
||||
}
|
||||
|
||||
function scheduleDesktopUpdateChecks() {
|
||||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer);
|
||||
updateCheckTimer = null;
|
||||
}
|
||||
setTimeout(() => {
|
||||
void checkForDesktopUpdate().catch(() => undefined);
|
||||
}, Math.max(0, updateCheckDelayMs));
|
||||
updateCheckTimer = setInterval(() => {
|
||||
void checkForDesktopUpdate().catch(() => undefined);
|
||||
}, Math.max(60_000, updateCheckIntervalMs));
|
||||
}
|
||||
|
||||
function createDesktopLocalRendererEntry(indexHtmlPath) {
|
||||
const html = fs.readFileSync(indexHtmlPath, 'utf8');
|
||||
const appAssetPattern =
|
||||
@@ -72,8 +358,8 @@ process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR =
|
||||
process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR ||
|
||||
(app.isPackaged ? process.resourcesPath : appRoot);
|
||||
|
||||
const hasSingleInstanceLock = app.requestSingleInstanceLock({
|
||||
rendererUrl,
|
||||
const hasSingleInstanceLock = app.requestSingleInstanceLock({
|
||||
rendererUrl,
|
||||
userDataPath,
|
||||
});
|
||||
|
||||
@@ -203,6 +489,20 @@ function installApplicationMenu() {
|
||||
? [{ type: 'separator' }, { role: 'front' }]
|
||||
: [{ role: 'close' }]),
|
||||
];
|
||||
const helpMenu = [
|
||||
{
|
||||
label: 'Check for Updates',
|
||||
click: () => {
|
||||
void checkForDesktopUpdate({ manual: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Open Update Channel',
|
||||
click: () => {
|
||||
openDesktopUpdateChannel();
|
||||
},
|
||||
},
|
||||
];
|
||||
const template = [
|
||||
...(process.platform === 'darwin'
|
||||
? [
|
||||
@@ -231,7 +531,7 @@ function installApplicationMenu() {
|
||||
{ label: 'Edit', submenu: editMenu },
|
||||
{ label: 'View', submenu: viewMenu },
|
||||
{ label: 'Window', submenu: windowMenu },
|
||||
{ label: 'Help', submenu: [] },
|
||||
{ label: 'Help', submenu: helpMenu },
|
||||
];
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
@@ -280,6 +580,7 @@ function createWindow() {
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
sendWindowState(mainWindow);
|
||||
sendSystemThemeState(mainWindow);
|
||||
broadcastUpdaterState();
|
||||
});
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
@@ -327,9 +628,11 @@ async function startLocalApi() {
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
configureAutoUpdater();
|
||||
installApplicationMenu();
|
||||
installWindowControlsIpc();
|
||||
installThemeIpc();
|
||||
installUpdateIpc();
|
||||
if (process.platform === 'darwin' && app.dock) {
|
||||
app.dock.setIcon(brandIconPath);
|
||||
}
|
||||
@@ -341,6 +644,7 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
await startLocalApi();
|
||||
createWindow();
|
||||
scheduleDesktopUpdateChecks();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
@@ -356,5 +660,9 @@ app.on('window-all-closed', () => {
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer);
|
||||
updateCheckTimer = null;
|
||||
}
|
||||
localApi?.close();
|
||||
});
|
||||
|
||||
@@ -35,6 +35,23 @@ const themeBridge = {
|
||||
},
|
||||
};
|
||||
|
||||
const updatesBridge = {
|
||||
getState: () => ipcRenderer.invoke('truegrowth-update:get-state'),
|
||||
check: () => ipcRenderer.invoke('truegrowth-update:check'),
|
||||
download: () => ipcRenderer.invoke('truegrowth-update:download'),
|
||||
install: () => ipcRenderer.invoke('truegrowth-update:install'),
|
||||
openChannel: () => ipcRenderer.invoke('truegrowth-update:open-channel'),
|
||||
onStateChange: (callback) => {
|
||||
if (typeof callback !== 'function') {
|
||||
return () => undefined;
|
||||
}
|
||||
const listener = (_event, state) => callback(state);
|
||||
ipcRenderer.on('truegrowth-update:state', listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener('truegrowth-update:state', listener);
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('trueGrowthRuntime', {
|
||||
app: 'TrueGrowth',
|
||||
platform: process.platform,
|
||||
@@ -42,6 +59,7 @@ contextBridge.exposeInMainWorld('trueGrowthRuntime', {
|
||||
localApiBaseUrl: 'http://127.0.0.1:48177/local-api',
|
||||
windowControls,
|
||||
theme: themeBridge,
|
||||
updates: updatesBridge,
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('__OPENTU_DESKTOP__', true);
|
||||
|
||||
Reference in New Issue
Block a user