feat: Added Port Forwarding Functionality (#1083)
* feat: Added Port Forwarding Functionality Implemented port forwarding functionality, including the following major changes: - Added a port forwarding configuration model and related state management - Added a port forwarding page and interaction logic - Implemented forwarding connections between local and remote ports - Integrated into the server features menu - Added necessary Hive adapters and storage support - Updated plugin configurations across all platforms to support the new feature * feat (Port Forwarding): Added multilingual support and optimized implementation Added multilingual support for the port forwarding feature, including Chinese, English, and other languages Optimized the port forwarding implementation by adding connection management and error handling Fixed an issue with state persistence when updating port forwarding configurations Updated related dependencies and submodules * fix(port_forward): Fixed port forwarding error handling and redesigned the configuration dialog Handled uncaught errors when port forwarding is disabled or during connection attempts Extracted the configuration dialog into a standalone component and added port range validation * fix(port_forward): Fixed issues with port forwarding connection management and UI layout Fixed an issue where port forwarding connections were not closed properly; now uses `clientGetter` to delay the retrieval of `SSHClient` Added cleanup logic when connections are closed to prevent memory leaks Added a `mounted` check in `PortForwardPage` to prevent operations from executing after the component is unmounted Wrapped the configuration dialog content in a `SingleChildScrollView` to prevent content overflow * fix(port_forward): Fixed a concurrent modification exception that occurred when closing a port forwarding connection Fixed a concurrent modification exception that could occur when closing a local forwarding entry by copying the connection list to prevent modifications to the collection during iteration. Also improved the UI by using theme colors and added error handling for configuration saving. * fix(port_forward_provider): Fixed an issue where entries were not properly removed when port forwarding was stopped When port forwarding is stopped, ensure that the corresponding entries are removed from the _forwards map. Additionally, before adding a new forwarding rule, check for and close any existing forwarding rules with the same ID to prevent resource leaks. * refactor(l1n): Remove unused localization and remote host port translations * fix(port_forward_provider): Handle errors when closing port forwarding Add error handling to prevent the program from crashing due to exceptions when closing port forwarding * refactor(port_forward): Refactor port forwarding state management to use serverId Directly link port forwarding state management to the server ID to simplify parameter passing Remove direct dependencies on Spi and use serverId as the core identifier instead Update relevant providers and page logic to accommodate the new state structure * fix(port_forward): Fixed a race condition issue in port forwarding operations Added an _inFlight collection to prevent duplicate operations Added a _saving state when saving configurations to prevent duplicate submissions Automatically cleans up forwarding when changes in server connection status are detected * refactor(port_forward_provider): Remove unnecessary concurrency control logic Simplify the `toggleForward` method by removing concurrency control for the `_inFlight` collection, as it is not required in the current scenario
This commit is contained in:
@@ -12,21 +12,33 @@ enum ServerFuncBtn {
|
||||
snippet(),
|
||||
iperf(),
|
||||
// pve(),
|
||||
systemd(1058);
|
||||
systemd(1058),
|
||||
portForward(1340);
|
||||
|
||||
final int? addedVersion;
|
||||
|
||||
const ServerFuncBtn([this.addedVersion]);
|
||||
|
||||
static void autoAddNewFuncs(int cur) {
|
||||
if (cur >= systemd.addedVersion!) {
|
||||
final prop = Stores.setting.serverFuncBtns;
|
||||
final list = prop.fetch();
|
||||
final originalLength = list.length;
|
||||
|
||||
if (systemd.addedVersion != null && cur >= systemd.addedVersion!) {
|
||||
if (!list.contains(systemd.index)) {
|
||||
list.add(systemd.index);
|
||||
prop.put(list);
|
||||
}
|
||||
}
|
||||
|
||||
if (portForward.addedVersion != null && cur >= portForward.addedVersion!) {
|
||||
if (!list.contains(portForward.index)) {
|
||||
list.add(portForward.index);
|
||||
}
|
||||
}
|
||||
|
||||
if (list.length > originalLength) {
|
||||
prop.put(list);
|
||||
}
|
||||
}
|
||||
|
||||
static final defaultIdxs = [
|
||||
@@ -48,6 +60,7 @@ enum ServerFuncBtn {
|
||||
terminal => Icons.terminal,
|
||||
iperf => Icons.speed,
|
||||
systemd => MingCute.plugin_2_fill,
|
||||
portForward => Icons.compare_arrows,
|
||||
};
|
||||
|
||||
String get toStr => switch (this) {
|
||||
@@ -59,5 +72,6 @@ enum ServerFuncBtn {
|
||||
terminal => libL10n.terminal,
|
||||
iperf => 'iperf',
|
||||
systemd => 'Systemd',
|
||||
portForward => libL10n.portForward,
|
||||
};
|
||||
}
|
||||
|
||||
59
lib/data/model/server/port_forward.dart
Normal file
59
lib/data/model/server/port_forward.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'port_forward.freezed.dart';
|
||||
part 'port_forward.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class PortForwardConfig with _$PortForwardConfig {
|
||||
const factory PortForwardConfig({
|
||||
required String id,
|
||||
required String serverId,
|
||||
required String name,
|
||||
@Default('localhost') String localHost,
|
||||
required int localPort,
|
||||
required String remoteHost,
|
||||
required int remotePort,
|
||||
String? description,
|
||||
}) = _PortForwardConfig;
|
||||
|
||||
factory PortForwardConfig.fromJson(Map<String, dynamic> json) => _$PortForwardConfigFromJson(json);
|
||||
|
||||
const PortForwardConfig._();
|
||||
|
||||
String get displayAddr => '$localHost:$localPort → $remoteHost:$remotePort';
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PortForwardState with _$PortForwardState {
|
||||
const factory PortForwardState({
|
||||
required String serverId,
|
||||
@Default([]) List<PortForwardConfig> configs,
|
||||
@Default({}) Map<String, PortForwardStatus> activeForwards,
|
||||
}) = _PortForwardState;
|
||||
}
|
||||
|
||||
class PortForwardStatus {
|
||||
final String id;
|
||||
final bool isActive;
|
||||
final String? error;
|
||||
|
||||
const PortForwardStatus({
|
||||
required this.id,
|
||||
this.isActive = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
PortForwardStatus copyWith({
|
||||
String? id,
|
||||
bool? isActive,
|
||||
Object? error = _sentinel,
|
||||
}) {
|
||||
return PortForwardStatus(
|
||||
id: id ?? this.id,
|
||||
isActive: isActive ?? this.isActive,
|
||||
error: error == _sentinel ? this.error : error as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _sentinel = Object();
|
||||
573
lib/data/model/server/port_forward.freezed.dart
Normal file
573
lib/data/model/server/port_forward.freezed.dart
Normal file
@@ -0,0 +1,573 @@
|
||||
// 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 'port_forward.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PortForwardConfig {
|
||||
|
||||
String get id; String get serverId; String get name; String get localHost; int get localPort; String get remoteHost; int get remotePort; String? get description;
|
||||
/// Create a copy of PortForwardConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PortForwardConfigCopyWith<PortForwardConfig> get copyWith => _$PortForwardConfigCopyWithImpl<PortForwardConfig>(this as PortForwardConfig, _$identity);
|
||||
|
||||
/// Serializes this PortForwardConfig to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PortForwardConfig&&(identical(other.id, id) || other.id == id)&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.name, name) || other.name == name)&&(identical(other.localHost, localHost) || other.localHost == localHost)&&(identical(other.localPort, localPort) || other.localPort == localPort)&&(identical(other.remoteHost, remoteHost) || other.remoteHost == remoteHost)&&(identical(other.remotePort, remotePort) || other.remotePort == remotePort)&&(identical(other.description, description) || other.description == description));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,serverId,name,localHost,localPort,remoteHost,remotePort,description);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortForwardConfig(id: $id, serverId: $serverId, name: $name, localHost: $localHost, localPort: $localPort, remoteHost: $remoteHost, remotePort: $remotePort, description: $description)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PortForwardConfigCopyWith<$Res> {
|
||||
factory $PortForwardConfigCopyWith(PortForwardConfig value, $Res Function(PortForwardConfig) _then) = _$PortForwardConfigCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String serverId, String name, String localHost, int localPort, String remoteHost, int remotePort, String? description
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PortForwardConfigCopyWithImpl<$Res>
|
||||
implements $PortForwardConfigCopyWith<$Res> {
|
||||
_$PortForwardConfigCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PortForwardConfig _self;
|
||||
final $Res Function(PortForwardConfig) _then;
|
||||
|
||||
/// Create a copy of PortForwardConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? serverId = null,Object? name = null,Object? localHost = null,Object? localPort = null,Object? remoteHost = null,Object? remotePort = null,Object? description = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,localHost: null == localHost ? _self.localHost : localHost // ignore: cast_nullable_to_non_nullable
|
||||
as String,localPort: null == localPort ? _self.localPort : localPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,remoteHost: null == remoteHost ? _self.remoteHost : remoteHost // ignore: cast_nullable_to_non_nullable
|
||||
as String,remotePort: null == remotePort ? _self.remotePort : remotePort // ignore: cast_nullable_to_non_nullable
|
||||
as int,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PortForwardConfig].
|
||||
extension PortForwardConfigPatterns on PortForwardConfig {
|
||||
/// 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( _PortForwardConfig value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardConfig() 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( _PortForwardConfig value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardConfig():
|
||||
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( _PortForwardConfig value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardConfig() 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( String id, String serverId, String name, String localHost, int localPort, String remoteHost, int remotePort, String? description)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardConfig() when $default != null:
|
||||
return $default(_that.id,_that.serverId,_that.name,_that.localHost,_that.localPort,_that.remoteHost,_that.remotePort,_that.description);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( String id, String serverId, String name, String localHost, int localPort, String remoteHost, int remotePort, String? description) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardConfig():
|
||||
return $default(_that.id,_that.serverId,_that.name,_that.localHost,_that.localPort,_that.remoteHost,_that.remotePort,_that.description);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( String id, String serverId, String name, String localHost, int localPort, String remoteHost, int remotePort, String? description)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardConfig() when $default != null:
|
||||
return $default(_that.id,_that.serverId,_that.name,_that.localHost,_that.localPort,_that.remoteHost,_that.remotePort,_that.description);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _PortForwardConfig extends PortForwardConfig {
|
||||
const _PortForwardConfig({required this.id, required this.serverId, required this.name, this.localHost = 'localhost', required this.localPort, required this.remoteHost, required this.remotePort, this.description}): super._();
|
||||
factory _PortForwardConfig.fromJson(Map<String, dynamic> json) => _$PortForwardConfigFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String serverId;
|
||||
@override final String name;
|
||||
@override@JsonKey() final String localHost;
|
||||
@override final int localPort;
|
||||
@override final String remoteHost;
|
||||
@override final int remotePort;
|
||||
@override final String? description;
|
||||
|
||||
/// Create a copy of PortForwardConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PortForwardConfigCopyWith<_PortForwardConfig> get copyWith => __$PortForwardConfigCopyWithImpl<_PortForwardConfig>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PortForwardConfigToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PortForwardConfig&&(identical(other.id, id) || other.id == id)&&(identical(other.serverId, serverId) || other.serverId == serverId)&&(identical(other.name, name) || other.name == name)&&(identical(other.localHost, localHost) || other.localHost == localHost)&&(identical(other.localPort, localPort) || other.localPort == localPort)&&(identical(other.remoteHost, remoteHost) || other.remoteHost == remoteHost)&&(identical(other.remotePort, remotePort) || other.remotePort == remotePort)&&(identical(other.description, description) || other.description == description));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,serverId,name,localHost,localPort,remoteHost,remotePort,description);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortForwardConfig(id: $id, serverId: $serverId, name: $name, localHost: $localHost, localPort: $localPort, remoteHost: $remoteHost, remotePort: $remotePort, description: $description)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PortForwardConfigCopyWith<$Res> implements $PortForwardConfigCopyWith<$Res> {
|
||||
factory _$PortForwardConfigCopyWith(_PortForwardConfig value, $Res Function(_PortForwardConfig) _then) = __$PortForwardConfigCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String serverId, String name, String localHost, int localPort, String remoteHost, int remotePort, String? description
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PortForwardConfigCopyWithImpl<$Res>
|
||||
implements _$PortForwardConfigCopyWith<$Res> {
|
||||
__$PortForwardConfigCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PortForwardConfig _self;
|
||||
final $Res Function(_PortForwardConfig) _then;
|
||||
|
||||
/// Create a copy of PortForwardConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? serverId = null,Object? name = null,Object? localHost = null,Object? localPort = null,Object? remoteHost = null,Object? remotePort = null,Object? description = freezed,}) {
|
||||
return _then(_PortForwardConfig(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,localHost: null == localHost ? _self.localHost : localHost // ignore: cast_nullable_to_non_nullable
|
||||
as String,localPort: null == localPort ? _self.localPort : localPort // ignore: cast_nullable_to_non_nullable
|
||||
as int,remoteHost: null == remoteHost ? _self.remoteHost : remoteHost // ignore: cast_nullable_to_non_nullable
|
||||
as String,remotePort: null == remotePort ? _self.remotePort : remotePort // ignore: cast_nullable_to_non_nullable
|
||||
as int,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PortForwardState {
|
||||
|
||||
String get serverId; List<PortForwardConfig> get configs; Map<String, PortForwardStatus> get activeForwards;
|
||||
/// Create a copy of PortForwardState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PortForwardStateCopyWith<PortForwardState> get copyWith => _$PortForwardStateCopyWithImpl<PortForwardState>(this as PortForwardState, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PortForwardState&&(identical(other.serverId, serverId) || other.serverId == serverId)&&const DeepCollectionEquality().equals(other.configs, configs)&&const DeepCollectionEquality().equals(other.activeForwards, activeForwards));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,const DeepCollectionEquality().hash(configs),const DeepCollectionEquality().hash(activeForwards));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortForwardState(serverId: $serverId, configs: $configs, activeForwards: $activeForwards)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PortForwardStateCopyWith<$Res> {
|
||||
factory $PortForwardStateCopyWith(PortForwardState value, $Res Function(PortForwardState) _then) = _$PortForwardStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String serverId, List<PortForwardConfig> configs, Map<String, PortForwardStatus> activeForwards
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PortForwardStateCopyWithImpl<$Res>
|
||||
implements $PortForwardStateCopyWith<$Res> {
|
||||
_$PortForwardStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PortForwardState _self;
|
||||
final $Res Function(PortForwardState) _then;
|
||||
|
||||
/// Create a copy of PortForwardState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? serverId = null,Object? configs = null,Object? activeForwards = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,configs: null == configs ? _self.configs : configs // ignore: cast_nullable_to_non_nullable
|
||||
as List<PortForwardConfig>,activeForwards: null == activeForwards ? _self.activeForwards : activeForwards // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, PortForwardStatus>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PortForwardState].
|
||||
extension PortForwardStatePatterns on PortForwardState {
|
||||
/// 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( _PortForwardState value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardState() 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( _PortForwardState value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardState():
|
||||
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( _PortForwardState value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardState() 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( String serverId, List<PortForwardConfig> configs, Map<String, PortForwardStatus> activeForwards)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardState() when $default != null:
|
||||
return $default(_that.serverId,_that.configs,_that.activeForwards);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( String serverId, List<PortForwardConfig> configs, Map<String, PortForwardStatus> activeForwards) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardState():
|
||||
return $default(_that.serverId,_that.configs,_that.activeForwards);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( String serverId, List<PortForwardConfig> configs, Map<String, PortForwardStatus> activeForwards)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PortForwardState() when $default != null:
|
||||
return $default(_that.serverId,_that.configs,_that.activeForwards);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _PortForwardState implements PortForwardState {
|
||||
const _PortForwardState({required this.serverId, final List<PortForwardConfig> configs = const [], final Map<String, PortForwardStatus> activeForwards = const {}}): _configs = configs,_activeForwards = activeForwards;
|
||||
|
||||
|
||||
@override final String serverId;
|
||||
final List<PortForwardConfig> _configs;
|
||||
@override@JsonKey() List<PortForwardConfig> get configs {
|
||||
if (_configs is EqualUnmodifiableListView) return _configs;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_configs);
|
||||
}
|
||||
|
||||
final Map<String, PortForwardStatus> _activeForwards;
|
||||
@override@JsonKey() Map<String, PortForwardStatus> get activeForwards {
|
||||
if (_activeForwards is EqualUnmodifiableMapView) return _activeForwards;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_activeForwards);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of PortForwardState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PortForwardStateCopyWith<_PortForwardState> get copyWith => __$PortForwardStateCopyWithImpl<_PortForwardState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PortForwardState&&(identical(other.serverId, serverId) || other.serverId == serverId)&&const DeepCollectionEquality().equals(other._configs, _configs)&&const DeepCollectionEquality().equals(other._activeForwards, _activeForwards));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,serverId,const DeepCollectionEquality().hash(_configs),const DeepCollectionEquality().hash(_activeForwards));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PortForwardState(serverId: $serverId, configs: $configs, activeForwards: $activeForwards)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PortForwardStateCopyWith<$Res> implements $PortForwardStateCopyWith<$Res> {
|
||||
factory _$PortForwardStateCopyWith(_PortForwardState value, $Res Function(_PortForwardState) _then) = __$PortForwardStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String serverId, List<PortForwardConfig> configs, Map<String, PortForwardStatus> activeForwards
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PortForwardStateCopyWithImpl<$Res>
|
||||
implements _$PortForwardStateCopyWith<$Res> {
|
||||
__$PortForwardStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PortForwardState _self;
|
||||
final $Res Function(_PortForwardState) _then;
|
||||
|
||||
/// Create a copy of PortForwardState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? serverId = null,Object? configs = null,Object? activeForwards = null,}) {
|
||||
return _then(_PortForwardState(
|
||||
serverId: null == serverId ? _self.serverId : serverId // ignore: cast_nullable_to_non_nullable
|
||||
as String,configs: null == configs ? _self._configs : configs // ignore: cast_nullable_to_non_nullable
|
||||
as List<PortForwardConfig>,activeForwards: null == activeForwards ? _self._activeForwards : activeForwards // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, PortForwardStatus>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
31
lib/data/model/server/port_forward.g.dart
Normal file
31
lib/data/model/server/port_forward.g.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'port_forward.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_PortForwardConfig _$PortForwardConfigFromJson(Map<String, dynamic> json) =>
|
||||
_PortForwardConfig(
|
||||
id: json['id'] as String,
|
||||
serverId: json['serverId'] as String,
|
||||
name: json['name'] as String,
|
||||
localHost: json['localHost'] as String? ?? 'localhost',
|
||||
localPort: (json['localPort'] as num).toInt(),
|
||||
remoteHost: json['remoteHost'] as String,
|
||||
remotePort: (json['remotePort'] as num).toInt(),
|
||||
description: json['description'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PortForwardConfigToJson(_PortForwardConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'serverId': instance.serverId,
|
||||
'name': instance.name,
|
||||
'localHost': instance.localHost,
|
||||
'localPort': instance.localPort,
|
||||
'remoteHost': instance.remoteHost,
|
||||
'remotePort': instance.remotePort,
|
||||
'description': instance.description,
|
||||
};
|
||||
197
lib/data/provider/port_forward_provider.dart
Normal file
197
lib/data/provider/port_forward_provider.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/data/model/server/port_forward.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
part 'port_forward_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class PortForwardNotifier extends _$PortForwardNotifier {
|
||||
final Map<String, _LocalForwardEntry> _forwards = {};
|
||||
final Set<String> _inFlight = {};
|
||||
|
||||
@override
|
||||
PortForwardState build(String serverId) {
|
||||
ref.onDispose(() => dispose());
|
||||
ref.listen(serverProvider(serverId), (prev, next) {
|
||||
if (next.client == null && prev?.client != null) {
|
||||
for (final entry in _forwards.values) {
|
||||
entry.close().catchError((_) {});
|
||||
}
|
||||
_forwards.clear();
|
||||
state = state.copyWith(activeForwards: {});
|
||||
}
|
||||
});
|
||||
final configs = Stores.portForward.fetch(serverId);
|
||||
return PortForwardState(serverId: serverId, configs: configs);
|
||||
}
|
||||
|
||||
String get _serverId => state.serverId;
|
||||
|
||||
SSHClient get _client {
|
||||
final serverState = ref.read(serverProvider(_serverId));
|
||||
final client = serverState.client;
|
||||
if (client == null) {
|
||||
throw StateError('SSH client is not connected');
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final entry in _forwards.values) {
|
||||
entry.close().catchError((_) {});
|
||||
}
|
||||
_forwards.clear();
|
||||
}
|
||||
|
||||
Future<void> addConfig(PortForwardConfig config) async {
|
||||
final configWithServerId = config.copyWith(serverId: _serverId);
|
||||
Stores.portForward.put(configWithServerId);
|
||||
final configs = [...state.configs, configWithServerId];
|
||||
state = state.copyWith(configs: configs);
|
||||
}
|
||||
|
||||
Future<void> updateConfig(PortForwardConfig oldConfig, PortForwardConfig newConfig) async {
|
||||
await stopForward(oldConfig.id);
|
||||
final configWithServerId = newConfig.copyWith(serverId: _serverId);
|
||||
Stores.portForward.update(oldConfig, configWithServerId);
|
||||
final configs = state.configs.map((c) => c.id == oldConfig.id ? configWithServerId : c).toList();
|
||||
state = state.copyWith(configs: configs);
|
||||
}
|
||||
|
||||
Future<void> removeConfig(String id) async {
|
||||
await stopForward(id);
|
||||
final config = state.configs.firstWhereOrNull((c) => c.id == id);
|
||||
if (config != null) {
|
||||
Stores.portForward.delete(config);
|
||||
}
|
||||
final configs = state.configs.where((c) => c.id != id).toList();
|
||||
final activeForwards = Map<String, PortForwardStatus>.from(state.activeForwards)..remove(id);
|
||||
state = state.copyWith(configs: configs, activeForwards: activeForwards);
|
||||
}
|
||||
|
||||
Future<void> startForward(String id) async {
|
||||
if (!_inFlight.add(id)) return;
|
||||
try {
|
||||
final config = state.configs.firstWhereOrNull((c) => c.id == id);
|
||||
if (config == null) {
|
||||
Loggers.app.warning('Port forward config not found: $id');
|
||||
return;
|
||||
}
|
||||
|
||||
final existing = _forwards[id];
|
||||
if (existing != null) {
|
||||
await existing.close().catchError((_) {});
|
||||
_forwards.remove(id);
|
||||
}
|
||||
|
||||
try {
|
||||
final serverSocket = await ServerSocket.bind(config.localHost, config.localPort);
|
||||
|
||||
Loggers.app.info('Port forward started: ${config.localHost}:${config.localPort} -> ${config.remoteHost}:${config.remotePort}');
|
||||
|
||||
final entry = _LocalForwardEntry(serverSocket: serverSocket);
|
||||
entry.start(config.remoteHost, config.remotePort, () => _client);
|
||||
_forwards[id] = entry;
|
||||
|
||||
_updateStatus(id, PortForwardStatus(id: id, isActive: true));
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Port forward failed to start: $e');
|
||||
_updateStatus(id, PortForwardStatus(id: id, isActive: false, error: e.toString()));
|
||||
}
|
||||
} finally {
|
||||
_inFlight.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopForward(String id) async {
|
||||
if (!_inFlight.add(id)) return;
|
||||
try {
|
||||
final entry = _forwards[id];
|
||||
if (entry != null) {
|
||||
await entry.close().catchError((_) {});
|
||||
_forwards.remove(id);
|
||||
Loggers.app.info('Port forward stopped: $id');
|
||||
}
|
||||
_updateStatus(id, PortForwardStatus(id: id, isActive: false));
|
||||
} finally {
|
||||
_inFlight.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleForward(String id) async {
|
||||
final isActive = state.activeForwards[id]?.isActive ?? false;
|
||||
if (isActive) {
|
||||
await stopForward(id);
|
||||
} else {
|
||||
await startForward(id);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateStatus(String id, PortForwardStatus status) {
|
||||
final activeForwards = Map<String, PortForwardStatus>.from(state.activeForwards);
|
||||
activeForwards[id] = status;
|
||||
state = state.copyWith(activeForwards: activeForwards);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalForwardEntry {
|
||||
final ServerSocket serverSocket;
|
||||
final List<_ActiveConnection> _connections = [];
|
||||
StreamSubscription<Socket>? _subscription;
|
||||
|
||||
_LocalForwardEntry({required this.serverSocket});
|
||||
|
||||
void start(String remoteHost, int remotePort, SSHClient Function() clientGetter) {
|
||||
_subscription = serverSocket.listen((socket) async {
|
||||
try {
|
||||
final forward = await clientGetter().forwardLocal(remoteHost, remotePort);
|
||||
final conn = _ActiveConnection(socket: socket, forward: forward);
|
||||
_connections.add(conn);
|
||||
final pipe1 = forward.stream.cast<List<int>>().pipe(socket).catchError((_) {});
|
||||
final pipe2 = socket.cast<List<int>>().pipe(forward.sink).catchError((_) {});
|
||||
Future.wait([pipe1, pipe2]).whenComplete(() {
|
||||
_connections.remove(conn);
|
||||
conn.close();
|
||||
});
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Port forward connection failed', e, s);
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
await _subscription?.cancel();
|
||||
await serverSocket.close();
|
||||
final connections = _connections.toList();
|
||||
for (final conn in connections) {
|
||||
await conn.close();
|
||||
}
|
||||
_connections.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _ActiveConnection {
|
||||
final Socket socket;
|
||||
final SSHForwardChannel forward;
|
||||
|
||||
_ActiveConnection({
|
||||
required this.socket,
|
||||
required this.forward,
|
||||
});
|
||||
|
||||
Future<void> close() async {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await forward.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
109
lib/data/provider/port_forward_provider.g.dart
Normal file
109
lib/data/provider/port_forward_provider.g.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'port_forward_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PortForwardNotifier)
|
||||
const portForwardProvider = PortForwardNotifierFamily._();
|
||||
|
||||
final class PortForwardNotifierProvider
|
||||
extends $NotifierProvider<PortForwardNotifier, PortForwardState> {
|
||||
const PortForwardNotifierProvider._({
|
||||
required PortForwardNotifierFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'portForwardProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$portForwardNotifierHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'portForwardProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PortForwardNotifier create() => PortForwardNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PortForwardState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PortForwardState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PortForwardNotifierProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$portForwardNotifierHash() =>
|
||||
r'e9a93e4e4ee526d334eaaba0e3e0093de7a337fd';
|
||||
|
||||
final class PortForwardNotifierFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
PortForwardNotifier,
|
||||
PortForwardState,
|
||||
PortForwardState,
|
||||
PortForwardState,
|
||||
String
|
||||
> {
|
||||
const PortForwardNotifierFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'portForwardProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
PortForwardNotifierProvider call(String serverId) =>
|
||||
PortForwardNotifierProvider._(argument: serverId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'portForwardProvider';
|
||||
}
|
||||
|
||||
abstract class _$PortForwardNotifier extends $Notifier<PortForwardState> {
|
||||
late final _$args = ref.$arg as String;
|
||||
String get serverId => _$args;
|
||||
|
||||
PortForwardState build(String serverId);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<PortForwardState, PortForwardState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<PortForwardState, PortForwardState>,
|
||||
PortForwardState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ final class PveNotifierProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$pveNotifierHash() => r'a66699f64eae680064a1904f475d0a241d6cb3f8';
|
||||
String _$pveNotifierHash() => r'1f80a27896013a275e5222f19e5ee3c3a68e2f84';
|
||||
|
||||
final class PveNotifierFamily extends $Family
|
||||
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
|
||||
|
||||
@@ -152,6 +152,8 @@ abstract final class GithubIds {
|
||||
'kuvaldini',
|
||||
'aliferne',
|
||||
'canronglan',
|
||||
'nickgirga',
|
||||
'xxnuo'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:get_it/get_it.dart';
|
||||
import 'package:server_box/data/store/connection_stats.dart';
|
||||
import 'package:server_box/data/store/container.dart';
|
||||
import 'package:server_box/data/store/history.dart';
|
||||
import 'package:server_box/data/store/port_forward.dart';
|
||||
import 'package:server_box/data/store/private_key.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
import 'package:server_box/data/store/setting.dart';
|
||||
@@ -19,6 +20,7 @@ abstract final class Stores {
|
||||
static HistoryStore get history => getIt<HistoryStore>();
|
||||
// Keep the legacy box registered so existing connection stats DB files remain intact.
|
||||
static ConnectionStatsStore get connectionStats => getIt<ConnectionStatsStore>();
|
||||
static PortForwardStore get portForward => getIt<PortForwardStore>();
|
||||
|
||||
/// All stores that need backup
|
||||
static List<HiveStore> get _allBackup => [
|
||||
@@ -29,6 +31,7 @@ abstract final class Stores {
|
||||
snippet,
|
||||
history,
|
||||
connectionStats,
|
||||
portForward,
|
||||
];
|
||||
|
||||
static Future<void> init() async {
|
||||
@@ -39,6 +42,7 @@ abstract final class Stores {
|
||||
getIt.registerLazySingleton<SnippetStore>(() => SnippetStore.instance);
|
||||
getIt.registerLazySingleton<HistoryStore>(() => HistoryStore.instance);
|
||||
getIt.registerLazySingleton<ConnectionStatsStore>(() => ConnectionStatsStore.instance);
|
||||
getIt.registerLazySingleton<PortForwardStore>(() => PortForwardStore.instance);
|
||||
|
||||
await Future.wait(_allBackup.map((store) => store.init()));
|
||||
}
|
||||
|
||||
67
lib/data/store/port_forward.dart
Normal file
67
lib/data/store/port_forward.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:server_box/data/model/server/port_forward.dart';
|
||||
|
||||
class PortForwardStore extends HiveStore {
|
||||
PortForwardStore._() : super('port_forward');
|
||||
|
||||
static final instance = PortForwardStore._();
|
||||
|
||||
void put(PortForwardConfig config) {
|
||||
set(config.id, config);
|
||||
}
|
||||
|
||||
List<PortForwardConfig> fetch(String serverId) {
|
||||
final configs = <PortForwardConfig>[];
|
||||
for (final key in keys()) {
|
||||
final config = get<PortForwardConfig>(
|
||||
key,
|
||||
fromObj: (val) {
|
||||
if (val is PortForwardConfig) return val;
|
||||
if (val is Map<dynamic, dynamic>) {
|
||||
final map = val.toStrDynMap;
|
||||
if (map == null) return null;
|
||||
try {
|
||||
final config = PortForwardConfig.fromJson(map as Map<String, dynamic>);
|
||||
put(config);
|
||||
return config;
|
||||
} catch (e) {
|
||||
dprint('Parsing PortForwardConfig from JSON', e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
if (config != null && config.serverId == serverId) {
|
||||
configs.add(config);
|
||||
}
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
void delete(PortForwardConfig config) {
|
||||
remove(config.id);
|
||||
}
|
||||
|
||||
void deleteByServer(String serverId) {
|
||||
final keysToDelete = <dynamic>[];
|
||||
for (final key in keys()) {
|
||||
final config = get<PortForwardConfig>(key);
|
||||
if (config?.serverId == serverId) {
|
||||
keysToDelete.add(key);
|
||||
}
|
||||
}
|
||||
for (final key in keysToDelete) {
|
||||
remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
void update(PortForwardConfig old, PortForwardConfig newConfig) {
|
||||
if (!have(old)) {
|
||||
throw Exception('Old config: $old not found');
|
||||
}
|
||||
delete(old);
|
||||
put(newConfig);
|
||||
}
|
||||
|
||||
bool have(PortForwardConfig config) => get(config.id) != null;
|
||||
}
|
||||
@@ -1643,6 +1643,48 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Podman Docker emulation detected. Please switch to Podman in settings.'**
|
||||
String get podmanDockerEmulationDetected;
|
||||
|
||||
/// No description provided for @portForwardBeta.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This feature is still in beta testing. Functionality is not guaranteed.'**
|
||||
String get portForwardBeta;
|
||||
|
||||
/// No description provided for @portForward_startPrompt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a port forward rule to get started'**
|
||||
String get portForward_startPrompt;
|
||||
|
||||
/// No description provided for @portForward_localHost.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Local Host'**
|
||||
String get portForward_localHost;
|
||||
|
||||
/// No description provided for @portForward_localPort.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Local Port'**
|
||||
String get portForward_localPort;
|
||||
|
||||
/// No description provided for @portForward_remoteHost.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remote Host'**
|
||||
String get portForward_remoteHost;
|
||||
|
||||
/// No description provided for @portForward_remotePort.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remote Port'**
|
||||
String get portForward_remotePort;
|
||||
|
||||
/// No description provided for @portForward_deleteConfirmFmt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete {name}?'**
|
||||
String portForward_deleteConfirmFmt(Object name);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -891,4 +891,29 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,4 +882,29 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker emulation detected. Please switch to Podman in settings.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,4 +893,29 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,4 +896,29 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,4 +882,29 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,4 +888,29 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Rilevata emulazione Docker Podman. Passa a Podman nelle impostazioni.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,4 +852,29 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,4 +851,29 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker 에뮬레이션이 감지되었습니다. 설정에서 Podman으로 전환해 주세요.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -889,4 +889,29 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,4 +884,29 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,4 +888,29 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,4 +883,29 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman\'a geçin.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,4 +888,29 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях.';
|
||||
|
||||
@override
|
||||
String get portForwardBeta =>
|
||||
'This feature is still in beta testing. Functionality is not guaranteed.';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt =>
|
||||
'Add a port forward rule to get started';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => 'Local Host';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => 'Local Port';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => 'Remote Host';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => 'Remote Port';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return 'Delete $name?';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,6 +832,29 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get podmanDockerEmulationDetected =>
|
||||
'检测到 Podman Docker 仿真。请在设置中切换到 Podman。';
|
||||
|
||||
@override
|
||||
String get portForwardBeta => '此功能仍在测试阶段,不保证功能可用性。';
|
||||
|
||||
@override
|
||||
String get portForward_startPrompt => '添加端口映射规则以开始使用';
|
||||
|
||||
@override
|
||||
String get portForward_localHost => '本地主机';
|
||||
|
||||
@override
|
||||
String get portForward_localPort => '本地端口';
|
||||
|
||||
@override
|
||||
String get portForward_remoteHost => '远端主机';
|
||||
|
||||
@override
|
||||
String get portForward_remotePort => '远端端口';
|
||||
|
||||
@override
|
||||
String portForward_deleteConfirmFmt(Object name) {
|
||||
return '删除 $name?';
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:hive_ce/hive.dart';
|
||||
import 'package:server_box/data/model/app/menu/server_func.dart';
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/port_forward.dart';
|
||||
import 'package:server_box/data/model/server/private_key_info.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/model/server/snippet.dart';
|
||||
@@ -19,5 +20,6 @@ import 'package:server_box/data/model/ssh/virtual_key.dart';
|
||||
AdapterSpec<ServerCustom>(),
|
||||
AdapterSpec<WakeOnLanCfg>(),
|
||||
AdapterSpec<SystemType>(),
|
||||
AdapterSpec<PortForwardConfig>(),
|
||||
])
|
||||
part 'hive_adapters.g.dart';
|
||||
|
||||
@@ -436,6 +436,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
||||
return ServerFuncBtn.iperf;
|
||||
case 8:
|
||||
return ServerFuncBtn.systemd;
|
||||
case 9:
|
||||
return ServerFuncBtn.portForward;
|
||||
default:
|
||||
return ServerFuncBtn.terminal;
|
||||
}
|
||||
@@ -458,6 +460,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
|
||||
writer.writeByte(6);
|
||||
case ServerFuncBtn.systemd:
|
||||
writer.writeByte(8);
|
||||
case ServerFuncBtn.portForward:
|
||||
writer.writeByte(9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,3 +611,58 @@ class SystemTypeAdapter extends TypeAdapter<SystemType> {
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class PortForwardConfigAdapter extends TypeAdapter<PortForwardConfig> {
|
||||
@override
|
||||
final typeId = 10;
|
||||
|
||||
@override
|
||||
PortForwardConfig read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return PortForwardConfig(
|
||||
id: fields[0] as String,
|
||||
serverId: fields[7] as String,
|
||||
name: fields[1] as String,
|
||||
localHost: fields[2] == null ? 'localhost' : fields[2] as String,
|
||||
localPort: (fields[3] as num).toInt(),
|
||||
remoteHost: fields[4] as String,
|
||||
remotePort: (fields[5] as num).toInt(),
|
||||
description: fields[6] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PortForwardConfig obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.localHost)
|
||||
..writeByte(3)
|
||||
..write(obj.localPort)
|
||||
..writeByte(4)
|
||||
..write(obj.remoteHost)
|
||||
..writeByte(5)
|
||||
..write(obj.remotePort)
|
||||
..writeByte(6)
|
||||
..write(obj.description)
|
||||
..writeByte(7)
|
||||
..write(obj.serverId);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PortForwardConfigAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Hive CE
|
||||
# Manual modifications may be necessary for certain migrations
|
||||
# Check in to version control
|
||||
nextTypeId: 10
|
||||
nextTypeId: 11
|
||||
types:
|
||||
PrivateKeyInfo:
|
||||
typeId: 1
|
||||
@@ -167,7 +167,7 @@ types:
|
||||
index: 2
|
||||
ServerFuncBtn:
|
||||
typeId: 6
|
||||
nextIndex: 9
|
||||
nextIndex: 10
|
||||
fields:
|
||||
terminal:
|
||||
index: 0
|
||||
@@ -183,6 +183,8 @@ types:
|
||||
index: 6
|
||||
systemd:
|
||||
index: 8
|
||||
portForward:
|
||||
index: 9
|
||||
ServerCustom:
|
||||
typeId: 7
|
||||
nextIndex: 9
|
||||
@@ -223,3 +225,23 @@ types:
|
||||
index: 1
|
||||
windows:
|
||||
index: 2
|
||||
PortForwardConfig:
|
||||
typeId: 10
|
||||
nextIndex: 8
|
||||
fields:
|
||||
id:
|
||||
index: 0
|
||||
name:
|
||||
index: 1
|
||||
localHost:
|
||||
index: 2
|
||||
localPort:
|
||||
index: 3
|
||||
remoteHost:
|
||||
index: 4
|
||||
remotePort:
|
||||
index: 5
|
||||
description:
|
||||
index: 6
|
||||
serverId:
|
||||
index: 7
|
||||
|
||||
@@ -13,6 +13,7 @@ extension HiveRegistrar on HiveInterface {
|
||||
registerAdapter(ConnectionResultAdapter());
|
||||
registerAdapter(ConnectionStatAdapter());
|
||||
registerAdapter(NetViewTypeAdapter());
|
||||
registerAdapter(PortForwardConfigAdapter());
|
||||
registerAdapter(PrivateKeyInfoAdapter());
|
||||
registerAdapter(ServerConnectionStatsAdapter());
|
||||
registerAdapter(ServerCustomAdapter());
|
||||
@@ -31,6 +32,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
registerAdapter(ConnectionResultAdapter());
|
||||
registerAdapter(ConnectionStatAdapter());
|
||||
registerAdapter(NetViewTypeAdapter());
|
||||
registerAdapter(PortForwardConfigAdapter());
|
||||
registerAdapter(PrivateKeyInfoAdapter());
|
||||
registerAdapter(ServerConnectionStatsAdapter());
|
||||
registerAdapter(ServerCustomAdapter());
|
||||
|
||||
@@ -255,5 +255,12 @@
|
||||
"writeScriptFailTip": "Writing to the script failed, possibly due to lack of permissions or the directory does not exist.",
|
||||
"writeScriptTip": "After connecting to the server, a script will be written to `~/.config/server_box` \n | `/tmp/server_box` to monitor the system status. You can review the script content.",
|
||||
"menuGitHubRepository": "GitHub Repository",
|
||||
"podmanDockerEmulationDetected": "Podman Docker emulation detected. Please switch to Podman in settings."
|
||||
"podmanDockerEmulationDetected": "Podman Docker emulation detected. Please switch to Podman in settings.",
|
||||
"portForwardBeta": "This feature is still in beta testing. Functionality is not guaranteed.",
|
||||
"portForward_startPrompt": "Add a port forward rule to get started",
|
||||
"portForward_localHost": "Local Host",
|
||||
"portForward_localPort": "Local Port",
|
||||
"portForward_remoteHost": "Remote Host",
|
||||
"portForward_remotePort": "Remote Port",
|
||||
"portForward_deleteConfirmFmt": "Delete {name}?"
|
||||
}
|
||||
|
||||
@@ -252,5 +252,12 @@
|
||||
"writeScriptFailTip": "写入脚本失败,可能是没有权限/目录不存在等",
|
||||
"writeScriptTip": "在连接服务器后,会向 `~/.config/server_box` \n | `/tmp/server_box` 写入脚本来监测系统状态,你可以审查脚本内容。",
|
||||
"menuGitHubRepository": "GitHub 仓库",
|
||||
"podmanDockerEmulationDetected": "检测到 Podman Docker 仿真。请在设置中切换到 Podman。"
|
||||
"podmanDockerEmulationDetected": "检测到 Podman Docker 仿真。请在设置中切换到 Podman。",
|
||||
"portForwardBeta": "此功能仍在测试阶段,不保证功能可用性。",
|
||||
"portForward_startPrompt": "添加端口映射规则以开始使用",
|
||||
"portForward_localHost": "本地主机",
|
||||
"portForward_localPort": "本地端口",
|
||||
"portForward_remoteHost": "远端主机",
|
||||
"portForward_remotePort": "远端端口",
|
||||
"portForward_deleteConfirmFmt": "删除 {name}?"
|
||||
}
|
||||
|
||||
321
lib/view/page/port_forward.dart
Normal file
321
lib/view/page/port_forward.dart
Normal file
@@ -0,0 +1,321 @@
|
||||
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/server/port_forward.dart';
|
||||
import 'package:server_box/data/provider/port_forward_provider.dart';
|
||||
|
||||
final class PortForwardPage extends ConsumerStatefulWidget {
|
||||
final SpiRequiredArgs args;
|
||||
|
||||
const PortForwardPage({super.key, required this.args});
|
||||
|
||||
static const route = AppRouteArg<void, SpiRequiredArgs>(page: PortForwardPage.new, path: '/port_forward');
|
||||
|
||||
@override
|
||||
ConsumerState<PortForwardPage> createState() => _PortForwardPageState();
|
||||
}
|
||||
|
||||
final class _PortForwardPageState extends ConsumerState<PortForwardPage> {
|
||||
late final PortForwardNotifier _notifier;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_notifier = ref.read(portForwardProvider(widget.args.spi.id).notifier);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_showBetaWarning();
|
||||
});
|
||||
}
|
||||
|
||||
void _showBetaWarning() {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(context.l10n.portForwardBeta),
|
||||
actions: [Btnx.ok],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: Text(libL10n.portForward),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: _onAdd,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
final state = ref.watch(portForwardProvider(widget.args.spi.id));
|
||||
final configs = state.configs;
|
||||
|
||||
if (configs.isEmpty) {
|
||||
return _buildEmpty();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: configs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final config = configs[index];
|
||||
final status = state.activeForwards[config.id];
|
||||
return _buildConfigTile(config, status);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmpty() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.compare_arrows, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
Text(libL10n.empty, style: UIs.textGrey),
|
||||
const SizedBox(height: 8),
|
||||
Text(context.l10n.portForward_startPrompt, style: UIs.text13Grey),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConfigTile(PortForwardConfig config, PortForwardStatus? status) {
|
||||
final isActive = status?.isActive ?? false;
|
||||
final hasError = status?.error != null;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isActive ? Icons.link : Icons.link_off,
|
||||
color: isActive ? colorScheme.primary : (hasError ? colorScheme.error : colorScheme.onSurfaceVariant),
|
||||
),
|
||||
title: Text(config.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(config.displayAddr, style: UIs.text13Grey),
|
||||
if (hasError) Text(status!.error!, style: TextStyle(color: colorScheme.error, fontSize: 12)),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Switch(
|
||||
value: isActive,
|
||||
onChanged: (_) => _notifier.toggleForward(config.id),
|
||||
),
|
||||
PopupMenu(
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.edit, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(libL10n.edit),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.delete, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(libL10n.delete),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (val) {
|
||||
if (val == 'edit') {
|
||||
_onEdit(config);
|
||||
} else if (val == 'delete') {
|
||||
_onDelete(config);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
isThreeLine: hasError,
|
||||
).cardx.paddingSymmetric(horizontal: 13, vertical: 4);
|
||||
}
|
||||
|
||||
void _onAdd() {
|
||||
_showConfigDialog(null);
|
||||
}
|
||||
|
||||
void _onEdit(PortForwardConfig config) {
|
||||
_showConfigDialog(config);
|
||||
}
|
||||
|
||||
void _onDelete(PortForwardConfig config) async {
|
||||
final sure = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
child: Text(context.l10n.portForward_deleteConfirmFmt(config.name)),
|
||||
actions: Btnx.cancelOk,
|
||||
);
|
||||
if (sure == true) {
|
||||
await _notifier.removeConfig(config.id);
|
||||
}
|
||||
}
|
||||
|
||||
void _showConfigDialog(PortForwardConfig? existing) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _PortForwardConfigDialog(
|
||||
existing: existing,
|
||||
serverId: widget.args.spi.id,
|
||||
onSave: (config) async {
|
||||
if (existing == null) {
|
||||
await _notifier.addConfig(config);
|
||||
} else {
|
||||
final wasActive = ref.read(portForwardProvider(widget.args.spi.id)).activeForwards[existing.id]?.isActive ?? false;
|
||||
await _notifier.updateConfig(existing, config);
|
||||
if (wasActive) {
|
||||
await _notifier.startForward(config.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PortForwardConfigDialog extends StatefulWidget {
|
||||
final PortForwardConfig? existing;
|
||||
final String serverId;
|
||||
final Future<void> Function(PortForwardConfig config) onSave;
|
||||
|
||||
const _PortForwardConfigDialog({
|
||||
required this.existing,
|
||||
required this.serverId,
|
||||
required this.onSave,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_PortForwardConfigDialog> createState() => _PortForwardConfigDialogState();
|
||||
}
|
||||
|
||||
class _PortForwardConfigDialogState extends State<_PortForwardConfigDialog> {
|
||||
late final TextEditingController nameController;
|
||||
late final TextEditingController localHostController;
|
||||
late final TextEditingController localPortController;
|
||||
late final TextEditingController remoteHostController;
|
||||
late final TextEditingController remotePortController;
|
||||
late final TextEditingController descController;
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
nameController = TextEditingController(text: widget.existing?.name ?? '');
|
||||
localHostController = TextEditingController(text: widget.existing?.localHost ?? 'localhost');
|
||||
localPortController = TextEditingController(text: widget.existing?.localPort.toString() ?? '');
|
||||
remoteHostController = TextEditingController(text: widget.existing?.remoteHost ?? '');
|
||||
remotePortController = TextEditingController(text: widget.existing?.remotePort.toString() ?? '');
|
||||
descController = TextEditingController(text: widget.existing?.description ?? '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
localHostController.dispose();
|
||||
localPortController.dispose();
|
||||
remoteHostController.dispose();
|
||||
remotePortController.dispose();
|
||||
descController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(widget.existing == null ? libL10n.add : libL10n.edit),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Input(controller: nameController, hint: libL10n.name),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Input(controller: localHostController, hint: context.l10n.portForward_localHost)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Input(controller: localPortController, hint: context.l10n.portForward_localPort, type: TextInputType.number)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Input(controller: remoteHostController, hint: context.l10n.portForward_remoteHost)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Input(controller: remotePortController, hint: context.l10n.portForward_remotePort, type: TextInputType.number)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Input(controller: descController, hint: libL10n.note),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Btn.cancel(),
|
||||
Btn.ok(
|
||||
onTap: () async {
|
||||
if (_saving) return;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final name = nameController.text.trim();
|
||||
final localHost = localHostController.text.trim();
|
||||
final localPort = int.tryParse(localPortController.text.trim()) ?? 0;
|
||||
final remoteHost = remoteHostController.text.trim();
|
||||
final remotePort = int.tryParse(remotePortController.text.trim()) ?? 0;
|
||||
final desc = descController.text.trim();
|
||||
|
||||
if (name.isEmpty ||
|
||||
localHost.isEmpty ||
|
||||
localPort <= 0 ||
|
||||
localPort > 65535 ||
|
||||
remoteHost.isEmpty ||
|
||||
remotePort <= 0 ||
|
||||
remotePort > 65535) {
|
||||
if (mounted) context.showSnackBar(libL10n.invalid);
|
||||
return;
|
||||
}
|
||||
|
||||
final config = PortForwardConfig(
|
||||
id: widget.existing?.id ?? ShortId.generate(),
|
||||
serverId: widget.serverId,
|
||||
name: name,
|
||||
localHost: localHost,
|
||||
localPort: localPort,
|
||||
remoteHost: remoteHost,
|
||||
remotePort: remotePort,
|
||||
description: desc.isEmpty ? null : desc,
|
||||
);
|
||||
|
||||
await widget.onSave(config);
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Failed to save port forward config', e, s);
|
||||
if (mounted) context.showSnackBar(libL10n.error);
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import 'package:server_box/data/provider/snippet.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/container/container.dart';
|
||||
import 'package:server_box/view/page/iperf.dart';
|
||||
import 'package:server_box/view/page/port_forward.dart';
|
||||
import 'package:server_box/view/page/process.dart';
|
||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||
import 'package:server_box/view/page/storage/sftp.dart';
|
||||
@@ -206,6 +207,11 @@ void _onTapMoreBtns(ServerFuncBtn value, Spi spi, BuildContext context, WidgetRe
|
||||
// );
|
||||
// }
|
||||
break;
|
||||
case ServerFuncBtn.portForward:
|
||||
if (!_checkClient(context, spi.id, ref)) return;
|
||||
final args = SpiRequiredArgs(spi);
|
||||
PortForwardPage.route.go(context, args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <dynamic_color/dynamic_color_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
@@ -19,6 +20,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
||||
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_color
|
||||
flutter_secure_storage_linux
|
||||
gtk
|
||||
screen_retriever_linux
|
||||
url_launcher_linux
|
||||
window_manager
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import app_links
|
||||
import dynamic_color
|
||||
import file_picker
|
||||
import flutter_secure_storage_macos
|
||||
@@ -19,6 +20,7 @@ import wakelock_plus
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
|
||||
Submodule packages/fl_lib updated: 61d62d23a8...bbadd10e12
496
pubspec.lock
496
pubspec.lock
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <local_auth_windows/local_auth_plugin.h>
|
||||
@@ -15,6 +16,8 @@
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
DynamicColorPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
dynamic_color
|
||||
flutter_secure_storage_windows
|
||||
local_auth_windows
|
||||
|
||||
Reference in New Issue
Block a user