mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 14:23:19 +08:00
feat: native-优化终端docker组件&其他优化
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user