refactor: remove mobile approval surface

This commit is contained in:
Haitao Pan 2026-05-24 12:15:54 +08:00
parent ffbb9cb9ba
commit 3160ab3eb5
19 changed files with 13 additions and 1881 deletions

View File

@ -1558,37 +1558,6 @@
}
]
},
{
"path": "lib/features/mobile/mobile_gateway_pairing_guide_page.dart",
"language": "dart",
"symbolCount": 3,
"symbols": [
{
"language": "dart",
"path": "lib/features/mobile/mobile_gateway_pairing_guide_page.dart",
"line": 13,
"kind": "class",
"name": "MobileGatewayPairingGuidePage",
"signature": "class MobileGatewayPairingGuidePage extends StatelessWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_gateway_pairing_guide_page.dart",
"line": 265,
"kind": "class",
"name": "MobileGatewayQrScannerPage",
"signature": "class MobileGatewayQrScannerPage extends StatefulWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_gateway_pairing_guide_page.dart",
"line": 361,
"kind": "top-level function",
"name": "resolveGatewaySetupCodeFromScan",
"signature": "String? resolveGatewaySetupCodeFromScan(String raw) {"
}
]
},
{
"path": "lib/features/mobile/mobile_shell.dart",
"language": "dart",
@ -1649,108 +1618,6 @@
}
]
},
{
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"language": "dart",
"symbolCount": 10,
"symbols": [
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 19,
"kind": "class",
"name": "MobileSafeSheetInternal",
"signature": "class MobileSafeSheetInternal extends StatelessWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 305,
"kind": "class",
"name": "MobileSafeSectionInternal",
"signature": "class MobileSafeSectionInternal extends StatelessWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 326,
"kind": "class",
"name": "MobileFactChipInternal",
"signature": "class MobileFactChipInternal extends StatelessWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 364,
"kind": "class",
"name": "MobileSafetyNoticeInternal",
"signature": "class MobileSafetyNoticeInternal extends StatelessWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 413,
"kind": "class",
"name": "MobilePendingApprovalCardInternal",
"signature": "class MobilePendingApprovalCardInternal extends StatelessWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 507,
"kind": "class",
"name": "MobilePairedDeviceCardInternal",
"signature": "class MobilePairedDeviceCardInternal extends StatelessWidget {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 599,
"kind": "top-level function",
"name": "confirmMobileActionInternal",
"signature": "Future<bool?> confirmMobileActionInternal( BuildContext context, { required String title, required String message, }) {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 625,
"kind": "top-level function",
"name": "mobileSecurePathLabelInternal",
"signature": "String mobileSecurePathLabelInternal({ required GatewayConnectionProfile profile, required GatewayConnectionSnapshot connection, }) {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 644,
"kind": "top-level function",
"name": "mobileTargetLabelInternal",
"signature": "String mobileTargetLabelInternal(AppController controller) {"
},
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_sheet.dart",
"line": 657,
"kind": "top-level function",
"name": "mobileRelativeTimeInternal",
"signature": "String mobileRelativeTimeInternal(int? timestampMs) {"
}
]
},
{
"path": "lib/features/mobile/mobile_shell_strip.dart",
"language": "dart",
"symbolCount": 1,
"symbols": [
{
"language": "dart",
"path": "lib/features/mobile/mobile_shell_strip.dart",
"line": 19,
"kind": "class",
"name": "MobileSafeStripInternal",
"signature": "class MobileSafeStripInternal extends StatelessWidget {"
}
]
},
{
"path": "lib/features/settings/settings_about_panel.dart",
"language": "dart",

View File

@ -551,17 +551,6 @@ _No extracted public top-level symbols._
| 66 | `top-level function` | `thinkingTooltipInternal` | `String thinkingTooltipInternal(String level) => appText( '推理强度: ${assistantThinkingLabelInternal(level)}', 'Reasoning: ${assistantThinkingLabelInternal(level)}', );` |
| 71 | `top-level function` | `skillOptionTooltipInternal` | `String skillOptionTooltipInternal(ComposerSkillOptionInternal option) {` |
### `lib/features/mobile/mobile_gateway_pairing_guide_page.dart`
- Language: `dart`
- Public symbols: `3`
| Line | Kind | Name | Signature |
| ---: | --- | --- | --- |
| 13 | `class` | `MobileGatewayPairingGuidePage` | `class MobileGatewayPairingGuidePage extends StatelessWidget {` |
| 265 | `class` | `MobileGatewayQrScannerPage` | `class MobileGatewayQrScannerPage extends StatefulWidget {` |
| 361 | `top-level function` | `resolveGatewaySetupCodeFromScan` | `String? resolveGatewaySetupCodeFromScan(String raw) {` |
### `lib/features/mobile/mobile_shell.dart`
- Language: `dart`
@ -590,33 +579,6 @@ _No extracted public top-level symbols._
| ---: | --- | --- | --- |
| 19 | `class` | `BottomPillNavInternal` | `class BottomPillNavInternal extends StatelessWidget {` |
### `lib/features/mobile/mobile_shell_sheet.dart`
- Language: `dart`
- Public symbols: `10`
| Line | Kind | Name | Signature |
| ---: | --- | --- | --- |
| 19 | `class` | `MobileSafeSheetInternal` | `class MobileSafeSheetInternal extends StatelessWidget {` |
| 305 | `class` | `MobileSafeSectionInternal` | `class MobileSafeSectionInternal extends StatelessWidget {` |
| 326 | `class` | `MobileFactChipInternal` | `class MobileFactChipInternal extends StatelessWidget {` |
| 364 | `class` | `MobileSafetyNoticeInternal` | `class MobileSafetyNoticeInternal extends StatelessWidget {` |
| 413 | `class` | `MobilePendingApprovalCardInternal` | `class MobilePendingApprovalCardInternal extends StatelessWidget {` |
| 507 | `class` | `MobilePairedDeviceCardInternal` | `class MobilePairedDeviceCardInternal extends StatelessWidget {` |
| 599 | `top-level function` | `confirmMobileActionInternal` | `Future<bool?> confirmMobileActionInternal( BuildContext context, { required String title, required String message, }) {` |
| 625 | `top-level function` | `mobileSecurePathLabelInternal` | `String mobileSecurePathLabelInternal({ required GatewayConnectionProfile profile, required GatewayConnectionSnapshot connection, }) {` |
| 644 | `top-level function` | `mobileTargetLabelInternal` | `String mobileTargetLabelInternal(AppController controller) {` |
| 657 | `top-level function` | `mobileRelativeTimeInternal` | `String mobileRelativeTimeInternal(int? timestampMs) {` |
### `lib/features/mobile/mobile_shell_strip.dart`
- Language: `dart`
- Public symbols: `1`
| Line | Kind | Name | Signature |
| ---: | --- | --- | --- |
| 19 | `class` | `MobileSafeStripInternal` | `class MobileSafeStripInternal extends StatelessWidget {` |
### `lib/features/settings/settings_about_panel.dart`
- Language: `dart`

View File

@ -153,7 +153,7 @@
- Source: `lib/features/mobile/mobile_shell_core.dart`
- Type: `class`
- Responsibility:
移动端的统一入口壳层,负责 tab 切换、pairing guide、setup code 连接流和 mobile-safe sheet
移动端的统一入口壳层,只负责 assistant/settings tab 切换与移动端页面容器
### Constructor Parameters
@ -165,9 +165,8 @@
| Method | Parameters | Returns | Meaning |
| --- | --- | --- | --- |
| `showConnectSheetInternal` | none | `void` | 打开 gateway connection detail |
| `openGatewaySetupCodeEntryInternal` | `{String? prefilledSetupCode}` | `Future<void>` | 进入 setup-code 输入流 |
| `connectWithScannedSetupCodeInternal` | `String setupCode` | `Future<void>` | 用扫码结果触发连接 |
| `selectTabInternal` | `MobileShellTab tab` | `void` | 在 assistant/settings 之间切换 |
| `buildCurrentPageInternal` | none | `Widget` | 构建当前移动端 page |
| `showPairingGuidePageFlowInternal` | none | `Future<void>` | 打开 pairing guide 页面 |
### Notes

View File

@ -129,7 +129,7 @@ Status: `Active`
- 移动端顶层 tab 只保留 `assistant`、`settings`
- 删除 `workspace / tasks / secrets` 顶层 tab
- 删除 `MobileWorkspaceLauncherInternal`
- 配对、Bridge connect、setup code 等流程保留为 `settings` detail flow 与 mobile-safe strip/sheet 能力,不再占独立 top-level surface
- 删除 iOS/移动端 Mobile-safe 审批、配对 guide、setup code 扫码入口gateway 相关配置继续收口到 `settings`
## Assistant

View File

@ -39,12 +39,11 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
next unless ['mobile_scanner', 'Pods-Runner', 'Pods-RunnerTests'].include?(target.name)
next unless ['Pods-Runner', 'Pods-RunnerTests'].include?(target.name)
target.build_configurations.each do |config|
# mobile_scanner and the generated Pods aggregate targets need to avoid
# arm64 on simulators here because MLImage ships a device-only object
# slice that fails when the Apple Silicon simulator tries to link it.
# The generated Pods aggregate targets need to avoid simulator slices
# that native dependencies do not ship consistently.
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386 armv7'
end
end

View File

@ -5,65 +5,16 @@ PODS:
- file_selector_ios (0.0.1):
- Flutter
- Flutter (1.0.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMLKit/BarcodeScanning (7.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 6.0.0)
- GoogleMLKit/MLKitCore (7.0.0):
- MLKitCommon (~> 12.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (3.5.0)
- integration_test (0.0.1):
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- MLImage (1.0.0-beta6)
- MLKitBarcodeScanning (6.0.0):
- MLKitCommon (~> 12.0)
- MLKitVision (~> 8.0)
- MLKitCommon (12.0.0):
- GoogleDataTransport (~> 10.0)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/Logger (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (8.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta6)
- MLKitCommon (~> 12.0)
- mobile_scanner (6.0.2):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- patrol (0.0.1):
- CocoaAsyncSocket (~> 7.6)
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@ -76,7 +27,6 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- patrol (from `.symlinks/plugins/patrol/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@ -85,17 +35,6 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- CocoaAsyncSocket
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
device_info_plus:
@ -108,8 +47,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios"
irondash_engine_context:
:path: ".symlinks/plugins/irondash_engine_context/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
patrol:
@ -124,25 +61,13 @@ SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
PODFILE CHECKSUM: 612706ed53555a28e529d2b87297b10550c80134
PODFILE CHECKSUM: 5ab2a375a52a76f419425b2b219d2743259d6f1f
COCOAPODS: 1.16.2

View File

@ -203,7 +203,6 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DD975FF86A4295D90BA173B0 /* [CP] Copy Pods Resources */,
F190BC0EA83544A81E2F67D3 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
@ -330,23 +329,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DD975FF86A4295D90BA173B0 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
E912C8AAAC64F492FD612899 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;

View File

@ -1,551 +0,0 @@
import 'dart:convert';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import '../../i18n/app_language.dart';
import '../../runtime/gateway_runtime.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
class MobileGatewayPairingGuidePage extends StatelessWidget {
const MobileGatewayPairingGuidePage({
super.key,
required this.supportsQrScan,
required this.onManualInput,
required this.onManualCodeInput,
required this.onScannedSetupCode,
});
final bool supportsQrScan;
final VoidCallback onManualInput;
final VoidCallback onManualCodeInput;
final Future<void> Function(String setupCode) onScannedSetupCode;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
return Scaffold(
backgroundColor: const Color(0xFFF3F1EF),
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 0),
child: Row(
children: [
_HeaderCircleButton(
key: const ValueKey('pairing-guide-close-button'),
icon: Icons.close_rounded,
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: Center(
child: Text(
'配对网关',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 56),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 28),
child: Column(
children: [
const SizedBox(height: 12),
Container(
width: 118,
height: 118,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 24,
offset: const Offset(0, 12),
),
],
border: Border.all(
color: Colors.black.withValues(alpha: 0.08),
),
),
alignment: Alignment.center,
child: Icon(
Icons.hub_outlined,
size: 56,
color: palette.textPrimary,
),
),
const SizedBox(height: 26),
Text(
'配对你的 OpenClaw 主机',
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 12),
Text(
'在 Mac、Windows 或云端部署的 OpenClaw 主机上安装 xworkmate然后生成配对二维码或配置码。',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: palette.textSecondary,
height: 1.35,
),
),
const SizedBox(height: 24),
_GuideCard(
key: const ValueKey('pairing-guide-install-card'),
title: '自主安装',
subtitle: '按下面两步在主机上安装 XWorkmate CLI然后生成配对码。',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'1. 安装',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 10),
_CommandBlock(
key: const ValueKey(
'pairing-guide-install-command',
),
command: 'npm install -g xworkmate',
),
const SizedBox(height: 16),
Text(
'2. 配对',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 10),
_CommandBlock(
key: const ValueKey('pairing-guide-pair-command'),
command: 'xworkmate pair',
),
],
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
key: const ValueKey('pairing-guide-scan-button'),
onPressed: () async {
if (!supportsQrScan) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
appText(
'Android 扫码即将支持,当前请先使用手动输入代码。',
'Android QR scanning is coming soon. Use manual code entry for now.',
),
),
),
);
return;
}
final result = await Navigator.of(context)
.push<String>(
MaterialPageRoute<String>(
fullscreenDialog: true,
builder: (_) =>
const MobileGatewayQrScannerPage(),
),
);
if (result == null || !context.mounted) {
return;
}
Navigator.of(context).pop();
await onScannedSetupCode(result);
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18),
backgroundColor: const Color(0xFF151517),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppRadius.button,
),
),
),
child: Text(
'扫描二维码',
style: theme.textTheme.titleMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton(
key: const ValueKey('pairing-guide-manual-code-button'),
onPressed: () {
Navigator.of(context).pop();
onManualCodeInput();
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18),
backgroundColor: Colors.white,
foregroundColor: palette.textPrimary,
side: BorderSide(
color: Colors.black.withValues(alpha: 0.08),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppRadius.button,
),
),
),
child: Text(
'输入配置码',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton(
key: const ValueKey('pairing-guide-manual-button'),
onPressed: () {
Navigator.of(context).pop();
onManualInput();
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 18),
backgroundColor: Colors.white,
foregroundColor: palette.textPrimary,
side: BorderSide(
color: Colors.black.withValues(alpha: 0.08),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppRadius.button,
),
),
),
child: Text(
'手动输入代码',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
),
),
),
],
),
),
),
],
),
),
);
}
}
class MobileGatewayQrScannerPage extends StatefulWidget {
const MobileGatewayQrScannerPage({super.key});
@override
State<MobileGatewayQrScannerPage> createState() =>
_MobileGatewayQrScannerPageState();
}
class _MobileGatewayQrScannerPageState
extends State<MobileGatewayQrScannerPage> {
bool _hasHandledDetection = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned.fill(
child: _QrScannerSurface(onCodeDetected: _handleDetectedCode),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_HeaderCircleButton(
key: const ValueKey('pairing-scanner-close-button'),
icon: Icons.close_rounded,
onPressed: () => Navigator.of(context).pop(),
foregroundColor: Colors.white,
backgroundColor: Colors.black.withValues(alpha: 0.28),
),
const Spacer(),
Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(AppRadius.dialog),
border: Border.all(
color: Colors.white.withValues(alpha: 0.12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'扫描配对二维码',
style: theme.textTheme.titleLarge?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 8),
Text(
'将二维码放入取景框内。扫描成功后会自动把配置码带入 Gateway 设置页。',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.white.withValues(alpha: 0.82),
height: 1.35,
),
),
],
),
),
],
),
),
),
],
),
);
}
void _handleDetectedCode(String raw) {
if (_hasHandledDetection) {
return;
}
final setupCode = resolveGatewaySetupCodeFromScan(raw);
if (setupCode == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
appText('未识别到有效配置码,请重试。', 'No valid setup code found. Try again.'),
),
),
);
return;
}
_hasHandledDetection = true;
Navigator.of(context).pop(setupCode);
}
}
String? resolveGatewaySetupCodeFromScan(String raw) {
final trimmed = raw.trim();
if (trimmed.isEmpty) {
return null;
}
final candidate = _extractSetupCodeFromJsonPayload(trimmed) ?? trimmed;
if (decodeGatewaySetupCode(candidate) != null) {
return candidate;
}
return null;
}
String? _extractSetupCodeFromJsonPayload(String raw) {
final normalized = raw.trim();
if (!normalized.startsWith('{')) {
return null;
}
try {
final dynamic decoded = jsonDecode(normalized);
if (decoded is! Map<String, dynamic>) {
return null;
}
final setupCode = decoded['setupCode'];
if (setupCode is! String || setupCode.trim().isEmpty) {
return null;
}
return setupCode.trim();
} catch (_) {
return null;
}
}
class _GuideCard extends StatelessWidget {
const _GuideCard({
super.key,
required this.title,
required this.subtitle,
required this.child,
});
final String title;
final String subtitle;
final Widget child;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(28),
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 6),
Text(subtitle, style: theme.textTheme.bodyLarge),
const SizedBox(height: 18),
child,
],
),
);
}
}
class _CommandBlock extends StatelessWidget {
const _CommandBlock({super.key, required this.command});
final String command;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
return Container(
padding: const EdgeInsets.fromLTRB(18, 14, 14, 14),
decoration: BoxDecoration(
color: const Color(0xFFF8F6F4),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
),
child: Row(
children: [
Expanded(
child: SelectableText(
command,
style: theme.textTheme.titleMedium?.copyWith(
color: palette.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 12),
IconButton(
onPressed: () async {
await Clipboard.setData(ClipboardData(text: command));
if (!context.mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(appText('已复制命令。', 'Command copied.'))),
);
},
icon: const Icon(Icons.content_copy_rounded),
tooltip: appText('复制命令', 'Copy command'),
),
],
),
);
}
}
class _HeaderCircleButton extends StatelessWidget {
const _HeaderCircleButton({
super.key,
required this.icon,
required this.onPressed,
this.foregroundColor,
this.backgroundColor,
});
final IconData icon;
final VoidCallback onPressed;
final Color? foregroundColor;
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
final palette = context.palette;
return SizedBox(
width: 56,
height: 56,
child: DecoratedBox(
decoration: BoxDecoration(
color: backgroundColor ?? Colors.white.withValues(alpha: 0.9),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: IconButton(
onPressed: onPressed,
icon: Icon(icon),
color: foregroundColor ?? palette.textPrimary,
),
),
);
}
}
class _QrScannerSurface extends StatelessWidget {
const _QrScannerSurface({required this.onCodeDetected});
final ValueChanged<String> onCodeDetected;
@override
Widget build(BuildContext context) {
return MobileScanner(
key: const ValueKey('pairing-guide-ios-scanner'),
onDetect: (capture) {
final code = capture.barcodes
.map((item) => item.rawValue?.trim() ?? '')
.firstWhere((item) => item.isNotEmpty, orElse: () => '');
if (code.isEmpty) {
return;
}
onCodeDetected(code);
},
);
}
}

View File

@ -1,4 +1,2 @@
export 'mobile_shell_core.dart';
export 'mobile_shell_strip.dart';
export 'mobile_shell_sheet.dart';
export 'mobile_shell_nav.dart';

View File

@ -1,7 +1,3 @@
// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
@ -9,14 +5,10 @@ import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_page_registry.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/detail_drawer.dart';
import 'mobile_gateway_pairing_guide_page.dart';
import 'mobile_shell_nav.dart';
import 'mobile_shell_sheet.dart';
import 'mobile_shell_strip.dart';
enum MobileShellTab { assistant, settings }
@ -64,152 +56,6 @@ class MobileShellStateInternal extends State<MobileShell> {
widget.controller.openDetail(detail);
}
void prefetchMobileSafeStateInternal() {
if (!widget.controller.runtime.isConnected) {
return;
}
unawaited(widget.controller.refreshGatewayHealth());
unawaited(widget.controller.refreshDevices(quiet: true));
}
void showConnectSheetInternal() {
widget.controller.openSettings(tab: SettingsTab.gateway);
}
Future<void> openGatewaySetupCodeEntryInternal({
String? prefilledSetupCode,
}) async {
final setupCode = prefilledSetupCode?.trim() ?? '';
if (setupCode.isEmpty) {
await promptBridgeVerificationCodeInternal();
return;
}
await widget.controller.connectWithSetupCode(setupCode: setupCode);
}
Future<void> connectWithScannedSetupCodeInternal(String setupCode) async {
final messenger = ScaffoldMessenger.maybeOf(context);
try {
await widget.controller.connectWithSetupCode(setupCode: setupCode);
if (!mounted) {
return;
}
prefetchMobileSafeStateInternal();
messenger?.showSnackBar(
SnackBar(
content: Text(
appText(
'已写入配置码并开始连接 xworkmate-bridge。',
'Setup code applied and xworkmate-bridge connection started.',
),
),
),
);
} catch (error) {
if (!mounted) {
return;
}
final message = error.toString().trim();
messenger?.showSnackBar(
SnackBar(
content: Text(
appText(
'扫码成功,但自动连接失败。请重新输入配置码或检查 Bridge 状态。\n$message',
'QR captured, but automatic connect failed. Re-enter the setup code or check Bridge status.\n$message',
),
),
),
);
}
}
Future<void> promptBridgeVerificationCodeInternal() async {
final codeController = TextEditingController();
final enteredCode = await showDialog<String>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(appText('输入配置码', 'Enter Setup Code')),
content: TextField(
controller: codeController,
autofocus: true,
textCapitalization: TextCapitalization.characters,
decoration: InputDecoration(
labelText: appText('配置码', 'Setup Code'),
hintText: appText('粘贴配置码', 'Paste setup code'),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(appText('取消', 'Cancel')),
),
FilledButton(
onPressed: () =>
Navigator.of(dialogContext).pop(codeController.text.trim()),
child: Text(appText('连接', 'Connect')),
),
],
);
},
);
codeController.dispose();
final resolved = enteredCode?.trim() ?? '';
if (resolved.isEmpty || !mounted) {
return;
}
await connectWithScannedSetupCodeInternal(resolved);
}
void showPairingGuidePageInternal() {
unawaited(showPairingGuidePageFlowInternal());
}
Future<void> showPairingGuidePageFlowInternal() async {
final supportsQrScan = Theme.of(context).platform == TargetPlatform.iOS;
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
fullscreenDialog: true,
builder: (_) => MobileGatewayPairingGuidePage(
supportsQrScan: supportsQrScan,
onManualInput: () =>
unawaited(promptBridgeVerificationCodeInternal()),
onManualCodeInput: () =>
unawaited(promptBridgeVerificationCodeInternal()),
onScannedSetupCode: (setupCode) async {
await connectWithScannedSetupCodeInternal(setupCode);
},
),
),
);
}
void showMobileSafeSheetInternal() {
prefetchMobileSafeStateInternal();
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
return FractionallySizedBox(
heightFactor: 0.94,
child: MobileSafeSheetInternal(
controller: widget.controller,
onClose: () => Navigator.of(sheetContext).pop(),
onOpenGatewayConnect: () {
Navigator.of(sheetContext).pop();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
showPairingGuidePageInternal();
}
});
},
),
);
},
);
}
Widget buildCurrentPageInternal() {
return buildWorkspacePage(
destination: widget.controller.destination,
@ -253,12 +99,6 @@ class MobileShellStateInternal extends State<MobileShell> {
padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
child: Column(
children: [
MobileSafeStripInternal(
controller: widget.controller,
onOpenSafeSheet: showMobileSafeSheetInternal,
onOpenGatewayConnect: showPairingGuidePageInternal,
),
const SizedBox(height: 10),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(

View File

@ -1,20 +1,7 @@
// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_page_registry.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/detail_drawer.dart';
import 'mobile_gateway_pairing_guide_page.dart';
import 'mobile_shell_core.dart';
import 'mobile_shell_strip.dart';
import 'mobile_shell_sheet.dart';
class BottomPillNavInternal extends StatelessWidget {
const BottomPillNavInternal({

View File

@ -1,674 +0,0 @@
// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_page_registry.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/detail_drawer.dart';
import 'mobile_gateway_pairing_guide_page.dart';
import 'mobile_shell_core.dart';
import 'mobile_shell_strip.dart';
import 'mobile_shell_nav.dart';
class MobileSafeSheetInternal extends StatelessWidget {
const MobileSafeSheetInternal({
super.key,
required this.controller,
required this.onClose,
required this.onOpenGatewayConnect,
});
final AppController controller;
final VoidCallback onClose;
final VoidCallback onOpenGatewayConnect;
@override
Widget build(BuildContext context) {
final palette = context.palette;
return Material(
color: Colors.transparent,
child: Container(
key: const ValueKey('mobile-safe-sheet'),
margin: const EdgeInsets.fromLTRB(12, 12, 12, 12),
decoration: BoxDecoration(
color: palette.surfacePrimary.withValues(alpha: 0.98),
borderRadius: BorderRadius.circular(AppRadius.dialog + 2),
border: Border.all(color: palette.strokeSoft),
boxShadow: [palette.chromeShadowAmbient],
),
child: SafeArea(
top: false,
child: AnimatedBuilder(
animation: controller,
builder: (context, _) {
final theme = Theme.of(context);
final connection = controller.connection;
final devices = controller.devices;
final hasPendingRun =
controller.hasAssistantPendingRun ||
controller.activeRunId != null;
final securePathLabel = mobileSecurePathLabelInternal(
profile: controller.settings.primaryGatewayProfile,
connection: connection,
);
final localDeviceLabel =
connection.deviceId ?? appText('未初始化', 'Not initialized');
final devicesError = controller.devicesController.error;
Future<void> handleConnect() async {
if (controller.canQuickConnectGateway) {
await controller.connectSavedGateway();
await controller.refreshDevices(quiet: true);
return;
}
onOpenGatewayConnect();
}
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mobile-safe',
style: theme.textTheme.headlineSmall?.copyWith(
color: palette.textPrimary,
),
),
const SizedBox(height: 6),
Text(
appText(
'移动端只提供结构化审批、配对管理和运行保护动作,不暴露全局 shell 放权。',
'Mobile only exposes structured approvals, pairing controls, and run-safe actions. No global shell approvals.',
),
style: theme.textTheme.bodyMedium?.copyWith(
color: palette.textSecondary,
),
),
],
),
),
const SizedBox(width: 12),
IconButton(
onPressed: onClose,
icon: const Icon(Icons.close_rounded),
),
],
),
const SizedBox(height: 16),
MobileSafeSectionInternal(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('安全直连', 'Secure Direct'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
MobileFactChipInternal(
icon: Icons.lock_outline_rounded,
label: securePathLabel,
color: palette.accent,
background: palette.accentMuted,
),
MobileFactChipInternal(
icon: Icons.monitor_heart_outlined,
label: connection.status.label,
color:
connection.status ==
RuntimeConnectionStatus.connected
? palette.success
: palette.textSecondary,
background:
connection.status ==
RuntimeConnectionStatus.connected
? palette.success.withValues(alpha: 0.14)
: palette.surfaceSecondary,
),
],
),
const SizedBox(height: 10),
Text(
mobileTargetLabelInternal(controller),
style: theme.textTheme.titleSmall?.copyWith(
color: palette.textPrimary,
),
),
const SizedBox(height: 4),
Text(
appText(
'本机设备 ID$localDeviceLabel',
'Local device ID: $localDeviceLabel',
),
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (controller.runtime.isConnected) ...[
OutlinedButton(
onPressed: () async {
await controller.refreshGatewayHealth();
await controller.refreshDevices(
quiet: true,
);
},
child: Text(appText('刷新', 'Refresh')),
),
OutlinedButton(
onPressed: controller.disconnectGateway,
child: Text(appText('断开', 'Disconnect')),
),
] else
FilledButton(
key: const ValueKey(
'mobile-safe-sheet-connect-button',
),
onPressed: () => unawaited(handleConnect()),
child: Text(
controller.canQuickConnectGateway
? appText('快速连接', 'Quick Connect')
: appText('配对网关', 'Pair Gateway'),
),
),
if (hasPendingRun)
FilledButton.tonal(
onPressed: controller.abortRun,
child: Text(appText('停止运行', 'Stop Run')),
),
],
),
],
),
),
if (connection.gatewayTokenMissing) ...[
const SizedBox(height: 12),
MobileSafetyNoticeInternal(
tone: palette.danger.withValues(alpha: 0.1),
borderColor: palette.danger.withValues(alpha: 0.2),
icon: Icons.key_off_outlined,
title: appText('缺少共享 Token', 'Shared Token Missing'),
message: appText(
'首次连接需要共享 Token配对完成后可继续使用 device token。',
'The first connection needs a shared token; after pairing, the device token can continue.',
),
),
],
if ((devicesError ?? '').isNotEmpty) ...[
const SizedBox(height: 12),
MobileSafetyNoticeInternal(
tone: palette.danger.withValues(alpha: 0.1),
borderColor: palette.danger.withValues(alpha: 0.2),
icon: Icons.error_outline_rounded,
title: appText('设备列表错误', 'Devices Error'),
message: devicesError!,
),
],
const SizedBox(height: 18),
Text(
appText('待审批请求', 'Pending Requests'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
if (!controller.runtime.isConnected)
Text(
appText(
'恢复 xworkmate-bridge 连接后加载待审批设备与已配对设备。',
'Pending and paired devices load again after xworkmate-bridge reconnects.',
),
style: theme.textTheme.bodyMedium,
)
else if (devices.pending.isEmpty)
Text(
appText('当前没有待审批设备。', 'No pending pairing requests.'),
style: theme.textTheme.bodyMedium,
)
else
Column(
key: const ValueKey('mobile-safe-pending-section'),
children: devices.pending
.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: MobilePendingApprovalCardInternal(
controller: controller,
item: item,
),
),
)
.toList(),
),
const SizedBox(height: 18),
Text(
appText('已配对设备', 'Paired Devices'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
if (!controller.runtime.isConnected)
Text(
appText(
'恢复 xworkmate-bridge 连接后可查看 paired device并在移动端直接吊销。',
'Paired devices are visible again after xworkmate-bridge reconnects, and can be revoked from mobile.',
),
style: theme.textTheme.bodyMedium,
)
else if (devices.paired.isEmpty)
Text(
appText('当前没有已配对设备。', 'No paired devices yet.'),
style: theme.textTheme.bodyMedium,
)
else
Column(
key: const ValueKey('mobile-safe-paired-section'),
children: devices.paired
.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: MobilePairedDeviceCardInternal(
controller: controller,
item: item,
),
),
)
.toList(),
),
],
),
);
},
),
),
),
);
}
}
class MobileSafeSectionInternal extends StatelessWidget {
const MobileSafeSectionInternal({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
final palette = context.palette;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: palette.surfaceSecondary.withValues(alpha: 0.78),
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: palette.strokeSoft),
),
child: child,
);
}
}
class MobileFactChipInternal extends StatelessWidget {
const MobileFactChipInternal({
super.key,
required this.icon,
required this.label,
required this.color,
required this.background,
});
final IconData icon;
final String label;
final Color color;
final Color background;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(AppRadius.chip),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 6),
Text(
label,
style: theme.textTheme.labelMedium?.copyWith(color: color),
),
],
),
);
}
}
class MobileSafetyNoticeInternal extends StatelessWidget {
const MobileSafetyNoticeInternal({
super.key,
required this.tone,
required this.borderColor,
required this.icon,
required this.title,
required this.message,
});
final Color tone;
final Color borderColor;
final IconData icon;
final String title;
final String message;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: tone,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: borderColor),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: palette.textPrimary),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleSmall),
const SizedBox(height: 4),
Text(message, style: theme.textTheme.bodySmall),
],
),
),
],
),
);
}
}
class MobilePendingApprovalCardInternal extends StatelessWidget {
const MobilePendingApprovalCardInternal({
super.key,
required this.controller,
required this.item,
});
final AppController controller;
final GatewayPendingDevice item;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
final metadata = <String>[
if ((item.role ?? '').isNotEmpty) 'role: ${item.role}',
if (item.scopes.isNotEmpty) item.scopes.join(', '),
if ((item.remoteIp ?? '').isNotEmpty) item.remoteIp!,
mobileRelativeTimeInternal(item.requestedAtMs),
if (item.isRepair) appText('修复请求', 'repair'),
];
return MobileSafeSectionInternal(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.label, style: theme.textTheme.titleSmall),
const SizedBox(height: 4),
Text(
item.deviceId,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
],
),
),
if (item.isRepair)
MobileFactChipInternal(
icon: Icons.build_circle_outlined,
label: appText('修复', 'Repair'),
color: palette.warning,
background: palette.warning.withValues(alpha: 0.12),
),
],
),
const SizedBox(height: 8),
Text(
metadata.join(' · '),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textSecondary,
),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.tonal(
onPressed: () =>
controller.approveDevicePairing(item.requestId),
child: Text(appText('批准配对', 'Approve Pairing')),
),
OutlinedButton(
onPressed: () async {
final confirmed = await confirmMobileActionInternal(
context,
title: appText('拒绝配对请求', 'Reject Pairing Request'),
message: appText(
'确定拒绝 ${item.label} 的配对请求吗?',
'Reject the pairing request from ${item.label}?',
),
);
if (confirmed == true) {
await controller.rejectDevicePairing(item.requestId);
}
},
child: Text(appText('拒绝配对', 'Reject Pairing')),
),
],
),
],
),
);
}
}
class MobilePairedDeviceCardInternal extends StatelessWidget {
const MobilePairedDeviceCardInternal({
super.key,
required this.controller,
required this.item,
});
final AppController controller;
final GatewayPairedDevice item;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
final metadata = <String>[
if (item.roles.isNotEmpty) 'roles: ${item.roles.join(', ')}',
if (item.scopes.isNotEmpty) 'scopes: ${item.scopes.join(', ')}',
if ((item.remoteIp ?? '').isNotEmpty) item.remoteIp!,
if (item.currentDevice) appText('当前设备', 'current device'),
];
return MobileSafeSectionInternal(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.label, style: theme.textTheme.titleSmall),
const SizedBox(height: 4),
Text(
item.deviceId,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
],
),
),
if (item.currentDevice)
MobileFactChipInternal(
icon: Icons.smartphone_outlined,
label: appText('当前设备', 'Current'),
color: palette.success,
background: palette.success.withValues(alpha: 0.12),
),
],
),
const SizedBox(height: 8),
Text(
metadata.join(' · '),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textSecondary,
),
),
if (item.tokens.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
appText(
'角色令牌:${item.tokens.first.role}',
'Role token: ${item.tokens.first.role}',
),
style: theme.textTheme.bodySmall,
),
],
const SizedBox(height: 10),
OutlinedButton(
onPressed: () async {
final confirmed = await confirmMobileActionInternal(
context,
title: appText('吊销已配对设备', 'Revoke Paired Device'),
message: appText(
'确定吊销 ${item.label} 吗?该设备之后需要重新配对。',
'Revoke ${item.label}? The device will need pairing again.',
),
);
if (confirmed == true) {
await controller.removePairedDevice(item.deviceId);
}
},
child: Text(appText('吊销设备', 'Revoke Device')),
),
],
),
);
}
}
Future<bool?> confirmMobileActionInternal(
BuildContext context, {
required String title,
required String message,
}) {
return showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(appText('取消', 'Cancel')),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(appText('确认', 'Confirm')),
),
],
);
},
);
}
String mobileSecurePathLabelInternal({
required GatewayConnectionProfile profile,
required GatewayConnectionSnapshot connection,
}) {
final mode = connection.mode == RuntimeConnectionMode.unconfigured
? profile.mode
: connection.mode;
return switch (mode) {
RuntimeConnectionMode.remote =>
profile.tls
? appText('Secure Direct TLS', 'Secure Direct TLS')
: appText('Remote Non-TLS', 'Remote Non-TLS'),
RuntimeConnectionMode.unconfigured => appText(
'Gateway 未配置',
'Gateway Not Configured',
),
};
}
String mobileTargetLabelInternal(AppController controller) {
final connection = controller.connection;
if ((connection.remoteAddress ?? '').isNotEmpty) {
return connection.remoteAddress!;
}
final profile = controller.settings.primaryGatewayProfile;
final host = profile.host.trim();
if (host.isNotEmpty && profile.port > 0) {
return '$host:${profile.port}';
}
return appText('未连接目标', 'No target');
}
String mobileRelativeTimeInternal(int? timestampMs) {
if (timestampMs == null || timestampMs <= 0) {
return appText('刚刚', 'just now');
}
final delta = DateTime.now().difference(
DateTime.fromMillisecondsSinceEpoch(timestampMs),
);
if (delta.inMinutes < 1) {
return appText('刚刚', 'just now');
}
if (delta.inHours < 1) {
return appText('${delta.inMinutes} 分钟前', '${delta.inMinutes}m ago');
}
if (delta.inDays < 1) {
return appText('${delta.inHours} 小时前', '${delta.inHours}h ago');
}
return appText('${delta.inDays} 天前', '${delta.inDays}d ago');
}

View File

@ -1,188 +0,0 @@
// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_page_registry.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/runtime_models.dart';
import '../../theme/app_palette.dart';
import '../../theme/app_theme.dart';
import '../../widgets/detail_drawer.dart';
import 'mobile_gateway_pairing_guide_page.dart';
import 'mobile_shell_core.dart';
import 'mobile_shell_sheet.dart';
import 'mobile_shell_nav.dart';
class MobileSafeStripInternal extends StatelessWidget {
const MobileSafeStripInternal({
super.key,
required this.controller,
required this.onOpenSafeSheet,
required this.onOpenGatewayConnect,
});
final AppController controller;
final VoidCallback onOpenSafeSheet;
final VoidCallback onOpenGatewayConnect;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
final connection = controller.connection;
final devices = controller.devices;
final hasPendingRun =
controller.hasAssistantPendingRun || controller.activeRunId != null;
final securePathLabel = mobileSecurePathLabelInternal(
profile: controller.settings.primaryGatewayProfile,
connection: connection,
);
Future<void> handlePrimaryConnect() async {
if (controller.canQuickConnectGateway) {
await controller.connectSavedGateway();
await controller.refreshDevices(quiet: true);
return;
}
onOpenGatewayConnect();
}
return Container(
key: const ValueKey('mobile-safe-strip'),
width: double.infinity,
padding: const EdgeInsets.fromLTRB(14, 14, 14, 12),
decoration: BoxDecoration(
color: palette.surfacePrimary.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(AppRadius.dialog),
border: Border.all(color: palette.strokeSoft),
boxShadow: [palette.chromeShadowAmbient],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Mobile-safe',
style: theme.textTheme.titleLarge?.copyWith(
color: palette.textPrimary,
),
),
const SizedBox(height: 4),
Text(
appText(
'结构化审批、配对和安全运行入口',
'Structured approvals, pairing, and run-safe controls',
),
style: theme.textTheme.bodySmall?.copyWith(
color: palette.textSecondary,
),
),
],
),
),
const SizedBox(width: 10),
MobileFactChipInternal(
icon: connection.status == RuntimeConnectionStatus.connected
? Icons.verified_outlined
: Icons.shield_outlined,
label: connection.status.label,
color: connection.status == RuntimeConnectionStatus.connected
? palette.success
: palette.textSecondary,
background:
connection.status == RuntimeConnectionStatus.connected
? palette.success.withValues(alpha: 0.14)
: palette.surfaceSecondary,
),
],
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
MobileFactChipInternal(
icon: Icons.lock_outline_rounded,
label: securePathLabel,
color: palette.accent,
background: palette.accentMuted,
),
MobileFactChipInternal(
icon: Icons.computer_outlined,
label: mobileTargetLabelInternal(controller),
color: palette.textPrimary,
background: palette.surfaceSecondary,
),
if (devices.pending.isNotEmpty)
MobileFactChipInternal(
icon: Icons.approval_outlined,
label: appText(
'${devices.pending.length} 个待审批',
'${devices.pending.length} pending',
),
color: palette.warning,
background: palette.warning.withValues(alpha: 0.12),
),
if (devices.paired.isNotEmpty)
MobileFactChipInternal(
icon: Icons.devices_outlined,
label: appText(
'${devices.paired.length} 台已配对',
'${devices.paired.length} paired',
),
color: palette.success,
background: palette.success.withValues(alpha: 0.12),
),
],
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.tonal(
key: const ValueKey('mobile-safe-open-button'),
onPressed: onOpenSafeSheet,
child: Text(appText('安全审批', 'Mobile-safe')),
),
if (controller.runtime.isConnected)
OutlinedButton(
key: const ValueKey('mobile-safe-refresh-button'),
onPressed: () async {
await controller.refreshGatewayHealth();
await controller.refreshDevices(quiet: true);
},
child: Text(appText('刷新', 'Refresh')),
)
else
FilledButton(
key: const ValueKey('mobile-safe-connect-button'),
onPressed: () => unawaited(handlePrimaryConnect()),
child: Text(
controller.canQuickConnectGateway
? appText('快速连接', 'Quick Connect')
: appText('配对网关', 'Pair Gateway'),
),
),
if (hasPendingRun)
OutlinedButton(
key: const ValueKey('mobile-safe-stop-run-button'),
onPressed: controller.abortRun,
child: Text(appText('停止运行', 'Stop Run')),
),
],
),
],
),
);
}
}

View File

@ -8,7 +8,6 @@ import Foundation
import device_info_plus
import file_selector_macos
import irondash_engine_context
import mobile_scanner
import package_info_plus
import patrol
import shared_preferences_foundation
@ -18,7 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PatrolPlugin.register(with: registry.registrar(forPlugin: "PatrolPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@ -94,7 +94,7 @@ post_install do |installer|
target.build_configurations.each do |config|
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '11.5'
next unless ['mobile_scanner', 'patrol', 'CocoaAsyncSocket', 'Pods-Runner', 'Pods-RunnerTests'].include?(target.name)
next unless ['patrol', 'CocoaAsyncSocket', 'Pods-Runner', 'Pods-RunnerTests'].include?(target.name)
append_ignored_attributes_suppression.call(config.build_settings)
append_deprecation_suppression.call(config.build_settings)

View File

@ -7,8 +7,6 @@ PODS:
- FlutterMacOS (1.0.0)
- irondash_engine_context (0.0.1):
- FlutterMacOS
- mobile_scanner (6.0.2):
- FlutterMacOS
- package_info_plus (0.0.1):
- FlutterMacOS
- patrol (0.0.1):
@ -26,7 +24,6 @@ DEPENDENCIES:
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`)
- mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- patrol (from `Flutter/ephemeral/.symlinks/plugins/patrol/darwin`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
@ -45,8 +42,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
irondash_engine_context:
:path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos
mobile_scanner:
:path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos
package_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
patrol:
@ -62,12 +57,11 @@ SPEC CHECKSUMS:
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
PODFILE CHECKSUM: 0a5e0e8e0ce2a1899d059024f709433be5b9c0e7
PODFILE CHECKSUM: ef2282d07ab509932defa9bc41c2af9516037afc
COCOAPODS: 1.16.2

View File

@ -436,14 +436,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173"
url: "https://pub.dev"
source: hosted
version: "6.0.11"
native_toolchain_c:
dependency: transitive
description:

View File

@ -23,7 +23,6 @@ dependencies:
flutter_markdown: ^0.7.7+1
http: ^1.5.0
markdown: ^7.3.0
mobile_scanner: ^6.0.7
package_info_plus: ^8.3.1
path_provider: ^2.1.5
shared_preferences: ^2.5.3

View File

@ -30,6 +30,9 @@ void main() {
expect(find.text('助手'), findsOneWidget);
expect(find.text('设置'), findsOneWidget);
expect(find.text('Mobile-safe'), findsNothing);
expect(find.text('安全审批'), findsNothing);
expect(find.byKey(const Key('mobile-safe-strip')), findsNothing);
expect(find.text('任务'), findsNothing);
expect(find.text('工作区'), findsNothing);
expect(find.text('密钥'), findsNothing);