refactor: remove mobile approval surface
This commit is contained in:
parent
ffbb9cb9ba
commit
3160ab3eb5
@ -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",
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,2 @@
|
||||
export 'mobile_shell_core.dart';
|
||||
export 'mobile_shell_strip.dart';
|
||||
export 'mobile_shell_sheet.dart';
|
||||
export 'mobile_shell_nav.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<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(
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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');
|
||||
}
|
||||
@ -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')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user