import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miaoji_zhang/shared/services/screenshot_channel.dart'; import 'package:miaoji_zhang/shared/services/session_store.dart'; import 'package:miaoji_zhang/shared/services/shanghai_time.dart'; import 'package:miaoji_zhang/shared/services/recognition_diagnostic_formatter.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; import 'package:miaoji_zhang/shared/widgets/app_controls.dart'; class ScreenshotSettingsPage extends StatefulWidget { const ScreenshotSettingsPage({super.key}); @override State createState() => _ScreenshotSettingsPageState(); } class _ScreenshotSettingsPageState extends State with WidgetsBindingObserver { RecognitionStatus? _status; bool _checking = true; bool _authorizing = false; String? _pendingAuthorizationKey; String? _error; Timer? _diagnosticRefreshTimer; Timer? _previewExpiryTimer; bool _diagnosticRefreshInFlight = false; bool _connectionCheckInFlight = false; bool _keepAliveStarting = false; bool _recentsProtectionNoticeChecked = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _check(); _diagnosticRefreshTimer = Timer.periodic( const Duration(seconds: 2), (_) => _refreshRunningDiagnostic(), ); } @override void dispose() { _diagnosticRefreshTimer?.cancel(); _previewExpiryTimer?.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { Future.delayed( const Duration(milliseconds: 500), _resumeAuthorization, ); } } void _refreshRunningDiagnostic() { if (!mounted || _diagnosticRefreshInFlight || !{'started', 'waiting'}.contains(_status?.latestDiagnostic?.result)) { return; } _diagnosticRefreshInFlight = true; _check().whenComplete(() => _diagnosticRefreshInFlight = false); } Future _check({bool monitorConnection = true}) async { try { var status = await ScreenshotChannel.recognitionStatus(); final invalid = [ if (status.accessibilityEvents && (!status.accessibilityAuthorized || !status.postNotificationsGranted)) 'accessibility_events', if (status.notificationEvents && (!status.notificationAuthorized || !status.postNotificationsGranted)) 'notification_events', if (status.aiScreenshot && (!status.accessibilityAuthorized || !status.postNotificationsGranted)) 'ai_screenshot', ]; for (final key in invalid) { await ScreenshotChannel.setRecognitionToggle(key, false); } if (invalid.isNotEmpty) { status = await ScreenshotChannel.recognitionStatus(); } if (!mounted) return; setState(() { _status = status; _checking = false; _error = null; }); _schedulePreviewExpiry(status); unawaited(_showRecentsProtectionNotice(status)); if (monitorConnection && status.accessibilityAuthorized && !status.accessibilityConnected && !status.taskCleanerRecoveryNeeded && (status.accessibilityEvents || status.aiScreenshot)) { unawaited(_monitorAccessibilityConnection()); } } catch (_) { if (!mounted) return; setState(() { _checking = false; _error = '状态读取失败,请重试'; }); } } Future _showRecentsProtectionNotice(RecognitionStatus status) async { if (_recentsProtectionNoticeChecked || !status.recentsProtectionActive) { return; } _recentsProtectionNoticeChecked = true; final prefs = await SharedPreferences.getInstance(); const key = 'originos_recents_protection_notice_v1'; if (prefs.getBool(key) == true || !mounted) return; await prefs.setBool(key, true); if (!mounted) return; _showMessage('记之已从最近任务隐藏,可从桌面图标重新打开;关闭全部识别后恢复。'); } Future _monitorAccessibilityConnection({ bool ensureKeepAlive = false, }) async { if (_connectionCheckInFlight) return; _connectionCheckInFlight = true; if (mounted) setState(() {}); try { if (ensureKeepAlive) { _keepAliveStarting = true; if (mounted) setState(() {}); await ScreenshotChannel.ensureRecognitionKeepAlive(); } await ScreenshotChannel.waitForAccessibilityConnection(); final status = await ScreenshotChannel.recognitionStatus(); if (!mounted) return; setState(() { _status = status; _error = null; }); _schedulePreviewExpiry(status); } catch (_) { if (!mounted) return; setState(() => _error = '连接状态复核失败,请重试'); } finally { _connectionCheckInFlight = false; _keepAliveStarting = false; if (mounted) setState(() {}); } } void _schedulePreviewExpiry(RecognitionStatus status) { _previewExpiryTimer?.cancel(); final expiresAt = status.ocrDiagnosticPreviewExpiresAt; if (!status.ocrDiagnosticPreview || expiresAt == null) return; final remaining = expiresAt.difference(DateTime.now()); if (remaining <= Duration.zero) { unawaited(_check()); return; } _previewExpiryTimer = Timer(remaining, _check); } bool _hasSystemAuthorization(String key, RecognitionStatus status) { return switch (key) { 'notification_events' => status.notificationAuthorized, 'accessibility_events' || 'ai_screenshot' => status.accessibilityAuthorized, _ => true, }; } Future _toggle(String key, bool enabled) async { if (_authorizing) return; if (key == 'ocr_diagnostic_preview') { final changed = await ScreenshotChannel.setRecognitionToggle( key, enabled, ); if (!mounted) return; if (!changed) { _showMessage('设置保存失败,请重试'); } else if (enabled) { _showMessage('脱敏预览已开启,将在 10 分钟后自动关闭'); } await _check(); return; } if (enabled && key == 'accessibility_events' && !await _confirmLocalOcrConsent()) { return; } if (enabled && key == 'ai_screenshot' && !await _confirmAiBatchConsent()) { return; } if (!enabled) { _pendingAuthorizationKey = null; final changed = await ScreenshotChannel.setRecognitionToggle(key, false); if (!mounted) return; if (!changed) { _showMessage('设置保存失败,请重试'); } await _check(); return; } final status = _status ?? await ScreenshotChannel.recognitionStatus(); if (!_hasSystemAuthorization(key, status)) { await ScreenshotChannel.setRecognitionToggle(key, false); _pendingAuthorizationKey = key; await _check(); if (!mounted) return; if (key == 'notification_events') { await ScreenshotChannel.openNotificationAccessSettings(); } else { await ScreenshotChannel.openAccessibilitySettings(); } return; } await _enableAfterAuthorization(key); } Future _resumeAuthorization() async { if (_authorizing) return; await _check(); final key = _pendingAuthorizationKey; final status = _status; if (key == null || status == null) return; if (!_hasSystemAuthorization(key, status)) { _pendingAuthorizationKey = null; await ScreenshotChannel.setRecognitionToggle(key, false); await _check(); if (mounted) _showMessage('未完成系统授权,识别开关仍保持关闭'); return; } await _enableAfterAuthorization(key); } Future _enableAfterAuthorization(String key) async { _authorizing = true; try { final notificationsAllowed = await ScreenshotChannel.requestNotificationPermission(); if (!notificationsAllowed) { await ScreenshotChannel.setRecognitionToggle(key, false); if (mounted) { _showMessage('未允许结果通知,识别开关仍保持关闭'); } return; } final changed = await ScreenshotChannel.setRecognitionToggle(key, true); if (!changed && mounted) { _showMessage('设置保存失败,请重试'); } } finally { _pendingAuthorizationKey = null; _authorizing = false; await _check(); } } Future _confirmLocalOcrConsent() async { final preferences = await SharedPreferences.getInstance(); if (preferences.getBool('local_ocr_consent_v1') == true) return true; if (!mounted) return false; final accepted = await showModalBottomSheet( context: context, useSafeArea: true, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (sheetContext) => Container( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), decoration: BoxDecoration( color: sheetContext.jz.card, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const JzSheetHeader(title: '启用本地视觉识别', subtitle: '请确认无障碍事件识别的截屏用途'), const SizedBox(height: 12), const _ConsentPoint( icon: Icons.visibility_outlined, text: '仅在微信、支付宝疑似支付流程结束时截取当前页面。', ), const _ConsentPoint( icon: Icons.memory_rounded, text: '图片只在内存中由本地 OCR 处理,完成后立即释放。', ), const _ConsentPoint( icon: Icons.cloud_off_outlined, text: '默认不会上传;只有你另行开启 AI 截图补全时才允许在线分析。', ), const SizedBox(height: 16), Row( children: [ Expanded( child: JzActionButton( label: '暂不开启', secondary: true, onPressed: () => Navigator.pop(sheetContext, false), ), ), const SizedBox(width: 10), Expanded( child: JzActionButton( label: '同意并继续', onPressed: () => Navigator.pop(sheetContext, true), ), ), ], ), ], ), ), ); if (accepted == true) { await preferences.setBool('local_ocr_consent_v1', true); return true; } return false; } Future _confirmAiBatchConsent() async { final preferences = await SharedPreferences.getInstance(); if (preferences.getBool('ai_batch_consent_v2') == true) return true; if (!mounted) return false; final accepted = await showModalBottomSheet( context: context, useSafeArea: true, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (sheetContext) => Container( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), decoration: BoxDecoration( color: sheetContext.jz.card, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const JzSheetHeader( title: '启用 AI 批次对账', subtitle: '请确认支付页截图的批量处理方式', ), const SizedBox(height: 12), const _ConsentPoint( icon: Icons.schedule_rounded, text: '支付结果会按 30 秒空闲窗口归为一批,最长等待 2 分钟或累计 10 条。', ), const _ConsentPoint( icon: Icons.auto_fix_high_rounded, text: '本批截图和本地候选会发送给 AI,仅允许在当前批次内保留、修正、补全或剔除。', ), const _ConsentPoint( icon: Icons.enhanced_encryption_outlined, text: '待提交图片仅在本机临时加密保存,批次完成或回退后立即删除,不进入最近对账记录。', ), const SizedBox(height: 16), Row( children: [ Expanded( child: JzActionButton( label: '暂不开启', secondary: true, onPressed: () => Navigator.pop(sheetContext, false), ), ), const SizedBox(width: 10), Expanded( child: JzActionButton( label: '同意并开启', onPressed: () => Navigator.pop(sheetContext, true), ), ), ], ), ], ), ), ); if (accepted == true) { await preferences.setBool('ai_batch_consent_v2', true); return true; } return false; } void _showMessage(String message) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { final palette = context.jz; final status = _status; final aiAvailable = SessionStore.instance.isAccount && SessionStore.instance.aiEnabled; return Scaffold( appBar: AppBar(title: const Text('智能识别')), body: RefreshIndicator( onRefresh: _check, child: ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), children: [ Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: palette.card, borderRadius: BorderRadius.circular(20), border: Border.all(color: palette.line), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: palette.primaryBackground, borderRadius: BorderRadius.circular(14), ), child: const Icon( Icons.auto_awesome_motion_rounded, color: AppTheme.primary, ), ), const SizedBox(width: 13), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '微信、支付宝账单自动识别', style: TextStyle( color: palette.text, fontSize: 16, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 6), Text( '两路结果会先在本机合并去重。金额、方向和来源明确时自动入账,其他情况只通知你确认。', style: TextStyle( color: palette.text2, fontSize: 12, height: 1.55, ), ), ], ), ), ], ), ), const SizedBox(height: 12), if (_checking) const Center( child: Padding( padding: EdgeInsets.all(30), child: CircularProgressIndicator(), ), ) else if (_error != null) _ErrorCard(message: _error!, onRetry: _check) else ...[ _RecognitionCard( icon: Icons.accessibility_new_rounded, title: '无障碍事件识别', description: '仅在微信和支付宝疑似支付流程中读取可见文字,并按需在内存中进行本地截图 OCR;不保存图片、完整控件树,也不监听按键。', authorized: status!.accessibilityAuthorized, connected: status.accessibilityConnected, statusLabel: _connectionCheckInFlight && !status.accessibilityConnected ? '正在连接' : status.accessibilityConnectionLabel, recoveryMessage: status.accessibilityNeedsRecovery ? status.taskCleanerRecoveryNeeded ? 'OriginOS 清理了识别进程,系统仍保留授权,但无障碍服务已被标记故障。' : '系统仍显示已授权,但 OriginOS 未重新绑定服务。请前往系统设置,将记之无障碍服务关闭后重新开启。' : (status.accessibilityEvents || status.aiScreenshot) && status.keepAliveNeedsRecovery ? _keepAliveStarting ? '正在启动后台保护…' : status.keepAliveError ?? '后台保护未运行,划掉应用后识别可能中断。' : null, onOpenSettings: ScreenshotChannel.openAccessibilitySettings, settingsLabel: '前往无障碍设置', onRetry: status.accessibilityNeedsRecovery ? () => _monitorAccessibilityConnection() : null, onStartKeepAlive: (status.accessibilityEvents || status.aiScreenshot) && status.keepAliveNeedsRecovery && !_keepAliveStarting ? () => _monitorAccessibilityConnection(ensureKeepAlive: true) : null, child: Column( children: [ JzSwitchTile( value: status.accessibilityEvents, title: '识别开关', subtitle: !status.accessibilityAuthorized ? '开启后需要前往系统设置授权' : !status.postNotificationsGranted ? '识别已开启,还需允许记之显示结果通知' : '系统权限已授权', onChanged: (value) => _toggle('accessibility_events', value), ), Divider(height: 1, color: context.jz.line), JzSwitchTile( value: status.ocrDiagnosticPreview, title: 'OCR 诊断预览', subtitle: '仅保留脱敏文字,10 分钟后自动关闭,不上传、不保存原图', onChanged: status.accessibilityEvents ? (value) => _toggle('ocr_diagnostic_preview', value) : null, ), ], ), ), const SizedBox(height: 10), _RecognitionCard( icon: Icons.notifications_active_outlined, title: '通知识别', description: '只处理开启后新产生的微信、支付宝通知,不读取通知历史;关闭识别不会撤销系统授权。', authorized: status.notificationAuthorized, connected: status.notificationConnected, onOpenSettings: ScreenshotChannel.openNotificationAccessSettings, child: JzSwitchTile( value: status.notificationEvents, title: '识别开关', subtitle: !status.notificationAuthorized ? '开启后需要前往系统设置授权' : !status.postNotificationsGranted ? '已允许读取,还需允许记之显示结果通知' : '通知访问权限已授权', onChanged: (value) => _toggle('notification_events', value), ), ), const SizedBox(height: 10), _RecognitionCard( icon: Icons.document_scanner_outlined, title: 'AI 截图补全', description: '支付结果按 30 秒空闲窗口批量提交,AI 只在本批次内纠错、补全或剔除。处理结束立即删除图片。', authorized: aiAvailable, connected: aiAvailable && status.accessibilityConnected, statusLabel: !aiAvailable ? 'AI 不可用' : null, onOpenSettings: status.accessibilityAuthorized ? null : ScreenshotChannel.openAccessibilitySettings, child: Column( children: [ JzSwitchTile( value: status.aiScreenshot, title: '批次对账开关', subtitle: !aiAvailable ? '需要登录且账号具备 AI 权限' : !status.accessibilityAuthorized ? '需要先授权无障碍截屏能力' : '默认关闭,仅在支付应用前台运行', onChanged: !aiAvailable ? null : (value) => _toggle('ai_screenshot', value), ), Divider(height: 1, color: context.jz.line), InkWell( onTap: () => context.push('/recognition-batches'), child: const Padding( padding: EdgeInsets.symmetric( horizontal: 4, vertical: 13, ), child: Row( children: [ Icon(Icons.fact_check_outlined, size: 20), SizedBox(width: 10), Expanded(child: Text('识别记录')), Icon(Icons.chevron_right_rounded), ], ), ), ), ], ), ), if (status.latestDiagnostic != null) ...[ const SizedBox(height: 10), _RecognitionDiagnosticCard( diagnostic: status.latestDiagnostic!, onClear: () async { await ScreenshotChannel.clearRecognitionDiagnostic(); await _check(); }, ), ], if (status.accessibilityEvents || status.notificationEvents || status.aiScreenshot) ...[ const SizedBox(height: 10), _BackgroundKeepAliveCard( status: status, starting: _keepAliveStarting, onStart: () => _monitorAccessibilityConnection(ensureKeepAlive: true), ), ], const SizedBox(height: 10), _RecognitionCard( icon: Icons.grid_view_rounded, title: '快捷磁贴截屏', description: '无障碍已连接时静默截屏;未连接时每次弹出 Android 一次性投屏授权。两种方式都不会占用音量键。', authorized: status.accessibilityAuthorized, connected: status.accessibilityConnected, statusLabel: status.accessibilityConnected ? '免授权截屏' : '每次系统授权', child: SizedBox( width: double.infinity, child: JzActionButton( label: '添加截屏记账磁贴', secondary: true, icon: const Icon(Icons.tune_rounded, size: 18), onPressed: ScreenshotChannel.openQuickSettings, ), ), ), ], const SizedBox(height: 12), Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: palette.warningBackground, borderRadius: BorderRadius.circular(14), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.shield_outlined, color: AppTheme.orange, size: 20, ), const SizedBox(width: 9), Expanded( child: Text( '银行密码页等安全窗口由系统禁止截屏,记之不会绕过限制。本地 OCR 图片仅在内存中处理;开启 AI 批次对账后,支付结果页会临时加密并按批上传,处理结束立即删除。', style: TextStyle( color: palette.text2, fontSize: 11.5, height: 1.55, ), ), ), ], ), ), ], ), ), ); } } class _ConsentPoint extends StatelessWidget { final IconData icon; final String text; const _ConsentPoint({required this.icon, required this.text}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 20, color: AppTheme.primary), const SizedBox(width: 10), Expanded( child: Text( text, style: TextStyle(color: context.jz.text2, height: 1.5), ), ), ], ), ); } } class _RecognitionDiagnosticCard extends StatelessWidget { final RecognitionDiagnostic diagnostic; final VoidCallback onClear; const _RecognitionDiagnosticCard({ required this.diagnostic, required this.onClear, }); @override Widget build(BuildContext context) { final civil = ShanghaiTime.toCivil(diagnostic.at.toUtc()); final time = '${civil.month.toString().padLeft(2, '0')}-' '${civil.day.toString().padLeft(2, '0')} ' '${civil.hour.toString().padLeft(2, '0')}:' '${civil.minute.toString().padLeft(2, '0')}'; final display = RecognitionDiagnosticDisplay.from(diagnostic); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.circular(18), border: Border.all(color: context.jz.line), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.fact_check_outlined, color: AppTheme.primary), const SizedBox(width: 9), Expanded( child: Text( '最近一次识别', style: TextStyle( color: context.jz.text, fontWeight: FontWeight.w800, ), ), ), Semantics( button: true, label: '清除识别诊断', child: InkWell( onTap: onClear, borderRadius: BorderRadius.circular(10), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 7, ), child: Text( '清除', style: TextStyle( color: AppTheme.primary, fontSize: 12, fontWeight: FontWeight.w700, ), ), ), ), ), ], ), const SizedBox(height: 8), Text( '${diagnostic.appName} · ${display.stageLabel} · ${display.summaryLabel}', style: TextStyle( color: context.jz.text, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 5), Text( display.reasonLabel, style: TextStyle(color: context.jz.text2, height: 1.45), ), if (display.recognitionKindLabel != null || display.amountSourceLabel != null || display.statusStrengthLabel != null || diagnostic.expectedAmountMatched != null || diagnostic.resultTransitionObserved != null) ...[ const SizedBox(height: 9), Wrap( spacing: 7, runSpacing: 7, children: [ _EvidencePill( label: display.resultLabel, positive: diagnostic.result == 'matched' || diagnostic.result == 'auto_ready', ), if (display.recognitionKindLabel != null) _EvidencePill( label: display.recognitionKindLabel!, positive: true, ), if (display.amountSourceLabel != null) _EvidencePill( label: display.amountSourceLabel!, positive: true, ), if (diagnostic.resultFingerprint != null) _EvidencePill( label: '结果指纹 ${diagnostic.resultFingerprint}', positive: true, ), if (display.statusStrengthLabel != null) _EvidencePill( label: display.statusStrengthLabel!, positive: display.statusStrengthPositive, ), if (diagnostic.expectedAmountMatched != null) _EvidencePill( label: diagnostic.expectedAmountMatched! ? '金额匹配' : '金额不匹配', positive: diagnostic.expectedAmountMatched!, ), if (diagnostic.resultTransitionObserved != null) _EvidencePill( label: diagnostic.resultTransitionObserved! ? '已观察到页面跳转' : '未观察到页面跳转', positive: diagnostic.resultTransitionObserved!, ), ], ), ], if (diagnostic.ocrPreview.isNotEmpty) ...[ const SizedBox(height: 10), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: context.jz.background, borderRadius: BorderRadius.circular(12), border: Border.all(color: context.jz.line), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'OCR 脱敏预览', style: TextStyle( color: context.jz.text, fontSize: 12, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), ...diagnostic.ocrPreview.map( (line) => Padding( padding: const EdgeInsets.only(top: 2), child: Text( line, style: TextStyle( color: context.jz.text2, fontSize: 12, height: 1.35, ), ), ), ), ], ), ), ], const SizedBox(height: 7), Text( '$time · 节点 ${diagnostic.nodeCount}' '${diagnostic.ocrMs == null ? '' : ' · OCR ${diagnostic.ocrMs}ms'}' '${diagnostic.amountCandidates == 0 ? '' : ' · 金额候选 ${diagnostic.amountCandidates}'}', style: TextStyle(color: context.jz.text3, fontSize: 11), ), ], ), ); } } class _EvidencePill extends StatelessWidget { final String label; final bool positive; const _EvidencePill({required this.label, required this.positive}); @override Widget build(BuildContext context) { final color = positive ? AppTheme.primary : context.jz.text3; return Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), decoration: BoxDecoration( color: positive ? context.jz.primaryBackground : context.jz.background, borderRadius: BorderRadius.circular(20), border: Border.all( color: positive ? AppTheme.primary : context.jz.line, ), ), child: Text( label, style: TextStyle( color: color, fontSize: 10, fontWeight: FontWeight.w700, ), ), ); } } class _BackgroundKeepAliveCard extends StatelessWidget { final RecognitionStatus status; final bool starting; final VoidCallback onStart; const _BackgroundKeepAliveCard({ required this.status, required this.starting, required this.onStart, }); @override Widget build(BuildContext context) { final isVivo = status.manufacturer.toLowerCase().contains('vivo'); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: context.jz.card, borderRadius: BorderRadius.circular(18), border: Border.all(color: context.jz.line), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: context.jz.warningBackground, borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.battery_saver_outlined, color: AppTheme.orange, size: 21, ), ), const SizedBox(width: 11), const Expanded( child: Text( '后台识别保活', style: TextStyle(fontWeight: FontWeight.w800), ), ), Text( status.recentsProtectionActive ? '任务清理防护中' : status.keepAliveRunning ? '保护运行中' : status.keepAliveExpected ? '保护未运行' : status.batteryOptimizationIgnored ? '后台限制较少' : '需要设置', style: TextStyle( color: status.keepAliveRunning || status.recentsProtectionActive ? AppTheme.primaryDeep : AppTheme.orange, fontSize: 10, fontWeight: FontWeight.w700, ), ), ], ), const SizedBox(height: 10), Text( status.recentsProtectionActive ? 'OriginOS 最近任务保护已开启。记之不会显示在最近任务中,请从桌面图标重新打开;关闭全部识别后会自动恢复任务卡。' : '无障碍和通知监听由 Android 系统持续绑定。请允许记之后台活动,' '${isVivo ? '并在 OriginOS 中开启自启动,' : ''}' '否则系统清理进程后可能暂时收不到支付事件。', style: TextStyle( color: context.jz.text2, fontSize: 11.5, height: 1.55, ), ), if (status.keepAliveNeedsRecovery) ...[ const SizedBox(height: 10), SizedBox( width: double.infinity, child: JzActionButton( label: starting ? '正在启动…' : '启动后台保护', secondary: true, icon: const Icon(Icons.shield_outlined, size: 18), onPressed: starting ? null : onStart, ), ), ], const SizedBox(height: 12), Row( children: [ Expanded( child: JzActionButton( label: '电池优化', secondary: true, onPressed: ScreenshotChannel.openBatteryOptimizationSettings, ), ), const SizedBox(width: 10), Expanded( child: JzActionButton( label: isVivo ? '自启动管理' : '后台设置', secondary: true, onPressed: ScreenshotChannel.openBackgroundStartupSettings, ), ), ], ), ], ), ); } } class _RecognitionCard extends StatelessWidget { final IconData icon; final String title; final String description; final bool authorized; final bool connected; final String? statusLabel; final Widget child; final VoidCallback? onOpenSettings; final VoidCallback? onRetry; final VoidCallback? onStartKeepAlive; final String? recoveryMessage; final String settingsLabel; const _RecognitionCard({ required this.icon, required this.title, required this.description, required this.authorized, required this.connected, required this.child, this.statusLabel, this.onOpenSettings, this.onRetry, this.onStartKeepAlive, this.recoveryMessage, this.settingsLabel = '前往系统设置', }); @override Widget build(BuildContext context) { final palette = context.jz; final color = connected ? AppTheme.primary : AppTheme.orange; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: palette.card, borderRadius: BorderRadius.circular(18), border: Border.all(color: palette.line), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: connected ? palette.primaryBackground : palette.warningBackground, borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: color, size: 21), ), const SizedBox(width: 11), Expanded( child: Text( title, style: TextStyle( color: palette.text, fontWeight: FontWeight.w800, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), decoration: BoxDecoration( color: connected ? palette.primaryBackground : palette.warningBackground, borderRadius: BorderRadius.circular(20), ), child: Text( statusLabel ?? (connected ? '已连接' : authorized ? '等待连接' : '未授权'), style: TextStyle( color: color, fontSize: 10, fontWeight: FontWeight.w700, ), ), ), ], ), const SizedBox(height: 10), Text( description, style: TextStyle(color: palette.text2, fontSize: 12, height: 1.55), ), if (recoveryMessage != null) ...[ const SizedBox(height: 9), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.info_outline_rounded, color: AppTheme.orange, size: 18, ), const SizedBox(width: 7), Expanded( child: Text( recoveryMessage!, style: TextStyle( color: palette.text2, fontSize: 11.5, height: 1.5, ), ), ), ], ), ], const SizedBox(height: 6), child, if (onOpenSettings != null || onRetry != null || onStartKeepAlive != null) ...[ const SizedBox(height: 6), Wrap( spacing: 6, runSpacing: 4, children: [ if (onRetry != null) TextButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('重新检测'), ), if (onStartKeepAlive != null) TextButton.icon( onPressed: onStartKeepAlive, icon: const Icon(Icons.shield_outlined, size: 18), label: const Text('启动后台保护'), ), if (onOpenSettings != null) TextButton.icon( onPressed: onOpenSettings, icon: const Icon(Icons.settings_outlined, size: 18), label: Text(settingsLabel), ), ], ), ], ], ), ); } } class _ErrorCard extends StatelessWidget { final String message; final VoidCallback onRetry; const _ErrorCard({required this.message, required this.onRetry}); @override Widget build(BuildContext context) { final palette = context.jz; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: palette.card, borderRadius: BorderRadius.circular(16), border: Border.all(color: palette.line), ), child: Column( children: [ Text(message, style: TextStyle(color: palette.text2)), const SizedBox(height: 10), JzActionButton(label: '重新读取', secondary: true, onPressed: onRetry), ], ), ); } }