new: use generated ids for servers (#765)

* new: use generated ids for servers
Fixes #743

* fix: deps.

* fix: migrate related settings

* fix: restore servers from json
This commit is contained in:
lollipopkit🏳️‍⚧️
2025-05-16 21:50:44 +08:00
committed by GitHub
parent d29bd1d806
commit d88e97e699
16 changed files with 1028 additions and 155 deletions

View File

@@ -16,6 +16,8 @@ analyzer:
# strict-casts: true # strict-casts: true
# strict-inference: true # strict-inference: true
# strict-raw-types: true # strict-raw-types: true
errors:
invalid_annotation_target: ignore
linter: linter:
# The lint rules applied to this project can be customized in the # The lint rules applied to this project can be customized in the

View File

@@ -1,17 +1,18 @@
import 'dart:convert'; import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:server_box/data/model/server/custom.dart'; import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server.dart'; import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/store/server.dart';
part 'server_private_info.g.dart'; part 'server_private_info.g.dart';
part 'server_private_info.freezed.dart';
/// In the first version, it's called `ServerPrivateInfo` which was designed to /// In the first version, it's called `ServerPrivateInfo` which was designed to
/// store the private information of a server. /// store the private information of a server.
@@ -19,76 +20,66 @@ part 'server_private_info.g.dart';
/// Some params named as `spi` in the codebase which is the abbreviation of `ServerPrivateInfo`. /// Some params named as `spi` in the codebase which is the abbreviation of `ServerPrivateInfo`.
/// ///
/// Nowaday, more fields are added to this class, and it's renamed to `Spi`. /// Nowaday, more fields are added to this class, and it's renamed to `Spi`.
@JsonSerializable() @freezed
@HiveType(typeId: 3) @HiveType(typeId: 3)
class Spi with EquatableMixin { class Spi with _$Spi {
@HiveField(0) const Spi._();
final String name;
@HiveField(1)
final String ip;
@HiveField(2)
final int port;
@HiveField(3)
final String user;
@HiveField(4)
final String? pwd;
/// [id] of private key const factory Spi({
@JsonKey(name: 'pubKeyId') @HiveField(0) required String name,
@HiveField(5) @HiveField(1) required String ip,
final String? keyId; @HiveField(2) required int port,
@HiveField(6) @HiveField(3) required String user,
final List<String>? tags; @HiveField(4) String? pwd,
@HiveField(7)
final String? alterUrl;
@HiveField(8, defaultValue: true)
final bool autoConnect;
/// [id] of the jump server /// [id] of private key
@HiveField(9) @JsonKey(name: 'pubKeyId') @HiveField(5) String? keyId,
final String? jumpId; @HiveField(6) List<String>? tags,
@HiveField(7) String? alterUrl,
@HiveField(8, defaultValue: true) @Default(true) bool autoConnect,
@HiveField(10) /// [id] of the jump server
final ServerCustom? custom; @HiveField(9) String? jumpId,
@HiveField(10) ServerCustom? custom,
@HiveField(11) WakeOnLanCfg? wolCfg,
@HiveField(11) /// It only applies to SSH terminal.
final WakeOnLanCfg? wolCfg; @HiveField(12) Map<String, String>? envs,
@JsonKey(fromJson: Spi.parseId) @HiveField(13, defaultValue: '') required String id,
/// It only applies to SSH terminal. }) = _Spi;
@HiveField(12)
final Map<String, String>? envs;
final String id;
const Spi({
required this.name,
required this.ip,
required this.port,
required this.user,
required this.pwd,
this.keyId,
this.tags,
this.alterUrl,
this.autoConnect = true,
this.jumpId,
this.custom,
this.wolCfg,
this.envs,
}) : id = '$user@$ip:$port';
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json); factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
Map<String, dynamic> toJson() => _$SpiToJson(this);
@override @override
String toString() => id; String toString() => 'Spi<$oldId>';
@override static String parseId(Object? id) {
List<Object?> get props => if (id == null || id is! String || id.isEmpty) return ShortId.generate();
[name, ip, port, user, pwd, keyId, tags, alterUrl, autoConnect, jumpId, custom, wolCfg, envs]; return id;
}
} }
extension Spix on Spi { extension Spix on Spi {
/// After upgrading to >= 1155, this field is only recommended to be used
/// for displaying the server name.
String get oldId => '$user@$ip:$port';
/// Save the [Spi] to the local storage.
void save() => ServerStore.instance.put(this);
/// Migrate the [oldId] to the new generated [id] by [ShortId.generate].
///
/// Returns:
/// - `null` if the [id] is not empty.
/// - The new [id] if the [id] is empty.
String? migrateId() {
if (id.isNotEmpty) return null;
ServerStore.instance.delete(oldId);
final newSpi = copyWith(id: ShortId.generate());
newSpi.save();
return newSpi.id;
}
String toJsonString() => json.encode(toJson()); String toJsonString() => json.encode(toJson());
VNode<Server>? get server => ServerProvider.pick(spi: this); VNode<Server>? get server => ServerProvider.pick(spi: this);
@@ -127,27 +118,27 @@ extension Spix on Spi {
/// Just for showing the struct of the class. /// Just for showing the struct of the class.
/// ///
/// **NOT** the default value. /// **NOT** the default value.
static const example = Spi( static final example = Spi(
name: 'name', name: 'name',
ip: 'ip', ip: 'ip',
port: 22, port: 22,
user: 'root', user: 'root',
pwd: 'pwd', pwd: 'pwd',
keyId: 'private_key_id', keyId: 'private_key_id',
tags: ['tag1', 'tag2'], tags: ['tag1', 'tag2'],
alterUrl: 'user@ip:port', alterUrl: 'user@ip:port',
autoConnect: true, autoConnect: true,
jumpId: 'jump_server_id', jumpId: 'jump_server_id',
custom: ServerCustom( custom: ServerCustom(
pveAddr: 'http://localhost:8006', pveAddr: 'http://localhost:8006',
pveIgnoreCert: false, pveIgnoreCert: false,
cmds: { cmds: {
'echo': 'echo hello', 'echo': 'echo hello',
}, },
preferTempDev: 'nvme-pci-0400', preferTempDev: 'nvme-pci-0400',
logoUrl: 'https://example.com/logo.png', logoUrl: 'https://example.com/logo.png',
), ),
); id: 'id');
bool get isRoot => user == 'root'; bool get isRoot => user == 'root';
} }

View File

@@ -0,0 +1,529 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'server_private_info.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
Spi _$SpiFromJson(Map<String, dynamic> json) {
return _Spi.fromJson(json);
}
/// @nodoc
mixin _$Spi {
@HiveField(0)
String get name => throw _privateConstructorUsedError;
@HiveField(1)
String get ip => throw _privateConstructorUsedError;
@HiveField(2)
int get port => throw _privateConstructorUsedError;
@HiveField(3)
String get user => throw _privateConstructorUsedError;
@HiveField(4)
String? get pwd => throw _privateConstructorUsedError;
/// [id] of private key
@JsonKey(name: 'pubKeyId')
@HiveField(5)
String? get keyId => throw _privateConstructorUsedError;
@HiveField(6)
List<String>? get tags => throw _privateConstructorUsedError;
@HiveField(7)
String? get alterUrl => throw _privateConstructorUsedError;
@HiveField(8, defaultValue: true)
bool get autoConnect => throw _privateConstructorUsedError;
/// [id] of the jump server
@HiveField(9)
String? get jumpId => throw _privateConstructorUsedError;
@HiveField(10)
ServerCustom? get custom => throw _privateConstructorUsedError;
@HiveField(11)
WakeOnLanCfg? get wolCfg => throw _privateConstructorUsedError;
/// It only applies to SSH terminal.
@HiveField(12)
Map<String, String>? get envs => throw _privateConstructorUsedError;
@JsonKey(fromJson: Spi.parseId)
@HiveField(13, defaultValue: '')
String get id => throw _privateConstructorUsedError;
/// Serializes this Spi to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SpiCopyWith<Spi> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SpiCopyWith<$Res> {
factory $SpiCopyWith(Spi value, $Res Function(Spi) then) =
_$SpiCopyWithImpl<$Res, Spi>;
@useResult
$Res call(
{@HiveField(0) String name,
@HiveField(1) String ip,
@HiveField(2) int port,
@HiveField(3) String user,
@HiveField(4) String? pwd,
@JsonKey(name: 'pubKeyId') @HiveField(5) String? keyId,
@HiveField(6) List<String>? tags,
@HiveField(7) String? alterUrl,
@HiveField(8, defaultValue: true) bool autoConnect,
@HiveField(9) String? jumpId,
@HiveField(10) ServerCustom? custom,
@HiveField(11) WakeOnLanCfg? wolCfg,
@HiveField(12) Map<String, String>? envs,
@JsonKey(fromJson: Spi.parseId)
@HiveField(13, defaultValue: '')
String id});
}
/// @nodoc
class _$SpiCopyWithImpl<$Res, $Val extends Spi> implements $SpiCopyWith<$Res> {
_$SpiCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? ip = null,
Object? port = null,
Object? user = null,
Object? pwd = freezed,
Object? keyId = freezed,
Object? tags = freezed,
Object? alterUrl = freezed,
Object? autoConnect = null,
Object? jumpId = freezed,
Object? custom = freezed,
Object? wolCfg = freezed,
Object? envs = freezed,
Object? id = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
ip: null == ip
? _value.ip
: ip // ignore: cast_nullable_to_non_nullable
as String,
port: null == port
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as int,
user: null == user
? _value.user
: user // ignore: cast_nullable_to_non_nullable
as String,
pwd: freezed == pwd
? _value.pwd
: pwd // ignore: cast_nullable_to_non_nullable
as String?,
keyId: freezed == keyId
? _value.keyId
: keyId // ignore: cast_nullable_to_non_nullable
as String?,
tags: freezed == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>?,
alterUrl: freezed == alterUrl
? _value.alterUrl
: alterUrl // ignore: cast_nullable_to_non_nullable
as String?,
autoConnect: null == autoConnect
? _value.autoConnect
: autoConnect // ignore: cast_nullable_to_non_nullable
as bool,
jumpId: freezed == jumpId
? _value.jumpId
: jumpId // ignore: cast_nullable_to_non_nullable
as String?,
custom: freezed == custom
? _value.custom
: custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,
wolCfg: freezed == wolCfg
? _value.wolCfg
: wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,
envs: freezed == envs
? _value.envs
: envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$SpiImplCopyWith<$Res> implements $SpiCopyWith<$Res> {
factory _$$SpiImplCopyWith(_$SpiImpl value, $Res Function(_$SpiImpl) then) =
__$$SpiImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{@HiveField(0) String name,
@HiveField(1) String ip,
@HiveField(2) int port,
@HiveField(3) String user,
@HiveField(4) String? pwd,
@JsonKey(name: 'pubKeyId') @HiveField(5) String? keyId,
@HiveField(6) List<String>? tags,
@HiveField(7) String? alterUrl,
@HiveField(8, defaultValue: true) bool autoConnect,
@HiveField(9) String? jumpId,
@HiveField(10) ServerCustom? custom,
@HiveField(11) WakeOnLanCfg? wolCfg,
@HiveField(12) Map<String, String>? envs,
@JsonKey(fromJson: Spi.parseId)
@HiveField(13, defaultValue: '')
String id});
}
/// @nodoc
class __$$SpiImplCopyWithImpl<$Res> extends _$SpiCopyWithImpl<$Res, _$SpiImpl>
implements _$$SpiImplCopyWith<$Res> {
__$$SpiImplCopyWithImpl(_$SpiImpl _value, $Res Function(_$SpiImpl) _then)
: super(_value, _then);
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? ip = null,
Object? port = null,
Object? user = null,
Object? pwd = freezed,
Object? keyId = freezed,
Object? tags = freezed,
Object? alterUrl = freezed,
Object? autoConnect = null,
Object? jumpId = freezed,
Object? custom = freezed,
Object? wolCfg = freezed,
Object? envs = freezed,
Object? id = null,
}) {
return _then(_$SpiImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
ip: null == ip
? _value.ip
: ip // ignore: cast_nullable_to_non_nullable
as String,
port: null == port
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as int,
user: null == user
? _value.user
: user // ignore: cast_nullable_to_non_nullable
as String,
pwd: freezed == pwd
? _value.pwd
: pwd // ignore: cast_nullable_to_non_nullable
as String?,
keyId: freezed == keyId
? _value.keyId
: keyId // ignore: cast_nullable_to_non_nullable
as String?,
tags: freezed == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>?,
alterUrl: freezed == alterUrl
? _value.alterUrl
: alterUrl // ignore: cast_nullable_to_non_nullable
as String?,
autoConnect: null == autoConnect
? _value.autoConnect
: autoConnect // ignore: cast_nullable_to_non_nullable
as bool,
jumpId: freezed == jumpId
? _value.jumpId
: jumpId // ignore: cast_nullable_to_non_nullable
as String?,
custom: freezed == custom
? _value.custom
: custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,
wolCfg: freezed == wolCfg
? _value.wolCfg
: wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,
envs: freezed == envs
? _value._envs
: envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SpiImpl extends _Spi {
const _$SpiImpl(
{@HiveField(0) required this.name,
@HiveField(1) required this.ip,
@HiveField(2) required this.port,
@HiveField(3) required this.user,
@HiveField(4) this.pwd,
@JsonKey(name: 'pubKeyId') @HiveField(5) this.keyId,
@HiveField(6) final List<String>? tags,
@HiveField(7) this.alterUrl,
@HiveField(8, defaultValue: true) this.autoConnect = true,
@HiveField(9) this.jumpId,
@HiveField(10) this.custom,
@HiveField(11) this.wolCfg,
@HiveField(12) final Map<String, String>? envs,
@JsonKey(fromJson: Spi.parseId)
@HiveField(13, defaultValue: '')
required this.id})
: _tags = tags,
_envs = envs,
super._();
factory _$SpiImpl.fromJson(Map<String, dynamic> json) =>
_$$SpiImplFromJson(json);
@override
@HiveField(0)
final String name;
@override
@HiveField(1)
final String ip;
@override
@HiveField(2)
final int port;
@override
@HiveField(3)
final String user;
@override
@HiveField(4)
final String? pwd;
/// [id] of private key
@override
@JsonKey(name: 'pubKeyId')
@HiveField(5)
final String? keyId;
final List<String>? _tags;
@override
@HiveField(6)
List<String>? get tags {
final value = _tags;
if (value == null) return null;
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
@HiveField(7)
final String? alterUrl;
@override
@JsonKey()
@HiveField(8, defaultValue: true)
final bool autoConnect;
/// [id] of the jump server
@override
@HiveField(9)
final String? jumpId;
@override
@HiveField(10)
final ServerCustom? custom;
@override
@HiveField(11)
final WakeOnLanCfg? wolCfg;
/// It only applies to SSH terminal.
final Map<String, String>? _envs;
/// It only applies to SSH terminal.
@override
@HiveField(12)
Map<String, String>? get envs {
final value = _envs;
if (value == null) return null;
if (_envs is EqualUnmodifiableMapView) return _envs;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override
@JsonKey(fromJson: Spi.parseId)
@HiveField(13, defaultValue: '')
final String id;
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SpiImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.ip, ip) || other.ip == ip) &&
(identical(other.port, port) || other.port == port) &&
(identical(other.user, user) || other.user == user) &&
(identical(other.pwd, pwd) || other.pwd == pwd) &&
(identical(other.keyId, keyId) || other.keyId == keyId) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
(identical(other.alterUrl, alterUrl) ||
other.alterUrl == alterUrl) &&
(identical(other.autoConnect, autoConnect) ||
other.autoConnect == autoConnect) &&
(identical(other.jumpId, jumpId) || other.jumpId == jumpId) &&
(identical(other.custom, custom) || other.custom == custom) &&
(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg) &&
const DeepCollectionEquality().equals(other._envs, _envs) &&
(identical(other.id, id) || other.id == id));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
name,
ip,
port,
user,
pwd,
keyId,
const DeepCollectionEquality().hash(_tags),
alterUrl,
autoConnect,
jumpId,
custom,
wolCfg,
const DeepCollectionEquality().hash(_envs),
id);
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SpiImplCopyWith<_$SpiImpl> get copyWith =>
__$$SpiImplCopyWithImpl<_$SpiImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SpiImplToJson(
this,
);
}
}
abstract class _Spi extends Spi {
const factory _Spi(
{@HiveField(0) required final String name,
@HiveField(1) required final String ip,
@HiveField(2) required final int port,
@HiveField(3) required final String user,
@HiveField(4) final String? pwd,
@JsonKey(name: 'pubKeyId') @HiveField(5) final String? keyId,
@HiveField(6) final List<String>? tags,
@HiveField(7) final String? alterUrl,
@HiveField(8, defaultValue: true) final bool autoConnect,
@HiveField(9) final String? jumpId,
@HiveField(10) final ServerCustom? custom,
@HiveField(11) final WakeOnLanCfg? wolCfg,
@HiveField(12) final Map<String, String>? envs,
@JsonKey(fromJson: Spi.parseId)
@HiveField(13, defaultValue: '')
required final String id}) = _$SpiImpl;
const _Spi._() : super._();
factory _Spi.fromJson(Map<String, dynamic> json) = _$SpiImpl.fromJson;
@override
@HiveField(0)
String get name;
@override
@HiveField(1)
String get ip;
@override
@HiveField(2)
int get port;
@override
@HiveField(3)
String get user;
@override
@HiveField(4)
String? get pwd;
/// [id] of private key
@override
@JsonKey(name: 'pubKeyId')
@HiveField(5)
String? get keyId;
@override
@HiveField(6)
List<String>? get tags;
@override
@HiveField(7)
String? get alterUrl;
@override
@HiveField(8, defaultValue: true)
bool get autoConnect;
/// [id] of the jump server
@override
@HiveField(9)
String? get jumpId;
@override
@HiveField(10)
ServerCustom? get custom;
@override
@HiveField(11)
WakeOnLanCfg? get wolCfg;
/// It only applies to SSH terminal.
@override
@HiveField(12)
Map<String, String>? get envs;
@override
@JsonKey(fromJson: Spi.parseId)
@HiveField(13, defaultValue: '')
String get id;
/// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SpiImplCopyWith<_$SpiImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -30,13 +30,14 @@ class SpiAdapter extends TypeAdapter<Spi> {
custom: fields[10] as ServerCustom?, custom: fields[10] as ServerCustom?,
wolCfg: fields[11] as WakeOnLanCfg?, wolCfg: fields[11] as WakeOnLanCfg?,
envs: (fields[12] as Map?)?.cast<String, String>(), envs: (fields[12] as Map?)?.cast<String, String>(),
id: fields[13] == null ? '' : fields[13] as String,
); );
} }
@override @override
void write(BinaryWriter writer, Spi obj) { void write(BinaryWriter writer, Spi obj) {
writer writer
..writeByte(13) ..writeByte(14)
..writeByte(0) ..writeByte(0)
..write(obj.name) ..write(obj.name)
..writeByte(1) ..writeByte(1)
@@ -62,7 +63,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
..writeByte(11) ..writeByte(11)
..write(obj.wolCfg) ..write(obj.wolCfg)
..writeByte(12) ..writeByte(12)
..write(obj.envs); ..write(obj.envs)
..writeByte(13)
..write(obj.id);
} }
@override @override
@@ -80,7 +83,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Spi _$SpiFromJson(Map<String, dynamic> json) => Spi( _$SpiImpl _$$SpiImplFromJson(Map<String, dynamic> json) => _$SpiImpl(
name: json['name'] as String, name: json['name'] as String,
ip: json['ip'] as String, ip: json['ip'] as String,
port: (json['port'] as num).toInt(), port: (json['port'] as num).toInt(),
@@ -100,9 +103,10 @@ Spi _$SpiFromJson(Map<String, dynamic> json) => Spi(
envs: (json['envs'] as Map<String, dynamic>?)?.map( envs: (json['envs'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String), (k, e) => MapEntry(k, e as String),
), ),
id: Spi.parseId(json['id']),
); );
Map<String, dynamic> _$SpiToJson(Spi instance) => <String, dynamic>{ Map<String, dynamic> _$$SpiImplToJson(_$SpiImpl instance) => <String, dynamic>{
'name': instance.name, 'name': instance.name,
'ip': instance.ip, 'ip': instance.ip,
'port': instance.port, 'port': instance.port,
@@ -116,4 +120,5 @@ Map<String, dynamic> _$SpiToJson(Spi instance) => <String, dynamic>{
'custom': instance.custom, 'custom': instance.custom,
'wolCfg': instance.wolCfg, 'wolCfg': instance.wolCfg,
'envs': instance.envs, 'envs': instance.envs,
'id': instance.id,
}; };

View File

@@ -1,50 +1,36 @@
import 'dart:async'; import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:xterm/core.dart'; import 'package:xterm/core.dart';
part 'snippet.g.dart'; part 'snippet.g.dart';
part 'snippet.freezed.dart';
@JsonSerializable() @freezed
@HiveType(typeId: 2) @HiveType(typeId: 2)
class Snippet with EquatableMixin { class Snippet with _$Snippet {
@HiveField(0) const factory Snippet({
final String name; @HiveField(0) required String name,
@HiveField(1) @HiveField(1) required String script,
final String script; @HiveField(2) List<String>? tags,
@HiveField(2) @HiveField(3) String? note,
final List<String>? tags;
@HiveField(3) /// List of server id that this snippet should be auto run on
final String? note; @HiveField(4) List<String>? autoRunOn,
}) = _Snippet;
/// List of server id that this snippet should be auto run on
@HiveField(4)
final List<String>? autoRunOn;
const Snippet({
required this.name,
required this.script,
this.tags,
this.note,
this.autoRunOn,
});
factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json); factory Snippet.fromJson(Map<String, dynamic> json) => _$SnippetFromJson(json);
Map<String, dynamic> toJson() => _$SnippetToJson(this); static const example = Snippet(
name: 'example',
@override script: 'echo hello',
List<Object?> get props => [ tags: ['tag'],
name, note: 'note',
script, autoRunOn: ['server_id'],
tags, );
note,
autoRunOn,
];
} }
extension SnippetX on Snippet { extension SnippetX on Snippet {
@@ -73,7 +59,7 @@ extension SnippetX on Snippet {
/// There is no [TerminalKey] in the script /// There is no [TerminalKey] in the script
if (matches.isEmpty) { if (matches.isEmpty) {
terminal.textInput(argsFmted); terminal.textInput(argsFmted);
if (autoEnter) terminal.keyInput(TerminalKey.enter); if (autoEnter) terminal.keyInput(TerminalKey.enter);
return; return;
} }
@@ -186,14 +172,6 @@ extension SnippetX on Snippet {
r'${ctrl': TerminalKey.control, r'${ctrl': TerminalKey.control,
r'${alt': TerminalKey.alt, r'${alt': TerminalKey.alt,
}; };
static const example = Snippet(
name: 'example',
script: 'echo hello',
tags: ['tag'],
note: 'note',
autoRunOn: ['server_id'],
);
} }
class SnippetResult { class SnippetResult {

View File

@@ -0,0 +1,291 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'snippet.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
Snippet _$SnippetFromJson(Map<String, dynamic> json) {
return _Snippet.fromJson(json);
}
/// @nodoc
mixin _$Snippet {
@HiveField(0)
String get name => throw _privateConstructorUsedError;
@HiveField(1)
String get script => throw _privateConstructorUsedError;
@HiveField(2)
List<String>? get tags => throw _privateConstructorUsedError;
@HiveField(3)
String? get note => throw _privateConstructorUsedError;
/// List of server id that this snippet should be auto run on
@HiveField(4)
List<String>? get autoRunOn => throw _privateConstructorUsedError;
/// Serializes this Snippet to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Snippet
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnippetCopyWith<Snippet> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnippetCopyWith<$Res> {
factory $SnippetCopyWith(Snippet value, $Res Function(Snippet) then) =
_$SnippetCopyWithImpl<$Res, Snippet>;
@useResult
$Res call(
{@HiveField(0) String name,
@HiveField(1) String script,
@HiveField(2) List<String>? tags,
@HiveField(3) String? note,
@HiveField(4) List<String>? autoRunOn});
}
/// @nodoc
class _$SnippetCopyWithImpl<$Res, $Val extends Snippet>
implements $SnippetCopyWith<$Res> {
_$SnippetCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Snippet
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? script = null,
Object? tags = freezed,
Object? note = freezed,
Object? autoRunOn = freezed,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
script: null == script
? _value.script
: script // ignore: cast_nullable_to_non_nullable
as String,
tags: freezed == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>?,
note: freezed == note
? _value.note
: note // ignore: cast_nullable_to_non_nullable
as String?,
autoRunOn: freezed == autoRunOn
? _value.autoRunOn
: autoRunOn // ignore: cast_nullable_to_non_nullable
as List<String>?,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnippetImplCopyWith<$Res> implements $SnippetCopyWith<$Res> {
factory _$$SnippetImplCopyWith(
_$SnippetImpl value, $Res Function(_$SnippetImpl) then) =
__$$SnippetImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{@HiveField(0) String name,
@HiveField(1) String script,
@HiveField(2) List<String>? tags,
@HiveField(3) String? note,
@HiveField(4) List<String>? autoRunOn});
}
/// @nodoc
class __$$SnippetImplCopyWithImpl<$Res>
extends _$SnippetCopyWithImpl<$Res, _$SnippetImpl>
implements _$$SnippetImplCopyWith<$Res> {
__$$SnippetImplCopyWithImpl(
_$SnippetImpl _value, $Res Function(_$SnippetImpl) _then)
: super(_value, _then);
/// Create a copy of Snippet
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? script = null,
Object? tags = freezed,
Object? note = freezed,
Object? autoRunOn = freezed,
}) {
return _then(_$SnippetImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
script: null == script
? _value.script
: script // ignore: cast_nullable_to_non_nullable
as String,
tags: freezed == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>?,
note: freezed == note
? _value.note
: note // ignore: cast_nullable_to_non_nullable
as String?,
autoRunOn: freezed == autoRunOn
? _value._autoRunOn
: autoRunOn // ignore: cast_nullable_to_non_nullable
as List<String>?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnippetImpl implements _Snippet {
const _$SnippetImpl(
{@HiveField(0) required this.name,
@HiveField(1) required this.script,
@HiveField(2) final List<String>? tags,
@HiveField(3) this.note,
@HiveField(4) final List<String>? autoRunOn})
: _tags = tags,
_autoRunOn = autoRunOn;
factory _$SnippetImpl.fromJson(Map<String, dynamic> json) =>
_$$SnippetImplFromJson(json);
@override
@HiveField(0)
final String name;
@override
@HiveField(1)
final String script;
final List<String>? _tags;
@override
@HiveField(2)
List<String>? get tags {
final value = _tags;
if (value == null) return null;
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
@HiveField(3)
final String? note;
/// List of server id that this snippet should be auto run on
final List<String>? _autoRunOn;
/// List of server id that this snippet should be auto run on
@override
@HiveField(4)
List<String>? get autoRunOn {
final value = _autoRunOn;
if (value == null) return null;
if (_autoRunOn is EqualUnmodifiableListView) return _autoRunOn;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
String toString() {
return 'Snippet(name: $name, script: $script, tags: $tags, note: $note, autoRunOn: $autoRunOn)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnippetImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.script, script) || other.script == script) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
(identical(other.note, note) || other.note == note) &&
const DeepCollectionEquality()
.equals(other._autoRunOn, _autoRunOn));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
name,
script,
const DeepCollectionEquality().hash(_tags),
note,
const DeepCollectionEquality().hash(_autoRunOn));
/// Create a copy of Snippet
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnippetImplCopyWith<_$SnippetImpl> get copyWith =>
__$$SnippetImplCopyWithImpl<_$SnippetImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnippetImplToJson(
this,
);
}
}
abstract class _Snippet implements Snippet {
const factory _Snippet(
{@HiveField(0) required final String name,
@HiveField(1) required final String script,
@HiveField(2) final List<String>? tags,
@HiveField(3) final String? note,
@HiveField(4) final List<String>? autoRunOn}) = _$SnippetImpl;
factory _Snippet.fromJson(Map<String, dynamic> json) = _$SnippetImpl.fromJson;
@override
@HiveField(0)
String get name;
@override
@HiveField(1)
String get script;
@override
@HiveField(2)
List<String>? get tags;
@override
@HiveField(3)
String? get note;
/// List of server id that this snippet should be auto run on
@override
@HiveField(4)
List<String>? get autoRunOn;
/// Create a copy of Snippet
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnippetImplCopyWith<_$SnippetImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -56,7 +56,8 @@ class SnippetAdapter extends TypeAdapter<Snippet> {
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Snippet _$SnippetFromJson(Map<String, dynamic> json) => Snippet( _$SnippetImpl _$$SnippetImplFromJson(Map<String, dynamic> json) =>
_$SnippetImpl(
name: json['name'] as String, name: json['name'] as String,
script: json['script'] as String, script: json['script'] as String,
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(), tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
@@ -66,7 +67,8 @@ Snippet _$SnippetFromJson(Map<String, dynamic> json) => Snippet(
.toList(), .toList(),
); );
Map<String, dynamic> _$SnippetToJson(Snippet instance) => <String, dynamic>{ Map<String, dynamic> _$$SnippetImplToJson(_$SnippetImpl instance) =>
<String, dynamic>{
'name': instance.name, 'name': instance.name,
'script': instance.script, 'script': instance.script,
'tags': instance.tags, 'tags': instance.tags,

View File

@@ -208,7 +208,7 @@ class ServerProvider extends Provider {
serverOrder.value.clear(); serverOrder.value.clear();
serverOrder.notify(); serverOrder.notify();
Stores.setting.serverOrder.put(serverOrder.value); Stores.setting.serverOrder.put(serverOrder.value);
Stores.server.deleteAll(); Stores.server.clear();
_updateTags(); _updateTags();
bakSync.sync(milliDelay: 1000); bakSync.sync(milliDelay: 1000);
} }

View File

@@ -1,6 +1,9 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/store/container.dart';
import 'package:server_box/data/store/setting.dart';
import 'package:server_box/data/store/snippet.dart';
class ServerStore extends HiveStore { class ServerStore extends HiveStore {
ServerStore._() : super('server'); ServerStore._() : super('server');
@@ -8,15 +11,12 @@ class ServerStore extends HiveStore {
static final instance = ServerStore._(); static final instance = ServerStore._();
void put(Spi info) { void put(Spi info) {
// box.put(info.id, info);
// box.updateLastModified();
set(info.id, info); set(info.id, info);
} }
List<Spi> fetch() { List<Spi> fetch() {
final ids = box.keys;
final List<Spi> ss = []; final List<Spi> ss = [];
for (final id in ids) { for (final id in keys()) {
final s = box.get(id); final s = box.get(id);
if (s != null && s is Spi) { if (s != null && s is Spi) {
ss.add(s); ss.add(s);
@@ -29,10 +29,6 @@ class ServerStore extends HiveStore {
remove(id); remove(id);
} }
void deleteAll() {
clear();
}
void update(Spi old, Spi newInfo) { void update(Spi old, Spi newInfo) {
if (!have(old)) { if (!have(old)) {
throw Exception('Old spi: $old not found'); throw Exception('Old spi: $old not found');
@@ -41,5 +37,74 @@ class ServerStore extends HiveStore {
put(newInfo); put(newInfo);
} }
bool have(Spi s) => box.get(s.id) != null; bool have(Spi s) => get(s.id) != null;
void migrateIds() {
final ss = fetch();
final idMap = <String, String>{};
// Collect all old to new ID mappings
for (final s in ss) {
final newId = s.migrateId();
if (newId == null) continue;
// Use s.oldId as the key, because s.id would be empty for a server being migrated.
// s.oldId represents the identifier used before migration.
idMap[s.oldId] = newId;
}
final srvOrder = SettingStore.instance.serverOrder.fetch();
final snippets = SnippetStore.instance.fetch();
final container = ContainerStore.instance;
bool srvOrderChanged = false;
// Update all references to the servers
for (final e in idMap.entries) {
final oldId = e.key;
final newId = e.value;
// Replace ids in ordering settings.
final srvIdx = srvOrder.indexOf(oldId);
if (srvIdx != -1) {
srvOrder[srvIdx] = newId;
srvOrderChanged = true;
}
// Replace ids in jump server settings.
final spi = get<Spi>(newId);
if (spi != null) {
final jumpId = spi.jumpId; // This could be an oldId.
// Check if this jumpId corresponds to a server that was also migrated.
if (jumpId != null && idMap.containsKey(jumpId)) {
final newJumpId = idMap[jumpId];
if (spi.jumpId != newJumpId) {
final newSpi = spi.copyWith(jumpId: newJumpId);
update(spi, newSpi);
}
}
}
// Replace ids in [Snippet]
for (final snippet in snippets) {
final autoRunsOn = snippet.autoRunOn;
final idx = autoRunsOn?.indexOf(oldId);
if (idx != null && idx != -1) {
final newAutoRunsOn = List<String>.from(autoRunsOn ?? []);
newAutoRunsOn[idx] = newId;
final newSnippet = snippet.copyWith(autoRunOn: newAutoRunsOn);
SnippetStore.instance.update(snippet, newSnippet);
}
}
// Replace ids in [Container]
final dockerHost = container.fetch(oldId);
if (dockerHost != null) {
container.remove(oldId);
container.set(newId, dockerHost);
}
}
if (srvOrderChanged) {
SettingStore.instance.serverOrder.put(srvOrder);
}
}
} }

View File

@@ -8,8 +8,6 @@ class SnippetStore extends HiveStore {
static final instance = SnippetStore._(); static final instance = SnippetStore._();
void put(Snippet snippet) { void put(Snippet snippet) {
// box.put(snippet.name, snippet);
// box.updateLastModified();
set(snippet.name, snippet); set(snippet.name, snippet);
} }
@@ -25,8 +23,16 @@ class SnippetStore extends HiveStore {
} }
void delete(Snippet s) { void delete(Snippet s) {
// box.delete(s.name);
// box.updateLastModified();
remove(s.name); remove(s.name);
} }
void update(Snippet old, Snippet newInfo) {
if (!have(old)) {
throw Exception('Old snippet: $old not found');
}
delete(old);
put(newInfo);
}
bool have(Snippet s) => get(s.name) != null;
} }

View File

@@ -25,6 +25,7 @@ import 'package:server_box/data/provider/sftp.dart';
import 'package:server_box/data/provider/snippet.dart'; import 'package:server_box/data/provider/snippet.dart';
import 'package:server_box/data/res/build_data.dart'; import 'package:server_box/data/res/build_data.dart';
import 'package:server_box/data/res/store.dart'; import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/store/server.dart';
Future<void> main() async { Future<void> main() async {
_runInZone(() async { _runInZone(() async {
@@ -75,6 +76,10 @@ Future<void> _initData() async {
await PrefStore.shared.init(); // Call this before accessing any store await PrefStore.shared.init(); // Call this before accessing any store
await Stores.init(); await Stores.init();
// It may effect the following logic, so await it.
// DO DB migration before load any provider.
await _doDbMigrate();
// DO NOT change the order of these providers. // DO NOT change the order of these providers.
PrivateKeyProvider.instance.load(); PrivateKeyProvider.instance.load();
SnippetProvider.instance.load(); SnippetProvider.instance.load();
@@ -82,9 +87,6 @@ Future<void> _initData() async {
SftpProvider.instance.load(); SftpProvider.instance.load();
if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta; if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta;
// It may effect the following logic, so await it.
await _doVersionRelated();
} }
void _setupDebug() { void _setupDebug() {
@@ -111,7 +113,7 @@ void _doPlatformRelated() async {
} }
// It may contains some async heavy funcs. // It may contains some async heavy funcs.
Future<void> _doVersionRelated() async { Future<void> _doDbMigrate() async {
final lastVer = Stores.setting.lastVer.fetch(); final lastVer = Stores.setting.lastVer.fetch();
const newVer = BuildData.build; const newVer = BuildData.build;
// It's only the version upgrade trigger logic. // It's only the version upgrade trigger logic.
@@ -121,6 +123,9 @@ Future<void> _doVersionRelated() async {
ServerFuncBtn.autoAddNewFuncs(newVer); ServerFuncBtn.autoAddNewFuncs(newVer);
Stores.setting.lastVer.put(newVer); Stores.setting.lastVer.put(newVer);
} }
// Migrate the old id to new id.
ServerStore.instance.migrateIds();
} }
Future<void> _initWindow() async { Future<void> _initWindow() async {

View File

@@ -251,7 +251,7 @@ final class _BackupPageState extends State<BackupPage>
onTap: () async { onTap: () async {
final data = await context.showImportDialog( final data = await context.showImportDialog(
title: l10n.snippet, title: l10n.snippet,
modelDef: SnippetX.example.toJson(), modelDef: Snippet.example.toJson(),
); );
if (data == null) return; if (data == null) return;
final str = String.fromCharCodes(data); final str = String.fromCharCodes(data);

View File

@@ -672,6 +672,7 @@ extension on _ServerEditPageState {
custom: custom, custom: custom,
wolCfg: wol, wolCfg: wol,
envs: _env.value.isEmpty ? null : _env.value, envs: _env.value.isEmpty ? null : _env.value,
id: widget.args?.spi.id ?? ShortId.generate(),
); );
if (this.spi == null) { if (this.spi == null) {

View File

@@ -118,7 +118,6 @@ final class _AppSettingsPageState extends State<AppSettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiList( return MultiList(
thumbVisibility: true,
children: [ children: [
[const CenterGreyTitle('App'), _buildApp()], [const CenterGreyTitle('App'), _buildApp()],
[CenterGreyTitle(l10n.server), _buildServer()], [CenterGreyTitle(l10n.server), _buildServer()],

View File

@@ -442,7 +442,7 @@ packages:
source: hosted source: hosted
version: "5.0.3" version: "5.0.3"
equatable: equatable:
dependency: "direct main" dependency: transitive
description: description:
name: equatable name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
@@ -526,8 +526,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "v1.0.289" ref: "v1.0.294"
resolved-ref: b272de58cdb0ee8d488337e7a0d00c57ee8f8a36 resolved-ref: aac04d5e6c649f59cae33d492d14bb59dacded9c
url: "https://github.com/lppcg/fl_lib" url: "https://github.com/lppcg/fl_lib"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@@ -16,7 +16,6 @@ dependencies:
easy_isolate: ^1.3.0 easy_isolate: ^1.3.0
intl: ^0.19.0 intl: ^0.19.0
highlight: ^0.7.0 highlight: ^0.7.0
equatable: ^2.0.7
flutter_highlight: ^0.7.0 flutter_highlight: ^0.7.0
re_editor: ^0.7.0 re_editor: ^0.7.0
shared_preferences: ^2.1.1 shared_preferences: ^2.1.1
@@ -63,7 +62,7 @@ dependencies:
fl_lib: fl_lib:
git: git:
url: https://github.com/lppcg/fl_lib url: https://github.com/lppcg/fl_lib
ref: v1.0.289 ref: v1.0.294
dependency_overrides: dependency_overrides:
# webdav_client_plus: # webdav_client_plus: