migrate: riverpod + freezed (#870)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-31 00:55:54 +08:00
committed by GitHub
parent 9cb705f8dd
commit 53a7c0d8ff
67 changed files with 5012 additions and 1328 deletions

View File

@@ -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) {

View File

@@ -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<SSHErrType> {
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<ContainerErrType> {
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<ContainerErrType> {
enum ICloudErrType { generic, notFound, multipleFiles }
class ICloudErr extends Err<ICloudErrType> {
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<ICloudErrType> {
enum WebdavErrType { generic, notFound }
class WebdavErr extends Err<WebdavErrType> {
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<WebdavErrType> {
enum PveErrType { unknown, net, loginFailed }
class PveErr extends Err<PveErrType> {
PveErr({required super.type, super.message});
const PveErr({required super.type, super.message});
@override
String? get solution => null;

View File

@@ -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);

View File

@@ -20,11 +20,11 @@ ServerCustom _$ServerCustomFromJson(Map<String, dynamic> json) => ServerCustom(
Map<String, dynamic> _$ServerCustomToJson(ServerCustom instance) =>
<String, dynamic>{
'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,
};

View File

@@ -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;

View File

@@ -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<Server>? get server => ServerProvider.pick(spi: this);
VNode<Server>? get jumpServer => ServerProvider.pick(id: jumpId);
bool shouldReconnect(Spi old) {
return user != old.user ||
ip != old.ip ||

View File

@@ -41,18 +41,19 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
'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 = {

View File

@@ -16,5 +16,5 @@ Map<String, dynamic> _$WakeOnLanCfgToJson(WakeOnLanCfg instance) =>
<String, dynamic>{
'mac': instance.mac,
'ip': instance.ip,
'pwd': ?instance.pwd,
if (instance.pwd case final value?) 'pwd': value,
};

View File

@@ -6,57 +6,20 @@ part of 'app.dart';
// RiverpodGenerator
// **************************************************************************
@ProviderFor(AppStates)
const appStatesProvider = AppStatesProvider._();
final class AppStatesProvider extends $NotifierProvider<AppStates, AppState> {
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<AppState>(value),
);
}
}
String _$appStatesHash() => r'ef96f10f6fff0f3dd6d3128ebf070ad79cbc8bc9';
abstract class _$AppStates extends $Notifier<AppState> {
AppState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AppState, AppState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AppState, AppState>,
AppState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// See also [AppStates].
@ProviderFor(AppStates)
final appStatesProvider = NotifierProvider<AppStates, AppState>.internal(
AppStates.new,
name: r'appStatesProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$appStatesHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AppStates = Notifier<AppState>;
// 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

View File

@@ -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<ContainerPs>? items;
List<ContainerImg>? images;
String? version;
ContainerErr? error;
String? runLog;
ContainerType type;
var sudoCompleter = Completer<bool>();
bool isBusy = false;
@freezed
abstract class ContainerState with _$ContainerState {
const factory ContainerState({
@Default(null) List<ContainerPs>? items,
@Default(null) List<ContainerImg>? 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<bool>();
@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<void> 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<bool>();
notifyListeners();
await refresh();
}
// Future<bool> _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<void> 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(
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<ContainerImg> 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<ContainerErr?> 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 = <String>[];
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 {

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$ContainerState {
List<ContainerPs>? get items; List<ContainerImg>? 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<ContainerState> get copyWith => _$ContainerStateCopyWithImpl<ContainerState>(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<ContainerPs>? items, List<ContainerImg>? 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<ContainerPs>?,images: freezed == images ? _self.images : images // ignore: cast_nullable_to_non_nullable
as List<ContainerImg>?,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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( List<ContainerPs>? items, List<ContainerImg>? 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 extends Object?>(TResult Function( List<ContainerPs>? items, List<ContainerImg>? 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 extends Object?>(TResult? Function( List<ContainerPs>? items, List<ContainerImg>? 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<ContainerPs>? items = null, final List<ContainerImg>? images = null, this.version = null, this.error = null, this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images;
final List<ContainerPs>? _items;
@override@JsonKey() List<ContainerPs>? 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<ContainerImg>? _images;
@override@JsonKey() List<ContainerImg>? 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<ContainerPs>? items, List<ContainerImg>? 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<ContainerPs>?,images: freezed == images ? _self._images : images // ignore: cast_nullable_to_non_nullable
as List<ContainerImg>?,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

View File

@@ -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<ContainerState> {
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<ContainerState> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'containerNotifierProvider';
}
/// See also [ContainerNotifier].
class ContainerNotifierProvider
extends AutoDisposeNotifierProviderImpl<ContainerNotifier, ContainerState> {
/// 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<ContainerNotifier, ContainerState>
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<ContainerState> {
/// 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<ContainerNotifier, ContainerState>
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

View File

@@ -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 = <PrivateKeyInfo>[].vn;
@freezed
abstract class PrivateKeyState with _$PrivateKeyState {
const factory PrivateKeyState({
@Default(<PrivateKeyInfo>[]) List<PrivateKeyInfo> 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);
}
}

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$PrivateKeyState {
List<PrivateKeyInfo> 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<PrivateKeyState> get copyWith => _$PrivateKeyStateCopyWithImpl<PrivateKeyState>(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<PrivateKeyInfo> 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<PrivateKeyInfo>,
));
}
}
/// 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( List<PrivateKeyInfo> 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 extends Object?>(TResult Function( List<PrivateKeyInfo> 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 extends Object?>(TResult? Function( List<PrivateKeyInfo> 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<PrivateKeyInfo> keys = const <PrivateKeyInfo>[]}): _keys = keys;
final List<PrivateKeyInfo> _keys;
@override@JsonKey() List<PrivateKeyInfo> 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<PrivateKeyInfo> 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<PrivateKeyInfo>,
));
}
}
// dart format on

View File

@@ -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<PrivateKeyNotifier, PrivateKeyState>.internal(
PrivateKeyNotifier.new,
name: r'privateKeyNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$privateKeyNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$PrivateKeyNotifier = Notifier<PrivateKeyState>;
// 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

View File

@@ -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<T extends Notifier<Object?>>(NotifierProvider<T, Object?> provider) {
return ref.read(provider.notifier);
}
T readProvider<T>(ProviderBase<T> provider) {
return ref.read(provider);
}
T watchProvider<T>(ProviderBase<T> 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<T>(ProviderBase<T> 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<T>(ProviderBase<T> 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<T extends Notifier<Object?>>(NotifierProvider<T, Object?> 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);
}

View File

@@ -7,41 +7,64 @@ 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<bool> 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<SSHForwardChannel> _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<String?>(null);
final connected = Completer<void>();
late final _ignoreCert = spi.custom?.pveIgnoreCert ?? false;
late final session = Dio()
void _initSession() {
session = Dio()
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
@@ -53,25 +76,21 @@ final class PveProvider extends ChangeNotifier {
},
validateCertificate: _ignoreCert ? (_, _, _) => true : null,
);
}
final data = ValueNotifier<PveRes?>(null);
bool get onlyOneNode => data.value?.nodes.length == 1;
String? release;
bool isBusy = false;
bool get onlyOneNode => state.data?.nodes.length == 1;
Future<void> _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<void> 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<bool> 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<bool> 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<bool> 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<bool> 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<void> dispose() async {
super.dispose();
try {
await _serverSocket.close();
} catch (_) {}
for (final forward in _forwards) {
try {
forward.close();
} catch (_) {}
}
}
}

View File

@@ -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>(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<PveState> get copyWith => _$PveStateCopyWithImpl<PveState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@@ -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<PveState> {
late final Spi spiParam;
PveState build(Spi spiParam);
}
/// See also [PveNotifier].
@ProviderFor(PveNotifier)
const pveNotifierProvider = PveNotifierFamily();
/// See also [PveNotifier].
class PveNotifierFamily extends Family<PveState> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'pveNotifierProvider';
}
/// See also [PveNotifier].
class PveNotifierProvider
extends AutoDisposeNotifierProviderImpl<PveNotifier, PveState> {
/// 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<PveNotifier, PveState> 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<PveState> {
/// The parameter `spiParam` of this provider.
Spi get spiParam;
}
class _PveNotifierProviderElement
extends AutoDisposeNotifierProviderElement<PveNotifier, PveState>
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

View File

@@ -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<String, VNode<Server>> servers = {};
static final serverOrder = <String>[].vn;
static final _tags = <String>{}.vn;
static VNode<Set<String>> get tags => _tags;
@freezed
abstract class ServersState with _$ServersState {
const factory ServersState({
@Default({}) Map<String, Spi> servers,
@Default([]) List<String> serverOrder,
@Default(<String>{}) Set<String> tags,
@Default(<String>{}) Set<String> 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<void>? updateFuture,
}) = _ServerState;
}
static final _manualDisconnectedIds = <String>{};
extension IndividualServerStateExtension on ServerState {
bool get needGenClient => conn < ServerConn.connecting;
static final _serverIdsUpdating = <String, Future<void>?>{};
bool get canViewDetails => conn == ServerConn.finished;
String get id => spi.id;
}
// Individual server state management
@riverpod
class IndividualServerNotifier extends _$IndividualServerNotifier {
@override
Future<void> load() async {
super.load();
// #147
// Clear all servers because of restarting app will cause duplicate servers
final oldServers = Map<String, VNode<Server>>.from(servers);
servers.clear();
serverOrder.value.clear();
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();
ServerState build(String serverId) {
final serverNotifier = ref.read(serverNotifierProvider);
final spi = serverNotifier.servers[serverId];
if (spi == null) {
throw StateError('Server $serverId not found');
}
/// Get a [Server] by [spi] or [id].
///
/// Priority: [spi] > [id]
static VNode<Server>? pick({Spi? spi, String? id}) {
if (spi != null) {
return servers[spi.id];
}
if (id != null) {
return servers[id];
}
return null;
return ServerState(spi: spi, status: InitStatus.status);
}
static void _updateTags() {
final tags = <String>{};
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 connection status
void updateConnection(ServerConn conn) {
state = state.copyWith(conn: conn);
}
static Server genServer(Spi spi) {
return Server(spi, InitStatus.status, ServerConn.disconnected);
// Update server status
void updateStatus(ServerStatus status) {
state = state.copyWith(status: status);
}
/// if [spi] is specificed then only refresh this server
/// [onlyFailed] only refresh failed servers
static Future<void> refresh({Spi? spi, bool onlyFailed = false}) async {
if (spi != null) {
_manualDisconnectedIds.remove(spi.id);
await _getData(spi);
// Update SSH client
void updateClient(SSHClient? client) {
state = state.copyWith(client: client);
}
// 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<void> 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);
}
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;
final updateFuture = _updateServer();
state = state.copyWith(updateFuture: updateFuture);
try {
await updateFuture;
} finally {
_serverIdsUpdating.remove(s.spi.id);
}
}),
);
}
static Future<void> _updateServer(Spi spi) async {
await _getData(spi);
}
static Future<void> 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;
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<void> _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<void> 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<Server> s, ServerConn ss) {
s.value.conn = ss;
s.notify();
}
static Future<void> _getData(Spi spi) async {
Future<void> _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,42 +164,43 @@ 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 (_, writeScriptResult) = await state.client!.exec(
(session) async {
final scriptRaw = ShellFuncManager.allScript(
spi.custom?.cmds,
systemType: detectedSystemType,
@@ -391,7 +208,13 @@ class ServerProvider extends Provider {
).uint8List;
session.stdin.add(scriptRaw);
session.stdin.close();
}, entry: ShellFuncManager.getInstallShellCmd(spi.id, systemType: detectedSystemType));
},
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,59 +222,57 @@ 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<String>? segments;
String? raw;
try {
final execResult = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system));
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 characters that cannot be parsed, try to fallback to gbk decoding
// If there are unparseable characters, try fallback to GBK decoding
if (rawStr.contains('<EFBFBD>')) {
Loggers.app.warning('UTF8 decoding failed, use GBK decoding');
needGbk = true;
@@ -459,7 +280,8 @@ class ServerProvider extends Provider {
} catch (e) {
Loggers.app.warning('UTF8 decoding failed, use GBK decoding', e);
needGbk = true;
}if (needGbk) {
}
if (needGbk) {
try {
rawStr = gbk.decode(execResult);
} catch (e2) {
@@ -477,74 +299,321 @@ class ServerProvider extends Provider {
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<void> _load() async {
final spis = Stores.server.fetch();
final newServers = <String, Spi>{};
final newServerOrder = <String>[];
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<String> _calculateTags(Map<String, Spi> servers) {
final tags = <String>{};
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<void> refresh({Spi? spi, bool onlyFailed = false}) async {
if (spi != null) {
final newManualDisconnected = Set<String>.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<void> 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<String>.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<String, Spi>.from(state.servers);
newServers[spi.id] = spi;
final newOrder = List<String>.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<String, Spi>.from(state.servers);
newServers.remove(id);
final newOrder = List<String>.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<void> updateServer(Spi old, Spi newSpi) async {
if (old != newSpi) {
Stores.server.update(old, newSpi);
final newServers = Map<String, Spi>.from(state.servers);
final newOrder = List<String>.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);
}
}

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$ServersState {
Map<String, Spi> get servers;// Only store server configuration information
List<String> get serverOrder; Set<String> get tags; Set<String> 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<ServersState> get copyWith => _$ServersStateCopyWithImpl<ServersState>(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<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> 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<String, Spi>,serverOrder: null == serverOrder ? _self.serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable
as List<String>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self.manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable
as Set<String>,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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> 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 extends Object?>(TResult Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> 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 extends Object?>(TResult? Function( Map<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> 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<String, Spi> servers = const {}, final List<String> serverOrder = const [], final Set<String> tags = const <String>{}, final Set<String> manualDisconnectedIds = const <String>{}, this.autoRefreshTimer}): _servers = servers,_serverOrder = serverOrder,_tags = tags,_manualDisconnectedIds = manualDisconnectedIds;
final Map<String, Spi> _servers;
@override@JsonKey() Map<String, Spi> get servers {
if (_servers is EqualUnmodifiableMapView) return _servers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_servers);
}
// Only store server configuration information
final List<String> _serverOrder;
// Only store server configuration information
@override@JsonKey() List<String> get serverOrder {
if (_serverOrder is EqualUnmodifiableListView) return _serverOrder;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_serverOrder);
}
final Set<String> _tags;
@override@JsonKey() Set<String> get tags {
if (_tags is EqualUnmodifiableSetView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_tags);
}
final Set<String> _manualDisconnectedIds;
@override@JsonKey() Set<String> 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<String, Spi> servers, List<String> serverOrder, Set<String> tags, Set<String> 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<String, Spi>,serverOrder: null == serverOrder ? _self._serverOrder : serverOrder // ignore: cast_nullable_to_non_nullable
as List<String>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,manualDisconnectedIds: null == manualDisconnectedIds ? _self._manualDisconnectedIds : manualDisconnectedIds // ignore: cast_nullable_to_non_nullable
as Set<String>,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<void>? 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<ServerState> get copyWith => _$ServerStateCopyWithImpl<ServerState>(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<void>? 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<void>?,
));
}
/// 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? 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 extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? 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 extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? 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<void>? 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<void>? 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<void>?,
));
}
/// 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

View File

@@ -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<ServerState> {
late final String serverId;
ServerState build(String serverId);
}
/// See also [IndividualServerNotifier].
@ProviderFor(IndividualServerNotifier)
const individualServerNotifierProvider = IndividualServerNotifierFamily();
/// See also [IndividualServerNotifier].
class IndividualServerNotifierFamily extends Family<ServerState> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'individualServerNotifierProvider';
}
/// See also [IndividualServerNotifier].
class IndividualServerNotifierProvider
extends
AutoDisposeNotifierProviderImpl<IndividualServerNotifier, ServerState> {
/// 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<IndividualServerNotifier, ServerState>
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<ServerState> {
/// 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<ServerNotifier, ServersState>.internal(
ServerNotifier.new,
name: r'serverNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$serverNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ServerNotifier = Notifier<ServersState>;
// 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

View File

@@ -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 = <SftpReqStatus>[].vn;
@freezed
abstract class SftpState with _$SftpState {
const factory SftpState({
@Default(<SftpReqStatus>[]) List<SftpReqStatus> 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<SftpReqStatus>.from(state.requests)
..removeAt(idx);
state = state.copyWith(requests: newRequests);
}
void _notifyListeners() {
// Force state update to notify listeners
state = state.copyWith();
}
}

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$SftpState {
List<SftpReqStatus> 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<SftpState> get copyWith => _$SftpStateCopyWithImpl<SftpState>(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<SftpReqStatus> 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<SftpReqStatus>,
));
}
}
/// 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( List<SftpReqStatus> 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 extends Object?>(TResult Function( List<SftpReqStatus> 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 extends Object?>(TResult? Function( List<SftpReqStatus> 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<SftpReqStatus> requests = const <SftpReqStatus>[]}): _requests = requests;
final List<SftpReqStatus> _requests;
@override@JsonKey() List<SftpReqStatus> 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<SftpReqStatus> 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<SftpReqStatus>,
));
}
}
// dart format on

View File

@@ -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<SftpNotifier, SftpState>.internal(
SftpNotifier.new,
name: r'sftpNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$sftpNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SftpNotifier = Notifier<SftpState>;
// 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

View File

@@ -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 = <Snippet>[].vn;
static final tags = <String>{}.vn;
@freezed
abstract class SnippetState with _$SnippetState {
const factory SnippetState({
@Default(<Snippet>[]) List<Snippet> snippets,
@Default(<String>{}) Set<String> 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<Snippet> 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);
}
}
snippets.value = snippets_;
_updateTags();
orderedSnippets = snippets;
}
static void _updateTags() {
final tags_ = <String>{};
for (final s in snippets.value) {
final tags = _computeTags(orderedSnippets);
return SnippetState(snippets: orderedSnippets, tags: tags);
}
Set<String> _computeTags(List<Snippet> snippets) {
final tags = <String>{};
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 = <Snippet>[];
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<String>.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);
}
}

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$SnippetState {
List<Snippet> get snippets; Set<String> 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<SnippetState> get copyWith => _$SnippetStateCopyWithImpl<SnippetState>(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<Snippet> snippets, Set<String> 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<Snippet>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,
));
}
}
/// 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( List<Snippet> snippets, Set<String> 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 extends Object?>(TResult Function( List<Snippet> snippets, Set<String> 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 extends Object?>(TResult? Function( List<Snippet> snippets, Set<String> 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<Snippet> snippets = const <Snippet>[], final Set<String> tags = const <String>{}}): _snippets = snippets,_tags = tags;
final List<Snippet> _snippets;
@override@JsonKey() List<Snippet> get snippets {
if (_snippets is EqualUnmodifiableListView) return _snippets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_snippets);
}
final Set<String> _tags;
@override@JsonKey() Set<String> 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<Snippet> snippets, Set<String> 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<Snippet>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as Set<String>,
));
}
}
// dart format on

View File

@@ -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<SnippetNotifier, SnippetState>.internal(
SnippetNotifier.new,
name: r'snippetNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$snippetNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SnippetNotifier = Notifier<SnippetState>;
// 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

View File

@@ -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<Server> _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(<SystemdUnit>[]) List<SystemdUnit> units,
@Default(SystemdScopeFilter.all) SystemdScopeFilter scopeFilter,
}) = _SystemdState;
}
final isBusy = false.vn;
final units = <SystemdUnit>[].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<SystemdUnit> 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<void> 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<List<SystemdUnit>> _parseUnitObj(List<String> 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);

View File

@@ -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>(T value) => value;
/// @nodoc
mixin _$SystemdState {
bool get isBusy; List<SystemdUnit> 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<SystemdState> get copyWith => _$SystemdStateCopyWithImpl<SystemdState>(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<SystemdUnit> 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<SystemdUnit>,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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( bool isBusy, List<SystemdUnit> 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 extends Object?>(TResult Function( bool isBusy, List<SystemdUnit> 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 extends Object?>(TResult? Function( bool isBusy, List<SystemdUnit> 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<SystemdUnit> units = const <SystemdUnit>[], this.scopeFilter = SystemdScopeFilter.all}): _units = units;
@override@JsonKey() final bool isBusy;
final List<SystemdUnit> _units;
@override@JsonKey() List<SystemdUnit> 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<SystemdUnit> 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<SystemdUnit>,scopeFilter: null == scopeFilter ? _self.scopeFilter : scopeFilter // ignore: cast_nullable_to_non_nullable
as SystemdScopeFilter,
));
}
}
// dart format on

View File

@@ -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<SystemdState> {
late final Spi spi;
SystemdState build(Spi spi);
}
/// See also [SystemdNotifier].
@ProviderFor(SystemdNotifier)
const systemdNotifierProvider = SystemdNotifierFamily();
/// See also [SystemdNotifier].
class SystemdNotifierFamily extends Family<SystemdState> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'systemdNotifierProvider';
}
/// See also [SystemdNotifier].
class SystemdNotifierProvider
extends AutoDisposeNotifierProviderImpl<SystemdNotifier, SystemdState> {
/// 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<SystemdNotifier, SystemdState>
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<SystemdState> {
/// The parameter `spi` of this provider.
Spi get spi;
}
class _SystemdNotifierProviderElement
extends AutoDisposeNotifierProviderElement<SystemdNotifier, SystemdState>
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

View File

@@ -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);

View File

@@ -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>(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<VirtKeyState> get copyWith => _$VirtKeyStateCopyWithImpl<VirtKeyState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@@ -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<VirtKeyboard, VirtKeyState>.internal(
VirtKeyboard.new,
name: r'virtKeyboardProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$virtKeyboardHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$VirtKeyboard = AutoDisposeNotifier<VirtKeyState>;
// 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

View File

@@ -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<void> main() async {
_runInZone(() async {
await _initApp();
runApp(const MyApp());
runApp(ProviderScope(child: const MyApp()));
});
}
@@ -66,12 +61,6 @@ Future<void> _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.

View File

@@ -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<BackupPage> createState() => _BackupPageState();
ConsumerState<BackupPage> createState() => _BackupPageState();
static const route = AppRouteNoArg(page: BackupPage.new, path: '/backup');
}
final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveClientMixin {
final class _BackupPageState extends ConsumerState<BackupPage> with AutomaticKeepAliveClientMixin {
final webdavLoading = false.vn;
final gistLoading = false.vn;
@@ -401,8 +402,9 @@ final class _BackupPageState extends State<BackupPage> 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();

View File

@@ -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<void> _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();
}

View File

@@ -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<ContainerPage> createState() => _ContainerPageState();
ConsumerState<ContainerPage> createState() => _ContainerPageState();
static const route = AppRouteArg(page: ContainerPage.new, path: '/container');
}
class _ContainerPageState extends State<ContainerPage> {
class _ContainerPageState extends ConsumerState<ContainerPage> {
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<ContainerProvider>(
builder: (_, _, _) {
final err = ref.watch(_provider.select((p) => p.error));
return Scaffold(
appBar: _buildAppBar,
body: SafeArea(child: _buildMain),
floatingActionButton: _container.error == null ? _buildFAB : null,
);
},
),
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<ContainerPage> {
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<ContainerPage> {
).paddingSymmetric(horizontal: 13),
);
}
if (_container.items == null || _container.images == null) {
if (containerState.items == null || containerState.images == null) {
return UIs.centerLoading;
}
return AutoMultiList(
children: <Widget>[
_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<ContainerPage> {
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<ContainerPage> {
);
}
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<ContainerPage> {
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<ContainerPage> {
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,8 +358,10 @@ class _ContainerPageState extends State<ContainerPage> {
_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;
}

View File

@@ -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<HomePage> createState() => _HomePageState();
ConsumerState<HomePage> createState() => _HomePageState();
static const route = AppRouteNoArg(page: HomePage.new, path: '/');
}
class _HomePageState extends State<HomePage>
class _HomePageState extends ConsumerState<HomePage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin, WidgetsBindingObserver {
late final PageController _pageController;
@@ -29,11 +31,14 @@ class _HomePageState extends State<HomePage>
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<HomePage>
_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<HomePage>
// }
} else {
//Pros.server.setDisconnected();
ServerProvider.stopAutoRefresh();
_notifier.stopAutoRefresh();
}
break;
default:
@@ -194,7 +200,9 @@ class _HomePageState extends State<HomePage>
AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context);
}
MethodChans.updateHomeWidget();
await ServerProvider.refresh();
await _notifier.refresh();
bakSync.sync(milliDelay: 1000);
}
// Future<void> _reqNotiPerm() async {
@@ -202,7 +210,6 @@ class _HomePageState extends State<HomePage>
// 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<HomePage>
// noNotiPerm.put(true);
// context.pop();
// },
// if (noNotiPerm.fetch()) return;
// child: Text(l10n.ok),
// ),
// ],

View File

@@ -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<PingPage> createState() => _PingPageState();
ConsumerState<PingPage> createState() => _PingPageState();
static const route = AppRouteNoArg(page: PingPage.new, path: '/ping');
}
class _PingPageState extends State<PingPage> with AutomaticKeepAliveClientMixin {
class _PingPageState extends ConsumerState<PingPage> with AutomaticKeepAliveClientMixin {
late TextEditingController _textEditingController;
final _results = ValueNotifier(<PingResult>[]);
bool get isInit => _results.value.isEmpty;
@@ -129,7 +130,7 @@ class _PingPageState extends State<PingPage> 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<PingPage> 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

View File

@@ -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<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
ConsumerState<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit');
}
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
class _PrivateKeyEditPageState extends ConsumerState<PrivateKeyEditPage> {
final _nameController = TextEditingController();
final _keyController = TextEditingController();
final _pwdController = TextEditingController();
@@ -39,6 +40,8 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
final _loading = ValueNotifier<Widget?>(null);
late final _notifier = ref.read(privateKeyNotifierProvider.notifier);
PrivateKeyInfo? get pki => widget.args?.pki;
@override
@@ -94,7 +97,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
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<PrivateKeyEditPage> {
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());

View File

@@ -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<PrivateKeysListPage> createState() => _PrivateKeyListState();
ConsumerState<PrivateKeysListPage> createState() => _PrivateKeyListState();
static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key');
}
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
class _PrivateKeyListState extends ConsumerState<PrivateKeysListPage> with AfterLayoutMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -31,14 +32,15 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMi
}
Widget _buildBody() {
return PrivateKeyProvider.pkis.listenVal((pkis) {
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);
});
}
Widget _buildKeyItem(PrivateKeyInfo item) {

View File

@@ -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<ProcessPage> createState() => _ProcessPageState();
ConsumerState<ProcessPage> createState() => _ProcessPageState();
static const route = AppRouteArg(page: ProcessPage.new, path: '/process');
}
class _ProcessPageState extends State<ProcessPage> {
class _ProcessPageState extends ConsumerState<ProcessPage> {
late Timer _timer;
late MediaQueryData _media;
@@ -36,6 +37,8 @@ class _ProcessPageState extends State<ProcessPage> {
ProcSortMode _procSortMode = ProcSortMode.cpu;
List<ProcSortMode> _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<ProcessPage> {
@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<ProcessPage> {
Future<void> _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);

View File

@@ -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<PvePage> createState() => _PvePageState();
ConsumerState<PvePage> createState() => _PvePageState();
static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');
}
const _kHorziPadding = 11.0;
final class _PvePageState extends State<PvePage> {
late final _pve = PveProvider(spi: widget.args.spi);
final class _PvePageState extends ConsumerState<PvePage> {
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<PvePage> {
@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
pveState.error == null
? UIs.placeholder
: Btn.icon(
icon: const Icon(Icons.refresh),
onTap: () {
_pve.err.value = null;
_pve.list();
_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<PvePage> {
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<PvePage> {
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<PvePage> {
}
extension on _PvePageState {
void _onCtrl(PveCtrlFunc func, String action, PveCtrlIface item) async {
void _onCtrl(String action, PveCtrlIface item, Future<bool> Function() func) async {
final sure = await context.showRoundDialog<bool>(
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) {
// 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));
}
}
}

View File

@@ -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<ServerDetailPage> createState() => _ServerDetailPageState();
ConsumerState<ServerDetailPage> createState() => _ServerDetailPageState();
static const route = AppRouteArg(page: ServerDetailPage.new, path: '/servers/detail');
}
class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerProviderStateMixin {
class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with SingleTickerProviderStateMixin {
late final _cardBuildMap = Map.fromIterables(ServerDetailCards.names, [
_buildAbout,
_buildCPUView,
@@ -84,17 +86,17 @@ class _ServerDetailPageState extends State<ServerDetailPage> 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 = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];
@@ -111,7 +113,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> with SingleTickerPr
);
}
Widget? _buildDiskView(Server si) {
Widget? _buildDiskView(ServerState si) {
final ss = si.status;
final children = <Widget>[];
@@ -553,7 +555,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> 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 = <Widget>[];
@@ -570,7 +572,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> with SingleTickerPr
);
}
Widget? _buildNetView(Server si) {
Widget? _buildNetView(ServerState si) {
final ss = si.status;
final ns = ss.netSpeed;
final children = <Widget>[];
@@ -847,7 +849,7 @@ class _ServerDetailPageState extends State<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> 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<ServerDetailPage> with SingleTickerPr
);
}
Widget? _buildCustomCmd(Server si) {
Widget? _buildCustomCmd(ServerState si) {
final ss = si.status;
if (ss.customCmds.isEmpty) return null;
return CardX(

View File

@@ -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<bool, SpiRequiredArgs>(page: ServerEditPage.new, path: '/servers/edit');
@override
State<ServerEditPage> createState() => _ServerEditPageState();
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
}
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayoutMixin {
late final spi = widget.args?.spi;
final _nameController = TextEditingController();
final _ipController = TextEditingController();
@@ -167,7 +167,7 @@ class _ServerEditPageState extends State<ServerEditPage> 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,7 +227,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
}
Widget _buildKeyAuth() {
return PrivateKeyProvider.pkis.listenVal((pkis) {
final privateKeyState = ref.watch(privateKeyNotifierProvider);
final pkis = privateKeyState.keys;
final tiles = List<Widget>.generate(pkis.length, (index) {
final e = pkis[index];
return ListTile(
@@ -254,7 +256,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
onChanged: (val) => _keyIdx.value = val,
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx,
);
});
}
Widget _buildEnvs() {
@@ -485,27 +486,26 @@ class _ServerEditPageState extends State<ServerEditPage> 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<Server>(
return Choice<Spi>(
multiple: false,
clearable: true,
value: srv != null ? [srv] : [],
builder: (state, _) => Wrap(
children: List<Widget>.generate(srvs.length, (index) {
final item = srvs[index];
return ChoiceChipX<Server>(
label: item.spi.name,
return ChoiceChipX<Spi>(
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<ServerEditPage> 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

View File

@@ -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),

View File

@@ -26,7 +26,9 @@ extension on _ServerPageState {
}
Widget _buildLandscapeBody() {
return ServerProvider.serverOrder.listenVal((order) {
final serverState = ref.watch(serverNotifierProvider);
final order = serverState.serverOrder;
if (order.isEmpty) {
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
}
@@ -35,10 +37,8 @@ extension on _ServerPageState {
itemCount: order.length,
itemBuilder: (_, idx) {
final id = order[idx];
final srv = ServerProvider.pick(id: id);
if (srv == null) return UIs.placeholder;
final srv = ref.watch(individualServerNotifierProvider(id));
return srv.listenVal((srv) {
final title = _buildServerCardTitle(srv);
final List<Widget> children = [title, _buildNormalCard(srv.status, srv.spi)];
@@ -50,9 +50,7 @@ extension on _ServerPageState {
children: children,
);
});
});
},
);
});
}
}

View File

@@ -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<ServerPage> createState() => _ServerPageState();
ConsumerState<ServerPage> 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<ServerPage> with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
class _ServerPageState extends ConsumerState<ServerPage>
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
late double _textFactorDouble;
double _offset = 1;
late TextScaler _textFactor;
final _cardsStatus = <String, _CardNotifier>{};
late final ValueNotifier<Set<String>> _tags;
Timer? _timer;
@@ -64,11 +67,13 @@ class _ServerPageState extends State<ServerPage> 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<ServerPage> 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<ServerPage> 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,9 +132,9 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
Widget _buildPortrait() {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
return ServerProvider.serverOrder.listenVal((order) {
final serverState = ref.watch(serverNotifierProvider);
return _tag.listenVal((val) {
final filtered = _filterServers(order);
final filtered = _filterServers(serverState.serverOrder);
final child = _buildScaffold(_buildBodySmall(filtered: filtered));
// if (isMobile) {
return child;
@@ -138,7 +148,6 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
// leftBuilder: (_, __) => child,
// );
});
});
}
Widget _buildBodySmall({required List<String> filtered}) {
@@ -173,10 +182,9 @@ class _ServerPageState extends State<ServerPage> 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<ServerPage> 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<ServerPage> 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<ServerPage> 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<ServerPage> with AutomaticKeepAliveClientMi
@override
Future<void> afterFirstLayout(BuildContext context) async {
ServerProvider.refresh();
ServerProvider.startAutoRefresh();
ref.read(serverNotifierProvider.notifier).refresh();
ref.read(serverNotifierProvider.notifier).startAutoRefresh();
}
static const _kCardHeightMin = 23.0;

View File

@@ -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;

View File

@@ -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<String>(
clearable: true,

View File

@@ -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<SettingsPage> createState() => _SettingsPageState();
ConsumerState<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> with SingleTickerProviderStateMixin {
class _SettingsPageState extends ConsumerState<SettingsPage> with SingleTickerProviderStateMixin {
late final _tabCtrl = TabController(length: SettingsTabs.values.length, vsync: this);
@override
@@ -98,14 +99,14 @@ class _SettingsPageState extends State<SettingsPage> with SingleTickerProviderSt
}
}
final class AppSettingsPage extends StatefulWidget {
final class AppSettingsPage extends ConsumerStatefulWidget {
const AppSettingsPage({super.key});
@override
State<AppSettingsPage> createState() => _AppSettingsPageState();
ConsumerState<AppSettingsPage> createState() => _AppSettingsPageState();
}
final class _AppSettingsPageState extends State<AppSettingsPage> {
final class _AppSettingsPageState extends ConsumerState<AppSettingsPage> {
final _setting = Stores.setting;
late final _sshOpacityCtrl = TextEditingController(text: _setting.sshBgOpacity.fetch().toString());

View File

@@ -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<ServerOrderPage> createState() => _ServerOrderPageState();
ConsumerState<ServerOrderPage> createState() => _ServerOrderPageState();
static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server');
}
class _ServerOrderPageState extends State<ServerOrderPage> {
class _ServerOrderPageState extends ConsumerState<ServerOrderPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -41,8 +42,9 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}
Widget _buildBody() {
final orders = ServerProvider.serverOrder;
return orders.listenVal((order) {
final serverState = ref.watch(serverNotifierProvider);
final order = serverState.serverOrder;
if (order.isEmpty) {
return Center(child: Text(libL10n.empty));
}
@@ -50,7 +52,9 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
footer: const SizedBox(height: 77),
onReorder: (oldIndex, newIndex) {
setState(() {
orders.value.move(oldIndex, newIndex, property: Stores.setting.serverOrder);
final newOrder = List<String>.from(order);
newOrder.move(oldIndex, newIndex);
Stores.setting.serverOrder.put(newOrder);
});
},
padding: const EdgeInsets.all(8),
@@ -59,7 +63,6 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
itemCount: order.length,
proxyDecorator: _proxyDecorator,
);
});
}
Widget _buildItem(int index, String id) {
@@ -74,8 +77,10 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}
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();
}

View File

@@ -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<SnippetEditPage> createState() => _SnippetEditPageState();
ConsumerState<SnippetEditPage> createState() => _SnippetEditPageState();
static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit');
}
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
class _SnippetEditPageState extends ConsumerState<SnippetEditPage> with AfterLayoutMixin {
final _nameController = TextEditingController();
final _scriptController = TextEditingController();
final _noteController = TextEditingController();
@@ -61,7 +62,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> 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<SnippetEditPage> 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<SnippetEditPage> 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<SnippetEditPage> 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<SnippetEditPage> 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,
);

View File

@@ -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<SnippetListPage> createState() => _SnippetListPageState();
ConsumerState<SnippetListPage> createState() => _SnippetListPageState();
static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets');
}
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
class _SnippetListPageState extends ConsumerState<SnippetListPage> with AutomaticKeepAliveClientMixin {
final _tag = ''.vn;
final _splitViewCtrl = SplitViewController();
@@ -35,7 +36,9 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
Widget _buildBody() {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
return SnippetProvider.snippets.listenVal((snippets) {
final snippetState = ref.watch(snippetNotifierProvider);
final snippets = snippetState.snippets;
return _tag.listenVal((tag) {
final child = _buildScaffold(snippets, tag);
// if (isMobile) {
@@ -50,13 +53,13 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
// leftBuilder: (_, __) => child,
// );
});
});
}
Widget _buildScaffold(List<Snippet> 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,
),

View File

@@ -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);
}

View File

@@ -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<SSHPage> createState() => SSHPageState();
ConsumerState<SSHPage> createState() => SSHPageState();
static const route = AppRouteArg<void, SshPageArgs>(page: SSHPage.new, path: '/ssh/page');
}
const _horizonPadding = 7.0;
class SSHPageState extends State<SSHPage>
class SSHPageState extends ConsumerState<SSHPage>
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<List<VirtKey>> _virtKeysList = [];
late final _termKey = widget.args.terminalKey ?? GlobalKey<TerminalViewState>();
@@ -81,7 +80,7 @@ class SSHPageState extends State<SSHPage>
bool _isDark = false;
Timer? _virtKeyLongPressTimer;
late SSHClient? _client = widget.args.spi.server?.value.client;
SSHClient? _client;
SSHSession? _session;
Timer? _discontinuityTimer;
@@ -118,6 +117,10 @@ class SSHPageState extends State<SSHPage>
_initVirtKeys();
_setupDiscontinuityTimer();
// Initialize client from provider
final serverState = ref.read(individualServerNotifierProvider(widget.args.spi.id));
_client = serverState.client;
if (++_sshConnCount == 1) {
WakelockPlus.enable();
if (isAndroid) {
@@ -262,19 +265,22 @@ class SSHPageState extends State<SSHPage>
child: Container(
color: _terminalTheme.background,
height: _virtKeysHeight + _media.padding.bottom,
child: ChangeNotifierProvider(
create: (_) => _keyboard,
builder: (_, _) => Consumer<VirtKeyProvider>(
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<SSHPage>
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<SSHPage>
);
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),
);
}
},

View File

@@ -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<Snippet>(
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();
},

View File

@@ -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<SSHTabPage> createState() => _SSHTabPageState();
ConsumerState<SSHTabPage> createState() => _SSHTabPageState();
static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh');
}
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
class _SSHTabPageState extends State<SSHTabPage>
class _SSHTabPageState extends ConsumerState<SSHTabPage>
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,7 +259,8 @@ class _AddPage extends StatelessWidget {
final crossCount = max(viewWidth ~/ (visualCrossCount * itemPadding + itemWidth), 1);
final mainCount = itemCount ~/ crossCount + 1;
return ServerProvider.serverOrder.listenVal((order) {
final order = serverState.serverOrder;
if (order.isEmpty) {
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
}
@@ -271,7 +274,7 @@ class _AddPage extends StatelessWidget {
children: List.generate(crossCount, (columnIndex) {
final idx = rowIndex * crossCount + columnIndex;
final id = order.elementAtOrNull(idx);
final spi = ServerProvider.pick(id: id)?.value.spi;
final spi = serverState.servers[id];
if (spi == null) return _placeholder;
return Expanded(
@@ -304,6 +307,5 @@ class _AddPage extends StatelessWidget {
),
),
);
});
}
}

View File

@@ -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<String, LocalFilePageArgs>(page: LocalFilePage.new, path: '/files/local');
@override
State<LocalFilePage> createState() => _LocalFilePageState();
ConsumerState<LocalFilePage> createState() => _LocalFilePageState();
}
class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveClientMixin {
class _LocalFilePageState extends ConsumerState<LocalFilePage> 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<Spi>(
title: libL10n.select,
items: ServerProvider.serverOrder.value
.map((e) => ServerProvider.pick(id: e)?.value.spi)
.whereType<Spi>()
.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);
}
}

View File

@@ -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,22 +31,30 @@ 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<SftpPage> createState() => _SftpPageState();
ConsumerState<SftpPage> createState() => _SftpPageState();
static const route = AppRouteArg<String, SftpPageArgs>(page: SftpPage.new, path: '/sftp');
}
class _SftpPageState extends State<SftpPage> with AfterLayoutMixin {
late final _status = SftpBrowserStatus(_client);
late final _client = widget.args.spi.server!.value.client!;
class _SftpPageState extends ConsumerState<SftpPage> 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() {
super.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),
);

View File

@@ -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<SftpMissionPage> createState() => _SftpMissionPageState();
ConsumerState<SftpMissionPage> createState() => _SftpMissionPageState();
static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission');
}
class _SftpMissionPageState extends State<SftpMissionPage> {
class _SftpMissionPageState extends ConsumerState<SftpMissionPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -24,7 +25,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
}
Widget _buildBody() {
return SftpProvider.status.listenVal((status) {
final status = ref.watch(sftpNotifierProvider.select((pro) => pro.requests));
if (status.isEmpty) {
return Center(child: Text(libL10n.empty));
}
@@ -35,7 +36,6 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
return _buildItem(status[index]);
},
);
});
}
Widget _buildItem(SftpReqStatus status) {
@@ -143,7 +143,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
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,

View File

@@ -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,45 +15,40 @@ final class SystemdPage extends StatefulWidget {
static const route = AppRouteArg<void, SpiRequiredArgs>(page: SystemdPage.new, path: '/systemd');
@override
State<SystemdPage> createState() => _SystemdPageState();
ConsumerState<SystemdPage> createState() => _SystemdPageState();
}
final class _SystemdPageState extends State<SystemdPage> {
late final _pro = SystemdProvider.init(widget.args.spi);
final class _SystemdPageState extends ConsumerState<SystemdPage> {
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: <Widget>[
SliverToBoxAdapter(
child: Column(
children: [
_buildScopeFilterChips(),
_pro.isBusy.listenVal(
(isBusy) => AnimatedContainer(
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,7 +58,7 @@ final class _SystemdPageState extends State<SystemdPage> {
}
Widget _buildScopeFilterChips() {
return _pro.scopeFilter.listenVal((currentFilter) {
final currentFilter = ref.watch(_pro.select((p) => p.scopeFilter));
return Wrap(
spacing: 8,
children: SystemdScopeFilter.values.map((filter) {
@@ -70,17 +66,16 @@ final class _SystemdPageState extends State<SystemdPage> {
return FilterChip(
selected: isSelected,
label: Text(filter.displayName),
onSelected: (_) => _pro.scopeFilter.value = filter,
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;
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));
}
@@ -97,8 +92,6 @@ final class _SystemdPageState extends State<SystemdPage> {
).cardx.paddingSymmetric(horizontal: 13);
}, childCount: filteredUnits.length),
);
});
});
}
Widget _buildUnitFuncs(SystemdUnit unit) {

View File

@@ -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<ServerFuncBtn>(
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<Snippet>(
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;
}

View File

@@ -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:

View File

@@ -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"

View File

@@ -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'));
}