From b4b67163ae4f9fd3c56a379d5d0892e7cd697cda Mon Sep 17 00:00:00 2001 From: chaoszhu Date: Sat, 27 Jun 2026 22:40:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20native-=E4=BC=98=E5=8C=96=E7=BB=88?= =?UTF-8?q?=E7=AB=AFdocker=E7=BB=84=E4=BB=B6&=E5=85=B6=E4=BB=96=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docker/docker_session_manager.dart | 21 ++-- .../features/docker/docker_socket_client.dart | 27 +++--- native/lib/features/docker/docker_tab.dart | 96 ++++++++++++------- .../lib/features/shell/main_shell_page.dart | 2 +- native/lib/features/shell/sftp_tab.dart | 60 ++++++++++++ .../terminal/terminal_bottom_menu.dart | 4 + 6 files changed, 154 insertions(+), 56 deletions(-) diff --git a/native/lib/features/docker/docker_session_manager.dart b/native/lib/features/docker/docker_session_manager.dart index 92b7b03..71cdc64 100644 --- a/native/lib/features/docker/docker_session_manager.dart +++ b/native/lib/features/docker/docker_session_manager.dart @@ -67,10 +67,7 @@ class DockerSessionManager extends ChangeNotifier { _activeHostId = server.id; if (existing.status == DockerConnectionStatus.disconnected || existing.status == DockerConnectionStatus.error) { - existing.status = DockerConnectionStatus.connecting; - existing.loading = true; - existing.errorMessage = null; - notifyListeners(); + _markConnecting(existing); await existing.client.connect(); } else { notifyListeners(); @@ -137,6 +134,17 @@ class DockerSessionManager extends ChangeNotifier { notifyListeners(); } + void _markConnecting(DockerSessionState session) { + session.status = DockerConnectionStatus.connecting; + session.loading = true; + session.refreshing = false; + session.containers = const []; + session.logs = ''; + session.errorMessage = null; + session.operatingIds.clear(); + notifyListeners(); + } + void refreshActive() { final session = activeSession; _refreshSession(session); @@ -165,10 +173,7 @@ class DockerSessionManager extends ChangeNotifier { void _reconnectSession(DockerSessionState? session) { if (session == null) return; - session.status = DockerConnectionStatus.connecting; - session.loading = true; - session.errorMessage = null; - notifyListeners(); + _markConnecting(session); unawaited(session.client.connect()); } diff --git a/native/lib/features/docker/docker_socket_client.dart b/native/lib/features/docker/docker_socket_client.dart index dd1f152..393dc79 100644 --- a/native/lib/features/docker/docker_socket_client.dart +++ b/native/lib/features/docker/docker_socket_client.dart @@ -64,17 +64,22 @@ class DockerSocketClient { if (_disposed || _candidateIndex >= _serverCandidates.length) return; final serverAddress = _serverCandidates[_candidateIndex]; - final socket = sio.io( - serverAddress, - sio.OptionBuilder() - .setTransports(['websocket']) - .setPath('/docker/') - .setAuth({'token': _authSession.token}) - .setExtraHeaders({'Cookie': cookie, 'Origin': _authSession.serverAddress}) - .disableAutoConnect() - .disableReconnection() - .build(), - ); + final options = sio.OptionBuilder() + .setTransports(['websocket']) + .setPath('/docker/') + .setAuth({'token': _authSession.token}) + .setExtraHeaders({ + 'Cookie': cookie, + 'Origin': _authSession.serverAddress, + }) + .disableAutoConnect() + .disableReconnection() + .build(); + options['forceNew'] = true; + options['multiplex'] = false; + options['query'] = {'hostId': hostId}; + + final socket = sio.io(serverAddress, options); socket.onConnect((_) { _connectedController.add(true); diff --git a/native/lib/features/docker/docker_tab.dart b/native/lib/features/docker/docker_tab.dart index 7fc10a8..cc4b32d 100644 --- a/native/lib/features/docker/docker_tab.dart +++ b/native/lib/features/docker/docker_tab.dart @@ -137,12 +137,13 @@ class _DockerPanelState extends ConsumerState { return AnimatedBuilder( animation: manager, builder: (context, _) { - final lockedHostId = widget.lockToHost ? widget.initialHostId : null; - final session = lockedHostId == null - ? manager.activeSession - : manager.sessionFor(lockedHostId); + final lockToHost = widget.lockToHost; + final lockedHostId = lockToHost ? widget.initialHostId : null; + final session = lockToHost + ? manager.sessionFor(lockedHostId) + : manager.activeSession; final showSelector = - !widget.lockToHost && + !lockToHost && !_connecting && session != null && session.status != DockerConnectionStatus.connecting; @@ -170,9 +171,9 @@ class _DockerPanelState extends ConsumerState { onRefresh: () async { if (session == null) { await _refreshHosts(); - } else if (lockedHostId == null) { + } else if (!lockToHost) { manager.reconnectActive(); - } else { + } else if (lockedHostId != null) { manager.reconnect(lockedHostId); } }, @@ -220,7 +221,7 @@ class _DockerPanelState extends ConsumerState { height: MediaQuery.sizeOf(context).height * 0.58, child: Center( child: _DockerEmptyCard( - onChooseServer: widget.lockToHost + onChooseServer: lockToHost ? null : _openServerPicker, ), @@ -232,7 +233,10 @@ class _DockerPanelState extends ConsumerState { return _DockerContainerList( session: session, manager: manager, - onRefresh: () => manager.reconnect(session.server.id), + lockedHostId: lockToHost ? lockedHostId : null, + onRefresh: !lockToHost + ? manager.reconnectActive + : () => manager.reconnect(session.server.id), onOpenPort: _openPort, onShowLogs: _showLogs, ); @@ -267,15 +271,19 @@ class _DockerPanelState extends ConsumerState { await launchUrl(url, mode: LaunchMode.externalApplication); } - Future _showLogs(String hostId, DockerContainer container) async { + Future _showLogs( + String? lockedHostId, + DockerContainer container, + ) async { final manager = ref.read(dockerSessionManagerProvider); - manager.getLogs(container, hostId: hostId); + manager.getLogs(container, hostId: lockedHostId); await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, barrierColor: Colors.black.withValues(alpha: 0.34), - builder: (_) => _DockerLogsSheet(hostId: hostId, container: container), + builder: (_) => + _DockerLogsSheet(hostId: lockedHostId, container: container), ); } } @@ -287,13 +295,15 @@ class _DockerContainerList extends StatefulWidget { required this.onRefresh, required this.onOpenPort, required this.onShowLogs, + this.lockedHostId, }); final DockerSessionState session; final DockerSessionManager manager; + final String? lockedHostId; final VoidCallback onRefresh; final Future Function(ServerModel server, String port) onOpenPort; - final Future Function(String hostId, DockerContainer container) + final Future Function(String? lockedHostId, DockerContainer container) onShowLogs; @override @@ -350,21 +360,21 @@ class _DockerContainerListState extends State<_DockerContainerList> { .toList(growable: false); void _batchStart() { - final hostId = widget.session.server.id; + final hostId = widget.lockedHostId; for (final c in _selectedContainers) { if (!c.isRunning) widget.manager.start(c, hostId: hostId); } } void _batchRestart() { - final hostId = widget.session.server.id; + final hostId = widget.lockedHostId; for (final c in _selectedContainers) { if (c.isRunning) widget.manager.restart(c, hostId: hostId); } } void _batchStop() { - final hostId = widget.session.server.id; + final hostId = widget.lockedHostId; for (final c in _selectedContainers) { if (c.isRunning) widget.manager.stop(c, hostId: hostId); } @@ -395,7 +405,7 @@ class _DockerContainerListState extends State<_DockerContainerList> { ), ); if (confirmed != true) return; - final hostId = widget.session.server.id; + final hostId = widget.lockedHostId; for (final c in _selectedContainers) { widget.manager.delete(c, hostId: hostId); } @@ -471,16 +481,18 @@ class _DockerContainerListState extends State<_DockerContainerList> { operating: session.operatingIds.contains(container.id), selected: _selected.contains(container.id), onSelect: () => _toggle(container.id), - onStart: () => - widget.manager.start(container, hostId: session.server.id), + onStart: () => widget.manager.start( + container, + hostId: widget.lockedHostId, + ), onStop: () => - widget.manager.stop(container, hostId: session.server.id), + widget.manager.stop(container, hostId: widget.lockedHostId), onRestart: () => widget.manager.restart( container, - hostId: session.server.id, + hostId: widget.lockedHostId, ), onDelete: () => _confirmDelete(context, container), - onLogs: () => widget.onShowLogs(session.server.id, container), + onLogs: () => widget.onShowLogs(widget.lockedHostId, container), onOpenPort: (port) => widget.onOpenPort(session.server, port), ); }, @@ -519,7 +531,7 @@ class _DockerContainerListState extends State<_DockerContainerList> { ), ); if (confirmed == true) { - widget.manager.delete(container, hostId: widget.session.server.id); + widget.manager.delete(container, hostId: widget.lockedHostId); } } } @@ -1369,7 +1381,7 @@ class _DockerCircleAction extends StatelessWidget { class _DockerLogsSheet extends ConsumerStatefulWidget { const _DockerLogsSheet({required this.hostId, required this.container}); - final String hostId; + final String? hostId; final DockerContainer container; @override @@ -1388,7 +1400,9 @@ class _DockerLogsSheetState extends ConsumerState<_DockerLogsSheet> { _manager = ref.read(dockerSessionManagerProvider); _manager.addListener(_onLogsChanged); _autoRefreshTimer = Timer.periodic(const Duration(seconds: 3), (_) { - final session = _manager.sessionFor(widget.hostId); + final session = widget.hostId == null + ? _manager.activeSession + : _manager.sessionFor(widget.hostId); if (session?.status != DockerConnectionStatus.connected) { return; } @@ -1405,7 +1419,12 @@ class _DockerLogsSheetState extends ConsumerState<_DockerLogsSheet> { void _onLogsChanged() { if (_initialScrollDone) return; - final logs = _manager.sessionFor(widget.hostId)?.logs ?? ''; + final logs = + (widget.hostId == null + ? _manager.activeSession + : _manager.sessionFor(widget.hostId)) + ?.logs ?? + ''; if (logs.trim().isEmpty) return; _initialScrollDone = true; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -1482,7 +1501,11 @@ class _DockerLogsSheetState extends ConsumerState<_DockerLogsSheet> { animation: _manager, builder: (context, _) { final logs = - _manager.sessionFor(widget.hostId)?.logs ?? ''; + (widget.hostId == null + ? _manager.activeSession + : _manager.sessionFor(widget.hostId)) + ?.logs ?? + ''; return Container( width: double.infinity, margin: const EdgeInsets.fromLTRB(12, 0, 12, 12), @@ -1819,16 +1842,17 @@ class _DockerServerRow extends StatelessWidget { ], ), ), - Text( - connected - ? l.tr('docker.statusConnected') - : l.tr('docker.statusDisconnected'), - style: TextStyle( - color: connected ? c.success : c.softMuted, - fontSize: 11, - fontWeight: FontWeight.w800, + if (active) + Icon(Icons.check_rounded, color: c.success, size: 20) + else if (!connected) + Text( + l.tr('docker.statusDisconnected'), + style: TextStyle( + color: c.softMuted, + fontSize: 11, + fontWeight: FontWeight.w800, + ), ), - ), ], ), ), diff --git a/native/lib/features/shell/main_shell_page.dart b/native/lib/features/shell/main_shell_page.dart index 1351e8d..add340f 100644 --- a/native/lib/features/shell/main_shell_page.dart +++ b/native/lib/features/shell/main_shell_page.dart @@ -187,7 +187,7 @@ class _WarmBottomBar extends StatelessWidget { child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18), child: Container( - height: 62, + height: 52, padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: context.colors.card, diff --git a/native/lib/features/shell/sftp_tab.dart b/native/lib/features/shell/sftp_tab.dart index 526c2b2..e109a16 100644 --- a/native/lib/features/shell/sftp_tab.dart +++ b/native/lib/features/shell/sftp_tab.dart @@ -2192,6 +2192,66 @@ class _SftpServerRow extends StatelessWidget { Widget build(BuildContext context) { final l = AppLocalizations.of(context); final c = context.colors; + if (connected) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Material( + color: active ? c.accentSoft : c.card, + borderRadius: BorderRadius.circular(14), + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: c.banner, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.folder_outlined, + size: 20, + color: c.primary, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + server.displayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: c.text, + fontSize: 14, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 3), + Text( + server.connectionLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: c.softMuted, fontSize: 12), + ), + ], + ), + ), + if (active) + Icon(Icons.check_rounded, color: c.success, size: 20), + ], + ), + ), + ), + ), + ); + } return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Material( diff --git a/native/lib/features/terminal/terminal_bottom_menu.dart b/native/lib/features/terminal/terminal_bottom_menu.dart index 354c664..88c77f8 100644 --- a/native/lib/features/terminal/terminal_bottom_menu.dart +++ b/native/lib/features/terminal/terminal_bottom_menu.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/services.dart'; import '../../core/api/api_result.dart'; import '../../core/ui/app_color_theme.dart'; @@ -290,6 +291,9 @@ class _TerminalBottomBarState extends ConsumerState { } Future _showDocker(BuildContext context) async { + FocusManager.instance.primaryFocus?.unfocus(); + await SystemChannels.textInput.invokeMethod('TextInput.hide'); + if (!context.mounted) return; final l = AppLocalizations.of(context); await showModalBottomSheet( context: context,