From 53a7c0d8ff3a886398fbb7b769fb89480ba63345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Sun, 31 Aug 2025 00:55:54 +0800 Subject: [PATCH] migrate: riverpod + freezed (#870) --- .../logo.dart => core/extension/server.dart} | 4 +- lib/data/model/app/error.dart | 10 +- lib/data/model/app/scripts/shell_func.dart | 21 +- lib/data/model/server/custom.g.dart | 12 +- lib/data/model/server/server.dart | 17 - .../model/server/server_private_info.dart | 5 - .../model/server/server_private_info.g.dart | 21 +- lib/data/model/server/wol_cfg.g.dart | 2 +- lib/data/provider/app.g.dart | 61 +- lib/data/provider/container.dart | 161 ++-- lib/data/provider/container.freezed.dart | 305 +++++++ lib/data/provider/container.g.dart | 228 +++++ lib/data/provider/private_key.dart | 46 +- lib/data/provider/private_key.freezed.dart | 277 +++++++ lib/data/provider/private_key.g.dart | 27 + lib/data/provider/providers.dart | 82 ++ lib/data/provider/pve.dart | 134 +-- lib/data/provider/pve.freezed.dart | 283 +++++++ lib/data/provider/pve.g.dart | 160 ++++ lib/data/provider/server.dart | 781 ++++++++++-------- lib/data/provider/server.freezed.dart | 597 +++++++++++++ lib/data/provider/server.g.dart | 187 +++++ lib/data/provider/sftp.dart | 68 +- lib/data/provider/sftp.freezed.dart | 277 +++++++ lib/data/provider/sftp.g.dart | 25 + lib/data/provider/snippet.dart | 87 +- lib/data/provider/snippet.freezed.dart | 286 +++++++ lib/data/provider/snippet.g.dart | 26 + lib/data/provider/systemd.dart | 59 +- lib/data/provider/systemd.freezed.dart | 283 +++++++ lib/data/provider/systemd.g.dart | 163 ++++ lib/data/provider/virtual_keyboard.dart | 75 +- .../provider/virtual_keyboard.freezed.dart | 277 +++++++ lib/data/provider/virtual_keyboard.g.dart | 26 + lib/main.dart | 18 +- lib/view/page/backup.dart | 10 +- lib/view/page/container/actions.dart | 28 +- lib/view/page/container/container.dart | 127 +-- lib/view/page/home.dart | 26 +- lib/view/page/ping.dart | 19 +- lib/view/page/private_key/edit.dart | 15 +- lib/view/page/private_key/list.dart | 22 +- lib/view/page/process.dart | 19 +- lib/view/page/pve.dart | 101 +-- lib/view/page/server/detail/view.dart | 56 +- lib/view/page/server/edit.dart | 50 +- lib/view/page/server/tab/content.dart | 16 +- lib/view/page/server/tab/landscape.dart | 48 +- lib/view/page/server/tab/tab.dart | 64 +- lib/view/page/server/tab/utils.dart | 22 +- lib/view/page/setting/entries/server.dart | 2 +- lib/view/page/setting/entry.dart | 13 +- lib/view/page/setting/seq/srv_seq.dart | 53 +- lib/view/page/snippet/edit.dart | 29 +- lib/view/page/snippet/list.dart | 25 +- lib/view/page/ssh/page/init.dart | 3 +- lib/view/page/ssh/page/page.dart | 54 +- lib/view/page/ssh/page/virt_key.dart | 19 +- lib/view/page/ssh/tab.dart | 102 +-- lib/view/page/storage/local.dart | 14 +- lib/view/page/storage/sftp.dart | 36 +- lib/view/page/storage/sftp_mission.dart | 32 +- lib/view/page/systemd.dart | 99 ++- lib/view/widget/server_func_btns.dart | 44 +- pubspec.lock | 72 +- pubspec.yaml | 25 +- test/windows_test.dart | 4 +- 67 files changed, 5012 insertions(+), 1328 deletions(-) rename lib/{view/page/server/logo.dart => core/extension/server.dart} (88%) create mode 100644 lib/data/provider/container.freezed.dart create mode 100644 lib/data/provider/container.g.dart create mode 100644 lib/data/provider/private_key.freezed.dart create mode 100644 lib/data/provider/private_key.g.dart create mode 100644 lib/data/provider/providers.dart create mode 100644 lib/data/provider/pve.freezed.dart create mode 100644 lib/data/provider/pve.g.dart create mode 100644 lib/data/provider/server.freezed.dart create mode 100644 lib/data/provider/server.g.dart create mode 100644 lib/data/provider/sftp.freezed.dart create mode 100644 lib/data/provider/sftp.g.dart create mode 100644 lib/data/provider/snippet.freezed.dart create mode 100644 lib/data/provider/snippet.g.dart create mode 100644 lib/data/provider/systemd.freezed.dart create mode 100644 lib/data/provider/systemd.g.dart create mode 100644 lib/data/provider/virtual_keyboard.freezed.dart create mode 100644 lib/data/provider/virtual_keyboard.g.dart diff --git a/lib/view/page/server/logo.dart b/lib/core/extension/server.dart similarity index 88% rename from lib/view/page/server/logo.dart rename to lib/core/extension/server.dart index fecf30e7..27e587dd 100644 --- a/lib/view/page/server/logo.dart +++ b/lib/core/extension/server.dart @@ -2,10 +2,10 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:server_box/data/model/app/scripts/cmd_types.dart'; import 'package:server_box/data/model/server/dist.dart'; -import 'package:server_box/data/model/server/server.dart'; +import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/res/store.dart'; -extension LogoExt on Server { +extension LogoExt on ServerState { String? getLogoUrl(BuildContext context) { var logoUrl = spi.custom?.logoUrl ?? Stores.setting.serverLogoUrl.fetch().selfNotEmptyOrNull; if (logoUrl == null) { diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart index ba609aa4..8ff3f41f 100644 --- a/lib/data/model/app/error.dart +++ b/lib/data/model/app/error.dart @@ -4,7 +4,7 @@ import 'package:server_box/core/extension/context/locale.dart'; enum SSHErrType { unknown, connect, auth, noPrivateKey, chdir, segements, writeScript, getStatus } class SSHErr extends Err { - SSHErr({required super.type, super.message}); + const SSHErr({required super.type, super.message}); @override String? get solution => switch (type) { @@ -29,7 +29,7 @@ enum ContainerErrType { } class ContainerErr extends Err { - ContainerErr({required super.type, super.message}); + const ContainerErr({required super.type, super.message}); @override String? get solution => null; @@ -38,7 +38,7 @@ class ContainerErr extends Err { enum ICloudErrType { generic, notFound, multipleFiles } class ICloudErr extends Err { - ICloudErr({required super.type, super.message}); + const ICloudErr({required super.type, super.message}); @override String? get solution => null; @@ -47,7 +47,7 @@ class ICloudErr extends Err { enum WebdavErrType { generic, notFound } class WebdavErr extends Err { - WebdavErr({required super.type, super.message}); + const WebdavErr({required super.type, super.message}); @override String? get solution => null; @@ -56,7 +56,7 @@ class WebdavErr extends Err { enum PveErrType { unknown, net, loginFailed } class PveErr extends Err { - PveErr({required super.type, super.message}); + const PveErr({required super.type, super.message}); @override String? get solution => null; diff --git a/lib/data/model/app/scripts/shell_func.dart b/lib/data/model/app/scripts/shell_func.dart index b61ab803..414831ca 100644 --- a/lib/data/model/app/scripts/shell_func.dart +++ b/lib/data/model/app/scripts/shell_func.dart @@ -1,7 +1,6 @@ import 'package:server_box/data/model/app/scripts/script_builders.dart'; import 'package:server_box/data/model/app/scripts/script_consts.dart'; import 'package:server_box/data/model/server/system.dart'; -import 'package:server_box/data/provider/server.dart'; /// Shell functions available in the ServerBox application enum ShellFunc { @@ -26,8 +25,8 @@ enum ShellFunc { }; /// Execute this shell function on the specified server - String exec(String id, {SystemType? systemType}) { - final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType); + String exec(String id, {SystemType? systemType, required String? customDir}) { + final scriptPath = ShellFuncManager.getScriptPath(id, systemType: systemType, customDir: customDir); final isWindows = systemType == SystemType.windows; final builder = ScriptBuilderFactory.getBuilder(isWindows); @@ -51,11 +50,10 @@ class ShellFuncManager { /// Get the script directory for the given [id]. /// /// Checks for custom script directory first, then falls back to default. - static String getScriptDir(String id, {SystemType? systemType}) { - final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir; + static String getScriptDir(String id, {SystemType? systemType, required String? customDir}) { final isWindows = systemType == SystemType.windows; - if (customScriptDir != null) return _normalizeDir(customScriptDir, isWindows); + if (customDir != null) return _normalizeDir(customDir, isWindows); return ScriptPaths.getScriptDir(id, isWindows: isWindows); } @@ -66,11 +64,10 @@ class ShellFuncManager { } /// Get the full script path for the given [id] - static String getScriptPath(String id, {SystemType? systemType}) { - final customScriptDir = ServerProvider.pick(id: id)?.value.spi.custom?.scriptDir; - if (customScriptDir != null) { + static String getScriptPath(String id, {SystemType? systemType, required String? customDir}) { + if (customDir != null) { final isWindows = systemType == SystemType.windows; - final normalizedDir = _normalizeDir(customScriptDir, isWindows); + final normalizedDir = _normalizeDir(customDir, isWindows); final fileName = isWindows ? ScriptConstants.scriptFileWindows : ScriptConstants.scriptFile; final separator = isWindows ? ScriptConstants.windowsPathSeparator : ScriptConstants.unixPathSeparator; return '$normalizedDir$separator$fileName'; @@ -81,8 +78,8 @@ class ShellFuncManager { } /// Get the installation shell command for the script - static String getInstallShellCmd(String id, {SystemType? systemType}) { - final scriptDir = getScriptDir(id, systemType: systemType); + static String getInstallShellCmd(String id, {SystemType? systemType, required String? customDir}) { + final scriptDir = getScriptDir(id, systemType: systemType, customDir: customDir); final isWindows = systemType == SystemType.windows; final normalizedDir = _normalizeDir(scriptDir, isWindows); final builder = ScriptBuilderFactory.getBuilder(isWindows); diff --git a/lib/data/model/server/custom.g.dart b/lib/data/model/server/custom.g.dart index c67bbeb4..f204a733 100644 --- a/lib/data/model/server/custom.g.dart +++ b/lib/data/model/server/custom.g.dart @@ -20,11 +20,11 @@ ServerCustom _$ServerCustomFromJson(Map json) => ServerCustom( Map _$ServerCustomToJson(ServerCustom instance) => { - 'pveAddr': ?instance.pveAddr, + if (instance.pveAddr case final value?) 'pveAddr': value, 'pveIgnoreCert': instance.pveIgnoreCert, - 'cmds': ?instance.cmds, - 'preferTempDev': ?instance.preferTempDev, - 'logoUrl': ?instance.logoUrl, - 'netDev': ?instance.netDev, - 'scriptDir': ?instance.scriptDir, + if (instance.cmds case final value?) 'cmds': value, + if (instance.preferTempDev case final value?) 'preferTempDev': value, + if (instance.logoUrl case final value?) 'logoUrl': value, + if (instance.netDev case final value?) 'netDev': value, + if (instance.scriptDir case final value?) 'scriptDir': value, }; diff --git a/lib/data/model/server/server.dart b/lib/data/model/server/server.dart index 4489dfa4..0bf3b7e9 100644 --- a/lib/data/model/server/server.dart +++ b/lib/data/model/server/server.dart @@ -1,4 +1,3 @@ -import 'package:dartssh2/dartssh2.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:server_box/data/model/app/scripts/cmd_types.dart'; import 'package:server_box/data/model/server/amd.dart'; @@ -11,25 +10,9 @@ import 'package:server_box/data/model/server/memory.dart'; import 'package:server_box/data/model/server/net_speed.dart'; import 'package:server_box/data/model/server/nvdia.dart'; import 'package:server_box/data/model/server/sensors.dart'; -import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/temp.dart'; -class Server { - Spi spi; - ServerStatus status; - SSHClient? client; - ServerConn conn; - - Server(this.spi, this.status, this.conn, {this.client}); - - bool get needGenClient => conn < ServerConn.connecting; - - bool get canViewDetails => conn == ServerConn.finished; - - String get id => spi.id; -} - class ServerStatus { Cpus cpu; Memory mem; diff --git a/lib/data/model/server/server_private_info.dart b/lib/data/model/server/server_private_info.dart index 42fc9d47..6408f7d4 100644 --- a/lib/data/model/server/server_private_info.dart +++ b/lib/data/model/server/server_private_info.dart @@ -4,10 +4,8 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/custom.dart'; -import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/system.dart'; import 'package:server_box/data/model/server/wol_cfg.dart'; -import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/store/server.dart'; part 'server_private_info.freezed.dart'; @@ -87,9 +85,6 @@ extension Spix on Spi { String toJsonString() => json.encode(toJson()); - VNode? get server => ServerProvider.pick(spi: this); - VNode? get jumpServer => ServerProvider.pick(id: jumpId); - bool shouldReconnect(Spi old) { return user != old.user || ip != old.ip || diff --git a/lib/data/model/server/server_private_info.g.dart b/lib/data/model/server/server_private_info.g.dart index a3fc1653..fc255435 100644 --- a/lib/data/model/server/server_private_info.g.dart +++ b/lib/data/model/server/server_private_info.g.dart @@ -41,18 +41,19 @@ Map _$SpiToJson(_Spi instance) => { 'ip': instance.ip, 'port': instance.port, 'user': instance.user, - 'pwd': ?instance.pwd, - 'pubKeyId': ?instance.keyId, - 'tags': ?instance.tags, - 'alterUrl': ?instance.alterUrl, + if (instance.pwd case final value?) 'pwd': value, + if (instance.keyId case final value?) 'pubKeyId': value, + if (instance.tags case final value?) 'tags': value, + if (instance.alterUrl case final value?) 'alterUrl': value, 'autoConnect': instance.autoConnect, - 'jumpId': ?instance.jumpId, - 'custom': ?instance.custom, - 'wolCfg': ?instance.wolCfg, - 'envs': ?instance.envs, + if (instance.jumpId case final value?) 'jumpId': value, + if (instance.custom case final value?) 'custom': value, + if (instance.wolCfg case final value?) 'wolCfg': value, + if (instance.envs case final value?) 'envs': value, 'id': instance.id, - 'customSystemType': ?_$SystemTypeEnumMap[instance.customSystemType], - 'disabledCmdTypes': ?instance.disabledCmdTypes, + if (_$SystemTypeEnumMap[instance.customSystemType] case final value?) + 'customSystemType': value, + if (instance.disabledCmdTypes case final value?) 'disabledCmdTypes': value, }; const _$SystemTypeEnumMap = { diff --git a/lib/data/model/server/wol_cfg.g.dart b/lib/data/model/server/wol_cfg.g.dart index f7049c84..c83fe845 100644 --- a/lib/data/model/server/wol_cfg.g.dart +++ b/lib/data/model/server/wol_cfg.g.dart @@ -16,5 +16,5 @@ Map _$WakeOnLanCfgToJson(WakeOnLanCfg instance) => { 'mac': instance.mac, 'ip': instance.ip, - 'pwd': ?instance.pwd, + if (instance.pwd case final value?) 'pwd': value, }; diff --git a/lib/data/provider/app.g.dart b/lib/data/provider/app.g.dart index 660396b2..23420a9c 100644 --- a/lib/data/provider/app.g.dart +++ b/lib/data/provider/app.g.dart @@ -6,57 +6,20 @@ part of 'app.dart'; // RiverpodGenerator // ************************************************************************** -@ProviderFor(AppStates) -const appStatesProvider = AppStatesProvider._(); - -final class AppStatesProvider extends $NotifierProvider { - const AppStatesProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'appStatesProvider', - isAutoDispose: false, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$appStatesHash(); - - @$internal - @override - AppStates create() => AppStates(); - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(AppState value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - String _$appStatesHash() => r'ef96f10f6fff0f3dd6d3128ebf070ad79cbc8bc9'; -abstract class _$AppStates extends $Notifier { - AppState build(); - @$mustCallSuper - @override - void runBuild() { - final created = build(); - final ref = this.ref as $Ref; - final element = - ref.element - as $ClassProviderElement< - AnyNotifier, - AppState, - Object?, - Object? - >; - element.handleValue(ref, created); - } -} +/// See also [AppStates]. +@ProviderFor(AppStates) +final appStatesProvider = NotifierProvider.internal( + AppStates.new, + name: r'appStatesProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$appStatesHash, + dependencies: null, + allTransitiveDependencies: null, +); +typedef _$AppStates = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 42e15342..1bbbc85e 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -4,6 +4,8 @@ import 'dart:convert'; import 'package:dartssh2/dartssh2.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:server_box/core/extension/ssh_client.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/scripts/script_consts.dart'; @@ -12,63 +14,66 @@ import 'package:server_box/data/model/container/ps.dart'; import 'package:server_box/data/model/container/type.dart'; import 'package:server_box/data/res/store.dart'; +part 'container.freezed.dart'; +part 'container.g.dart'; + final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found"); -class ContainerProvider extends ChangeNotifier { - final SSHClient? client; - final String userName; - final String hostId; - final BuildContext context; - List? items; - List? images; - String? version; - ContainerErr? error; - String? runLog; - ContainerType type; - var sudoCompleter = Completer(); - bool isBusy = false; +@freezed +abstract class ContainerState with _$ContainerState { + const factory ContainerState({ + @Default(null) List? items, + @Default(null) List? images, + @Default(null) String? version, + @Default(null) ContainerErr? error, + @Default(null) String? runLog, + @Default(ContainerType.docker) ContainerType type, + @Default(false) bool isBusy, + }) = _ContainerState; +} - ContainerProvider({ - required this.client, - required this.userName, - required this.hostId, - required this.context, - }) : type = Stores.container.getType(hostId) { - refresh(); +@riverpod +class ContainerNotifier extends _$ContainerNotifier { + var sudoCompleter = Completer(); + + @override + ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) { + this.client = client; + this.userName = userName; + this.hostId = hostId; + this.context = context; + + final type = Stores.container.getType(hostId); + final initialState = ContainerState(type: type); + + // Async initialization + Future.microtask(() => refresh()); + + return initialState; } Future setType(ContainerType type) async { - this.type = type; + state = state.copyWith( + type: type, + error: null, + runLog: null, + items: null, + images: null, + version: null, + ); Stores.container.setType(type, hostId); - error = runLog = items = images = version = null; sudoCompleter = Completer(); - notifyListeners(); await refresh(); } - // Future _checkDockerInstalled(SSHClient client) async { - // final session = await client.execute("docker"); - // await session.done; - // // debugPrint('docker code: ${session.exitCode}'); - // return session.exitCode == 0; - // } - - // String _removeSudoPrompts(String value) { - // final regex = RegExp(r"\[sudo\] password for \w+:"); - // if (value.startsWith(regex)) { - // return value.replaceFirstMapped(regex, (match) => ""); - // } - // return value; - // } - void _requiresSudo() async { /// Podman is rootless - if (type == ContainerType.podman) return sudoCompleter.complete(false); + if (state.type == ContainerType.podman) return sudoCompleter.complete(false); if (!Stores.setting.containerTrySudo.fetch()) { return sudoCompleter.complete(false); } - final res = await client?.run(_wrap(ContainerCmdType.images.exec(type))); + final res = await client?.run(_wrap(ContainerCmdType.images.exec(state.type))); if (res?.string.toLowerCase().contains('permission denied') ?? false) { return sudoCompleter.complete(true); } @@ -76,8 +81,8 @@ class ContainerProvider extends ChangeNotifier { } Future refresh({bool isAuto = false}) async { - if (isBusy) return; - isBusy = true; + if (state.isBusy) return; + state = state.copyWith(isBusy: true); if (!sudoCompleter.isCompleted) _requiresSudo(); @@ -85,11 +90,14 @@ class ContainerProvider extends ChangeNotifier { /// If sudo is required and auto refresh is enabled, skip the refresh. /// Or this will ask for pwd again and again. - if (sudo && isAuto) return; + if (sudo && isAuto) { + state = state.copyWith(isBusy: false); + return; + } final includeStats = Stores.setting.containerParseStat.fetch(); var raw = ''; - final cmd = _wrap(ContainerCmdType.execAll(type, sudo: sudo, includeStats: includeStats)); + final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats)); final code = await client?.execWithPwd( cmd, context: context, @@ -97,75 +105,79 @@ class ContainerProvider extends ChangeNotifier { id: hostId, ); - isBusy = false; + state = state.copyWith(isBusy: false); if (!context.mounted) return; /// Code 127 means command not found if (code == 127 || raw.contains(_dockerNotFound)) { - error = ContainerErr(type: ContainerErrType.notInstalled); - notifyListeners(); + state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled)); return; } // Check result segments count final segments = raw.split(ScriptConstants.separator); if (segments.length != ContainerCmdType.values.length) { - error = ContainerErr( - type: ContainerErrType.segmentsNotMatch, - message: 'Container segments: ${segments.length}', + state = state.copyWith( + error: ContainerErr( + type: ContainerErrType.segmentsNotMatch, + message: 'Container segments: ${segments.length}', + ), ); Loggers.app.warning('Container segments: ${segments.length}\n$raw'); - notifyListeners(); return; } // Parse version final verRaw = ContainerCmdType.version.find(segments); try { - version = json.decode(verRaw)['Client']['Version']; + final version = json.decode(verRaw)['Client']['Version']; + state = state.copyWith(version: version, error: null); } catch (e, trace) { - error = ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'); + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'), + ); Loggers.app.warning('Container version failed', e, trace); - } finally { - notifyListeners(); } // Parse ps final psRaw = ContainerCmdType.ps.find(segments); try { final lines = psRaw.split('\n'); - if (type == ContainerType.docker) { + if (state.type == ContainerType.docker) { /// Due to the fetched data is not in json format, skip table header lines.removeWhere((element) => element.contains('CONTAINER ID')); } lines.removeWhere((element) => element.isEmpty); - items = lines.map((e) => ContainerPs.fromRaw(e, type)).toList(); + final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList(); + state = state.copyWith(items: items); } catch (e, trace) { - error = ContainerErr(type: ContainerErrType.parsePs, message: '$e'); + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'), + ); Loggers.app.warning('Container ps failed', e, trace); - } finally { - notifyListeners(); } // Parse images final imageRaw = ContainerCmdType.images.find(segments).trim(); final isEntireJson = imageRaw.startsWith('[') && imageRaw.endsWith(']'); try { + List images; if (isEntireJson) { images = (json.decode(imageRaw) as List) - .map((e) => ContainerImg.fromRawJson(json.encode(e), type)) + .map((e) => ContainerImg.fromRawJson(json.encode(e), state.type)) .toList(); } else { final lines = imageRaw.split('\n'); lines.removeWhere((element) => element.isEmpty); - images = lines.map((e) => ContainerImg.fromRawJson(e, type)).toList(); + images = lines.map((e) => ContainerImg.fromRawJson(e, state.type)).toList(); } + state = state.copyWith(images: images); } catch (e, trace) { - error = ContainerErr(type: ContainerErrType.parseImages, message: '$e'); + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'), + ); Loggers.app.warning('Container images failed', e, trace); - } finally { - notifyListeners(); } // Parse stats @@ -173,7 +185,7 @@ class ContainerProvider extends ChangeNotifier { try { final statsLines = statsRaw.split('\n'); statsLines.removeWhere((element) => element.isEmpty); - for (var item in items!) { + for (var item in state.items!) { final id = item.id; if (id == null) continue; final statsLine = statsLines.firstWhereOrNull( @@ -185,10 +197,10 @@ class ContainerProvider extends ChangeNotifier { item.parseStats(statsLine); } } catch (e, trace) { - error = ContainerErr(type: ContainerErrType.parseStats, message: '$e'); + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'), + ); Loggers.app.warning('Parse docker stats: $statsRaw', e, trace); - } finally { - notifyListeners(); } } @@ -223,25 +235,23 @@ class ContainerProvider extends ChangeNotifier { } Future run(String cmd, {bool autoRefresh = true}) async { - cmd = switch (type) { + cmd = switch (state.type) { ContainerType.docker => 'docker $cmd', ContainerType.podman => 'podman $cmd', }; - runLog = ''; + state = state.copyWith(runLog: ''); final errs = []; final code = await client?.execWithPwd( _wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd), context: context, onStdout: (data, _) { - runLog = '$runLog$data'; - notifyListeners(); + state = state.copyWith(runLog: '${state.runLog}$data'); }, onStderr: (data, _) => errs.add(data), id: hostId, ); - runLog = null; - notifyListeners(); + state = state.copyWith(runLog: null); if (code != 0) { return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim()); @@ -262,6 +272,7 @@ class ContainerProvider extends ChangeNotifier { } } + const _jsonFmt = '--format "{{json .}}"'; enum ContainerCmdType { diff --git a/lib/data/provider/container.freezed.dart b/lib/data/provider/container.freezed.dart new file mode 100644 index 00000000..614e4d74 --- /dev/null +++ b/lib/data/provider/container.freezed.dart @@ -0,0 +1,305 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'container.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ContainerState { + + List? get items; List? get images; String? get version; ContainerErr? get error; String? get runLog; ContainerType get type; bool get isBusy; +/// Create a copy of ContainerState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ContainerStateCopyWith get copyWith => _$ContainerStateCopyWithImpl(this as ContainerState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,error,runLog,type,isBusy); + +@override +String toString() { + return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)'; +} + + +} + +/// @nodoc +abstract mixin class $ContainerStateCopyWith<$Res> { + factory $ContainerStateCopyWith(ContainerState value, $Res Function(ContainerState) _then) = _$ContainerStateCopyWithImpl; +@useResult +$Res call({ + List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy +}); + + + + +} +/// @nodoc +class _$ContainerStateCopyWithImpl<$Res> + implements $ContainerStateCopyWith<$Res> { + _$ContainerStateCopyWithImpl(this._self, this._then); + + final ContainerState _self; + final $Res Function(ContainerState) _then; + +/// Create a copy of ContainerState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { + return _then(_self.copyWith( +items: freezed == items ? _self.items : items // ignore: cast_nullable_to_non_nullable +as List?,images: freezed == images ? _self.images : images // ignore: cast_nullable_to_non_nullable +as List?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable +as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ContainerState]. +extension ContainerStatePatterns on ContainerState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ContainerState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ContainerState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ContainerState value) $default,){ +final _that = this; +switch (_that) { +case _ContainerState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ContainerState value)? $default,){ +final _that = this; +switch (_that) { +case _ContainerState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ContainerState() when $default != null: +return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy) $default,) {final _that = this; +switch (_that) { +case _ContainerState(): +return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,) {final _that = this; +switch (_that) { +case _ContainerState() when $default != null: +return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ContainerState implements ContainerState { + const _ContainerState({final List? items = null, final List? images = null, this.version = null, this.error = null, this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images; + + + final List? _items; +@override@JsonKey() List? get items { + final value = _items; + if (value == null) return null; + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + final List? _images; +@override@JsonKey() List? get images { + final value = _images; + if (value == null) return null; + if (_images is EqualUnmodifiableListView) return _images; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + +@override@JsonKey() final String? version; +@override@JsonKey() final ContainerErr? error; +@override@JsonKey() final String? runLog; +@override@JsonKey() final ContainerType type; +@override@JsonKey() final bool isBusy; + +/// Create a copy of ContainerState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ContainerStateCopyWith<_ContainerState> get copyWith => __$ContainerStateCopyWithImpl<_ContainerState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,error,runLog,type,isBusy); + +@override +String toString() { + return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)'; +} + + +} + +/// @nodoc +abstract mixin class _$ContainerStateCopyWith<$Res> implements $ContainerStateCopyWith<$Res> { + factory _$ContainerStateCopyWith(_ContainerState value, $Res Function(_ContainerState) _then) = __$ContainerStateCopyWithImpl; +@override @useResult +$Res call({ + List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy +}); + + + + +} +/// @nodoc +class __$ContainerStateCopyWithImpl<$Res> + implements _$ContainerStateCopyWith<$Res> { + __$ContainerStateCopyWithImpl(this._self, this._then); + + final _ContainerState _self; + final $Res Function(_ContainerState) _then; + +/// Create a copy of ContainerState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { + return _then(_ContainerState( +items: freezed == items ? _self._items : items // ignore: cast_nullable_to_non_nullable +as List?,images: freezed == images ? _self._images : images // ignore: cast_nullable_to_non_nullable +as List?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable +as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/data/provider/container.g.dart b/lib/data/provider/container.g.dart new file mode 100644 index 00000000..684a765a --- /dev/null +++ b/lib/data/provider/container.g.dart @@ -0,0 +1,228 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'container.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$containerNotifierHash() => r'db8f8a6b6071b7b33fbf79128dfed408a5b9fdad'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ContainerNotifier + extends BuildlessAutoDisposeNotifier { + late final SSHClient? client; + late final String userName; + late final String hostId; + late final BuildContext context; + + ContainerState build( + SSHClient? client, + String userName, + String hostId, + BuildContext context, + ); +} + +/// See also [ContainerNotifier]. +@ProviderFor(ContainerNotifier) +const containerNotifierProvider = ContainerNotifierFamily(); + +/// See also [ContainerNotifier]. +class ContainerNotifierFamily extends Family { + /// See also [ContainerNotifier]. + const ContainerNotifierFamily(); + + /// See also [ContainerNotifier]. + ContainerNotifierProvider call( + SSHClient? client, + String userName, + String hostId, + BuildContext context, + ) { + return ContainerNotifierProvider(client, userName, hostId, context); + } + + @override + ContainerNotifierProvider getProviderOverride( + covariant ContainerNotifierProvider provider, + ) { + return call( + provider.client, + provider.userName, + provider.hostId, + provider.context, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'containerNotifierProvider'; +} + +/// See also [ContainerNotifier]. +class ContainerNotifierProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [ContainerNotifier]. + ContainerNotifierProvider( + SSHClient? client, + String userName, + String hostId, + BuildContext context, + ) : this._internal( + () => ContainerNotifier() + ..client = client + ..userName = userName + ..hostId = hostId + ..context = context, + from: containerNotifierProvider, + name: r'containerNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$containerNotifierHash, + dependencies: ContainerNotifierFamily._dependencies, + allTransitiveDependencies: + ContainerNotifierFamily._allTransitiveDependencies, + client: client, + userName: userName, + hostId: hostId, + context: context, + ); + + ContainerNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.client, + required this.userName, + required this.hostId, + required this.context, + }) : super.internal(); + + final SSHClient? client; + final String userName; + final String hostId; + final BuildContext context; + + @override + ContainerState runNotifierBuild(covariant ContainerNotifier notifier) { + return notifier.build(client, userName, hostId, context); + } + + @override + Override overrideWith(ContainerNotifier Function() create) { + return ProviderOverride( + origin: this, + override: ContainerNotifierProvider._internal( + () => create() + ..client = client + ..userName = userName + ..hostId = hostId + ..context = context, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + client: client, + userName: userName, + hostId: hostId, + context: context, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _ContainerNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ContainerNotifierProvider && + other.client == client && + other.userName == userName && + other.hostId == hostId && + other.context == context; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, client.hashCode); + hash = _SystemHash.combine(hash, userName.hashCode); + hash = _SystemHash.combine(hash, hostId.hashCode); + hash = _SystemHash.combine(hash, context.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ContainerNotifierRef on AutoDisposeNotifierProviderRef { + /// The parameter `client` of this provider. + SSHClient? get client; + + /// The parameter `userName` of this provider. + String get userName; + + /// The parameter `hostId` of this provider. + String get hostId; + + /// The parameter `context` of this provider. + BuildContext get context; +} + +class _ContainerNotifierProviderElement + extends + AutoDisposeNotifierProviderElement + with ContainerNotifierRef { + _ContainerNotifierProviderElement(super.provider); + + @override + SSHClient? get client => (origin as ContainerNotifierProvider).client; + @override + String get userName => (origin as ContainerNotifierProvider).userName; + @override + String get hostId => (origin as ContainerNotifierProvider).hostId; + @override + BuildContext get context => (origin as ContainerNotifierProvider).context; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/private_key.dart b/lib/data/provider/private_key.dart index 4a44ba87..a737e9af 100644 --- a/lib/data/provider/private_key.dart +++ b/lib/data/provider/private_key.dart @@ -1,45 +1,53 @@ -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/private_key_info.dart'; import 'package:server_box/data/res/store.dart'; -class PrivateKeyProvider extends Provider { - const PrivateKeyProvider._(); - static const instance = PrivateKeyProvider._(); +part 'private_key.freezed.dart'; +part 'private_key.g.dart'; - static final pkis = [].vn; +@freezed +abstract class PrivateKeyState with _$PrivateKeyState { + const factory PrivateKeyState({ + @Default([]) List keys, + }) = _PrivateKeyState; +} +@Riverpod(keepAlive: true) +class PrivateKeyNotifier extends _$PrivateKeyNotifier { @override - void load() { - super.load(); - pkis.value = Stores.key.fetch(); + PrivateKeyState build() { + final keys = Stores.key.fetch(); + return PrivateKeyState(keys: keys); } - static void add(PrivateKeyInfo info) { - pkis.value.add(info); - pkis.notify(); + void add(PrivateKeyInfo info) { + final newKeys = [...state.keys, info]; + state = state.copyWith(keys: newKeys); Stores.key.put(info); bakSync.sync(milliDelay: 1000); } - static void delete(PrivateKeyInfo info) { - pkis.value.removeWhere((e) => e.id == info.id); - pkis.notify(); + void delete(PrivateKeyInfo info) { + final newKeys = state.keys.where((e) => e.id != info.id).toList(); + state = state.copyWith(keys: newKeys); Stores.key.delete(info); bakSync.sync(milliDelay: 1000); } - static void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) { - final idx = pkis.value.indexWhere((e) => e.id == old.id); + void update(PrivateKeyInfo old, PrivateKeyInfo newInfo) { + final keys = [...state.keys]; + final idx = keys.indexWhere((e) => e.id == old.id); if (idx == -1) { - pkis.value.add(newInfo); + keys.add(newInfo); Stores.key.put(newInfo); Stores.key.delete(old); } else { - pkis.value[idx] = newInfo; + keys[idx] = newInfo; Stores.key.put(newInfo); } - pkis.notify(); + state = state.copyWith(keys: keys); bakSync.sync(milliDelay: 1000); } } diff --git a/lib/data/provider/private_key.freezed.dart b/lib/data/provider/private_key.freezed.dart new file mode 100644 index 00000000..e371a620 --- /dev/null +++ b/lib/data/provider/private_key.freezed.dart @@ -0,0 +1,277 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'private_key.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$PrivateKeyState { + + List get keys; +/// Create a copy of PrivateKeyState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PrivateKeyStateCopyWith get copyWith => _$PrivateKeyStateCopyWithImpl(this as PrivateKeyState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PrivateKeyState&&const DeepCollectionEquality().equals(other.keys, keys)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(keys)); + +@override +String toString() { + return 'PrivateKeyState(keys: $keys)'; +} + + +} + +/// @nodoc +abstract mixin class $PrivateKeyStateCopyWith<$Res> { + factory $PrivateKeyStateCopyWith(PrivateKeyState value, $Res Function(PrivateKeyState) _then) = _$PrivateKeyStateCopyWithImpl; +@useResult +$Res call({ + List keys +}); + + + + +} +/// @nodoc +class _$PrivateKeyStateCopyWithImpl<$Res> + implements $PrivateKeyStateCopyWith<$Res> { + _$PrivateKeyStateCopyWithImpl(this._self, this._then); + + final PrivateKeyState _self; + final $Res Function(PrivateKeyState) _then; + +/// Create a copy of PrivateKeyState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? keys = null,}) { + return _then(_self.copyWith( +keys: null == keys ? _self.keys : keys // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [PrivateKeyState]. +extension PrivateKeyStatePatterns on PrivateKeyState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _PrivateKeyState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _PrivateKeyState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _PrivateKeyState value) $default,){ +final _that = this; +switch (_that) { +case _PrivateKeyState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _PrivateKeyState value)? $default,){ +final _that = this; +switch (_that) { +case _PrivateKeyState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List keys)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _PrivateKeyState() when $default != null: +return $default(_that.keys);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List keys) $default,) {final _that = this; +switch (_that) { +case _PrivateKeyState(): +return $default(_that.keys);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List keys)? $default,) {final _that = this; +switch (_that) { +case _PrivateKeyState() when $default != null: +return $default(_that.keys);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _PrivateKeyState implements PrivateKeyState { + const _PrivateKeyState({final List keys = const []}): _keys = keys; + + + final List _keys; +@override@JsonKey() List get keys { + if (_keys is EqualUnmodifiableListView) return _keys; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_keys); +} + + +/// Create a copy of PrivateKeyState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PrivateKeyStateCopyWith<_PrivateKeyState> get copyWith => __$PrivateKeyStateCopyWithImpl<_PrivateKeyState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PrivateKeyState&&const DeepCollectionEquality().equals(other._keys, _keys)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_keys)); + +@override +String toString() { + return 'PrivateKeyState(keys: $keys)'; +} + + +} + +/// @nodoc +abstract mixin class _$PrivateKeyStateCopyWith<$Res> implements $PrivateKeyStateCopyWith<$Res> { + factory _$PrivateKeyStateCopyWith(_PrivateKeyState value, $Res Function(_PrivateKeyState) _then) = __$PrivateKeyStateCopyWithImpl; +@override @useResult +$Res call({ + List keys +}); + + + + +} +/// @nodoc +class __$PrivateKeyStateCopyWithImpl<$Res> + implements _$PrivateKeyStateCopyWith<$Res> { + __$PrivateKeyStateCopyWithImpl(this._self, this._then); + + final _PrivateKeyState _self; + final $Res Function(_PrivateKeyState) _then; + +/// Create a copy of PrivateKeyState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? keys = null,}) { + return _then(_PrivateKeyState( +keys: null == keys ? _self._keys : keys // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/data/provider/private_key.g.dart b/lib/data/provider/private_key.g.dart new file mode 100644 index 00000000..974da627 --- /dev/null +++ b/lib/data/provider/private_key.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'private_key.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$privateKeyNotifierHash() => + r'404836a4409f64d305c1e22f4a57b52985a57b68'; + +/// See also [PrivateKeyNotifier]. +@ProviderFor(PrivateKeyNotifier) +final privateKeyNotifierProvider = + NotifierProvider.internal( + PrivateKeyNotifier.new, + name: r'privateKeyNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$privateKeyNotifierHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$PrivateKeyNotifier = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/providers.dart b/lib/data/provider/providers.dart new file mode 100644 index 00000000..231b65a1 --- /dev/null +++ b/lib/data/provider/providers.dart @@ -0,0 +1,82 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:server_box/data/provider/app.dart'; +import 'package:server_box/data/provider/private_key.dart'; +import 'package:server_box/data/provider/server.dart'; +import 'package:server_box/data/provider/sftp.dart'; +import 'package:server_box/data/provider/snippet.dart'; + +/// !library; +/// ref.useNotifier, ref.readProvider, ref.watchProvider +/// +/// Usage: +/// - `providers.read.server` -> `ref.read(serversNotifierProvider)` +/// - `providers.use.snippet` -> `ref.read(snippetsNotifierProvider.notifier)` + +extension RiverpodNotifiers on ConsumerState { + T useNotifier>(NotifierProvider provider) { + return ref.read(provider.notifier); + } + + T readProvider(ProviderBase provider) { + return ref.read(provider); + } + + T watchProvider(ProviderBase provider) { + return ref.watch(provider); + } + + MyProviders get providers => MyProviders(ref); +} + +final class MyProviders { + final WidgetRef ref; + const MyProviders(this.ref); + + ReadMyProvider get read => ReadMyProvider(ref); + WatchMyProvider get watch => WatchMyProvider(ref); + UseNotifierMyProvider get use => UseNotifierMyProvider(ref); +} + +final class ReadMyProvider { + final WidgetRef ref; + const ReadMyProvider(this.ref); + + T call(ProviderBase provider) => ref.read(provider); + + // Specific provider getters + ServersState get server => ref.read(serverNotifierProvider); + SnippetState get snippet => ref.read(snippetNotifierProvider); + AppState get app => ref.read(appStatesProvider); + PrivateKeyState get privateKey => ref.read(privateKeyNotifierProvider); + SftpState get sftp => ref.read(sftpNotifierProvider); +} + +final class WatchMyProvider { + final WidgetRef ref; + const WatchMyProvider(this.ref); + + T call(ProviderBase provider) => ref.watch(provider); + + // Specific provider getters + ServersState get server => ref.watch(serverNotifierProvider); + SnippetState get snippet => ref.watch(snippetNotifierProvider); + AppState get app => ref.watch(appStatesProvider); + PrivateKeyState get privateKey => ref.watch(privateKeyNotifierProvider); + SftpState get sftp => ref.watch(sftpNotifierProvider); +} + +final class UseNotifierMyProvider { + final WidgetRef ref; + const UseNotifierMyProvider(this.ref); + + T call>(NotifierProvider provider) => + ref.read(provider.notifier); + + // Specific provider notifier getters + ServerNotifier get server => ref.read(serverNotifierProvider.notifier); + SnippetNotifier get snippet => ref.read(snippetNotifierProvider.notifier); + AppStates get app => ref.read(appStatesProvider.notifier); + PrivateKeyNotifier get privateKey => ref.read(privateKeyNotifierProvider.notifier); + SftpNotifier get sftp => ref.read(sftpNotifierProvider.notifier); +} \ No newline at end of file diff --git a/lib/data/provider/pve.dart b/lib/data/provider/pve.dart index 8ef23730..0da1ba5d 100644 --- a/lib/data/provider/pve.dart +++ b/lib/data/provider/pve.dart @@ -7,71 +7,90 @@ import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/pve.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/provider/server.dart'; + +part 'pve.freezed.dart'; +part 'pve.g.dart'; typedef PveCtrlFunc = Future Function(String node, String id); -final class PveProvider extends ChangeNotifier { - final Spi spi; +@freezed +abstract class PveState with _$PveState { + const factory PveState({ + @Default(null) PveErr? error, + @Default(null) PveRes? data, + @Default(null) String? release, + @Default(false) bool isBusy, + @Default(false) bool isConnected, + }) = _PveState; +} + +@riverpod +class PveNotifier extends _$PveNotifier { + late final Spi spi; late String addr; late final SSHClient _client; late final ServerSocket _serverSocket; final List _forwards = []; int _localPort = 0; + late final Dio session; + late final bool _ignoreCert; - PveProvider({required this.spi}) { - final client = spi.server?.value.client; + @override + PveState build(Spi spiParam) { + spi = spiParam; + final serverState = ref.watch(individualServerNotifierProvider(spi.id)); + final client = serverState.client; if (client == null) { - throw Exception('Server client is null'); + return const PveState(error: PveErr(type: PveErrType.net, message: 'Server client is null')); } _client = client; final addr = spi.custom?.pveAddr; if (addr == null) { - err.value = 'PVE address is null'; - return; + return PveState(error: PveErr(type: PveErrType.net, message: 'PVE address is null')); } this.addr = addr; - _init(); + _ignoreCert = spi.custom?.pveIgnoreCert ?? false; + _initSession(); + // Async initialization + Future.microtask(() => _init()); + return const PveState(); } - final err = ValueNotifier(null); - final connected = Completer(); + void _initSession() { + session = Dio() + ..httpClientAdapter = IOHttpClientAdapter( + createHttpClient: () { + final client = HttpClient(); + client.connectionFactory = cf; + if (_ignoreCert) { + client.badCertificateCallback = (_, _, _) => true; + } + return client; + }, + validateCertificate: _ignoreCert ? (_, _, _) => true : null, + ); + } - late final _ignoreCert = spi.custom?.pveIgnoreCert ?? false; - late final session = Dio() - ..httpClientAdapter = IOHttpClientAdapter( - createHttpClient: () { - final client = HttpClient(); - client.connectionFactory = cf; - if (_ignoreCert) { - client.badCertificateCallback = (_, _, _) => true; - } - return client; - }, - validateCertificate: _ignoreCert ? (_, _, _) => true : null, - ); - - final data = ValueNotifier(null); - - bool get onlyOneNode => data.value?.nodes.length == 1; - String? release; - bool isBusy = false; + bool get onlyOneNode => state.data?.nodes.length == 1; Future _init() async { try { await _forward(); await _login(); await _getRelease(); + state = state.copyWith(isConnected: true); } on PveErr { - err.value = l10n.pveLoginFailed; + state = state.copyWith(error: PveErr(type: PveErrType.loginFailed, message: l10n.pveLoginFailed)); } catch (e, s) { Loggers.app.warning('PVE init failed', e, s); - err.value = e.toString(); - } finally { - connected.complete(); + state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString())); } } @@ -146,72 +165,81 @@ final class PveProvider extends ChangeNotifier { final resp = await session.get('$addr/api2/extjs/version'); final version = resp.data['data']['release'] as String?; if (version != null) { - release = version; + state = state.copyWith(release: version); } } Future list() async { - await connected.future; - if (isBusy) return; - isBusy = true; + if (!state.isConnected || state.isBusy) return; + state = state.copyWith(isBusy: true); try { final resp = await session.get('$addr/api2/json/cluster/resources'); final res = resp.data['data'] as List; final result = await Computer.shared.start(PveRes.parse, ( res, - data.value, + state.data, )); - data.value = result; + state = state.copyWith(data: result, error: null); } catch (e) { Loggers.app.warning('PVE list failed', e); - err.value = e.toString(); + state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString())); } finally { - isBusy = false; + state = state.copyWith(isBusy: false); } } Future reboot(String node, String id) async { - await connected.future; + if (!state.isConnected) return false; final resp = await session.post( '$addr/api2/json/nodes/$node/$id/status/reboot', ); - return _isCtrlSuc(resp); + final success = _isCtrlSuc(resp); + if (success) await list(); // Refresh data + return success; } Future start(String node, String id) async { - await connected.future; + if (!state.isConnected) return false; final resp = await session.post( '$addr/api2/json/nodes/$node/$id/status/start', ); - return _isCtrlSuc(resp); + final success = _isCtrlSuc(resp); + if (success) await list(); // Refresh data + return success; } Future stop(String node, String id) async { - await connected.future; + if (!state.isConnected) return false; final resp = await session.post( '$addr/api2/json/nodes/$node/$id/status/stop', ); - return _isCtrlSuc(resp); + final success = _isCtrlSuc(resp); + if (success) await list(); // Refresh data + return success; } Future shutdown(String node, String id) async { - await connected.future; + if (!state.isConnected) return false; final resp = await session.post( '$addr/api2/json/nodes/$node/$id/status/shutdown', ); - return _isCtrlSuc(resp); + final success = _isCtrlSuc(resp); + if (success) await list(); // Refresh data + return success; } bool _isCtrlSuc(Response resp) { return resp.statusCode == 200; } - @override Future dispose() async { - super.dispose(); - await _serverSocket.close(); + try { + await _serverSocket.close(); + } catch (_) {} for (final forward in _forwards) { - forward.close(); + try { + forward.close(); + } catch (_) {} } } } diff --git a/lib/data/provider/pve.freezed.dart b/lib/data/provider/pve.freezed.dart new file mode 100644 index 00000000..dc07269a --- /dev/null +++ b/lib/data/provider/pve.freezed.dart @@ -0,0 +1,283 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'pve.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$PveState { + + PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected; +/// Create a copy of PveState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PveStateCopyWith get copyWith => _$PveStateCopyWithImpl(this as PveState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); +} + + +@override +int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected); + +@override +String toString() { + return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)'; +} + + +} + +/// @nodoc +abstract mixin class $PveStateCopyWith<$Res> { + factory $PveStateCopyWith(PveState value, $Res Function(PveState) _then) = _$PveStateCopyWithImpl; +@useResult +$Res call({ + PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected +}); + + + + +} +/// @nodoc +class _$PveStateCopyWithImpl<$Res> + implements $PveStateCopyWith<$Res> { + _$PveStateCopyWithImpl(this._self, this._then); + + final PveState _self; + final $Res Function(PveState) _then; + +/// Create a copy of PveState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) { + return _then(_self.copyWith( +error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable +as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable +as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [PveState]. +extension PveStatePatterns on PveState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _PveState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _PveState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _PveState value) $default,){ +final _that = this; +switch (_that) { +case _PveState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _PveState value)? $default,){ +final _that = this; +switch (_that) { +case _PveState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _PveState() when $default != null: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected) $default,) {final _that = this; +switch (_that) { +case _PveState(): +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,) {final _that = this; +switch (_that) { +case _PveState() when $default != null: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _PveState implements PveState { + const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false}); + + +@override@JsonKey() final PveErr? error; +@override@JsonKey() final PveRes? data; +@override@JsonKey() final String? release; +@override@JsonKey() final bool isBusy; +@override@JsonKey() final bool isConnected; + +/// Create a copy of PveState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PveStateCopyWith<_PveState> get copyWith => __$PveStateCopyWithImpl<_PveState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); +} + + +@override +int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected); + +@override +String toString() { + return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)'; +} + + +} + +/// @nodoc +abstract mixin class _$PveStateCopyWith<$Res> implements $PveStateCopyWith<$Res> { + factory _$PveStateCopyWith(_PveState value, $Res Function(_PveState) _then) = __$PveStateCopyWithImpl; +@override @useResult +$Res call({ + PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected +}); + + + + +} +/// @nodoc +class __$PveStateCopyWithImpl<$Res> + implements _$PveStateCopyWith<$Res> { + __$PveStateCopyWithImpl(this._self, this._then); + + final _PveState _self; + final $Res Function(_PveState) _then; + +/// Create a copy of PveState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) { + return _then(_PveState( +error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable +as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable +as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/data/provider/pve.g.dart b/lib/data/provider/pve.g.dart new file mode 100644 index 00000000..bde21a40 --- /dev/null +++ b/lib/data/provider/pve.g.dart @@ -0,0 +1,160 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pve.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$pveNotifierHash() => r'667cfb11cd7118d57b29918d137ef2cda2bad7ad'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$PveNotifier extends BuildlessAutoDisposeNotifier { + late final Spi spiParam; + + PveState build(Spi spiParam); +} + +/// See also [PveNotifier]. +@ProviderFor(PveNotifier) +const pveNotifierProvider = PveNotifierFamily(); + +/// See also [PveNotifier]. +class PveNotifierFamily extends Family { + /// See also [PveNotifier]. + const PveNotifierFamily(); + + /// See also [PveNotifier]. + PveNotifierProvider call(Spi spiParam) { + return PveNotifierProvider(spiParam); + } + + @override + PveNotifierProvider getProviderOverride( + covariant PveNotifierProvider provider, + ) { + return call(provider.spiParam); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'pveNotifierProvider'; +} + +/// See also [PveNotifier]. +class PveNotifierProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [PveNotifier]. + PveNotifierProvider(Spi spiParam) + : this._internal( + () => PveNotifier()..spiParam = spiParam, + from: pveNotifierProvider, + name: r'pveNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$pveNotifierHash, + dependencies: PveNotifierFamily._dependencies, + allTransitiveDependencies: PveNotifierFamily._allTransitiveDependencies, + spiParam: spiParam, + ); + + PveNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.spiParam, + }) : super.internal(); + + final Spi spiParam; + + @override + PveState runNotifierBuild(covariant PveNotifier notifier) { + return notifier.build(spiParam); + } + + @override + Override overrideWith(PveNotifier Function() create) { + return ProviderOverride( + origin: this, + override: PveNotifierProvider._internal( + () => create()..spiParam = spiParam, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + spiParam: spiParam, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement createElement() { + return _PveNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PveNotifierProvider && other.spiParam == spiParam; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, spiParam.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin PveNotifierRef on AutoDisposeNotifierProviderRef { + /// The parameter `spiParam` of this provider. + Spi get spiParam; +} + +class _PveNotifierProviderElement + extends AutoDisposeNotifierProviderElement + with PveNotifierRef { + _PveNotifierProviderElement(super.provider); + + @override + Spi get spiParam => (origin as PveNotifierProvider).spiParam; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 532e8dce..605c109a 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:convert'; -// import 'dart:io'; - import 'package:computer/computer.dart'; import 'package:dartssh2/dartssh2.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter_gbk2utf8/flutter_gbk2utf8.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:server_box/core/extension/ssh_client.dart'; import 'package:server_box/core/sync.dart'; import 'package:server_box/core/utils/server.dart'; @@ -24,322 +24,138 @@ import 'package:server_box/data/res/status.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/ssh/session_manager.dart'; -class ServerProvider extends Provider { - const ServerProvider._(); - static const instance = ServerProvider._(); +part 'server.freezed.dart'; +part 'server.g.dart'; - static final Map> servers = {}; - static final serverOrder = [].vn; - static final _tags = {}.vn; - static VNode> get tags => _tags; +@freezed +abstract class ServersState with _$ServersState { + const factory ServersState({ + @Default({}) Map servers, + @Default([]) List serverOrder, + @Default({}) Set tags, + @Default({}) Set manualDisconnectedIds, + Timer? autoRefreshTimer, + }) = _ServersState; +} - static Timer? _timer; +// Individual server state, including connection and status information +@freezed +abstract class ServerState with _$ServerState { + const factory ServerState({ + required Spi spi, + required ServerStatus status, + @Default(ServerConn.disconnected) ServerConn conn, + SSHClient? client, + Future? updateFuture, + }) = _ServerState; +} - static final _manualDisconnectedIds = {}; +extension IndividualServerStateExtension on ServerState { + bool get needGenClient => conn < ServerConn.connecting; - static final _serverIdsUpdating = ?>{}; + bool get canViewDetails => conn == ServerConn.finished; + String get id => spi.id; +} + +// Individual server state management +@riverpod +class IndividualServerNotifier extends _$IndividualServerNotifier { @override - Future load() async { - super.load(); - // #147 - // Clear all servers because of restarting app will cause duplicate servers - final oldServers = Map>.from(servers); - servers.clear(); - serverOrder.value.clear(); + ServerState build(String serverId) { + final serverNotifier = ref.read(serverNotifierProvider); + final spi = serverNotifier.servers[serverId]; + if (spi == null) { + throw StateError('Server $serverId not found'); + } - final spis = Stores.server.fetch(); - for (int idx = 0; idx < spis.length; idx++) { - final spi = spis[idx]; - final originServer = oldServers[spi.id]; - - /// #258 - /// If not [shouldReconnect], then keep the old state. - if (originServer != null && !originServer.value.spi.shouldReconnect(spi)) { - originServer.value.spi = spi; - servers[spi.id] = originServer; - } else { - final newServer = genServer(spi); - servers[spi.id] = newServer.vn; - } - } - final serverOrder_ = Stores.setting.serverOrder.fetch(); - if (serverOrder_.isNotEmpty) { - spis.reorder(order: serverOrder_, finder: (n, id) => n.id == id); - serverOrder.value.addAll(spis.map((e) => e.id)); - } else { - serverOrder.value.addAll(servers.keys); - } - // Must use [equals] to compare [Order] here. - if (!serverOrder.value.equals(serverOrder_)) { - Stores.setting.serverOrder.put(serverOrder.value); - } - _updateTags(); - // Must notify here, or the UI will not be updated. - serverOrder.notify(); + return ServerState(spi: spi, status: InitStatus.status); } - /// Get a [Server] by [spi] or [id]. - /// - /// Priority: [spi] > [id] - static VNode? pick({Spi? spi, String? id}) { - if (spi != null) { - return servers[spi.id]; - } - if (id != null) { - return servers[id]; - } - return null; + // Update connection status + void updateConnection(ServerConn conn) { + state = state.copyWith(conn: conn); } - static void _updateTags() { - final tags = {}; - for (final s in servers.values) { - final spiTags = s.value.spi.tags; - if (spiTags == null) continue; - for (final t in spiTags) { - tags.add(t); - } - } - _tags.value = tags; + // Update server status + void updateStatus(ServerStatus status) { + state = state.copyWith(status: status); } - static Server genServer(Spi spi) { - return Server(spi, InitStatus.status, ServerConn.disconnected); + // Update SSH client + void updateClient(SSHClient? client) { + state = state.copyWith(client: client); } - /// if [spi] is specificed then only refresh this server - /// [onlyFailed] only refresh failed servers - static Future refresh({Spi? spi, bool onlyFailed = false}) async { - if (spi != null) { - _manualDisconnectedIds.remove(spi.id); - await _getData(spi); + // Update SPI configuration + void updateSpi(Spi spi) { + state = state.copyWith(spi: spi); + } + + // Close connection + void closeConnection() { + final client = state.client; + client?.close(); + state = state.copyWith(client: null, conn: ServerConn.disconnected); + } + + // Refresh server status + Future refresh() async { + if (state.updateFuture != null) { + await state.updateFuture; return; } - await Future.wait( - servers.values.map((val) async { - final s = val.value; - if (onlyFailed) { - if (s.conn != ServerConn.failed) return; - TryLimiter.reset(s.spi.id); - } + final updateFuture = _updateServer(); + state = state.copyWith(updateFuture: updateFuture); - if (_manualDisconnectedIds.contains(s.spi.id)) return; - - if (s.conn == ServerConn.disconnected && !s.spi.autoConnect) { - return; - } - - // Check if already updating, and if so, wait for it to complete - final existingUpdate = _serverIdsUpdating[s.spi.id]; - if (existingUpdate != null) { - // Already updating, wait for the existing update to complete - try { - await existingUpdate; - } catch (e) { - // Ignore errors from the existing update, we'll try our own - } - return; - } - - // Start a new update operation - final updateFuture = _updateServer(s.spi); - _serverIdsUpdating[s.spi.id] = updateFuture; - - try { - await updateFuture; - } finally { - _serverIdsUpdating.remove(s.spi.id); - } - }), - ); - } - - static Future _updateServer(Spi spi) async { - await _getData(spi); - } - - static 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'); - } - _timer = Timer.periodic(Duration(seconds: duration), (_) async { - await refresh(); - }); - } - - static void stopAutoRefresh() { - if (_timer != null) { - _timer!.cancel(); - _timer = null; + try { + await updateFuture; + } finally { + state = state.copyWith(updateFuture: null); } } - static bool get isAutoRefreshOn => _timer != null; - - static void setDisconnected() { - for (final s in servers.values) { - s.value.conn = ServerConn.disconnected; - s.notify(); - - // Update SSH session status to disconnected - final sessionId = 'ssh_${s.value.spi.id}'; - TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); - } - //TryLimiter.clear(); + Future _updateServer() async { + await _getData(); } - static void closeServer({String? id}) { - if (id == null) { - for (final s in servers.values) { - _closeOneServer(s.value.spi.id); - } - return; - } - _closeOneServer(id); - } - - static void _closeOneServer(String id) { - final s = servers[id]; - if (s == null) { - Loggers.app.warning('Server with id $id not found'); - return; - } - final item = s.value; - item.client?.close(); - item.client = null; - item.conn = ServerConn.disconnected; - _manualDisconnectedIds.add(id); - s.notify(); - - // Remove SSH session when server is manually closed - final sessionId = 'ssh_$id'; - TermSessionManager.remove(sessionId); - } - - static void addServer(Spi spi) { - servers[spi.id] = genServer(spi).vn; - Stores.server.put(spi); - serverOrder.value.add(spi.id); - serverOrder.notify(); - Stores.setting.serverOrder.put(serverOrder.value); - _updateTags(); - refresh(spi: spi); - bakSync.sync(milliDelay: 1000); - } - - static void delServer(String id) { - servers.remove(id); - serverOrder.value.remove(id); - serverOrder.notify(); - Stores.setting.serverOrder.put(serverOrder.value); - Stores.server.delete(id); - _updateTags(); - - // Remove SSH session when server is deleted - final sessionId = 'ssh_$id'; - TermSessionManager.remove(sessionId); - - bakSync.sync(milliDelay: 1000); - } - - static void deleteAll() { - // Remove all SSH sessions before clearing servers - for (final id in servers.keys) { - final sessionId = 'ssh_$id'; - TermSessionManager.remove(sessionId); - } - - servers.clear(); - serverOrder.value.clear(); - serverOrder.notify(); - Stores.setting.serverOrder.put(serverOrder.value); - Stores.server.clear(); - _updateTags(); - bakSync.sync(milliDelay: 1000); - } - - static Future updateServer(Spi old, Spi newSpi) async { - if (old != newSpi) { - Stores.server.update(old, newSpi); - servers[old.id]?.value.spi = newSpi; - - if (newSpi.id != old.id) { - servers[newSpi.id] = servers[old.id]!; - servers.remove(old.id); - serverOrder.value.update(old.id, newSpi.id); - Stores.setting.serverOrder.put(serverOrder.value); - serverOrder.notify(); - - // 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 - } - - // 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); - } - } - _updateTags(); - bakSync.sync(milliDelay: 1000); - } - - static void _setServerState(VNode s, ServerConn ss) { - s.value.conn = ss; - s.notify(); - } - - static Future _getData(Spi spi) async { + Future _getData() async { + final spi = state.spi; final sid = spi.id; - final s = servers[sid]; - if (s == null) return; - - final sv = s.value; if (!TryLimiter.canTry(sid)) { - if (sv.conn != ServerConn.failed) { - _setServerState(s, ServerConn.failed); + if (state.conn != ServerConn.failed) { + updateConnection(ServerConn.failed); } return; } - sv.status.err = null; + final newStatus = state.status..err = null; // Clear previous error + updateStatus(newStatus); - if (sv.needGenClient || (sv.client?.isClosed ?? true)) { - _setServerState(s, ServerConn.connecting); + if (state.conn < ServerConn.connecting || (state.client?.isClosed ?? true)) { + updateConnection(ServerConn.connecting); + // Wake on LAN final wol = spi.wolCfg; if (wol != null) { try { await wol.wake(); } catch (e) { - // TryLimiter.inc(sid); - // s.status.err = SSHErr( - // type: SSHErrType.connect, - // message: 'Wake on lan failed: $e', - // ); - // _setServerState(s, ServerConn.failed); Loggers.app.warning('Wake on lan failed', e); - // return; } } try { final time1 = DateTime.now(); - sv.client = await genClient( + final client = await genClient( spi, timeout: Duration(seconds: Stores.setting.timeout.fetch()), onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi), ); + updateClient(client); + final time2 = DateTime.now(); final spentTime = time2.difference(time1).inMilliseconds; if (spi.jumpId == null) { @@ -348,50 +164,57 @@ class ServerProvider extends Provider { Loggers.app.info('Jump to ${spi.name} in $spentTime ms.'); } - // Add SSH session to TermSessionManager final sessionId = 'ssh_${spi.id}'; TermSessionManager.add( id: sessionId, spi: spi, startTimeMs: time1.millisecondsSinceEpoch, - disconnect: () => _closeOneServer(spi.id), + disconnect: () => ref.read(serverNotifierProvider.notifier)._closeOneServer(spi.id), status: TermSessionStatus.connecting, ); TermSessionManager.setActive(sessionId, hasTerminal: false); } catch (e) { TryLimiter.inc(sid); - sv.status.err = SSHErr(type: SSHErrType.connect, message: e.toString()); - _setServerState(s, ServerConn.failed); + final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString()); + updateStatus(newStatus); + updateConnection(ServerConn.failed); - // Remove SSH session on connection failure + // Remove SSH session when connection fails final sessionId = 'ssh_${spi.id}'; TermSessionManager.remove(sessionId); - /// In order to keep privacy, print [spi.name] instead of [spi.id] Loggers.app.warning('Connect to ${spi.name} failed', e); return; } - _setServerState(s, ServerConn.connected); + updateConnection(ServerConn.connected); // Update SSH session status to connected final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected); try { - // Detect system type using helper - final detectedSystemType = await SystemDetector.detect(sv.client!, spi); - sv.status.system = detectedSystemType; + // Detect system type + final detectedSystemType = await SystemDetector.detect(state.client!, spi); + final newStatus = state.status..system = detectedSystemType; + updateStatus(newStatus); - final (_, writeScriptResult) = await sv.client!.exec((session) async { - final scriptRaw = ShellFuncManager.allScript( - spi.custom?.cmds, + final (_, writeScriptResult) = await state.client!.exec( + (session) async { + final scriptRaw = ShellFuncManager.allScript( + spi.custom?.cmds, + systemType: detectedSystemType, + disabledCmdTypes: spi.disabledCmdTypes, + ).uint8List; + session.stdin.add(scriptRaw); + session.stdin.close(); + }, + entry: ShellFuncManager.getInstallShellCmd( + spi.id, systemType: detectedSystemType, - disabledCmdTypes: spi.disabledCmdTypes, - ).uint8List; - session.stdin.add(scriptRaw); - session.stdin.close(); - }, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType)); + customDir: spi.custom?.scriptDir, + ), + ); if (writeScriptResult.isNotEmpty && detectedSystemType != SystemType.windows) { ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType); throw writeScriptResult; @@ -399,152 +222,398 @@ class ServerProvider extends Provider { } on SSHAuthAbortError catch (e) { TryLimiter.inc(sid); final err = SSHErr(type: SSHErrType.auth, message: e.toString()); - sv.status.err = err; + final newStatus = state.status..err = err; + updateStatus(newStatus); Loggers.app.warning(err); - _setServerState(s, ServerConn.failed); + updateConnection(ServerConn.failed); - // Update SSH session status to disconnected final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); return; } on SSHAuthFailError catch (e) { TryLimiter.inc(sid); final err = SSHErr(type: SSHErrType.auth, message: e.toString()); - sv.status.err = err; + final newStatus = state.status..err = err; + updateStatus(newStatus); Loggers.app.warning(err); - _setServerState(s, ServerConn.failed); + updateConnection(ServerConn.failed); - // Update SSH session status to disconnected final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); return; } catch (e) { - // If max try times < 2 and can't write script, this will stop the status getting and etc. - // TryLimiter.inc(sid); final err = SSHErr(type: SSHErrType.writeScript, message: e.toString()); - sv.status.err = err; + final newStatus = state.status..err = err; + updateStatus(newStatus); Loggers.app.warning(err); - _setServerState(s, ServerConn.failed); + updateConnection(ServerConn.failed); - // Update SSH session status to disconnected final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); } } - if (sv.conn == ServerConn.connecting) return; + if (state.conn == ServerConn.connecting) return; - /// Keep [finished] state, or the UI will be refreshed to [loading] state - /// instead of the '$Temp | $Uptime'. - /// eg: '32C | 7 days' - if (sv.conn != ServerConn.finished) { - _setServerState(s, ServerConn.loading); + // Keep finished status to prevent UI from refreshing to loading state + if (state.conn != ServerConn.finished) { + updateConnection(ServerConn.loading); } List? segments; String? raw; try { - final execResult = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)); - if (execResult != null) { - String? rawStr; - bool needGbk = false; - try { - rawStr = utf8.decode(execResult, allowMalformed: true); - // If there are characters that cannot be parsed, try to fallback to gbk decoding - if (rawStr.contains('�')) { - Loggers.app.warning('UTF8 decoding failed, use GBK decoding'); - needGbk = true; - } - } catch (e) { - Loggers.app.warning('UTF8 decoding failed, use GBK decoding', e); + final execResult = await state.client?.run( + ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir), + ); + if (execResult != null) { + String? rawStr; + bool needGbk = false; + try { + rawStr = utf8.decode(execResult, allowMalformed: true); + // If there are unparseable characters, try fallback to GBK decoding + if (rawStr.contains('�')) { + Loggers.app.warning('UTF8 decoding failed, use GBK decoding'); needGbk = true; - }if (needGbk) { - try { - rawStr = gbk.decode(execResult); - } catch (e2) { - Loggers.app.warning('GBK decoding failed', e2); - rawStr = null; - } } - if (rawStr == null) { - Loggers.app.warning('Decoding failed, execResult: $execResult'); - } - raw = rawStr; - } else { - raw = execResult.toString(); + } catch (e) { + Loggers.app.warning('UTF8 decoding failed, use GBK decoding', e); + needGbk = true; } + if (needGbk) { + try { + rawStr = gbk.decode(execResult); + } catch (e2) { + Loggers.app.warning('GBK decoding failed', e2); + rawStr = null; + } + } + if (rawStr == null) { + Loggers.app.warning('Decoding failed, execResult: $execResult'); + } + raw = rawStr; + } else { + raw = execResult.toString(); + } if (raw == null || raw.isEmpty) { TryLimiter.inc(sid); - sv.status.err = SSHErr( - type: SSHErrType.segements, - message: 'decode or split failed, raw:\n$raw', - ); - _setServerState(s, ServerConn.failed); + final newStatus = state.status + ..err = SSHErr(type: SSHErrType.segements, message: 'decode or split failed, raw:\n$raw'); + updateStatus(newStatus); + updateConnection(ServerConn.failed); - // Update SSH session status to disconnected on segments error final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); return; } - //dprint('Get status from ${spi.name}:\n$raw'); segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList(); if (raw.isEmpty || segments.isEmpty) { if (Stores.setting.keepStatusWhenErr.fetch()) { - // Keep previous server status when err occurs - if (sv.conn != ServerConn.failed && sv.status.more.isNotEmpty) { + // Keep previous server status when error occurs + if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) { return; } } TryLimiter.inc(sid); - sv.status.err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw'); - _setServerState(s, ServerConn.failed); + final newStatus = state.status + ..err = SSHErr(type: SSHErrType.segements, message: 'Seperate segments failed, raw:\n$raw'); + updateStatus(newStatus); + updateConnection(ServerConn.failed); - // Update SSH session status to disconnected on segments error final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); return; } } catch (e) { TryLimiter.inc(sid); - sv.status.err = SSHErr(type: SSHErrType.getStatus, message: e.toString()); - _setServerState(s, ServerConn.failed); + final newStatus = state.status..err = SSHErr(type: SSHErrType.getStatus, message: e.toString()); + updateStatus(newStatus); + updateConnection(ServerConn.failed); Loggers.app.warning('Get status from ${spi.name} failed', e); - // Update SSH session status to disconnected on status error final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); return; } try { - // Parse script output into command-specific map + // Parse script output into command-specific mappings final parsedOutput = ScriptConstants.parseScriptOutput(raw); final req = ServerStatusUpdateReq( - ss: sv.status, + ss: state.status, parsedOutput: parsedOutput, - system: sv.status.system, + system: state.status.system, customCmds: spi.custom?.cmds ?? {}, ); - sv.status = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${sv.id}>'); + final newStatus = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${spi.id}>'); + updateStatus(newStatus); } catch (e, trace) { TryLimiter.inc(sid); - sv.status.err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw'); - _setServerState(s, ServerConn.failed); + final newStatus = state.status + ..err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw'); + updateStatus(newStatus); + updateConnection(ServerConn.failed); Loggers.app.warning('Server status', e, trace); - // Update SSH session status to disconnected on parse error final sessionId = 'ssh_${spi.id}'; TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected); return; } - /// Call this every time for setting [Server.isBusy] to false - _setServerState(s, ServerConn.finished); - // reset try times only after prepared successfully + // Set Server.isBusy to false each time this method is called + updateConnection(ServerConn.finished); + // Reset retry count only after successful preparation TryLimiter.reset(sid); } } + +@Riverpod(keepAlive: true) +class ServerNotifier extends _$ServerNotifier { + @override + ServersState build() { + // Initialize with empty state, load data asynchronously + Future.microtask(() => _load()); + return const ServersState(); + } + + Future _load() async { + 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); + + state = state.copyWith(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(individualServerNotifierProvider(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(individualServerNotifierProvider(serverId)); + if (serverState.conn != ServerConn.failed) return; + TryLimiter.reset(serverId); + } + + if (state.manualDisconnectedIds.contains(serverId)) return; + + final serverState = ref.read(individualServerNotifierProvider(serverId)); + if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) { + return; + } + + final serverNotifier = ref.read(individualServerNotifierProvider(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(individualServerNotifierProvider(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(individualServerNotifierProvider(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(individualServerNotifierProvider(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); + } +} + diff --git a/lib/data/provider/server.freezed.dart b/lib/data/provider/server.freezed.dart new file mode 100644 index 00000000..043e6c70 --- /dev/null +++ b/lib/data/provider/server.freezed.dart @@ -0,0 +1,597 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'server.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ServersState { + + Map get servers;// Only store server configuration information + List get serverOrder; Set get tags; Set get manualDisconnectedIds; Timer? get autoRefreshTimer; +/// Create a copy of ServersState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ServersStateCopyWith get copyWith => _$ServersStateCopyWithImpl(this as ServersState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ServersState&&const DeepCollectionEquality().equals(other.servers, servers)&&const DeepCollectionEquality().equals(other.serverOrder, serverOrder)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.manualDisconnectedIds, manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(servers),const DeepCollectionEquality().hash(serverOrder),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(manualDisconnectedIds),autoRefreshTimer); + +@override +String toString() { + return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)'; +} + + +} + +/// @nodoc +abstract mixin class $ServersStateCopyWith<$Res> { + factory $ServersStateCopyWith(ServersState value, $Res Function(ServersState) _then) = _$ServersStateCopyWithImpl; +@useResult +$Res call({ + Map servers, List serverOrder, Set tags, Set manualDisconnectedIds, Timer? autoRefreshTimer +}); + + + + +} +/// @nodoc +class _$ServersStateCopyWithImpl<$Res> + implements $ServersStateCopyWith<$Res> { + _$ServersStateCopyWithImpl(this._self, this._then); + + final ServersState _self; + final $Res Function(ServersState) _then; + +/// Create a copy of ServersState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) { + return _then(_self.copyWith( +servers: null == servers ? _self.servers : servers // ignore: cast_nullable_to_non_nullable +as Map,serverOrder: null == serverOrder ? _self.serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable +as List,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable +as Set,manualDisconnectedIds: null == manualDisconnectedIds ? _self.manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable +as Set,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable +as Timer?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ServersState]. +extension ServersStatePatterns on ServersState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ServersState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ServersState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ServersState value) $default,){ +final _that = this; +switch (_that) { +case _ServersState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ServersState value)? $default,){ +final _that = this; +switch (_that) { +case _ServersState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( Map servers, List serverOrder, Set tags, Set manualDisconnectedIds, Timer? autoRefreshTimer)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ServersState() when $default != null: +return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( Map servers, List serverOrder, Set tags, Set manualDisconnectedIds, Timer? autoRefreshTimer) $default,) {final _that = this; +switch (_that) { +case _ServersState(): +return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( Map servers, List serverOrder, Set tags, Set manualDisconnectedIds, Timer? autoRefreshTimer)? $default,) {final _that = this; +switch (_that) { +case _ServersState() when $default != null: +return $default(_that.servers,_that.serverOrder,_that.tags,_that.manualDisconnectedIds,_that.autoRefreshTimer);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ServersState implements ServersState { + const _ServersState({final Map servers = const {}, final List serverOrder = const [], final Set tags = const {}, final Set manualDisconnectedIds = const {}, this.autoRefreshTimer}): _servers = servers,_serverOrder = serverOrder,_tags = tags,_manualDisconnectedIds = manualDisconnectedIds; + + + final Map _servers; +@override@JsonKey() Map get servers { + if (_servers is EqualUnmodifiableMapView) return _servers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_servers); +} + +// Only store server configuration information + final List _serverOrder; +// Only store server configuration information +@override@JsonKey() List get serverOrder { + if (_serverOrder is EqualUnmodifiableListView) return _serverOrder; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_serverOrder); +} + + final Set _tags; +@override@JsonKey() Set get tags { + if (_tags is EqualUnmodifiableSetView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_tags); +} + + final Set _manualDisconnectedIds; +@override@JsonKey() Set get manualDisconnectedIds { + if (_manualDisconnectedIds is EqualUnmodifiableSetView) return _manualDisconnectedIds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_manualDisconnectedIds); +} + +@override final Timer? autoRefreshTimer; + +/// Create a copy of ServersState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ServersStateCopyWith<_ServersState> get copyWith => __$ServersStateCopyWithImpl<_ServersState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServersState&&const DeepCollectionEquality().equals(other._servers, _servers)&&const DeepCollectionEquality().equals(other._serverOrder, _serverOrder)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._manualDisconnectedIds, _manualDisconnectedIds)&&(identical(other.autoRefreshTimer, autoRefreshTimer) || other.autoRefreshTimer == autoRefreshTimer)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_servers),const DeepCollectionEquality().hash(_serverOrder),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_manualDisconnectedIds),autoRefreshTimer); + +@override +String toString() { + return 'ServersState(servers: $servers, serverOrder: $serverOrder, tags: $tags, manualDisconnectedIds: $manualDisconnectedIds, autoRefreshTimer: $autoRefreshTimer)'; +} + + +} + +/// @nodoc +abstract mixin class _$ServersStateCopyWith<$Res> implements $ServersStateCopyWith<$Res> { + factory _$ServersStateCopyWith(_ServersState value, $Res Function(_ServersState) _then) = __$ServersStateCopyWithImpl; +@override @useResult +$Res call({ + Map servers, List serverOrder, Set tags, Set manualDisconnectedIds, Timer? autoRefreshTimer +}); + + + + +} +/// @nodoc +class __$ServersStateCopyWithImpl<$Res> + implements _$ServersStateCopyWith<$Res> { + __$ServersStateCopyWithImpl(this._self, this._then); + + final _ServersState _self; + final $Res Function(_ServersState) _then; + +/// Create a copy of ServersState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? servers = null,Object? serverOrder = null,Object? tags = null,Object? manualDisconnectedIds = null,Object? autoRefreshTimer = freezed,}) { + return _then(_ServersState( +servers: null == servers ? _self._servers : servers // ignore: cast_nullable_to_non_nullable +as Map,serverOrder: null == serverOrder ? _self._serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable +as List,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable +as Set,manualDisconnectedIds: null == manualDisconnectedIds ? _self._manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable +as Set,autoRefreshTimer: freezed == autoRefreshTimer ? _self.autoRefreshTimer : autoRefreshTimer // ignore: cast_nullable_to_non_nullable +as Timer?, + )); +} + + +} + +/// @nodoc +mixin _$ServerState { + + Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client; Future? get updateFuture; +/// Create a copy of ServerState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ServerStateCopyWith get copyWith => _$ServerStateCopyWithImpl(this as ServerState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture)); +} + + +@override +int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture); + +@override +String toString() { + return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)'; +} + + +} + +/// @nodoc +abstract mixin class $ServerStateCopyWith<$Res> { + factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl; +@useResult +$Res call({ + Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future? updateFuture +}); + + +$SpiCopyWith<$Res> get spi; + +} +/// @nodoc +class _$ServerStateCopyWithImpl<$Res> + implements $ServerStateCopyWith<$Res> { + _$ServerStateCopyWithImpl(this._self, this._then); + + final ServerState _self; + final $Res Function(ServerState) _then; + +/// Create a copy of ServerState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) { + return _then(_self.copyWith( +spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable +as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable +as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable +as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable +as Future?, + )); +} +/// Create a copy of ServerState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SpiCopyWith<$Res> get spi { + + return $SpiCopyWith<$Res>(_self.spi, (value) { + return _then(_self.copyWith(spi: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [ServerState]. +extension ServerStatePatterns on ServerState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ServerState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ServerState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ServerState value) $default,){ +final _that = this; +switch (_that) { +case _ServerState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ServerState value)? $default,){ +final _that = this; +switch (_that) { +case _ServerState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future? updateFuture)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ServerState() when $default != null: +return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future? updateFuture) $default,) {final _that = this; +switch (_that) { +case _ServerState(): +return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future? updateFuture)? $default,) {final _that = this; +switch (_that) { +case _ServerState() when $default != null: +return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _ServerState implements ServerState { + const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client, this.updateFuture}); + + +@override final Spi spi; +@override final ServerStatus status; +@override@JsonKey() final ServerConn conn; +@override final SSHClient? client; +@override final Future? updateFuture; + +/// Create a copy of ServerState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ServerStateCopyWith<_ServerState> get copyWith => __$ServerStateCopyWithImpl<_ServerState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture)); +} + + +@override +int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture); + +@override +String toString() { + return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)'; +} + + +} + +/// @nodoc +abstract mixin class _$ServerStateCopyWith<$Res> implements $ServerStateCopyWith<$Res> { + factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl; +@override @useResult +$Res call({ + Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future? updateFuture +}); + + +@override $SpiCopyWith<$Res> get spi; + +} +/// @nodoc +class __$ServerStateCopyWithImpl<$Res> + implements _$ServerStateCopyWith<$Res> { + __$ServerStateCopyWithImpl(this._self, this._then); + + final _ServerState _self; + final $Res Function(_ServerState) _then; + +/// Create a copy of ServerState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) { + return _then(_ServerState( +spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable +as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable +as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable +as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable +as Future?, + )); +} + +/// Create a copy of ServerState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SpiCopyWith<$Res> get spi { + + return $SpiCopyWith<$Res>(_self.spi, (value) { + return _then(_self.copyWith(spi: value)); + }); +} +} + +// dart format on diff --git a/lib/data/provider/server.g.dart b/lib/data/provider/server.g.dart new file mode 100644 index 00000000..3baae147 --- /dev/null +++ b/lib/data/provider/server.g.dart @@ -0,0 +1,187 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'server.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$individualServerNotifierHash() => + r'e3d74fb95ca994cd8419b1deab743e8b3e21bee2'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$IndividualServerNotifier + extends BuildlessAutoDisposeNotifier { + late final String serverId; + + ServerState build(String serverId); +} + +/// See also [IndividualServerNotifier]. +@ProviderFor(IndividualServerNotifier) +const individualServerNotifierProvider = IndividualServerNotifierFamily(); + +/// See also [IndividualServerNotifier]. +class IndividualServerNotifierFamily extends Family { + /// See also [IndividualServerNotifier]. + const IndividualServerNotifierFamily(); + + /// See also [IndividualServerNotifier]. + IndividualServerNotifierProvider call(String serverId) { + return IndividualServerNotifierProvider(serverId); + } + + @override + IndividualServerNotifierProvider getProviderOverride( + covariant IndividualServerNotifierProvider provider, + ) { + return call(provider.serverId); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'individualServerNotifierProvider'; +} + +/// See also [IndividualServerNotifier]. +class IndividualServerNotifierProvider + extends + AutoDisposeNotifierProviderImpl { + /// See also [IndividualServerNotifier]. + IndividualServerNotifierProvider(String serverId) + : this._internal( + () => IndividualServerNotifier()..serverId = serverId, + from: individualServerNotifierProvider, + name: r'individualServerNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$individualServerNotifierHash, + dependencies: IndividualServerNotifierFamily._dependencies, + allTransitiveDependencies: + IndividualServerNotifierFamily._allTransitiveDependencies, + serverId: serverId, + ); + + IndividualServerNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.serverId, + }) : super.internal(); + + final String serverId; + + @override + ServerState runNotifierBuild(covariant IndividualServerNotifier notifier) { + return notifier.build(serverId); + } + + @override + Override overrideWith(IndividualServerNotifier Function() create) { + return ProviderOverride( + origin: this, + override: IndividualServerNotifierProvider._internal( + () => create()..serverId = serverId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + serverId: serverId, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _IndividualServerNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is IndividualServerNotifierProvider && + other.serverId == serverId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, serverId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin IndividualServerNotifierRef + on AutoDisposeNotifierProviderRef { + /// The parameter `serverId` of this provider. + String get serverId; +} + +class _IndividualServerNotifierProviderElement + extends + AutoDisposeNotifierProviderElement< + IndividualServerNotifier, + ServerState + > + with IndividualServerNotifierRef { + _IndividualServerNotifierProviderElement(super.provider); + + @override + String get serverId => (origin as IndividualServerNotifierProvider).serverId; +} + +String _$serverNotifierHash() => r'8e2bc3aef3c56263f88df3c2bb1ba88b6cf83c8f'; + +/// See also [ServerNotifier]. +@ProviderFor(ServerNotifier) +final serverNotifierProvider = + NotifierProvider.internal( + ServerNotifier.new, + name: r'serverNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$serverNotifierHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$ServerNotifier = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/sftp.dart b/lib/data/provider/sftp.dart index ca3ae4ba..665e26bb 100644 --- a/lib/data/provider/sftp.dart +++ b/lib/data/provider/sftp.dart @@ -1,41 +1,69 @@ 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/data/model/sftp/worker.dart'; -class SftpProvider extends Provider { - const SftpProvider._(); - static const instance = SftpProvider._(); +part 'sftp.freezed.dart'; +part 'sftp.g.dart'; - static final status = [].vn; +@freezed +abstract class SftpState with _$SftpState { + const factory SftpState({ + @Default([]) List requests, + }) = _SftpState; +} - static SftpReqStatus? get(int id) { - return status.value.singleWhere((element) => element.id == id); +@Riverpod(keepAlive: true) +class SftpNotifier extends _$SftpNotifier { + @override + SftpState build() { + return const SftpState(); } - static int add(SftpReq req, {Completer? completer}) { - final reqStat = SftpReqStatus(notifyListeners: status.notify, completer: completer, req: req); - status.value.add(reqStat); - status.notify(); + SftpReqStatus? get(int id) { + try { + return state.requests.singleWhere((element) => element.id == id); + } catch (e) { + return null; + } + } + + int add(SftpReq req, {Completer? completer}) { + final reqStat = SftpReqStatus( + notifyListeners: _notifyListeners, + completer: completer, + req: req, + ); + state = state.copyWith( + requests: [...state.requests, reqStat], + ); return reqStat.id; } - static void dispose() { - for (final item in status.value) { + void dispose() { + for (final item in state.requests) { item.dispose(); } - status.value.clear(); - status.notify(); + state = state.copyWith(requests: []); } - static void cancel(int id) { - final idx = status.value.indexWhere((e) => e.id == id); - if (idx < 0 || idx >= status.value.length) { + void cancel(int id) { + final idx = state.requests.indexWhere((e) => e.id == id); + if (idx < 0 || idx >= state.requests.length) { dprint('SftpProvider.cancel: id $id not found'); return; } - status.value[idx].dispose(); - status.value.removeAt(idx); - status.notify(); + final item = state.requests[idx]; + item.dispose(); + final newRequests = List.from(state.requests) + ..removeAt(idx); + state = state.copyWith(requests: newRequests); + } + + void _notifyListeners() { + // Force state update to notify listeners + state = state.copyWith(); } } diff --git a/lib/data/provider/sftp.freezed.dart b/lib/data/provider/sftp.freezed.dart new file mode 100644 index 00000000..03ee7cf8 --- /dev/null +++ b/lib/data/provider/sftp.freezed.dart @@ -0,0 +1,277 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'sftp.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$SftpState { + + List get requests; +/// Create a copy of SftpState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SftpStateCopyWith get copyWith => _$SftpStateCopyWithImpl(this as SftpState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SftpState&&const DeepCollectionEquality().equals(other.requests, requests)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(requests)); + +@override +String toString() { + return 'SftpState(requests: $requests)'; +} + + +} + +/// @nodoc +abstract mixin class $SftpStateCopyWith<$Res> { + factory $SftpStateCopyWith(SftpState value, $Res Function(SftpState) _then) = _$SftpStateCopyWithImpl; +@useResult +$Res call({ + List requests +}); + + + + +} +/// @nodoc +class _$SftpStateCopyWithImpl<$Res> + implements $SftpStateCopyWith<$Res> { + _$SftpStateCopyWithImpl(this._self, this._then); + + final SftpState _self; + final $Res Function(SftpState) _then; + +/// Create a copy of SftpState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? requests = null,}) { + return _then(_self.copyWith( +requests: null == requests ? _self.requests : requests // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SftpState]. +extension SftpStatePatterns on SftpState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SftpState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SftpState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SftpState value) $default,){ +final _that = this; +switch (_that) { +case _SftpState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SftpState value)? $default,){ +final _that = this; +switch (_that) { +case _SftpState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List requests)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SftpState() when $default != null: +return $default(_that.requests);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List requests) $default,) {final _that = this; +switch (_that) { +case _SftpState(): +return $default(_that.requests);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List requests)? $default,) {final _that = this; +switch (_that) { +case _SftpState() when $default != null: +return $default(_that.requests);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _SftpState implements SftpState { + const _SftpState({final List requests = const []}): _requests = requests; + + + final List _requests; +@override@JsonKey() List get requests { + if (_requests is EqualUnmodifiableListView) return _requests; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_requests); +} + + +/// Create a copy of SftpState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SftpStateCopyWith<_SftpState> get copyWith => __$SftpStateCopyWithImpl<_SftpState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SftpState&&const DeepCollectionEquality().equals(other._requests, _requests)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_requests)); + +@override +String toString() { + return 'SftpState(requests: $requests)'; +} + + +} + +/// @nodoc +abstract mixin class _$SftpStateCopyWith<$Res> implements $SftpStateCopyWith<$Res> { + factory _$SftpStateCopyWith(_SftpState value, $Res Function(_SftpState) _then) = __$SftpStateCopyWithImpl; +@override @useResult +$Res call({ + List requests +}); + + + + +} +/// @nodoc +class __$SftpStateCopyWithImpl<$Res> + implements _$SftpStateCopyWith<$Res> { + __$SftpStateCopyWithImpl(this._self, this._then); + + final _SftpState _self; + final $Res Function(_SftpState) _then; + +/// Create a copy of SftpState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? requests = null,}) { + return _then(_SftpState( +requests: null == requests ? _self._requests : requests // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/data/provider/sftp.g.dart b/lib/data/provider/sftp.g.dart new file mode 100644 index 00000000..557ebfb7 --- /dev/null +++ b/lib/data/provider/sftp.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sftp.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$sftpNotifierHash() => r'f8412a4bd1f2bc5919ec31a3eba1c27e9a578f41'; + +/// See also [SftpNotifier]. +@ProviderFor(SftpNotifier) +final sftpNotifierProvider = NotifierProvider.internal( + SftpNotifier.new, + name: r'sftpNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$sftpNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SftpNotifier = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/snippet.dart b/lib/data/provider/snippet.dart index 87e312f5..0e96bfd7 100644 --- a/lib/data/provider/snippet.dart +++ b/lib/data/provider/snippet.dart @@ -1,22 +1,31 @@ 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/snippet.dart'; import 'package:server_box/data/res/store.dart'; -class SnippetProvider extends Provider { - const SnippetProvider._(); - static const instance = SnippetProvider._(); +part 'snippet.freezed.dart'; +part 'snippet.g.dart'; - static final snippets = [].vn; - static final tags = {}.vn; +@freezed +abstract class SnippetState with _$SnippetState { + const factory SnippetState({ + @Default([]) List snippets, + @Default({}) Set tags, + }) = _SnippetState; +} +@Riverpod(keepAlive: true) +class SnippetNotifier extends _$SnippetNotifier { @override - void load() { - super.load(); - final snippets_ = Stores.snippet.fetch(); + SnippetState build() { + final snippets = Stores.snippet.fetch(); final order = Stores.setting.snippetOrder.fetch(); + + List orderedSnippets = snippets; if (order.isNotEmpty) { - final surplus = snippets_.reorder( + final surplus = snippets.reorder( order: order, finder: (n, name) => n.name == name, ); @@ -24,57 +33,65 @@ class SnippetProvider extends Provider { if (order != Stores.setting.snippetOrder.fetch()) { Stores.setting.snippetOrder.put(order); } + orderedSnippets = snippets; } - snippets.value = snippets_; - _updateTags(); + + final tags = _computeTags(orderedSnippets); + return SnippetState(snippets: orderedSnippets, tags: tags); } - static void _updateTags() { - final tags_ = {}; - for (final s in snippets.value) { + Set _computeTags(List snippets) { + final tags = {}; + for (final s in snippets) { final t = s.tags; if (t != null) { - tags_.addAll(t); + tags.addAll(t); } } - tags.value = tags_; + return tags; } - static void add(Snippet snippet) { - snippets.value.add(snippet); - snippets.notify(); + void add(Snippet snippet) { + final newSnippets = [...state.snippets, snippet]; + final newTags = _computeTags(newSnippets); + state = state.copyWith(snippets: newSnippets, tags: newTags); Stores.snippet.put(snippet); - _updateTags(); bakSync.sync(milliDelay: 1000); } - static void del(Snippet snippet) { - snippets.value.remove(snippet); - snippets.notify(); + void del(Snippet snippet) { + final newSnippets = state.snippets.where((s) => s != snippet).toList(); + final newTags = _computeTags(newSnippets); + state = state.copyWith(snippets: newSnippets, tags: newTags); Stores.snippet.delete(snippet); - _updateTags(); bakSync.sync(milliDelay: 1000); } - static void update(Snippet old, Snippet newOne) { - snippets.value.remove(old); - snippets.value.add(newOne); - snippets.notify(); + void update(Snippet old, Snippet newOne) { + final newSnippets = state.snippets.map((s) => s == old ? newOne : s).toList(); + final newTags = _computeTags(newSnippets); + state = state.copyWith(snippets: newSnippets, tags: newTags); Stores.snippet.delete(old); Stores.snippet.put(newOne); - _updateTags(); bakSync.sync(milliDelay: 1000); } - static void renameTag(String old, String newOne) { - for (final s in snippets.value) { + void renameTag(String old, String newOne) { + final updatedSnippets = []; + for (final s in state.snippets) { if (s.tags?.contains(old) ?? false) { - s.tags?.remove(old); - s.tags?.add(newOne); - Stores.snippet.put(s); + final newTags = Set.from(s.tags!); + newTags.remove(old); + newTags.add(newOne); + final updatedSnippet = s.copyWith(tags: newTags.toList()); + updatedSnippets.add(updatedSnippet); + Stores.snippet.put(updatedSnippet); + } else { + updatedSnippets.add(s); } } - _updateTags(); + final newTags = _computeTags(updatedSnippets); + state = state.copyWith(snippets: updatedSnippets, tags: newTags); bakSync.sync(milliDelay: 1000); } } diff --git a/lib/data/provider/snippet.freezed.dart b/lib/data/provider/snippet.freezed.dart new file mode 100644 index 00000000..f5d54c08 --- /dev/null +++ b/lib/data/provider/snippet.freezed.dart @@ -0,0 +1,286 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'snippet.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$SnippetState { + + List get snippets; Set get tags; +/// Create a copy of SnippetState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnippetStateCopyWith get copyWith => _$SnippetStateCopyWithImpl(this as SnippetState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnippetState&&const DeepCollectionEquality().equals(other.snippets, snippets)&&const DeepCollectionEquality().equals(other.tags, tags)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(snippets),const DeepCollectionEquality().hash(tags)); + +@override +String toString() { + return 'SnippetState(snippets: $snippets, tags: $tags)'; +} + + +} + +/// @nodoc +abstract mixin class $SnippetStateCopyWith<$Res> { + factory $SnippetStateCopyWith(SnippetState value, $Res Function(SnippetState) _then) = _$SnippetStateCopyWithImpl; +@useResult +$Res call({ + List snippets, Set tags +}); + + + + +} +/// @nodoc +class _$SnippetStateCopyWithImpl<$Res> + implements $SnippetStateCopyWith<$Res> { + _$SnippetStateCopyWithImpl(this._self, this._then); + + final SnippetState _self; + final $Res Function(SnippetState) _then; + +/// Create a copy of SnippetState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? snippets = null,Object? tags = null,}) { + return _then(_self.copyWith( +snippets: null == snippets ? _self.snippets : snippets // ignore: cast_nullable_to_non_nullable +as List,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable +as Set, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SnippetState]. +extension SnippetStatePatterns on SnippetState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SnippetState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SnippetState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SnippetState value) $default,){ +final _that = this; +switch (_that) { +case _SnippetState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SnippetState value)? $default,){ +final _that = this; +switch (_that) { +case _SnippetState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List snippets, Set tags)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SnippetState() when $default != null: +return $default(_that.snippets,_that.tags);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List snippets, Set tags) $default,) {final _that = this; +switch (_that) { +case _SnippetState(): +return $default(_that.snippets,_that.tags);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List snippets, Set tags)? $default,) {final _that = this; +switch (_that) { +case _SnippetState() when $default != null: +return $default(_that.snippets,_that.tags);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _SnippetState implements SnippetState { + const _SnippetState({final List snippets = const [], final Set tags = const {}}): _snippets = snippets,_tags = tags; + + + final List _snippets; +@override@JsonKey() List get snippets { + if (_snippets is EqualUnmodifiableListView) return _snippets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_snippets); +} + + final Set _tags; +@override@JsonKey() Set get tags { + if (_tags is EqualUnmodifiableSetView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_tags); +} + + +/// Create a copy of SnippetState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnippetStateCopyWith<_SnippetState> get copyWith => __$SnippetStateCopyWithImpl<_SnippetState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnippetState&&const DeepCollectionEquality().equals(other._snippets, _snippets)&&const DeepCollectionEquality().equals(other._tags, _tags)); +} + + +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_snippets),const DeepCollectionEquality().hash(_tags)); + +@override +String toString() { + return 'SnippetState(snippets: $snippets, tags: $tags)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnippetStateCopyWith<$Res> implements $SnippetStateCopyWith<$Res> { + factory _$SnippetStateCopyWith(_SnippetState value, $Res Function(_SnippetState) _then) = __$SnippetStateCopyWithImpl; +@override @useResult +$Res call({ + List snippets, Set tags +}); + + + + +} +/// @nodoc +class __$SnippetStateCopyWithImpl<$Res> + implements _$SnippetStateCopyWith<$Res> { + __$SnippetStateCopyWithImpl(this._self, this._then); + + final _SnippetState _self; + final $Res Function(_SnippetState) _then; + +/// Create a copy of SnippetState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? snippets = null,Object? tags = null,}) { + return _then(_SnippetState( +snippets: null == snippets ? _self._snippets : snippets // ignore: cast_nullable_to_non_nullable +as List,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable +as Set, + )); +} + + +} + +// dart format on diff --git a/lib/data/provider/snippet.g.dart b/lib/data/provider/snippet.g.dart new file mode 100644 index 00000000..96d07c9e --- /dev/null +++ b/lib/data/provider/snippet.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'snippet.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$snippetNotifierHash() => r'caf0361f9a0346fb99cb90f032f1ceb29446dd71'; + +/// See also [SnippetNotifier]. +@ProviderFor(SnippetNotifier) +final snippetNotifierProvider = + NotifierProvider.internal( + SnippetNotifier.new, + name: r'snippetNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$snippetNotifierHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$SnippetNotifier = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/systemd.dart b/lib/data/provider/systemd.dart index 71bdc46f..abd64438 100644 --- a/lib/data/provider/systemd.dart +++ b/lib/data/provider/systemd.dart @@ -1,45 +1,57 @@ 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/extension/ssh_client.dart'; import 'package:server_box/data/model/app/scripts/script_consts.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/systemd.dart'; import 'package:server_box/data/provider/server.dart'; -final class SystemdProvider { - late final VNode _si; +part 'systemd.freezed.dart'; +part 'systemd.g.dart'; - SystemdProvider.init(Spi spi) { - _si = ServerProvider.pick(spi: spi)!; - getUnits(); - } +@freezed +abstract class SystemdState with _$SystemdState { + const factory SystemdState({ + @Default(false) bool isBusy, + @Default([]) List units, + @Default(SystemdScopeFilter.all) SystemdScopeFilter scopeFilter, + }) = _SystemdState; +} - final isBusy = false.vn; - final units = [].vn; - final scopeFilter = SystemdScopeFilter.all.vn; +@riverpod +class SystemdNotifier extends _$SystemdNotifier { + late final ServerState _si; - void dispose() { - isBusy.dispose(); - units.dispose(); - scopeFilter.dispose(); + @override + SystemdState build(Spi spi) { + final si = ref.read(individualServerNotifierProvider(spi.id)); + _si = si; + // Async initialization + Future.microtask(() => getUnits()); + return const SystemdState(); } List get filteredUnits { - switch (scopeFilter.value) { + switch (state.scopeFilter) { case SystemdScopeFilter.all: - return units.value; + return state.units; case SystemdScopeFilter.system: - return units.value.where((unit) => unit.scope == SystemdUnitScope.system).toList(); + return state.units.where((unit) => unit.scope == SystemdUnitScope.system).toList(); case SystemdScopeFilter.user: - return units.value.where((unit) => unit.scope == SystemdUnitScope.user).toList(); + return state.units.where((unit) => unit.scope == SystemdUnitScope.user).toList(); } } + void setScopeFilter(SystemdScopeFilter filter) { + state = state.copyWith(scopeFilter: filter); + } + Future getUnits() async { - isBusy.value = true; + state = state.copyWith(isBusy: true); try { - final client = _si.value.client; + final client = _si.client; final result = await client!.execForOutput(_getUnitsCmd); final units = result.split('\n'); @@ -57,12 +69,11 @@ final class SystemdProvider { final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user); final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system); - this.units.value = [...parsedUserUnits, ...parsedSystemUnits]; + state = state.copyWith(units: [...parsedUserUnits, ...parsedSystemUnits], isBusy: false); } catch (e, s) { dprint('Parse systemd', e, s); + state = state.copyWith(isBusy: false); } - - isBusy.value = false; } Future> _parseUnitObj(List unitNames, SystemdUnitScope scope) async { @@ -75,7 +86,7 @@ for unit in ${unitNames_.join(' ')}; do echo -n "\n${ScriptConstants.separator}\n" done '''; - final client = _si.value.client!; + final client = _si.client!; final result = await client.execForOutput(script); final units = result.split(ScriptConstants.separator); diff --git a/lib/data/provider/systemd.freezed.dart b/lib/data/provider/systemd.freezed.dart new file mode 100644 index 00000000..87b46f26 --- /dev/null +++ b/lib/data/provider/systemd.freezed.dart @@ -0,0 +1,283 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'systemd.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$SystemdState { + + bool get isBusy; List get units; SystemdScopeFilter get scopeFilter; +/// Create a copy of SystemdState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SystemdStateCopyWith get copyWith => _$SystemdStateCopyWithImpl(this as SystemdState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other.units, units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(units),scopeFilter); + +@override +String toString() { + return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)'; +} + + +} + +/// @nodoc +abstract mixin class $SystemdStateCopyWith<$Res> { + factory $SystemdStateCopyWith(SystemdState value, $Res Function(SystemdState) _then) = _$SystemdStateCopyWithImpl; +@useResult +$Res call({ + bool isBusy, List units, SystemdScopeFilter scopeFilter +}); + + + + +} +/// @nodoc +class _$SystemdStateCopyWithImpl<$Res> + implements $SystemdStateCopyWith<$Res> { + _$SystemdStateCopyWithImpl(this._self, this._then); + + final SystemdState _self; + final $Res Function(SystemdState) _then; + +/// Create a copy of SystemdState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) { + return _then(_self.copyWith( +isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable +as bool,units: null == units ? _self.units : units // ignore: cast_nullable_to_non_nullable +as List,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable +as SystemdScopeFilter, + )); +} + +} + + +/// Adds pattern-matching-related methods to [SystemdState]. +extension SystemdStatePatterns on SystemdState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SystemdState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SystemdState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SystemdState value) $default,){ +final _that = this; +switch (_that) { +case _SystemdState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SystemdState value)? $default,){ +final _that = this; +switch (_that) { +case _SystemdState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool isBusy, List units, SystemdScopeFilter scopeFilter)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SystemdState() when $default != null: +return $default(_that.isBusy,_that.units,_that.scopeFilter);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool isBusy, List units, SystemdScopeFilter scopeFilter) $default,) {final _that = this; +switch (_that) { +case _SystemdState(): +return $default(_that.isBusy,_that.units,_that.scopeFilter);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isBusy, List units, SystemdScopeFilter scopeFilter)? $default,) {final _that = this; +switch (_that) { +case _SystemdState() when $default != null: +return $default(_that.isBusy,_that.units,_that.scopeFilter);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _SystemdState implements SystemdState { + const _SystemdState({this.isBusy = false, final List units = const [], this.scopeFilter = SystemdScopeFilter.all}): _units = units; + + +@override@JsonKey() final bool isBusy; + final List _units; +@override@JsonKey() List get units { + if (_units is EqualUnmodifiableListView) return _units; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_units); +} + +@override@JsonKey() final SystemdScopeFilter scopeFilter; + +/// Create a copy of SystemdState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SystemdStateCopyWith<_SystemdState> get copyWith => __$SystemdStateCopyWithImpl<_SystemdState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SystemdState&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&const DeepCollectionEquality().equals(other._units, _units)&&(identical(other.scopeFilter, scopeFilter) || other.scopeFilter == scopeFilter)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isBusy,const DeepCollectionEquality().hash(_units),scopeFilter); + +@override +String toString() { + return 'SystemdState(isBusy: $isBusy, units: $units, scopeFilter: $scopeFilter)'; +} + + +} + +/// @nodoc +abstract mixin class _$SystemdStateCopyWith<$Res> implements $SystemdStateCopyWith<$Res> { + factory _$SystemdStateCopyWith(_SystemdState value, $Res Function(_SystemdState) _then) = __$SystemdStateCopyWithImpl; +@override @useResult +$Res call({ + bool isBusy, List units, SystemdScopeFilter scopeFilter +}); + + + + +} +/// @nodoc +class __$SystemdStateCopyWithImpl<$Res> + implements _$SystemdStateCopyWith<$Res> { + __$SystemdStateCopyWithImpl(this._self, this._then); + + final _SystemdState _self; + final $Res Function(_SystemdState) _then; + +/// Create a copy of SystemdState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? isBusy = null,Object? units = null,Object? scopeFilter = null,}) { + return _then(_SystemdState( +isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable +as bool,units: null == units ? _self._units : units // ignore: cast_nullable_to_non_nullable +as List,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable +as SystemdScopeFilter, + )); +} + + +} + +// dart format on diff --git a/lib/data/provider/systemd.g.dart b/lib/data/provider/systemd.g.dart new file mode 100644 index 00000000..c9c0495f --- /dev/null +++ b/lib/data/provider/systemd.g.dart @@ -0,0 +1,163 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'systemd.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$systemdNotifierHash() => r'617fb7637fbc5c5100e5b522d246984f22b44cca'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$SystemdNotifier + extends BuildlessAutoDisposeNotifier { + late final Spi spi; + + SystemdState build(Spi spi); +} + +/// See also [SystemdNotifier]. +@ProviderFor(SystemdNotifier) +const systemdNotifierProvider = SystemdNotifierFamily(); + +/// See also [SystemdNotifier]. +class SystemdNotifierFamily extends Family { + /// See also [SystemdNotifier]. + const SystemdNotifierFamily(); + + /// See also [SystemdNotifier]. + SystemdNotifierProvider call(Spi spi) { + return SystemdNotifierProvider(spi); + } + + @override + SystemdNotifierProvider getProviderOverride( + covariant SystemdNotifierProvider provider, + ) { + return call(provider.spi); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'systemdNotifierProvider'; +} + +/// See also [SystemdNotifier]. +class SystemdNotifierProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [SystemdNotifier]. + SystemdNotifierProvider(Spi spi) + : this._internal( + () => SystemdNotifier()..spi = spi, + from: systemdNotifierProvider, + name: r'systemdNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$systemdNotifierHash, + dependencies: SystemdNotifierFamily._dependencies, + allTransitiveDependencies: + SystemdNotifierFamily._allTransitiveDependencies, + spi: spi, + ); + + SystemdNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.spi, + }) : super.internal(); + + final Spi spi; + + @override + SystemdState runNotifierBuild(covariant SystemdNotifier notifier) { + return notifier.build(spi); + } + + @override + Override overrideWith(SystemdNotifier Function() create) { + return ProviderOverride( + origin: this, + override: SystemdNotifierProvider._internal( + () => create()..spi = spi, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + spi: spi, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _SystemdNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SystemdNotifierProvider && other.spi == spi; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, spi.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SystemdNotifierRef on AutoDisposeNotifierProviderRef { + /// The parameter `spi` of this provider. + Spi get spi; +} + +class _SystemdNotifierProviderElement + extends AutoDisposeNotifierProviderElement + with SystemdNotifierRef { + _SystemdNotifierProviderElement(super.provider); + + @override + Spi get spi => (origin as SystemdNotifierProvider).spi; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/data/provider/virtual_keyboard.dart b/lib/data/provider/virtual_keyboard.dart index 8538da33..df8fac12 100644 --- a/lib/data/provider/virtual_keyboard.dart +++ b/lib/data/provider/virtual_keyboard.dart @@ -1,56 +1,63 @@ -import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:server_box/data/res/store.dart'; import 'package:xterm/core.dart'; -class VirtKeyProvider extends TerminalInputHandler with ChangeNotifier { - VirtKeyProvider(); +part 'virtual_keyboard.g.dart'; +part 'virtual_keyboard.freezed.dart'; - bool _ctrl = false; - bool get ctrl => _ctrl; - set ctrl(bool value) { - if (value != _ctrl) { - _ctrl = value; - notifyListeners(); +@freezed +abstract class VirtKeyState with _$VirtKeyState { + const factory VirtKeyState({ + @Default(false) final bool ctrl, + @Default(false) final bool alt, + @Default(false) final bool shift, + }) = _VirtKeyState; +} + +@riverpod +class VirtKeyboard extends _$VirtKeyboard implements TerminalInputHandler { + @override + VirtKeyState build() { + return const VirtKeyState(); + } + + bool get ctrl => state.ctrl; + bool get alt => state.alt; + bool get shift => state.shift; + + void setCtrl(bool value) { + if (value != state.ctrl) { + state = state.copyWith(ctrl: value); } } - bool _alt = false; - bool get alt => _alt; - set alt(bool value) { - if (value != _alt) { - _alt = value; - notifyListeners(); + void setAlt(bool value) { + if (value != state.alt) { + state = state.copyWith(alt: value); } } - bool _shift = false; - bool get shift => _shift; - set shift(bool value) { - if (value != _shift) { - _shift = value; - notifyListeners(); + void setShift(bool value) { + if (value != state.shift) { + state = state.copyWith(shift: value); } } void reset(TerminalKeyboardEvent e) { - if (e.ctrl) { - ctrl = false; - } - if (e.alt) { - alt = false; - } - if (e.shift) { - shift = false; - } - notifyListeners(); + state = state.copyWith( + ctrl: e.ctrl ? false : state.ctrl, + alt: e.alt ? false : state.alt, + shift: e.shift ? false : state.shift, + ); } @override String? call(TerminalKeyboardEvent event) { final e = event.copyWith( - ctrl: event.ctrl || ctrl, - alt: event.alt || alt, - shift: event.shift || shift, + ctrl: event.ctrl || state.ctrl, + alt: event.alt || state.alt, + shift: event.shift || state.shift, ); if (Stores.setting.sshVirtualKeyAutoOff.fetch()) { reset(e); diff --git a/lib/data/provider/virtual_keyboard.freezed.dart b/lib/data/provider/virtual_keyboard.freezed.dart new file mode 100644 index 00000000..5074e811 --- /dev/null +++ b/lib/data/provider/virtual_keyboard.freezed.dart @@ -0,0 +1,277 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'virtual_keyboard.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$VirtKeyState { + + bool get ctrl; bool get alt; bool get shift; +/// Create a copy of VirtKeyState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$VirtKeyStateCopyWith get copyWith => _$VirtKeyStateCopyWithImpl(this as VirtKeyState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift)); +} + + +@override +int get hashCode => Object.hash(runtimeType,ctrl,alt,shift); + +@override +String toString() { + return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)'; +} + + +} + +/// @nodoc +abstract mixin class $VirtKeyStateCopyWith<$Res> { + factory $VirtKeyStateCopyWith(VirtKeyState value, $Res Function(VirtKeyState) _then) = _$VirtKeyStateCopyWithImpl; +@useResult +$Res call({ + bool ctrl, bool alt, bool shift +}); + + + + +} +/// @nodoc +class _$VirtKeyStateCopyWithImpl<$Res> + implements $VirtKeyStateCopyWith<$Res> { + _$VirtKeyStateCopyWithImpl(this._self, this._then); + + final VirtKeyState _self; + final $Res Function(VirtKeyState) _then; + +/// Create a copy of VirtKeyState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) { + return _then(_self.copyWith( +ctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable +as bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable +as bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [VirtKeyState]. +extension VirtKeyStatePatterns on VirtKeyState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _VirtKeyState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _VirtKeyState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _VirtKeyState value) $default,){ +final _that = this; +switch (_that) { +case _VirtKeyState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _VirtKeyState value)? $default,){ +final _that = this; +switch (_that) { +case _VirtKeyState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool ctrl, bool alt, bool shift)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _VirtKeyState() when $default != null: +return $default(_that.ctrl,_that.alt,_that.shift);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool ctrl, bool alt, bool shift) $default,) {final _that = this; +switch (_that) { +case _VirtKeyState(): +return $default(_that.ctrl,_that.alt,_that.shift);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool ctrl, bool alt, bool shift)? $default,) {final _that = this; +switch (_that) { +case _VirtKeyState() when $default != null: +return $default(_that.ctrl,_that.alt,_that.shift);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _VirtKeyState implements VirtKeyState { + const _VirtKeyState({this.ctrl = false, this.alt = false, this.shift = false}); + + +@override@JsonKey() final bool ctrl; +@override@JsonKey() final bool alt; +@override@JsonKey() final bool shift; + +/// Create a copy of VirtKeyState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$VirtKeyStateCopyWith<_VirtKeyState> get copyWith => __$VirtKeyStateCopyWithImpl<_VirtKeyState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _VirtKeyState&&(identical(other.ctrl, ctrl) || other.ctrl == ctrl)&&(identical(other.alt, alt) || other.alt == alt)&&(identical(other.shift, shift) || other.shift == shift)); +} + + +@override +int get hashCode => Object.hash(runtimeType,ctrl,alt,shift); + +@override +String toString() { + return 'VirtKeyState(ctrl: $ctrl, alt: $alt, shift: $shift)'; +} + + +} + +/// @nodoc +abstract mixin class _$VirtKeyStateCopyWith<$Res> implements $VirtKeyStateCopyWith<$Res> { + factory _$VirtKeyStateCopyWith(_VirtKeyState value, $Res Function(_VirtKeyState) _then) = __$VirtKeyStateCopyWithImpl; +@override @useResult +$Res call({ + bool ctrl, bool alt, bool shift +}); + + + + +} +/// @nodoc +class __$VirtKeyStateCopyWithImpl<$Res> + implements _$VirtKeyStateCopyWith<$Res> { + __$VirtKeyStateCopyWithImpl(this._self, this._then); + + final _VirtKeyState _self; + final $Res Function(_VirtKeyState) _then; + +/// Create a copy of VirtKeyState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? ctrl = null,Object? alt = null,Object? shift = null,}) { + return _then(_VirtKeyState( +ctrl: null == ctrl ? _self.ctrl : ctrl // ignore: cast_nullable_to_non_nullable +as bool,alt: null == alt ? _self.alt : alt // ignore: cast_nullable_to_non_nullable +as bool,shift: null == shift ? _self.shift : shift // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/data/provider/virtual_keyboard.g.dart b/lib/data/provider/virtual_keyboard.g.dart new file mode 100644 index 00000000..ffabb564 --- /dev/null +++ b/lib/data/provider/virtual_keyboard.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'virtual_keyboard.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$virtKeyboardHash() => r'1327d412bfb0dd261f3b555f353a8852b4f753e5'; + +/// See also [VirtKeyboard]. +@ProviderFor(VirtKeyboard) +final virtKeyboardProvider = + AutoDisposeNotifierProvider.internal( + VirtKeyboard.new, + name: r'virtKeyboardProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$virtKeyboardHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$VirtKeyboard = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/main.dart b/lib/main.dart index f1073e42..3d51bf41 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,18 +5,13 @@ import 'dart:async'; import 'package:computer/computer.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:logging/logging.dart'; import 'package:server_box/app.dart'; -import 'package:server_box/core/sync.dart'; import 'package:server_box/data/model/app/menu/server_func.dart'; import 'package:server_box/data/model/app/server_detail_card.dart'; -import 'package:server_box/data/provider/private_key.dart'; -import 'package:server_box/data/provider/server.dart'; -import 'package:server_box/data/provider/sftp.dart'; -import 'package:server_box/data/provider/snippet.dart'; import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/ssh/session_manager.dart'; @@ -26,7 +21,7 @@ import 'package:server_box/hive/hive_registrar.g.dart'; Future main() async { _runInZone(() async { await _initApp(); - runApp(const MyApp()); + runApp(ProviderScope(child: const MyApp())); }); } @@ -66,12 +61,6 @@ Future _initData() async { // DO DB migration before load any provider. await _doDbMigrate(); - // DO NOT change the order of these providers. - PrivateKeyProvider.instance.load(); - SnippetProvider.instance.load(); - ServerProvider.instance.load(); - SftpProvider.instance.load(); - if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta; FontUtils.loadFrom(Stores.setting.fontPath.fetch()); @@ -94,10 +83,7 @@ void _doPlatformRelated() async { } final serversCount = Stores.server.keys().length; - BackgroundIsolateBinaryMessenger.ensureInitialized(RootIsolateToken.instance!); Computer.shared.turnOn(workersCount: (serversCount / 3).round() + 1); // Plus 1 to avoid 0. - - bakSync.sync(); } // It may contains some async heavy funcs. diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart index ed8becef..67a630bb 100644 --- a/lib/view/page/backup.dart +++ b/lib/view/page/backup.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:computer/computer.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/sync.dart'; @@ -17,16 +18,16 @@ import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/store.dart'; import 'package:webdav_client_plus/webdav_client_plus.dart'; -class BackupPage extends StatefulWidget { +class BackupPage extends ConsumerStatefulWidget { const BackupPage({super.key}); @override - State createState() => _BackupPageState(); + ConsumerState createState() => _BackupPageState(); static const route = AppRouteNoArg(page: BackupPage.new, path: '/backup'); } -final class _BackupPageState extends State with AutomaticKeepAliveClientMixin { +final class _BackupPageState extends ConsumerState with AutomaticKeepAliveClientMixin { final webdavLoading = false.vn; final gistLoading = false.vn; @@ -401,8 +402,9 @@ final class _BackupPageState extends State with AutomaticKeepAliveCl child: SingleChildScrollView(child: Text(libL10n.askContinue('${libL10n.import} [$snippetNames]'))), actions: Btn.ok( onTap: () { + final notifier = ref.read(snippetNotifierProvider.notifier); for (final snippet in snippets) { - SnippetProvider.add(snippet); + notifier.add(snippet); } context.pop(); context.pop(); diff --git a/lib/view/page/container/actions.dart b/lib/view/page/container/actions.dart index d44dcac6..a84ff81b 100644 --- a/lib/view/page/container/actions.dart +++ b/lib/view/page/container/actions.dart @@ -1,6 +1,12 @@ part of 'container.dart'; extension on _ContainerPageState { + /// The notifier for the container state. + ContainerNotifier get _containerNotifier => ref.read(_provider.notifier); + + /// Watch the current state of the container. + ContainerState get _containerState => ref.watch(_provider); + Future _showAddFAB() async { final imageCtrl = TextEditingController(); final nameCtrl = TextEditingController(); @@ -79,7 +85,7 @@ extension on _ContainerPageState { onPressed: () async { context.pop(); - final (result, err) = await context.showLoadingDialog(fn: () => _container.run(cmd)); + final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.run(cmd)); if (err != null || result != null) { final e = result?.message ?? err?.toString(); context.showRoundDialog(title: libL10n.error, child: Text(e.toString())); @@ -111,7 +117,7 @@ extension on _ContainerPageState { void _onSaveDockerHost(String val) { context.pop(); Stores.container.put(widget.args.spi.id, val.trim()); - _container.refresh(); + _containerNotifier.refresh(); } void _showImageRmDialog(ContainerImg e) { @@ -121,7 +127,7 @@ extension on _ContainerPageState { actions: Btn.ok( onTap: () async { context.pop(); - final result = await _container.run('rmi ${e.id} -f'); + final result = await _containerNotifier.run('rmi ${e.id} -f'); if (result != null) { context.showSnackBar(result.message ?? 'null'); } @@ -163,7 +169,9 @@ extension on _ContainerPageState { onTap: () async { context.pop(); - final (result, err) = await context.showLoadingDialog(fn: () => _container.delete(id, force)); + final (result, err) = await context.showLoadingDialog( + fn: () => _containerNotifier.delete(id, force), + ); if (err != null || result != null) { final e = result?.message ?? err?.toString(); context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null')); @@ -173,21 +181,21 @@ extension on _ContainerPageState { ); break; case ContainerMenu.start: - final (result, err) = await context.showLoadingDialog(fn: () => _container.start(id)); + final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.start(id)); if (err != null || result != null) { final e = result?.message ?? err?.toString(); context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null')); } break; case ContainerMenu.stop: - final (result, err) = await context.showLoadingDialog(fn: () => _container.stop(id)); + final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.stop(id)); if (err != null || result != null) { final e = result?.message ?? err?.toString(); context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null')); } break; case ContainerMenu.restart: - final (result, err) = await context.showLoadingDialog(fn: () => _container.restart(id)); + final (result, err) = await context.showLoadingDialog(fn: () => _containerNotifier.restart(id)); if (err != null || result != null) { final e = result?.message ?? err?.toString(); context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null')); @@ -197,7 +205,7 @@ extension on _ContainerPageState { final args = SshPageArgs( spi: widget.args.spi, initCmd: - '${switch (_container.type) { + '${switch (_containerState.type) { ContainerType.podman => 'podman', ContainerType.docker => 'docker', }} logs -f --tail 100 ${dItem.id}', @@ -208,7 +216,7 @@ extension on _ContainerPageState { final args = SshPageArgs( spi: widget.args.spi, initCmd: - '${switch (_container.type) { + '${switch (_containerState.type) { ContainerType.podman => 'podman', ContainerType.docker => 'docker', }} exec -it ${dItem.id} sh', @@ -222,7 +230,7 @@ extension on _ContainerPageState { if (Stores.setting.containerAutoRefresh.fetch()) { Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (timer) { if (mounted) { - _container.refresh(isAuto: true); + _containerNotifier.refresh(isAuto: true); } else { timer.cancel(); } diff --git a/lib/view/page/container/container.dart b/lib/view/page/container/container.dart index 0a2df85e..8f9a950b 100644 --- a/lib/view/page/container/container.dart +++ b/lib/view/page/container/container.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; -import 'package:provider/provider.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/route.dart'; import 'package:server_box/data/model/app/error.dart'; @@ -12,81 +12,79 @@ import 'package:server_box/data/model/app/menu/container.dart'; import 'package:server_box/data/model/container/image.dart'; import 'package:server_box/data/model/container/ps.dart'; import 'package:server_box/data/model/container/type.dart'; -import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/provider/container.dart'; +import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/view/page/ssh/page/page.dart'; part 'actions.dart'; part 'types.dart'; -class ContainerPage extends StatefulWidget { +class ContainerPage extends ConsumerStatefulWidget { final SpiRequiredArgs args; const ContainerPage({required this.args, super.key}); @override - State createState() => _ContainerPageState(); + ConsumerState createState() => _ContainerPageState(); static const route = AppRouteArg(page: ContainerPage.new, path: '/container'); } -class _ContainerPageState extends State { +class _ContainerPageState extends ConsumerState { final _textController = TextEditingController(); - late final _container = ContainerProvider( - client: widget.args.spi.server?.value.client, - userName: widget.args.spi.user, - hostId: widget.args.spi.id, - context: context, - ); + late final ContainerNotifierProvider _provider; @override void dispose() { super.dispose(); _textController.dispose(); - _container.dispose(); } @override void initState() { super.initState(); + final serverState = ref.read(individualServerNotifierProvider(widget.args.spi.id)); + _provider = containerNotifierProvider( + serverState.client, + widget.args.spi.user, + widget.args.spi.id, + context, + ); _initAutoRefresh(); } @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => _container, - builder: (_, _) => Consumer( - builder: (_, _, _) { - return Scaffold( - appBar: _buildAppBar, - body: SafeArea(child: _buildMain), - floatingActionButton: _container.error == null ? _buildFAB : null, - ); - }, - ), + final err = ref.watch(_provider.select((p) => p.error)); + + return Scaffold( + appBar: _buildAppBar(), + body: SafeArea(child: _buildMain()), + floatingActionButton: err == null ? _buildFAB() : null, ); } - CustomAppBar get _buildAppBar { + CustomAppBar _buildAppBar() { return CustomAppBar( centerTitle: true, title: TwoLineText(up: l10n.container, down: widget.args.spi.name), actions: [ IconButton( - onPressed: () => context.showLoadingDialog(fn: () => _container.refresh()), + onPressed: () => context.showLoadingDialog(fn: () => _containerNotifier.refresh()), icon: const Icon(Icons.refresh), ), ], ); } - Widget get _buildFAB { + Widget _buildFAB() { return FloatingActionButton(onPressed: () async => await _showAddFAB(), child: const Icon(Icons.add)); } - Widget get _buildMain { - if (_container.error != null && _container.items == null) { + Widget _buildMain() { + final containerState = _containerState; + + if (containerState.error != null && containerState.items == null) { return SizedBox.expand( child: Column( children: [ @@ -95,7 +93,7 @@ class _ContainerPageState extends State { UIs.height13, Padding( padding: const EdgeInsets.symmetric(horizontal: 23), - child: Text(_container.error.toString()), + child: Text(containerState.error.toString()), ), const Spacer(), UIs.height13, @@ -104,27 +102,27 @@ class _ContainerPageState extends State { ).paddingSymmetric(horizontal: 13), ); } - if (_container.items == null || _container.images == null) { + if (containerState.items == null || containerState.images == null) { return UIs.centerLoading; } return AutoMultiList( children: [ - _buildLoading(), - _buildVersion(), - _buildPs(), - _buildImage(), - _buildEmptyStateMessage(), + _buildLoading(containerState), + _buildVersion(containerState), + _buildPs(containerState), + _buildImage(containerState), + _buildEmptyStateMessage(containerState), _buildPruneBtns, _buildSettingsBtns, ], ); } - Widget _buildEmptyStateMessage() { - final emptyImgs = _container.images?.isEmpty ?? true; - final emptyPs = _container.items?.isEmpty ?? true; - if (emptyPs && emptyImgs && _container.runLog == null) { + Widget _buildEmptyStateMessage(ContainerState containerState) { + final emptyImgs = containerState.images?.isEmpty ?? true; + final emptyPs = containerState.items?.isEmpty ?? true; + if (emptyPs && emptyImgs && containerState.runLog == null) { return CardX( child: Padding( padding: const EdgeInsets.fromLTRB(17, 17, 17, 7), @@ -135,13 +133,13 @@ class _ContainerPageState extends State { return UIs.placeholder; } - Widget _buildImage() { + Widget _buildImage(ContainerState containerState) { return ExpandTile( leading: const Icon(MingCute.clapperboard_line), title: Text(l10n.imagesList), - subtitle: Text(l10n.dockerImagesFmt(_container.images!.length), style: UIs.textGrey), - initiallyExpanded: (_container.images?.length ?? 0) <= 3, - children: _container.images?.map(_buildImageItem).toList() ?? [], + subtitle: Text(l10n.dockerImagesFmt(containerState.images?.length ?? 'null'), style: UIs.textGrey), + initiallyExpanded: (containerState.images?.length ?? 0) <= 3, + children: containerState.images?.map(_buildImageItem).toList() ?? [], ).cardx; } @@ -161,34 +159,34 @@ class _ContainerPageState extends State { ); } - Widget _buildLoading() { - if (_container.runLog == null) return UIs.placeholder; + Widget _buildLoading(ContainerState containerState) { + if (containerState.runLog == null) return UIs.placeholder; return Padding( padding: const EdgeInsets.all(17), child: Column( children: [ const Center(child: CircularProgressIndicator()), UIs.height13, - Text(_container.runLog ?? '...'), + Text(containerState.runLog ?? '...'), ], ), ); } - Widget _buildVersion() { + Widget _buildVersion(ContainerState containerState) { return CardX( child: Padding( padding: const EdgeInsets.all(17), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text(_container.type.name.capitalize), Text(_container.version ?? l10n.unknown)], + children: [Text(containerState.type.name.capitalize), Text(containerState.version ?? l10n.unknown)], ), ), ); } - Widget _buildPs() { - final items = _container.items; + Widget _buildPs(ContainerState containerState) { + final items = containerState.items; if (items == null) return UIs.placeholder; final running = items.where((e) => e.running).length; final stopped = items.length - running; @@ -309,16 +307,17 @@ class _ContainerPageState extends State { Widget _buildPruneBtn(_PruneTypes type) { final title = type.name.capitalize; + final containerNotifier = _containerNotifier; return ListTile( onTap: () async { await _showPruneDialog( title: title, message: type.tip, onConfirm: switch (type) { - _PruneTypes.images => _container.pruneImages, - _PruneTypes.containers => _container.pruneContainers, - _PruneTypes.volumes => _container.pruneVolumes, - _PruneTypes.system => _container.pruneSystem, + _PruneTypes.images => containerNotifier.pruneImages, + _PruneTypes.containers => containerNotifier.pruneContainers, + _PruneTypes.volumes => containerNotifier.pruneVolumes, + _PruneTypes.system => containerNotifier.pruneSystem, }, ); }, @@ -330,22 +329,26 @@ class _ContainerPageState extends State { Widget get _buildSettingsBtns { final len = _SettingsMenuItems.values.length; if (len == 0) return UIs.placeholder; + final containerState = _containerState; + return ExpandTile( leading: const Icon(Icons.settings), title: Text(libL10n.setting), - initiallyExpanded: _container.error != null, - children: _SettingsMenuItems.values.map(_buildSettingTile).toList(), + initiallyExpanded: containerState.error != null, + children: _SettingsMenuItems.values.map((item) => _buildSettingTile(item, containerState)).toList(), ).cardx; } - Widget _buildSettingTile(_SettingsMenuItems item) { + Widget _buildSettingTile(_SettingsMenuItems item, ContainerState containerState) { final String title; switch (item) { case _SettingsMenuItems.editDockerHost: title = '${libL10n.edit} DOCKER_HOST'; break; case _SettingsMenuItems.switchProvider: - title = _container.type == ContainerType.podman ? l10n.switchTo('Docker') : l10n.switchTo('Podman'); + title = containerState.type == ContainerType.podman + ? l10n.switchTo('Docker') + : l10n.switchTo('Podman'); break; } return ListTile( @@ -355,9 +358,11 @@ class _ContainerPageState extends State { _showEditHostDialog(); break; case _SettingsMenuItems.switchProvider: - _container.setType( - _container.type == ContainerType.docker ? ContainerType.podman : ContainerType.docker, - ); + ref + .read(_provider.notifier) + .setType( + containerState.type == ContainerType.docker ? ContainerType.podman : ContainerType.docker, + ); break; } }, diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index 0eafb614..c9ca7b22 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -1,7 +1,9 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:server_box/core/chan.dart'; +import 'package:server_box/core/sync.dart'; import 'package:server_box/data/model/app/tab.dart'; import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/res/build_data.dart'; @@ -10,16 +12,16 @@ import 'package:server_box/data/res/url.dart'; import 'package:server_box/view/page/setting/entry.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -class HomePage extends StatefulWidget { +class HomePage extends ConsumerStatefulWidget { const HomePage({super.key}); @override - State createState() => _HomePageState(); + ConsumerState createState() => _HomePageState(); static const route = AppRouteNoArg(page: HomePage.new, path: '/'); } -class _HomePageState extends State +class _HomePageState extends ConsumerState with AutomaticKeepAliveClientMixin, AfterLayoutMixin, WidgetsBindingObserver { late final PageController _pageController; @@ -29,11 +31,14 @@ class _HomePageState extends State bool _shouldAuth = false; DateTime? _pausedTime; + late final _notifier = ref.read(serverNotifierProvider.notifier); + late final _provider = ref.read(serverNotifierProvider); + @override void dispose() { super.dispose(); WidgetsBinding.instance.removeObserver(this); - ServerProvider.closeServer(); + Future(() => _notifier.closeServer()); _pageController.dispose(); WakelockPlus.disable(); @@ -76,8 +81,9 @@ class _HomePageState extends State _goAuth(); } } - if (!ServerProvider.isAutoRefreshOn) { - ServerProvider.startAutoRefresh(); + final serverNotifier = _notifier; + if (_provider.autoRefreshTimer == null) { + serverNotifier.startAutoRefresh(); } MethodChans.updateHomeWidget(); break; @@ -92,7 +98,7 @@ class _HomePageState extends State // } } else { //Pros.server.setDisconnected(); - ServerProvider.stopAutoRefresh(); + _notifier.stopAutoRefresh(); } break; default: @@ -194,7 +200,9 @@ class _HomePageState extends State AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context); } MethodChans.updateHomeWidget(); - await ServerProvider.refresh(); + await _notifier.refresh(); + + bakSync.sync(milliDelay: 1000); } // Future _reqNotiPerm() async { @@ -202,7 +210,6 @@ class _HomePageState extends State // final suc = await PermUtils.request(Permission.notification); // if (!suc) { // final noNotiPerm = Stores.setting.noNotiPerm; - // if (noNotiPerm.fetch()) return; // context.showRoundDialog( // title: l10n.error, // child: Text(l10n.noNotiPerm), @@ -212,6 +219,7 @@ class _HomePageState extends State // noNotiPerm.put(true); // context.pop(); // }, + // if (noNotiPerm.fetch()) return; // child: Text(l10n.ok), // ), // ], diff --git a/lib/view/page/ping.dart b/lib/view/page/ping.dart index 75017a21..e72df5ed 100644 --- a/lib/view/page/ping.dart +++ b/lib/view/page/ping.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/server/ping_result.dart'; import 'package:server_box/data/provider/server.dart'; @@ -9,16 +10,16 @@ import 'package:server_box/data/provider/server.dart'; /// Only permit ipv4 / ipv6 / domain chars final targetReg = RegExp(r'[a-zA-Z0-9\.-_:]+'); -class PingPage extends StatefulWidget { +class PingPage extends ConsumerStatefulWidget { const PingPage({super.key}); @override - State createState() => _PingPageState(); + ConsumerState createState() => _PingPageState(); static const route = AppRouteNoArg(page: PingPage.new, path: '/ping'); } -class _PingPageState extends State with AutomaticKeepAliveClientMixin { +class _PingPageState extends ConsumerState with AutomaticKeepAliveClientMixin { late TextEditingController _textEditingController; final _results = ValueNotifier([]); bool get isInit => _results.value.isEmpty; @@ -129,7 +130,7 @@ class _PingPageState extends State with AutomaticKeepAliveClientMixin return; } - if (ServerProvider.serverOrder.value.isEmpty) { + if (ref.read(serverNotifierProvider).serverOrder.isEmpty) { context.showSnackBar(l10n.pingNoServer); return; } @@ -141,13 +142,13 @@ class _PingPageState extends State with AutomaticKeepAliveClientMixin } await Future.wait( - ServerProvider.servers.values.map((v) async { - final e = v.value; - if (e.client == null) { + ref.read(serverNotifierProvider).servers.values.map((spi) async { + final serverState = ref.read(individualServerNotifierProvider(spi.id)); + if (serverState.client == null) { return; } - final result = await e.client!.run('ping -c 3 $target').string; - _results.value.add(PingResult.parse(e.spi.name, result)); + final result = await serverState.client!.run('ping -c 3 $target').string; + _results.value.add(PingResult.parse(spi.name, result)); // [ValueNotifier] only notify when value is changed // But we just add a element to list without changing the list itself // So we need to notify manually diff --git a/lib/view/page/private_key/edit.dart b/lib/view/page/private_key/edit.dart index 4ce260ad..836b5530 100644 --- a/lib/view/page/private_key/edit.dart +++ b/lib/view/page/private_key/edit.dart @@ -4,6 +4,7 @@ import 'package:computer/computer.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/utils/server.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; @@ -17,17 +18,17 @@ final class PrivateKeyEditPageArgs { const PrivateKeyEditPageArgs({this.pki}); } -class PrivateKeyEditPage extends StatefulWidget { +class PrivateKeyEditPage extends ConsumerStatefulWidget { final PrivateKeyEditPageArgs? args; const PrivateKeyEditPage({super.key, this.args}); @override - State createState() => _PrivateKeyEditPageState(); + ConsumerState createState() => _PrivateKeyEditPageState(); static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit'); } -class _PrivateKeyEditPageState extends State { +class _PrivateKeyEditPageState extends ConsumerState { final _nameController = TextEditingController(); final _keyController = TextEditingController(); final _pwdController = TextEditingController(); @@ -39,6 +40,8 @@ class _PrivateKeyEditPageState extends State { final _loading = ValueNotifier(null); + late final _notifier = ref.read(privateKeyNotifierProvider.notifier); + PrivateKeyInfo? get pki => widget.args?.pki; @override @@ -94,7 +97,7 @@ class _PrivateKeyEditPageState extends State { child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.privateKey}(${pki.id})')), actions: Btn.ok( onTap: () { - PrivateKeyProvider.delete(pki); + _notifier.delete(pki); context.pop(); context.pop(); }, @@ -196,9 +199,9 @@ class _PrivateKeyEditPageState extends State { final pki = PrivateKeyInfo(id: name, key: decrypted); final originPki = this.pki; if (originPki != null) { - PrivateKeyProvider.update(originPki, pki); + _notifier.update(originPki, pki); } else { - PrivateKeyProvider.add(pki); + _notifier.add(pki); } } catch (e) { context.showSnackBar(e.toString()); diff --git a/lib/view/page/private_key/list.dart b/lib/view/page/private_key/list.dart index 3351c0d7..949a3ae0 100644 --- a/lib/view/page/private_key/list.dart +++ b/lib/view/page/private_key/list.dart @@ -3,22 +3,23 @@ import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/server/private_key_info.dart'; import 'package:server_box/data/provider/private_key.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/view/page/private_key/edit.dart'; -class PrivateKeysListPage extends StatefulWidget { +class PrivateKeysListPage extends ConsumerStatefulWidget { const PrivateKeysListPage({super.key}); @override - State createState() => _PrivateKeyListState(); + ConsumerState createState() => _PrivateKeyListState(); static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key'); } -class _PrivateKeyListState extends State with AfterLayoutMixin { +class _PrivateKeyListState extends ConsumerState with AfterLayoutMixin { @override Widget build(BuildContext context) { return Scaffold( @@ -31,14 +32,15 @@ class _PrivateKeyListState extends State with AfterLayoutMi } Widget _buildBody() { - return PrivateKeyProvider.pkis.listenVal((pkis) { - if (pkis.isEmpty) { - return Center(child: Text(libL10n.empty)); - } + final privateKeyState = ref.watch(privateKeyNotifierProvider); + final pkis = privateKeyState.keys; + + if (pkis.isEmpty) { + return Center(child: Text(libL10n.empty)); + } - final children = pkis.map(_buildKeyItem).toList(); - return AutoMultiList(children: children); - }); + final children = pkis.map(_buildKeyItem).toList(); + return AutoMultiList(children: children); } Widget _buildKeyItem(PrivateKeyInfo item) { diff --git a/lib/view/page/process.dart b/lib/view/page/process.dart index d4cbd4a9..338d5582 100644 --- a/lib/view/page/process.dart +++ b/lib/view/page/process.dart @@ -3,25 +3,26 @@ import 'dart:async'; import 'package:dartssh2/dartssh2.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/route.dart'; import 'package:server_box/data/model/app/scripts/shell_func.dart'; import 'package:server_box/data/model/server/proc.dart'; -import 'package:server_box/data/model/server/server_private_info.dart'; +import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/res/store.dart'; -class ProcessPage extends StatefulWidget { +class ProcessPage extends ConsumerStatefulWidget { final SpiRequiredArgs args; const ProcessPage({super.key, required this.args}); @override - State createState() => _ProcessPageState(); + ConsumerState createState() => _ProcessPageState(); static const route = AppRouteArg(page: ProcessPage.new, path: '/process'); } -class _ProcessPageState extends State { +class _ProcessPageState extends ConsumerState { late Timer _timer; late MediaQueryData _media; @@ -36,6 +37,8 @@ class _ProcessPageState extends State { ProcSortMode _procSortMode = ProcSortMode.cpu; List _sortModes = List.from(ProcSortMode.values); + late final _provider = individualServerNotifierProvider(widget.args.spi.id); + @override void dispose() { super.dispose(); @@ -45,7 +48,8 @@ class _ProcessPageState extends State { @override void initState() { super.initState(); - _client = widget.args.spi.server?.value.client; + final serverState = ref.read(_provider); + _client = serverState.client; final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()); _timer = Timer.periodic(duration, (_) => _refresh()); } @@ -58,9 +62,10 @@ class _ProcessPageState extends State { Future _refresh() async { if (mounted) { - final systemType = widget.args.spi.server?.value.status.system; + final serverState = ref.read(_provider); + final systemType = serverState.status.system; final result = await _client - ?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType)) + ?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType, customDir: null)) .string; if (result == null || result.isEmpty) { context.showSnackBar(libL10n.empty); diff --git a/lib/view/page/pve.dart b/lib/view/page/pve.dart index 0cfcaa41..f83b807c 100644 --- a/lib/view/page/pve.dart +++ b/lib/view/page/pve.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/server/pve.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; @@ -15,29 +16,30 @@ final class PvePageArgs { const PvePageArgs({required this.spi}); } -final class PvePage extends StatefulWidget { +final class PvePage extends ConsumerStatefulWidget { final PvePageArgs args; const PvePage({super.key, required this.args}); @override - State createState() => _PvePageState(); + ConsumerState createState() => _PvePageState(); static const route = AppRouteArg(page: PvePage.new, path: '/pve'); } const _kHorziPadding = 11.0; -final class _PvePageState extends State { - late final _pve = PveProvider(spi: widget.args.spi); +final class _PvePageState extends ConsumerState { late MediaQueryData _media; Timer? _timer; + late final _provider = pveNotifierProvider(widget.args.spi); + late final _notifier = ref.read(_provider.notifier); + @override void dispose() { super.dispose(); _timer?.cancel(); - _pve.dispose(); } @override @@ -55,43 +57,34 @@ final class _PvePageState extends State { @override Widget build(BuildContext context) { + final pveState = ref.watch(_provider); + + // If there is an error, stop the timer + if (pveState.error != null) { + _timer?.cancel(); + } + return Scaffold( appBar: CustomAppBar( title: TwoLineText(up: 'PVE', down: widget.args.spi.name), actions: [ - ValBuilder( - listenable: _pve.err, - builder: (val) => val == null - ? UIs.placeholder - : Btn.icon( - icon: const Icon(Icons.refresh), - onTap: () { - _pve.err.value = null; - _pve.list(); - _initRefreshTimer(); - }, - ), - ), + pveState.error == null + ? UIs.placeholder + : Btn.icon( + icon: const Icon(Icons.refresh), + onTap: () { + _notifier.list(); + _initRefreshTimer(); + }, + ), ], ), - body: ValBuilder( - listenable: _pve.err, - builder: (val) { - if (val != null) { - _timer?.cancel(); - return Padding( + body: pveState.error != null + ? Padding( padding: const EdgeInsets.all(13), - child: Center(child: Text(val)), - ); - } - return ValBuilder( - listenable: _pve.data, - builder: (val) { - return _buildBody(val); - }, - ); - }, - ), + child: Center(child: Text(pveState.error.toString())), + ) + : _buildBody(pveState.data), ); } @@ -342,7 +335,7 @@ final class _PvePageState extends State { if (!item.available) { return Btn.icon( icon: const Icon(Icons.play_arrow, color: Colors.grey), - onTap: () => _onCtrl(_pve.start, l10n.start, item), + onTap: () => _onCtrl(l10n.start, item, () => _notifier.start(item.node, item.id)), ); } return Row( @@ -350,17 +343,17 @@ final class _PvePageState extends State { Btn.icon( icon: const Icon(Icons.stop, color: Colors.grey, size: 20), padding: pad, - onTap: () => _onCtrl(_pve.stop, l10n.stop, item), + onTap: () => _onCtrl(l10n.stop, item, () => _notifier.stop(item.node, item.id)), ), Btn.icon( icon: const Icon(Icons.refresh, color: Colors.grey, size: 20), padding: pad, - onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item), + onTap: () => _onCtrl(l10n.reboot, item, () => _notifier.reboot(item.node, item.id)), ), Btn.icon( icon: const Icon(Icons.power_off, color: Colors.grey, size: 20), padding: pad, - onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item), + onTap: () => _onCtrl(l10n.shutdown, item, () => _notifier.shutdown(item.node, item.id)), ), ], ); @@ -368,7 +361,7 @@ final class _PvePageState extends State { } extension on _PvePageState { - void _onCtrl(PveCtrlFunc func, String action, PveCtrlIface item) async { + void _onCtrl(String action, PveCtrlIface item, Future Function() func) async { final sure = await context.showRoundDialog( title: libL10n.attention, child: Text(libL10n.askContinue('$action ${item.id}')), @@ -376,7 +369,7 @@ extension on _PvePageState { ); if (sure != true) return; - final (suc, err) = await context.showLoadingDialog(fn: () => func(item.node, item.id)); + final (suc, err) = await context.showLoadingDialog(fn: func); if (suc == true) { context.showSnackBar(libL10n.success); } else { @@ -384,28 +377,40 @@ extension on _PvePageState { } } - /// Add PveNode if [PveProvider.onlyOneNode] is false + /// Add PveNode if only one node exists String _wrapNodeName(PveCtrlIface item) { - if (_pve.onlyOneNode) { + final pveState = ref.read(_provider); + if (pveState.data?.onlyOneNode ?? false) { return item.name; } return '${item.node} / ${item.name}'; } void _initRefreshTimer() { + _timer?.cancel(); _timer = Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (_) { if (mounted) { - _pve.list(); + _notifier.list(); } }); } void _afterInit() async { - await _pve.connected.future; - if (_pve.release != null && _pve.release!.compareTo('8.0') < 0) { - if (mounted) { - context.showSnackBar(l10n.pveVersionLow); + // Wait for the PVE state to be connected + while (mounted) { + final pveState = ref.read(_provider); + if (pveState.isConnected) { + if (pveState.release != null && pveState.release!.compareTo('8.0') < 0) { + if (mounted) { + context.showSnackBar(l10n.pveVersionLow); + } + } + break; } + if (pveState.error != null) { + break; // Skip if there is an error + } + await Future.delayed(const Duration(milliseconds: 100)); } } } diff --git a/lib/view/page/server/detail/view.dart b/lib/view/page/server/detail/view.dart index 1e9786da..a66a5e3f 100644 --- a/lib/view/page/server/detail/view.dart +++ b/lib/view/page/server/detail/view.dart @@ -3,8 +3,10 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; +import 'package:server_box/core/extension/server.dart'; import 'package:server_box/core/route.dart'; import 'package:server_box/data/model/app/scripts/cmd_types.dart'; import 'package:server_box/data/model/app/server_detail_card.dart'; @@ -16,28 +18,28 @@ import 'package:server_box/data/model/server/disk_smart.dart'; import 'package:server_box/data/model/server/net_speed.dart'; import 'package:server_box/data/model/server/nvdia.dart'; import 'package:server_box/data/model/server/sensors.dart'; -import 'package:server_box/data/model/server/server.dart'; +import 'package:server_box/data/model/server/server.dart' as server_model; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/system.dart'; +import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/res/store.dart'; import 'package:server_box/view/page/pve.dart'; import 'package:server_box/view/page/server/edit.dart'; -import 'package:server_box/view/page/server/logo.dart'; import 'package:server_box/view/widget/server_func_btns.dart'; part 'misc.dart'; -class ServerDetailPage extends StatefulWidget { +class ServerDetailPage extends ConsumerStatefulWidget { final SpiRequiredArgs args; const ServerDetailPage({super.key, required this.args}); @override - State createState() => _ServerDetailPageState(); + ConsumerState createState() => _ServerDetailPageState(); static const route = AppRouteArg(page: ServerDetailPage.new, path: '/servers/detail'); } -class _ServerDetailPageState extends State with SingleTickerProviderStateMixin { +class _ServerDetailPageState extends ConsumerState with SingleTickerProviderStateMixin { late final _cardBuildMap = Map.fromIterables(ServerDetailCards.names, [ _buildAbout, _buildCPUView, @@ -84,17 +86,17 @@ class _ServerDetailPageState extends State with SingleTickerPr @override Widget build(BuildContext context) { - final s = widget.args.spi.server; - if (s == null) { + final serverState = ref.watch(individualServerNotifierProvider(widget.args.spi.id)); + if (serverState.client == null) { return Scaffold( appBar: CustomAppBar(), body: Center(child: Text(libL10n.empty)), ); } - return s.listenVal(_buildMainPage); + return _buildMainPage(serverState); } - Widget _buildMainPage(Server si) { + Widget _buildMainPage(ServerState si) { final buildFuncs = !Stores.setting.moveServerFuncs.fetch(); final logo = _buildLogo(si); final children = [if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)]; @@ -111,7 +113,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - CustomAppBar _buildAppBar(Server si) { + CustomAppBar _buildAppBar(ServerState si) { return CustomAppBar( title: Text( si.spi.name, @@ -132,7 +134,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildLogo(Server si) { + Widget? _buildLogo(ServerState si) { final logoUrl = si.getLogoUrl(context); return Padding( @@ -153,7 +155,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildAbout(Server si) { + Widget? _buildAbout(ServerState si) { final ss = si.status; return ExpandTile( key: ValueKey(ss.more.hashCode), // Use hashCode to avoid perf issue @@ -178,7 +180,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ).cardx; } - Widget? _buildCPUView(Server si) { + Widget? _buildCPUView(ServerState si) { final ss = si.status; final percent = ss.cpu.usedPercent(coreIdx: 0).toInt(); final details = [ @@ -305,7 +307,7 @@ class _ServerDetailPageState extends State with SingleTickerPr return children; } - Widget _buildCPUChart(ServerStatus ss) { + Widget _buildCPUChart(server_model.ServerStatus ss) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 13), child: LayoutBuilder( @@ -335,7 +337,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildMemView(Server si) { + Widget? _buildMemView(ServerState si) { final ss = si.status; final free = ss.mem.free / ss.mem.total * 100; final avail = ss.mem.availPercent * 100; @@ -376,7 +378,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ).cardx; } - Widget? _buildSwapView(Server si) { + Widget? _buildSwapView(ServerState si) { final ss = si.status; if (ss.swap.total == 0) return null; @@ -408,7 +410,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ).cardx; } - Widget? _buildGpuView(Server si) { + Widget? _buildGpuView(ServerState si) { final ss = si.status; final hasNvidia = ss.nvidia != null && ss.nvidia!.isNotEmpty; final hasAmd = ss.amd != null && ss.amd!.isNotEmpty; @@ -532,7 +534,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildDiskView(Server si) { + Widget? _buildDiskView(ServerState si) { final ss = si.status; final children = []; @@ -553,7 +555,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ).cardx; } - Widget _buildDiskItemWithHierarchy(Disk disk, ServerStatus ss, int depth) { + Widget _buildDiskItemWithHierarchy(Disk disk, server_model.ServerStatus ss, int depth) { // Create a list to hold this disk and its children final items = []; @@ -570,7 +572,7 @@ class _ServerDetailPageState extends State with SingleTickerPr return Column(children: items); } - Widget _buildDiskItem(Disk disk, ServerStatus ss, int depth) { + Widget _buildDiskItem(Disk disk, server_model.ServerStatus ss, int depth) { final (read, write) = ss.diskIO.getSpeed(disk.path); final text = () { final use = '${l10n.used} ${disk.used.kb2Str} / ${disk.size.kb2Str}'; @@ -625,7 +627,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildDiskSmart(Server si) { + Widget? _buildDiskSmart(ServerState si) { final smarts = si.status.diskSmart; if (smarts.isEmpty) return null; return CardX( @@ -770,7 +772,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildNetView(Server si) { + Widget? _buildNetView(ServerState si) { final ss = si.status; final ns = ss.netSpeed; final children = []; @@ -847,7 +849,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildTemperature(Server si) { + Widget? _buildTemperature(ServerState si) { final ss = si.status; if (ss.temps.isEmpty) return null; @@ -879,7 +881,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildBatteries(Server si) { + Widget? _buildBatteries(ServerState si) { final ss = si.status; if (ss.batteries.isEmpty) return null; @@ -914,7 +916,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildSensors(Server si) { + Widget? _buildSensors(ServerState si) { final ss = si.status; if (ss.sensors.isEmpty) return UIs.placeholder; return CardX( @@ -967,7 +969,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildPve(Server si) { + Widget? _buildPve(ServerState si) { final addr = si.spi.custom?.pveAddr; if (addr == null || addr.isEmpty) return null; return CardX( @@ -980,7 +982,7 @@ class _ServerDetailPageState extends State with SingleTickerPr ); } - Widget? _buildCustomCmd(Server si) { + Widget? _buildCustomCmd(ServerState si) { final ss = si.status; if (ss.customCmds.isEmpty) return null; return CardX( diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart index 67596a97..a0b626e7 100644 --- a/lib/view/page/server/edit.dart +++ b/lib/view/page/server/edit.dart @@ -3,12 +3,12 @@ import 'dart:convert'; import 'package:choice/choice.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/route.dart'; import 'package:server_box/data/model/app/scripts/cmd_types.dart'; import 'package:server_box/data/model/server/custom.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/system.dart'; import 'package:server_box/data/model/server/wol_cfg.dart'; @@ -17,7 +17,7 @@ import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/store/server.dart'; import 'package:server_box/view/page/private_key/edit.dart'; -class ServerEditPage extends StatefulWidget { +class ServerEditPage extends ConsumerStatefulWidget { final SpiRequiredArgs? args; const ServerEditPage({super.key, this.args}); @@ -25,10 +25,10 @@ class ServerEditPage extends StatefulWidget { static const route = AppRoute(page: ServerEditPage.new, path: '/servers/edit'); @override - State createState() => _ServerEditPageState(); + ConsumerState createState() => _ServerEditPageState(); } -class _ServerEditPageState extends State with AfterLayoutMixin { +class _ServerEditPageState extends ConsumerState with AfterLayoutMixin { late final spi = widget.args?.spi; final _nameController = TextEditingController(); final _ipController = TextEditingController(); @@ -167,7 +167,7 @@ class _ServerEditPageState extends State with AfterLayoutMixin { hint: 'root', suggestion: false, ), - TagTile(tags: _tags, allTags: ServerProvider.tags.value).cardx, + TagTile(tags: _tags, allTags: ref.watch(serverNotifierProvider).tags).cardx, ListTile( title: Text(l10n.autoConnect), trailing: _autoConnect.listenVal( @@ -227,12 +227,14 @@ class _ServerEditPageState extends State with AfterLayoutMixin { } Widget _buildKeyAuth() { - return PrivateKeyProvider.pkis.listenVal((pkis) { - final tiles = List.generate(pkis.length, (index) { - final e = pkis[index]; - return ListTile( - contentPadding: const EdgeInsets.only(left: 10, right: 15), - leading: Radio(value: index), + final privateKeyState = ref.watch(privateKeyNotifierProvider); + final pkis = privateKeyState.keys; + + final tiles = List.generate(pkis.length, (index) { + final e = pkis[index]; + return ListTile( + contentPadding: const EdgeInsets.only(left: 10, right: 15), + leading: Radio(value: index), title: Text(e.id, textAlign: TextAlign.start), subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey), trailing: Btn.icon( @@ -254,7 +256,6 @@ class _ServerEditPageState extends State with AfterLayoutMixin { onChanged: (val) => _keyIdx.value = val, child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx, ); - }); } Widget _buildEnvs() { @@ -485,27 +486,26 @@ class _ServerEditPageState extends State with AfterLayoutMixin { Widget _buildJumpServer() { const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7); - final srvs = ServerProvider.servers.values - .map((e) => e.value) - .where((e) => e.spi.jumpId == null) - .where((e) => e.spi.id != spi?.id) + final srvs = ref.watch(serverNotifierProvider).servers.values + .where((e) => e.jumpId == null) + .where((e) => e.id != spi?.id) .toList(); final choice = _jumpServer.listenVal((val) { final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value); - return Choice( + return Choice( multiple: false, clearable: true, value: srv != null ? [srv] : [], builder: (state, _) => Wrap( children: List.generate(srvs.length, (index) { final item = srvs[index]; - return ChoiceChipX( - label: item.spi.name, + return ChoiceChipX( + label: item.name, state: state, value: item, onSelected: (srv, on) { if (on) { - _jumpServer.value = srv.spi.id; + _jumpServer.value = srv.id; } else { _jumpServer.value = null; } @@ -569,7 +569,7 @@ class _ServerEditPageState extends State with AfterLayoutMixin { actions: Btn.ok( onTap: () async { context.pop(); - ServerProvider.delServer(spi!.id); + ref.read(serverNotifierProvider.notifier).delServer(spi!.id); context.pop(true); }, red: true, @@ -705,7 +705,7 @@ extension on _ServerEditPageState { port: int.parse(_portController.text), user: _usernameController.text, pwd: _passwordController.text.selfNotEmptyOrNull, - keyId: _keyIdx.value != null ? PrivateKeyProvider.pkis.value.elementAt(_keyIdx.value!).id : null, + keyId: _keyIdx.value != null ? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id : null, tags: _tags.value.isEmpty ? null : _tags.value.toList(), alterUrl: _altUrlController.text.selfNotEmptyOrNull, autoConnect: _autoConnect.value, @@ -724,9 +724,9 @@ extension on _ServerEditPageState { context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}'); return; } - ServerProvider.addServer(spi); + ref.read(serverNotifierProvider.notifier).addServer(spi); } else { - ServerProvider.updateServer(this.spi!, spi); + ref.read(serverNotifierProvider.notifier).updateServer(this.spi!, spi); } context.pop(); @@ -740,7 +740,7 @@ extension on _ServerEditPageState { if (spi.keyId == null) { _passwordController.text = spi.pwd ?? ''; } else { - _keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere((e) => e.id == spi.keyId); + _keyIdx.value = ref.read(privateKeyNotifierProvider).keys.indexWhere((e) => e.id == spi.keyId); } /// List in dart is passed by pointer, so you need to copy it here diff --git a/lib/view/page/server/tab/content.dart b/lib/view/page/server/tab/content.dart index 78b610e9..a9651988 100644 --- a/lib/view/page/server/tab/content.dart +++ b/lib/view/page/server/tab/content.dart @@ -1,7 +1,7 @@ part of 'tab.dart'; extension on _ServerPageState { - Widget _buildServerCardTitle(Server s) { + Widget _buildServerCardTitle(ServerState s) { return Padding( padding: const EdgeInsets.only(left: 7, right: 13), child: Row( @@ -17,12 +17,12 @@ extension on _ServerPageState { ); } - Widget _buildTopRightWidget(Server s) { + Widget _buildTopRightWidget(ServerState s) { final (child, onTap) = switch (s.conn) { ServerConn.connecting || ServerConn.loading || ServerConn.connected => ( SizedBox.square( dimension: _ServerPageState._kCardHeightMin, - child: SizedLoading(_ServerPageState._kCardHeightMin, strokeWidth: 3, padding: 3), + child: SizedLoading(_ServerPageState._kCardHeightMin, padding: 3), ), null, ), @@ -30,16 +30,16 @@ extension on _ServerPageState { const Icon(Icons.refresh, size: 21, color: Colors.grey), () { TryLimiter.reset(s.spi.id); - ServerProvider.refresh(spi: s.spi); + ref.read(serverNotifierProvider.notifier).refresh(spi: s.spi); }, ), ServerConn.disconnected => ( const Icon(MingCute.link_3_line, size: 19, color: Colors.grey), - () => ServerProvider.refresh(spi: s.spi), + () => ref.read(serverNotifierProvider.notifier).refresh(spi: s.spi), ), ServerConn.finished => ( const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey), - () => ServerProvider.closeServer(id: s.spi.id), + () => ref.read(serverNotifierProvider.notifier).closeServer(id: s.spi.id), ), }; @@ -51,7 +51,7 @@ extension on _ServerPageState { return InkWell(borderRadius: BorderRadius.circular(7), onTap: onTap, child: wrapped).paddingOnly(left: 5); } - Widget _buildTopRightText(Server s) { + Widget _buildTopRightText(ServerState s) { final hasErr = s.status.err != null; final str = s._getTopRightStr(s.spi); if (str == null) return UIs.placeholder; @@ -106,7 +106,7 @@ ${ss.err?.message ?? 'null'} Widget _buildNet(ServerStatus ss, String id) { final cardNoti = _getCardNoti(id); final type = cardNoti.value.net ?? Stores.setting.netViewType.fetch(); - final device = ServerProvider.pick(id: id)?.value.spi.custom?.netDev; + final device = ref.watch(serverNotifierProvider).servers[id]?.custom?.netDev; final (a, b) = type.build(ss, dev: device); return AnimatedSwitcher( duration: const Duration(milliseconds: 377), diff --git a/lib/view/page/server/tab/landscape.dart b/lib/view/page/server/tab/landscape.dart index 0923029d..7f33bcfc 100644 --- a/lib/view/page/server/tab/landscape.dart +++ b/lib/view/page/server/tab/landscape.dart @@ -26,33 +26,31 @@ extension on _ServerPageState { } Widget _buildLandscapeBody() { - return ServerProvider.serverOrder.listenVal((order) { - if (order.isEmpty) { - return Center(child: Text(libL10n.empty, textAlign: TextAlign.center)); - } + final serverState = ref.watch(serverNotifierProvider); + final order = serverState.serverOrder; + + if (order.isEmpty) { + return Center(child: Text(libL10n.empty, textAlign: TextAlign.center)); + } - return PageView.builder( - itemCount: order.length, - itemBuilder: (_, idx) { - final id = order[idx]; - final srv = ServerProvider.pick(id: id); - if (srv == null) return UIs.placeholder; + return PageView.builder( + itemCount: order.length, + itemBuilder: (_, idx) { + final id = order[idx]; + final srv = ref.watch(individualServerNotifierProvider(id)); - return srv.listenVal((srv) { - final title = _buildServerCardTitle(srv); - final List children = [title, _buildNormalCard(srv.status, srv.spi)]; + final title = _buildServerCardTitle(srv); + final List children = [title, _buildNormalCard(srv.status, srv.spi)]; - return _getCardNoti(id).listenVal((_) { - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: children, - ); - }); - }); - }, - ); - }); + return _getCardNoti(id).listenVal((_) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: children, + ); + }); + }, + ); } } diff --git a/lib/view/page/server/tab/tab.dart b/lib/view/page/server/tab/tab.dart index 53b680c2..ba372111 100644 --- a/lib/view/page/server/tab/tab.dart +++ b/lib/view/page/server/tab/tab.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:server_box/core/extension/context/locale.dart'; @@ -31,11 +32,11 @@ part 'landscape.dart'; part 'top_bar.dart'; part 'utils.dart'; -class ServerPage extends StatefulWidget { +class ServerPage extends ConsumerStatefulWidget { const ServerPage({super.key}); @override - State createState() => _ServerPageState(); + ConsumerState createState() => _ServerPageState(); static const route = AppRouteNoArg(page: ServerPage.new, path: '/servers'); } @@ -43,12 +44,14 @@ class ServerPage extends StatefulWidget { const _cardPad = 74.0; const _cardPadSingle = 13.0; -class _ServerPageState extends State with AutomaticKeepAliveClientMixin, AfterLayoutMixin { +class _ServerPageState extends ConsumerState + with AutomaticKeepAliveClientMixin, AfterLayoutMixin { late double _textFactorDouble; double _offset = 1; late TextScaler _textFactor; final _cardsStatus = {}; + late final ValueNotifier> _tags; Timer? _timer; @@ -64,11 +67,13 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi _scrollController.dispose(); _autoHideCtrl.dispose(); _tag.dispose(); + _tags.dispose(); } @override void initState() { super.initState(); + _tags = ValueNotifier(ref.read(serverNotifierProvider).tags); _startAvoidJitterTimer(); } @@ -78,9 +83,14 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi _updateOffset(); } + @override Widget build(BuildContext context) { super.build(context); + // Listen to provider changes and update the ValueNotifier + ref.listen(serverNotifierProvider, (previous, next) { + _tags.value = next.tags; + }); return OrientationBuilder( builder: (_, orientation) { if (orientation == Orientation.landscape) { @@ -96,7 +106,7 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi Widget _buildScaffold(Widget child) { return Scaffold( - appBar: _TopBar(tags: ServerProvider.tags, onTagChanged: (p0) => _tag.value = p0, initTag: _tag.value), + appBar: _TopBar(tags: _tags, onTagChanged: (p0) => _tag.value = p0, initTag: _tag.value), body: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _autoHideCtrl.show, @@ -122,22 +132,21 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi Widget _buildPortrait() { // final isMobile = ResponsiveBreakpoints.of(context).isMobile; - return ServerProvider.serverOrder.listenVal((order) { - return _tag.listenVal((val) { - final filtered = _filterServers(order); - final child = _buildScaffold(_buildBodySmall(filtered: filtered)); - // if (isMobile) { - return child; - // } + final serverState = ref.watch(serverNotifierProvider); + return _tag.listenVal((val) { + final filtered = _filterServers(serverState.serverOrder); + final child = _buildScaffold(_buildBodySmall(filtered: filtered)); + // if (isMobile) { + return child; + // } - // return SplitView( - // controller: _splitViewCtrl, - // leftWeight: 1, - // rightWeight: 1.3, - // initialRight: Center(child: CircularProgressIndicator()), - // leftBuilder: (_, __) => child, - // ); - }); + // return SplitView( + // controller: _splitViewCtrl, + // leftWeight: 1, + // rightWeight: 1.3, + // initialRight: Center(child: CircularProgressIndicator()), + // leftBuilder: (_, __) => child, + // ); }); } @@ -173,10 +182,9 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi // Last item is just spacing if (index == lens) return SizedBox(height: 77); - final vnode = ServerProvider.pick(id: serversInThisColumn[index]); - if (vnode == null) return UIs.placeholder; + final individualState = ref.watch(individualServerNotifierProvider(serversInThisColumn[index])); - return vnode.listenVal(_buildEachServerCard); + return _buildEachServerCard(individualState); }, ), ); @@ -186,9 +194,7 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi ); } - Widget _buildEachServerCard(Server? srv) { - if (srv == null) return UIs.placeholder; - + Widget _buildEachServerCard(ServerState srv) { return CardX( key: Key(srv.spi.id + _tag.value), child: InkWell( @@ -218,7 +224,7 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi ); } - Widget _buildRealServerCard(Server srv) { + Widget _buildRealServerCard(ServerState srv) { final id = srv.spi.id; final cardStatus = _getCardNoti(id); final title = _buildServerCardTitle(srv); @@ -255,7 +261,7 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi }); } - Widget _buildFlippedCard(Server srv) { + Widget _buildFlippedCard(ServerState srv) { const color = Colors.grey; const textStyle = TextStyle(fontSize: 13, color: color); final children = [ @@ -332,8 +338,8 @@ class _ServerPageState extends State with AutomaticKeepAliveClientMi @override Future afterFirstLayout(BuildContext context) async { - ServerProvider.refresh(); - ServerProvider.startAutoRefresh(); + ref.read(serverNotifierProvider.notifier).refresh(); + ref.read(serverNotifierProvider.notifier).startAutoRefresh(); } static const _kCardHeightMin = 23.0; diff --git a/lib/view/page/server/tab/utils.dart b/lib/view/page/server/tab/utils.dart index a5f0dc6e..a8733420 100644 --- a/lib/view/page/server/tab/utils.dart +++ b/lib/view/page/server/tab/utils.dart @@ -3,7 +3,7 @@ part of 'tab.dart'; extension _Actions on _ServerPageState { - void _onTapCard(Server srv) { + void _onTapCard(ServerState srv) { if (srv.canViewDetails) { // _splitViewCtrl.replace(ServerDetailPage( // key: ValueKey(srv.spi.id), @@ -19,7 +19,7 @@ extension _Actions on _ServerPageState { } } - void _onLongPressCard(Server srv) { + void _onLongPressCard(ServerState srv) { if (srv.conn == ServerConn.finished) { final id = srv.spi.id; final cardStatus = _getCardNoti(id); @@ -42,7 +42,7 @@ extension _Actions on _ServerPageState { } extension _Operation on _ServerPageState { - void _onTapSuspend(Server srv) { + void _onTapSuspend(ServerState srv) { _askFor( func: () async { if (Stores.setting.showSuspendTip.fetch()) { @@ -50,7 +50,7 @@ extension _Operation on _ServerPageState { Stores.setting.showSuspendTip.put(false); } srv.client?.execWithPwd( - ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system), + ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system, customDir: null), context: context, id: srv.id, ); @@ -60,10 +60,10 @@ extension _Operation on _ServerPageState { ); } - void _onTapShutdown(Server srv) { + void _onTapShutdown(ServerState srv) { _askFor( func: () => srv.client?.execWithPwd( - ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system), + ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null), context: context, id: srv.id, ), @@ -72,10 +72,10 @@ extension _Operation on _ServerPageState { ); } - void _onTapReboot(Server srv) { + void _onTapReboot(ServerState srv) { _askFor( func: () => srv.client?.execWithPwd( - ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system), + ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null), context: context, id: srv.id, ), @@ -84,7 +84,7 @@ extension _Operation on _ServerPageState { ); } - void _onTapEdit(Server srv) { + void _onTapEdit(ServerState srv) { if (srv.canViewDetails) { ServerDetailPage.route.go(context, SpiRequiredArgs(srv.spi)); } else { @@ -98,7 +98,7 @@ extension _Utils on _ServerPageState { final tag = _tag.value; if (tag == TagSwitcher.kDefaultTag) return order; return order.where((e) { - final tags = ServerProvider.pick(id: e)?.value.spi.tags; + final tags = ref.read(serverNotifierProvider).servers[e]?.tags; if (tags == null) return false; return tags.contains(tag); }).toList(); @@ -160,7 +160,7 @@ extension _Utils on _ServerPageState { } } -extension _ServerX on Server { +extension _ServerX on ServerState { String? _getTopRightStr(Spi spi) { if (status.err != null) { return l10n.viewErr; diff --git a/lib/view/page/setting/entries/server.dart b/lib/view/page/setting/entries/server.dart index 6369f49b..ca64eed6 100644 --- a/lib/view/page/setting/entries/server.dart +++ b/lib/view/page/setting/entries/server.dart @@ -46,7 +46,7 @@ extension _Server on _AppSettingsPageState { onTap: () async { final keys = Stores.server.keys(); final names = Map.fromEntries( - keys.map((e) => MapEntry(e, ServerProvider.pick(id: e)?.value.spi.name ?? e)), + keys.map((e) => MapEntry(e, ref.read(serverNotifierProvider).servers[e]?.name ?? e)), ); final deleteKeys = await context.showPickDialog( clearable: true, diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index c3359af6..4cdf8067 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_highlight/theme_map.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/net_view.dart'; @@ -35,16 +36,16 @@ part 'entries/ssh.dart'; const _kIconSize = 23.0; -class SettingsPage extends StatefulWidget { +class SettingsPage extends ConsumerStatefulWidget { const SettingsPage({super.key}); static const route = AppRouteNoArg(page: SettingsPage.new, path: '/settings'); @override - State createState() => _SettingsPageState(); + ConsumerState createState() => _SettingsPageState(); } -class _SettingsPageState extends State with SingleTickerProviderStateMixin { +class _SettingsPageState extends ConsumerState with SingleTickerProviderStateMixin { late final _tabCtrl = TabController(length: SettingsTabs.values.length, vsync: this); @override @@ -98,14 +99,14 @@ class _SettingsPageState extends State with SingleTickerProviderSt } } -final class AppSettingsPage extends StatefulWidget { +final class AppSettingsPage extends ConsumerStatefulWidget { const AppSettingsPage({super.key}); @override - State createState() => _AppSettingsPageState(); + ConsumerState createState() => _AppSettingsPageState(); } -final class _AppSettingsPageState extends State { +final class _AppSettingsPageState extends ConsumerState { final _setting = Stores.setting; late final _sshOpacityCtrl = TextEditingController(text: _setting.sshBgOpacity.fetch().toString()); diff --git a/lib/view/page/setting/seq/srv_seq.dart b/lib/view/page/setting/seq/srv_seq.dart index 742d6b04..85fdd4bd 100644 --- a/lib/view/page/setting/seq/srv_seq.dart +++ b/lib/view/page/setting/seq/srv_seq.dart @@ -1,21 +1,22 @@ import 'dart:ui'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/res/store.dart'; -class ServerOrderPage extends StatefulWidget { +class ServerOrderPage extends ConsumerStatefulWidget { const ServerOrderPage({super.key}); @override - State createState() => _ServerOrderPageState(); + ConsumerState createState() => _ServerOrderPageState(); static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server'); } -class _ServerOrderPageState extends State { +class _ServerOrderPageState extends ConsumerState { @override Widget build(BuildContext context) { return Scaffold( @@ -41,25 +42,27 @@ class _ServerOrderPageState extends State { } Widget _buildBody() { - final orders = ServerProvider.serverOrder; - return orders.listenVal((order) { - if (order.isEmpty) { - return Center(child: Text(libL10n.empty)); - } - return ReorderableListView.builder( - footer: const SizedBox(height: 77), - onReorder: (oldIndex, newIndex) { - setState(() { - orders.value.move(oldIndex, newIndex, property: Stores.setting.serverOrder); - }); - }, - padding: const EdgeInsets.all(8), - buildDefaultDragHandles: false, - itemBuilder: (_, idx) => _buildItem(idx, order[idx]), - itemCount: order.length, - proxyDecorator: _proxyDecorator, - ); - }); + final serverState = ref.watch(serverNotifierProvider); + final order = serverState.serverOrder; + + if (order.isEmpty) { + return Center(child: Text(libL10n.empty)); + } + return ReorderableListView.builder( + footer: const SizedBox(height: 77), + onReorder: (oldIndex, newIndex) { + setState(() { + final newOrder = List.from(order); + newOrder.move(oldIndex, newIndex); + Stores.setting.serverOrder.put(newOrder); + }); + }, + padding: const EdgeInsets.all(8), + buildDefaultDragHandles: false, + itemBuilder: (_, idx) => _buildItem(idx, order[idx]), + itemCount: order.length, + proxyDecorator: _proxyDecorator, + ); } Widget _buildItem(int index, String id) { @@ -74,8 +77,10 @@ class _ServerOrderPageState extends State { } Widget _buildCardTile(int index) { - final id = ServerProvider.serverOrder.value[index]; - final spi = ServerProvider.pick(id: id)?.value.spi; + final serverState = ref.watch(serverNotifierProvider); + final order = serverState.serverOrder; + final id = order[index]; + final spi = serverState.servers[id]; if (spi == null) { return const SizedBox(); } diff --git a/lib/view/page/snippet/edit.dart b/lib/view/page/snippet/edit.dart index bf5d2274..43bdf75b 100644 --- a/lib/view/page/snippet/edit.dart +++ b/lib/view/page/snippet/edit.dart @@ -1,6 +1,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/provider/server.dart'; @@ -11,18 +12,18 @@ final class SnippetEditPageArgs { const SnippetEditPageArgs({this.snippet}); } -class SnippetEditPage extends StatefulWidget { +class SnippetEditPage extends ConsumerStatefulWidget { final SnippetEditPageArgs? args; const SnippetEditPage({super.key, this.args}); @override - State createState() => _SnippetEditPageState(); + ConsumerState createState() => _SnippetEditPageState(); static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit'); } -class _SnippetEditPageState extends State with AfterLayoutMixin { +class _SnippetEditPageState extends ConsumerState with AfterLayoutMixin { final _nameController = TextEditingController(); final _scriptController = TextEditingController(); final _noteController = TextEditingController(); @@ -61,7 +62,7 @@ class _SnippetEditPageState extends State with AfterLayoutMixin child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.snippet}(${snippet.name})')), actions: Btn.ok( onTap: () { - SnippetProvider.del(snippet); + ref.read(snippetNotifierProvider.notifier).del(snippet); context.pop(); context.pop(); }, @@ -95,10 +96,11 @@ class _SnippetEditPageState extends State with AfterLayoutMixin autoRunOn: _autoRunOn.value.isEmpty ? null : _autoRunOn.value, ); final oldSnippet = widget.args?.snippet; + final notifier = ref.read(snippetNotifierProvider.notifier); if (oldSnippet != null) { - SnippetProvider.update(oldSnippet, snippet); + notifier.update(oldSnippet, snippet); } else { - SnippetProvider.add(snippet); + notifier.add(snippet); } context.pop(); }, @@ -126,7 +128,12 @@ class _SnippetEditPageState extends State with AfterLayoutMixin icon: Icons.note, suggestion: true, ), - TagTile(tags: _tags, allTags: SnippetProvider.tags.value).cardx, + Consumer( + builder: (_, ref, _) { + final tags = ref.watch(snippetNotifierProvider.select((p) => p.tags)); + return TagTile(tags: _tags, allTags: tags).cardx; + }, + ), Input( controller: _scriptController, node: _scriptNode, @@ -150,7 +157,7 @@ class _SnippetEditPageState extends State with AfterLayoutMixin builder: (vals) { final subtitle = vals.isEmpty ? null - : vals.map((e) => ServerProvider.pick(id: e)?.value.spi.name ?? e).join(', '); + : vals.map((e) => ref.read(serverNotifierProvider).servers[e]?.name ?? e).join(', '); return ListTile( leading: const Padding( padding: EdgeInsets.only(left: 5), @@ -162,11 +169,11 @@ class _SnippetEditPageState extends State with AfterLayoutMixin ? null : Text(subtitle, maxLines: 1, style: UIs.textGrey, overflow: TextOverflow.ellipsis), onTap: () async { - vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e)); + vals.removeWhere((e) => !ref.read(serverNotifierProvider).serverOrder.contains(e)); final serverIds = await context.showPickDialog( title: l10n.autoRun, - items: ServerProvider.serverOrder.value, - display: (e) => ServerProvider.pick(id: e)?.value.spi.name ?? e, + items: ref.read(serverNotifierProvider).serverOrder, + display: (e) => ref.read(serverNotifierProvider).servers[e]?.name ?? e, initial: vals, clearable: true, ); diff --git a/lib/view/page/snippet/list.dart b/lib/view/page/snippet/list.dart index e9b8fbd1..79c52565 100644 --- a/lib/view/page/snippet/list.dart +++ b/lib/view/page/snippet/list.dart @@ -1,20 +1,21 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/provider/snippet.dart'; import 'package:server_box/view/page/snippet/edit.dart'; -class SnippetListPage extends StatefulWidget { +class SnippetListPage extends ConsumerStatefulWidget { const SnippetListPage({super.key}); @override - State createState() => _SnippetListPageState(); + ConsumerState createState() => _SnippetListPageState(); static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets'); } -class _SnippetListPageState extends State with AutomaticKeepAliveClientMixin { +class _SnippetListPageState extends ConsumerState with AutomaticKeepAliveClientMixin { final _tag = ''.vn; final _splitViewCtrl = SplitViewController(); @@ -35,12 +36,14 @@ class _SnippetListPageState extends State with AutomaticKeepAli Widget _buildBody() { // final isMobile = ResponsiveBreakpoints.of(context).isMobile; - return SnippetProvider.snippets.listenVal((snippets) { - return _tag.listenVal((tag) { - final child = _buildScaffold(snippets, tag); - // if (isMobile) { - return child; - // } + final snippetState = ref.watch(snippetNotifierProvider); + final snippets = snippetState.snippets; + + return _tag.listenVal((tag) { + final child = _buildScaffold(snippets, tag); + // if (isMobile) { + return child; + // } // return SplitView( // controller: _splitViewCtrl, @@ -49,14 +52,14 @@ class _SnippetListPageState extends State with AutomaticKeepAli // initialRight: Center(child: Text(libL10n.empty)), // leftBuilder: (_, __) => child, // ); - }); }); } Widget _buildScaffold(List snippets, String tag) { + final snippetState = ref.watch(snippetNotifierProvider); return Scaffold( appBar: TagSwitcher( - tags: SnippetProvider.tags, + tags: snippetState.tags.vn, onTagChanged: (tag) => _tag.value = tag, initTag: _tag.value, ), diff --git a/lib/view/page/ssh/page/init.dart b/lib/view/page/ssh/page/init.dart index b9929441..078e8d8e 100644 --- a/lib/view/page/ssh/page/init.dart +++ b/lib/view/page/ssh/page/init.dart @@ -66,7 +66,8 @@ extension _Init on SSHPageState { // Mark status connected for notifications / live activities TermSessionManager.updateStatus(_sessionId, TermSessionStatus.connected); - for (final snippet in SnippetProvider.snippets.value) { + final snippets = ref.read(snippetNotifierProvider.select((p) => p.snippets)); + for (final snippet in snippets) { if (snippet.autoRunOn?.contains(widget.args.spi.id) == true) { snippet.runInTerm(_terminal, widget.args.spi); } diff --git a/lib/view/page/ssh/page/page.dart b/lib/view/page/ssh/page/page.dart index ed1d136b..871b18b7 100644 --- a/lib/view/page/ssh/page/page.dart +++ b/lib/view/page/ssh/page/page.dart @@ -7,9 +7,8 @@ import 'package:dartssh2/dartssh2.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; -import 'package:provider/provider.dart'; - import 'package:server_box/core/chan.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/utils/server.dart'; @@ -17,6 +16,7 @@ import 'package:server_box/core/utils/ssh_auth.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/snippet.dart'; import 'package:server_box/data/model/ssh/virtual_key.dart'; +import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/provider/snippet.dart'; import 'package:server_box/data/provider/virtual_keyboard.dart'; import 'package:server_box/data/res/store.dart'; @@ -52,23 +52,22 @@ final class SshPageArgs { }); } -class SSHPage extends StatefulWidget { +class SSHPage extends ConsumerStatefulWidget { final SshPageArgs args; const SSHPage({super.key, required this.args}); @override - State createState() => SSHPageState(); + ConsumerState createState() => SSHPageState(); static const route = AppRouteArg(page: SSHPage.new, path: '/ssh/page'); } const _horizonPadding = 7.0; -class SSHPageState extends State +class SSHPageState extends ConsumerState with AutomaticKeepAliveClientMixin, AfterLayoutMixin, TickerProviderStateMixin { - final _keyboard = VirtKeyProvider(); - late final _terminal = Terminal(inputHandler: _keyboard); + late final _terminal = Terminal(); late final TerminalController _terminalController = TerminalController(vsync: this); final List> _virtKeysList = []; late final _termKey = widget.args.terminalKey ?? GlobalKey(); @@ -81,7 +80,7 @@ class SSHPageState extends State bool _isDark = false; Timer? _virtKeyLongPressTimer; - late SSHClient? _client = widget.args.spi.server?.value.client; + SSHClient? _client; SSHSession? _session; Timer? _discontinuityTimer; @@ -117,6 +116,10 @@ class SSHPageState extends State _initStoredCfg(); _initVirtKeys(); _setupDiscontinuityTimer(); + + // Initialize client from provider + final serverState = ref.read(individualServerNotifierProvider(widget.args.spi.id)); + _client = serverState.client; if (++_sshConnCount == 1) { WakelockPlus.enable(); @@ -262,19 +265,22 @@ class SSHPageState extends State child: Container( color: _terminalTheme.background, height: _virtKeysHeight + _media.padding.bottom, - child: ChangeNotifierProvider( - create: (_) => _keyboard, - builder: (_, _) => Consumer( - builder: (_, _, _) { - return _buildVirtualKey(); - }, - ), + child: Consumer( + builder: (context, ref, child) { + final virtKeyState = ref.watch(virtKeyboardProvider); + final virtKeyNotifier = ref.read(virtKeyboardProvider.notifier); + + // Set the terminal input handler + _terminal.inputHandler = virtKeyNotifier; + + return _buildVirtualKey(virtKeyState, virtKeyNotifier); + }, ), ), ); } - Widget _buildVirtualKey() { + Widget _buildVirtualKey(VirtKeyState virtKeyState, VirtKeyboard virtKeyNotifier) { final count = _horizonVirtKeys ? _virtKeysList.length : _virtKeysList.firstOrNull?.length ?? 0; if (count == 0) return UIs.placeholder; return LayoutBuilder( @@ -286,30 +292,30 @@ class SSHPageState extends State child: Row( children: _virtKeysList .expand((e) => e) - .map((e) => _buildVirtKeyItem(e, virtKeyWidth)) + .map((e) => _buildVirtKeyItem(e, virtKeyWidth, virtKeyState, virtKeyNotifier)) .toList(), ), ); } final rows = _virtKeysList - .map((e) => Row(children: e.map((e) => _buildVirtKeyItem(e, virtKeyWidth)).toList())) + .map((e) => Row(children: e.map((e) => _buildVirtKeyItem(e, virtKeyWidth, virtKeyState, virtKeyNotifier)).toList())) .toList(); return Column(mainAxisSize: MainAxisSize.min, children: rows); }, ); } - Widget _buildVirtKeyItem(VirtKey item, double virtKeyWidth) { + Widget _buildVirtKeyItem(VirtKey item, double virtKeyWidth, VirtKeyState virtKeyState, VirtKeyboard virtKeyNotifier) { var selected = false; switch (item.key) { case TerminalKey.control: - selected = _keyboard.ctrl; + selected = virtKeyState.ctrl; break; case TerminalKey.alt: - selected = _keyboard.alt; + selected = virtKeyState.alt; break; case TerminalKey.shift: - selected = _keyboard.shift; + selected = virtKeyState.shift; break; default: break; @@ -326,12 +332,12 @@ class SSHPageState extends State ); return InkWell( - onTap: () => _doVirtualKey(item), + onTap: () => _doVirtualKey(item, virtKeyNotifier), onTapDown: (details) { if (item.canLongPress) { _virtKeyLongPressTimer = Timer.periodic( const Duration(milliseconds: 137), - (_) => _doVirtualKey(item), + (_) => _doVirtualKey(item, virtKeyNotifier), ); } }, diff --git a/lib/view/page/ssh/page/virt_key.dart b/lib/view/page/ssh/page/virt_key.dart index 759c93c7..7fb3bd00 100644 --- a/lib/view/page/ssh/page/virt_key.dart +++ b/lib/view/page/ssh/page/virt_key.dart @@ -1,7 +1,7 @@ part of 'page.dart'; extension _VirtKey on SSHPageState { - void _doVirtualKey(VirtKey item) { + void _doVirtualKey(VirtKey item, VirtKeyboard virtKeyNotifier) { if (item.func != null) { HapticFeedback.mediumImpact(); _doVirtualKeyFunc(item.func!); @@ -9,7 +9,7 @@ extension _VirtKey on SSHPageState { } if (item.key != null) { HapticFeedback.mediumImpact(); - _doVirtualKeyInput(item.key!); + _doVirtualKeyInput(item.key!, virtKeyNotifier); } final inputRaw = item.inputRaw; if (inputRaw != null) { @@ -18,16 +18,16 @@ extension _VirtKey on SSHPageState { } } - void _doVirtualKeyInput(TerminalKey key) { + void _doVirtualKeyInput(TerminalKey key, VirtKeyboard virtKeyNotifier) { switch (key) { case TerminalKey.control: - _keyboard.ctrl = !_keyboard.ctrl; + virtKeyNotifier.setCtrl(!virtKeyNotifier.ctrl); break; case TerminalKey.alt: - _keyboard.alt = !_keyboard.alt; + virtKeyNotifier.setAlt(!virtKeyNotifier.alt); break; case TerminalKey.shift: - _keyboard.shift = !_keyboard.shift; + virtKeyNotifier.setShift(!virtKeyNotifier.shift); break; default: _terminal.keyInput(key); @@ -52,14 +52,15 @@ extension _VirtKey on SSHPageState { } break; case VirtualKeyFunc.snippet: + final snippetState = ref.read(snippetNotifierProvider); final snippets = await context.showPickWithTagDialog( title: l10n.snippet, - tags: SnippetProvider.tags, + tags: snippetState.tags.vn, itemsBuilder: (e) { if (e == TagSwitcher.kDefaultTag) { - return SnippetProvider.snippets.value; + return snippetState.snippets; } - return SnippetProvider.snippets.value + return snippetState.snippets .where((element) => element.tags?.contains(e) ?? false) .toList(); }, diff --git a/lib/view/page/ssh/tab.dart b/lib/view/page/ssh/tab.dart index 72dc32cb..6a940ec3 100644 --- a/lib/view/page/ssh/tab.dart +++ b/lib/view/page/ssh/tab.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; @@ -10,18 +11,18 @@ import 'package:server_box/data/provider/server.dart'; import 'package:server_box/view/page/server/edit.dart'; import 'package:server_box/view/page/ssh/page/page.dart'; -class SSHTabPage extends StatefulWidget { +class SSHTabPage extends ConsumerStatefulWidget { const SSHTabPage({super.key}); @override - State createState() => _SSHTabPageState(); + ConsumerState createState() => _SSHTabPageState(); static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh'); } typedef _TabMap = Map; -class _SSHTabPageState extends State +class _SSHTabPageState extends ConsumerState with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)}; final _pageCtrl = PageController(); @@ -236,7 +237,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget { } } -class _AddPage extends StatelessWidget { +class _AddPage extends ConsumerWidget { const _AddPage({required this.onTapInitCard}); final void Function(Spi spi) onTapInitCard; @@ -244,11 +245,12 @@ class _AddPage extends StatelessWidget { Widget get _placeholder => const Expanded(child: UIs.placeholder); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { const viewPadding = 7.0; - final viewWidth = context.mediaQuery.size.width - 2 * viewPadding; + final viewWidth = context.windowSize.width - 2 * viewPadding; - final itemCount = ServerProvider.servers.length; + final serverState = ref.watch(serverNotifierProvider); + final itemCount = serverState.servers.length; const itemPadding = 1.0; const itemWidth = 150.0; const itemHeight = 50.0; @@ -257,53 +259,53 @@ class _AddPage extends StatelessWidget { final crossCount = max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1); final mainCount = itemCount ~/ crossCount + 1; - return ServerProvider.serverOrder.listenVal((order) { - if (order.isEmpty) { - return Center(child: Text(libL10n.empty, textAlign: TextAlign.center)); - } + final order = serverState.serverOrder; + + if (order.isEmpty) { + return Center(child: Text(libL10n.empty, textAlign: TextAlign.center)); + } - // Custom grid - return ListView( - padding: const EdgeInsets.all(viewPadding), - children: List.generate( - mainCount, - (rowIndex) => Row( - children: List.generate(crossCount, (columnIndex) { - final idx = rowIndex * crossCount + columnIndex; - final id = order.elementAtOrNull(idx); - final spi = ServerProvider.pick(id: id)?.value.spi; - if (spi == null) return _placeholder; + // Custom grid + return ListView( + padding: const EdgeInsets.all(viewPadding), + children: List.generate( + mainCount, + (rowIndex) => Row( + children: List.generate(crossCount, (columnIndex) { + final idx = rowIndex * crossCount + columnIndex; + final id = order.elementAtOrNull(idx); + final spi = serverState.servers[id]; + if (spi == null) return _placeholder; - return Expanded( - child: Padding( - padding: const EdgeInsets.all(itemPadding), - child: InkWell( - onTap: () => onTapInitCard(spi), - child: Container( - height: itemHeight, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 17, right: 7), - child: Row( - children: [ - Expanded( - child: Text( - spi.name, - style: UIs.text18, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + return Expanded( + child: Padding( + padding: const EdgeInsets.all(itemPadding), + child: InkWell( + onTap: () => onTapInitCard(spi), + child: Container( + height: itemHeight, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 17, right: 7), + child: Row( + children: [ + Expanded( + child: Text( + spi.name, + style: UIs.text18, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - const Icon(Icons.chevron_right), - ], - ), + ), + const Icon(Icons.chevron_right), + ], ), - ).cardx, - ), - ); - }), - ), + ), + ).cardx, + ), + ); + }), ), - ); - }); + ), + ); } } diff --git a/lib/view/page/storage/local.dart b/lib/view/page/storage/local.dart index b9d2a6fd..015bf544 100644 --- a/lib/view/page/storage/local.dart +++ b/lib/view/page/storage/local.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/path_with_prefix.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; @@ -19,7 +20,7 @@ final class LocalFilePageArgs { const LocalFilePageArgs({this.isPickFile, this.initDir}); } -class LocalFilePage extends StatefulWidget { +class LocalFilePage extends ConsumerStatefulWidget { final LocalFilePageArgs? args; const LocalFilePage({super.key, this.args}); @@ -27,10 +28,10 @@ class LocalFilePage extends StatefulWidget { static const route = AppRoute(page: LocalFilePage.new, path: '/files/local'); @override - State createState() => _LocalFilePageState(); + ConsumerState createState() => _LocalFilePageState(); } -class _LocalFilePageState extends State with AutomaticKeepAliveClientMixin { +class _LocalFilePageState extends ConsumerState with AutomaticKeepAliveClientMixin { late final _path = LocalPath(widget.args?.initDir ?? Paths.file); final _sortType = _SortType.name.vn; bool get isPickFile => widget.args?.isPickFile ?? false; @@ -358,10 +359,7 @@ extension _OnTapFile on _LocalFilePageState { final spi = await context.showPickSingleDialog( title: libL10n.select, - items: ServerProvider.serverOrder.value - .map((e) => ServerProvider.pick(id: e)?.value.spi) - .whereType() - .toList(), + items: ref.read(serverNotifierProvider).servers.values.toList(), display: (e) => e.name, ); if (spi == null) return; @@ -372,7 +370,7 @@ extension _OnTapFile on _LocalFilePageState { return; } - SftpProvider.add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload)); + ref.read(sftpNotifierProvider.notifier).add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload)); context.showSnackBar(l10n.added2List); } } diff --git a/lib/view/page/storage/sftp.dart b/lib/view/page/storage/sftp.dart index 2c394f0e..c4ed448b 100644 --- a/lib/view/page/storage/sftp.dart +++ b/lib/view/page/storage/sftp.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:dartssh2/dartssh2.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/sftpfile.dart'; @@ -11,6 +12,7 @@ import 'package:server_box/core/utils/comparator.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/sftp/browser_status.dart'; import 'package:server_box/data/model/sftp/worker.dart'; +import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/provider/sftp.dart'; import 'package:server_box/data/res/misc.dart'; import 'package:server_box/data/res/store.dart'; @@ -29,21 +31,29 @@ final class SftpPageArgs { const SftpPageArgs({required this.spi, this.isSelect = false, this.initPath}); } -class SftpPage extends StatefulWidget { +class SftpPage extends ConsumerStatefulWidget { final SftpPageArgs args; const SftpPage({super.key, required this.args}); @override - State createState() => _SftpPageState(); + ConsumerState createState() => _SftpPageState(); static const route = AppRouteArg(page: SftpPage.new, path: '/sftp'); } -class _SftpPageState extends State with AfterLayoutMixin { - late final _status = SftpBrowserStatus(_client); - late final _client = widget.args.spi.server!.value.client!; +class _SftpPageState extends ConsumerState with AfterLayoutMixin { + late final SftpBrowserStatus _status; + late final SSHClient _client; final _sortOption = _SortOption().vn; + + @override + void initState() { + super.initState(); + final serverState = ref.read(individualServerNotifierProvider(widget.args.spi.id)); + _client = serverState.client!; + _status = SftpBrowserStatus(_client); + } @override void dispose() { @@ -280,7 +290,7 @@ extension _Actions on _SftpPageState { final localPath = _getLocalPath(remotePath); final completer = Completer(); final req = SftpReq(widget.args.spi, remotePath, localPath, SftpReqType.download); - SftpProvider.add(req, completer: completer); + ref.read(sftpNotifierProvider.notifier).add(req, completer: completer); final (suc, err) = await context.showLoadingDialog(fn: () => completer.future); if (suc == null || err != null) return; @@ -289,7 +299,9 @@ extension _Actions on _SftpPageState { args: EditorPageArgs( path: localPath, onSave: (_) { - SftpProvider.add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload)); + ref + .read(sftpNotifierProvider.notifier) + .add(SftpReq(req.spi, remotePath, localPath, SftpReqType.upload)); context.showSnackBar(l10n.added2List); }, closeAfterSave: SettingStore.instance.closeAfterSave.fetch(), @@ -310,9 +322,9 @@ extension _Actions on _SftpPageState { context.pop(); final remotePath = _getRemotePath(name); - SftpProvider.add( - SftpReq(widget.args.spi, remotePath, _getLocalPath(remotePath), SftpReqType.download), - ); + ref + .read(sftpNotifierProvider.notifier) + .add(SftpReq(widget.args.spi, remotePath, _getLocalPath(remotePath), SftpReqType.download)); context.pop(); }, @@ -640,7 +652,9 @@ extension _Actions on _SftpPageState { final fileName = path.split(Platform.pathSeparator).lastOrNull; final remotePath = '$remoteDir/$fileName'; Loggers.app.info('SFTP upload local: $path, remote: $remotePath'); - SftpProvider.add(SftpReq(widget.args.spi, remotePath, path, SftpReqType.upload)); + ref + .read(sftpNotifierProvider.notifier) + .add(SftpReq(widget.args.spi, remotePath, path, SftpReqType.upload)); }, icon: const Icon(Icons.upload_file), ); diff --git a/lib/view/page/storage/sftp_mission.dart b/lib/view/page/storage/sftp_mission.dart index 6e9fb10a..9928c21e 100644 --- a/lib/view/page/storage/sftp_mission.dart +++ b/lib/view/page/storage/sftp_mission.dart @@ -1,20 +1,21 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/sftp/worker.dart'; import 'package:server_box/data/provider/sftp.dart'; import 'package:server_box/view/page/storage/local.dart'; -class SftpMissionPage extends StatefulWidget { +class SftpMissionPage extends ConsumerStatefulWidget { const SftpMissionPage({super.key}); @override - State createState() => _SftpMissionPageState(); + ConsumerState createState() => _SftpMissionPageState(); static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission'); } -class _SftpMissionPageState extends State { +class _SftpMissionPageState extends ConsumerState { @override Widget build(BuildContext context) { return Scaffold( @@ -24,18 +25,17 @@ class _SftpMissionPageState extends State { } Widget _buildBody() { - return SftpProvider.status.listenVal((status) { - if (status.isEmpty) { - return Center(child: Text(libL10n.empty)); - } - return ListView.builder( - padding: const EdgeInsets.all(11), - itemCount: status.length, - itemBuilder: (context, index) { - return _buildItem(status[index]); - }, - ); - }); + final status = ref.watch(sftpNotifierProvider.select((pro) => pro.requests)); + if (status.isEmpty) { + return Center(child: Text(libL10n.empty)); + } + return ListView.builder( + padding: const EdgeInsets.all(11), + itemCount: status.length, + itemBuilder: (context, index) { + return _buildItem(status[index]); + }, + ); } Widget _buildItem(SftpReqStatus status) { @@ -143,7 +143,7 @@ class _SftpMissionPageState extends State { child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.mission}($name)')), actions: Btn.ok( onTap: () { - SftpProvider.cancel(id); + ref.read(sftpNotifierProvider.notifier).cancel(id); context.pop(); }, ).toList, diff --git a/lib/view/page/systemd.dart b/lib/view/page/systemd.dart index 580c9fa9..84868c59 100644 --- a/lib/view/page/systemd.dart +++ b/lib/view/page/systemd.dart @@ -1,12 +1,13 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/route.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/systemd.dart'; import 'package:server_box/data/provider/systemd.dart'; import 'package:server_box/view/page/ssh/page/page.dart'; -final class SystemdPage extends StatefulWidget { +final class SystemdPage extends ConsumerStatefulWidget { final SpiRequiredArgs args; const SystemdPage({super.key, required this.args}); @@ -14,44 +15,39 @@ final class SystemdPage extends StatefulWidget { static const route = AppRouteArg(page: SystemdPage.new, path: '/systemd'); @override - State createState() => _SystemdPageState(); + ConsumerState createState() => _SystemdPageState(); } -final class _SystemdPageState extends State { - late final _pro = SystemdProvider.init(widget.args.spi); +final class _SystemdPageState extends ConsumerState { + late final _pro = systemdNotifierProvider(widget.args.spi); - @override - void dispose() { - super.dispose(); - _pro.dispose(); - } + late final _notifier = ref.read(_pro.notifier); @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( title: const Text('Systemd'), - actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)] : null, + actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _notifier.getUnits)] : null, ), - body: RefreshIndicator(onRefresh: _pro.getUnits, child: _buildBody()), + body: RefreshIndicator(onRefresh: _notifier.getUnits, child: _buildBody()), ); } Widget _buildBody() { + final isBusy = ref.watch(_pro.select((pro) => pro.isBusy)); return CustomScrollView( slivers: [ SliverToBoxAdapter( child: Column( children: [ _buildScopeFilterChips(), - _pro.isBusy.listenVal( - (isBusy) => AnimatedContainer( - duration: Durations.medium1, - curve: Curves.fastEaseInToSlowEaseOut, - height: isBusy ? SizedLoading.medium.size : 0, - width: isBusy ? SizedLoading.medium.size : 0, - child: isBusy ? SizedLoading.medium : const SizedBox.shrink(), - ), + AnimatedContainer( + duration: Durations.medium1, + curve: Curves.fastEaseInToSlowEaseOut, + height: isBusy ? SizedLoading.medium.size : 0, + width: isBusy ? SizedLoading.medium.size : 0, + child: isBusy ? SizedLoading.medium : const SizedBox.shrink(), ), ], ), @@ -62,43 +58,40 @@ final class _SystemdPageState extends State { } Widget _buildScopeFilterChips() { - return _pro.scopeFilter.listenVal((currentFilter) { - return Wrap( - spacing: 8, - children: SystemdScopeFilter.values.map((filter) { - final isSelected = filter == currentFilter; - return FilterChip( - selected: isSelected, - label: Text(filter.displayName), - onSelected: (_) => _pro.scopeFilter.value = filter, - ); - }).toList(), - ).paddingSymmetric(horizontal: 13, vertical: 8); - }); + final currentFilter = ref.watch(_pro.select((p) => p.scopeFilter)); + return Wrap( + spacing: 8, + children: SystemdScopeFilter.values.map((filter) { + final isSelected = filter == currentFilter; + return FilterChip( + selected: isSelected, + label: Text(filter.displayName), + onSelected: (_) => _notifier.setScopeFilter(filter), + ); + }).toList(), + ).paddingSymmetric(horizontal: 13, vertical: 8); } Widget _buildUnitList() { - return _pro.units.listenVal((allUnits) { - return _pro.scopeFilter.listenVal((filter) { - final filteredUnits = _pro.filteredUnits; - if (filteredUnits.isEmpty) { - return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13)); - } - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - final unit = filteredUnits[index]; - return ListTile( - leading: _buildScopeTag(unit.scope), - title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name), - subtitle: Wrap( - children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)], - ).paddingOnly(top: 7), - trailing: _buildUnitFuncs(unit), - ).cardx.paddingSymmetric(horizontal: 13); - }, childCount: filteredUnits.length), - ); - }); - }); + ref.watch(_pro.select((p) => p.units)); + ref.watch(_pro.select((p) => p.scopeFilter)); + final filteredUnits = _notifier.filteredUnits; + if (filteredUnits.isEmpty) { + return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13)); + } + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final unit = filteredUnits[index]; + return ListTile( + leading: _buildScopeTag(unit.scope), + title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name), + subtitle: Wrap( + children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)], + ).paddingOnly(top: 7), + trailing: _buildUnitFuncs(unit), + ).cardx.paddingSymmetric(horizontal: 13); + }, childCount: filteredUnits.length), + ); } Widget _buildUnitFuncs(SystemdUnit unit) { diff --git a/lib/view/widget/server_func_btns.dart b/lib/view/widget/server_func_btns.dart index 2f861dab..b7a7529a 100644 --- a/lib/view/widget/server_func_btns.dart +++ b/lib/view/widget/server_func_btns.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/route.dart'; import 'package:server_box/core/utils/server.dart'; @@ -19,17 +20,17 @@ import 'package:server_box/view/page/ssh/page/page.dart'; import 'package:server_box/view/page/storage/sftp.dart'; import 'package:server_box/view/page/systemd.dart'; -class ServerFuncBtnsTopRight extends StatelessWidget { +class ServerFuncBtnsTopRight extends ConsumerWidget { final Spi spi; const ServerFuncBtnsTopRight({super.key, required this.spi}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return PopupMenu( items: ServerFuncBtn.values.map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(), padding: const EdgeInsets.symmetric(horizontal: 10), - onSelected: (val) => _onTapMoreBtns(val, spi, context), + onSelected: (val) => _onTapMoreBtns(val, spi, context, ref), ); } } @@ -52,18 +53,18 @@ class ServerFuncBtns extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 13), itemBuilder: (context, index) { final value = btns[index]; - final item = _buildItem(context, value); + final item = Consumer(builder: (_, ref, _) => _buildItem(context, value, ref)); return item.paddingSymmetric(horizontal: 7); }, ), ); } - Widget _buildItem(BuildContext context, ServerFuncBtn e) { + Widget _buildItem(BuildContext context, ServerFuncBtn e, WidgetRef ref) { final move = Stores.setting.moveServerFuncs.fetch(); if (move) { return IconButton( - onPressed: () => _onTapMoreBtns(e, spi, context), + onPressed: () => _onTapMoreBtns(e, spi, context, ref), padding: EdgeInsets.zero, tooltip: e.toStr, icon: Icon(e.icon, size: 15), @@ -76,7 +77,7 @@ class ServerFuncBtns extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ IconButton( - onPressed: () => _onTapMoreBtns(e, spi, context), + onPressed: () => _onTapMoreBtns(e, spi, context, ref), padding: EdgeInsets.zero, icon: Icon(e.icon, size: 17), ), @@ -101,14 +102,14 @@ class ServerFuncBtns extends StatelessWidget { } } -void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async { +void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context, WidgetRef ref) async { // final isMobile = ResponsiveBreakpoints.of(context).isMobile; switch (value) { // case ServerFuncBtn.pkg: // _onPkg(context, spi); // break; case ServerFuncBtn.sftp: - if (!_checkClient(context, spi.id)) return; + if (!_checkClient(context, spi.id, ref)) return; final args = SftpPageArgs(spi: spi); // if (isMobile) { SftpPage.route.go(context, args); @@ -120,18 +121,19 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async { break; case ServerFuncBtn.snippet: - if (SnippetProvider.snippets.value.isEmpty) { + final snippetState = ref.read(snippetNotifierProvider); + if (snippetState.snippets.isEmpty) { context.showSnackBar(libL10n.empty); return; } final snippets = await context.showPickWithTagDialog( title: l10n.snippet, - tags: SnippetProvider.tags, + tags: snippetState.tags.vn, itemsBuilder: (e) { if (e == TagSwitcher.kDefaultTag) { - return SnippetProvider.snippets.value; + return snippetState.snippets; } - return SnippetProvider.snippets.value + return snippetState.snippets .where((element) => element.tags?.contains(e) ?? false) .toList(); }, @@ -147,7 +149,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async { actions: [CountDownBtn(onTap: () => context.pop(true), text: l10n.run, afterColor: Colors.red)], ); if (sure != true) return; - if (!_checkClient(context, spi.id)) return; + if (!_checkClient(context, spi.id, ref)) return; final args = SshPageArgs(spi: spi, initSnippet: snippet); // if (isMobile) { SSHPage.route.go(context, args); @@ -158,7 +160,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async { // } break; case ServerFuncBtn.container: - if (!_checkClient(context, spi.id)) return; + if (!_checkClient(context, spi.id, ref)) return; final args = SpiRequiredArgs(spi); // if (isMobile) { ContainerPage.route.go(context, args); @@ -169,7 +171,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async { // } break; case ServerFuncBtn.process: - if (!_checkClient(context, spi.id)) return; + if (!_checkClient(context, spi.id, ref)) return; final args = SpiRequiredArgs(spi); // if (isMobile) { ProcessPage.route.go(context, args); @@ -183,7 +185,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async { _gotoSSH(spi, context); break; case ServerFuncBtn.iperf: - if (!_checkClient(context, spi.id)) return; + if (!_checkClient(context, spi.id, ref)) return; final args = SpiRequiredArgs(spi); // if (isMobile) { IPerfPage.route.go(context, args); @@ -194,7 +196,7 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context) async { // } break; case ServerFuncBtn.systemd: - if (!_checkClient(context, spi.id)) return; + if (!_checkClient(context, spi.id, ref)) return; final args = SpiRequiredArgs(spi); // if (isMobile) { SystemdPage.route.go(context, args); @@ -270,9 +272,9 @@ void _gotoSSH(Spi spi, BuildContext context) async { } } -bool _checkClient(BuildContext context, String id) { - final server = ServerProvider.pick(id: id)?.value; - if (server == null || server.client == null) { +bool _checkClient(BuildContext context, String id, WidgetRef ref) { + final serverState = ref.read(individualServerNotifierProvider(id)); + if (serverState.client == null) { context.showSnackBar(l10n.waitConnection); return false; } diff --git a/pubspec.lock b/pubspec.lock index 9a0afe09..9a16fcf1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,18 +117,18 @@ packages: dependency: transitive description: name: build - sha256: "6439a9c71a4e6eca8d9490c1b380a25b02675aa688137dfbe66d2062884a23ac" + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "2.5.4" build_config: dependency: transitive description: name: build_config - sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.1.2" build_daemon: dependency: transitive description: @@ -141,26 +141,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "2b21a125d66a86b9511cc3fb6c668c42e9a1185083922bf60e46d483a81a9712" + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: fd3c09f4bbff7fa6e8d8ef688a0b2e8a6384e6483a25af0dac75fef362bcfe6f + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: ab27e46c8aa233e610cf6084ee6d8a22c6f873a0a9929241d8855b7a72978ae7 + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.1.2" built_collection: dependency: transitive description: @@ -343,10 +343,10 @@ packages: dependency: transitive description: name: custom_lint_core - sha256: cc4684d22ca05bf0a4a51127e19a8aea576b42079ed2bc9e956f11aaebe35dd1 + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.7.5" custom_lint_visitor: dependency: transitive description: @@ -497,8 +497,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.0.338" - resolved-ref: d681131c436fa346bfb4675bca5c37cd38b1fdc2 + ref: "v1.0.343" + resolved-ref: a65b7447ac2cc5c25e5a96dc559b1b67a40b2c82 url: "https://github.com/lppcg/fl_lib" source: git version: "0.0.1" @@ -588,10 +588,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "56c3cc75d04c34fc824ce1d52ec9076c431e3c47ed55fd8cbf9756ca6d50479e" + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" url: "https://pub.dev" source: hosted - version: "3.0.0-dev.17" + version: "2.6.1" flutter_secure_storage: dependency: transitive description: @@ -662,10 +662,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: da32f8ba8cfcd4ec71d9decc8cbf28bd2c31b5283d9887eb51eb4a0659d8110c + sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.1.0" freezed_annotation: dependency: "direct main" description: @@ -735,10 +735,10 @@ packages: dependency: "direct dev" description: name: hive_ce_generator - sha256: a169feeff2da9cc2c417ce5ae9bcebf7c8a95d7a700492b276909016ad70a786 + sha256: "609678c10ebee7503505a0007050af40a0a4f498b1fb7def3220df341e573a89" url: "https://pub.dev" source: hosted - version: "1.9.3" + version: "1.9.2" html: dependency: transitive description: @@ -863,10 +863,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ce2cf974ccdee13be2a510832d7fba0b94b364e0b0395dee42abaa51b855be27 + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.10.0" + version: "6.9.5" leak_tracker: dependency: transitive description: @@ -995,14 +995,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - mockito: - dependency: transitive - description: - name: mockito - sha256: "2314cbe9165bcd16106513df9cf3c3224713087f09723b128928dc11a4379f99" - url: "https://pub.dev" - source: hosted - version: "5.5.0" multi_split_view: dependency: transitive description: @@ -1272,34 +1264,34 @@ packages: dependency: transitive description: name: riverpod - sha256: "82507cfb140c044f12e929c054dcdfc478359f473bcd2976af26908318e91b8e" + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" url: "https://pub.dev" source: hosted - version: "3.0.0-dev.17" + version: "2.6.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: ce9dfa8dccc5029535a09d1582681c894c8853613aaca5869d372348cf432114 + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" url: "https://pub.dev" source: hosted - version: "1.0.0-dev.4" + version: "0.5.10" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: d4449ce911fe1e211a2f6fbc110c907859b01419f720f604791fe8583a06620e + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 url: "https://pub.dev" source: hosted - version: "3.0.0-dev.17" + version: "2.6.1" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "16569af989111e5087da6cfd71660eb0dbcfb87e5395cfa5181ce089ff4f7729" + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" url: "https://pub.dev" source: hosted - version: "3.0.0-dev.17" + version: "2.6.5" screen_retriever: dependency: transitive description: @@ -1461,10 +1453,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "2.0.0" source_helper: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bf14afba..a4188900 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: easy_isolate: ^1.3.0 extended_image: ^10.0.0 file_picker: ^10.1.9 - flutter_riverpod: ^3.0.0-dev.17 + flutter_riverpod: ^2.6.1 flutter_highlight: ^0.7.0 flutter_displaymode: ^0.6.0 fl_chart: ^1.0.0 @@ -30,7 +30,7 @@ dependencies: json_annotation: ^4.9.0 responsive_framework: ^1.5.1 re_editor: ^0.7.0 - riverpod_annotation: ^3.0.0-dev.17 + riverpod_annotation: ^2.6.1 shared_preferences: ^2.1.1 wakelock_plus: ^1.2.4 wake_on_lan: ^4.1.1+3 @@ -63,7 +63,7 @@ dependencies: fl_lib: git: url: https://github.com/lppcg/fl_lib - ref: v1.0.338 + ref: v1.0.343 flutter_gbk2utf8: ^1.0.1 dependency_overrides: @@ -90,10 +90,14 @@ dev_dependencies: flutter_lints: ^6.0.0 json_serializable: ^6.8.0 freezed: ^3.0.0 - riverpod_generator: ^3.0.0-dev.17 + riverpod_generator: ^2.6.1 test: ^1.24.0 flutter_test: sdk: flutter + # riverpod_reg: + # git: + # url: https://github.com/lollipopkit/riverpod_reg + # ref: v0.0.2 fl_build: git: url: https://github.com/lppcg/fl_build.git @@ -120,18 +124,15 @@ flutter: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 + +riverpod_reg: + class_name: Riverpods + gen_path: lib/generated/riverpod_reg.dart + flutter_native_splash: color: "#ffffff" image: assets/app_icon.png color_dark: "#121212" - #background_image_dark: "assets/dark-background.png" - #image_dark: assets/splash-invert.png - #android: false - #ios: false - #web: false - #android_gravity: center - #ios_content_mode: center - #fullscreen: true info_plist_files: - "ios/Runner/Info-Debug.plist" - "ios/Runner/Info-Profile.plist" diff --git a/test/windows_test.dart b/test/windows_test.dart index 2a67f76f..7a0bdb17 100644 --- a/test/windows_test.dart +++ b/test/windows_test.dart @@ -122,7 +122,7 @@ void main() { }); test('should handle Windows script path generation', () { - final scriptPath = ShellFunc.status.exec('test-server', systemType: SystemType.windows); + final scriptPath = ShellFunc.status.exec('test-server', systemType: SystemType.windows, customDir: null); expect(scriptPath, contains('powershell')); expect(scriptPath, contains('-ExecutionPolicy Bypass')); @@ -131,7 +131,7 @@ void main() { test('should execute Windows commands correctly', () { for (final func in ShellFunc.values) { - final command = func.exec('test-server', systemType: SystemType.windows); + final command = func.exec('test-server', systemType: SystemType.windows, customDir: null); expect(command, isNotEmpty); expect(command, contains('powershell')); }