diff --git a/docs/architecture/public-api/_generated/public-symbol-inventory.json b/docs/architecture/public-api/_generated/public-symbol-inventory.json index 5907438c..a93fbb52 100644 --- a/docs/architecture/public-api/_generated/public-symbol-inventory.json +++ b/docs/architecture/public-api/_generated/public-symbol-inventory.json @@ -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 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", diff --git a/docs/architecture/public-api/_generated/public-symbol-inventory.md b/docs/architecture/public-api/_generated/public-symbol-inventory.md index dcfe1f15..fc71ba08 100644 --- a/docs/architecture/public-api/_generated/public-symbol-inventory.md +++ b/docs/architecture/public-api/_generated/public-symbol-inventory.md @@ -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 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` diff --git a/docs/architecture/public-api/feature-surfaces.md b/docs/architecture/public-api/feature-surfaces.md index 33dbb085..e99c10b2 100644 --- a/docs/architecture/public-api/feature-surfaces.md +++ b/docs/architecture/public-api/feature-surfaces.md @@ -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` | 进入 setup-code 输入流 | -| `connectWithScannedSetupCodeInternal` | `String setupCode` | `Future` | 用扫码结果触发连接 | +| `selectTabInternal` | `MobileShellTab tab` | `void` | 在 assistant/settings 之间切换 | +| `buildCurrentPageInternal` | none | `Widget` | 构建当前移动端 page | | `showPairingGuidePageFlowInternal` | none | `Future` | 打开 pairing guide 页面 | ### Notes diff --git a/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md b/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md index 1995f610..0903bfb1 100644 --- a/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md +++ b/docs/architecture/xworkmate-core-module-inventory-2026-04-13.md @@ -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 diff --git a/ios/Podfile b/ios/Podfile index 46e4e5b6..233398f4 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 573f7a01..6d8d91ee 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 328ac293..8d812b83 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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; diff --git a/lib/features/mobile/mobile_gateway_pairing_guide_page.dart b/lib/features/mobile/mobile_gateway_pairing_guide_page.dart deleted file mode 100644 index 4f14fb8a..00000000 --- a/lib/features/mobile/mobile_gateway_pairing_guide_page.dart +++ /dev/null @@ -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 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( - MaterialPageRoute( - 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 createState() => - _MobileGatewayQrScannerPageState(); -} - -class _MobileGatewayQrScannerPageState - extends State { - 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) { - 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 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); - }, - ); - } -} diff --git a/lib/features/mobile/mobile_shell.dart b/lib/features/mobile/mobile_shell.dart index c8bea1d9..ccea1b07 100644 --- a/lib/features/mobile/mobile_shell.dart +++ b/lib/features/mobile/mobile_shell.dart @@ -1,4 +1,2 @@ export 'mobile_shell_core.dart'; -export 'mobile_shell_strip.dart'; -export 'mobile_shell_sheet.dart'; export 'mobile_shell_nav.dart'; diff --git a/lib/features/mobile/mobile_shell_core.dart b/lib/features/mobile/mobile_shell_core.dart index 77d7e9fd..04635fe6 100644 --- a/lib/features/mobile/mobile_shell_core.dart +++ b/lib/features/mobile/mobile_shell_core.dart @@ -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 { 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 openGatewaySetupCodeEntryInternal({ - String? prefilledSetupCode, - }) async { - final setupCode = prefilledSetupCode?.trim() ?? ''; - if (setupCode.isEmpty) { - await promptBridgeVerificationCodeInternal(); - return; - } - await widget.controller.connectWithSetupCode(setupCode: setupCode); - } - - Future 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 promptBridgeVerificationCodeInternal() async { - final codeController = TextEditingController(); - final enteredCode = await showDialog( - 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 showPairingGuidePageFlowInternal() async { - final supportsQrScan = Theme.of(context).platform == TargetPlatform.iOS; - await Navigator.of(context).push( - MaterialPageRoute( - fullscreenDialog: true, - builder: (_) => MobileGatewayPairingGuidePage( - supportsQrScan: supportsQrScan, - onManualInput: () => - unawaited(promptBridgeVerificationCodeInternal()), - onManualCodeInput: () => - unawaited(promptBridgeVerificationCodeInternal()), - onScannedSetupCode: (setupCode) async { - await connectWithScannedSetupCodeInternal(setupCode); - }, - ), - ), - ); - } - - void showMobileSafeSheetInternal() { - prefetchMobileSafeStateInternal(); - showModalBottomSheet( - 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 { 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( diff --git a/lib/features/mobile/mobile_shell_nav.dart b/lib/features/mobile/mobile_shell_nav.dart index c9c9e60c..9eddee1d 100644 --- a/lib/features/mobile/mobile_shell_nav.dart +++ b/lib/features/mobile/mobile_shell_nav.dart @@ -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({ diff --git a/lib/features/mobile/mobile_shell_sheet.dart b/lib/features/mobile/mobile_shell_sheet.dart deleted file mode 100644 index 8c8d8517..00000000 --- a/lib/features/mobile/mobile_shell_sheet.dart +++ /dev/null @@ -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 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 = [ - 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 = [ - 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 confirmMobileActionInternal( - BuildContext context, { - required String title, - required String message, -}) { - return showDialog( - 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'); -} diff --git a/lib/features/mobile/mobile_shell_strip.dart b/lib/features/mobile/mobile_shell_strip.dart deleted file mode 100644 index 16ff9c61..00000000 --- a/lib/features/mobile/mobile_shell_strip.dart +++ /dev/null @@ -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 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')), - ), - ], - ), - ], - ), - ); - } -} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9991ad68..9fdc93fd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/macos/Podfile b/macos/Podfile index 4b863b7f..cfc4f733 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -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) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a114214b..cd2e4f29 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -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 diff --git a/pubspec.lock b/pubspec.lock index 8ffb18df..7f184593 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index ac1aa4de..02cdf7d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/features/app/app_shell_surface_test.dart b/test/features/app/app_shell_surface_test.dart index a0551ef7..a5d4fc88 100644 --- a/test/features/app/app_shell_surface_test.dart +++ b/test/features/app/app_shell_surface_test.dart @@ -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);