feat: Phase 4 收尾 + Phase 5 打包(首启向导/开机自启/URL scheme 闭环/mac dmg)
Phase 4 剩余三项: - 首次引导向导(setup/first_run + ui/setup_wizard):服务自检(含 C# 助手共存 提示)→ 打印机检测与 HiTi 驱动安装指引(官网链接/重新检测,mac 与 win 文案 分支,允许无打印机完成)→ 开机自启开关 + scheme 注册;侧栏底部常驻设置区 (自启开关 + 重新运行引导) - 开机自启(setup/autostart):mac 写 LaunchAgent(RunAtLoad 一次性,不用 KeepAlive——托盘退出不能被拉回);Windows 走 HKCU Run(reg 命令免 win32 依赖) - URL scheme 闭环:单实例唤醒端口 38210(setup/single_instance)——zaxprint:// 唤起或重复双击启动第二进程时通知常驻实例弹窗后自退,跨平台一致(弃用 app_links 的 mac 事件转发不确定性);Windows 启动时幂等注册 HKCU scheme; 前端 cardPrinter.ts 加 launchAgent()(zaxprint://open),开卡抽屉离线时显示 "启动打印助手"按钮,点击唤起后 1s×6 轮询自动上线(biz 前端改动待用户提交) Phase 5: - tool/package_macos.sh:构建 → ad-hoc 就地签名(--deep,勿加 --options=runtime: hardened runtime 库校验会因 ad-hoc 无 Team ID 拒载 framework,实测踩坑)→ hdiutil dmg;实测产出 22MB dmg + 签名后冒烟通过 - windows/installer.iss:Inno Setup 安装包脚本(快捷方式/桌面图标/开机自启 Tasks/zaxprint scheme 注册与卸载清理/HiTi SDK DLL 随 Release 目录打包) 依赖 +url_launcher +shared_preferences;13 测试全过、analyze 零问题。
This commit is contained in:
1 parent
a7cc69bb92
commit
3ecac9a302
15 files changed
+934
-51
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import 'dart:io';
|
||||
|
||||
import '../utils/command_runner.dart';
|
||||
|
||||
/// 开机自启:mac 写 LaunchAgent(RunAtLoad 一次性——托盘退出不能被拉回,故不用 KeepAlive);
|
||||
/// Windows 写 HKCU Run 注册表键(reg 命令实现,免 win32 依赖)。
|
||||
/// 自启指向当前可执行文件(Platform.resolvedExecutable),以安装位置为准。
|
||||
class Autostart {
|
||||
static const macLabel = 'com.zax.desktop-plugin';
|
||||
static const winRunName = 'ZaxDesktopPlugin';
|
||||
|
||||
static bool get isSupported => Platform.isMacOS || Platform.isWindows;
|
||||
|
||||
static String? get _macPlistPath {
|
||||
final home = Platform.environment['HOME'];
|
||||
if (home == null) return null;
|
||||
return '$home/Library/LaunchAgents/$macLabel.plist';
|
||||
}
|
||||
|
||||
/// 自启是否已开启。
|
||||
static Future<bool> isEnabled() async {
|
||||
if (Platform.isMacOS) {
|
||||
final path = _macPlistPath;
|
||||
return path != null && File(path).existsSync();
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
final r = await runCommand(
|
||||
'reg',
|
||||
['query', r'HKCU\Software\Microsoft\Windows\CurrentVersion\Run', '/v', winRunName],
|
||||
tolerateExit: true,
|
||||
);
|
||||
return r.contains(winRunName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 开启自启(mac 顺带把 C# 助手的 LaunchAgent 状态返回,供向导提示端口关系)。
|
||||
static Future<void> enable() async {
|
||||
if (Platform.isMacOS) {
|
||||
final path = _macPlistPath;
|
||||
if (path == null) throw StateError('无法定位用户目录');
|
||||
final exe = Platform.resolvedExecutable;
|
||||
await Directory(File(path).parent.path).create(recursive: true);
|
||||
await File(path).writeAsString('''
|
||||
<?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>Label</key>
|
||||
<string>$macLabel</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>$exe</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>LaunchOnlyOnce</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
''');
|
||||
return;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
final exe = Platform.resolvedExecutable;
|
||||
await runCommand('reg', [
|
||||
'add', r'HKCU\Software\Microsoft\Windows\CurrentVersion\Run',
|
||||
'/v', winRunName, '/t', 'REG_SZ', '/d', '"$exe"', '/f',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
throw UnsupportedError('当前平台不支持开机自启');
|
||||
}
|
||||
|
||||
static Future<void> disable() async {
|
||||
if (Platform.isMacOS) {
|
||||
final path = _macPlistPath;
|
||||
if (path != null && File(path).existsSync()) {
|
||||
await File(path).delete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
await runCommand(
|
||||
'reg',
|
||||
['delete', r'HKCU\Software\Microsoft\Windows\CurrentVersion\Run', '/v', winRunName, '/f'],
|
||||
tolerateExit: true, // 不存在时也算关闭成功
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw UnsupportedError('当前平台不支持开机自启');
|
||||
}
|
||||
|
||||
/// C# 版助手的 LaunchAgent 是否在(本机灰度共存提示用;mac only)。
|
||||
static bool get legacyCsharpAgentInstalled {
|
||||
if (!Platform.isMacOS) return false;
|
||||
final home = Platform.environment['HOME'];
|
||||
return home != null &&
|
||||
File('$home/Library/LaunchAgents/com.zax.cardprinteragent.plist').existsSync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// 首次引导向导完成标记(shared_preferences 持久化)。
|
||||
class FirstRun {
|
||||
static const _key = 'first_run_done';
|
||||
|
||||
static Future<bool> isDone() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_key) ?? false;
|
||||
}
|
||||
|
||||
static Future<void> complete() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_key, true);
|
||||
}
|
||||
|
||||
/// 重新运行向导(设置入口用)。
|
||||
static Future<void> reset() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_key, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
/// 单实例 + 唤醒端口:应用只允许一个常驻实例。
|
||||
///
|
||||
/// zaxprint:// 唤起或用户再次双击图标时,OS 会启动第二个进程——第二实例连上
|
||||
/// 唤醒端口发送 wake 后立即退出,常驻实例收到后把主窗口带到前台。
|
||||
/// (比 app_links 的方案更跨平台一致:mac/win 都只依赖回环 socket。)
|
||||
class SingleInstance {
|
||||
static const int wakePort = 38210;
|
||||
|
||||
/// 尝试成为主实例。
|
||||
///
|
||||
/// 返回 true = 本实例是主实例(已开始监听唤醒,[onWake] 在收到唤醒时回调);
|
||||
/// 返回 false = 已有常驻实例(已通知其显示窗口),调用方应当退出进程。
|
||||
static Future<bool> acquire({void Function()? onWake}) async {
|
||||
try {
|
||||
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, wakePort);
|
||||
server.listen((socket) {
|
||||
socket
|
||||
.cast<List<int>>()
|
||||
.transform(utf8.decoder)
|
||||
.listen((message) {
|
||||
if (message.contains('wake')) {
|
||||
onWake?.call();
|
||||
}
|
||||
}, onDone: () {
|
||||
socket.destroy();
|
||||
});
|
||||
}, onError: (_) {});
|
||||
return true;
|
||||
} on SocketException {
|
||||
// 端口被占:已有常驻实例,通知它显示窗口
|
||||
try {
|
||||
final socket = await Socket.connect(
|
||||
InternetAddress.loopbackIPv4,
|
||||
wakePort,
|
||||
timeout: const Duration(seconds: 2),
|
||||
);
|
||||
socket.add(utf8.encode('wake'));
|
||||
await socket.flush();
|
||||
await socket.close();
|
||||
} catch (_) {
|
||||
// 唤醒端口被非本应用进程占用等异常:放弃唤醒,按第二实例退出
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'dart:io';
|
||||
|
||||
import '../utils/command_runner.dart';
|
||||
|
||||
/// zaxprint:// URL scheme 注册:
|
||||
/// - macOS:Info.plist CFBundleURLTypes 已声明(构建即生效),无需运行时处理
|
||||
/// - Windows:写 HKCU\Software\Classes\zaxprint(用户级,免管理员),App 启动时 ensure
|
||||
class UrlScheme {
|
||||
static const scheme = 'zaxprint';
|
||||
|
||||
static bool get needsRuntimeRegister => Platform.isWindows;
|
||||
|
||||
/// Windows:确保 scheme 已注册到当前可执行文件(每次启动幂等调用,路径变了会刷新)。
|
||||
static Future<void> ensureRegistered() async {
|
||||
if (!Platform.isWindows) return;
|
||||
final exe = Platform.resolvedExecutable;
|
||||
final root = r'HKCU\Software\Classes' '\\$scheme';
|
||||
await runCommand('reg', ['add', root, '/ve', '/t', 'REG_SZ', '/d', 'URL:资安信插件助手唤起', '/f']);
|
||||
await runCommand('reg', ['add', root, '/v', 'URL Protocol', '/t', 'REG_SZ', '/d', '', '/f']);
|
||||
await runCommand('reg', [
|
||||
'add', r'HKCU\Software\Classes' '\\$scheme' r'\shell\open\command',
|
||||
'/ve', '/t', 'REG_SZ', '/d', '"$exe" "%1"', '/f',
|
||||
]);
|
||||
}
|
||||
|
||||
/// Windows:注册是否指向当前可执行文件(向导自检展示用)。
|
||||
static Future<bool> isRegisteredToSelf() async {
|
||||
if (!Platform.isWindows) return true; // mac 由 Info.plist 保证
|
||||
final exe = Platform.resolvedExecutable;
|
||||
final out = await runCommand(
|
||||
'reg',
|
||||
['query', r'HKCU\Software\Classes' '\\$scheme' r'\shell\open\command', '/ve'],
|
||||
tolerateExit: true,
|
||||
);
|
||||
return out.contains(exe);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user