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:
7 files changed
+2280
-11
No files matched your search
+71
-1
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user