import 'dart:async'; import 'package:fl_lib/fl_lib.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:server_box/core/sync.dart'; import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/try_limiter.dart'; import 'package:server_box/data/provider/server/single.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/ssh/session_manager.dart'; part 'all.freezed.dart'; part 'all.g.dart'; @freezed abstract class ServersState with _$ServersState { const factory ServersState({ @Default({}) Map servers, @Default([]) List serverOrder, @Default({}) Set tags, @Default({}) Set manualDisconnectedIds, Timer? autoRefreshTimer, }) = _ServersState; } @Riverpod(keepAlive: true) class ServersNotifier extends _$ServersNotifier { @override ServersState build() { return _load(); } Future reload() async { final newState = _load(); if (newState == state) return; state = newState; await refresh(); } ServersState _load() { final spis = Stores.server.fetch(); final newServers = {}; final newServerOrder = []; for (final spi in spis) { newServers[spi.id] = spi; } final serverOrder_ = Stores.setting.serverOrder.fetch(); if (serverOrder_.isNotEmpty) { spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id); newServerOrder.addAll(spis.map((e) => e.id)); } else { newServerOrder.addAll(newServers.keys); } // Must use [equals] to compare [Order] here. if (!newServerOrder.equals(serverOrder_)) { Stores.setting.serverOrder.put(newServerOrder); } final newTags = _calculateTags(newServers); return stateOrNull?.copyWith(servers: newServers, serverOrder: newServerOrder, tags: newTags) ?? ServersState(servers: newServers, serverOrder: newServerOrder, tags: newTags); } Set _calculateTags(Map servers) { final tags = {}; for (final spi in servers.values) { final spiTags = spi.tags; if (spiTags == null) continue; for (final t in spiTags) { tags.add(t); } } return tags; } /// Get a [Spi] by [spi] or [id]. /// /// Priority: [spi] > [id] Spi? pick({Spi? spi, String? id}) { if (spi != null) { return state.servers[spi.id]; } if (id != null) { return state.servers[id]; } return null; } /// if [spi] is specificed then only refresh this server /// [onlyFailed] only refresh failed servers Future refresh({Spi? spi, bool onlyFailed = false}) async { if (spi != null) { final newManualDisconnected = Set.from(state.manualDisconnectedIds)..remove(spi.id); state = state.copyWith(manualDisconnectedIds: newManualDisconnected); final serverNotifier = ref.read(serverNotifierProvider(spi.id).notifier); await serverNotifier.refresh(); return; } await Future.wait( state.servers.entries.map((entry) async { final serverId = entry.key; final spi = entry.value; if (onlyFailed) { final serverState = ref.read(serverNotifierProvider(serverId)); if (serverState.conn != ServerConn.failed) return; TryLimiter.reset(serverId); } if (state.manualDisconnectedIds.contains(serverId)) return; final serverState = ref.read(serverNotifierProvider(serverId)); if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) { return; } final serverNotifier = ref.read(serverNotifierProvider(serverId).notifier); await serverNotifier.refresh(); }), ); } Future startAutoRefresh() async { var duration = Stores.setting.serverStatusUpdateInterval.fetch(); stopAutoRefresh(); if (duration == 0) return; if (duration < 0 || duration > 10 || duration == 1) { duration = 3; Loggers.app.warning('Invalid duration: $duration, use default 3'); } final timer = Timer.periodic(Duration(seconds: duration), (_) async { await refresh(); }); state = state.copyWith(autoRefreshTimer: timer); } void stopAutoRefresh() { final timer = state.autoRefreshTimer; if (timer != null) { timer.cancel(); state = state.copyWith(autoRefreshTimer: null); } } bool get isAutoRefreshOn => state.autoRefreshTimer != null; void setDisconnected() { for (final serverId in state.servers.keys) { final serverNotifier = ref.read(serverNotifierProvider(serverId).notifier); serverNotifier.updateConnection(ServerConn.disconnected); // Update SSH session status to disconnected final sessionId = 'ssh_$serverId'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); } //TryLimiter.clear(); } void closeServer({String? id}) { if (id == null) { for (final serverId in state.servers.keys) { closeOneServer(serverId); } return; } closeOneServer(id); } void closeOneServer(String id) { final spi = state.servers[id]; if (spi == null) { Loggers.app.warning('Server with id $id not found'); return; } final serverNotifier = ref.read(serverNotifierProvider(id).notifier); serverNotifier.closeConnection(); final newManualDisconnected = Set.from(state.manualDisconnectedIds)..add(id); state = state.copyWith(manualDisconnectedIds: newManualDisconnected); // Remove SSH session when server is manually closed final sessionId = 'ssh_$id'; TermSessionManager.remove(sessionId); } void addServer(Spi spi) { final newServers = Map.from(state.servers); newServers[spi.id] = spi; final newOrder = List.from(state.serverOrder)..add(spi.id); final newTags = _calculateTags(newServers); state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags); Stores.server.put(spi); Stores.setting.serverOrder.put(newOrder); refresh(spi: spi); bakSync.sync(milliDelay: 1000); } void delServer(String id) { final newServers = Map.from(state.servers); newServers.remove(id); final newOrder = List.from(state.serverOrder)..remove(id); final newTags = _calculateTags(newServers); state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags); Stores.setting.serverOrder.put(newOrder); Stores.server.delete(id); // Remove SSH session when server is deleted final sessionId = 'ssh_$id'; TermSessionManager.remove(sessionId); bakSync.sync(milliDelay: 1000); } void deleteAll() { // Remove all SSH sessions before clearing servers for (final id in state.servers.keys) { final sessionId = 'ssh_$id'; TermSessionManager.remove(sessionId); } state = const ServersState(); Stores.setting.serverOrder.put([]); Stores.server.clear(); bakSync.sync(milliDelay: 1000); } Future updateServer(Spi old, Spi newSpi) async { if (old != newSpi) { Stores.server.update(old, newSpi); final newServers = Map.from(state.servers); final newOrder = List.from(state.serverOrder); if (newSpi.id != old.id) { newServers[newSpi.id] = newSpi; newServers.remove(old.id); newOrder.update(old.id, newSpi.id); Stores.setting.serverOrder.put(newOrder); // Update SSH session ID when server ID changes final oldSessionId = 'ssh_${old.id}'; TermSessionManager.remove(oldSessionId); // Session will be re-added when reconnecting if necessary } else { newServers[old.id] = newSpi; // Update SPI in the corresponding IndividualServerNotifier final serverNotifier = ref.read(serverNotifierProvider(old.id).notifier); serverNotifier.updateSpi(newSpi); } final newTags = _calculateTags(newServers); state = state.copyWith(servers: newServers, serverOrder: newOrder, tags: newTags); // Only reconnect if neccessary if (newSpi.shouldReconnect(old)) { // Use [newSpi.id] instead of [old.id] because [old.id] may be changed TryLimiter.reset(newSpi.id); refresh(spi: newSpi); } } bakSync.sync(milliDelay: 1000); } }