feat: windows install guidance (#63)

* build: add dmg/appimage and artifact metadata

* feat: enhance packaging outputs and add Windows install-path notice
This commit is contained in:
vin authored and GitHub committed 2026-02-04 18:10:26 +08:00
1 parent 080fb67974
commit ce7147025d
7 files changed
+2280 -11

No files matched your search

+71 -1
View File
@@ -1,13 +1,21 @@
import './instrument'; // MUST be the first import to ensure Sentry initializes before app ready
import { app, BrowserWindow } from 'electron';
import { app, BrowserWindow, dialog, shell } from 'electron';
import type { MessageBoxOptions } from 'electron';
import path from 'path';
import fs from 'fs';
import squirrelStartup from 'electron-squirrel-startup';
import { ipcMain } from 'electron/main';
import { ipcContext } from '@/ipc/context';
import { IPC_CHANNELS } from './constants';
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
import { logger } from './utils/logger';
import {
getExpectedInstallRoot,
getInstallNoticeText,
isRunningFromExpectedInstallDir as isRunningFromExpectedInstallDirUtil,
resolveInstallNoticeLanguage,
} from './utils/installNotice';
import { CloudAccountRepo } from './ipc/database/cloudHandler';
import { initDatabase } from './ipc/database/handler';
import { CloudMonitorService } from './services/CloudMonitorService';
@@ -58,6 +66,11 @@ ipcMain.on(IPC_CHANNELS.CHANGE_LANGUAGE, (event, lang) => {
app.disableHardwareAcceleration();
if (squirrelStartup) {
app.quit();
process.exit(0);
}
const inDevelopment = process.env.NODE_ENV === 'development';
// Keep a global reference of the window object, if you don't, the window will
@@ -67,6 +80,62 @@ let globalMainWindow: BrowserWindow | null = null;
let isQuitting = false;
let startupConfig: AppConfig | null = null;
let shouldStartHidden = false;
let hasShownInstallNotice = false;
function isRunningFromExpectedInstallDir() {
return isRunningFromExpectedInstallDirUtil({
platform: process.platform,
isPackaged: app.isPackaged,
localAppData: process.env.LOCALAPPDATA,
appName: app.getName(),
execPath: process.execPath,
});
}
function showWindowsInstallNoticeIfNeeded() {
if (hasShownInstallNotice) {
return;
}
if (isRunningFromExpectedInstallDir()) {
return;
}
const expectedRoot = getExpectedInstallRoot({
platform: process.platform,
localAppData: process.env.LOCALAPPDATA,
appName: app.getName(),
});
if (!expectedRoot) {
return;
}
hasShownInstallNotice = true;
const language = resolveInstallNoticeLanguage({
configLanguage: startupConfig?.language,
locale: app.getLocale(),
});
const text = getInstallNoticeText(language);
const options: MessageBoxOptions = {
type: 'info',
title: text.title,
message: text.message,
detail: `${text.detailPrefix}${expectedRoot}`,
buttons: [...text.buttons],
defaultId: 1,
};
const showPromise = globalMainWindow
? dialog.showMessageBox(globalMainWindow, options)
: dialog.showMessageBox(options);
showPromise.then(({ response }) => {
if (response === 0) {
shell.openPath(expectedRoot);
}
});
}
const gotSingleInstanceLock = app.requestSingleInstanceLock();
@@ -154,6 +223,7 @@ function createWindow({ startHidden }: { startHidden: boolean }) {
}
logger.info('Window created');
showWindowsInstallNoticeIfNeeded();
mainWindow.on('close', (event) => {
if (!isQuitting) {
+120
View File
@@ -0,0 +1,120 @@
import path from 'path';
export type InstallNoticeLanguage = 'zh-CN' | 'en' | 'ru';
const installNoticeText: Record<
InstallNoticeLanguage,
{
title: string;
message: string;
detailPrefix: string;
buttons: [string, string];
}
> = {
'zh-CN': {
title: '建议从开始菜单启动',
message:
'检测到你正在从非安装路径启动应用。为了确保自动更新生效,请从开始菜单或桌面快捷方式启动。如果没有快捷方式,请重新运行安装包。',
detailPrefix: '安装目录:',
buttons: ['打开安装目录', '知道了'],
},
en: {
title: 'Please launch from the Start menu',
message:
'We detected the app is running from a non-install location. To ensure auto-updates work, launch it from the Start menu or desktop shortcut. If no shortcut exists, run the installer again.',
detailPrefix: 'Install location: ',
buttons: ['Open install folder', 'OK'],
},
ru: {
title: 'Запускайте приложение из меню «Пуск»',
message:
'Обнаружен запуск из неустановленного пути. Чтобы автообновления работали, запускайте приложение из меню «Пуск» или ярлыка. Если ярлыка нет, запустите установщик ещё раз.',
detailPrefix: 'Папка установки: ',
buttons: ['Открыть папку', 'Понятно'],
},
};
export function resolveInstallNoticeLanguage({
configLanguage,
locale,
}: {
configLanguage?: string | null;
locale?: string | null;
}): InstallNoticeLanguage {
const rawLanguage = configLanguage || locale;
if (!rawLanguage) {
return 'en';
}
const normalized = rawLanguage.toLowerCase();
if (normalized.startsWith('zh')) {
return 'zh-CN';
}
if (normalized.startsWith('ru')) {
return 'ru';
}
return 'en';
}
export function getInstallNoticeText(language: InstallNoticeLanguage) {
return installNoticeText[language] || installNoticeText.en;
}
function getPathApi(platform: string) {
if (platform === 'win32') {
return path.win32;
}
return path;
}
export function getExpectedInstallRoot({
platform,
localAppData,
appName,
}: {
platform: string;
localAppData?: string | null;
appName: string;
}) {
if (platform !== 'win32') {
return null;
}
if (!localAppData) {
return null;
}
const pathApi = getPathApi(platform);
return pathApi.resolve(pathApi.join(localAppData, appName));
}
export function isRunningFromExpectedInstallDir({
platform,
isPackaged,
localAppData,
appName,
execPath,
}: {
platform: string;
isPackaged: boolean;
localAppData?: string | null;
appName: string;
execPath: string;
}) {
if (platform !== 'win32' || !isPackaged) {
return true;
}
const expectedRoot = getExpectedInstallRoot({ platform, localAppData, appName });
if (!expectedRoot) {
return true;
}
const pathApi = getPathApi(platform);
const normalizedExecPath = pathApi.resolve(execPath);
return normalizedExecPath.toLowerCase().startsWith(expectedRoot.toLowerCase() + pathApi.sep);
}