From d88e97e699dc52972f3fe271dea73c3558ace0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Fri, 16 May 2025 21:50:44 +0800 Subject: [PATCH] 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 --- analysis_options.yaml | 2 + .../model/server/server_private_info.dart | 151 +++-- .../server/server_private_info.freezed.dart | 529 ++++++++++++++++++ .../model/server/server_private_info.g.dart | 13 +- lib/data/model/server/snippet.dart | 66 +-- lib/data/model/server/snippet.freezed.dart | 291 ++++++++++ lib/data/model/server/snippet.g.dart | 6 +- lib/data/provider/server.dart | 2 +- lib/data/store/server.dart | 83 ++- lib/data/store/snippet.dart | 14 +- lib/main.dart | 13 +- lib/view/page/backup.dart | 2 +- lib/view/page/server/edit.dart | 1 + lib/view/page/setting/entry.dart | 1 - pubspec.lock | 6 +- pubspec.yaml | 3 +- 16 files changed, 1028 insertions(+), 155 deletions(-) create mode 100644 lib/data/model/server/server_private_info.freezed.dart create mode 100644 lib/data/model/server/snippet.freezed.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 72d290fc..e37b312b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,6 +16,8 @@ analyzer: # strict-casts: true # strict-inference: true # strict-raw-types: true + errors: + invalid_annotation_target: ignore linter: # The lint rules applied to this project can be customized in the diff --git a/lib/data/model/server/server_private_info.dart b/lib/data/model/server/server_private_info.dart index b6dbbe50..a458b602 100644 --- a/lib/data/model/server/server_private_info.dart +++ b/lib/data/model/server/server_private_info.dart @@ -1,17 +1,18 @@ import 'dart:convert'; -import 'package:equatable/equatable.dart'; import 'package:fl_lib/fl_lib.dart'; +import 'package:freezed_annotation/freezed_annotation.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/server.dart'; import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/provider/server.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.freezed.dart'; /// In the first version, it's called `ServerPrivateInfo` which was designed to /// 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`. /// /// Nowaday, more fields are added to this class, and it's renamed to `Spi`. -@JsonSerializable() +@freezed @HiveType(typeId: 3) -class Spi with EquatableMixin { - @HiveField(0) - final String name; - @HiveField(1) - final String ip; - @HiveField(2) - final int port; - @HiveField(3) - final String user; - @HiveField(4) - final String? pwd; +class Spi with _$Spi { + const Spi._(); - /// [id] of private key - @JsonKey(name: 'pubKeyId') - @HiveField(5) - final String? keyId; - @HiveField(6) - final List? tags; - @HiveField(7) - final String? alterUrl; - @HiveField(8, defaultValue: true) - final bool autoConnect; + const factory Spi({ + @HiveField(0) required String name, + @HiveField(1) required String ip, + @HiveField(2) required int port, + @HiveField(3) required String user, + @HiveField(4) String? pwd, - /// [id] of the jump server - @HiveField(9) - final String? jumpId; + /// [id] of private key + @JsonKey(name: 'pubKeyId') @HiveField(5) String? keyId, + @HiveField(6) List? tags, + @HiveField(7) String? alterUrl, + @HiveField(8, defaultValue: true) @Default(true) bool autoConnect, - @HiveField(10) - final ServerCustom? custom; + /// [id] of the jump server + @HiveField(9) String? jumpId, + @HiveField(10) ServerCustom? custom, + @HiveField(11) WakeOnLanCfg? wolCfg, - @HiveField(11) - final WakeOnLanCfg? wolCfg; - - /// It only applies to SSH terminal. - @HiveField(12) - final Map? 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'; + /// It only applies to SSH terminal. + @HiveField(12) Map? envs, + @JsonKey(fromJson: Spi.parseId) @HiveField(13, defaultValue: '') required String id, + }) = _Spi; factory Spi.fromJson(Map json) => _$SpiFromJson(json); - Map toJson() => _$SpiToJson(this); - @override - String toString() => id; + String toString() => 'Spi<$oldId>'; - @override - List get props => - [name, ip, port, user, pwd, keyId, tags, alterUrl, autoConnect, jumpId, custom, wolCfg, envs]; + static String parseId(Object? id) { + if (id == null || id is! String || id.isEmpty) return ShortId.generate(); + return id; + } } 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()); VNode? get server => ServerProvider.pick(spi: this); @@ -127,27 +118,27 @@ extension Spix on Spi { /// Just for showing the struct of the class. /// /// **NOT** the default value. - static const example = Spi( - name: 'name', - ip: 'ip', - port: 22, - user: 'root', - pwd: 'pwd', - keyId: 'private_key_id', - tags: ['tag1', 'tag2'], - alterUrl: 'user@ip:port', - autoConnect: true, - jumpId: 'jump_server_id', - custom: ServerCustom( - pveAddr: 'http://localhost:8006', - pveIgnoreCert: false, - cmds: { - 'echo': 'echo hello', - }, - preferTempDev: 'nvme-pci-0400', - logoUrl: 'https://example.com/logo.png', - ), - ); + static final example = Spi( + name: 'name', + ip: 'ip', + port: 22, + user: 'root', + pwd: 'pwd', + keyId: 'private_key_id', + tags: ['tag1', 'tag2'], + alterUrl: 'user@ip:port', + autoConnect: true, + jumpId: 'jump_server_id', + custom: ServerCustom( + pveAddr: 'http://localhost:8006', + pveIgnoreCert: false, + cmds: { + 'echo': 'echo hello', + }, + preferTempDev: 'nvme-pci-0400', + logoUrl: 'https://example.com/logo.png', + ), + id: 'id'); bool get isRoot => user == 'root'; } diff --git a/lib/data/model/server/server_private_info.freezed.dart b/lib/data/model/server/server_private_info.freezed.dart new file mode 100644 index 00000000..2fd912cc --- /dev/null +++ b/lib/data/model/server/server_private_info.freezed.dart @@ -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 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 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? 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? get envs => throw _privateConstructorUsedError; + @JsonKey(fromJson: Spi.parseId) + @HiveField(13, defaultValue: '') + String get id => throw _privateConstructorUsedError; + + /// Serializes this Spi to a JSON map. + Map 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 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? 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? 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?, + 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?, + 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? 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? 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?, + 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?, + 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? 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? envs, + @JsonKey(fromJson: Spi.parseId) + @HiveField(13, defaultValue: '') + required this.id}) + : _tags = tags, + _envs = envs, + super._(); + + factory _$SpiImpl.fromJson(Map 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? _tags; + @override + @HiveField(6) + List? 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? _envs; + + /// It only applies to SSH terminal. + @override + @HiveField(12) + Map? 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 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? 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? envs, + @JsonKey(fromJson: Spi.parseId) + @HiveField(13, defaultValue: '') + required final String id}) = _$SpiImpl; + const _Spi._() : super._(); + + factory _Spi.fromJson(Map 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? 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? 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; +} diff --git a/lib/data/model/server/server_private_info.g.dart b/lib/data/model/server/server_private_info.g.dart index 8c7d184e..a5c589e7 100644 --- a/lib/data/model/server/server_private_info.g.dart +++ b/lib/data/model/server/server_private_info.g.dart @@ -30,13 +30,14 @@ class SpiAdapter extends TypeAdapter { custom: fields[10] as ServerCustom?, wolCfg: fields[11] as WakeOnLanCfg?, envs: (fields[12] as Map?)?.cast(), + id: fields[13] == null ? '' : fields[13] as String, ); } @override void write(BinaryWriter writer, Spi obj) { writer - ..writeByte(13) + ..writeByte(14) ..writeByte(0) ..write(obj.name) ..writeByte(1) @@ -62,7 +63,9 @@ class SpiAdapter extends TypeAdapter { ..writeByte(11) ..write(obj.wolCfg) ..writeByte(12) - ..write(obj.envs); + ..write(obj.envs) + ..writeByte(13) + ..write(obj.id); } @override @@ -80,7 +83,7 @@ class SpiAdapter extends TypeAdapter { // JsonSerializableGenerator // ************************************************************************** -Spi _$SpiFromJson(Map json) => Spi( +_$SpiImpl _$$SpiImplFromJson(Map json) => _$SpiImpl( name: json['name'] as String, ip: json['ip'] as String, port: (json['port'] as num).toInt(), @@ -100,9 +103,10 @@ Spi _$SpiFromJson(Map json) => Spi( envs: (json['envs'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ), + id: Spi.parseId(json['id']), ); -Map _$SpiToJson(Spi instance) => { +Map _$$SpiImplToJson(_$SpiImpl instance) => { 'name': instance.name, 'ip': instance.ip, 'port': instance.port, @@ -116,4 +120,5 @@ Map _$SpiToJson(Spi instance) => { 'custom': instance.custom, 'wolCfg': instance.wolCfg, 'envs': instance.envs, + 'id': instance.id, }; diff --git a/lib/data/model/server/snippet.dart b/lib/data/model/server/snippet.dart index 51bc0ad6..96bf686b 100644 --- a/lib/data/model/server/snippet.dart +++ b/lib/data/model/server/snippet.dart @@ -1,50 +1,36 @@ import 'dart:async'; -import 'package:equatable/equatable.dart'; import 'package:fl_lib/fl_lib.dart'; +import 'package:freezed_annotation/freezed_annotation.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:xterm/core.dart'; part 'snippet.g.dart'; +part 'snippet.freezed.dart'; -@JsonSerializable() +@freezed @HiveType(typeId: 2) -class Snippet with EquatableMixin { - @HiveField(0) - final String name; - @HiveField(1) - final String script; - @HiveField(2) - final List? tags; - @HiveField(3) - final String? note; - - /// List of server id that this snippet should be auto run on - @HiveField(4) - final List? autoRunOn; - - const Snippet({ - required this.name, - required this.script, - this.tags, - this.note, - this.autoRunOn, - }); +class Snippet with _$Snippet { + const factory Snippet({ + @HiveField(0) required String name, + @HiveField(1) required String script, + @HiveField(2) List? tags, + @HiveField(3) String? note, + + /// List of server id that this snippet should be auto run on + @HiveField(4) List? autoRunOn, + }) = _Snippet; factory Snippet.fromJson(Map json) => _$SnippetFromJson(json); - - Map toJson() => _$SnippetToJson(this); - - @override - List get props => [ - name, - script, - tags, - note, - autoRunOn, - ]; + + static const example = Snippet( + name: 'example', + script: 'echo hello', + tags: ['tag'], + note: 'note', + autoRunOn: ['server_id'], + ); } extension SnippetX on Snippet { @@ -73,7 +59,7 @@ extension SnippetX on Snippet { /// There is no [TerminalKey] in the script if (matches.isEmpty) { - terminal.textInput(argsFmted); + terminal.textInput(argsFmted); if (autoEnter) terminal.keyInput(TerminalKey.enter); return; } @@ -186,14 +172,6 @@ extension SnippetX on Snippet { r'${ctrl': TerminalKey.control, r'${alt': TerminalKey.alt, }; - - static const example = Snippet( - name: 'example', - script: 'echo hello', - tags: ['tag'], - note: 'note', - autoRunOn: ['server_id'], - ); } class SnippetResult { diff --git a/lib/data/model/server/snippet.freezed.dart b/lib/data/model/server/snippet.freezed.dart new file mode 100644 index 00000000..0f055558 --- /dev/null +++ b/lib/data/model/server/snippet.freezed.dart @@ -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 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 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? 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? get autoRunOn => throw _privateConstructorUsedError; + + /// Serializes this Snippet to a JSON map. + Map 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 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? tags, + @HiveField(3) String? note, + @HiveField(4) List? 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?, + 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?, + ) 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? tags, + @HiveField(3) String? note, + @HiveField(4) List? 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?, + 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?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnippetImpl implements _Snippet { + const _$SnippetImpl( + {@HiveField(0) required this.name, + @HiveField(1) required this.script, + @HiveField(2) final List? tags, + @HiveField(3) this.note, + @HiveField(4) final List? autoRunOn}) + : _tags = tags, + _autoRunOn = autoRunOn; + + factory _$SnippetImpl.fromJson(Map json) => + _$$SnippetImplFromJson(json); + + @override + @HiveField(0) + final String name; + @override + @HiveField(1) + final String script; + final List? _tags; + @override + @HiveField(2) + List? 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? _autoRunOn; + + /// List of server id that this snippet should be auto run on + @override + @HiveField(4) + List? 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 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? tags, + @HiveField(3) final String? note, + @HiveField(4) final List? autoRunOn}) = _$SnippetImpl; + + factory _Snippet.fromJson(Map json) = _$SnippetImpl.fromJson; + + @override + @HiveField(0) + String get name; + @override + @HiveField(1) + String get script; + @override + @HiveField(2) + List? get tags; + @override + @HiveField(3) + String? get note; + + /// List of server id that this snippet should be auto run on + @override + @HiveField(4) + List? 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; +} diff --git a/lib/data/model/server/snippet.g.dart b/lib/data/model/server/snippet.g.dart index 755ef423..b281da55 100644 --- a/lib/data/model/server/snippet.g.dart +++ b/lib/data/model/server/snippet.g.dart @@ -56,7 +56,8 @@ class SnippetAdapter extends TypeAdapter { // JsonSerializableGenerator // ************************************************************************** -Snippet _$SnippetFromJson(Map json) => Snippet( +_$SnippetImpl _$$SnippetImplFromJson(Map json) => + _$SnippetImpl( name: json['name'] as String, script: json['script'] as String, tags: (json['tags'] as List?)?.map((e) => e as String).toList(), @@ -66,7 +67,8 @@ Snippet _$SnippetFromJson(Map json) => Snippet( .toList(), ); -Map _$SnippetToJson(Snippet instance) => { +Map _$$SnippetImplToJson(_$SnippetImpl instance) => + { 'name': instance.name, 'script': instance.script, 'tags': instance.tags, diff --git a/lib/data/provider/server.dart b/lib/data/provider/server.dart index 5b8adc41..1412095d 100644 --- a/lib/data/provider/server.dart +++ b/lib/data/provider/server.dart @@ -208,7 +208,7 @@ class ServerProvider extends Provider { serverOrder.value.clear(); serverOrder.notify(); Stores.setting.serverOrder.put(serverOrder.value); - Stores.server.deleteAll(); + Stores.server.clear(); _updateTags(); bakSync.sync(milliDelay: 1000); } diff --git a/lib/data/store/server.dart b/lib/data/store/server.dart index e1f758ab..8dceada6 100644 --- a/lib/data/store/server.dart +++ b/lib/data/store/server.dart @@ -1,6 +1,9 @@ import 'package:fl_lib/fl_lib.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 { ServerStore._() : super('server'); @@ -8,15 +11,12 @@ class ServerStore extends HiveStore { static final instance = ServerStore._(); void put(Spi info) { - // box.put(info.id, info); - // box.updateLastModified(); set(info.id, info); } List fetch() { - final ids = box.keys; final List ss = []; - for (final id in ids) { + for (final id in keys()) { final s = box.get(id); if (s != null && s is Spi) { ss.add(s); @@ -29,10 +29,6 @@ class ServerStore extends HiveStore { remove(id); } - void deleteAll() { - clear(); - } - void update(Spi old, Spi newInfo) { if (!have(old)) { throw Exception('Old spi: $old not found'); @@ -41,5 +37,74 @@ class ServerStore extends HiveStore { 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 = {}; + + // 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(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.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); + } + } } diff --git a/lib/data/store/snippet.dart b/lib/data/store/snippet.dart index f17cb070..98c8432f 100644 --- a/lib/data/store/snippet.dart +++ b/lib/data/store/snippet.dart @@ -8,8 +8,6 @@ class SnippetStore extends HiveStore { static final instance = SnippetStore._(); void put(Snippet snippet) { - // box.put(snippet.name, snippet); - // box.updateLastModified(); set(snippet.name, snippet); } @@ -25,8 +23,16 @@ class SnippetStore extends HiveStore { } void delete(Snippet s) { - // box.delete(s.name); - // box.updateLastModified(); 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; } diff --git a/lib/main.dart b/lib/main.dart index 79fd1902..3f9baaba 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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/res/build_data.dart'; import 'package:server_box/data/res/store.dart'; +import 'package:server_box/data/store/server.dart'; Future main() async { _runInZone(() async { @@ -75,6 +76,10 @@ Future _initData() async { await PrefStore.shared.init(); // Call this before accessing any store 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. PrivateKeyProvider.instance.load(); SnippetProvider.instance.load(); @@ -82,9 +87,6 @@ Future _initData() async { SftpProvider.instance.load(); if (Stores.setting.betaTest.fetch()) AppUpdate.chan = AppUpdateChan.beta; - - // It may effect the following logic, so await it. - await _doVersionRelated(); } void _setupDebug() { @@ -111,7 +113,7 @@ void _doPlatformRelated() async { } // It may contains some async heavy funcs. -Future _doVersionRelated() async { +Future _doDbMigrate() async { final lastVer = Stores.setting.lastVer.fetch(); const newVer = BuildData.build; // It's only the version upgrade trigger logic. @@ -121,6 +123,9 @@ Future _doVersionRelated() async { ServerFuncBtn.autoAddNewFuncs(newVer); Stores.setting.lastVer.put(newVer); } + + // Migrate the old id to new id. + ServerStore.instance.migrateIds(); } Future _initWindow() async { diff --git a/lib/view/page/backup.dart b/lib/view/page/backup.dart index c5a96211..24ff69d0 100644 --- a/lib/view/page/backup.dart +++ b/lib/view/page/backup.dart @@ -251,7 +251,7 @@ final class _BackupPageState extends State onTap: () async { final data = await context.showImportDialog( title: l10n.snippet, - modelDef: SnippetX.example.toJson(), + modelDef: Snippet.example.toJson(), ); if (data == null) return; final str = String.fromCharCodes(data); diff --git a/lib/view/page/server/edit.dart b/lib/view/page/server/edit.dart index 23aa815a..0448f657 100644 --- a/lib/view/page/server/edit.dart +++ b/lib/view/page/server/edit.dart @@ -672,6 +672,7 @@ extension on _ServerEditPageState { custom: custom, wolCfg: wol, envs: _env.value.isEmpty ? null : _env.value, + id: widget.args?.spi.id ?? ShortId.generate(), ); if (this.spi == null) { diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index 222cda51..2b70ba96 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -118,7 +118,6 @@ final class _AppSettingsPageState extends State { @override Widget build(BuildContext context) { return MultiList( - thumbVisibility: true, children: [ [const CenterGreyTitle('App'), _buildApp()], [CenterGreyTitle(l10n.server), _buildServer()], diff --git a/pubspec.lock b/pubspec.lock index 5fd13511..785b5d57 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -442,7 +442,7 @@ packages: source: hosted version: "5.0.3" equatable: - dependency: "direct main" + dependency: transitive description: name: equatable sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" @@ -526,8 +526,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.0.289" - resolved-ref: b272de58cdb0ee8d488337e7a0d00c57ee8f8a36 + ref: "v1.0.294" + resolved-ref: aac04d5e6c649f59cae33d492d14bb59dacded9c url: "https://github.com/lppcg/fl_lib" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 2de9223d..58d7c6ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,6 @@ dependencies: easy_isolate: ^1.3.0 intl: ^0.19.0 highlight: ^0.7.0 - equatable: ^2.0.7 flutter_highlight: ^0.7.0 re_editor: ^0.7.0 shared_preferences: ^2.1.1 @@ -63,7 +62,7 @@ dependencies: fl_lib: git: url: https://github.com/lppcg/fl_lib - ref: v1.0.289 + ref: v1.0.294 dependency_overrides: # webdav_client_plus: