From d6e17ff58c5dbf6234b4814563a6a06f61a6f558 Mon Sep 17 00:00:00 2001 From: GT610 <79314033+GT-610@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:38:58 +0800 Subject: [PATCH] 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 --- lib/data/model/app/menu/server_func.dart | 24 +- lib/data/model/server/port_forward.dart | 59 ++ .../model/server/port_forward.freezed.dart | 573 ++++++++++++++++++ lib/data/model/server/port_forward.g.dart | 31 + lib/data/provider/port_forward_provider.dart | 197 ++++++ .../provider/port_forward_provider.g.dart | 109 ++++ lib/data/provider/pve.g.dart | 2 +- lib/data/res/github_id.dart | 2 + lib/data/res/store.dart | 4 + lib/data/store/port_forward.dart | 67 ++ lib/generated/l10n/l10n.dart | 42 ++ lib/generated/l10n/l10n_de.dart | 25 + lib/generated/l10n/l10n_en.dart | 25 + lib/generated/l10n/l10n_es.dart | 25 + lib/generated/l10n/l10n_fr.dart | 25 + lib/generated/l10n/l10n_id.dart | 25 + lib/generated/l10n/l10n_it.dart | 25 + lib/generated/l10n/l10n_ja.dart | 25 + lib/generated/l10n/l10n_ko.dart | 25 + lib/generated/l10n/l10n_nl.dart | 25 + lib/generated/l10n/l10n_pt.dart | 25 + lib/generated/l10n/l10n_ru.dart | 25 + lib/generated/l10n/l10n_tr.dart | 25 + lib/generated/l10n/l10n_uk.dart | 25 + lib/generated/l10n/l10n_zh.dart | 23 + lib/hive/hive_adapters.dart | 2 + lib/hive/hive_adapters.g.dart | 59 ++ lib/hive/hive_adapters.g.yaml | 26 +- lib/hive/hive_registrar.g.dart | 2 + lib/l10n/app_en.arb | 9 +- lib/l10n/app_zh.arb | 9 +- lib/view/page/port_forward.dart | 321 ++++++++++ lib/view/widget/server_func_btns.dart | 6 + linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + packages/fl_lib | 2 +- pubspec.lock | 496 ++++++++------- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 40 files changed, 2162 insertions(+), 239 deletions(-) create mode 100644 lib/data/model/server/port_forward.dart create mode 100644 lib/data/model/server/port_forward.freezed.dart create mode 100644 lib/data/model/server/port_forward.g.dart create mode 100644 lib/data/provider/port_forward_provider.dart create mode 100644 lib/data/provider/port_forward_provider.g.dart create mode 100644 lib/data/store/port_forward.dart create mode 100644 lib/view/page/port_forward.dart diff --git a/lib/data/model/app/menu/server_func.dart b/lib/data/model/app/menu/server_func.dart index 046dcc9b..4ccff487 100644 --- a/lib/data/model/app/menu/server_func.dart +++ b/lib/data/model/app/menu/server_func.dart @@ -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 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, }; } diff --git a/lib/data/model/server/port_forward.dart b/lib/data/model/server/port_forward.dart new file mode 100644 index 00000000..79b2860c --- /dev/null +++ b/lib/data/model/server/port_forward.dart @@ -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 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 configs, + @Default({}) Map 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(); diff --git a/lib/data/model/server/port_forward.freezed.dart b/lib/data/model/server/port_forward.freezed.dart new file mode 100644 index 00000000..567c5742 --- /dev/null +++ b/lib/data/model/server/port_forward.freezed.dart @@ -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 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 get copyWith => _$PortForwardConfigCopyWithImpl(this as PortForwardConfig, _$identity); + + /// Serializes this PortForwardConfig to a JSON map. + Map 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 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 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? 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 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 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? 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 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 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 get configs; Map 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 get copyWith => _$PortForwardStateCopyWithImpl(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 configs, Map 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,activeForwards: null == activeForwards ? _self.activeForwards : activeForwards // ignore: cast_nullable_to_non_nullable +as Map, + )); +} + +} + + +/// 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 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 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? 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 Function( String serverId, List configs, Map 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 Function( String serverId, List configs, Map 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? Function( String serverId, List configs, Map 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 configs = const [], final Map activeForwards = const {}}): _configs = configs,_activeForwards = activeForwards; + + +@override final String serverId; + final List _configs; +@override@JsonKey() List get configs { + if (_configs is EqualUnmodifiableListView) return _configs; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_configs); +} + + final Map _activeForwards; +@override@JsonKey() Map 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 configs, Map 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,activeForwards: null == activeForwards ? _self._activeForwards : activeForwards // ignore: cast_nullable_to_non_nullable +as Map, + )); +} + + +} + +// dart format on diff --git a/lib/data/model/server/port_forward.g.dart b/lib/data/model/server/port_forward.g.dart new file mode 100644 index 00000000..fe29c834 --- /dev/null +++ b/lib/data/model/server/port_forward.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'port_forward.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_PortForwardConfig _$PortForwardConfigFromJson(Map 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 _$PortForwardConfigToJson(_PortForwardConfig instance) => + { + 'id': instance.id, + 'serverId': instance.serverId, + 'name': instance.name, + 'localHost': instance.localHost, + 'localPort': instance.localPort, + 'remoteHost': instance.remoteHost, + 'remotePort': instance.remotePort, + 'description': instance.description, + }; diff --git a/lib/data/provider/port_forward_provider.dart b/lib/data/provider/port_forward_provider.dart new file mode 100644 index 00000000..a658cfbd --- /dev/null +++ b/lib/data/provider/port_forward_provider.dart @@ -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 _forwards = {}; + final Set _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 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 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 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.from(state.activeForwards)..remove(id); + state = state.copyWith(configs: configs, activeForwards: activeForwards); + } + + Future 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 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 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.from(state.activeForwards); + activeForwards[id] = status; + state = state.copyWith(activeForwards: activeForwards); + } +} + +class _LocalForwardEntry { + final ServerSocket serverSocket; + final List<_ActiveConnection> _connections = []; + StreamSubscription? _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>().pipe(socket).catchError((_) {}); + final pipe2 = socket.cast>().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 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 close() async { + try { + socket.destroy(); + } catch (_) {} + try { + await forward.close(); + } catch (_) {} + } +} diff --git a/lib/data/provider/port_forward_provider.g.dart b/lib/data/provider/port_forward_provider.g.dart new file mode 100644 index 00000000..3ce95687 --- /dev/null +++ b/lib/data/provider/port_forward_provider.g.dart @@ -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 { + 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(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 { + 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; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + PortForwardState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/data/provider/pve.g.dart b/lib/data/provider/pve.g.dart index 476c92d1..201ee95d 100644 --- a/lib/data/provider/pve.g.dart +++ b/lib/data/provider/pve.g.dart @@ -58,7 +58,7 @@ final class PveNotifierProvider } } -String _$pveNotifierHash() => r'a66699f64eae680064a1904f475d0a241d6cb3f8'; +String _$pveNotifierHash() => r'1f80a27896013a275e5222f19e5ee3c3a68e2f84'; final class PveNotifierFamily extends $Family with $ClassFamilyOverride { diff --git a/lib/data/res/github_id.dart b/lib/data/res/github_id.dart index 5ab5ee4e..5edde434 100644 --- a/lib/data/res/github_id.dart +++ b/lib/data/res/github_id.dart @@ -152,6 +152,8 @@ abstract final class GithubIds { 'kuvaldini', 'aliferne', 'canronglan', + 'nickgirga', + 'xxnuo' }; } diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index 8954f92a..da2321a1 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -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(); // Keep the legacy box registered so existing connection stats DB files remain intact. static ConnectionStatsStore get connectionStats => getIt(); + static PortForwardStore get portForward => getIt(); /// All stores that need backup static List get _allBackup => [ @@ -29,6 +31,7 @@ abstract final class Stores { snippet, history, connectionStats, + portForward, ]; static Future init() async { @@ -39,6 +42,7 @@ abstract final class Stores { getIt.registerLazySingleton(() => SnippetStore.instance); getIt.registerLazySingleton(() => HistoryStore.instance); getIt.registerLazySingleton(() => ConnectionStatsStore.instance); + getIt.registerLazySingleton(() => PortForwardStore.instance); await Future.wait(_allBackup.map((store) => store.init())); } diff --git a/lib/data/store/port_forward.dart b/lib/data/store/port_forward.dart new file mode 100644 index 00000000..6a0c64dc --- /dev/null +++ b/lib/data/store/port_forward.dart @@ -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 fetch(String serverId) { + final configs = []; + for (final key in keys()) { + final config = get( + key, + fromObj: (val) { + if (val is PortForwardConfig) return val; + if (val is Map) { + final map = val.toStrDynMap; + if (map == null) return null; + try { + final config = PortForwardConfig.fromJson(map as Map); + 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 = []; + for (final key in keys()) { + final config = get(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; +} diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 136d462f..95dd8168 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -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 diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index 42785d35..dd1e5370 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index a2979309..06386246 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 2bfd80a3..a024193b 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index 2f39be8c..789c4825 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 22bc3978..bfdc42d2 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_it.dart b/lib/generated/l10n/l10n_it.dart index e9c10586..192d90b9 100644 --- a/lib/generated/l10n/l10n_it.dart +++ b/lib/generated/l10n/l10n_it.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index 47a7697d..fd89adc5 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_ko.dart b/lib/generated/l10n/l10n_ko.dart index 25c5376c..bcfbd1de 100644 --- a/lib/generated/l10n/l10n_ko.dart +++ b/lib/generated/l10n/l10n_ko.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 86396c90..d9d0de79 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index d2cbc7cc..1ee540f5 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index cda82788..fdffa64b 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index c8f5f918..7f34487c 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index b53c172e..a4fc2d74 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -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?'; + } } diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index e2758aa4..f1959e49 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -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`). diff --git a/lib/hive/hive_adapters.dart b/lib/hive/hive_adapters.dart index 63ecce7e..b1053c0d 100644 --- a/lib/hive/hive_adapters.dart +++ b/lib/hive/hive_adapters.dart @@ -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(), AdapterSpec(), AdapterSpec(), + AdapterSpec(), ]) part 'hive_adapters.g.dart'; diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart index 630a06f0..f3663333 100644 --- a/lib/hive/hive_adapters.g.dart +++ b/lib/hive/hive_adapters.g.dart @@ -436,6 +436,8 @@ class ServerFuncBtnAdapter extends TypeAdapter { 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 { writer.writeByte(6); case ServerFuncBtn.systemd: writer.writeByte(8); + case ServerFuncBtn.portForward: + writer.writeByte(9); } } @@ -607,3 +611,58 @@ class SystemTypeAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class PortForwardConfigAdapter extends TypeAdapter { + @override + final typeId = 10; + + @override + PortForwardConfig read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index f5439221..e4abd0b5 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -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 diff --git a/lib/hive/hive_registrar.g.dart b/lib/hive/hive_registrar.g.dart index b1da7739..79e37bd0 100644 --- a/lib/hive/hive_registrar.g.dart +++ b/lib/hive/hive_registrar.g.dart @@ -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()); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e58c8ba0..5a8e5a4c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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}?" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 608d9465..ad8705d6 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -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}?" } diff --git a/lib/view/page/port_forward.dart b/lib/view/page/port_forward.dart new file mode 100644 index 00000000..046a8719 --- /dev/null +++ b/lib/view/page/port_forward.dart @@ -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(page: PortForwardPage.new, path: '/port_forward'); + + @override + ConsumerState createState() => _PortForwardPageState(); +} + +final class _PortForwardPageState extends ConsumerState { + 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( + 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 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); + } + }, + ), + ], + ); + } +} diff --git a/lib/view/widget/server_func_btns.dart b/lib/view/widget/server_func_btns.dart index d84158e1..3b33a071 100644 --- a/lib/view/widget/server_func_btns.dart +++ b/lib/view/widget/server_func_btns.dart @@ -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; } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 820c3a7e..adc190d4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index d3790845..32f9bfe9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color flutter_secure_storage_linux + gtk screen_retriever_linux url_launcher_linux window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6abf80cb..1058d345 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/packages/fl_lib b/packages/fl_lib index 61d62d23..bbadd10e 160000 --- a/packages/fl_lib +++ b/packages/fl_lib @@ -1 +1 @@ -Subproject commit 61d62d23a885d8dc1b36bae32d7c4572125e1f92 +Subproject commit bbadd10e1250bd89ba753136726f7da25854618a diff --git a/pubspec.lock b/pubspec.lock index 113284d3..8cecceb7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _fe_analyzer_shared sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "91.0.0" analyzer: @@ -14,7 +14,7 @@ packages: description: name: analyzer sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.4.0" analyzer_buffer: @@ -22,7 +22,7 @@ packages: description: name: analyzer_buffer sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.11" analyzer_plugin: @@ -30,7 +30,7 @@ packages: description: name: analyzer_plugin sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.10" animations: @@ -38,7 +38,7 @@ packages: description: name: animations sha256: "18938cefd7dcc04e1ecac0db78973761a01e4bc2d6bfae0cfa596bfeac9e96ab" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" ansicolor: @@ -46,15 +46,47 @@ packages: description: name: ansicolor sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" + app_links: + dependency: transitive + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" archive: dependency: transitive description: name: archive sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.9" args: @@ -62,7 +94,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" asn1lib: @@ -70,7 +102,7 @@ packages: description: name: asn1lib sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.5" async: @@ -78,7 +110,7 @@ packages: description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.0" boolean_selector: @@ -86,7 +118,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" build: @@ -94,7 +126,7 @@ packages: description: name: build sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.5" build_config: @@ -102,7 +134,7 @@ packages: description: name: build_config sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" build_daemon: @@ -110,7 +142,7 @@ packages: description: name: build_daemon sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" build_runner: @@ -118,7 +150,7 @@ packages: description: name: build_runner sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.1" built_collection: @@ -126,7 +158,7 @@ packages: description: name: built_collection sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: @@ -134,7 +166,7 @@ packages: description: name: built_value sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.12.4" camera: @@ -142,7 +174,7 @@ packages: description: name: camera sha256: "4142a19a38e388d3bab444227636610ba88982e36dff4552d5191a86f65dc437" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.11.4" camera_android_camerax: @@ -150,7 +182,7 @@ packages: description: name: camera_android_camerax sha256: "8516fe308bc341a5067fb1a48edff0ddfa57c0d3cdcc9dbe7ceca3ba119e2577" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.30" camera_avfoundation: @@ -158,7 +190,7 @@ packages: description: name: camera_avfoundation sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.23+2" camera_platform_interface: @@ -166,7 +198,7 @@ packages: description: name: camera_platform_interface sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.12.0" camera_web: @@ -174,7 +206,7 @@ packages: description: name: camera_web sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.5+3" characters: @@ -182,7 +214,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" charset: @@ -190,7 +222,7 @@ packages: description: name: charset sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" checked_yaml: @@ -198,7 +230,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" choice: @@ -206,7 +238,7 @@ packages: description: name: choice sha256: "52d07065e8056beba5b26cff7786134cbfa24927b1f5bf60a05d50058597b2d9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" circle_chart: @@ -221,7 +253,7 @@ packages: description: name: cli_config sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" clock: @@ -229,7 +261,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" code_assets: @@ -237,7 +269,7 @@ packages: description: name: code_assets sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" code_builder: @@ -245,7 +277,7 @@ packages: description: name: code_builder sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.11.1" collection: @@ -253,7 +285,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" computer: @@ -270,7 +302,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.2" coverage: @@ -278,7 +310,7 @@ packages: description: name: coverage sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.15.0" cross_file: @@ -286,7 +318,7 @@ packages: description: name: cross_file sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.5+2" crypto: @@ -294,7 +326,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.7" csslib: @@ -302,7 +334,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" custom_lint_core: @@ -310,7 +342,7 @@ packages: description: name: custom_lint_core sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.1" custom_lint_visitor: @@ -318,7 +350,7 @@ packages: description: name: custom_lint_visitor sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0+8.4.0" dart_style: @@ -326,7 +358,7 @@ packages: description: name: dart_style sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" dartssh2: @@ -341,7 +373,7 @@ packages: description: name: dbus sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.12" dio: @@ -349,7 +381,7 @@ packages: description: name: dio sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.2" dio_web_adapter: @@ -357,7 +389,7 @@ packages: description: name: dio_web_adapter sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" dynamic_color: @@ -365,7 +397,7 @@ packages: description: name: dynamic_color sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" easy_isolate: @@ -373,7 +405,7 @@ packages: description: name: easy_isolate sha256: "5c1dd21d77af0ac82e0ce25ddda652a17b87cee56fb4d8a708e13be0a2adc180" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" equatable: @@ -381,7 +413,7 @@ packages: description: name: equatable sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.8" extended_image: @@ -389,7 +421,7 @@ packages: description: name: extended_image sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.0.1" extended_image_library: @@ -397,7 +429,7 @@ packages: description: name: extended_image_library sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.1" fake_async: @@ -405,7 +437,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" ffi: @@ -413,7 +445,7 @@ packages: description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" file: @@ -421,7 +453,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" file_picker: @@ -429,7 +461,7 @@ packages: description: name: file_picker sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.3.10" fixnum: @@ -437,7 +469,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" fl_build: @@ -452,7 +484,7 @@ packages: description: name: fl_chart sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" fl_lib: @@ -472,7 +504,7 @@ packages: description: name: flutter_displaymode sha256: ecd44b1e902b0073b42ff5b55bf283f38e088270724cdbb7f7065ccf54aa60a8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_gbk2utf8: @@ -480,7 +512,7 @@ packages: description: name: flutter_gbk2utf8 sha256: c17323808d6ae7cfaf7676669e0130c33df6be322eb807cdd32face5824c1134 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_highlight: @@ -488,7 +520,7 @@ packages: description: name: flutter_highlight sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_lints: @@ -496,7 +528,7 @@ packages: description: name: flutter_lints sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.0" flutter_localizations: @@ -509,7 +541,7 @@ packages: description: name: flutter_markdown_plus sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.7" flutter_markdown_plus_latex: @@ -517,7 +549,7 @@ packages: description: name: flutter_markdown_plus_latex sha256: "2e7698b291f0657ca445efab730bb25a8c5851037e882cb7bf47d16a5c218de7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" flutter_math_fork: @@ -525,7 +557,7 @@ packages: description: name: flutter_math_fork sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.4" flutter_native_splash: @@ -533,7 +565,7 @@ packages: description: name: flutter_native_splash sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.7" flutter_plugin_android_lifecycle: @@ -541,7 +573,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.33" flutter_riverpod: @@ -549,7 +581,7 @@ packages: description: name: flutter_riverpod sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" flutter_secure_storage: @@ -557,7 +589,7 @@ packages: description: name: flutter_secure_storage sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.2.4" flutter_secure_storage_linux: @@ -565,7 +597,7 @@ packages: description: name: flutter_secure_storage_linux sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.3" flutter_secure_storage_macos: @@ -573,7 +605,7 @@ packages: description: name: flutter_secure_storage_macos sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" flutter_secure_storage_platform_interface: @@ -581,7 +613,7 @@ packages: description: name: flutter_secure_storage_platform_interface sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" flutter_secure_storage_web: @@ -589,7 +621,7 @@ packages: description: name: flutter_secure_storage_web sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" flutter_secure_storage_windows: @@ -597,7 +629,7 @@ packages: description: name: flutter_secure_storage_windows sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.2" flutter_staggered_grid_view: @@ -605,7 +637,7 @@ packages: description: name: flutter_staggered_grid_view sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_svg: @@ -613,7 +645,7 @@ packages: description: name: flutter_svg sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.4" flutter_test: @@ -631,7 +663,7 @@ packages: description: name: freezed sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.3" freezed_annotation: @@ -639,7 +671,7 @@ packages: description: name: freezed_annotation sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" frontend_server_client: @@ -647,7 +679,7 @@ packages: description: name: frontend_server_client sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" get_it: @@ -655,7 +687,7 @@ packages: description: name: get_it sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.2.1" glob: @@ -663,7 +695,7 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" graphs: @@ -671,15 +703,23 @@ packages: description: name: graphs sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" highlight: dependency: "direct main" description: name: highlight sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" hive_ce: @@ -687,7 +727,7 @@ packages: description: name: hive_ce sha256: "8e9980e68643afb1e765d3af32b47996552a64e190d03faf622cea07c1294418" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.19.3" hive_ce_flutter: @@ -695,7 +735,7 @@ packages: description: name: hive_ce_flutter sha256: "2677e95a333ff15af43ccd06af7eb7abbf1a4f154ea071997f3de4346cae913a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.4" hive_ce_generator: @@ -703,7 +743,7 @@ packages: description: name: hive_ce_generator sha256: b19ac263cb37529513508ba47352c41e6de72ba879952898d9c18c9c8a955921 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" hooks: @@ -711,7 +751,7 @@ packages: description: name: hooks sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" html: @@ -719,7 +759,7 @@ packages: description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.6" http: @@ -727,7 +767,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" http_client_helper: @@ -735,7 +775,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" http_multi_server: @@ -743,7 +783,7 @@ packages: description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" http_parser: @@ -751,7 +791,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" icloud_storage: @@ -759,7 +799,7 @@ packages: description: name: icloud_storage sha256: fa91d9c3b4264651f01a4f5b99cffa354ffe455623b13ecf92be86d88b1e26ea - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" icons_plus: @@ -767,7 +807,7 @@ packages: description: name: icons_plus sha256: "8e2f601b8605d45dd55b106a0da084a1809125077a49574ca22e8bcd5b6e86f0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" image: @@ -775,7 +815,7 @@ packages: description: name: image sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.8.0" intl: @@ -783,7 +823,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" io: @@ -791,7 +831,7 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" isolate_channel: @@ -799,7 +839,7 @@ packages: description: name: isolate_channel sha256: a9d3d620695bc984244dafae00b95e4319d6974b2d77f4b9e1eb4f2efe099094 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.1" isolate_contactor: @@ -807,7 +847,7 @@ packages: description: name: isolate_contactor sha256: "6ba8434ceb58238a1389d6365111a3efe7baa1c68a66f4db6d63d351cf6c3a0f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" isolate_manager: @@ -815,7 +855,7 @@ packages: description: name: isolate_manager sha256: "22ed0c25f80ec3b5f21e3a55d060f4650afff33f27c2dff34c0f9409d5759ae5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.5+1" js: @@ -823,7 +863,7 @@ packages: description: name: js sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.7" json_annotation: @@ -831,7 +871,7 @@ packages: description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.9.0" json_serializable: @@ -839,7 +879,7 @@ packages: description: name: json_serializable sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.11.2" leak_tracker: @@ -847,7 +887,7 @@ packages: description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -855,7 +895,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.10" leak_tracker_testing: @@ -863,7 +903,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" lints: @@ -871,7 +911,7 @@ packages: description: name: lints sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" local_auth: @@ -879,7 +919,7 @@ packages: description: name: local_auth sha256: ae6f382f638108c6becd134318d7c3f0a93875383a54010f61d7c97ac05d5137 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" local_auth_android: @@ -887,7 +927,7 @@ packages: description: name: local_auth_android sha256: b41970749c2d43791790724b76917eeee1e90de76e6b0eec3edca03a329bf44c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" local_auth_darwin: @@ -895,7 +935,7 @@ packages: description: name: local_auth_darwin sha256: a8c3d4e17454111f7fd31ff72a31222359f6059f7fe956c2dcfe0f88f49826d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" local_auth_platform_interface: @@ -903,7 +943,7 @@ packages: description: name: local_auth_platform_interface sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" local_auth_windows: @@ -911,7 +951,7 @@ packages: description: name: local_auth_windows sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" locale_names: @@ -919,7 +959,7 @@ packages: description: name: locale_names sha256: "7a89ca54072f4f13d0f5df5a9ba69337554bf2fd057d1dd2a238898f3f159374" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" logging: @@ -927,7 +967,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" markdown: @@ -935,23 +975,23 @@ packages: description: name: markdown sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.3.1" matcher: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.0" meta: @@ -959,7 +999,7 @@ packages: description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.17.0" mime: @@ -967,7 +1007,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" mockito: @@ -975,7 +1015,7 @@ packages: description: name: mockito sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.6.3" multi_split_view: @@ -983,7 +1023,7 @@ packages: description: name: multi_split_view sha256: "06f5126a65d3010ce0a9d5c003e793041fe99377b23e3534bb05059f79a580e9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.6.1" native_toolchain_c: @@ -991,7 +1031,7 @@ packages: description: name: native_toolchain_c sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.6" nested: @@ -999,7 +1039,7 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" node_preamble: @@ -1007,7 +1047,7 @@ packages: description: name: node_preamble sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" objective_c: @@ -1015,7 +1055,7 @@ packages: description: name: objective_c sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.3.0" package_config: @@ -1023,7 +1063,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" package_info_plus: @@ -1031,7 +1071,7 @@ packages: description: name: package_info_plus sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "9.0.0" package_info_plus_platform_interface: @@ -1039,7 +1079,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" path: @@ -1047,7 +1087,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" path_parsing: @@ -1055,7 +1095,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" path_provider: @@ -1063,7 +1103,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.5" path_provider_android: @@ -1071,7 +1111,7 @@ packages: description: name: path_provider_android sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.22" path_provider_foundation: @@ -1079,7 +1119,7 @@ packages: description: name: path_provider_foundation sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.0" path_provider_linux: @@ -1087,7 +1127,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -1095,7 +1135,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" path_provider_windows: @@ -1103,7 +1143,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" petitparser: @@ -1111,7 +1151,7 @@ packages: description: name: petitparser sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.2" pinenacl: @@ -1119,7 +1159,7 @@ packages: description: name: pinenacl sha256: "57e907beaacbc3c024a098910b6240758e899674de07d6949a67b52fd984cbdf" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.0" plain_notification_token: @@ -1134,7 +1174,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -1142,7 +1182,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" pointycastle: @@ -1150,7 +1190,7 @@ packages: description: name: pointycastle sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" pool: @@ -1158,7 +1198,7 @@ packages: description: name: pool sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.2" posix: @@ -1166,7 +1206,7 @@ packages: description: name: posix sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.5.0" pretty_qr_code: @@ -1174,7 +1214,7 @@ packages: description: name: pretty_qr_code sha256: "474f8a4512113fba06f14a6ec9bbf42353b4e651d7a520e3096f2a9b6bbe7a8a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.6.0" provider: @@ -1182,7 +1222,7 @@ packages: description: name: provider sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5+1" pub_semver: @@ -1190,7 +1230,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" pubspec_parse: @@ -1198,7 +1238,7 @@ packages: description: name: pubspec_parse sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.0" qr: @@ -1206,7 +1246,7 @@ packages: description: name: qr sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" qr_code_dart_decoder: @@ -1214,7 +1254,7 @@ packages: description: name: qr_code_dart_decoder sha256: "4044f13a071da6102f7e9bc44a6b1ce577604d7846bcbeb1be412a137b825017" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" qr_code_dart_scan: @@ -1222,7 +1262,7 @@ packages: description: name: qr_code_dart_scan sha256: "81443d940f8f27baaa4b9aeaa8d3d2155ad2c0b9842a9bacb03dab85c111e2f6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.11.5" quiver: @@ -1230,7 +1270,7 @@ packages: description: name: quiver sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" re_editor: @@ -1238,7 +1278,7 @@ packages: description: name: re_editor sha256: dd4e6ca7350a8fa0cda4e425b82a0c3c010f0f6b3f618c74223e05b8129ab629 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.0" re_highlight: @@ -1246,7 +1286,7 @@ packages: description: name: re_highlight sha256: "6c4ac3f76f939fb7ca9df013df98526634e17d8f7460e028bd23a035870024f2" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.3" responsive_framework: @@ -1254,7 +1294,7 @@ packages: description: name: responsive_framework sha256: a8e1c13d4ba980c60cbf6fa1e9907cd60662bf2585184d7c96ca46c43de91552 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" riverpod: @@ -1262,7 +1302,7 @@ packages: description: name: riverpod sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" riverpod_analyzer_utils: @@ -1270,7 +1310,7 @@ packages: description: name: riverpod_analyzer_utils sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0-dev.7" riverpod_annotation: @@ -1278,7 +1318,7 @@ packages: description: name: riverpod_annotation sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" riverpod_generator: @@ -1286,7 +1326,7 @@ packages: description: name: riverpod_generator sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" screen_retriever: @@ -1294,7 +1334,7 @@ packages: description: name: screen_retriever sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_linux: @@ -1302,7 +1342,7 @@ packages: description: name: screen_retriever_linux sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_macos: @@ -1310,7 +1350,7 @@ packages: description: name: screen_retriever_macos sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_platform_interface: @@ -1318,7 +1358,7 @@ packages: description: name: screen_retriever_platform_interface sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_windows: @@ -1326,7 +1366,7 @@ packages: description: name: screen_retriever_windows sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screenshot: @@ -1334,7 +1374,7 @@ packages: description: name: screenshot sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" share_plus: @@ -1342,7 +1382,7 @@ packages: description: name: share_plus sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "12.0.1" share_plus_platform_interface: @@ -1350,7 +1390,7 @@ packages: description: name: share_plus_platform_interface sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" shared_preferences: @@ -1358,7 +1398,7 @@ packages: description: name: shared_preferences sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.4" shared_preferences_android: @@ -1366,7 +1406,7 @@ packages: description: name: shared_preferences_android sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.21" shared_preferences_foundation: @@ -1374,7 +1414,7 @@ packages: description: name: shared_preferences_foundation sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.6" shared_preferences_linux: @@ -1382,7 +1422,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -1390,7 +1430,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shared_preferences_web: @@ -1398,7 +1438,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" shared_preferences_windows: @@ -1406,7 +1446,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shelf: @@ -1414,7 +1454,7 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.2" shelf_packages_handler: @@ -1422,7 +1462,7 @@ packages: description: name: shelf_packages_handler sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" shelf_static: @@ -1430,7 +1470,7 @@ packages: description: name: shelf_static sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.3" shelf_web_socket: @@ -1438,7 +1478,7 @@ packages: description: name: shelf_web_socket sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" sky_engine: @@ -1451,7 +1491,7 @@ packages: description: name: source_gen sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.2" source_helper: @@ -1459,7 +1499,7 @@ packages: description: name: source_helper sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.8" source_map_stack_trace: @@ -1467,7 +1507,7 @@ packages: description: name: source_map_stack_trace sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" source_maps: @@ -1475,7 +1515,7 @@ packages: description: name: source_maps sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.10.13" source_span: @@ -1483,7 +1523,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.2" stack_trace: @@ -1491,7 +1531,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" state_notifier: @@ -1499,7 +1539,7 @@ packages: description: name: state_notifier sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" stream_channel: @@ -1507,7 +1547,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" stream_transform: @@ -1515,7 +1555,7 @@ packages: description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" string_scanner: @@ -1523,7 +1563,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" term_glyph: @@ -1531,39 +1571,39 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" - url: "https://pub.dev" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.30.0" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.dev" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.10" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" - url: "https://pub.dev" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.6.16" + version: "0.6.15" tuple: dependency: transitive description: name: tuple sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" typed_data: @@ -1571,7 +1611,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" universal_io: @@ -1579,7 +1619,7 @@ packages: description: name: universal_io sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" url_launcher: @@ -1587,7 +1627,7 @@ packages: description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.2" url_launcher_android: @@ -1595,7 +1635,7 @@ packages: description: name: url_launcher_android sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.28" url_launcher_ios: @@ -1603,7 +1643,7 @@ packages: description: name: url_launcher_ios sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.1" url_launcher_linux: @@ -1611,7 +1651,7 @@ packages: description: name: url_launcher_linux sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" url_launcher_macos: @@ -1619,7 +1659,7 @@ packages: description: name: url_launcher_macos sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.5" url_launcher_platform_interface: @@ -1627,7 +1667,7 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" url_launcher_web: @@ -1635,7 +1675,7 @@ packages: description: name: url_launcher_web sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" url_launcher_windows: @@ -1643,7 +1683,7 @@ packages: description: name: url_launcher_windows sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.5" uuid: @@ -1651,7 +1691,7 @@ packages: description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.3" vector_graphics: @@ -1659,7 +1699,7 @@ packages: description: name: vector_graphics sha256: "7076216a10d5c390315fbe536a30f1254c341e7543e6c4c8a815e591307772b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.20" vector_graphics_codec: @@ -1667,7 +1707,7 @@ packages: description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.13" vector_graphics_compiler: @@ -1675,7 +1715,7 @@ packages: description: name: vector_graphics_compiler sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" vector_math: @@ -1683,7 +1723,7 @@ packages: description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" vm_service: @@ -1691,7 +1731,7 @@ packages: description: name: vm_service sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "15.0.2" wake_on_lan: @@ -1699,7 +1739,7 @@ packages: description: name: wake_on_lan sha256: a0db43df0cd05181f476f38ec63345a763b7d3b9d8ab25cabbff45881780cb8e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1+3" wakelock_plus: @@ -1707,7 +1747,7 @@ packages: description: name: wakelock_plus sha256: "8b12256f616346910c519a35606fb69b1fe0737c06b6a447c6df43888b097f39" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" wakelock_plus_platform_interface: @@ -1715,7 +1755,7 @@ packages: description: name: wakelock_plus_platform_interface sha256: "24b84143787220a403491c2e5de0877fbbb87baf3f0b18a2a988973863db4b03" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" watch_connectivity: @@ -1730,7 +1770,7 @@ packages: description: name: watcher sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" web: @@ -1738,7 +1778,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" web_socket: @@ -1746,7 +1786,7 @@ packages: description: name: web_socket sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: @@ -1754,7 +1794,7 @@ packages: description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.3" webdav_client_plus: @@ -1762,7 +1802,7 @@ packages: description: name: webdav_client_plus sha256: "0f992fe05a46674a800d9fd8fdc5c54952ff739da155c558776a62ca0c2bed3a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" webkit_inspection_protocol: @@ -1770,7 +1810,7 @@ packages: description: name: webkit_inspection_protocol sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" win32: @@ -1778,7 +1818,7 @@ packages: description: name: win32 sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.15.0" window_manager: @@ -1786,7 +1826,7 @@ packages: description: name: window_manager sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.1" xdg_directories: @@ -1794,7 +1834,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" xml: @@ -1802,7 +1842,7 @@ packages: description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.6.1" xterm: @@ -1817,7 +1857,7 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" yaml_writer: @@ -1825,7 +1865,7 @@ packages: description: name: yaml_writer sha256: "69651cd7238411179ac32079937d4aa9a2970150d6b2ae2c6fe6de09402a5dc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" zmodem: @@ -1833,7 +1873,7 @@ packages: description: name: zmodem sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.6" zxing_lib: @@ -1841,7 +1881,7 @@ packages: description: name: zxing_lib sha256: f9170470b6bc947d21a6783486f88ef48aad66fc1380c8acd02b118418ec0ce0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.4" sdks: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 61c38279..a271dfc9 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -15,6 +16,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 421b2378..1fa81ed8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links dynamic_color flutter_secure_storage_windows local_auth_windows