feat: native-优化终端docker组件&其他优化

This commit is contained in:
chaoszhu
2026-06-27 22:40:36 +08:00
parent 5fd24e32dc
commit b4b67163ae
6 changed files with 154 additions and 56 deletions
@@ -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());
}
@@ -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);
+60 -36
View File
@@ -137,12 +137,13 @@ class _DockerPanelState extends ConsumerState<DockerPanel> {
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<DockerPanel> {
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<DockerPanel> {
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<DockerPanel> {
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<DockerPanel> {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
Future<void> _showLogs(String hostId, DockerContainer container) async {
Future<void> _showLogs(
String? lockedHostId,
DockerContainer container,
) async {
final manager = ref.read(dockerSessionManagerProvider);
manager.getLogs(container, hostId: hostId);
manager.getLogs(container, hostId: lockedHostId);
await showModalBottomSheet<void>(
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<void> Function(ServerModel server, String port) onOpenPort;
final Future<void> Function(String hostId, DockerContainer container)
final Future<void> 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,
),
),
),
],
),
),
@@ -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,
+60
View File
@@ -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(
@@ -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<TerminalBottomBar> {
}
Future<void> _showDocker(BuildContext context) async {
FocusManager.instance.primaryFocus?.unfocus();
await SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
if (!context.mounted) return;
final l = AppLocalizations.of(context);
await showModalBottomSheet<void>(
context: context,