## 算分正确性(6 份只读审查 → 收敛成 5 个病根,均先复现失败再修) - 「人工名次」与「自动名次回写」互斥:原来同一份成绩**发布两次分数不同** (团体报名项 dual 通道 10/8/6 → 25/18/14),现在连发多次逐字节一致 - 删小项时在同一事务作废旧流水并推榜:原来分数**永久残留**在总榜、任何重算都打不到 - 规则包导入整体事务化:原来中途失败会清空名次表且补不回(奖牌全丢); 另加守卫「compare=none 的小项不得配 basis=result/aggregate」 - 末位归零只抹真正落在末位的那一档:原来跨末位的并列组会被整组抹 0 (7 人三人并列第 5 时第 5、6 名的分也一起没了) - 模板「分值表档位超出录取名次」现在拒绝保存:原来静默截断成 0 分 ## 现场反馈(主人实测报的) - 已发布成绩可改:行内「修改本条」+ 成绩格/状态格可点(退回草稿→改→重新发布本组), 与「作废」语义分开;取消路径不产生任何副作用 - 单位积分明细补「运动员 / 来源」列,并显示「已修订 ×N(时间·原因)」: 管理端、大屏覆盖层、xlsx 备注列三处同源 - 单位奖牌榜改为按奖牌独立排名(金→银→铜→单位编码),与积分榜解耦 - 历史孤儿积分流水清理(迁移 0022,只动 result/record_bonus,人工调整分不受影响) ## 新功能 - 团体项目聚合口径二选一:平均(默认)/ 总和(迁移 0023,通道编辑器可切) ## 安装器 - 自绘五页安装界面:无边框、自绘标题栏、DPI 自适应(4K@200% 实测)、 安装/卸载逻辑复用 electron-builder 宏不变;默认「为所有用户安装」并自动放行防火墙 19540 - 构建期校验:安装器素材里的版本号必须与 VERSION 一致,否则构建失败 自检:服务端 14 包 ×2 / 前端 vue-tsc + build + 742 例 ×2 / Console 20 文件 + 68 例 ×2,全绿。 端到端对拍:修前 vs 修后,纯团体 / 双计 / 普通个人三形态**逐字节一致**,仅出 bug 的双通道形态按要求改变。
1376 lines
53 KiB
NSIS
1376 lines
53 KiB
NSIS
; =============================================================================
|
||
; 赛事积分榜 安装器 —— 完全自绘的五页向导
|
||
;
|
||
; 这是 electron-builder 的「自定义 installer 脚本」(package.json 的 build.nsis.script)。
|
||
; 只换 UI 层:安装/卸载/升级/快捷方式/注册表/防火墙全部复用 electron-builder 模板里的宏,
|
||
; 一个字节的安装逻辑都不重写 —— 现场装机成功率不被这次改版动摇。
|
||
;
|
||
; 为什么整窗去边框、铺满位图:
|
||
; 设计要求是 760×480 的固定版面(左侧品牌栏 / 顶部标题栏 / 右侧内容区 / 底部按钮条)。
|
||
; 系统标题栏和原生按钮条在不同 Windows 版本上高度都不一样,要对齐只能自己画。
|
||
; 所以:去掉窗口边框 → 内层对话框铺满整个客户区 → 位图当画布 → 标准按钮条被盖住,
|
||
; 但仍然是「程序化翻页」的工具(点我们的按钮 = 点被盖住的系统按钮)。
|
||
;
|
||
; 页面流程:P1 欢迎 → P2 许可协议 → P3 安装位置与安装模式 → P4 正在安装 → P5 完成。
|
||
; P1/P2/P3/P5 是 Page custom(nsDialogs 自建内层对话框,POC 已验证);
|
||
; P4 必须是**真正的** instfiles 页,原因有两个,都不是偏好问题:
|
||
; 1. 安装段(Section)只在 instfiles 页上执行。不用 instfiles 页就得把整段安装逻辑塞进
|
||
; 一个函数,静默安装、取消中断、进度刷新全得自己重写 —— 那才是真正的风险源;
|
||
; 2. WriteUninstaller / 卸载段的执行时机也依赖这条路径。
|
||
; 代价是 instfiles 页自带的内层对话框(原生进度条 + 详情列表)只能藏起来,
|
||
; 在上面再盖一层我们自己的对话框。
|
||
;
|
||
; 高 DPI(现场是 4K@200%):
|
||
; ManifestDPIAware true + 运行时读 LOGPIXELSX,所有坐标 = 1x 设计值 × dpi / 96,
|
||
; 一个绝对像素值都不许写死在控件上;素材出 1x/2x 两套,dpi ≥ 144 用 2x,
|
||
; 并且给 LoadImage 传目标 cx/cy,让系统在 125%/150% 这类中间档位上兜底缩放。
|
||
; 字号不加倍:NSIS 的 CreateFont 自己会按系统 DPI 把点数换成像素,再乘一次就双倍了。
|
||
; =============================================================================
|
||
|
||
; ── 素材位置 ────────────────────────────────────────────────────────────────
|
||
; 走 ${PROJECT_DIR}(electron-builder 注入的绝对路径)而不是相对路径:
|
||
; electron-builder 是把脚本喂给 makensis 的 stdin、并把 makensis 的工作目录设成它的模板目录,
|
||
; 相对路径落点不确定。
|
||
!define ART_SRC "${PROJECT_DIR}\installer\art"
|
||
|
||
; ── 版面常量 ────────────────────────────────────────────────────────────────
|
||
; 全部是 art-gen/gen_art.py 里的 1x 设计值,改素材就要回来改这里(两边必须一致)。
|
||
!define WIN_W 760
|
||
!define WIN_H 480
|
||
!define TB_H 40
|
||
!define SIDE_W 240
|
||
!define BRAND_H 440
|
||
!define PG_X 240
|
||
!define PG_Y 40
|
||
!define PG_W 520
|
||
!define PG_H 440
|
||
!define PAD_L 32
|
||
!define CONTENT_W 456
|
||
!define BTN_W 108
|
||
!define BTN_H 36
|
||
!define BTN_Y 416
|
||
!define BTN_PRIMARY_X 620
|
||
!define BTN_SECONDARY_X 500
|
||
!define BROWSE_W 96
|
||
!define BROWSE_H 34
|
||
!define TB_MIN_X 672
|
||
!define TB_CLOSE_X 716
|
||
!define TB_BTN_W 44
|
||
; 页面内控件 → 窗口坐标 = 页内坐标 + (PG_X, PG_Y),这里直接写成窗口坐标
|
||
!define LIC_EDIT_X 273
|
||
!define LIC_EDIT_Y 237
|
||
!define LIC_EDIT_H 174
|
||
!define LIC_EDIT_W 454
|
||
!define LIC_CHK_X 272
|
||
!define LIC_CHK_Y 432
|
||
!define PATH_EDIT_X 273
|
||
!define PATH_EDIT_Y 151
|
||
!define PATH_EDIT_W 357
|
||
!define PATH_EDIT_H 32
|
||
!define BROWSE_X 632
|
||
!define BROWSE_Y 150
|
||
!define RADIO1_X 272
|
||
!define RADIO1_Y 260
|
||
!define RADIO2_X 272
|
||
!define RADIO2_Y 300
|
||
!define SIZE_TXT_Y 336
|
||
!define STAGE_X 272
|
||
!define STAGE_Y 156
|
||
!define STAGE_W 456
|
||
!define STAGE_H 18
|
||
!define PROG_X 272
|
||
!define PROG_Y 180
|
||
!define PROG_W 456
|
||
!define PROG_H 8
|
||
!define FIN_CHK_X 272
|
||
!define FIN_CHK_Y 338
|
||
!define FIN_PATH_Y 376
|
||
|
||
; 颜色取自 gen_art.py 的配色常量(SetCtlColors 收 RRGGBB,不是 BGR)
|
||
!define CLR_TXT "0x16233a"
|
||
!define CLR_DIM "0x5a6b85"
|
||
!define CLR_BG "0xf6f9fd"
|
||
|
||
; ── 变量 ───────────────────────────────────────────────────────────────────
|
||
; electron-builder 的模板(assistedInstaller / installSection)引用了这些变量,
|
||
; 平时由 installer.nsi 主模板声明;我们用自定义脚本,得自己声明,名字必须一致。
|
||
Var newStartMenuLink
|
||
Var oldStartMenuLink
|
||
Var newDesktopLink
|
||
Var oldDesktopLink
|
||
Var oldShortcutName
|
||
Var oldMenuDirectory
|
||
Var appExe
|
||
Var launchLink
|
||
|
||
; 自绘界面状态
|
||
Var hPage ; 当前页的内层对话框
|
||
Var hBg
|
||
Var hBrand
|
||
Var hTitleBar
|
||
Var hMin
|
||
Var hClose
|
||
Var hNext
|
||
Var hBack
|
||
Var hCancel
|
||
Var hBrowse
|
||
Var hFinish
|
||
Var hCheck
|
||
Var hRadio1
|
||
Var hRadio2
|
||
Var hPath
|
||
Var hLicEdit
|
||
Var hStage
|
||
Var hProg
|
||
Var stMin
|
||
Var stClose
|
||
Var stNext
|
||
Var stBack
|
||
Var stCancel
|
||
Var stBrowse
|
||
Var stFinish
|
||
Var uiPage ; 0=无页面,1..5=当前页;定时器靠它判断还能碰哪些控件
|
||
Var timerId
|
||
Var gDpi ; 系统 DPI(LOGPIXELSX)
|
||
Var gArt ; 本次要用的素材目录($PLUGINSDIR\1x 或 \2x)
|
||
Var gCx
|
||
Var gCy
|
||
Var gBw ; Track 宏把基准宽高放这里,转交给 SetBmp
|
||
Var gBh
|
||
Var tmpName
|
||
Var tmpPath
|
||
Var tmpTxt
|
||
Var /GLOBAL IsPowerShellAvailable
|
||
Var artReady
|
||
Var licOk ; 许可页勾选
|
||
Var runApp ; 完成页「立即运行」
|
||
Var modeAll ; 安装模式单选:1=为所有用户,0=仅为我
|
||
Var instStarted ; 安装是否已经开工(Tick 里触发,只触发一次)
|
||
Var keepShortcuts ; 快捷方式保留策略,由安装流程写定(上游在安装段里声明,我们提前到全局)
|
||
Var dragging
|
||
|
||
|
||
; ── include ────────────────────────────────────────────────────────────────
|
||
; MUI2 不是为了它的页面,是为了 LangString / MUI_LANGUAGE:
|
||
; electron-builder 注入的 messages.nsh(以及 assistedMessages.nsh)用的是 MUI 的 LangString,
|
||
; 没有 MUI2 直接编译不过。我们的页面全是 Page custom,一个 MUI_PAGE_* 都不插。
|
||
!define MUI_DISABLE_INSERT_LANGUAGE_AFTER_PAGES_WARNING
|
||
!include "MUI2.nsh"
|
||
!include "nsDialogs.nsh"
|
||
!include "WinMessages.nsh"
|
||
!include "LogicLib.nsh"
|
||
!include "FileFunc.nsh"
|
||
!include "StrContains.nsh"
|
||
!include "common.nsh"
|
||
!include "multiUser.nsh"
|
||
!include "allowOnlyOneInstallerInstance.nsh"
|
||
!include "installUtil.nsh"
|
||
!include "installer.nsh"
|
||
|
||
; 安装模式初值逻辑(照抄 templates/nsis/assistedInstaller.nsh 的 initMultiUser):
|
||
; 上次怎么装的还怎么装;命令行 --allusers / --currentuser 压过它;
|
||
; 都没有时看 INSTALL_MODE_PER_ALL_USERS_DEFAULT(= package.json 的 selectPerMachineByDefault)。
|
||
; 我们不 include assistedInstaller.nsh(它会插一堆 MUI 原生页面),所以这个宏得自己带一份,
|
||
; 否则 uninstaller.nsh 的 un.onInit 里那句 !insertmacro initMultiUser 直接找不到宏。
|
||
!macro initMultiUser
|
||
!ifdef INSTALL_MODE_PER_ALL_USERS
|
||
!insertmacro setInstallModePerAllUsers
|
||
!else
|
||
; 静默安装跳过「内层进程 / 非管理员」这道判断:它俩走 UAC 插件的 UAC::_,
|
||
; 而静默模式下没有页窗口,插件会在这一句上不返回(本机 wine 实测卡死 60 秒)。
|
||
; 静默安装要提权时由安装流程开头那段($installMode == "all" + ${Silent})自己处理。
|
||
${IfNot} ${Silent}
|
||
${If} ${UAC_IsInnerInstance}
|
||
${AndIfNot} ${UAC_IsAdmin}
|
||
SetErrorLevel 0x666666
|
||
Quit
|
||
${EndIf}
|
||
${EndIf}
|
||
|
||
!ifndef MULTIUSER_INIT_TEXT_ADMINREQUIRED
|
||
!define MULTIUSER_INIT_TEXT_ADMINREQUIRED "$(^Caption) requires administrator privileges."
|
||
!endif
|
||
!ifndef MULTIUSER_INIT_TEXT_POWERREQUIRED
|
||
!define MULTIUSER_INIT_TEXT_POWERREQUIRED "$(^Caption) requires at least Power User privileges."
|
||
!endif
|
||
!ifndef MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE
|
||
!define MULTIUSER_INIT_TEXT_ALLUSERSNOTPOSSIBLE "Your user account does not have sufficient privileges to install $(^Name) for all users of this computer."
|
||
!endif
|
||
|
||
StrCpy $hasPerMachineInstallation "0"
|
||
StrCpy $hasPerUserInstallation "0"
|
||
ReadRegStr $perMachineInstallationFolder HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation
|
||
${if} $perMachineInstallationFolder != ""
|
||
StrCpy $hasPerMachineInstallation "1"
|
||
${endif}
|
||
ReadRegStr $perUserInstallationFolder HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation
|
||
${if} $perUserInstallationFolder != ""
|
||
StrCpy $hasPerUserInstallation "1"
|
||
${endif}
|
||
|
||
${GetParameters} $R0
|
||
${GetOptions} $R0 "/allusers" $R1
|
||
${IfNot} ${Errors}
|
||
StrCpy $hasPerMachineInstallation "1"
|
||
StrCpy $hasPerUserInstallation "0"
|
||
${EndIf}
|
||
${GetOptions} $R0 "/currentuser" $R1
|
||
${IfNot} ${Errors}
|
||
StrCpy $hasPerMachineInstallation "0"
|
||
StrCpy $hasPerUserInstallation "1"
|
||
${EndIf}
|
||
|
||
${if} $hasPerUserInstallation == "1"
|
||
${andif} $hasPerMachineInstallation == "0"
|
||
!insertmacro setInstallModePerUser
|
||
${elseif} $hasPerUserInstallation == "0"
|
||
${andif} $hasPerMachineInstallation == "1"
|
||
!insertmacro setInstallModePerAllUsers
|
||
${else}
|
||
!ifdef INSTALL_MODE_PER_ALL_USERS
|
||
!insertmacro setInstallModePerAllUsers
|
||
!else
|
||
!ifdef INSTALL_MODE_PER_ALL_USERS_DEFAULT
|
||
!insertmacro setInstallModePerAllUsers
|
||
!else
|
||
!insertmacro setInstallModePerUser
|
||
!endif
|
||
!endif
|
||
${endif}
|
||
!endif
|
||
!macroend
|
||
|
||
; ── 编译期替 electron-builder 补的两处 ──────────────────────────────────────
|
||
; 1) 自定义脚本下 electron-builder 不做 BUILD_UNINSTALLER 那一趟,UNINSTALLER_OUT_FILE
|
||
; 被留成空值,而 include/installer.nsh 里有一条无条件的
|
||
; File "/oname=${UNINSTALL_FILENAME}" "${UNINSTALLER_OUT_FILE}"
|
||
; 空路径直接编译失败。真卸载器由安装段末尾的 WriteUninstaller 现写,
|
||
; 这里顶一个占位文件上去只为让编译过去(安装时会被立刻删掉再重写,见安装段)。
|
||
!ifdef UNINSTALLER_OUT_FILE
|
||
!define /redef UNINSTALLER_OUT_FILE "${PROJECT_DIR}\installer\uninstaller-stub.txt"
|
||
!else
|
||
!define UNINSTALLER_OUT_FILE "${PROJECT_DIR}\installer\uninstaller-stub.txt"
|
||
!endif
|
||
|
||
; 2) MUI_INSERT 是在第一个 MUI_LANGUAGE 处展开的,界面初始化钩子必须在那之前定义。
|
||
!define MUI_CUSTOMFUNCTION_GUIINIT "GuiInit"
|
||
|
||
; 「应用还在跑吗」的检查。不查的话覆盖安装时 CopyFiles 会一路失败,
|
||
; 用户最后只看到一个「无法关闭,重试/取消」的框,根本不知道要先关程序。
|
||
; 逻辑取自 allowOnlyOneInstallerInstance.nsh 的 CHECK_APP_RUNNING,但不能直接插那个宏:
|
||
; 1. 它的宏体带 Var /GLOBAL CmdPath / PowerShellPath,一个脚本只能插一次,
|
||
; 而那一次已经被 un.checkAppRunning 占掉(我们 include 了 uninstaller.nsh);
|
||
; 2. 卸载器侧的 Call 只能指向 un. 开头的函数,安装器又调不了 un. 函数,
|
||
; 所以两边各要一个入口,共用下面这一份宏体。
|
||
; 另外 $CmdPath / $PowerShellPath / $IsPowerShellAvailable 都由 un.checkAppRunning
|
||
; 那次展开声明(所以本文件里这一整段必须在 !include "uninstaller.nsh" 之后)。
|
||
!macro AppRunningCheck
|
||
StrCpy $CmdPath "$SYSDIR\cmd.exe"
|
||
StrCpy $PowerShellPath "$SYSDIR\WindowsPowerShell\v1.0\powershell.exe"
|
||
!insertmacro ProbePowerShell
|
||
!insertmacro _CHECK_APP_RUNNING
|
||
!macroend
|
||
|
||
; IS_POWERSHELL_AVAILABLE 的宏体自带 Var /GLOBAL IsPowerShellAvailable,
|
||
; 两边各插一次会报「already declared」,所以那句 Var 已经提到文件开头的变量区,
|
||
; 探测逻辑原样抄在这里(十几行,抄一份比去改上游模板安全)。
|
||
!macro ProbePowerShell
|
||
; 先用一个必然返回 7 的命令确认这是「真的」PowerShell。
|
||
; 上游模板直接问 Get-Command Get-CimInstance,但假壳(wine 自带的 powershell.exe 就是,
|
||
; 任何参数都 exit 0)会让这个探测和后面那条 Get-CimInstance 查询一律「成功」,
|
||
; 于是安装器误判「程序正在运行」,去杀一个不存在的进程、卡在重试框里。
|
||
; 这类假壳在现场也可能出现(安全软件挂的 shell 代理),所以宁可多花一次子进程。
|
||
nsExec::Exec `"$PowerShellPath" -C "exit 7"`
|
||
Pop $0
|
||
${if} $0 != 7
|
||
StrCpy $IsPowerShellAvailable 1
|
||
${else}
|
||
nsExec::Exec `"$PowerShellPath" -C "if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }"`
|
||
Pop $0
|
||
${if} $0 == 0
|
||
nsExec::Exec `"$PowerShellPath" -C "if ((Get-ExecutionPolicy -Scope Process) -eq 'Restricted') { exit 1 } else { exit 0 }"`
|
||
Pop $0
|
||
${endIf}
|
||
${if} $0 != 0
|
||
StrCpy $0 1
|
||
${endIf}
|
||
StrCpy $IsPowerShellAvailable $0
|
||
${endIf}
|
||
!macroend
|
||
|
||
; ── 卸载段 ─────────────────────────────────────────────────────────────────
|
||
; 卸载逻辑与 un.onInit 直接用 electron-builder 的模板(uninstaller.nsh),一个字节都不改:
|
||
; 升级、快捷方式回收、防火墙规则回收的行为与上游完全一致。
|
||
; 卸载器的界面仍是原生外观 —— 素材只有安装器那一套、5 张页面位图全带「安装」字样,
|
||
; 硬套上去只会更难看。
|
||
;
|
||
; ⚠️ 为什么这里要自己给 BUILD_UNINSTALLER:
|
||
; electron-builder 平时编两趟(安装器一趟、卸载器一趟),靠 BUILD_UNINSTALLER 这个
|
||
; define 区分两边的代码路径(典型的是 getProcessInfo.nsh:安装器定义 _GetProcessInfo、
|
||
; 卸载器定义 un._GetProcessInfo)。自定义脚本只有一趟编译,安装器和卸载器的代码同处
|
||
; 一个文件里,所以只能给「卸载器这一段」自己划一个作用域:
|
||
; · 定义 BUILD_UNINSTALLER → 让 un. 侧拿到 Call un._GetProcessInfo(否则卸载器里的
|
||
; Call 不许指向非 un. 函数,直接编译不过);
|
||
; · include 完之后 !undef,安装器侧的代码照旧走 _GetProcessInfo。
|
||
; 顺带这也让 un.onInit 里的 initMultiUser 走到「卸载器需要提权」的那条分支,
|
||
; 与上游卸载器编译结果一致。
|
||
|
||
!macro customCheckAppRunning
|
||
Call un.CheckAppRunningImpl
|
||
!macroend
|
||
|
||
!define BUILD_UNINSTALLER
|
||
!include "uninstaller.nsh"
|
||
|
||
; getProcessInfo.nsh 的 un._GetProcessInfo 只在 BUILD_UNINSTALLER 那一趟里定义;
|
||
; 我们的单趟编译里补一个,宏体用的还是上游那份 FUNC_GETPROCESSINFO。
|
||
Function un._GetProcessInfo
|
||
!insertmacro FUNC_GETPROCESSINFO
|
||
FunctionEnd
|
||
|
||
Function un.CheckAppRunningImpl
|
||
!insertmacro AppRunningCheck
|
||
FunctionEnd
|
||
!undef BUILD_UNINSTALLER
|
||
|
||
!insertmacro addLangs
|
||
|
||
|
||
; ── 通用函数 ───────────────────────────────────────────────────────────────
|
||
|
||
Function CheckAppRunningImpl
|
||
!insertmacro AppRunningCheck
|
||
FunctionEnd
|
||
|
||
; 按 DPI 缩放基准值。栈:基准像素值 → 返回缩放后的像素值。
|
||
; 只用 $R8/$R9:所有调用方都用 $R0..$R7 放自己的临时值,S 不能踩它们。
|
||
Function S
|
||
Exch $R9
|
||
Push $R8
|
||
IntOp $R8 $R9 * $gDpi
|
||
IntOp $R8 $R8 / 96
|
||
StrCpy $R9 $R8
|
||
Pop $R8
|
||
Exch $R9
|
||
FunctionEnd
|
||
|
||
; 贴位图。栈:句柄 文件名 消息 基准宽 基准高。
|
||
; 文件名只有名字(如 base.bmp),目录由 $gArt 决定;cx/cy 用控件的目标像素尺寸,
|
||
; 于是 2x 素材在 150% 这类非整数倍下也能由系统缩到刚好。
|
||
Function SetBmp
|
||
Pop $R4 ; 文件名
|
||
Pop $R3 ; 句柄
|
||
Pop $R2 ; 消息
|
||
Pop $R1 ; 基准宽
|
||
Pop $R0 ; 基准高
|
||
StrCpy $tmpPath "$gArt\$R4"
|
||
Push $R1
|
||
Call S
|
||
Pop $R5 ; 目标宽
|
||
Push $R0
|
||
Call S
|
||
Pop $R6 ; 目标高
|
||
System::Call 'user32::LoadImageW(p 0, w "$tmpPath", i 0, i $R5, i $R6, i 0x0010) p .R0'
|
||
${If} $R0 == 0
|
||
Return ; 素材没加载出来就保持原图,宁可画面旧一点也不画成空白
|
||
${EndIf}
|
||
System::Call 'user32::SendMessageW(p $R3, i $R2, p 0, p $R0)'
|
||
; 立即重画:安装段执行期间消息泵不一定马上转到,进度不能等到空闲才更新
|
||
System::Call 'user32::InvalidateRect(p $R3, p 0, i 1)'
|
||
System::Call 'user32::UpdateWindow(p $R3)'
|
||
FunctionEnd
|
||
|
||
; 建控件。栈顺序见宏;类名/样式是编译期文本,坐标是 1x 设计值。
|
||
!macro NewCtl CLASS STYLE BX BY BW BH OUT
|
||
Push ${BX}
|
||
Call S
|
||
Pop $R0
|
||
Push ${BY}
|
||
Call S
|
||
Pop $R1
|
||
Push ${BW}
|
||
Call S
|
||
Pop $R2
|
||
Push ${BH}
|
||
Call S
|
||
Pop $R3
|
||
nsDialogs::CreateControl "${CLASS}" "${STYLE}" 0 $R0 $R1 $R2 $R3 ""
|
||
Pop ${OUT}
|
||
!macroend
|
||
|
||
; 贴位图(参数按 1x 设计值写,内部转成像素)
|
||
!macro SetBmpBase HWND ONAME W H
|
||
Push ${H}
|
||
Push ${W}
|
||
Push ${BM_SETIMAGE}
|
||
Push ${HWND}
|
||
Push ${ONAME}
|
||
Call SetBmp
|
||
!macroend
|
||
|
||
!macro SetBmpStatic HWND ONAME W H
|
||
Push ${H}
|
||
Push ${W}
|
||
Push ${STM_SETIMAGE}
|
||
Push ${HWND}
|
||
Push ${ONAME}
|
||
Call SetBmp
|
||
!macroend
|
||
|
||
; 位图按钮:创建 + 贴常态图
|
||
!macro NewBmpBtn BX BY BW BH OUT ONAME
|
||
!insertmacro NewCtl "Button" "${BS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${BX} ${BY} ${BW} ${BH} ${OUT}
|
||
!insertmacro SetBmpBase ${OUT} ${ONAME} ${BW} ${BH}
|
||
!macroend
|
||
|
||
; 悬停跟踪:栈里带齐「基准矩形 + 句柄 + 当前状态 + 位图基名」。
|
||
; 状态机 0=常态 1=悬停 2=按下,和 art/ 的三态位图一一对应。
|
||
!macro Track HWND STATE ONAME BX BY BW BH
|
||
StrCpy $gBw ${BW}
|
||
StrCpy $gBh ${BH}
|
||
Push ${BX}
|
||
Push ${BY}
|
||
Push ${BW}
|
||
Push ${BH}
|
||
Push ${HWND}
|
||
Push ${STATE}
|
||
Push "${ONAME}"
|
||
Call HoverBtn
|
||
Pop ${STATE}
|
||
!macroend
|
||
|
||
; 光标位置换算 + 命中测试 → 需要时换位图。栈:基准x 基准y 基准宽 基准高 句柄 状态 位图基名(base)。
|
||
; 用 $R0..$R6 干活、$R7 存结果 —— SetBmp 只踩 $R0..$R6,所以 $R7 能活到返回。
|
||
Function HoverBtn
|
||
Pop $R0 ; 位图基名
|
||
Pop $R1 ; 当前状态
|
||
Pop $R2 ; 句柄
|
||
Pop $R3 ; 基准高
|
||
Pop $R4 ; 基准宽
|
||
Pop $R5 ; 基准 y
|
||
Pop $R6 ; 基准 x
|
||
Push $R6
|
||
Call S
|
||
Pop $R6 ; x
|
||
Push $R5
|
||
Call S
|
||
Pop $R5 ; y
|
||
Push $R4
|
||
Call S
|
||
Pop $R4 ; w
|
||
Push $R3
|
||
Call S
|
||
Pop $R3 ; h
|
||
; 右/下边界(命中测试用半开区间)
|
||
IntOp $R4 $R4 + $R6
|
||
IntOp $R3 $R3 + $R5
|
||
StrCpy $R7 0
|
||
${If} $gCx >= $R6
|
||
${AndIf} $gCx < $R4
|
||
${AndIf} $gCy >= $R5
|
||
${AndIf} $gCy < $R3
|
||
StrCpy $R7 1
|
||
System::Call 'user32::GetAsyncKeyState(i 1) i .R8'
|
||
IntOp $R8 $R8 & 0x8000
|
||
${If} $R8 != 0
|
||
StrCpy $R7 2
|
||
${EndIf}
|
||
${EndIf}
|
||
${If} $R7 != $R1
|
||
${If} $R7 == 0
|
||
StrCpy $tmpName "$R0-normal.bmp"
|
||
${ElseIf} $R7 == 1
|
||
StrCpy $tmpName "$R0-hover.bmp"
|
||
${Else}
|
||
StrCpy $tmpName "$R0-pressed.bmp"
|
||
${EndIf}
|
||
Push $gBh
|
||
Push $gBw
|
||
Push ${BM_SETIMAGE}
|
||
Push $R2
|
||
Push $tmpName
|
||
Call SetBmp
|
||
${EndIf}
|
||
Push $R7
|
||
FunctionEnd
|
||
|
||
; 定时器回调:光标 → 客户区坐标 → 逐控件做悬停/按下 → 标题栏拖动。
|
||
; 不做窗口子类化(POC 结论):60ms 轮询足够跟手,也没有子类化回调崩掉整个安装器的风险。
|
||
Function Tick
|
||
System::Call '*(i, i) p .R0'
|
||
System::Call 'user32::GetCursorPos(p R0)'
|
||
System::Call 'user32::ScreenToClient(p $HWNDPARENT, p R0)'
|
||
System::Call '*$R0(i .R1, i .R2)'
|
||
System::Free $R0
|
||
StrCpy $gCx $R1
|
||
StrCpy $gCy $R2
|
||
|
||
${If} $uiPage == 0
|
||
Return
|
||
${EndIf}
|
||
|
||
; 标题栏两个按钮每页都在
|
||
!insertmacro Track $hMin $stMin "tb-min" ${TB_MIN_X} 0 ${TB_BTN_W} ${TB_H}
|
||
!insertmacro Track $hClose $stClose "tb-close" ${TB_CLOSE_X} 0 ${TB_BTN_W} ${TB_H}
|
||
|
||
${If} $uiPage == 1
|
||
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
${ElseIf} $uiPage == 2
|
||
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
${ElseIf} $uiPage == 3
|
||
!insertmacro Track $hNext $stNext "btn-next" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hBack $stBack "btn-back" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
!insertmacro Track $hBrowse $stBrowse "btn-browse" ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H}
|
||
${ElseIf} $uiPage == 4
|
||
!insertmacro Track $hCancel $stCancel "btn-cancel" ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
; 安装在这里开工:等第一个 tick(窗口已经画完)再动手,然后完工后翻到 P5。
|
||
; 为什么不用 NSIS 自带的 instfiles 页:那一页在跑安装时只泵它自己的消息,
|
||
; 我们挂在 StdUtils 定时器上的悬停/进度刷新全都收不到回调(实测:P4 上按钮不悬停、
|
||
; 定时器不触发)。改成自绘页 + 定时器驱动,消息泵一直是通的。
|
||
${If} $instStarted == 0
|
||
StrCpy $instStarted 1
|
||
Call DoInstall
|
||
GetDlgItem $0 $HWNDPARENT 1
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
${EndIf}
|
||
${ElseIf} $uiPage == 5
|
||
!insertmacro Track $hFinish $stFinish "btn-finish" ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H}
|
||
${EndIf}
|
||
|
||
; 标题栏拖动。用系统拖动循环(HTCAPTION)而不是自己算位移:跟手、贴边、最大化吸附都免费。
|
||
; 判定用「屏幕级的左键状态」,所以必须把标题栏按钮那块从拖动区里扣掉,
|
||
; 否则点最小化/关闭会连带开始拖窗(POC 踩过)。
|
||
${If} $gCy >= 0
|
||
${AndIf} $gCy < ${TB_H}
|
||
${AndIf} $gCx >= 0
|
||
${AndIf} $gCx < ${TB_MIN_X}
|
||
System::Call 'user32::GetAsyncKeyState(i 1) i .R3'
|
||
IntOp $R3 $R3 & 0x8000
|
||
${If} $R3 != 0
|
||
${If} $dragging != 1
|
||
StrCpy $dragging 1
|
||
System::Call '*(i, i) p .R4'
|
||
System::Call 'user32::GetCursorPos(p R4)'
|
||
System::Call '*$R4(i .R5, i .R6)'
|
||
System::Free $R4
|
||
IntOp $R5 $R5 & 0xFFFF
|
||
IntOp $R6 $R6 & 0xFFFF
|
||
IntOp $R6 $R6 << 16
|
||
IntOp $R5 $R5 | $R6
|
||
System::Call 'user32::ReleaseCapture()'
|
||
System::Call 'user32::SendMessageW(p $HWNDPARENT, i ${WM_NCLBUTTONDOWN}, p 2, p $R5)'
|
||
${EndIf}
|
||
${Else}
|
||
StrCpy $dragging 0
|
||
${EndIf}
|
||
${Else}
|
||
StrCpy $dragging 0
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; ── 素材嵌入 ───────────────────────────────────────────────────────────────
|
||
; 逐个文件写 File 而不是 File /r:素材缺一张就在编译期报错,
|
||
; 而不是运行期画出一块空白(装机现场没有回头补素材的机会)。
|
||
!macro ArtFiles SUB
|
||
SetOutPath "$PLUGINSDIR\${SUB}"
|
||
File /oname=base.bmp "${ART_SRC}\${SUB}\base.bmp"
|
||
File /oname=brand.bmp "${ART_SRC}\${SUB}\brand.bmp"
|
||
File /oname=titlebar.bmp "${ART_SRC}\${SUB}\titlebar.bmp"
|
||
File /oname=page-welcome.bmp "${ART_SRC}\${SUB}\page-welcome.bmp"
|
||
File /oname=page-license.bmp "${ART_SRC}\${SUB}\page-license.bmp"
|
||
File /oname=page-dir.bmp "${ART_SRC}\${SUB}\page-dir.bmp"
|
||
File /oname=page-progress.bmp "${ART_SRC}\${SUB}\page-progress.bmp"
|
||
File /oname=page-finish.bmp "${ART_SRC}\${SUB}\page-finish.bmp"
|
||
File /oname=btn-next-normal.bmp "${ART_SRC}\${SUB}\btn-next-normal.bmp"
|
||
File /oname=btn-next-hover.bmp "${ART_SRC}\${SUB}\btn-next-hover.bmp"
|
||
File /oname=btn-next-pressed.bmp "${ART_SRC}\${SUB}\btn-next-pressed.bmp"
|
||
File /oname=btn-install-normal.bmp "${ART_SRC}\${SUB}\btn-install-normal.bmp"
|
||
File /oname=btn-install-hover.bmp "${ART_SRC}\${SUB}\btn-install-hover.bmp"
|
||
File /oname=btn-install-pressed.bmp "${ART_SRC}\${SUB}\btn-install-pressed.bmp"
|
||
File /oname=btn-finish-normal.bmp "${ART_SRC}\${SUB}\btn-finish-normal.bmp"
|
||
File /oname=btn-finish-hover.bmp "${ART_SRC}\${SUB}\btn-finish-hover.bmp"
|
||
File /oname=btn-finish-pressed.bmp "${ART_SRC}\${SUB}\btn-finish-pressed.bmp"
|
||
File /oname=btn-cancel-normal.bmp "${ART_SRC}\${SUB}\btn-cancel-normal.bmp"
|
||
File /oname=btn-cancel-hover.bmp "${ART_SRC}\${SUB}\btn-cancel-hover.bmp"
|
||
File /oname=btn-cancel-pressed.bmp "${ART_SRC}\${SUB}\btn-cancel-pressed.bmp"
|
||
File /oname=btn-back-normal.bmp "${ART_SRC}\${SUB}\btn-back-normal.bmp"
|
||
File /oname=btn-back-hover.bmp "${ART_SRC}\${SUB}\btn-back-hover.bmp"
|
||
File /oname=btn-back-pressed.bmp "${ART_SRC}\${SUB}\btn-back-pressed.bmp"
|
||
File /oname=btn-browse-normal.bmp "${ART_SRC}\${SUB}\btn-browse-normal.bmp"
|
||
File /oname=btn-browse-hover.bmp "${ART_SRC}\${SUB}\btn-browse-hover.bmp"
|
||
File /oname=btn-browse-pressed.bmp "${ART_SRC}\${SUB}\btn-browse-pressed.bmp"
|
||
File /oname=tb-min-normal.bmp "${ART_SRC}\${SUB}\tb-min-normal.bmp"
|
||
File /oname=tb-min-hover.bmp "${ART_SRC}\${SUB}\tb-min-hover.bmp"
|
||
File /oname=tb-min-pressed.bmp "${ART_SRC}\${SUB}\tb-min-pressed.bmp"
|
||
File /oname=tb-close-normal.bmp "${ART_SRC}\${SUB}\tb-close-normal.bmp"
|
||
File /oname=tb-close-hover.bmp "${ART_SRC}\${SUB}\tb-close-hover.bmp"
|
||
File /oname=tb-close-pressed.bmp "${ART_SRC}\${SUB}\tb-close-pressed.bmp"
|
||
File /oname=check-off.bmp "${ART_SRC}\${SUB}\check-off.bmp"
|
||
File /oname=check-on.bmp "${ART_SRC}\${SUB}\check-on.bmp"
|
||
File /oname=radio-off.bmp "${ART_SRC}\${SUB}\radio-off.bmp"
|
||
File /oname=radio-on.bmp "${ART_SRC}\${SUB}\radio-on.bmp"
|
||
File /oname=progress-0.bmp "${ART_SRC}\${SUB}\progress-0.bmp"
|
||
File /oname=progress-1.bmp "${ART_SRC}\${SUB}\progress-1.bmp"
|
||
File /oname=progress-2.bmp "${ART_SRC}\${SUB}\progress-2.bmp"
|
||
File /oname=progress-3.bmp "${ART_SRC}\${SUB}\progress-3.bmp"
|
||
File /oname=progress-4.bmp "${ART_SRC}\${SUB}\progress-4.bmp"
|
||
File /oname=progress-5.bmp "${ART_SRC}\${SUB}\progress-5.bmp"
|
||
File /oname=stage-1.bmp "${ART_SRC}\${SUB}\stage-1.bmp"
|
||
File /oname=stage-2.bmp "${ART_SRC}\${SUB}\stage-2.bmp"
|
||
File /oname=stage-3.bmp "${ART_SRC}\${SUB}\stage-3.bmp"
|
||
File /oname=stage-4.bmp "${ART_SRC}\${SUB}\stage-4.bmp"
|
||
File /oname=stage-5.bmp "${ART_SRC}\${SUB}\stage-5.bmp"
|
||
!macroend
|
||
|
||
; 只解当前 DPI 需要的那一套:两套都解要写 26MB 临时文件,首次显示会明显卡一下。
|
||
Function EnsureArt
|
||
${If} $artReady == 1
|
||
Return
|
||
${EndIf}
|
||
StrCpy $artReady 1
|
||
InitPluginsDir
|
||
${If} $gDpi >= 144
|
||
StrCpy $gArt "$PLUGINSDIR\2x"
|
||
!insertmacro ArtFiles 2x
|
||
${Else}
|
||
StrCpy $gArt "$PLUGINSDIR\1x"
|
||
!insertmacro ArtFiles 1x
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; 系统 DPI。ManifestDPIAware 之后进程不会被人为放大,只能自己按这个数缩放版面。
|
||
Function PickDpi
|
||
System::Call 'user32::GetDC(p 0) p .R0'
|
||
System::Call 'gdi32::GetDeviceCaps(p R0, i 88) i .R1' ; LOGPIXELSX
|
||
System::Call 'user32::ReleaseDC(p 0, p R0)'
|
||
${If} $R1 < 96
|
||
StrCpy $R1 96
|
||
${EndIf}
|
||
StrCpy $gDpi $R1
|
||
FunctionEnd
|
||
|
||
; 内层对话框铺满整个客户区:MUI 外层框架(页眉 / 品牌字 / 原生按钮条)全被盖住,
|
||
; 自绘位图才有完整的 760×480 画布。
|
||
Function NewPage
|
||
nsDialogs::Create 1018
|
||
Pop $hPage
|
||
${If} $hPage == error
|
||
Abort
|
||
${EndIf}
|
||
; 悬停/拖动全靠这个 60ms 轮询(POC 结论:比窗口子类化稳,代价是光标移动要等一个 tick)。
|
||
; 建在这里而不是 .onGUIInit:StdUtils 的定时器要挂在页对话框上才跑得起来。
|
||
${If} $timerId == 0
|
||
${StdUtils.TimerCreate} $timerId Tick 60
|
||
${EndIf}
|
||
Push ${WIN_W}
|
||
Call S
|
||
Pop $R0
|
||
Push ${WIN_H}
|
||
Call S
|
||
Pop $R1
|
||
System::Call 'user32::SetWindowPos(p $hPage, p 0, i 0, i 0, i $R0, i $R1, i 0x0004)'
|
||
; 上一页的句柄全部作废:定时器靠这组变量判断「还能碰谁」
|
||
StrCpy $hBg 0
|
||
StrCpy $hBrand 0
|
||
StrCpy $hTitleBar 0
|
||
StrCpy $hMin 0
|
||
StrCpy $hClose 0
|
||
StrCpy $hNext 0
|
||
StrCpy $hBack 0
|
||
StrCpy $hCancel 0
|
||
StrCpy $hBrowse 0
|
||
StrCpy $hFinish 0
|
||
StrCpy $hCheck 0
|
||
StrCpy $hStage 0
|
||
StrCpy $hProg 0
|
||
StrCpy $stMin 0
|
||
StrCpy $stClose 0
|
||
StrCpy $stNext 0
|
||
StrCpy $stBack 0
|
||
StrCpy $stCancel 0
|
||
StrCpy $stBrowse 0
|
||
StrCpy $stFinish 0
|
||
FunctionEnd
|
||
|
||
; 全窗共用的底:背景 → 品牌栏 → 页面位图 → 标题栏 → 标题栏按钮。
|
||
; 栈:页面位图名(空字符串 = 该页不贴页面位图,比如安装中页自己控制进度)
|
||
Function Chrome
|
||
Pop $tmpName
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 0 ${WIN_W} ${WIN_H} $hBg
|
||
!insertmacro SetBmpStatic $hBg "base.bmp" ${WIN_W} ${WIN_H}
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 ${TB_H} ${SIDE_W} ${BRAND_H} $hBrand
|
||
!insertmacro SetBmpStatic $hBrand "brand.bmp" ${SIDE_W} ${BRAND_H}
|
||
${If} $tmpName != ""
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${PG_X} ${PG_Y} ${PG_W} ${PG_H} $hProg
|
||
!insertmacro SetBmpStatic $hProg "$tmpName" ${PG_W} ${PG_H}
|
||
${EndIf}
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" 0 0 ${WIN_W} ${TB_H} $hTitleBar
|
||
!insertmacro SetBmpStatic $hTitleBar "titlebar.bmp" ${WIN_W} ${TB_H}
|
||
!insertmacro NewBmpBtn ${TB_MIN_X} 0 ${TB_BTN_W} ${TB_H} $hMin "tb-min-normal.bmp"
|
||
!insertmacro NewBmpBtn ${TB_CLOSE_X} 0 ${TB_BTN_W} ${TB_H} $hClose "tb-close-normal.bmp"
|
||
${NSD_OnClick} $hMin OnMin
|
||
${NSD_OnClick} $hClose OnClose
|
||
FunctionEnd
|
||
|
||
; 自绘文字标签:对话框默认底色是灰的,不显式设色就会在页面上贴出一块灰,
|
||
; 所以一律用「底色 = 内容区底色」把标签融进位图。
|
||
; 栈(后压先弹):文本 字号 基准高 基准宽 基准y 基准x
|
||
!macro AddLabel BX BY BW BH SIZE TEXT
|
||
Push ${BX}
|
||
Push ${BY}
|
||
Push ${BW}
|
||
Push ${BH}
|
||
Push ${SIZE}
|
||
Push "${TEXT}"
|
||
Call Label
|
||
!macroend
|
||
|
||
Function Label
|
||
Pop $R0
|
||
Pop $R1
|
||
Pop $R2
|
||
Pop $R3
|
||
Pop $R4
|
||
Pop $R5
|
||
Push $R5
|
||
Call S
|
||
Pop $R5
|
||
Push $R4
|
||
Call S
|
||
Pop $R4
|
||
Push $R3
|
||
Call S
|
||
Pop $R3
|
||
Push $R2
|
||
Call S
|
||
Pop $R2
|
||
nsDialogs::CreateControl "Static" "${SS_LEFT}|${SS_CENTERIMAGE}|${WS_CHILD}|${WS_VISIBLE}" 0 $R5 $R4 $R3 $R2 ""
|
||
Pop $R6
|
||
SetCtlColors $R6 "${CLR_TXT}" "${CLR_BG}"
|
||
CreateFont $R7 "Microsoft YaHei UI" "$R1" "400"
|
||
SendMessage $R6 ${WM_SETFONT} $R7 1
|
||
${NSD_SetText} $R6 "$R0"
|
||
FunctionEnd
|
||
|
||
; 栈(后压先弹):样式 基准高 基准宽 基准y 基准x → 返回控件句柄
|
||
!macro NewEdit STYLE BX BY BW BH OUT
|
||
Push ${BH}
|
||
Push ${BW}
|
||
Push ${BY}
|
||
Push ${BX}
|
||
Push "${STYLE}"
|
||
Call MakeEdit
|
||
Pop ${OUT}
|
||
!macroend
|
||
|
||
Function MakeEdit
|
||
Pop $R5
|
||
Pop $R0
|
||
Pop $R1
|
||
Pop $R2
|
||
Pop $R3
|
||
Push $R0
|
||
Call S
|
||
Pop $R0
|
||
Push $R1
|
||
Call S
|
||
Pop $R1
|
||
Push $R2
|
||
Call S
|
||
Pop $R2
|
||
Push $R3
|
||
Call S
|
||
Pop $R3
|
||
nsDialogs::CreateControl "Edit" "$R5" 0 $R0 $R1 $R2 $R3 ""
|
||
Pop $R6
|
||
SetCtlColors $R6 "${CLR_TXT}" "0xffffff"
|
||
CreateFont $R7 "Microsoft YaHei UI" "12" "400"
|
||
SendMessage $R6 ${WM_SETFONT} $R7 1
|
||
Push $R6
|
||
FunctionEnd
|
||
|
||
; ── 按钮动作 ───────────────────────────────────────────────────────────────
|
||
; 标准按钮条被自绘层盖住了,但按钮还在:点它等于点系统按钮,翻页/取消都由 NSIS 自己走。
|
||
Function GoNext
|
||
GetDlgItem $0 $HWNDPARENT 1
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function GoBack
|
||
GetDlgItem $0 $HWNDPARENT 3
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function GoCancel
|
||
GetDlgItem $0 $HWNDPARENT 2
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function OnMin
|
||
; 最小化走系统命令:自绘窗口没有系统按钮,但 ShowWindow 一样有效
|
||
System::Call 'user32::ShowWindow(p $HWNDPARENT, i 6)' ; SW_MINIMIZE
|
||
FunctionEnd
|
||
|
||
Function OnClose
|
||
Call GoCancel
|
||
FunctionEnd
|
||
|
||
Function OnLicCheck
|
||
; 用 != 1 而不是 == 0:LogicLib 的 == 是字符串比较,$licOk 初值是空串,"" == 0 为假
|
||
${If} $licOk != 1
|
||
StrCpy $licOk 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${Else}
|
||
StrCpy $licOk 0
|
||
!insertmacro SetBmpBase $hCheck "check-off.bmp" 16 16
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
Function OnLicNext
|
||
${If} $licOk != 1
|
||
; 没同意就不让走:这是许可页唯一的硬性要求
|
||
MessageBox MB_OK|MB_ICONINFORMATION|MB_TOPMOST "请先勾选「我已阅读并同意」再继续。"
|
||
Return
|
||
${EndIf}
|
||
Call GoNext
|
||
FunctionEnd
|
||
|
||
Function OnModeUser
|
||
StrCpy $modeAll 0
|
||
!insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16
|
||
!insertmacro SetBmpBase $hRadio2 "radio-off.bmp" 16 16
|
||
FunctionEnd
|
||
|
||
Function OnModeAll
|
||
StrCpy $modeAll 1
|
||
!insertmacro SetBmpBase $hRadio1 "radio-off.bmp" 16 16
|
||
!insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16
|
||
FunctionEnd
|
||
|
||
; 「浏览…」:系统文件夹选择框没有 NSIS 包装,用 System::Call 直接调 shell32。
|
||
; BROWSEINFO = { hwndOwner, pidlRoot, pszDisplayName, lpszTitle, ulFlags, lpfn, lParam, iImage }
|
||
; ulFlags = BIF_RETURNONLYFSDIRS(0x1) | BIF_NEWDIALOGSTYLE(0x40)
|
||
Function OnBrowse
|
||
System::Call 'ole32::CoInitialize(p 0)'
|
||
StrCpy $R6 $HWNDPARENT
|
||
System::Alloc 260
|
||
Pop $R7
|
||
System::Alloc 32
|
||
Pop $R0
|
||
System::Call '*$R0(p $R6, p 0, p $R7, p 0, i 0x41, p 0, p 0, i 0)'
|
||
System::Call 'shell32::SHBrowseForFolder(p R0) p .R1'
|
||
${If} $R1 != 0
|
||
System::Call 'shell32::SHGetPathFromIDList(p R1, t .R2)'
|
||
System::Call 'ole32::CoTaskMemFree(p R1)'
|
||
${If} $R2 != ""
|
||
StrCpy $tmpTxt $R2
|
||
${NSD_SetText} $hPath "$tmpTxt"
|
||
${EndIf}
|
||
${EndIf}
|
||
System::Free $R7
|
||
System::Free $R0
|
||
FunctionEnd
|
||
|
||
Function OnFinish
|
||
${If} $runApp == 1
|
||
Call StartAppNow
|
||
${EndIf}
|
||
; 结束安装器。这里踩过两个坑:页面回调里 Quit 不生效(窗口不退、反而被推去下一页),
|
||
; 对主窗口发 WM_CLOSE 也不生效;能真正退出的只有点系统那个「取消」按钮。
|
||
; 先把错误码归零,别让「正常装完」的退出码看起来像被取消。
|
||
SetErrorLevel 0
|
||
GetDlgItem $0 $HWNDPARENT 2
|
||
SendMessage $0 ${BM_CLICK} 0 0
|
||
FunctionEnd
|
||
|
||
Function OnFinCheck
|
||
${If} $runApp != 1
|
||
StrCpy $runApp 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${Else}
|
||
StrCpy $runApp 0
|
||
!insertmacro SetBmpBase $hCheck "check-off.bmp" 16 16
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; ── 进度分段 ───────────────────────────────────────────────────────────────
|
||
; app-64.7z 是一次 Nsis7z::Extract 调用,拿不到百分比;所以进度按「宏观步骤」分段
|
||
; (解压 → 注册表 → 快捷方式 → 防火墙 → 收尾),不画假进度条。
|
||
; NSIS 的 Call 不收参数,用法统一走这个宏
|
||
!macro StageCall N
|
||
Push ${N}
|
||
Call Stage
|
||
!macroend
|
||
|
||
; 栈:段号 1..5
|
||
Function Stage
|
||
Pop $R0
|
||
${If} ${Silent}
|
||
Return
|
||
${EndIf}
|
||
${If} $uiPage != 4
|
||
Return
|
||
${EndIf}
|
||
; 段号先搬进全局变量:SetBmp 会踩掉 $R0..$R7(它的局部值都放那儿)
|
||
StrCpy $tmpTxt $R0
|
||
StrCpy $tmpPath "stage-$tmpTxt.bmp"
|
||
Push ${STAGE_H}
|
||
Push ${STAGE_W}
|
||
Push ${STM_SETIMAGE}
|
||
Push $hStage
|
||
Push $tmpPath
|
||
Call SetBmp
|
||
StrCpy $tmpPath "progress-$tmpTxt.bmp"
|
||
Push ${PROG_H}
|
||
Push ${PROG_W}
|
||
Push ${STM_SETIMAGE}
|
||
Push $hProg
|
||
Push $tmpPath
|
||
Call SetBmp
|
||
FunctionEnd
|
||
|
||
; ── P1 欢迎 ────────────────────────────────────────────────────────────────
|
||
Function PageWelcome
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 1
|
||
Call NewPage
|
||
Push "page-welcome.bmp"
|
||
Call Chrome
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
|
||
${NSD_OnClick} $hCancel OnClose
|
||
${NSD_OnClick} $hNext GoNext
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
; ── P2 许可协议 ────────────────────────────────────────────────────────────
|
||
; 中文摘要烘焙在 page-license.bmp 里;英文原文(LICENSE)用真编辑框显示,
|
||
; 因为它比框大一截、必须能滚 —— 位图放不下整篇协议。
|
||
Function PageLicense
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 2
|
||
Call NewPage
|
||
Push "page-license.bmp"
|
||
Call Chrome
|
||
|
||
; 先按可写建,灌完文本再设只读:以只读样式创建的编辑框会被刷成灰底,
|
||
; 后期用 EM_SETREADONLY 设的只读框底色还是白的(SetCtlColors 再兜一层)。
|
||
!insertmacro NewEdit "${ES_MULTILINE}|${ES_AUTOVSCROLL}|${WS_VSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${LIC_EDIT_X} ${LIC_EDIT_Y} ${LIC_EDIT_W} ${LIC_EDIT_H} $hLicEdit
|
||
File /oname=$PLUGINSDIR\license.txt "${PROJECT_DIR}\..\LICENSE"
|
||
FileOpen $9 "$PLUGINSDIR\license.txt" r
|
||
lic_loop:
|
||
ClearErrors
|
||
FileRead $9 $8
|
||
IfErrors lic_done
|
||
SendMessage $hLicEdit ${EM_REPLACESEL} 1 "STR:$8$\r$\n"
|
||
Goto lic_loop
|
||
lic_done:
|
||
FileClose $9
|
||
; FileRead + EM_REPLACESEL 会把插入点一路推到末尾、视图跟着滚到底;
|
||
; 用户进这一页看到的应该是协议开头,所以显式回到顶部
|
||
SendMessage $hLicEdit ${EM_SETSEL} 0 0
|
||
SendMessage $hLicEdit ${WM_VSCROLL} 6 0 ; SB_TOP
|
||
SendMessage $hLicEdit ${EM_SETREADONLY} 1 0
|
||
|
||
!insertmacro NewBmpBtn 272 432 16 16 $hCheck "check-off.bmp"
|
||
${If} $licOk == 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${EndIf}
|
||
!insertmacro AddLabel 296 430 300 20 12 "我已阅读并同意上述条款"
|
||
${NSD_OnClick} $hCheck OnLicCheck
|
||
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
|
||
${NSD_OnClick} $hCancel OnClose
|
||
${NSD_OnClick} $hNext OnLicNext
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageLicenseLeave
|
||
StrCpy $uiPage 0
|
||
FunctionEnd
|
||
|
||
; ── P3 安装位置 + 安装模式 ─────────────────────────────────────────────────
|
||
Function PageDir
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 3
|
||
Call NewPage
|
||
Push "page-dir.bmp"
|
||
Call Chrome
|
||
|
||
!insertmacro NewEdit "${ES_AUTOHSCROLL}|${WS_CHILD}|${WS_VISIBLE}" ${PATH_EDIT_X} ${PATH_EDIT_Y} ${PATH_EDIT_W} ${PATH_EDIT_H} $hPath
|
||
${NSD_SetText} $hPath "$INSTDIR"
|
||
!insertmacro NewBmpBtn ${BROWSE_X} ${BROWSE_Y} ${BROWSE_W} ${BROWSE_H} $hBrowse "btn-browse-normal.bmp"
|
||
${NSD_OnClick} $hBrowse OnBrowse
|
||
|
||
!insertmacro NewBmpBtn ${RADIO1_X} ${RADIO1_Y} 16 16 $hRadio1 "radio-off.bmp"
|
||
!insertmacro NewBmpBtn ${RADIO2_X} ${RADIO2_Y} 16 16 $hRadio2 "radio-off.bmp"
|
||
${NSD_OnClick} $hRadio1 OnModeUser
|
||
${NSD_OnClick} $hRadio2 OnModeAll
|
||
; 初始勾选跟随 $installMode:initMultiUser 已经按「上次装法 / 命令行 / 默认」定好了模式
|
||
${If} $installMode == "all"
|
||
StrCpy $modeAll 1
|
||
!insertmacro SetBmpBase $hRadio2 "radio-on.bmp" 16 16
|
||
${Else}
|
||
StrCpy $modeAll 0
|
||
!insertmacro SetBmpBase $hRadio1 "radio-on.bmp" 16 16
|
||
${EndIf}
|
||
|
||
; 位图上的「需要空间 约 260 MB」是设计稿的占位数字,用真实体积盖掉
|
||
!ifdef ESTIMATED_SIZE
|
||
IntOp $R0 ${ESTIMATED_SIZE} / 1024
|
||
StrCpy $tmpTxt "需要空间 约 $R0 MB"
|
||
!insertmacro AddLabel ${PG_X} ${SIZE_TXT_Y} ${CONTENT_W} 20 11 "$tmpTxt"
|
||
!endif
|
||
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hBack "btn-back-normal.bmp"
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hNext "btn-next-normal.bmp"
|
||
${NSD_OnClick} $hBack GoBack
|
||
${NSD_OnClick} $hNext GoNext
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageDirLeave
|
||
StrCpy $uiPage 0
|
||
; 手改过就以输入框为准(浏览按钮也是写回这个框)
|
||
${NSD_GetText} $hPath $R1
|
||
${If} $R1 != ""
|
||
StrCpy $INSTDIR $R1
|
||
${EndIf}
|
||
StrCpy $R2 $INSTDIR 1 -1
|
||
${If} $R2 == "\"
|
||
StrCpy $INSTDIR $INSTDIR -1
|
||
${EndIf}
|
||
; 和 electron-builder 的目录页同一套规矩:路径必须以应用目录名结尾,
|
||
; 否则用户填 D:\Games 会把整个文件树倒进 D:\Games 而不是 D:\Games\Scoreboard_Console
|
||
${StrContains} $R3 "${APP_FILENAME}" $INSTDIR
|
||
${If} $R3 == ""
|
||
StrCpy $INSTDIR "$INSTDIR\${APP_FILENAME}"
|
||
${EndIf}
|
||
|
||
${If} $modeAll == 1
|
||
; 「为所有用户安装」:照抄 multiUserUi.nsh 的提权范式。
|
||
; 提权成功后是**另一个进程**在跑整个向导(它已经是管理员,会重新走到这一页并直接接着往下),
|
||
; 外层进程在 ${Case} 0 里 Quit,避免两个进程同时写同一份文件。
|
||
${IfNot} ${UAC_IsAdmin}
|
||
ShowWindow $HWNDPARENT ${SW_HIDE}
|
||
!insertmacro UAC_RunElevated
|
||
${Switch} $0
|
||
${Case} 0
|
||
${If} $1 = 1
|
||
Quit
|
||
${EndIf}
|
||
${If} $1 = 3
|
||
${OrIf} $2 = 0x666666
|
||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "$(loginWithAdminAccount)"
|
||
${EndIf}
|
||
${Break}
|
||
${Case} 1223 ;用户取消了 UAC
|
||
${Break}
|
||
${Case} 1062
|
||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "登录服务未运行,无法提权。"
|
||
${Break}
|
||
${Default}
|
||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "提权失败,错误码 $0"
|
||
${Break}
|
||
${EndSwitch}
|
||
ShowWindow $HWNDPARENT ${SW_SHOW}
|
||
BringToFront
|
||
Abort ; 留在本页:提权没成时让用户改选「仅为我安装」
|
||
${Else}
|
||
!insertmacro setInstallModePerAllUsers
|
||
${EndIf}
|
||
${Else}
|
||
!insertmacro setInstallModePerUser
|
||
${EndIf}
|
||
FunctionEnd
|
||
|
||
; ── P4 正在安装 ────────────────────────────────────────────────────────────
|
||
; 自绘页:分段进度由 DoInstall 里的 Stage 调用推动(7z 一次解压拿不到百分比)。
|
||
; 不用 NSIS 自带的 instfiles 页,原因见 Tick 里的注释。
|
||
Function PageInstall
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 4
|
||
Call NewPage
|
||
Push "page-progress.bmp"
|
||
Call Chrome
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${STAGE_X} ${STAGE_Y} ${STAGE_W} ${STAGE_H} $hStage
|
||
!insertmacro SetBmpStatic $hStage "stage-1.bmp" ${STAGE_W} ${STAGE_H}
|
||
!insertmacro NewCtl "Static" "${SS_BITMAP}|${WS_CHILD}|${WS_VISIBLE}" ${PROG_X} ${PROG_Y} ${PROG_W} ${PROG_H} $hProg
|
||
!insertmacro SetBmpStatic $hProg "progress-0.bmp" ${PROG_W} ${PROG_H}
|
||
!insertmacro NewBmpBtn ${BTN_SECONDARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hCancel "btn-cancel-normal.bmp"
|
||
${NSD_OnClick} $hCancel OnClose
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageInstallLeave
|
||
StrCpy $uiPage 0
|
||
FunctionEnd
|
||
|
||
; ── P5 完成 ────────────────────────────────────────────────────────────────
|
||
Function PageFinish
|
||
${If} ${Silent}
|
||
Abort
|
||
${EndIf}
|
||
StrCpy $uiPage 5
|
||
Call NewPage
|
||
Push "page-finish.bmp"
|
||
Call Chrome
|
||
|
||
!insertmacro NewBmpBtn ${FIN_CHK_X} ${FIN_CHK_Y} 16 16 $hCheck "check-off.bmp"
|
||
${If} $runApp == 1
|
||
!insertmacro SetBmpBase $hCheck "check-on.bmp" 16 16
|
||
${EndIf}
|
||
!insertmacro AddLabel 296 336 300 20 13 "立即运行 赛事积分榜"
|
||
${NSD_OnClick} $hCheck OnFinCheck
|
||
|
||
; 台上的实际安装目录(位图里写的是设计稿的占位路径)
|
||
StrCpy $tmpTxt "安装位置:$INSTDIR"
|
||
!insertmacro AddLabel ${PG_X} ${FIN_PATH_Y} ${CONTENT_W} 18 11 "$tmpTxt"
|
||
|
||
!insertmacro NewBmpBtn ${BTN_PRIMARY_X} ${BTN_Y} ${BTN_W} ${BTN_H} $hFinish "btn-finish-normal.bmp"
|
||
${NSD_OnClick} $hFinish OnFinish
|
||
nsDialogs::Show
|
||
FunctionEnd
|
||
|
||
Function PageFinishLeave
|
||
StrCpy $uiPage 0
|
||
FunctionEnd
|
||
|
||
; ── 界面初始化 ─────────────────────────────────────────────────────────────
|
||
; 去系统边框 + 固定 760×480(按 DPI 缩放)+ 居中。
|
||
; MUI 的外层框架本身用不上,但它是 $HWNDPARENT、是标准按钮条的宿主,
|
||
; 所以不能不要,只能去边框、盖住、把标准按钮当程序化翻页的工具。
|
||
Function GuiInit
|
||
Call PickDpi
|
||
System::Call 'user32::GetWindowLong(p $HWNDPARENT, i -16) i .R0'
|
||
IntOp $R0 $R0 & 0xFF30FFFF ; 清 WS_CAPTION / BORDER / DLGFRAME / SYSMENU / MIN / MAX
|
||
System::Call 'user32::SetWindowLong(p $HWNDPARENT, i -16, i R0)'
|
||
Push ${WIN_W}
|
||
Call S
|
||
Pop $R0
|
||
Push ${WIN_H}
|
||
Call S
|
||
Pop $R1
|
||
System::Call 'user32::GetSystemMetrics(i 0) i .R2'
|
||
System::Call 'user32::GetSystemMetrics(i 1) i .R3'
|
||
IntOp $R2 $R2 - $R0
|
||
IntOp $R2 $R2 / 2
|
||
IntOp $R3 $R3 - $R1
|
||
IntOp $R3 $R3 / 2
|
||
${If} $R2 < 0
|
||
StrCpy $R2 0
|
||
${EndIf}
|
||
${If} $R3 < 0
|
||
StrCpy $R3 0
|
||
${EndIf}
|
||
System::Call 'user32::SetWindowPos(p $HWNDPARENT, p 0, i $R2, i $R3, i $R0, i $R1, i 0x0014)'
|
||
Call EnsureArt
|
||
FunctionEnd
|
||
|
||
; ── .onInit ────────────────────────────────────────────────────────────────
|
||
; 逐项对照 electron-builder 的 templates/nsis/installer.nsi,只去掉 setInstallSectionSpaceRequired
|
||
; (那是给组件页算体积用的,我们没有组件页)。
|
||
Function .onInit
|
||
; 显式赋 "0":下面 NewPage 里用 $timerId == 0 判断「建过没有」,
|
||
; 而 LogicLib 的 == 是字符串比较,空串跟 0 不相等,会一次都不建(踩过)
|
||
StrCpy $timerId 0
|
||
StrCpy $uiPage 0
|
||
StrCpy $instStarted 0
|
||
SetOutPath $INSTDIR
|
||
${LogSet} on
|
||
!insertmacro check64BitAndSetRegView
|
||
; 静默安装跳过单实例检查:这个宏的头一句是 BringToFront,而静默模式压根没有窗口,
|
||
; 实测在这种情况下会挂死(wine 下 45 秒不返回 —— 安装器就卡在这儿永远装不完)。
|
||
; 交互安装照常查:那才是「用户重复双击」真正会发生的地方。
|
||
${IfNot} ${Silent}
|
||
${AndIfNot} ${UAC_IsInnerInstance}
|
||
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
|
||
${EndIf}
|
||
!insertmacro initMultiUser
|
||
!ifmacrodef customInit
|
||
!insertmacro customInit
|
||
!endif
|
||
StrCpy $runApp 1
|
||
FunctionEnd
|
||
|
||
; ── 页面序列 ───────────────────────────────────────────────────────────────
|
||
|
||
Page custom PageWelcome
|
||
Page custom PageLicense PageLicenseLeave
|
||
Page custom PageDir PageDirLeave
|
||
|
||
Page custom PageInstall PageInstallLeave
|
||
Page custom PageFinish PageFinishLeave
|
||
|
||
; 空跑的 instfiles 页(放在页面序列最后):NSIS 要求「有安装段就必须有 instfiles 页」,
|
||
; 否则 -WX 下直接报 warning 8000 编译不过。在 PRE 回调里 Abort 掉、页面不会显示,
|
||
; 而 P5 的「完成」是直接结束安装器(见 OnFinish),根本走不到这一页。
|
||
PageEx instfiles
|
||
Caption " "
|
||
PageCallbacks SkipInstfiles "" ""
|
||
PageExEnd
|
||
|
||
Function SkipInstfiles
|
||
Abort
|
||
FunctionEnd
|
||
|
||
; ── 安装流程 ───────────────────────────────────────────────────────────────
|
||
; 「立即运行」用 electron-builder 的 StartApp 宏(common.nsh):它内部走 ExecShellAsUser,
|
||
; 以**登录用户**身份启动 —— 「为所有用户安装」那一趟进程是提权过的,直接 Exec 会让应用
|
||
; 继承管理员令牌,装完跑起来的会是个管理员身份的客户端(数据目录/开机自启都跑到别人家去)。
|
||
; 这个宏里带一句 Var /GLOBAL startAppArgs,整个脚本只能插入一次,所以包成一个函数给两处调。
|
||
Function StartAppNow
|
||
HideWindow
|
||
!insertmacro StartApp
|
||
FunctionEnd
|
||
|
||
; 安装流程本体。放在函数里而不是 Section 里,是为了让 P4 能是自绘页(原因见 Tick)。
|
||
; 顺序逐行对照 electron-builder 的 templates/nsis/installSection.nsh。
|
||
Function DoInstall
|
||
; 静默安装没有 UI 可选安装模式,per-machine 这一趟要在这里补提权。
|
||
; 比上游多一个 $installMode == "all" 的条件:上游只在「已有 per-machine 安装」时提权,
|
||
; 而我们把默认模式改成了「为所有用户」,全新静默安装(/S)就会默认往 Program Files 写,
|
||
; 不提权必然写不进去 —— 现场用脚本装机时这一步比交互装机更要紧。
|
||
!ifndef INSTALL_MODE_PER_ALL_USERS
|
||
!ifndef ONE_CLICK
|
||
${if} $installMode == "all"
|
||
${andIf} ${Silent}
|
||
${ifNot} ${UAC_IsAdmin}
|
||
ShowWindow $HWNDPARENT ${SW_HIDE}
|
||
!insertmacro UAC_RunElevated
|
||
${Switch} $0
|
||
${Case} 0
|
||
${Break}
|
||
${Case} 1223 ;用户取消了 UAC
|
||
${Break}
|
||
${Default}
|
||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
|
||
${Break}
|
||
${EndSwitch}
|
||
Quit
|
||
${else}
|
||
!insertmacro setInstallModePerAllUsers
|
||
${endIf}
|
||
${endIf}
|
||
!endif
|
||
!endif
|
||
|
||
InitPluginsDir
|
||
|
||
${IfNot} ${Silent}
|
||
SetDetailsPrint none
|
||
${endif}
|
||
|
||
StrCpy $appExe "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
||
|
||
# must be called before uninstallOldVersion
|
||
!insertmacro setLinkVars
|
||
|
||
; 非 one-click 时只在最外层进程里查:提权出来的内层进程和用户看到的可能是同一个应用,
|
||
; 两个进程同时抢着关它会互相打架(上游 installer.nsh / installSection.nsh 就是这么分的)。
|
||
; 静默安装没有窗口,那句 UAC_IsInnerInstance 走插件、会卡住不返回,所以直接查。
|
||
${If} ${Silent}
|
||
Call CheckAppRunningImpl
|
||
${Else}
|
||
${ifNot} ${UAC_IsInnerInstance}
|
||
Call CheckAppRunningImpl
|
||
${endif}
|
||
${EndIf}
|
||
|
||
StrCpy $keepShortcuts "false"
|
||
!insertMacro setIsTryToKeepShortcuts
|
||
${if} $isTryToKeepShortcuts == "true"
|
||
ReadRegStr $R1 SHELL_CONTEXT "${INSTALL_REGISTRY_KEY}" KeepShortcuts
|
||
${if} $R1 == "true"
|
||
${andIf} ${FileExists} "$appExe"
|
||
StrCpy $keepShortcuts "true"
|
||
${endIf}
|
||
${endif}
|
||
|
||
; 覆盖安装:先跑旧卸载器(它自己会带 --updated / --keep-shortcuts)
|
||
!insertmacro uninstallOldVersion SHELL_CONTEXT
|
||
!insertmacro handleUninstallResult SHELL_CONTEXT
|
||
${if} $installMode == "all"
|
||
!insertmacro uninstallOldVersion HKEY_CURRENT_USER
|
||
!insertmacro handleUninstallResult HKEY_CURRENT_USER
|
||
${endIf}
|
||
|
||
SetOutPath $INSTDIR
|
||
|
||
!insertmacro StageCall 1
|
||
!insertmacro installApplicationFiles
|
||
!insertmacro StageCall 2
|
||
!insertmacro registryAddInstallInfo
|
||
!insertmacro StageCall 3
|
||
!insertmacro addStartMenuLink $keepShortcuts
|
||
!insertmacro addDesktopLink $keepShortcuts
|
||
|
||
${if} ${FileExists} "$newStartMenuLink"
|
||
StrCpy $launchLink "$newStartMenuLink"
|
||
${else}
|
||
StrCpy $launchLink "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
|
||
${endif}
|
||
|
||
!ifmacrodef registerFileAssociations
|
||
!insertmacro registerFileAssociations
|
||
!endif
|
||
|
||
!insertmacro StageCall 4
|
||
!ifmacrodef customInstall
|
||
!insertmacro customInstall
|
||
!endif
|
||
!insertmacro StageCall 5
|
||
|
||
; 真卸载器在这里现写。先删掉 installApplicationFiles 落下来的那个占位文件,
|
||
; 免得「万一没写成功、$INSTDIR 里却留着一个假卸载器」这种最坏情况。
|
||
Delete "$INSTDIR\${UNINSTALL_FILENAME}"
|
||
WriteUninstaller "$INSTDIR\${UNINSTALL_FILENAME}"
|
||
|
||
|
||
|
||
!ifdef ONE_CLICK
|
||
!ifdef RUN_AFTER_FINISH
|
||
${ifNot} ${Silent}
|
||
${orIf} ${isForceRun}
|
||
Call StartAppNow
|
||
${endIf}
|
||
!else
|
||
${if} ${isForceRun}
|
||
Call StartAppNow
|
||
${endIf}
|
||
!endif
|
||
!insertmacro quitSuccess
|
||
!else
|
||
; 交互安装的「立即运行」在 P5 上处理;静默安装只能靠 --force-run
|
||
${if} ${isForceRun}
|
||
${andIf} ${Silent}
|
||
Call StartAppNow
|
||
${endIf}
|
||
!endif
|
||
FunctionEnd
|
||
|
||
; 安装段的两条路:
|
||
; 交互安装:安装段根本不会执行(页面序列里的 instfiles 页被 Abort 掉了),
|
||
; 安装流程由 P4 的定时器调 DoInstall 跑;
|
||
; 静默安装(/S):没有页面可走,段就是唯一入口 —— 这一步走的是 electron-builder
|
||
; 原生的「段在 instfiles 页上执行」,和上游同一条路。
|
||
; 之所以不把静默安装也塞进 .onInit:实测那样会在 wine 里挂死(.onInit 阶段还没有
|
||
; 页对话框,DoInstall 里的界面相关动作没有宿主)。段这条路是上游验证过的。
|
||
Section "install" INSTALL_SECTION_ID
|
||
${If} ${Silent}
|
||
Call DoInstall
|
||
!insertmacro quitSuccess
|
||
${EndIf}
|
||
SectionEnd
|