mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 23:34:24 +01:00
220 lines
6.1 KiB
Dart
220 lines
6.1 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:fl_lib/fl_lib.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:server_box/core/chan.dart';
|
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
|
|
enum TermSessionStatus {
|
|
connecting,
|
|
connected,
|
|
disconnected;
|
|
|
|
@override
|
|
String toString() {
|
|
return name.capitalize;
|
|
}
|
|
}
|
|
|
|
/// Represents a running SSH terminal session for Android notifications and iOS Live Activities.
|
|
class TermSessionInfo {
|
|
final String id;
|
|
final String title; // e.g. server name
|
|
final String subtitle; // e.g. user@ip:port
|
|
final int startTimeMs;
|
|
final TermSessionStatus status;
|
|
|
|
TermSessionInfo({
|
|
required this.id,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.startTimeMs,
|
|
required this.status,
|
|
});
|
|
|
|
Map<String, Object> toJson() => {
|
|
'id': id,
|
|
'title': title,
|
|
'subtitle': subtitle,
|
|
'startTimeMs': startTimeMs,
|
|
'status': status.toString(),
|
|
};
|
|
}
|
|
|
|
/// Singleton to track active SSH sessions and sync to Android notifications.
|
|
abstract final class TermSessionManager {
|
|
static final Map<String, _Entry> _entries = {};
|
|
static String? _activeId; // For iOS Live Activity
|
|
static Timer? _updateTimer; // Timer for iOS Live Activity updates
|
|
static const _updateInterval = Duration(seconds: 5); // 5-second update interval
|
|
|
|
static void init() {
|
|
if (isAndroid) {
|
|
MethodChans.registerHandler(
|
|
(id) async {
|
|
_entries[id]?.disconnect?.call();
|
|
},
|
|
() {
|
|
// Stop all connections when notification "Stop All" is pressed
|
|
stopAllConnections();
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Called when Android notification "Stop All" button is pressed
|
|
static void stopAllConnections() {
|
|
// Disconnect all sessions
|
|
final disconnectCallbacks = _entries.values.map((e) => e.disconnect).where((cb) => cb != null).toList();
|
|
for (final disconnect in disconnectCallbacks) {
|
|
disconnect!();
|
|
}
|
|
// Clear all entries
|
|
_entries.clear();
|
|
_activeId = null;
|
|
_sync();
|
|
}
|
|
|
|
/// Add a session record and push update to Android.
|
|
static void add({
|
|
required String id,
|
|
required Spi spi,
|
|
required int startTimeMs,
|
|
required VoidCallback disconnect,
|
|
TermSessionStatus status = TermSessionStatus.connecting,
|
|
}) {
|
|
final info = TermSessionInfo(
|
|
id: id,
|
|
title: spi.name,
|
|
subtitle: spi.oldId,
|
|
startTimeMs: startTimeMs,
|
|
status: status,
|
|
);
|
|
_entries[id] = _Entry(info, disconnect, hasTerminalUI: true);
|
|
_activeId = id; // most recent as active
|
|
_sync();
|
|
}
|
|
|
|
static void updateStatus(String id, TermSessionStatus status) {
|
|
final old = _entries[id];
|
|
if (old == null) return;
|
|
_entries[id] = _Entry(
|
|
TermSessionInfo(
|
|
id: old.info.id,
|
|
title: old.info.title,
|
|
subtitle: old.info.subtitle,
|
|
startTimeMs: old.info.startTimeMs,
|
|
status: status,
|
|
),
|
|
old.disconnect,
|
|
hasTerminalUI: old.hasTerminalUI,
|
|
);
|
|
_sync();
|
|
}
|
|
|
|
static void remove(String id) {
|
|
_entries.remove(id);
|
|
if (_activeId == id) {
|
|
_activeId = _entries.keys.firstOrNull;
|
|
}
|
|
_sync();
|
|
}
|
|
|
|
static Future<void> _sync() async {
|
|
// Android: update foreground service notifications
|
|
if (isAndroid) {
|
|
final isRunning = await MethodChans.isServiceRunning();
|
|
if (_entries.isEmpty) {
|
|
if (isRunning) {
|
|
MethodChans.stopService();
|
|
}
|
|
await MethodChans.updateSessions(jsonEncode({'sessions': []}));
|
|
} else {
|
|
if (!isRunning) {
|
|
MethodChans.startService();
|
|
}
|
|
final payload = jsonEncode({'sessions': _entries.values.map((e) => e.info.toJson()).toList()});
|
|
await MethodChans.updateSessions(payload);
|
|
}
|
|
}
|
|
|
|
// iOS: manage Live Activity timer
|
|
if (isIOS) {
|
|
if (_entries.isEmpty) {
|
|
_updateTimer?.cancel();
|
|
_updateTimer = null;
|
|
await MethodChans.stopLiveActivity();
|
|
} else {
|
|
// Start timer if not already running
|
|
_updateTimer ??= Timer.periodic(_updateInterval, (_) => _updateLiveActivity());
|
|
// Immediately update for immediate feedback
|
|
await _updateLiveActivity();
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<void> _updateLiveActivity() async {
|
|
if (!isIOS || _entries.isEmpty) return;
|
|
|
|
final connectionCount = _entries.length;
|
|
|
|
if (connectionCount == 1) {
|
|
// Single connection: show hostname
|
|
final id = _activeId ?? _entries.keys.first;
|
|
final entry = _entries[id];
|
|
if (entry == null) return;
|
|
final payload = jsonEncode({
|
|
...entry.info.toJson(),
|
|
'hasTerminal': entry.hasTerminalUI,
|
|
'connectionCount': connectionCount,
|
|
});
|
|
await MethodChans.updateLiveActivity(payload);
|
|
} else {
|
|
// Multiple connections: show connection count
|
|
final id = _activeId ?? _entries.keys.first;
|
|
final entry = _entries[id];
|
|
if (entry == null) return;
|
|
final payload = jsonEncode({
|
|
'id': 'multi_connections',
|
|
'title': '$connectionCount connections',
|
|
'subtitle': 'Multiple SSH sessions active',
|
|
'startTimeMs': entry.info.startTimeMs,
|
|
'status': TermSessionStatus.connected.toString(),
|
|
'hasTerminal': entry.hasTerminalUI,
|
|
'connectionCount': connectionCount,
|
|
});
|
|
await MethodChans.updateLiveActivity(payload);
|
|
}
|
|
}
|
|
|
|
/// Mark which session is actively displayed in UI (for iOS Live Activity).
|
|
static void setActive(String id, {bool hasTerminal = true}) {
|
|
_activeId = id;
|
|
final old = _entries[id];
|
|
if (old != null) {
|
|
_entries[id] = _Entry(old.info, old.disconnect, hasTerminalUI: hasTerminal);
|
|
_sync();
|
|
}
|
|
}
|
|
|
|
/// Stop Live Activity when app is closed/terminated (iOS only).
|
|
static Future<void> stopLiveActivityOnAppClose() async {
|
|
if (!isIOS) return;
|
|
|
|
// Cancel any running timers
|
|
_updateTimer?.cancel();
|
|
_updateTimer = null;
|
|
|
|
// Stop the Live Activity
|
|
await MethodChans.stopLiveActivity();
|
|
}
|
|
}
|
|
|
|
class _Entry {
|
|
final TermSessionInfo info;
|
|
final VoidCallback? disconnect;
|
|
final bool hasTerminalUI;
|
|
_Entry(this.info, this.disconnect, {this.hasTerminalUI = true});
|
|
}
|