import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import '../agent.dart'; import '../server/protocol.dart'; import '../setup/autostart.dart'; import '../setup/driver_install.dart'; import '../setup/first_run.dart'; import '../setup/url_scheme.dart'; /// 首次引导向导:服务自检 → 打印机检测(含驱动引导)→ 自启/唤起配置。 /// 深色主题对话框,三步 Stepper 风格;允许无打印机完成(纯联调机)。 class SetupWizard extends StatefulWidget { const SetupWizard({super.key, required this.agent, required this.onFinished}); final Agent agent; final VoidCallback onFinished; /// 便捷入口:未完成过向导时弹出。 static Future showIfNeeded(BuildContext context, Agent agent) async { if (await FirstRun.isDone()) return; if (!context.mounted) return; await showDialog( context: context, barrierDismissible: false, builder: (_) => SetupWizard( agent: agent, onFinished: () {}, ), ); } @override State createState() => _SetupWizardState(); } class _SetupWizardState extends State { static const _driverUrl = 'https://www.hiti.com/support/download.aspx?MenuID=72&lang=CN'; int _step = 0; bool _autostart = true; bool _autostartBusy = false; bool _printerChecking = false; bool _driverInstalling = false; String? _driverInstallError; PrinterStatus? _printerStatus; Agent get _agent => widget.agent; @override void initState() { super.initState(); _checkPrinter(); // 第 2 步期间自动轮询:用户手动装驱动 / 插上打印机后页面自动刷新 // (驱动目录判断零成本;getStatus 走 lpoptions,3s 一次可接受) _pollTimer = Timer.periodic(const Duration(seconds: 3), (_) { if (_step == 1 && mounted && !_driverInstalling) { _checkPrinter(silent: true); } }); } Timer? _pollTimer; @override void dispose() { _pollTimer?.cancel(); super.dispose(); } Future _checkPrinter({bool silent = false}) async { if (!silent) setState(() => _printerChecking = true); try { final status = await _agent.backend.getStatus(); if (mounted) setState(() => _printerStatus = status); } catch (_) { if (mounted) setState(() => _printerStatus = null); } finally { if (mounted && !silent) setState(() => _printerChecking = false); } } Future _finish() async { setState(() => _autostartBusy = true); try { if (_autostart && Autostart.isSupported) { await Autostart.enable(); } await UrlScheme.ensureRegistered(); await FirstRun.complete(); } finally { if (mounted) setState(() => _autostartBusy = false); } if (mounted) { Navigator.of(context).pop(); widget.onFinished(); } } @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Dialog( backgroundColor: scheme.surfaceContainerLow, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide(color: scheme.outlineVariant), ), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _header(scheme), const SizedBox(height: 18), if (_step == 0) _stepWelcome(scheme), if (_step == 1) _stepPrinter(scheme), if (_step == 2) _stepFinish(scheme), const SizedBox(height: 20), _footer(scheme), ], ), ), ), ); } Widget _header(ColorScheme scheme) => Row( children: [ Icon(Icons.extension_rounded, color: scheme.primary, size: 26), const SizedBox(width: 10), Text('初次使用引导', style: Theme.of(context).textTheme.titleLarge), const Spacer(), _stepBadge(scheme), ], ); Widget _stepBadge(ColorScheme scheme) => Row( children: [ for (var i = 0; i < 3; i++) ...[ Container( width: i == _step ? 18 : 8, height: 8, decoration: BoxDecoration( color: i <= _step ? scheme.primary : scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), ), if (i < 2) const SizedBox(width: 6), ], ], ); // ---------------- Step 0:服务自检 ---------------- Widget _stepWelcome(ColorScheme scheme) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('资安信插件助手在本机为管理端网页提供打印等桌面能力,安装完成后保持后台运行即可,无需日常操作。', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 14), _checkRow( scheme, ok: _agent.port != null, okText: '本机服务运行中${_agent.port != null ? "(127.0.0.1:${_agent.port})" : ""}', badText: '本机服务未启动(可能是端口被占用,请重启应用)', ), if (Autostart.legacyCsharpAgentInstalled) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Text( '检测到旧版打印助手(C# 版)在本机自启。两者协议兼容可并存,' '如需完全切换到本应用,可在终端执行:\n' 'launchctl unload ~/Library/LaunchAgents/com.zax.cardprinteragent.plist', style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.5), ), ), ], ], ); // ---------------- Step 1:打印机检测 ---------------- /// 一键安装内置驱动(mac):装完自动重检(队列要等打印机连上/系统识别后才出现)。 Future _installDriver() async { final dmg = DriverInstall.bundledDmgPath(); if (dmg == null) return; setState(() { _driverInstalling = true; _driverInstallError = null; }); try { await DriverInstall.installFromDmg(dmg, onLog: _agent.log.write); await _checkPrinter(); } catch (e) { if (mounted) setState(() => _driverInstallError = '$e'); } finally { if (mounted) setState(() => _driverInstalling = false); } } Widget _stepPrinter(ColorScheme scheme) { final s = _printerStatus; final ready = s != null && s.online; final driverMissing = Platform.isMacOS && !DriverInstall.isInstalled(); final bundledDmg = Platform.isMacOS ? DriverInstall.bundledDmgPath() : null; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _checkRow( scheme, ok: ready, loading: _printerChecking, okText: '打印机就绪${s != null && s.model.isNotEmpty ? "(${s.model})" : ""}', badText: s == null ? '未检测到打印队列' : (s.statusText.isEmpty ? '打印机未就绪' : s.statusText), ), const SizedBox(height: 12), // mac:驱动未装 → 首选一键安装内置驱动,其次官网链接 if (driverMissing) ...[ Text( '本机尚未安装 HiTi 打印机驱动(mac 打印必需的系统组件)。', style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6), ), const SizedBox(height: 8), Row( children: [ FilledButton.tonalIcon( onPressed: _driverInstalling || bundledDmg == null ? null : _installDriver, icon: _driverInstalling ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.download, size: 16), label: Text(_driverInstalling ? '正在安装…(弹窗中输入管理员密码)' : '一键安装驱动(内置安装包)'), ), const SizedBox(width: 8), TextButton.icon( onPressed: () => launchUrl(Uri.parse(_driverUrl)), icon: const Icon(Icons.open_in_new, size: 14), label: const Text('改为手动下载'), ), ], ), if (bundledDmg == null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( '(当前构建未内置驱动安装包,请用官网链接手动安装)', style: Theme.of(context).textTheme.bodySmall, ), ), if (_driverInstallError != null) Padding( padding: const EdgeInsets.only(top: 6), child: Text( '安装失败:$_driverInstallError', style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: scheme.error), ), ), const SizedBox(height: 8), Text( '安装完成后页面会自动刷新(也可手动重新检测);随后 USB 连接打印机并开机,系统识别出现打印队列即完成。', style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 4), TextButton.icon( onPressed: _printerChecking ? null : () => _checkPrinter(), icon: const Icon(Icons.refresh, size: 16), label: const Text('重新检测'), ), ] // 驱动已装但打印机未就绪 → 引导连接/添加队列 else if (!ready) ...[ Text( Platform.isMacOS ? '驱动已安装。请确认:① 打印机 USB 已连接并开机;② 系统设置 → 打印机与扫描仪 里出现 HiTi 打印机(没有的话点下方「+」添加,选 HiTi CS-200e);③ 回到这里重新检测。' : 'Windows 打印站:打开 HiTi 官网下载 CS-2xx Windows 驱动并安装,USB 连接打印机;本应用也支持免驱动的 SDK 直连模式,装完驱动或接好打印机后点「重新检测」。', style: Theme.of(context).textTheme.bodySmall?.copyWith(height: 1.6), ), const SizedBox(height: 8), Align( alignment: Alignment.centerLeft, child: TextButton.icon( onPressed: () => launchUrl(Uri.parse(_driverUrl)), icon: const Icon(Icons.open_in_new, size: 14), label: const Text('打开 HiTi 驱动下载页'), ), ), const SizedBox(height: 4), TextButton.icon( onPressed: _printerChecking ? null : _checkPrinter, icon: const Icon(Icons.refresh, size: 16), label: const Text('重新检测'), ), ] else ...[ Text('打印机已连接,可以正常接收管理端提交的卡面打印。', style: Theme.of(context).textTheme.bodySmall), ], ], ); } // ---------------- Step 2:完成配置 ---------------- Widget _stepFinish(ColorScheme scheme) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( contentPadding: EdgeInsets.zero, value: _autostart, onChanged: Autostart.isSupported ? (v) => setState(() => _autostart = v) : null, title: const Text('开机自动启动'), subtitle: Text( Autostart.isSupported ? '登录系统后自动在后台运行(可在左侧菜单底部随时修改)' : '当前平台不支持', style: Theme.of(context).textTheme.bodySmall, ), ), const SizedBox(height: 6), _checkRow( scheme, ok: true, okText: '网页唤起支持(zaxprint:// 协议)已启用', badText: '', ), const SizedBox(height: 8), Text( '全部完成。关闭窗口将最小化到托盘,服务持续运行;退出请通过托盘菜单。', style: Theme.of(context).textTheme.bodySmall, ), ], ); // ---------------- 公共组件 ---------------- Widget _checkRow(ColorScheme scheme, {required bool ok, required String okText, required String badText, bool loading = false}) { final color = ok ? scheme.primary : const Color(0xFFD97706); return Row( children: [ if (loading) const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) else Icon(ok ? Icons.check_circle : Icons.info_outline, size: 18, color: color), const SizedBox(width: 8), Expanded( child: Text( ok ? okText : badText, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: color), ), ), ], ); } Widget _footer(ColorScheme scheme) => Row( children: [ if (_step > 0) TextButton( onPressed: () => setState(() => _step--), child: const Text('上一步'), ), const Spacer(), if (_step < 2) FilledButton( onPressed: () => setState(() => _step++), child: Text(_step == 0 ? '下一步' : '跳过,下一步'), ) else FilledButton( onPressed: _autostartBusy ? null : _finish, child: const Text('完成'), ), ], ); }