initial commit

This commit is contained in:
2026-07-27 14:42:23 +02:00
commit 833c68a189
257 changed files with 17947 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'connection.freezed.dart';
part 'connection.g.dart';
/// A saved NodeMaster API endpoint. Client-side only — has no server-side
/// counterpart. `baseUrl` excludes the `/v1` suffix (added by
/// [NodeMasterApiClient]) and any trailing slash, e.g.
/// `https://edge-01.local:8080`.
@freezed
sealed class Connection with _$Connection {
const factory Connection({
required String id,
required String name,
required String baseUrl,
}) = _Connection;
factory Connection.fromJson(Map<String, dynamic> json) => _$ConnectionFromJson(json);
}
@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'connection.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Connection {
String get id; String get name; String get baseUrl;
/// Create a copy of Connection
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ConnectionCopyWith<Connection> get copyWith => _$ConnectionCopyWithImpl<Connection>(this as Connection, _$identity);
/// Serializes this Connection to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Connection&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.baseUrl, baseUrl) || other.baseUrl == baseUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,baseUrl);
@override
String toString() {
return 'Connection(id: $id, name: $name, baseUrl: $baseUrl)';
}
}
/// @nodoc
abstract mixin class $ConnectionCopyWith<$Res> {
factory $ConnectionCopyWith(Connection value, $Res Function(Connection) _then) = _$ConnectionCopyWithImpl;
@useResult
$Res call({
String id, String name, String baseUrl
});
}
/// @nodoc
class _$ConnectionCopyWithImpl<$Res>
implements $ConnectionCopyWith<$Res> {
_$ConnectionCopyWithImpl(this._self, this._then);
final Connection _self;
final $Res Function(Connection) _then;
/// Create a copy of Connection
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? baseUrl = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,baseUrl: null == baseUrl ? _self.baseUrl : baseUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [Connection].
extension ConnectionPatterns on Connection {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Connection value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _Connection() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Connection value) $default,){
final _that = this;
switch (_that) {
case _Connection():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Connection value)? $default,){
final _that = this;
switch (_that) {
case _Connection() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String baseUrl)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Connection() when $default != null:
return $default(_that.id,_that.name,_that.baseUrl);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String baseUrl) $default,) {final _that = this;
switch (_that) {
case _Connection():
return $default(_that.id,_that.name,_that.baseUrl);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String baseUrl)? $default,) {final _that = this;
switch (_that) {
case _Connection() when $default != null:
return $default(_that.id,_that.name,_that.baseUrl);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _Connection implements Connection {
const _Connection({required this.id, required this.name, required this.baseUrl});
factory _Connection.fromJson(Map<String, dynamic> json) => _$ConnectionFromJson(json);
@override final String id;
@override final String name;
@override final String baseUrl;
/// Create a copy of Connection
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ConnectionCopyWith<_Connection> get copyWith => __$ConnectionCopyWithImpl<_Connection>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ConnectionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Connection&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.baseUrl, baseUrl) || other.baseUrl == baseUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,baseUrl);
@override
String toString() {
return 'Connection(id: $id, name: $name, baseUrl: $baseUrl)';
}
}
/// @nodoc
abstract mixin class _$ConnectionCopyWith<$Res> implements $ConnectionCopyWith<$Res> {
factory _$ConnectionCopyWith(_Connection value, $Res Function(_Connection) _then) = __$ConnectionCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String baseUrl
});
}
/// @nodoc
class __$ConnectionCopyWithImpl<$Res>
implements _$ConnectionCopyWith<$Res> {
__$ConnectionCopyWithImpl(this._self, this._then);
final _Connection _self;
final $Res Function(_Connection) _then;
/// Create a copy of Connection
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? baseUrl = null,}) {
return _then(_Connection(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,baseUrl: null == baseUrl ? _self.baseUrl : baseUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on
+20
View File
@@ -0,0 +1,20 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'connection.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_Connection _$ConnectionFromJson(Map<String, dynamic> json) => _Connection(
id: json['id'] as String,
name: json['name'] as String,
baseUrl: json['baseUrl'] as String,
);
Map<String, dynamic> _$ConnectionToJson(_Connection instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'baseUrl': instance.baseUrl,
};
@@ -0,0 +1,75 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../utils/shared_preferences_provider.dart';
import 'connection.dart';
import 'connection_storage.dart';
part 'connection_providers.g.dart';
@Riverpod(keepAlive: true)
ConnectionStorage connectionStorage(Ref ref) {
return ConnectionStorage(ref.watch(sharedPreferencesProvider));
}
/// The full list of saved connections. Mutations persist immediately.
@Riverpod(keepAlive: true)
class SavedConnections extends _$SavedConnections {
@override
List<Connection> build() => ref.watch(connectionStorageProvider).readConnections();
Future<void> add(Connection connection) async {
state = [...state, connection];
await ref.read(connectionStorageProvider).writeConnections(state);
}
Future<void> update(Connection connection) async {
state = [
for (final c in state)
if (c.id == connection.id) connection else c,
];
await ref.read(connectionStorageProvider).writeConnections(state);
}
Future<void> remove(String id) async {
state = state.where((c) => c.id != id).toList(growable: false);
await ref.read(connectionStorageProvider).writeConnections(state);
final activeId = ref.read(activeConnectionIdProvider);
if (activeId == id) {
await ref.read(activeConnectionIdProvider.notifier).setActive(
state.isEmpty ? null : state.first.id,
);
}
}
}
/// The id of the currently active connection, or null if none is selected
/// (e.g. first launch with nothing configured yet).
@Riverpod(keepAlive: true)
class ActiveConnectionId extends _$ActiveConnectionId {
@override
String? build() {
final stored = ref.watch(connectionStorageProvider).readActiveConnectionId();
final connections = ref.watch(savedConnectionsProvider);
if (stored != null && connections.any((c) => c.id == stored)) return stored;
return connections.isEmpty ? null : connections.first.id;
}
Future<void> setActive(String? id) async {
state = id;
await ref.read(connectionStorageProvider).writeActiveConnectionId(id);
}
}
/// The currently active [Connection], or null if none is configured.
/// Everything in `core/network` derives from this — switching it rebuilds
/// the API client and, transitively, every provider that watches it.
@riverpod
Connection? activeConnection(Ref ref) {
final id = ref.watch(activeConnectionIdProvider);
if (id == null) return null;
final connections = ref.watch(savedConnectionsProvider);
for (final c in connections) {
if (c.id == id) return c;
}
return null;
}
@@ -0,0 +1,232 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'connection_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(connectionStorage)
const connectionStorageProvider = ConnectionStorageProvider._();
final class ConnectionStorageProvider
extends
$FunctionalProvider<
ConnectionStorage,
ConnectionStorage,
ConnectionStorage
>
with $Provider<ConnectionStorage> {
const ConnectionStorageProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'connectionStorageProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$connectionStorageHash();
@$internal
@override
$ProviderElement<ConnectionStorage> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ConnectionStorage create(Ref ref) {
return connectionStorage(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ConnectionStorage value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ConnectionStorage>(value),
);
}
}
String _$connectionStorageHash() => r'0faf909e3978260785f2f8ce4bc41f101e27ad66';
/// The full list of saved connections. Mutations persist immediately.
@ProviderFor(SavedConnections)
const savedConnectionsProvider = SavedConnectionsProvider._();
/// The full list of saved connections. Mutations persist immediately.
final class SavedConnectionsProvider
extends $NotifierProvider<SavedConnections, List<Connection>> {
/// The full list of saved connections. Mutations persist immediately.
const SavedConnectionsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'savedConnectionsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$savedConnectionsHash();
@$internal
@override
SavedConnections create() => SavedConnections();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<Connection> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<Connection>>(value),
);
}
}
String _$savedConnectionsHash() => r'2595280b9ef1f5835ecce70b791a901a51aaf8e8';
/// The full list of saved connections. Mutations persist immediately.
abstract class _$SavedConnections extends $Notifier<List<Connection>> {
List<Connection> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<List<Connection>, List<Connection>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<List<Connection>, List<Connection>>,
List<Connection>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// The id of the currently active connection, or null if none is selected
/// (e.g. first launch with nothing configured yet).
@ProviderFor(ActiveConnectionId)
const activeConnectionIdProvider = ActiveConnectionIdProvider._();
/// The id of the currently active connection, or null if none is selected
/// (e.g. first launch with nothing configured yet).
final class ActiveConnectionIdProvider
extends $NotifierProvider<ActiveConnectionId, String?> {
/// The id of the currently active connection, or null if none is selected
/// (e.g. first launch with nothing configured yet).
const ActiveConnectionIdProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'activeConnectionIdProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$activeConnectionIdHash();
@$internal
@override
ActiveConnectionId create() => ActiveConnectionId();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String?>(value),
);
}
}
String _$activeConnectionIdHash() =>
r'ac8b7d48bc04e6f9245b23c83a15e11a539e1fc8';
/// The id of the currently active connection, or null if none is selected
/// (e.g. first launch with nothing configured yet).
abstract class _$ActiveConnectionId extends $Notifier<String?> {
String? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<String?, String?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<String?, String?>,
String?,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// The currently active [Connection], or null if none is configured.
/// Everything in `core/network` derives from this — switching it rebuilds
/// the API client and, transitively, every provider that watches it.
@ProviderFor(activeConnection)
const activeConnectionProvider = ActiveConnectionProvider._();
/// The currently active [Connection], or null if none is configured.
/// Everything in `core/network` derives from this — switching it rebuilds
/// the API client and, transitively, every provider that watches it.
final class ActiveConnectionProvider
extends $FunctionalProvider<Connection?, Connection?, Connection?>
with $Provider<Connection?> {
/// The currently active [Connection], or null if none is configured.
/// Everything in `core/network` derives from this — switching it rebuilds
/// the API client and, transitively, every provider that watches it.
const ActiveConnectionProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'activeConnectionProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$activeConnectionHash();
@$internal
@override
$ProviderElement<Connection?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Connection? create(Ref ref) {
return activeConnection(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Connection? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Connection?>(value),
);
}
}
String _$activeConnectionHash() => r'544cf6f795b35636cbfc497e6379a84b1e8f5b88';
@@ -0,0 +1,41 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'connection.dart';
const _connectionsKey = 'connections';
const _activeConnectionIdKey = 'active_connection_id';
/// Reads/writes the saved connection list and active-connection id to
/// [SharedPreferences] as a plain JSON-encoded array. Not a secret store —
/// the API has no auth, so there's nothing sensitive in a saved base URL.
class ConnectionStorage {
const ConnectionStorage(this._prefs);
final SharedPreferences _prefs;
List<Connection> readConnections() {
final raw = _prefs.getString(_connectionsKey);
if (raw == null || raw.isEmpty) return const [];
final decoded = jsonDecode(raw) as List<dynamic>;
return decoded
.map((e) => Connection.fromJson(e as Map<String, dynamic>))
.toList(growable: false);
}
Future<void> writeConnections(List<Connection> connections) async {
final encoded = jsonEncode(connections.map((c) => c.toJson()).toList());
await _prefs.setString(_connectionsKey, encoded);
}
String? readActiveConnectionId() => _prefs.getString(_activeConnectionIdKey);
Future<void> writeActiveConnectionId(String? id) async {
if (id == null) {
await _prefs.remove(_activeConnectionIdKey);
} else {
await _prefs.setString(_activeConnectionIdKey, id);
}
}
}
@@ -0,0 +1,25 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'remote_node.dart';
import 'service.dart';
part 'aggregated_node_status.freezed.dart';
part 'aggregated_node_status.g.dart';
/// Mirrors the anonymous `nodeServices` struct returned by
/// `GET /nodes/aggregated`: `{node, services?, error?}`. `error` is the
/// *local* API reporting that fetching *this remote* failed (e.g.
/// unreachable) — a per-entry condition, distinct from the outer call
/// itself failing. Render `error` inline per-card; never conflate it with
/// an [ApiException] on the aggregated fetch as a whole.
@freezed
sealed class AggregatedNodeStatus with _$AggregatedNodeStatus {
const factory AggregatedNodeStatus({
required RemoteNode node,
@Default(<Service>[]) List<Service> services,
String? error,
}) = _AggregatedNodeStatus;
factory AggregatedNodeStatus.fromJson(Map<String, dynamic> json) =>
_$AggregatedNodeStatusFromJson(json);
}
@@ -0,0 +1,301 @@
// 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 'aggregated_node_status.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$AggregatedNodeStatus {
RemoteNode get node; List<Service> get services; String? get error;
/// Create a copy of AggregatedNodeStatus
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$AggregatedNodeStatusCopyWith<AggregatedNodeStatus> get copyWith => _$AggregatedNodeStatusCopyWithImpl<AggregatedNodeStatus>(this as AggregatedNodeStatus, _$identity);
/// Serializes this AggregatedNodeStatus to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is AggregatedNodeStatus&&(identical(other.node, node) || other.node == node)&&const DeepCollectionEquality().equals(other.services, services)&&(identical(other.error, error) || other.error == error));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,node,const DeepCollectionEquality().hash(services),error);
@override
String toString() {
return 'AggregatedNodeStatus(node: $node, services: $services, error: $error)';
}
}
/// @nodoc
abstract mixin class $AggregatedNodeStatusCopyWith<$Res> {
factory $AggregatedNodeStatusCopyWith(AggregatedNodeStatus value, $Res Function(AggregatedNodeStatus) _then) = _$AggregatedNodeStatusCopyWithImpl;
@useResult
$Res call({
RemoteNode node, List<Service> services, String? error
});
$RemoteNodeCopyWith<$Res> get node;
}
/// @nodoc
class _$AggregatedNodeStatusCopyWithImpl<$Res>
implements $AggregatedNodeStatusCopyWith<$Res> {
_$AggregatedNodeStatusCopyWithImpl(this._self, this._then);
final AggregatedNodeStatus _self;
final $Res Function(AggregatedNodeStatus) _then;
/// Create a copy of AggregatedNodeStatus
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? node = null,Object? services = null,Object? error = freezed,}) {
return _then(_self.copyWith(
node: null == node ? _self.node : node // ignore: cast_nullable_to_non_nullable
as RemoteNode,services: null == services ? _self.services : services // ignore: cast_nullable_to_non_nullable
as List<Service>,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of AggregatedNodeStatus
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$RemoteNodeCopyWith<$Res> get node {
return $RemoteNodeCopyWith<$Res>(_self.node, (value) {
return _then(_self.copyWith(node: value));
});
}
}
/// Adds pattern-matching-related methods to [AggregatedNodeStatus].
extension AggregatedNodeStatusPatterns on AggregatedNodeStatus {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AggregatedNodeStatus value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _AggregatedNodeStatus() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AggregatedNodeStatus value) $default,){
final _that = this;
switch (_that) {
case _AggregatedNodeStatus():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AggregatedNodeStatus value)? $default,){
final _that = this;
switch (_that) {
case _AggregatedNodeStatus() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( RemoteNode node, List<Service> services, String? error)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _AggregatedNodeStatus() when $default != null:
return $default(_that.node,_that.services,_that.error);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( RemoteNode node, List<Service> services, String? error) $default,) {final _that = this;
switch (_that) {
case _AggregatedNodeStatus():
return $default(_that.node,_that.services,_that.error);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( RemoteNode node, List<Service> services, String? error)? $default,) {final _that = this;
switch (_that) {
case _AggregatedNodeStatus() when $default != null:
return $default(_that.node,_that.services,_that.error);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _AggregatedNodeStatus implements AggregatedNodeStatus {
const _AggregatedNodeStatus({required this.node, final List<Service> services = const <Service>[], this.error}): _services = services;
factory _AggregatedNodeStatus.fromJson(Map<String, dynamic> json) => _$AggregatedNodeStatusFromJson(json);
@override final RemoteNode node;
final List<Service> _services;
@override@JsonKey() List<Service> get services {
if (_services is EqualUnmodifiableListView) return _services;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_services);
}
@override final String? error;
/// Create a copy of AggregatedNodeStatus
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$AggregatedNodeStatusCopyWith<_AggregatedNodeStatus> get copyWith => __$AggregatedNodeStatusCopyWithImpl<_AggregatedNodeStatus>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$AggregatedNodeStatusToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AggregatedNodeStatus&&(identical(other.node, node) || other.node == node)&&const DeepCollectionEquality().equals(other._services, _services)&&(identical(other.error, error) || other.error == error));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,node,const DeepCollectionEquality().hash(_services),error);
@override
String toString() {
return 'AggregatedNodeStatus(node: $node, services: $services, error: $error)';
}
}
/// @nodoc
abstract mixin class _$AggregatedNodeStatusCopyWith<$Res> implements $AggregatedNodeStatusCopyWith<$Res> {
factory _$AggregatedNodeStatusCopyWith(_AggregatedNodeStatus value, $Res Function(_AggregatedNodeStatus) _then) = __$AggregatedNodeStatusCopyWithImpl;
@override @useResult
$Res call({
RemoteNode node, List<Service> services, String? error
});
@override $RemoteNodeCopyWith<$Res> get node;
}
/// @nodoc
class __$AggregatedNodeStatusCopyWithImpl<$Res>
implements _$AggregatedNodeStatusCopyWith<$Res> {
__$AggregatedNodeStatusCopyWithImpl(this._self, this._then);
final _AggregatedNodeStatus _self;
final $Res Function(_AggregatedNodeStatus) _then;
/// Create a copy of AggregatedNodeStatus
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? node = null,Object? services = null,Object? error = freezed,}) {
return _then(_AggregatedNodeStatus(
node: null == node ? _self.node : node // ignore: cast_nullable_to_non_nullable
as RemoteNode,services: null == services ? _self._services : services // ignore: cast_nullable_to_non_nullable
as List<Service>,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of AggregatedNodeStatus
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$RemoteNodeCopyWith<$Res> get node {
return $RemoteNodeCopyWith<$Res>(_self.node, (value) {
return _then(_self.copyWith(node: value));
});
}
}
// dart format on
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'aggregated_node_status.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_AggregatedNodeStatus _$AggregatedNodeStatusFromJson(
Map<String, dynamic> json,
) => _AggregatedNodeStatus(
node: RemoteNode.fromJson(json['node'] as Map<String, dynamic>),
services:
(json['services'] as List<dynamic>?)
?.map((e) => Service.fromJson(e as Map<String, dynamic>))
.toList() ??
const <Service>[],
error: json['error'] as String?,
);
Map<String, dynamic> _$AggregatedNodeStatusToJson(
_AggregatedNodeStatus instance,
) => <String, dynamic>{
'node': instance.node.toJson(),
'services': instance.services.map((e) => e.toJson()).toList(),
'error': instance.error,
};
+25
View File
@@ -0,0 +1,25 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'backup_execution.dart';
import 'backup_target.dart';
part 'backup_config.freezed.dart';
part 'backup_config.g.dart';
/// Mirrors `models.BackupConfig`. Used both at node level (`GET /node/backup`)
/// and per-service (`Service.backup`). `nextRun`/`lastRun` here are
/// aggregates (earliest/latest) across `targets` — each target now tracks
/// its own `schedule`/`lastRun`/`nextRun` too. There's no `frequency` field
/// on the Go side anymore; scheduling moved to a per-target cron `schedule`.
@freezed
sealed class BackupConfig with _$BackupConfig {
const factory BackupConfig({
@JsonKey(name: 'source_path') String? sourcePath,
@Default(<BackupTarget>[]) List<BackupTarget> targets,
@JsonKey(name: 'next_run') DateTime? nextRun,
@JsonKey(name: 'last_run') DateTime? lastRun,
@Default(<BackupExecution>[]) List<BackupExecution> history,
}) = _BackupConfig;
factory BackupConfig.fromJson(Map<String, dynamic> json) => _$BackupConfigFromJson(json);
}
+295
View File
@@ -0,0 +1,295 @@
// 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 'backup_config.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BackupConfig {
@JsonKey(name: 'source_path') String? get sourcePath; List<BackupTarget> get targets;@JsonKey(name: 'next_run') DateTime? get nextRun;@JsonKey(name: 'last_run') DateTime? get lastRun; List<BackupExecution> get history;
/// Create a copy of BackupConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BackupConfigCopyWith<BackupConfig> get copyWith => _$BackupConfigCopyWithImpl<BackupConfig>(this as BackupConfig, _$identity);
/// Serializes this BackupConfig to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupConfig&&(identical(other.sourcePath, sourcePath) || other.sourcePath == sourcePath)&&const DeepCollectionEquality().equals(other.targets, targets)&&(identical(other.nextRun, nextRun) || other.nextRun == nextRun)&&(identical(other.lastRun, lastRun) || other.lastRun == lastRun)&&const DeepCollectionEquality().equals(other.history, history));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,sourcePath,const DeepCollectionEquality().hash(targets),nextRun,lastRun,const DeepCollectionEquality().hash(history));
@override
String toString() {
return 'BackupConfig(sourcePath: $sourcePath, targets: $targets, nextRun: $nextRun, lastRun: $lastRun, history: $history)';
}
}
/// @nodoc
abstract mixin class $BackupConfigCopyWith<$Res> {
factory $BackupConfigCopyWith(BackupConfig value, $Res Function(BackupConfig) _then) = _$BackupConfigCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'source_path') String? sourcePath, List<BackupTarget> targets,@JsonKey(name: 'next_run') DateTime? nextRun,@JsonKey(name: 'last_run') DateTime? lastRun, List<BackupExecution> history
});
}
/// @nodoc
class _$BackupConfigCopyWithImpl<$Res>
implements $BackupConfigCopyWith<$Res> {
_$BackupConfigCopyWithImpl(this._self, this._then);
final BackupConfig _self;
final $Res Function(BackupConfig) _then;
/// Create a copy of BackupConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? sourcePath = freezed,Object? targets = null,Object? nextRun = freezed,Object? lastRun = freezed,Object? history = null,}) {
return _then(_self.copyWith(
sourcePath: freezed == sourcePath ? _self.sourcePath : sourcePath // ignore: cast_nullable_to_non_nullable
as String?,targets: null == targets ? _self.targets : targets // ignore: cast_nullable_to_non_nullable
as List<BackupTarget>,nextRun: freezed == nextRun ? _self.nextRun : nextRun // ignore: cast_nullable_to_non_nullable
as DateTime?,lastRun: freezed == lastRun ? _self.lastRun : lastRun // ignore: cast_nullable_to_non_nullable
as DateTime?,history: null == history ? _self.history : history // ignore: cast_nullable_to_non_nullable
as List<BackupExecution>,
));
}
}
/// Adds pattern-matching-related methods to [BackupConfig].
extension BackupConfigPatterns on BackupConfig {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BackupConfig value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _BackupConfig() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BackupConfig value) $default,){
final _that = this;
switch (_that) {
case _BackupConfig():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BackupConfig value)? $default,){
final _that = this;
switch (_that) {
case _BackupConfig() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'source_path') String? sourcePath, List<BackupTarget> targets, @JsonKey(name: 'next_run') DateTime? nextRun, @JsonKey(name: 'last_run') DateTime? lastRun, List<BackupExecution> history)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _BackupConfig() when $default != null:
return $default(_that.sourcePath,_that.targets,_that.nextRun,_that.lastRun,_that.history);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'source_path') String? sourcePath, List<BackupTarget> targets, @JsonKey(name: 'next_run') DateTime? nextRun, @JsonKey(name: 'last_run') DateTime? lastRun, List<BackupExecution> history) $default,) {final _that = this;
switch (_that) {
case _BackupConfig():
return $default(_that.sourcePath,_that.targets,_that.nextRun,_that.lastRun,_that.history);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'source_path') String? sourcePath, List<BackupTarget> targets, @JsonKey(name: 'next_run') DateTime? nextRun, @JsonKey(name: 'last_run') DateTime? lastRun, List<BackupExecution> history)? $default,) {final _that = this;
switch (_that) {
case _BackupConfig() when $default != null:
return $default(_that.sourcePath,_that.targets,_that.nextRun,_that.lastRun,_that.history);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _BackupConfig implements BackupConfig {
const _BackupConfig({@JsonKey(name: 'source_path') this.sourcePath, final List<BackupTarget> targets = const <BackupTarget>[], @JsonKey(name: 'next_run') this.nextRun, @JsonKey(name: 'last_run') this.lastRun, final List<BackupExecution> history = const <BackupExecution>[]}): _targets = targets,_history = history;
factory _BackupConfig.fromJson(Map<String, dynamic> json) => _$BackupConfigFromJson(json);
@override@JsonKey(name: 'source_path') final String? sourcePath;
final List<BackupTarget> _targets;
@override@JsonKey() List<BackupTarget> get targets {
if (_targets is EqualUnmodifiableListView) return _targets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_targets);
}
@override@JsonKey(name: 'next_run') final DateTime? nextRun;
@override@JsonKey(name: 'last_run') final DateTime? lastRun;
final List<BackupExecution> _history;
@override@JsonKey() List<BackupExecution> get history {
if (_history is EqualUnmodifiableListView) return _history;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_history);
}
/// Create a copy of BackupConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$BackupConfigCopyWith<_BackupConfig> get copyWith => __$BackupConfigCopyWithImpl<_BackupConfig>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$BackupConfigToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupConfig&&(identical(other.sourcePath, sourcePath) || other.sourcePath == sourcePath)&&const DeepCollectionEquality().equals(other._targets, _targets)&&(identical(other.nextRun, nextRun) || other.nextRun == nextRun)&&(identical(other.lastRun, lastRun) || other.lastRun == lastRun)&&const DeepCollectionEquality().equals(other._history, _history));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,sourcePath,const DeepCollectionEquality().hash(_targets),nextRun,lastRun,const DeepCollectionEquality().hash(_history));
@override
String toString() {
return 'BackupConfig(sourcePath: $sourcePath, targets: $targets, nextRun: $nextRun, lastRun: $lastRun, history: $history)';
}
}
/// @nodoc
abstract mixin class _$BackupConfigCopyWith<$Res> implements $BackupConfigCopyWith<$Res> {
factory _$BackupConfigCopyWith(_BackupConfig value, $Res Function(_BackupConfig) _then) = __$BackupConfigCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'source_path') String? sourcePath, List<BackupTarget> targets,@JsonKey(name: 'next_run') DateTime? nextRun,@JsonKey(name: 'last_run') DateTime? lastRun, List<BackupExecution> history
});
}
/// @nodoc
class __$BackupConfigCopyWithImpl<$Res>
implements _$BackupConfigCopyWith<$Res> {
__$BackupConfigCopyWithImpl(this._self, this._then);
final _BackupConfig _self;
final $Res Function(_BackupConfig) _then;
/// Create a copy of BackupConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? sourcePath = freezed,Object? targets = null,Object? nextRun = freezed,Object? lastRun = freezed,Object? history = null,}) {
return _then(_BackupConfig(
sourcePath: freezed == sourcePath ? _self.sourcePath : sourcePath // ignore: cast_nullable_to_non_nullable
as String?,targets: null == targets ? _self._targets : targets // ignore: cast_nullable_to_non_nullable
as List<BackupTarget>,nextRun: freezed == nextRun ? _self.nextRun : nextRun // ignore: cast_nullable_to_non_nullable
as DateTime?,lastRun: freezed == lastRun ? _self.lastRun : lastRun // ignore: cast_nullable_to_non_nullable
as DateTime?,history: null == history ? _self._history : history // ignore: cast_nullable_to_non_nullable
as List<BackupExecution>,
));
}
}
// dart format on
+37
View File
@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup_config.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_BackupConfig _$BackupConfigFromJson(Map<String, dynamic> json) =>
_BackupConfig(
sourcePath: json['source_path'] as String?,
targets:
(json['targets'] as List<dynamic>?)
?.map((e) => BackupTarget.fromJson(e as Map<String, dynamic>))
.toList() ??
const <BackupTarget>[],
nextRun: json['next_run'] == null
? null
: DateTime.parse(json['next_run'] as String),
lastRun: json['last_run'] == null
? null
: DateTime.parse(json['last_run'] as String),
history:
(json['history'] as List<dynamic>?)
?.map((e) => BackupExecution.fromJson(e as Map<String, dynamic>))
.toList() ??
const <BackupExecution>[],
);
Map<String, dynamic> _$BackupConfigToJson(_BackupConfig instance) =>
<String, dynamic>{
'source_path': instance.sourcePath,
'targets': instance.targets.map((e) => e.toJson()).toList(),
'next_run': instance.nextRun?.toIso8601String(),
'last_run': instance.lastRun?.toIso8601String(),
'history': instance.history.map((e) => e.toJson()).toList(),
};
+19
View File
@@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'backup_execution.freezed.dart';
part 'backup_execution.g.dart';
/// Mirrors `models.BackupExecution`.
@freezed
sealed class BackupExecution with _$BackupExecution {
const factory BackupExecution({
required String id,
required DateTime timestamp,
required bool success,
@JsonKey(name: 'duration_seconds') required int durationSeconds,
@Default('') String message,
@JsonKey(name: 'target_id') String? targetId,
}) = _BackupExecution;
factory BackupExecution.fromJson(Map<String, dynamic> json) => _$BackupExecutionFromJson(json);
}
@@ -0,0 +1,286 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'backup_execution.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BackupExecution {
String get id; DateTime get timestamp; bool get success;@JsonKey(name: 'duration_seconds') int get durationSeconds; String get message;@JsonKey(name: 'target_id') String? get targetId;
/// Create a copy of BackupExecution
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BackupExecutionCopyWith<BackupExecution> get copyWith => _$BackupExecutionCopyWithImpl<BackupExecution>(this as BackupExecution, _$identity);
/// Serializes this BackupExecution to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupExecution&&(identical(other.id, id) || other.id == id)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.success, success) || other.success == success)&&(identical(other.durationSeconds, durationSeconds) || other.durationSeconds == durationSeconds)&&(identical(other.message, message) || other.message == message)&&(identical(other.targetId, targetId) || other.targetId == targetId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,timestamp,success,durationSeconds,message,targetId);
@override
String toString() {
return 'BackupExecution(id: $id, timestamp: $timestamp, success: $success, durationSeconds: $durationSeconds, message: $message, targetId: $targetId)';
}
}
/// @nodoc
abstract mixin class $BackupExecutionCopyWith<$Res> {
factory $BackupExecutionCopyWith(BackupExecution value, $Res Function(BackupExecution) _then) = _$BackupExecutionCopyWithImpl;
@useResult
$Res call({
String id, DateTime timestamp, bool success,@JsonKey(name: 'duration_seconds') int durationSeconds, String message,@JsonKey(name: 'target_id') String? targetId
});
}
/// @nodoc
class _$BackupExecutionCopyWithImpl<$Res>
implements $BackupExecutionCopyWith<$Res> {
_$BackupExecutionCopyWithImpl(this._self, this._then);
final BackupExecution _self;
final $Res Function(BackupExecution) _then;
/// Create a copy of BackupExecution
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? timestamp = null,Object? success = null,Object? durationSeconds = null,Object? message = null,Object? targetId = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,success: null == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool,durationSeconds: null == durationSeconds ? _self.durationSeconds : durationSeconds // ignore: cast_nullable_to_non_nullable
as int,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,targetId: freezed == targetId ? _self.targetId : targetId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [BackupExecution].
extension BackupExecutionPatterns on BackupExecution {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BackupExecution value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _BackupExecution() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BackupExecution value) $default,){
final _that = this;
switch (_that) {
case _BackupExecution():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BackupExecution value)? $default,){
final _that = this;
switch (_that) {
case _BackupExecution() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime timestamp, bool success, @JsonKey(name: 'duration_seconds') int durationSeconds, String message, @JsonKey(name: 'target_id') String? targetId)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _BackupExecution() when $default != null:
return $default(_that.id,_that.timestamp,_that.success,_that.durationSeconds,_that.message,_that.targetId);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime timestamp, bool success, @JsonKey(name: 'duration_seconds') int durationSeconds, String message, @JsonKey(name: 'target_id') String? targetId) $default,) {final _that = this;
switch (_that) {
case _BackupExecution():
return $default(_that.id,_that.timestamp,_that.success,_that.durationSeconds,_that.message,_that.targetId);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime timestamp, bool success, @JsonKey(name: 'duration_seconds') int durationSeconds, String message, @JsonKey(name: 'target_id') String? targetId)? $default,) {final _that = this;
switch (_that) {
case _BackupExecution() when $default != null:
return $default(_that.id,_that.timestamp,_that.success,_that.durationSeconds,_that.message,_that.targetId);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _BackupExecution implements BackupExecution {
const _BackupExecution({required this.id, required this.timestamp, required this.success, @JsonKey(name: 'duration_seconds') required this.durationSeconds, this.message = '', @JsonKey(name: 'target_id') this.targetId});
factory _BackupExecution.fromJson(Map<String, dynamic> json) => _$BackupExecutionFromJson(json);
@override final String id;
@override final DateTime timestamp;
@override final bool success;
@override@JsonKey(name: 'duration_seconds') final int durationSeconds;
@override@JsonKey() final String message;
@override@JsonKey(name: 'target_id') final String? targetId;
/// Create a copy of BackupExecution
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$BackupExecutionCopyWith<_BackupExecution> get copyWith => __$BackupExecutionCopyWithImpl<_BackupExecution>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$BackupExecutionToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupExecution&&(identical(other.id, id) || other.id == id)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.success, success) || other.success == success)&&(identical(other.durationSeconds, durationSeconds) || other.durationSeconds == durationSeconds)&&(identical(other.message, message) || other.message == message)&&(identical(other.targetId, targetId) || other.targetId == targetId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,timestamp,success,durationSeconds,message,targetId);
@override
String toString() {
return 'BackupExecution(id: $id, timestamp: $timestamp, success: $success, durationSeconds: $durationSeconds, message: $message, targetId: $targetId)';
}
}
/// @nodoc
abstract mixin class _$BackupExecutionCopyWith<$Res> implements $BackupExecutionCopyWith<$Res> {
factory _$BackupExecutionCopyWith(_BackupExecution value, $Res Function(_BackupExecution) _then) = __$BackupExecutionCopyWithImpl;
@override @useResult
$Res call({
String id, DateTime timestamp, bool success,@JsonKey(name: 'duration_seconds') int durationSeconds, String message,@JsonKey(name: 'target_id') String? targetId
});
}
/// @nodoc
class __$BackupExecutionCopyWithImpl<$Res>
implements _$BackupExecutionCopyWith<$Res> {
__$BackupExecutionCopyWithImpl(this._self, this._then);
final _BackupExecution _self;
final $Res Function(_BackupExecution) _then;
/// Create a copy of BackupExecution
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? timestamp = null,Object? success = null,Object? durationSeconds = null,Object? message = null,Object? targetId = freezed,}) {
return _then(_BackupExecution(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,success: null == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool,durationSeconds: null == durationSeconds ? _self.durationSeconds : durationSeconds // ignore: cast_nullable_to_non_nullable
as int,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,targetId: freezed == targetId ? _self.targetId : targetId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on
+27
View File
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup_execution.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_BackupExecution _$BackupExecutionFromJson(Map<String, dynamic> json) =>
_BackupExecution(
id: json['id'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
success: json['success'] as bool,
durationSeconds: (json['duration_seconds'] as num).toInt(),
message: json['message'] as String? ?? '',
targetId: json['target_id'] as String?,
);
Map<String, dynamic> _$BackupExecutionToJson(_BackupExecution instance) =>
<String, dynamic>{
'id': instance.id,
'timestamp': instance.timestamp.toIso8601String(),
'success': instance.success,
'duration_seconds': instance.durationSeconds,
'message': instance.message,
'target_id': instance.targetId,
};
+11
View File
@@ -0,0 +1,11 @@
import 'package:json_annotation/json_annotation.dart';
/// Mirrors `models.BackupMethod` in `nodemaster/models/types.go`.
enum BackupMethod {
@JsonValue('rsync')
rsync,
@JsonValue('restic')
restic,
@JsonValue('external')
external,
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'backup_method.dart';
part 'backup_target.freezed.dart';
part 'backup_target.g.dart';
/// Mirrors `models.BackupTarget`. `schedule` is a standard 5-field cron
/// expression; empty means manual-only (no automatic scheduling).
@freezed
sealed class BackupTarget with _$BackupTarget {
const factory BackupTarget({
@Default('') String id,
required String name,
required BackupMethod method,
required String remote,
@Default(<String, String>{}) Map<String, String> params,
String? schedule,
@JsonKey(name: 'last_run') DateTime? lastRun,
@JsonKey(name: 'next_run') DateTime? nextRun,
}) = _BackupTarget;
factory BackupTarget.fromJson(Map<String, dynamic> json) => _$BackupTargetFromJson(json);
}
+298
View File
@@ -0,0 +1,298 @@
// 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 'backup_target.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BackupTarget {
String get id; String get name; BackupMethod get method; String get remote; Map<String, String> get params; String? get schedule;@JsonKey(name: 'last_run') DateTime? get lastRun;@JsonKey(name: 'next_run') DateTime? get nextRun;
/// Create a copy of BackupTarget
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BackupTargetCopyWith<BackupTarget> get copyWith => _$BackupTargetCopyWithImpl<BackupTarget>(this as BackupTarget, _$identity);
/// Serializes this BackupTarget to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BackupTarget&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.method, method) || other.method == method)&&(identical(other.remote, remote) || other.remote == remote)&&const DeepCollectionEquality().equals(other.params, params)&&(identical(other.schedule, schedule) || other.schedule == schedule)&&(identical(other.lastRun, lastRun) || other.lastRun == lastRun)&&(identical(other.nextRun, nextRun) || other.nextRun == nextRun));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,method,remote,const DeepCollectionEquality().hash(params),schedule,lastRun,nextRun);
@override
String toString() {
return 'BackupTarget(id: $id, name: $name, method: $method, remote: $remote, params: $params, schedule: $schedule, lastRun: $lastRun, nextRun: $nextRun)';
}
}
/// @nodoc
abstract mixin class $BackupTargetCopyWith<$Res> {
factory $BackupTargetCopyWith(BackupTarget value, $Res Function(BackupTarget) _then) = _$BackupTargetCopyWithImpl;
@useResult
$Res call({
String id, String name, BackupMethod method, String remote, Map<String, String> params, String? schedule,@JsonKey(name: 'last_run') DateTime? lastRun,@JsonKey(name: 'next_run') DateTime? nextRun
});
}
/// @nodoc
class _$BackupTargetCopyWithImpl<$Res>
implements $BackupTargetCopyWith<$Res> {
_$BackupTargetCopyWithImpl(this._self, this._then);
final BackupTarget _self;
final $Res Function(BackupTarget) _then;
/// Create a copy of BackupTarget
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? method = null,Object? remote = null,Object? params = null,Object? schedule = freezed,Object? lastRun = freezed,Object? nextRun = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,method: null == method ? _self.method : method // ignore: cast_nullable_to_non_nullable
as BackupMethod,remote: null == remote ? _self.remote : remote // ignore: cast_nullable_to_non_nullable
as String,params: null == params ? _self.params : params // ignore: cast_nullable_to_non_nullable
as Map<String, String>,schedule: freezed == schedule ? _self.schedule : schedule // ignore: cast_nullable_to_non_nullable
as String?,lastRun: freezed == lastRun ? _self.lastRun : lastRun // ignore: cast_nullable_to_non_nullable
as DateTime?,nextRun: freezed == nextRun ? _self.nextRun : nextRun // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [BackupTarget].
extension BackupTargetPatterns on BackupTarget {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BackupTarget value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _BackupTarget() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BackupTarget value) $default,){
final _that = this;
switch (_that) {
case _BackupTarget():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BackupTarget value)? $default,){
final _that = this;
switch (_that) {
case _BackupTarget() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, BackupMethod method, String remote, Map<String, String> params, String? schedule, @JsonKey(name: 'last_run') DateTime? lastRun, @JsonKey(name: 'next_run') DateTime? nextRun)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _BackupTarget() when $default != null:
return $default(_that.id,_that.name,_that.method,_that.remote,_that.params,_that.schedule,_that.lastRun,_that.nextRun);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, BackupMethod method, String remote, Map<String, String> params, String? schedule, @JsonKey(name: 'last_run') DateTime? lastRun, @JsonKey(name: 'next_run') DateTime? nextRun) $default,) {final _that = this;
switch (_that) {
case _BackupTarget():
return $default(_that.id,_that.name,_that.method,_that.remote,_that.params,_that.schedule,_that.lastRun,_that.nextRun);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, BackupMethod method, String remote, Map<String, String> params, String? schedule, @JsonKey(name: 'last_run') DateTime? lastRun, @JsonKey(name: 'next_run') DateTime? nextRun)? $default,) {final _that = this;
switch (_that) {
case _BackupTarget() when $default != null:
return $default(_that.id,_that.name,_that.method,_that.remote,_that.params,_that.schedule,_that.lastRun,_that.nextRun);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _BackupTarget implements BackupTarget {
const _BackupTarget({this.id = '', required this.name, required this.method, required this.remote, final Map<String, String> params = const <String, String>{}, this.schedule, @JsonKey(name: 'last_run') this.lastRun, @JsonKey(name: 'next_run') this.nextRun}): _params = params;
factory _BackupTarget.fromJson(Map<String, dynamic> json) => _$BackupTargetFromJson(json);
@override@JsonKey() final String id;
@override final String name;
@override final BackupMethod method;
@override final String remote;
final Map<String, String> _params;
@override@JsonKey() Map<String, String> get params {
if (_params is EqualUnmodifiableMapView) return _params;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_params);
}
@override final String? schedule;
@override@JsonKey(name: 'last_run') final DateTime? lastRun;
@override@JsonKey(name: 'next_run') final DateTime? nextRun;
/// Create a copy of BackupTarget
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$BackupTargetCopyWith<_BackupTarget> get copyWith => __$BackupTargetCopyWithImpl<_BackupTarget>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$BackupTargetToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BackupTarget&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.method, method) || other.method == method)&&(identical(other.remote, remote) || other.remote == remote)&&const DeepCollectionEquality().equals(other._params, _params)&&(identical(other.schedule, schedule) || other.schedule == schedule)&&(identical(other.lastRun, lastRun) || other.lastRun == lastRun)&&(identical(other.nextRun, nextRun) || other.nextRun == nextRun));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,method,remote,const DeepCollectionEquality().hash(_params),schedule,lastRun,nextRun);
@override
String toString() {
return 'BackupTarget(id: $id, name: $name, method: $method, remote: $remote, params: $params, schedule: $schedule, lastRun: $lastRun, nextRun: $nextRun)';
}
}
/// @nodoc
abstract mixin class _$BackupTargetCopyWith<$Res> implements $BackupTargetCopyWith<$Res> {
factory _$BackupTargetCopyWith(_BackupTarget value, $Res Function(_BackupTarget) _then) = __$BackupTargetCopyWithImpl;
@override @useResult
$Res call({
String id, String name, BackupMethod method, String remote, Map<String, String> params, String? schedule,@JsonKey(name: 'last_run') DateTime? lastRun,@JsonKey(name: 'next_run') DateTime? nextRun
});
}
/// @nodoc
class __$BackupTargetCopyWithImpl<$Res>
implements _$BackupTargetCopyWith<$Res> {
__$BackupTargetCopyWithImpl(this._self, this._then);
final _BackupTarget _self;
final $Res Function(_BackupTarget) _then;
/// Create a copy of BackupTarget
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? method = null,Object? remote = null,Object? params = null,Object? schedule = freezed,Object? lastRun = freezed,Object? nextRun = freezed,}) {
return _then(_BackupTarget(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,method: null == method ? _self.method : method // ignore: cast_nullable_to_non_nullable
as BackupMethod,remote: null == remote ? _self.remote : remote // ignore: cast_nullable_to_non_nullable
as String,params: null == params ? _self._params : params // ignore: cast_nullable_to_non_nullable
as Map<String, String>,schedule: freezed == schedule ? _self.schedule : schedule // ignore: cast_nullable_to_non_nullable
as String?,lastRun: freezed == lastRun ? _self.lastRun : lastRun // ignore: cast_nullable_to_non_nullable
as DateTime?,nextRun: freezed == nextRun ? _self.nextRun : nextRun // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on
+45
View File
@@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup_target.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_BackupTarget _$BackupTargetFromJson(Map<String, dynamic> json) =>
_BackupTarget(
id: json['id'] as String? ?? '',
name: json['name'] as String,
method: $enumDecode(_$BackupMethodEnumMap, json['method']),
remote: json['remote'] as String,
params:
(json['params'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
const <String, String>{},
schedule: json['schedule'] as String?,
lastRun: json['last_run'] == null
? null
: DateTime.parse(json['last_run'] as String),
nextRun: json['next_run'] == null
? null
: DateTime.parse(json['next_run'] as String),
);
Map<String, dynamic> _$BackupTargetToJson(_BackupTarget instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'method': _$BackupMethodEnumMap[instance.method]!,
'remote': instance.remote,
'params': instance.params,
'schedule': instance.schedule,
'last_run': instance.lastRun?.toIso8601String(),
'next_run': instance.nextRun?.toIso8601String(),
};
const _$BackupMethodEnumMap = {
BackupMethod.rsync: 'rsync',
BackupMethod.restic: 'restic',
BackupMethod.external: 'external',
};
+12
View File
@@ -0,0 +1,12 @@
export 'aggregated_node_status.dart';
export 'backup_config.dart';
export 'backup_execution.dart';
export 'backup_method.dart';
export 'backup_target.dart';
export 'node_info.dart';
export 'remote_node.dart';
export 'run_state.dart';
export 'scan_result.dart';
export 'service.dart';
export 'service_scan_info.dart';
export 'update_record.dart';
+30
View File
@@ -0,0 +1,30 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'backup_config.dart';
import 'update_record.dart';
part 'node_info.freezed.dart';
part 'node_info.g.dart';
/// Mirrors `models.NodeInfo` — the local/current host's identity + backup
/// config + update log, as served by `GET /node/`.
///
/// `resticPassword` is write-only: the API never echoes the real password
/// back (`GET /node/` sends `restic_password_set` instead, see
/// `controllers/node.go`), so this stays `null` unless the user is actively
/// setting/changing it in the identity form, and is omitted from the PUT
/// body entirely when `null` so an untouched form never clobbers the
/// existing password (mirrors the Go side's "empty string = don't change").
@freezed
sealed class NodeInfo with _$NodeInfo {
const factory NodeInfo({
required String hostname,
String? description,
@JsonKey(name: 'update_history') @Default(<UpdateRecord>[]) List<UpdateRecord> updateHistory,
required BackupConfig backup,
@JsonKey(name: 'restic_password_set') @Default(false) bool resticPasswordSet,
@JsonKey(name: 'restic_password', includeIfNull: false) String? resticPassword,
}) = _NodeInfo;
factory NodeInfo.fromJson(Map<String, dynamic> json) => _$NodeInfoFromJson(json);
}
+310
View File
@@ -0,0 +1,310 @@
// 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 'node_info.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$NodeInfo {
String get hostname; String? get description;@JsonKey(name: 'update_history') List<UpdateRecord> get updateHistory; BackupConfig get backup;@JsonKey(name: 'restic_password_set') bool get resticPasswordSet;@JsonKey(name: 'restic_password', includeIfNull: false) String? get resticPassword;
/// Create a copy of NodeInfo
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$NodeInfoCopyWith<NodeInfo> get copyWith => _$NodeInfoCopyWithImpl<NodeInfo>(this as NodeInfo, _$identity);
/// Serializes this NodeInfo to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is NodeInfo&&(identical(other.hostname, hostname) || other.hostname == hostname)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.updateHistory, updateHistory)&&(identical(other.backup, backup) || other.backup == backup)&&(identical(other.resticPasswordSet, resticPasswordSet) || other.resticPasswordSet == resticPasswordSet)&&(identical(other.resticPassword, resticPassword) || other.resticPassword == resticPassword));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,hostname,description,const DeepCollectionEquality().hash(updateHistory),backup,resticPasswordSet,resticPassword);
@override
String toString() {
return 'NodeInfo(hostname: $hostname, description: $description, updateHistory: $updateHistory, backup: $backup, resticPasswordSet: $resticPasswordSet, resticPassword: $resticPassword)';
}
}
/// @nodoc
abstract mixin class $NodeInfoCopyWith<$Res> {
factory $NodeInfoCopyWith(NodeInfo value, $Res Function(NodeInfo) _then) = _$NodeInfoCopyWithImpl;
@useResult
$Res call({
String hostname, String? description,@JsonKey(name: 'update_history') List<UpdateRecord> updateHistory, BackupConfig backup,@JsonKey(name: 'restic_password_set') bool resticPasswordSet,@JsonKey(name: 'restic_password', includeIfNull: false) String? resticPassword
});
$BackupConfigCopyWith<$Res> get backup;
}
/// @nodoc
class _$NodeInfoCopyWithImpl<$Res>
implements $NodeInfoCopyWith<$Res> {
_$NodeInfoCopyWithImpl(this._self, this._then);
final NodeInfo _self;
final $Res Function(NodeInfo) _then;
/// Create a copy of NodeInfo
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? hostname = null,Object? description = freezed,Object? updateHistory = null,Object? backup = null,Object? resticPasswordSet = null,Object? resticPassword = freezed,}) {
return _then(_self.copyWith(
hostname: null == hostname ? _self.hostname : hostname // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,updateHistory: null == updateHistory ? _self.updateHistory : updateHistory // ignore: cast_nullable_to_non_nullable
as List<UpdateRecord>,backup: null == backup ? _self.backup : backup // ignore: cast_nullable_to_non_nullable
as BackupConfig,resticPasswordSet: null == resticPasswordSet ? _self.resticPasswordSet : resticPasswordSet // ignore: cast_nullable_to_non_nullable
as bool,resticPassword: freezed == resticPassword ? _self.resticPassword : resticPassword // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of NodeInfo
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$BackupConfigCopyWith<$Res> get backup {
return $BackupConfigCopyWith<$Res>(_self.backup, (value) {
return _then(_self.copyWith(backup: value));
});
}
}
/// Adds pattern-matching-related methods to [NodeInfo].
extension NodeInfoPatterns on NodeInfo {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _NodeInfo value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _NodeInfo() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _NodeInfo value) $default,){
final _that = this;
switch (_that) {
case _NodeInfo():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _NodeInfo value)? $default,){
final _that = this;
switch (_that) {
case _NodeInfo() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String hostname, String? description, @JsonKey(name: 'update_history') List<UpdateRecord> updateHistory, BackupConfig backup, @JsonKey(name: 'restic_password_set') bool resticPasswordSet, @JsonKey(name: 'restic_password', includeIfNull: false) String? resticPassword)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _NodeInfo() when $default != null:
return $default(_that.hostname,_that.description,_that.updateHistory,_that.backup,_that.resticPasswordSet,_that.resticPassword);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String hostname, String? description, @JsonKey(name: 'update_history') List<UpdateRecord> updateHistory, BackupConfig backup, @JsonKey(name: 'restic_password_set') bool resticPasswordSet, @JsonKey(name: 'restic_password', includeIfNull: false) String? resticPassword) $default,) {final _that = this;
switch (_that) {
case _NodeInfo():
return $default(_that.hostname,_that.description,_that.updateHistory,_that.backup,_that.resticPasswordSet,_that.resticPassword);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String hostname, String? description, @JsonKey(name: 'update_history') List<UpdateRecord> updateHistory, BackupConfig backup, @JsonKey(name: 'restic_password_set') bool resticPasswordSet, @JsonKey(name: 'restic_password', includeIfNull: false) String? resticPassword)? $default,) {final _that = this;
switch (_that) {
case _NodeInfo() when $default != null:
return $default(_that.hostname,_that.description,_that.updateHistory,_that.backup,_that.resticPasswordSet,_that.resticPassword);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _NodeInfo implements NodeInfo {
const _NodeInfo({required this.hostname, this.description, @JsonKey(name: 'update_history') final List<UpdateRecord> updateHistory = const <UpdateRecord>[], required this.backup, @JsonKey(name: 'restic_password_set') this.resticPasswordSet = false, @JsonKey(name: 'restic_password', includeIfNull: false) this.resticPassword}): _updateHistory = updateHistory;
factory _NodeInfo.fromJson(Map<String, dynamic> json) => _$NodeInfoFromJson(json);
@override final String hostname;
@override final String? description;
final List<UpdateRecord> _updateHistory;
@override@JsonKey(name: 'update_history') List<UpdateRecord> get updateHistory {
if (_updateHistory is EqualUnmodifiableListView) return _updateHistory;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_updateHistory);
}
@override final BackupConfig backup;
@override@JsonKey(name: 'restic_password_set') final bool resticPasswordSet;
@override@JsonKey(name: 'restic_password', includeIfNull: false) final String? resticPassword;
/// Create a copy of NodeInfo
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$NodeInfoCopyWith<_NodeInfo> get copyWith => __$NodeInfoCopyWithImpl<_NodeInfo>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$NodeInfoToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NodeInfo&&(identical(other.hostname, hostname) || other.hostname == hostname)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._updateHistory, _updateHistory)&&(identical(other.backup, backup) || other.backup == backup)&&(identical(other.resticPasswordSet, resticPasswordSet) || other.resticPasswordSet == resticPasswordSet)&&(identical(other.resticPassword, resticPassword) || other.resticPassword == resticPassword));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,hostname,description,const DeepCollectionEquality().hash(_updateHistory),backup,resticPasswordSet,resticPassword);
@override
String toString() {
return 'NodeInfo(hostname: $hostname, description: $description, updateHistory: $updateHistory, backup: $backup, resticPasswordSet: $resticPasswordSet, resticPassword: $resticPassword)';
}
}
/// @nodoc
abstract mixin class _$NodeInfoCopyWith<$Res> implements $NodeInfoCopyWith<$Res> {
factory _$NodeInfoCopyWith(_NodeInfo value, $Res Function(_NodeInfo) _then) = __$NodeInfoCopyWithImpl;
@override @useResult
$Res call({
String hostname, String? description,@JsonKey(name: 'update_history') List<UpdateRecord> updateHistory, BackupConfig backup,@JsonKey(name: 'restic_password_set') bool resticPasswordSet,@JsonKey(name: 'restic_password', includeIfNull: false) String? resticPassword
});
@override $BackupConfigCopyWith<$Res> get backup;
}
/// @nodoc
class __$NodeInfoCopyWithImpl<$Res>
implements _$NodeInfoCopyWith<$Res> {
__$NodeInfoCopyWithImpl(this._self, this._then);
final _NodeInfo _self;
final $Res Function(_NodeInfo) _then;
/// Create a copy of NodeInfo
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? hostname = null,Object? description = freezed,Object? updateHistory = null,Object? backup = null,Object? resticPasswordSet = null,Object? resticPassword = freezed,}) {
return _then(_NodeInfo(
hostname: null == hostname ? _self.hostname : hostname // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,updateHistory: null == updateHistory ? _self._updateHistory : updateHistory // ignore: cast_nullable_to_non_nullable
as List<UpdateRecord>,backup: null == backup ? _self.backup : backup // ignore: cast_nullable_to_non_nullable
as BackupConfig,resticPasswordSet: null == resticPasswordSet ? _self.resticPasswordSet : resticPasswordSet // ignore: cast_nullable_to_non_nullable
as bool,resticPassword: freezed == resticPassword ? _self.resticPassword : resticPassword // ignore: cast_nullable_to_non_nullable
as String?,
));
}
/// Create a copy of NodeInfo
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$BackupConfigCopyWith<$Res> get backup {
return $BackupConfigCopyWith<$Res>(_self.backup, (value) {
return _then(_self.copyWith(backup: value));
});
}
}
// dart format on
+29
View File
@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'node_info.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_NodeInfo _$NodeInfoFromJson(Map<String, dynamic> json) => _NodeInfo(
hostname: json['hostname'] as String,
description: json['description'] as String?,
updateHistory:
(json['update_history'] as List<dynamic>?)
?.map((e) => UpdateRecord.fromJson(e as Map<String, dynamic>))
.toList() ??
const <UpdateRecord>[],
backup: BackupConfig.fromJson(json['backup'] as Map<String, dynamic>),
resticPasswordSet: json['restic_password_set'] as bool? ?? false,
resticPassword: json['restic_password'] as String?,
);
Map<String, dynamic> _$NodeInfoToJson(_NodeInfo instance) => <String, dynamic>{
'hostname': instance.hostname,
'description': instance.description,
'update_history': instance.updateHistory.map((e) => e.toJson()).toList(),
'backup': instance.backup.toJson(),
'restic_password_set': instance.resticPasswordSet,
'restic_password': ?instance.resticPassword,
};
+21
View File
@@ -0,0 +1,21 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'remote_node.freezed.dart';
part 'remote_node.g.dart';
/// Mirrors `models.RemoteNode` — an entry in this node's registry of
/// sibling NodeMaster hosts (`/nodes/*`).
@freezed
sealed class RemoteNode with _$RemoteNode {
const factory RemoteNode({
@Default('') String id,
required String name,
required String url,
@Default('') String status,
String? description,
@Default(<String>[]) List<String> tags,
@JsonKey(name: 'last_seen') DateTime? lastSeen,
}) = _RemoteNode;
factory RemoteNode.fromJson(Map<String, dynamic> json) => _$RemoteNodeFromJson(json);
}
+295
View File
@@ -0,0 +1,295 @@
// 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 'remote_node.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RemoteNode {
String get id; String get name; String get url; String get status; String? get description; List<String> get tags;@JsonKey(name: 'last_seen') DateTime? get lastSeen;
/// Create a copy of RemoteNode
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RemoteNodeCopyWith<RemoteNode> get copyWith => _$RemoteNodeCopyWithImpl<RemoteNode>(this as RemoteNode, _$identity);
/// Serializes this RemoteNode to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RemoteNode&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url)&&(identical(other.status, status) || other.status == status)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.lastSeen, lastSeen) || other.lastSeen == lastSeen));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,url,status,description,const DeepCollectionEquality().hash(tags),lastSeen);
@override
String toString() {
return 'RemoteNode(id: $id, name: $name, url: $url, status: $status, description: $description, tags: $tags, lastSeen: $lastSeen)';
}
}
/// @nodoc
abstract mixin class $RemoteNodeCopyWith<$Res> {
factory $RemoteNodeCopyWith(RemoteNode value, $Res Function(RemoteNode) _then) = _$RemoteNodeCopyWithImpl;
@useResult
$Res call({
String id, String name, String url, String status, String? description, List<String> tags,@JsonKey(name: 'last_seen') DateTime? lastSeen
});
}
/// @nodoc
class _$RemoteNodeCopyWithImpl<$Res>
implements $RemoteNodeCopyWith<$Res> {
_$RemoteNodeCopyWithImpl(this._self, this._then);
final RemoteNode _self;
final $Res Function(RemoteNode) _then;
/// Create a copy of RemoteNode
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? url = null,Object? status = null,Object? description = freezed,Object? tags = null,Object? lastSeen = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>,lastSeen: freezed == lastSeen ? _self.lastSeen : lastSeen // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [RemoteNode].
extension RemoteNodePatterns on RemoteNode {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _RemoteNode value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _RemoteNode() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _RemoteNode value) $default,){
final _that = this;
switch (_that) {
case _RemoteNode():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _RemoteNode value)? $default,){
final _that = this;
switch (_that) {
case _RemoteNode() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, String url, String status, String? description, List<String> tags, @JsonKey(name: 'last_seen') DateTime? lastSeen)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RemoteNode() when $default != null:
return $default(_that.id,_that.name,_that.url,_that.status,_that.description,_that.tags,_that.lastSeen);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, String url, String status, String? description, List<String> tags, @JsonKey(name: 'last_seen') DateTime? lastSeen) $default,) {final _that = this;
switch (_that) {
case _RemoteNode():
return $default(_that.id,_that.name,_that.url,_that.status,_that.description,_that.tags,_that.lastSeen);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, String url, String status, String? description, List<String> tags, @JsonKey(name: 'last_seen') DateTime? lastSeen)? $default,) {final _that = this;
switch (_that) {
case _RemoteNode() when $default != null:
return $default(_that.id,_that.name,_that.url,_that.status,_that.description,_that.tags,_that.lastSeen);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _RemoteNode implements RemoteNode {
const _RemoteNode({this.id = '', required this.name, required this.url, this.status = '', this.description, final List<String> tags = const <String>[], @JsonKey(name: 'last_seen') this.lastSeen}): _tags = tags;
factory _RemoteNode.fromJson(Map<String, dynamic> json) => _$RemoteNodeFromJson(json);
@override@JsonKey() final String id;
@override final String name;
@override final String url;
@override@JsonKey() final String status;
@override final String? description;
final List<String> _tags;
@override@JsonKey() List<String> get tags {
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tags);
}
@override@JsonKey(name: 'last_seen') final DateTime? lastSeen;
/// Create a copy of RemoteNode
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$RemoteNodeCopyWith<_RemoteNode> get copyWith => __$RemoteNodeCopyWithImpl<_RemoteNode>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$RemoteNodeToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RemoteNode&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url)&&(identical(other.status, status) || other.status == status)&&(identical(other.description, description) || other.description == description)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.lastSeen, lastSeen) || other.lastSeen == lastSeen));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,url,status,description,const DeepCollectionEquality().hash(_tags),lastSeen);
@override
String toString() {
return 'RemoteNode(id: $id, name: $name, url: $url, status: $status, description: $description, tags: $tags, lastSeen: $lastSeen)';
}
}
/// @nodoc
abstract mixin class _$RemoteNodeCopyWith<$Res> implements $RemoteNodeCopyWith<$Res> {
factory _$RemoteNodeCopyWith(_RemoteNode value, $Res Function(_RemoteNode) _then) = __$RemoteNodeCopyWithImpl;
@override @useResult
$Res call({
String id, String name, String url, String status, String? description, List<String> tags,@JsonKey(name: 'last_seen') DateTime? lastSeen
});
}
/// @nodoc
class __$RemoteNodeCopyWithImpl<$Res>
implements _$RemoteNodeCopyWith<$Res> {
__$RemoteNodeCopyWithImpl(this._self, this._then);
final _RemoteNode _self;
final $Res Function(_RemoteNode) _then;
/// Create a copy of RemoteNode
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? url = null,Object? status = null,Object? description = freezed,Object? tags = null,Object? lastSeen = freezed,}) {
return _then(_RemoteNode(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>,lastSeen: freezed == lastSeen ? _self.lastSeen : lastSeen // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on
+32
View File
@@ -0,0 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'remote_node.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_RemoteNode _$RemoteNodeFromJson(Map<String, dynamic> json) => _RemoteNode(
id: json['id'] as String? ?? '',
name: json['name'] as String,
url: json['url'] as String,
status: json['status'] as String? ?? '',
description: json['description'] as String?,
tags:
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ??
const <String>[],
lastSeen: json['last_seen'] == null
? null
: DateTime.parse(json['last_seen'] as String),
);
Map<String, dynamic> _$RemoteNodeToJson(_RemoteNode instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'url': instance.url,
'status': instance.status,
'description': instance.description,
'tags': instance.tags,
'last_seen': instance.lastSeen?.toIso8601String(),
};
+24
View File
@@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'backup_method.dart';
part 'run_state.freezed.dart';
part 'run_state.g.dart';
/// Mirrors `models.RunState` — the live (in-memory, non-persisted) progress
/// of a single backup target's current or most recent run. Polled from
/// `GET /.../backup/targets/:tid/progress` while a run is in flight.
@freezed
sealed class RunState with _$RunState {
const factory RunState({
@Default(false) bool running,
@JsonKey(name: 'target_id') @Default('') String targetId,
BackupMethod? method,
@Default(0) double percent,
String? eta,
String? message,
@JsonKey(name: 'started_at') DateTime? startedAt,
}) = _RunState;
factory RunState.fromJson(Map<String, dynamic> json) => _$RunStateFromJson(json);
}
+289
View File
@@ -0,0 +1,289 @@
// 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 'run_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RunState {
bool get running;@JsonKey(name: 'target_id') String get targetId; BackupMethod? get method; double get percent; String? get eta; String? get message;@JsonKey(name: 'started_at') DateTime? get startedAt;
/// Create a copy of RunState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RunStateCopyWith<RunState> get copyWith => _$RunStateCopyWithImpl<RunState>(this as RunState, _$identity);
/// Serializes this RunState to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RunState&&(identical(other.running, running) || other.running == running)&&(identical(other.targetId, targetId) || other.targetId == targetId)&&(identical(other.method, method) || other.method == method)&&(identical(other.percent, percent) || other.percent == percent)&&(identical(other.eta, eta) || other.eta == eta)&&(identical(other.message, message) || other.message == message)&&(identical(other.startedAt, startedAt) || other.startedAt == startedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,running,targetId,method,percent,eta,message,startedAt);
@override
String toString() {
return 'RunState(running: $running, targetId: $targetId, method: $method, percent: $percent, eta: $eta, message: $message, startedAt: $startedAt)';
}
}
/// @nodoc
abstract mixin class $RunStateCopyWith<$Res> {
factory $RunStateCopyWith(RunState value, $Res Function(RunState) _then) = _$RunStateCopyWithImpl;
@useResult
$Res call({
bool running,@JsonKey(name: 'target_id') String targetId, BackupMethod? method, double percent, String? eta, String? message,@JsonKey(name: 'started_at') DateTime? startedAt
});
}
/// @nodoc
class _$RunStateCopyWithImpl<$Res>
implements $RunStateCopyWith<$Res> {
_$RunStateCopyWithImpl(this._self, this._then);
final RunState _self;
final $Res Function(RunState) _then;
/// Create a copy of RunState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? running = null,Object? targetId = null,Object? method = freezed,Object? percent = null,Object? eta = freezed,Object? message = freezed,Object? startedAt = freezed,}) {
return _then(_self.copyWith(
running: null == running ? _self.running : running // ignore: cast_nullable_to_non_nullable
as bool,targetId: null == targetId ? _self.targetId : targetId // ignore: cast_nullable_to_non_nullable
as String,method: freezed == method ? _self.method : method // ignore: cast_nullable_to_non_nullable
as BackupMethod?,percent: null == percent ? _self.percent : percent // ignore: cast_nullable_to_non_nullable
as double,eta: freezed == eta ? _self.eta : eta // ignore: cast_nullable_to_non_nullable
as String?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,startedAt: freezed == startedAt ? _self.startedAt : startedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// Adds pattern-matching-related methods to [RunState].
extension RunStatePatterns on RunState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _RunState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _RunState() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _RunState value) $default,){
final _that = this;
switch (_that) {
case _RunState():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _RunState value)? $default,){
final _that = this;
switch (_that) {
case _RunState() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool running, @JsonKey(name: 'target_id') String targetId, BackupMethod? method, double percent, String? eta, String? message, @JsonKey(name: 'started_at') DateTime? startedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RunState() when $default != null:
return $default(_that.running,_that.targetId,_that.method,_that.percent,_that.eta,_that.message,_that.startedAt);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool running, @JsonKey(name: 'target_id') String targetId, BackupMethod? method, double percent, String? eta, String? message, @JsonKey(name: 'started_at') DateTime? startedAt) $default,) {final _that = this;
switch (_that) {
case _RunState():
return $default(_that.running,_that.targetId,_that.method,_that.percent,_that.eta,_that.message,_that.startedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool running, @JsonKey(name: 'target_id') String targetId, BackupMethod? method, double percent, String? eta, String? message, @JsonKey(name: 'started_at') DateTime? startedAt)? $default,) {final _that = this;
switch (_that) {
case _RunState() when $default != null:
return $default(_that.running,_that.targetId,_that.method,_that.percent,_that.eta,_that.message,_that.startedAt);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _RunState implements RunState {
const _RunState({this.running = false, @JsonKey(name: 'target_id') this.targetId = '', this.method, this.percent = 0, this.eta, this.message, @JsonKey(name: 'started_at') this.startedAt});
factory _RunState.fromJson(Map<String, dynamic> json) => _$RunStateFromJson(json);
@override@JsonKey() final bool running;
@override@JsonKey(name: 'target_id') final String targetId;
@override final BackupMethod? method;
@override@JsonKey() final double percent;
@override final String? eta;
@override final String? message;
@override@JsonKey(name: 'started_at') final DateTime? startedAt;
/// Create a copy of RunState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$RunStateCopyWith<_RunState> get copyWith => __$RunStateCopyWithImpl<_RunState>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$RunStateToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RunState&&(identical(other.running, running) || other.running == running)&&(identical(other.targetId, targetId) || other.targetId == targetId)&&(identical(other.method, method) || other.method == method)&&(identical(other.percent, percent) || other.percent == percent)&&(identical(other.eta, eta) || other.eta == eta)&&(identical(other.message, message) || other.message == message)&&(identical(other.startedAt, startedAt) || other.startedAt == startedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,running,targetId,method,percent,eta,message,startedAt);
@override
String toString() {
return 'RunState(running: $running, targetId: $targetId, method: $method, percent: $percent, eta: $eta, message: $message, startedAt: $startedAt)';
}
}
/// @nodoc
abstract mixin class _$RunStateCopyWith<$Res> implements $RunStateCopyWith<$Res> {
factory _$RunStateCopyWith(_RunState value, $Res Function(_RunState) _then) = __$RunStateCopyWithImpl;
@override @useResult
$Res call({
bool running,@JsonKey(name: 'target_id') String targetId, BackupMethod? method, double percent, String? eta, String? message,@JsonKey(name: 'started_at') DateTime? startedAt
});
}
/// @nodoc
class __$RunStateCopyWithImpl<$Res>
implements _$RunStateCopyWith<$Res> {
__$RunStateCopyWithImpl(this._self, this._then);
final _RunState _self;
final $Res Function(_RunState) _then;
/// Create a copy of RunState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? running = null,Object? targetId = null,Object? method = freezed,Object? percent = null,Object? eta = freezed,Object? message = freezed,Object? startedAt = freezed,}) {
return _then(_RunState(
running: null == running ? _self.running : running // ignore: cast_nullable_to_non_nullable
as bool,targetId: null == targetId ? _self.targetId : targetId // ignore: cast_nullable_to_non_nullable
as String,method: freezed == method ? _self.method : method // ignore: cast_nullable_to_non_nullable
as BackupMethod?,percent: null == percent ? _self.percent : percent // ignore: cast_nullable_to_non_nullable
as double,eta: freezed == eta ? _self.eta : eta // ignore: cast_nullable_to_non_nullable
as String?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,startedAt: freezed == startedAt ? _self.startedAt : startedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on
+35
View File
@@ -0,0 +1,35 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'run_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_RunState _$RunStateFromJson(Map<String, dynamic> json) => _RunState(
running: json['running'] as bool? ?? false,
targetId: json['target_id'] as String? ?? '',
method: $enumDecodeNullable(_$BackupMethodEnumMap, json['method']),
percent: (json['percent'] as num?)?.toDouble() ?? 0,
eta: json['eta'] as String?,
message: json['message'] as String?,
startedAt: json['started_at'] == null
? null
: DateTime.parse(json['started_at'] as String),
);
Map<String, dynamic> _$RunStateToJson(_RunState instance) => <String, dynamic>{
'running': instance.running,
'target_id': instance.targetId,
'method': _$BackupMethodEnumMap[instance.method],
'percent': instance.percent,
'eta': instance.eta,
'message': instance.message,
'started_at': instance.startedAt?.toIso8601String(),
};
const _$BackupMethodEnumMap = {
BackupMethod.rsync: 'rsync',
BackupMethod.restic: 'restic',
BackupMethod.external: 'external',
};
+20
View File
@@ -0,0 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'service_scan_info.dart';
part 'scan_result.freezed.dart';
part 'scan_result.g.dart';
/// Mirrors `models.ScanResult`, returned by `POST /node/scan`.
@freezed
sealed class ScanResult with _$ScanResult {
const factory ScanResult({
@JsonKey(name: 'scanned_paths') @Default(<String>[]) List<String> scannedPaths,
@Default(0) int found,
@Default(<ServiceScanInfo>[]) List<ServiceScanInfo> created,
@Default(<ServiceScanInfo>[]) List<ServiceScanInfo> updated,
@Default(<String>[]) List<String> errors,
}) = _ScanResult;
factory ScanResult.fromJson(Map<String, dynamic> json) => _$ScanResultFromJson(json);
}
+307
View File
@@ -0,0 +1,307 @@
// 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 'scan_result.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ScanResult {
@JsonKey(name: 'scanned_paths') List<String> get scannedPaths; int get found; List<ServiceScanInfo> get created; List<ServiceScanInfo> get updated; List<String> get errors;
/// Create a copy of ScanResult
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ScanResultCopyWith<ScanResult> get copyWith => _$ScanResultCopyWithImpl<ScanResult>(this as ScanResult, _$identity);
/// Serializes this ScanResult to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ScanResult&&const DeepCollectionEquality().equals(other.scannedPaths, scannedPaths)&&(identical(other.found, found) || other.found == found)&&const DeepCollectionEquality().equals(other.created, created)&&const DeepCollectionEquality().equals(other.updated, updated)&&const DeepCollectionEquality().equals(other.errors, errors));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(scannedPaths),found,const DeepCollectionEquality().hash(created),const DeepCollectionEquality().hash(updated),const DeepCollectionEquality().hash(errors));
@override
String toString() {
return 'ScanResult(scannedPaths: $scannedPaths, found: $found, created: $created, updated: $updated, errors: $errors)';
}
}
/// @nodoc
abstract mixin class $ScanResultCopyWith<$Res> {
factory $ScanResultCopyWith(ScanResult value, $Res Function(ScanResult) _then) = _$ScanResultCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'scanned_paths') List<String> scannedPaths, int found, List<ServiceScanInfo> created, List<ServiceScanInfo> updated, List<String> errors
});
}
/// @nodoc
class _$ScanResultCopyWithImpl<$Res>
implements $ScanResultCopyWith<$Res> {
_$ScanResultCopyWithImpl(this._self, this._then);
final ScanResult _self;
final $Res Function(ScanResult) _then;
/// Create a copy of ScanResult
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? scannedPaths = null,Object? found = null,Object? created = null,Object? updated = null,Object? errors = null,}) {
return _then(_self.copyWith(
scannedPaths: null == scannedPaths ? _self.scannedPaths : scannedPaths // ignore: cast_nullable_to_non_nullable
as List<String>,found: null == found ? _self.found : found // ignore: cast_nullable_to_non_nullable
as int,created: null == created ? _self.created : created // ignore: cast_nullable_to_non_nullable
as List<ServiceScanInfo>,updated: null == updated ? _self.updated : updated // ignore: cast_nullable_to_non_nullable
as List<ServiceScanInfo>,errors: null == errors ? _self.errors : errors // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// Adds pattern-matching-related methods to [ScanResult].
extension ScanResultPatterns on ScanResult {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ScanResult value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ScanResult() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ScanResult value) $default,){
final _that = this;
switch (_that) {
case _ScanResult():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ScanResult value)? $default,){
final _that = this;
switch (_that) {
case _ScanResult() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'scanned_paths') List<String> scannedPaths, int found, List<ServiceScanInfo> created, List<ServiceScanInfo> updated, List<String> errors)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ScanResult() when $default != null:
return $default(_that.scannedPaths,_that.found,_that.created,_that.updated,_that.errors);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'scanned_paths') List<String> scannedPaths, int found, List<ServiceScanInfo> created, List<ServiceScanInfo> updated, List<String> errors) $default,) {final _that = this;
switch (_that) {
case _ScanResult():
return $default(_that.scannedPaths,_that.found,_that.created,_that.updated,_that.errors);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'scanned_paths') List<String> scannedPaths, int found, List<ServiceScanInfo> created, List<ServiceScanInfo> updated, List<String> errors)? $default,) {final _that = this;
switch (_that) {
case _ScanResult() when $default != null:
return $default(_that.scannedPaths,_that.found,_that.created,_that.updated,_that.errors);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ScanResult implements ScanResult {
const _ScanResult({@JsonKey(name: 'scanned_paths') final List<String> scannedPaths = const <String>[], this.found = 0, final List<ServiceScanInfo> created = const <ServiceScanInfo>[], final List<ServiceScanInfo> updated = const <ServiceScanInfo>[], final List<String> errors = const <String>[]}): _scannedPaths = scannedPaths,_created = created,_updated = updated,_errors = errors;
factory _ScanResult.fromJson(Map<String, dynamic> json) => _$ScanResultFromJson(json);
final List<String> _scannedPaths;
@override@JsonKey(name: 'scanned_paths') List<String> get scannedPaths {
if (_scannedPaths is EqualUnmodifiableListView) return _scannedPaths;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_scannedPaths);
}
@override@JsonKey() final int found;
final List<ServiceScanInfo> _created;
@override@JsonKey() List<ServiceScanInfo> get created {
if (_created is EqualUnmodifiableListView) return _created;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_created);
}
final List<ServiceScanInfo> _updated;
@override@JsonKey() List<ServiceScanInfo> get updated {
if (_updated is EqualUnmodifiableListView) return _updated;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_updated);
}
final List<String> _errors;
@override@JsonKey() List<String> get errors {
if (_errors is EqualUnmodifiableListView) return _errors;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_errors);
}
/// Create a copy of ScanResult
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ScanResultCopyWith<_ScanResult> get copyWith => __$ScanResultCopyWithImpl<_ScanResult>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ScanResultToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ScanResult&&const DeepCollectionEquality().equals(other._scannedPaths, _scannedPaths)&&(identical(other.found, found) || other.found == found)&&const DeepCollectionEquality().equals(other._created, _created)&&const DeepCollectionEquality().equals(other._updated, _updated)&&const DeepCollectionEquality().equals(other._errors, _errors));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_scannedPaths),found,const DeepCollectionEquality().hash(_created),const DeepCollectionEquality().hash(_updated),const DeepCollectionEquality().hash(_errors));
@override
String toString() {
return 'ScanResult(scannedPaths: $scannedPaths, found: $found, created: $created, updated: $updated, errors: $errors)';
}
}
/// @nodoc
abstract mixin class _$ScanResultCopyWith<$Res> implements $ScanResultCopyWith<$Res> {
factory _$ScanResultCopyWith(_ScanResult value, $Res Function(_ScanResult) _then) = __$ScanResultCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'scanned_paths') List<String> scannedPaths, int found, List<ServiceScanInfo> created, List<ServiceScanInfo> updated, List<String> errors
});
}
/// @nodoc
class __$ScanResultCopyWithImpl<$Res>
implements _$ScanResultCopyWith<$Res> {
__$ScanResultCopyWithImpl(this._self, this._then);
final _ScanResult _self;
final $Res Function(_ScanResult) _then;
/// Create a copy of ScanResult
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? scannedPaths = null,Object? found = null,Object? created = null,Object? updated = null,Object? errors = null,}) {
return _then(_ScanResult(
scannedPaths: null == scannedPaths ? _self._scannedPaths : scannedPaths // ignore: cast_nullable_to_non_nullable
as List<String>,found: null == found ? _self.found : found // ignore: cast_nullable_to_non_nullable
as int,created: null == created ? _self._created : created // ignore: cast_nullable_to_non_nullable
as List<ServiceScanInfo>,updated: null == updated ? _self._updated : updated // ignore: cast_nullable_to_non_nullable
as List<ServiceScanInfo>,errors: null == errors ? _self._errors : errors // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
// dart format on
+38
View File
@@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'scan_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ScanResult _$ScanResultFromJson(Map<String, dynamic> json) => _ScanResult(
scannedPaths:
(json['scanned_paths'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const <String>[],
found: (json['found'] as num?)?.toInt() ?? 0,
created:
(json['created'] as List<dynamic>?)
?.map((e) => ServiceScanInfo.fromJson(e as Map<String, dynamic>))
.toList() ??
const <ServiceScanInfo>[],
updated:
(json['updated'] as List<dynamic>?)
?.map((e) => ServiceScanInfo.fromJson(e as Map<String, dynamic>))
.toList() ??
const <ServiceScanInfo>[],
errors:
(json['errors'] as List<dynamic>?)?.map((e) => e as String).toList() ??
const <String>[],
);
Map<String, dynamic> _$ScanResultToJson(_ScanResult instance) =>
<String, dynamic>{
'scanned_paths': instance.scannedPaths,
'found': instance.found,
'created': instance.created.map((e) => e.toJson()).toList(),
'updated': instance.updated.map((e) => e.toJson()).toList(),
'errors': instance.errors,
};
+29
View File
@@ -0,0 +1,29 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'backup_config.dart';
part 'service.freezed.dart';
part 'service.g.dart';
/// Mirrors `models.Service` — a compose-based service on this host.
/// `status` is one of "up" / "down" / "unknown" (untyped string on the Go
/// side); kept as a raw string here too and mapped to [Severity] at the
/// widget boundary rather than modeled as a closed enum, since the server
/// can emit values this client doesn't yet know about.
@freezed
sealed class Service with _$Service {
const factory Service({
@Default('') String id,
required String name,
@JsonKey(name: 'compose_file') required String composeFile,
@JsonKey(name: 'compose_type') String? composeType,
@JsonKey(name: 'backup_folder') String? backupFolder,
@Default('unknown') String status,
@JsonKey(name: 'stop_on_backup') @Default(false) bool stopOnBackup,
@JsonKey(name: 'external_backup_stop_minutes') @Default(0) int externalBackupStopMinutes,
@JsonKey(name: 'external_backup_start_minutes') @Default(0) int externalBackupStartMinutes,
BackupConfig? backup,
}) = _Service;
factory Service.fromJson(Map<String, dynamic> json) => _$ServiceFromJson(json);
}
+322
View File
@@ -0,0 +1,322 @@
// 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 'service.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Service {
String get id; String get name;@JsonKey(name: 'compose_file') String get composeFile;@JsonKey(name: 'compose_type') String? get composeType;@JsonKey(name: 'backup_folder') String? get backupFolder; String get status;@JsonKey(name: 'stop_on_backup') bool get stopOnBackup;@JsonKey(name: 'external_backup_stop_minutes') int get externalBackupStopMinutes;@JsonKey(name: 'external_backup_start_minutes') int get externalBackupStartMinutes; BackupConfig? get backup;
/// Create a copy of Service
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ServiceCopyWith<Service> get copyWith => _$ServiceCopyWithImpl<Service>(this as Service, _$identity);
/// Serializes this Service to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Service&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.composeFile, composeFile) || other.composeFile == composeFile)&&(identical(other.composeType, composeType) || other.composeType == composeType)&&(identical(other.backupFolder, backupFolder) || other.backupFolder == backupFolder)&&(identical(other.status, status) || other.status == status)&&(identical(other.stopOnBackup, stopOnBackup) || other.stopOnBackup == stopOnBackup)&&(identical(other.externalBackupStopMinutes, externalBackupStopMinutes) || other.externalBackupStopMinutes == externalBackupStopMinutes)&&(identical(other.externalBackupStartMinutes, externalBackupStartMinutes) || other.externalBackupStartMinutes == externalBackupStartMinutes)&&(identical(other.backup, backup) || other.backup == backup));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,composeFile,composeType,backupFolder,status,stopOnBackup,externalBackupStopMinutes,externalBackupStartMinutes,backup);
@override
String toString() {
return 'Service(id: $id, name: $name, composeFile: $composeFile, composeType: $composeType, backupFolder: $backupFolder, status: $status, stopOnBackup: $stopOnBackup, externalBackupStopMinutes: $externalBackupStopMinutes, externalBackupStartMinutes: $externalBackupStartMinutes, backup: $backup)';
}
}
/// @nodoc
abstract mixin class $ServiceCopyWith<$Res> {
factory $ServiceCopyWith(Service value, $Res Function(Service) _then) = _$ServiceCopyWithImpl;
@useResult
$Res call({
String id, String name,@JsonKey(name: 'compose_file') String composeFile,@JsonKey(name: 'compose_type') String? composeType,@JsonKey(name: 'backup_folder') String? backupFolder, String status,@JsonKey(name: 'stop_on_backup') bool stopOnBackup,@JsonKey(name: 'external_backup_stop_minutes') int externalBackupStopMinutes,@JsonKey(name: 'external_backup_start_minutes') int externalBackupStartMinutes, BackupConfig? backup
});
$BackupConfigCopyWith<$Res>? get backup;
}
/// @nodoc
class _$ServiceCopyWithImpl<$Res>
implements $ServiceCopyWith<$Res> {
_$ServiceCopyWithImpl(this._self, this._then);
final Service _self;
final $Res Function(Service) _then;
/// Create a copy of Service
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? composeFile = null,Object? composeType = freezed,Object? backupFolder = freezed,Object? status = null,Object? stopOnBackup = null,Object? externalBackupStopMinutes = null,Object? externalBackupStartMinutes = null,Object? backup = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,composeFile: null == composeFile ? _self.composeFile : composeFile // ignore: cast_nullable_to_non_nullable
as String,composeType: freezed == composeType ? _self.composeType : composeType // ignore: cast_nullable_to_non_nullable
as String?,backupFolder: freezed == backupFolder ? _self.backupFolder : backupFolder // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,stopOnBackup: null == stopOnBackup ? _self.stopOnBackup : stopOnBackup // ignore: cast_nullable_to_non_nullable
as bool,externalBackupStopMinutes: null == externalBackupStopMinutes ? _self.externalBackupStopMinutes : externalBackupStopMinutes // ignore: cast_nullable_to_non_nullable
as int,externalBackupStartMinutes: null == externalBackupStartMinutes ? _self.externalBackupStartMinutes : externalBackupStartMinutes // ignore: cast_nullable_to_non_nullable
as int,backup: freezed == backup ? _self.backup : backup // ignore: cast_nullable_to_non_nullable
as BackupConfig?,
));
}
/// Create a copy of Service
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$BackupConfigCopyWith<$Res>? get backup {
if (_self.backup == null) {
return null;
}
return $BackupConfigCopyWith<$Res>(_self.backup!, (value) {
return _then(_self.copyWith(backup: value));
});
}
}
/// Adds pattern-matching-related methods to [Service].
extension ServicePatterns on Service {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Service value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _Service() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Service value) $default,){
final _that = this;
switch (_that) {
case _Service():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Service value)? $default,){
final _that = this;
switch (_that) {
case _Service() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String name, @JsonKey(name: 'compose_file') String composeFile, @JsonKey(name: 'compose_type') String? composeType, @JsonKey(name: 'backup_folder') String? backupFolder, String status, @JsonKey(name: 'stop_on_backup') bool stopOnBackup, @JsonKey(name: 'external_backup_stop_minutes') int externalBackupStopMinutes, @JsonKey(name: 'external_backup_start_minutes') int externalBackupStartMinutes, BackupConfig? backup)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Service() when $default != null:
return $default(_that.id,_that.name,_that.composeFile,_that.composeType,_that.backupFolder,_that.status,_that.stopOnBackup,_that.externalBackupStopMinutes,_that.externalBackupStartMinutes,_that.backup);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String name, @JsonKey(name: 'compose_file') String composeFile, @JsonKey(name: 'compose_type') String? composeType, @JsonKey(name: 'backup_folder') String? backupFolder, String status, @JsonKey(name: 'stop_on_backup') bool stopOnBackup, @JsonKey(name: 'external_backup_stop_minutes') int externalBackupStopMinutes, @JsonKey(name: 'external_backup_start_minutes') int externalBackupStartMinutes, BackupConfig? backup) $default,) {final _that = this;
switch (_that) {
case _Service():
return $default(_that.id,_that.name,_that.composeFile,_that.composeType,_that.backupFolder,_that.status,_that.stopOnBackup,_that.externalBackupStopMinutes,_that.externalBackupStartMinutes,_that.backup);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String name, @JsonKey(name: 'compose_file') String composeFile, @JsonKey(name: 'compose_type') String? composeType, @JsonKey(name: 'backup_folder') String? backupFolder, String status, @JsonKey(name: 'stop_on_backup') bool stopOnBackup, @JsonKey(name: 'external_backup_stop_minutes') int externalBackupStopMinutes, @JsonKey(name: 'external_backup_start_minutes') int externalBackupStartMinutes, BackupConfig? backup)? $default,) {final _that = this;
switch (_that) {
case _Service() when $default != null:
return $default(_that.id,_that.name,_that.composeFile,_that.composeType,_that.backupFolder,_that.status,_that.stopOnBackup,_that.externalBackupStopMinutes,_that.externalBackupStartMinutes,_that.backup);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _Service implements Service {
const _Service({this.id = '', required this.name, @JsonKey(name: 'compose_file') required this.composeFile, @JsonKey(name: 'compose_type') this.composeType, @JsonKey(name: 'backup_folder') this.backupFolder, this.status = 'unknown', @JsonKey(name: 'stop_on_backup') this.stopOnBackup = false, @JsonKey(name: 'external_backup_stop_minutes') this.externalBackupStopMinutes = 0, @JsonKey(name: 'external_backup_start_minutes') this.externalBackupStartMinutes = 0, this.backup});
factory _Service.fromJson(Map<String, dynamic> json) => _$ServiceFromJson(json);
@override@JsonKey() final String id;
@override final String name;
@override@JsonKey(name: 'compose_file') final String composeFile;
@override@JsonKey(name: 'compose_type') final String? composeType;
@override@JsonKey(name: 'backup_folder') final String? backupFolder;
@override@JsonKey() final String status;
@override@JsonKey(name: 'stop_on_backup') final bool stopOnBackup;
@override@JsonKey(name: 'external_backup_stop_minutes') final int externalBackupStopMinutes;
@override@JsonKey(name: 'external_backup_start_minutes') final int externalBackupStartMinutes;
@override final BackupConfig? backup;
/// Create a copy of Service
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ServiceCopyWith<_Service> get copyWith => __$ServiceCopyWithImpl<_Service>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ServiceToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Service&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.composeFile, composeFile) || other.composeFile == composeFile)&&(identical(other.composeType, composeType) || other.composeType == composeType)&&(identical(other.backupFolder, backupFolder) || other.backupFolder == backupFolder)&&(identical(other.status, status) || other.status == status)&&(identical(other.stopOnBackup, stopOnBackup) || other.stopOnBackup == stopOnBackup)&&(identical(other.externalBackupStopMinutes, externalBackupStopMinutes) || other.externalBackupStopMinutes == externalBackupStopMinutes)&&(identical(other.externalBackupStartMinutes, externalBackupStartMinutes) || other.externalBackupStartMinutes == externalBackupStartMinutes)&&(identical(other.backup, backup) || other.backup == backup));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,name,composeFile,composeType,backupFolder,status,stopOnBackup,externalBackupStopMinutes,externalBackupStartMinutes,backup);
@override
String toString() {
return 'Service(id: $id, name: $name, composeFile: $composeFile, composeType: $composeType, backupFolder: $backupFolder, status: $status, stopOnBackup: $stopOnBackup, externalBackupStopMinutes: $externalBackupStopMinutes, externalBackupStartMinutes: $externalBackupStartMinutes, backup: $backup)';
}
}
/// @nodoc
abstract mixin class _$ServiceCopyWith<$Res> implements $ServiceCopyWith<$Res> {
factory _$ServiceCopyWith(_Service value, $Res Function(_Service) _then) = __$ServiceCopyWithImpl;
@override @useResult
$Res call({
String id, String name,@JsonKey(name: 'compose_file') String composeFile,@JsonKey(name: 'compose_type') String? composeType,@JsonKey(name: 'backup_folder') String? backupFolder, String status,@JsonKey(name: 'stop_on_backup') bool stopOnBackup,@JsonKey(name: 'external_backup_stop_minutes') int externalBackupStopMinutes,@JsonKey(name: 'external_backup_start_minutes') int externalBackupStartMinutes, BackupConfig? backup
});
@override $BackupConfigCopyWith<$Res>? get backup;
}
/// @nodoc
class __$ServiceCopyWithImpl<$Res>
implements _$ServiceCopyWith<$Res> {
__$ServiceCopyWithImpl(this._self, this._then);
final _Service _self;
final $Res Function(_Service) _then;
/// Create a copy of Service
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? composeFile = null,Object? composeType = freezed,Object? backupFolder = freezed,Object? status = null,Object? stopOnBackup = null,Object? externalBackupStopMinutes = null,Object? externalBackupStartMinutes = null,Object? backup = freezed,}) {
return _then(_Service(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,composeFile: null == composeFile ? _self.composeFile : composeFile // ignore: cast_nullable_to_non_nullable
as String,composeType: freezed == composeType ? _self.composeType : composeType // ignore: cast_nullable_to_non_nullable
as String?,backupFolder: freezed == backupFolder ? _self.backupFolder : backupFolder // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,stopOnBackup: null == stopOnBackup ? _self.stopOnBackup : stopOnBackup // ignore: cast_nullable_to_non_nullable
as bool,externalBackupStopMinutes: null == externalBackupStopMinutes ? _self.externalBackupStopMinutes : externalBackupStopMinutes // ignore: cast_nullable_to_non_nullable
as int,externalBackupStartMinutes: null == externalBackupStartMinutes ? _self.externalBackupStartMinutes : externalBackupStartMinutes // ignore: cast_nullable_to_non_nullable
as int,backup: freezed == backup ? _self.backup : backup // ignore: cast_nullable_to_non_nullable
as BackupConfig?,
));
}
/// Create a copy of Service
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$BackupConfigCopyWith<$Res>? get backup {
if (_self.backup == null) {
return null;
}
return $BackupConfigCopyWith<$Res>(_self.backup!, (value) {
return _then(_self.copyWith(backup: value));
});
}
}
// dart format on
+37
View File
@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'service.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_Service _$ServiceFromJson(Map<String, dynamic> json) => _Service(
id: json['id'] as String? ?? '',
name: json['name'] as String,
composeFile: json['compose_file'] as String,
composeType: json['compose_type'] as String?,
backupFolder: json['backup_folder'] as String?,
status: json['status'] as String? ?? 'unknown',
stopOnBackup: json['stop_on_backup'] as bool? ?? false,
externalBackupStopMinutes:
(json['external_backup_stop_minutes'] as num?)?.toInt() ?? 0,
externalBackupStartMinutes:
(json['external_backup_start_minutes'] as num?)?.toInt() ?? 0,
backup: json['backup'] == null
? null
: BackupConfig.fromJson(json['backup'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ServiceToJson(_Service instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'compose_file': instance.composeFile,
'compose_type': instance.composeType,
'backup_folder': instance.backupFolder,
'status': instance.status,
'stop_on_backup': instance.stopOnBackup,
'external_backup_stop_minutes': instance.externalBackupStopMinutes,
'external_backup_start_minutes': instance.externalBackupStartMinutes,
'backup': instance.backup?.toJson(),
};
+18
View File
@@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'service_scan_info.freezed.dart';
part 'service_scan_info.g.dart';
/// Mirrors `models.ServiceScanInfo` — one entry in a [ScanResult]'s
/// `created`/`updated` lists.
@freezed
sealed class ServiceScanInfo with _$ServiceScanInfo {
const factory ServiceScanInfo({
required String name,
@JsonKey(name: 'compose_file') required String composeFile,
@JsonKey(name: 'backup_folder') String? backupFolder,
@Default('unknown') String status,
}) = _ServiceScanInfo;
factory ServiceScanInfo.fromJson(Map<String, dynamic> json) => _$ServiceScanInfoFromJson(json);
}
@@ -0,0 +1,280 @@
// 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 'service_scan_info.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ServiceScanInfo {
String get name;@JsonKey(name: 'compose_file') String get composeFile;@JsonKey(name: 'backup_folder') String? get backupFolder; String get status;
/// Create a copy of ServiceScanInfo
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ServiceScanInfoCopyWith<ServiceScanInfo> get copyWith => _$ServiceScanInfoCopyWithImpl<ServiceScanInfo>(this as ServiceScanInfo, _$identity);
/// Serializes this ServiceScanInfo to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServiceScanInfo&&(identical(other.name, name) || other.name == name)&&(identical(other.composeFile, composeFile) || other.composeFile == composeFile)&&(identical(other.backupFolder, backupFolder) || other.backupFolder == backupFolder)&&(identical(other.status, status) || other.status == status));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,composeFile,backupFolder,status);
@override
String toString() {
return 'ServiceScanInfo(name: $name, composeFile: $composeFile, backupFolder: $backupFolder, status: $status)';
}
}
/// @nodoc
abstract mixin class $ServiceScanInfoCopyWith<$Res> {
factory $ServiceScanInfoCopyWith(ServiceScanInfo value, $Res Function(ServiceScanInfo) _then) = _$ServiceScanInfoCopyWithImpl;
@useResult
$Res call({
String name,@JsonKey(name: 'compose_file') String composeFile,@JsonKey(name: 'backup_folder') String? backupFolder, String status
});
}
/// @nodoc
class _$ServiceScanInfoCopyWithImpl<$Res>
implements $ServiceScanInfoCopyWith<$Res> {
_$ServiceScanInfoCopyWithImpl(this._self, this._then);
final ServiceScanInfo _self;
final $Res Function(ServiceScanInfo) _then;
/// Create a copy of ServiceScanInfo
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? composeFile = null,Object? backupFolder = freezed,Object? status = null,}) {
return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,composeFile: null == composeFile ? _self.composeFile : composeFile // ignore: cast_nullable_to_non_nullable
as String,backupFolder: freezed == backupFolder ? _self.backupFolder : backupFolder // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [ServiceScanInfo].
extension ServiceScanInfoPatterns on ServiceScanInfo {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ServiceScanInfo value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _ServiceScanInfo() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ServiceScanInfo value) $default,){
final _that = this;
switch (_that) {
case _ServiceScanInfo():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ServiceScanInfo value)? $default,){
final _that = this;
switch (_that) {
case _ServiceScanInfo() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, @JsonKey(name: 'compose_file') String composeFile, @JsonKey(name: 'backup_folder') String? backupFolder, String status)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ServiceScanInfo() when $default != null:
return $default(_that.name,_that.composeFile,_that.backupFolder,_that.status);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, @JsonKey(name: 'compose_file') String composeFile, @JsonKey(name: 'backup_folder') String? backupFolder, String status) $default,) {final _that = this;
switch (_that) {
case _ServiceScanInfo():
return $default(_that.name,_that.composeFile,_that.backupFolder,_that.status);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, @JsonKey(name: 'compose_file') String composeFile, @JsonKey(name: 'backup_folder') String? backupFolder, String status)? $default,) {final _that = this;
switch (_that) {
case _ServiceScanInfo() when $default != null:
return $default(_that.name,_that.composeFile,_that.backupFolder,_that.status);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _ServiceScanInfo implements ServiceScanInfo {
const _ServiceScanInfo({required this.name, @JsonKey(name: 'compose_file') required this.composeFile, @JsonKey(name: 'backup_folder') this.backupFolder, this.status = 'unknown'});
factory _ServiceScanInfo.fromJson(Map<String, dynamic> json) => _$ServiceScanInfoFromJson(json);
@override final String name;
@override@JsonKey(name: 'compose_file') final String composeFile;
@override@JsonKey(name: 'backup_folder') final String? backupFolder;
@override@JsonKey() final String status;
/// Create a copy of ServiceScanInfo
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ServiceScanInfoCopyWith<_ServiceScanInfo> get copyWith => __$ServiceScanInfoCopyWithImpl<_ServiceScanInfo>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$ServiceScanInfoToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServiceScanInfo&&(identical(other.name, name) || other.name == name)&&(identical(other.composeFile, composeFile) || other.composeFile == composeFile)&&(identical(other.backupFolder, backupFolder) || other.backupFolder == backupFolder)&&(identical(other.status, status) || other.status == status));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,name,composeFile,backupFolder,status);
@override
String toString() {
return 'ServiceScanInfo(name: $name, composeFile: $composeFile, backupFolder: $backupFolder, status: $status)';
}
}
/// @nodoc
abstract mixin class _$ServiceScanInfoCopyWith<$Res> implements $ServiceScanInfoCopyWith<$Res> {
factory _$ServiceScanInfoCopyWith(_ServiceScanInfo value, $Res Function(_ServiceScanInfo) _then) = __$ServiceScanInfoCopyWithImpl;
@override @useResult
$Res call({
String name,@JsonKey(name: 'compose_file') String composeFile,@JsonKey(name: 'backup_folder') String? backupFolder, String status
});
}
/// @nodoc
class __$ServiceScanInfoCopyWithImpl<$Res>
implements _$ServiceScanInfoCopyWith<$Res> {
__$ServiceScanInfoCopyWithImpl(this._self, this._then);
final _ServiceScanInfo _self;
final $Res Function(_ServiceScanInfo) _then;
/// Create a copy of ServiceScanInfo
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? composeFile = null,Object? backupFolder = freezed,Object? status = null,}) {
return _then(_ServiceScanInfo(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,composeFile: null == composeFile ? _self.composeFile : composeFile // ignore: cast_nullable_to_non_nullable
as String,backupFolder: freezed == backupFolder ? _self.backupFolder : backupFolder // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on
+23
View File
@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'service_scan_info.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_ServiceScanInfo _$ServiceScanInfoFromJson(Map<String, dynamic> json) =>
_ServiceScanInfo(
name: json['name'] as String,
composeFile: json['compose_file'] as String,
backupFolder: json['backup_folder'] as String?,
status: json['status'] as String? ?? 'unknown',
);
Map<String, dynamic> _$ServiceScanInfoToJson(_ServiceScanInfo instance) =>
<String, dynamic>{
'name': instance.name,
'compose_file': instance.composeFile,
'backup_folder': instance.backupFolder,
'status': instance.status,
};
+16
View File
@@ -0,0 +1,16 @@
import '../theme/status_colors.dart';
/// Maps the API's untyped status strings to a [Severity] for [StatusPill]
/// rendering. Centralized here (not per-screen) since `Service.status` is
/// rendered on both the Services table and Fleet's aggregated card grid.
Severity severityForServiceStatus(String status) => switch (status) {
'up' => Severity.good,
'down' => Severity.critical,
_ => Severity.warning, // "unknown" or anything this client doesn't recognize yet
};
/// Maps a backup/update execution's `success` flag to a [Severity].
Severity severityForSuccess(bool success) => success ? Severity.good : Severity.critical;
/// Maps a [RemoteNode.status]/reachability signal to a [Severity].
Severity severityForNodeReachable(bool reachable) => reachable ? Severity.good : Severity.critical;
+18
View File
@@ -0,0 +1,18 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'update_record.freezed.dart';
part 'update_record.g.dart';
/// Mirrors `models.UpdateRecord` — an OS package update log entry.
@freezed
sealed class UpdateRecord with _$UpdateRecord {
const factory UpdateRecord({
@Default('') String id,
required DateTime timestamp,
required bool success,
@Default(<String>[]) List<String> packages,
@Default('') String message,
}) = _UpdateRecord;
factory UpdateRecord.fromJson(Map<String, dynamic> json) => _$UpdateRecordFromJson(json);
}
+289
View File
@@ -0,0 +1,289 @@
// 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 'update_record.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$UpdateRecord {
String get id; DateTime get timestamp; bool get success; List<String> get packages; String get message;
/// Create a copy of UpdateRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$UpdateRecordCopyWith<UpdateRecord> get copyWith => _$UpdateRecordCopyWithImpl<UpdateRecord>(this as UpdateRecord, _$identity);
/// Serializes this UpdateRecord to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is UpdateRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.success, success) || other.success == success)&&const DeepCollectionEquality().equals(other.packages, packages)&&(identical(other.message, message) || other.message == message));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,timestamp,success,const DeepCollectionEquality().hash(packages),message);
@override
String toString() {
return 'UpdateRecord(id: $id, timestamp: $timestamp, success: $success, packages: $packages, message: $message)';
}
}
/// @nodoc
abstract mixin class $UpdateRecordCopyWith<$Res> {
factory $UpdateRecordCopyWith(UpdateRecord value, $Res Function(UpdateRecord) _then) = _$UpdateRecordCopyWithImpl;
@useResult
$Res call({
String id, DateTime timestamp, bool success, List<String> packages, String message
});
}
/// @nodoc
class _$UpdateRecordCopyWithImpl<$Res>
implements $UpdateRecordCopyWith<$Res> {
_$UpdateRecordCopyWithImpl(this._self, this._then);
final UpdateRecord _self;
final $Res Function(UpdateRecord) _then;
/// Create a copy of UpdateRecord
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? timestamp = null,Object? success = null,Object? packages = null,Object? message = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,success: null == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool,packages: null == packages ? _self.packages : packages // ignore: cast_nullable_to_non_nullable
as List<String>,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// Adds pattern-matching-related methods to [UpdateRecord].
extension UpdateRecordPatterns on UpdateRecord {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UpdateRecord value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _UpdateRecord() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UpdateRecord value) $default,){
final _that = this;
switch (_that) {
case _UpdateRecord():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UpdateRecord value)? $default,){
final _that = this;
switch (_that) {
case _UpdateRecord() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime timestamp, bool success, List<String> packages, String message)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _UpdateRecord() when $default != null:
return $default(_that.id,_that.timestamp,_that.success,_that.packages,_that.message);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime timestamp, bool success, List<String> packages, String message) $default,) {final _that = this;
switch (_that) {
case _UpdateRecord():
return $default(_that.id,_that.timestamp,_that.success,_that.packages,_that.message);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime timestamp, bool success, List<String> packages, String message)? $default,) {final _that = this;
switch (_that) {
case _UpdateRecord() when $default != null:
return $default(_that.id,_that.timestamp,_that.success,_that.packages,_that.message);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _UpdateRecord implements UpdateRecord {
const _UpdateRecord({this.id = '', required this.timestamp, required this.success, final List<String> packages = const <String>[], this.message = ''}): _packages = packages;
factory _UpdateRecord.fromJson(Map<String, dynamic> json) => _$UpdateRecordFromJson(json);
@override@JsonKey() final String id;
@override final DateTime timestamp;
@override final bool success;
final List<String> _packages;
@override@JsonKey() List<String> get packages {
if (_packages is EqualUnmodifiableListView) return _packages;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_packages);
}
@override@JsonKey() final String message;
/// Create a copy of UpdateRecord
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$UpdateRecordCopyWith<_UpdateRecord> get copyWith => __$UpdateRecordCopyWithImpl<_UpdateRecord>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$UpdateRecordToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UpdateRecord&&(identical(other.id, id) || other.id == id)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&(identical(other.success, success) || other.success == success)&&const DeepCollectionEquality().equals(other._packages, _packages)&&(identical(other.message, message) || other.message == message));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,timestamp,success,const DeepCollectionEquality().hash(_packages),message);
@override
String toString() {
return 'UpdateRecord(id: $id, timestamp: $timestamp, success: $success, packages: $packages, message: $message)';
}
}
/// @nodoc
abstract mixin class _$UpdateRecordCopyWith<$Res> implements $UpdateRecordCopyWith<$Res> {
factory _$UpdateRecordCopyWith(_UpdateRecord value, $Res Function(_UpdateRecord) _then) = __$UpdateRecordCopyWithImpl;
@override @useResult
$Res call({
String id, DateTime timestamp, bool success, List<String> packages, String message
});
}
/// @nodoc
class __$UpdateRecordCopyWithImpl<$Res>
implements _$UpdateRecordCopyWith<$Res> {
__$UpdateRecordCopyWithImpl(this._self, this._then);
final _UpdateRecord _self;
final $Res Function(_UpdateRecord) _then;
/// Create a copy of UpdateRecord
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? timestamp = null,Object? success = null,Object? packages = null,Object? message = null,}) {
return _then(_UpdateRecord(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,success: null == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool,packages: null == packages ? _self._packages : packages // ignore: cast_nullable_to_non_nullable
as List<String>,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on
+29
View File
@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'update_record.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_UpdateRecord _$UpdateRecordFromJson(Map<String, dynamic> json) =>
_UpdateRecord(
id: json['id'] as String? ?? '',
timestamp: DateTime.parse(json['timestamp'] as String),
success: json['success'] as bool,
packages:
(json['packages'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const <String>[],
message: json['message'] as String? ?? '',
);
Map<String, dynamic> _$UpdateRecordToJson(_UpdateRecord instance) =>
<String, dynamic>{
'id': instance.id,
'timestamp': instance.timestamp.toIso8601String(),
'success': instance.success,
'packages': instance.packages,
'message': instance.message,
};
+41
View File
@@ -0,0 +1,41 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../connections/connection_providers.dart';
import 'node_master_api_client.dart';
part 'api_client_provider.g.dart';
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
@riverpod
NodeMasterApiClient? apiClient(Ref ref) {
final connection = ref.watch(activeConnectionProvider);
if (connection == null) return null;
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel('Active connection changed.'));
final baseUrl = connection.baseUrl.endsWith('/')
? connection.baseUrl.substring(0, connection.baseUrl.length - 1)
: connection.baseUrl;
final dio = Dio(
BaseOptions(
baseUrl: '$baseUrl/v1',
connectTimeout: const Duration(seconds: 8),
sendTimeout: const Duration(seconds: 8),
receiveTimeout: const Duration(seconds: 15),
contentType: 'application/json',
),
);
if (kDebugMode) {
dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
}
return NodeMasterApiClient(dio, cancelToken);
}
@@ -0,0 +1,73 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_client_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
@ProviderFor(apiClient)
const apiClientProvider = ApiClientProvider._();
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
final class ApiClientProvider
extends
$FunctionalProvider<
NodeMasterApiClient?,
NodeMasterApiClient?,
NodeMasterApiClient?
>
with $Provider<NodeMasterApiClient?> {
/// Rebuilt (not mutated) whenever the active connection changes, so every
/// watcher automatically rebuilds against the new host with no manual
/// invalidation elsewhere. Returns null when no connection is configured —
/// `app_router.dart` redirects to Settings in that case rather than every
/// screen needing its own "no connection" branch.
const ApiClientProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'apiClientProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$apiClientHash();
@$internal
@override
$ProviderElement<NodeMasterApiClient?> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
NodeMasterApiClient? create(Ref ref) {
return apiClient(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(NodeMasterApiClient? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<NodeMasterApiClient?>(value),
);
}
}
String _$apiClientHash() => r'0a33d0449a51fd825f43bdcddd686bd413ff74ea';
+44
View File
@@ -0,0 +1,44 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'api_exception.freezed.dart';
/// Typed errors thrown by [NodeMasterApiClient]/repositories. Riverpod's
/// `AsyncValue` (via `FutureProvider`/`AsyncNotifier`) already catches
/// thrown exceptions into `AsyncValue.error`, so this is the result-type
/// boundary — no separate `Result<T, E>` wrapper is layered on top.
@freezed
sealed class ApiException with _$ApiException implements Exception {
/// DNS/connection failure — couldn't reach the host at all.
const factory ApiException.network(String message) = ApiNetworkException;
/// Connect/send/receive timeout.
const factory ApiException.timeout() = ApiTimeoutException;
/// The server responded with a 4xx/5xx and (usually) a `{"error": msg}`
/// body.
const factory ApiException.server(int statusCode, String message) = ApiServerException;
/// The response body wasn't the JSON shape a model expected.
const factory ApiException.parse(String message) = ApiParseException;
/// Request was cancelled (e.g. the active connection changed mid-flight).
const factory ApiException.cancelled() = ApiCancelledException;
/// Anything else.
const factory ApiException.unknown(String message) = ApiUnknownException;
}
extension ApiExceptionUserMessage on ApiException {
/// Short, user-facing copy — screens can use this directly or switch on
/// the exception type themselves for more specific handling.
String get userMessage => switch (this) {
ApiNetworkException() => "Can't reach this node — check the URL and that it's reachable.",
ApiTimeoutException() => 'The node took too long to respond.',
ApiServerException(:final statusCode, :final message) => statusCode == 404
? 'Not found.'
: 'Server error ($statusCode): $message',
ApiParseException() => 'Received an unexpected response from the node.',
ApiCancelledException() => 'Cancelled.',
ApiUnknownException(:final message) => message,
};
}
+528
View File
@@ -0,0 +1,528 @@
// 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 'api_exception.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ApiException {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiException);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'ApiException()';
}
}
/// @nodoc
class $ApiExceptionCopyWith<$Res> {
$ApiExceptionCopyWith(ApiException _, $Res Function(ApiException) __);
}
/// Adds pattern-matching-related methods to [ApiException].
extension ApiExceptionPatterns on ApiException {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( ApiNetworkException value)? network,TResult Function( ApiTimeoutException value)? timeout,TResult Function( ApiServerException value)? server,TResult Function( ApiParseException value)? parse,TResult Function( ApiCancelledException value)? cancelled,TResult Function( ApiUnknownException value)? unknown,required TResult orElse(),}){
final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that);case ApiTimeoutException() when timeout != null:
return timeout(_that);case ApiServerException() when server != null:
return server(_that);case ApiParseException() when parse != null:
return parse(_that);case ApiCancelledException() when cancelled != null:
return cancelled(_that);case ApiUnknownException() when unknown != null:
return unknown(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( ApiNetworkException value) network,required TResult Function( ApiTimeoutException value) timeout,required TResult Function( ApiServerException value) server,required TResult Function( ApiParseException value) parse,required TResult Function( ApiCancelledException value) cancelled,required TResult Function( ApiUnknownException value) unknown,}){
final _that = this;
switch (_that) {
case ApiNetworkException():
return network(_that);case ApiTimeoutException():
return timeout(_that);case ApiServerException():
return server(_that);case ApiParseException():
return parse(_that);case ApiCancelledException():
return cancelled(_that);case ApiUnknownException():
return unknown(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( ApiNetworkException value)? network,TResult? Function( ApiTimeoutException value)? timeout,TResult? Function( ApiServerException value)? server,TResult? Function( ApiParseException value)? parse,TResult? Function( ApiCancelledException value)? cancelled,TResult? Function( ApiUnknownException value)? unknown,}){
final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that);case ApiTimeoutException() when timeout != null:
return timeout(_that);case ApiServerException() when server != null:
return server(_that);case ApiParseException() when parse != null:
return parse(_that);case ApiCancelledException() when cancelled != null:
return cancelled(_that);case ApiUnknownException() when unknown != null:
return unknown(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String message)? network,TResult Function()? timeout,TResult Function( int statusCode, String message)? server,TResult Function( String message)? parse,TResult Function()? cancelled,TResult Function( String message)? unknown,required TResult orElse(),}) {final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that.message);case ApiTimeoutException() when timeout != null:
return timeout();case ApiServerException() when server != null:
return server(_that.statusCode,_that.message);case ApiParseException() when parse != null:
return parse(_that.message);case ApiCancelledException() when cancelled != null:
return cancelled();case ApiUnknownException() when unknown != null:
return unknown(_that.message);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String message) network,required TResult Function() timeout,required TResult Function( int statusCode, String message) server,required TResult Function( String message) parse,required TResult Function() cancelled,required TResult Function( String message) unknown,}) {final _that = this;
switch (_that) {
case ApiNetworkException():
return network(_that.message);case ApiTimeoutException():
return timeout();case ApiServerException():
return server(_that.statusCode,_that.message);case ApiParseException():
return parse(_that.message);case ApiCancelledException():
return cancelled();case ApiUnknownException():
return unknown(_that.message);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String message)? network,TResult? Function()? timeout,TResult? Function( int statusCode, String message)? server,TResult? Function( String message)? parse,TResult? Function()? cancelled,TResult? Function( String message)? unknown,}) {final _that = this;
switch (_that) {
case ApiNetworkException() when network != null:
return network(_that.message);case ApiTimeoutException() when timeout != null:
return timeout();case ApiServerException() when server != null:
return server(_that.statusCode,_that.message);case ApiParseException() when parse != null:
return parse(_that.message);case ApiCancelledException() when cancelled != null:
return cancelled();case ApiUnknownException() when unknown != null:
return unknown(_that.message);case _:
return null;
}
}
}
/// @nodoc
class ApiNetworkException implements ApiException {
const ApiNetworkException(this.message);
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiNetworkExceptionCopyWith<ApiNetworkException> get copyWith => _$ApiNetworkExceptionCopyWithImpl<ApiNetworkException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiNetworkException&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'ApiException.network(message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiNetworkExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiNetworkExceptionCopyWith(ApiNetworkException value, $Res Function(ApiNetworkException) _then) = _$ApiNetworkExceptionCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class _$ApiNetworkExceptionCopyWithImpl<$Res>
implements $ApiNetworkExceptionCopyWith<$Res> {
_$ApiNetworkExceptionCopyWithImpl(this._self, this._then);
final ApiNetworkException _self;
final $Res Function(ApiNetworkException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(ApiNetworkException(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class ApiTimeoutException implements ApiException {
const ApiTimeoutException();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiTimeoutException);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'ApiException.timeout()';
}
}
/// @nodoc
class ApiServerException implements ApiException {
const ApiServerException(this.statusCode, this.message);
final int statusCode;
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiServerExceptionCopyWith<ApiServerException> get copyWith => _$ApiServerExceptionCopyWithImpl<ApiServerException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiServerException&&(identical(other.statusCode, statusCode) || other.statusCode == statusCode)&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,statusCode,message);
@override
String toString() {
return 'ApiException.server(statusCode: $statusCode, message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiServerExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiServerExceptionCopyWith(ApiServerException value, $Res Function(ApiServerException) _then) = _$ApiServerExceptionCopyWithImpl;
@useResult
$Res call({
int statusCode, String message
});
}
/// @nodoc
class _$ApiServerExceptionCopyWithImpl<$Res>
implements $ApiServerExceptionCopyWith<$Res> {
_$ApiServerExceptionCopyWithImpl(this._self, this._then);
final ApiServerException _self;
final $Res Function(ApiServerException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? statusCode = null,Object? message = null,}) {
return _then(ApiServerException(
null == statusCode ? _self.statusCode : statusCode // ignore: cast_nullable_to_non_nullable
as int,null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class ApiParseException implements ApiException {
const ApiParseException(this.message);
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiParseExceptionCopyWith<ApiParseException> get copyWith => _$ApiParseExceptionCopyWithImpl<ApiParseException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiParseException&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'ApiException.parse(message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiParseExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiParseExceptionCopyWith(ApiParseException value, $Res Function(ApiParseException) _then) = _$ApiParseExceptionCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class _$ApiParseExceptionCopyWithImpl<$Res>
implements $ApiParseExceptionCopyWith<$Res> {
_$ApiParseExceptionCopyWithImpl(this._self, this._then);
final ApiParseException _self;
final $Res Function(ApiParseException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(ApiParseException(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class ApiCancelledException implements ApiException {
const ApiCancelledException();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiCancelledException);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'ApiException.cancelled()';
}
}
/// @nodoc
class ApiUnknownException implements ApiException {
const ApiUnknownException(this.message);
final String message;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$ApiUnknownExceptionCopyWith<ApiUnknownException> get copyWith => _$ApiUnknownExceptionCopyWithImpl<ApiUnknownException>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiUnknownException&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'ApiException.unknown(message: $message)';
}
}
/// @nodoc
abstract mixin class $ApiUnknownExceptionCopyWith<$Res> implements $ApiExceptionCopyWith<$Res> {
factory $ApiUnknownExceptionCopyWith(ApiUnknownException value, $Res Function(ApiUnknownException) _then) = _$ApiUnknownExceptionCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class _$ApiUnknownExceptionCopyWithImpl<$Res>
implements $ApiUnknownExceptionCopyWith<$Res> {
_$ApiUnknownExceptionCopyWithImpl(this._self, this._then);
final ApiUnknownException _self;
final $Res Function(ApiUnknownException) _then;
/// Create a copy of ApiException
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(ApiUnknownException(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on
+39
View File
@@ -0,0 +1,39 @@
import 'package:dio/dio.dart';
import 'api_exception.dart';
/// Maps a caught [DioException] (or any other error) to a typed
/// [ApiException]. The one place this translation happens — repositories
/// and the API client both funnel through this rather than inspecting
/// `DioException` directly.
ApiException mapDioException(Object error) {
if (error is ApiException) return error;
if (error is! DioException) {
return ApiException.unknown(error.toString());
}
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.transformTimeout:
return const ApiException.timeout();
case DioExceptionType.connectionError:
return ApiException.network(error.message ?? 'Connection failed.');
case DioExceptionType.cancel:
return const ApiException.cancelled();
case DioExceptionType.badResponse:
final response = error.response;
final statusCode = response?.statusCode ?? 0;
final data = response?.data;
final message = data is Map && data['error'] is String
? data['error'] as String
: (response?.statusMessage ?? 'Request failed.');
return ApiException.server(statusCode, message);
case DioExceptionType.badCertificate:
return ApiException.network('Bad certificate: ${error.message ?? ''}');
case DioExceptionType.unknown:
return ApiException.unknown(error.message ?? error.toString());
}
}
@@ -0,0 +1,261 @@
import 'dart:async';
import 'package:dio/dio.dart';
import '../models/models.dart';
import 'api_exception.dart';
import 'dio_error_mapper.dart';
/// Thin typed wrapper over [Dio], one method per NodeMaster REST endpoint.
/// Every method funnels errors through [mapDioException] so callers only
/// ever see [ApiException]. Holds a [CancelToken] tied to the client's own
/// lifetime (cancelled by [apiClientProvider] when the active connection
/// changes), so a request from an abandoned connection can't resolve into
/// the next connection's UI state.
class NodeMasterApiClient {
NodeMasterApiClient(this._dio, this._cancelToken);
final Dio _dio;
final CancelToken _cancelToken;
/// The overall budget for one request, enforced by Dart's own
/// `Future.timeout` rather than relying solely on Dio's
/// `connectTimeout`/`receiveTimeout` `BaseOptions`. Dio's browser HTTP
/// adapter has no socket-level hooks to honor those (the underlying
/// `fetch`/`XMLHttpRequest` APIs don't expose a connect-phase timeout),
/// so on web a request to an unreachable host would otherwise hang
/// until the browser's own (much longer, sometimes unbounded) default —
/// this timeout is what actually bounds it on every platform.
static const _requestBudget = Duration(seconds: 20);
Future<T> _guard<T>(Future<T> Function() body) async {
try {
return await body().timeout(_requestBudget);
} on ApiException {
rethrow;
} on DioException catch (e) {
throw mapDioException(e);
} on TimeoutException {
throw const ApiException.timeout();
} on TypeError catch (e) {
throw ApiException.parse(e.toString());
} on FormatException catch (e) {
throw ApiException.parse(e.toString());
}
}
Future<Response<dynamic>> _get(String path, {Map<String, dynamic>? query}) => _dio.get(
path,
queryParameters: query,
cancelToken: _cancelToken,
);
Future<Response<dynamic>> _post(String path, {Object? data}) => _dio.post(
path,
data: data,
cancelToken: _cancelToken,
);
Future<Response<dynamic>> _put(String path, {Object? data}) => _dio.put(
path,
data: data,
cancelToken: _cancelToken,
);
Future<Response<dynamic>> _delete(String path) => _dio.delete(path, cancelToken: _cancelToken);
// ---- node ----
Future<NodeInfo> getNode() => _guard(() async {
final res = await _get('/node/');
return NodeInfo.fromJson(res.data as Map<String, dynamic>);
});
Future<void> updateNode(NodeInfo node) => _guard(() async {
await _put('/node/', data: node.toJson());
});
Future<BackupConfig> getNodeBackup() => _guard(() async {
final res = await _get('/node/backup');
return BackupConfig.fromJson(res.data as Map<String, dynamic>);
});
Future<void> updateNodeBackup(BackupConfig config) => _guard(() async {
await _put('/node/backup', data: config.toJson());
});
Future<void> runNodeBackup() => _guard(() async {
await _post('/node/backup/run');
});
Future<List<BackupTarget>> getNodeBackupTargets() => _guard(() async {
final res = await _get('/node/backup/targets');
return (res.data as List<dynamic>)
.map((e) => BackupTarget.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<BackupTarget> addNodeBackupTarget(BackupTarget target) => _guard(() async {
final res = await _post('/node/backup/targets', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<BackupTarget> updateNodeBackupTarget(String targetId, BackupTarget target) => _guard(() async {
final res = await _put('/node/backup/targets/$targetId', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteNodeBackupTarget(String targetId) => _guard(() async {
await _delete('/node/backup/targets/$targetId');
});
Future<void> runNodeBackupTarget(String targetId) => _guard(() async {
await _post('/node/backup/targets/$targetId/run');
});
Future<RunState> getNodeBackupTargetProgress(String targetId) => _guard(() async {
final res = await _get('/node/backup/targets/$targetId/progress');
return RunState.fromJson(res.data as Map<String, dynamic>);
});
Future<List<RunState>> getNodeBackupProgress() => _guard(() async {
final res = await _get('/node/backup/progress');
return (res.data as List<dynamic>)
.map((e) => RunState.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<ScanResult> scan({String? folder}) => _guard(() async {
final res = await _post('/node/scan', data: folder == null ? {} : {'folder': folder});
return ScanResult.fromJson(res.data as Map<String, dynamic>);
});
Future<List<UpdateRecord>> getUpdates() => _guard(() async {
final res = await _get('/node/updates');
return (res.data as List<dynamic>)
.map((e) => UpdateRecord.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<UpdateRecord> addUpdate(UpdateRecord record) => _guard(() async {
final res = await _post('/node/updates', data: record.toJson());
return UpdateRecord.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteUpdate(String id) => _guard(() async {
await _delete('/node/updates/$id');
});
// ---- nodes (fleet registry) ----
Future<List<RemoteNode>> getAllRemoteNodes() => _guard(() async {
final res = await _get('/nodes/');
return (res.data as List<dynamic>)
.map((e) => RemoteNode.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<RemoteNode> addRemoteNode(RemoteNode node) => _guard(() async {
final res = await _post('/nodes/', data: node.toJson());
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
});
Future<RemoteNode> getRemoteNode(String id) => _guard(() async {
final res = await _get('/nodes/$id');
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
});
Future<RemoteNode> updateRemoteNode(String id, RemoteNode node) => _guard(() async {
final res = await _put('/nodes/$id', data: node.toJson());
return RemoteNode.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteRemoteNode(String id) => _guard(() async {
await _delete('/nodes/$id');
});
Future<List<AggregatedNodeStatus>> getAggregated() => _guard(() async {
final res = await _get('/nodes/aggregated');
return (res.data as List<dynamic>)
.map((e) => AggregatedNodeStatus.fromJson(e as Map<String, dynamic>))
.toList();
});
// ---- services ----
Future<List<Service>> getAllServices() => _guard(() async {
final res = await _get('/services/');
return (res.data as List<dynamic>)
.map((e) => Service.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<Service> addService(Service service) => _guard(() async {
final res = await _post('/services/', data: service.toJson());
return Service.fromJson(res.data as Map<String, dynamic>);
});
Future<Service> getService(String id) => _guard(() async {
final res = await _get('/services/$id');
return Service.fromJson(res.data as Map<String, dynamic>);
});
Future<Service> updateService(String id, Service service) => _guard(() async {
final res = await _put('/services/$id', data: service.toJson());
return Service.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteService(String id) => _guard(() async {
await _delete('/services/$id');
});
Future<void> startService(String id) => _guard(() async {
await _post('/services/$id/start');
});
Future<void> stopService(String id) => _guard(() async {
await _post('/services/$id/stop');
});
Future<void> backupService(String id) => _guard(() async {
await _post('/services/$id/backup');
});
Future<List<BackupTarget>> getServiceBackupTargets(String id) => _guard(() async {
final res = await _get('/services/$id/backup/targets');
return (res.data as List<dynamic>)
.map((e) => BackupTarget.fromJson(e as Map<String, dynamic>))
.toList();
});
Future<BackupTarget> addServiceBackupTarget(String id, BackupTarget target) => _guard(() async {
final res = await _post('/services/$id/backup/targets', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<BackupTarget> updateServiceBackupTarget(String id, String targetId, BackupTarget target) =>
_guard(() async {
final res = await _put('/services/$id/backup/targets/$targetId', data: target.toJson());
return BackupTarget.fromJson(res.data as Map<String, dynamic>);
});
Future<void> deleteServiceBackupTarget(String id, String targetId) => _guard(() async {
await _delete('/services/$id/backup/targets/$targetId');
});
Future<void> runServiceBackupTarget(String id, String targetId) => _guard(() async {
await _post('/services/$id/backup/targets/$targetId/run');
});
Future<RunState> getServiceBackupTargetProgress(String id, String targetId) => _guard(() async {
final res = await _get('/services/$id/backup/targets/$targetId/progress');
return RunState.fromJson(res.data as Map<String, dynamic>);
});
Future<List<RunState>> getServiceBackupProgress(String id) => _guard(() async {
final res = await _get('/services/$id/backup/progress');
return (res.data as List<dynamic>)
.map((e) => RunState.fromJson(e as Map<String, dynamic>))
.toList();
});
}
@@ -0,0 +1,37 @@
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
/// Mixed into a `@riverpod` `AutoDisposeAsyncNotifier` to auto-refresh it on
/// a timer while it has at least one listener — Dashboard's summary and
/// Fleet's aggregated view, specifically. The API has no push/websocket, so
/// this is the honest refresh model: a manual pull plus a background
/// timer, not a live stream.
///
/// Usage: `class FooNotifier extends _$FooNotifier with PollingMixin<Foo>`,
/// implement [interval] and [fetch], and have `build()` return
/// `startPolling()`.
mixin PollingMixin<T> on $AsyncNotifier<T> {
/// How often to refetch while this provider has at least one listener.
Duration get interval;
/// Performs the actual fetch. Thrown exceptions surface as
/// `AsyncValue.error` exactly like any other async provider.
Future<T> fetch();
/// Call from `build()`: starts the periodic timer (cancelled on dispose)
/// and returns the initial fetch.
Future<T> startPolling() {
final timer = Timer.periodic(interval, (_) => refresh());
ref.onDispose(timer.cancel);
return fetch();
}
/// Re-fetches and updates state. Deliberately never emits an
/// intermediate `AsyncLoading` — the previous data/error stays on screen
/// until the new result lands, so a background tick never flashes the
/// UI back to a loading spinner.
Future<void> refresh() async {
state = await AsyncValue.guard(fetch);
}
}
+72
View File
@@ -0,0 +1,72 @@
import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../features/backups/backups_screen.dart';
import '../../features/dashboard/dashboard_screen.dart';
import '../../features/fleet/fleet_screen.dart';
import '../../features/services/services_screen.dart';
import '../../features/settings/settings_screen.dart';
import '../../features/updates/updates_screen.dart';
import '../connections/connection_providers.dart';
import '../widgets/nav_rail_shell.dart';
import 'route_paths.dart';
part 'app_router.g.dart';
/// Bridges Riverpod's [activeConnectionProvider] to go_router's
/// [Listenable]-based `refreshListenable` — so removing/adding the active
/// connection re-evaluates the top-level redirect immediately, not just on
/// the next navigation.
class _ConnectionRefreshListenable extends ChangeNotifier {
_ConnectionRefreshListenable(Ref ref) {
ref.listen(activeConnectionProvider, (previous, next) => notifyListeners());
}
}
@Riverpod(keepAlive: true)
GoRouter appRouter(Ref ref) {
return GoRouter(
initialLocation: RoutePaths.dashboard,
refreshListenable: _ConnectionRefreshListenable(ref),
redirect: (context, state) {
final hasConnection = ref.read(activeConnectionProvider) != null;
final onSettings = state.matchedLocation == RoutePaths.settings;
if (!hasConnection && !onSettings) return RoutePaths.settings;
return null;
},
routes: [
ShellRoute(
builder: (context, state, child) {
return NavRailShell(currentPath: state.uri.path, child: child);
},
routes: [
GoRoute(
path: RoutePaths.dashboard,
builder: (context, state) => const DashboardScreen(),
),
GoRoute(
path: RoutePaths.services,
builder: (context, state) => ServicesScreen(serviceId: state.uri.queryParameters['id']),
),
GoRoute(
path: RoutePaths.backups,
builder: (context, state) => const BackupsScreen(),
),
GoRoute(
path: RoutePaths.fleet,
builder: (context, state) => const FleetScreen(),
),
GoRoute(
path: RoutePaths.updates,
builder: (context, state) => const UpdatesScreen(),
),
GoRoute(
path: RoutePaths.settings,
builder: (context, state) => const SettingsScreen(),
),
],
),
],
);
}
+51
View File
@@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_router.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(appRouter)
const appRouterProvider = AppRouterProvider._();
final class AppRouterProvider
extends $FunctionalProvider<GoRouter, GoRouter, GoRouter>
with $Provider<GoRouter> {
const AppRouterProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'appRouterProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$appRouterHash();
@$internal
@override
$ProviderElement<GoRouter> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
GoRouter create(Ref ref) {
return appRouter(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GoRouter value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GoRouter>(value),
);
}
}
String _$appRouterHash() => r'564cdc8b6bb47dea31d32b10bbfa1253170018af';
+17
View File
@@ -0,0 +1,17 @@
/// Route path constants — the single source of truth so screens and the
/// nav rail never hand-type a path string more than once.
abstract final class RoutePaths {
static const dashboard = '/dashboard';
static const services = '/services';
static const backups = '/backups';
static const fleet = '/fleet';
static const updates = '/updates';
static const settings = '/settings';
/// The service id is a query parameter (`?id=`), not a path segment.
/// `/services` and `/services?id=x` are the *same* go_router route, so
/// switching between them updates the existing page in place — no page
/// transition — which is what makes the slide-over panel read as an
/// overlay on the list rather than a navigation to a new screen.
static String serviceDetail(String id) => '/services?id=$id';
}
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
/// Exact hex values from the approved design mockup. Hand-authored rather
/// than derived from [ColorScheme.fromSeed] so these values can't drift.
abstract final class AppColors {
// Light theme
static const lightBg = Color(0xFFF2F6F6);
static const lightSurface = Color(0xFFFFFFFF);
static const lightSurfaceAlt = Color(0xFFE9EFEF);
static const lightBorder = Color(0xFFDAE3E4);
static const lightText = Color(0xFF0E1B1D);
static const lightTextMuted = Color(0xFF47585B);
static const lightTextFaint = Color(0xFF7C9195);
static const lightAccent = Color(0xFF0E8E92);
static const lightAccentStrong = Color(0xFF0A7377);
static const lightAccentInk = Color(0xFFFFFFFF);
// Dark theme
static const darkBg = Color(0xFF0C1315);
static const darkSurface = Color(0xFF121B1D);
static const darkSurfaceAlt = Color(0xFF182224);
static const darkBorder = Color(0xFF223032);
static const darkText = Color(0xFFE8F0F0);
static const darkTextMuted = Color(0xFFA9BFC2);
static const darkTextFaint = Color(0xFF6C8386);
static const darkAccent = Color(0xFF2FC6CA);
static const darkAccentStrong = Color(0xFF55D8DB);
static const darkAccentInk = Color(0xFF06282A);
// Status — semantic, deliberately distinct from the accent hue, same
// across themes except `warning`, which is re-tuned per theme below to
// hold contrast against each surface.
static const good = Color(0xFF0CA30C);
static const warningLight = Color(0xFFC8890A);
static const warningDark = Color(0xFFE3A82B);
static const serious = Color(0xFFEC835A);
static const critical = Color(0xFFD03B3B);
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
/// Typography for "technical values" (compose paths, hostnames, ids,
/// timestamps) and for tabular numerals. These are two distinct rules from
/// the design spec, not one:
/// - `dataMono*` swaps the font *family* to JetBrains Mono.
/// - `numericTabular` keeps the UI sans (Inter) but turns on the
/// `tnum` font feature so digits line up in columns (stat tiles,
/// numeric table cells) without looking like a code snippet.
@immutable
class AppDataStyles extends ThemeExtension<AppDataStyles> {
const AppDataStyles({
required this.dataMono,
required this.dataMonoSmall,
required this.numericTabular,
});
final TextStyle dataMono;
final TextStyle dataMonoSmall;
final TextStyle numericTabular;
static AppDataStyles resolve({required Color textColor, required Color mutedColor}) {
return AppDataStyles(
dataMono: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 13,
color: mutedColor,
height: 1.3,
),
dataMonoSmall: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11.5,
color: mutedColor,
height: 1.3,
),
numericTabular: TextStyle(
fontFamily: 'Inter',
color: textColor,
fontFeatures: const [FontFeature.tabularFigures()],
),
);
}
@override
AppDataStyles copyWith({
TextStyle? dataMono,
TextStyle? dataMonoSmall,
TextStyle? numericTabular,
}) {
return AppDataStyles(
dataMono: dataMono ?? this.dataMono,
dataMonoSmall: dataMonoSmall ?? this.dataMonoSmall,
numericTabular: numericTabular ?? this.numericTabular,
);
}
@override
AppDataStyles lerp(ThemeExtension<AppDataStyles>? other, double t) {
if (other is! AppDataStyles) return this;
return AppDataStyles(
dataMono: TextStyle.lerp(dataMono, other.dataMono, t)!,
dataMonoSmall: TextStyle.lerp(dataMonoSmall, other.dataMonoSmall, t)!,
numericTabular: TextStyle.lerp(numericTabular, other.numericTabular, t)!,
);
}
}
+146
View File
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
import 'app_data_styles.dart';
import 'status_colors.dart';
/// Builds the app's light/dark [ThemeData]. Colors are constructed
/// explicitly from [AppColors] rather than via [ColorScheme.fromSeed] so the
/// approved mockup's exact hex values can't drift through algorithmic tone
/// generation.
abstract final class AppTheme {
static ThemeData light() => _build(brightness: Brightness.light);
static ThemeData dark() => _build(brightness: Brightness.dark);
static ThemeData _build({required Brightness brightness}) {
final isDark = brightness == Brightness.dark;
final colorScheme = isDark
? const ColorScheme.dark(
surface: AppColors.darkBg,
onSurface: AppColors.darkText,
surfaceContainerHighest: AppColors.darkSurfaceAlt,
primary: AppColors.darkAccent,
onPrimary: AppColors.darkAccentInk,
secondary: AppColors.darkAccentStrong,
onSecondary: AppColors.darkAccentInk,
error: AppColors.critical,
onError: Colors.white,
outline: AppColors.darkBorder,
outlineVariant: AppColors.darkBorder,
)
: const ColorScheme.light(
surface: AppColors.lightBg,
onSurface: AppColors.lightText,
surfaceContainerHighest: AppColors.lightSurfaceAlt,
primary: AppColors.lightAccent,
onPrimary: AppColors.lightAccentInk,
secondary: AppColors.lightAccentStrong,
onSecondary: AppColors.lightAccentInk,
error: AppColors.critical,
onError: Colors.white,
outline: AppColors.lightBorder,
outlineVariant: AppColors.lightBorder,
);
final surfaceCard = isDark ? AppColors.darkSurface : AppColors.lightSurface;
final textColor = isDark ? AppColors.darkText : AppColors.lightText;
final textMuted = isDark ? AppColors.darkTextMuted : AppColors.lightTextMuted;
final textFaint = isDark ? AppColors.darkTextFaint : AppColors.lightTextFaint;
final border = isDark ? AppColors.darkBorder : AppColors.lightBorder;
final textTheme = TextTheme(
displaySmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 30, color: textColor, letterSpacing: -0.2),
headlineSmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 22, color: textColor, letterSpacing: -0.1),
titleLarge: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 17, color: textColor, letterSpacing: -0.1),
titleMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 14, color: textColor),
titleSmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 11, color: textFaint, letterSpacing: 0.6),
bodyLarge: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w400, fontSize: 15, color: textColor, height: 1.5),
bodyMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w400, fontSize: 13.5, color: textColor, height: 1.45),
bodySmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w500, fontSize: 12, color: textMuted),
labelLarge: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 13, color: textColor),
labelMedium: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 12, color: textMuted),
labelSmall: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 11, color: textFaint, letterSpacing: 0.5),
);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: colorScheme,
scaffoldBackgroundColor: colorScheme.surface,
canvasColor: colorScheme.surface,
dividerColor: border,
fontFamily: 'Inter',
textTheme: textTheme,
cardTheme: CardThemeData(
color: surfaceCard,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(color: border),
),
),
appBarTheme: AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: textColor,
elevation: 0,
surfaceTintColor: Colors.transparent,
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: isDark ? AppColors.darkSurfaceAlt : AppColors.lightSurfaceAlt,
indicatorColor: colorScheme.primary.withValues(alpha: 0.16),
selectedIconTheme: IconThemeData(color: colorScheme.secondary),
unselectedIconTheme: IconThemeData(color: textMuted),
selectedLabelTextStyle: TextStyle(color: colorScheme.secondary, fontWeight: FontWeight.w700, fontSize: 12.5),
unselectedLabelTextStyle: TextStyle(color: textMuted, fontWeight: FontWeight.w500, fontSize: 12.5),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
textStyle: const TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 13),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: textColor,
side: BorderSide(color: border),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
textStyle: const TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w600, fontSize: 13),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surface,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(7),
borderSide: BorderSide(color: border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(7),
borderSide: BorderSide(color: border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(7),
borderSide: BorderSide(color: colorScheme.primary, width: 1.5),
),
labelStyle: TextStyle(color: textMuted, fontSize: 12.5, fontWeight: FontWeight.w600),
),
dataTableTheme: DataTableThemeData(
headingTextStyle: TextStyle(fontFamily: 'Inter', fontWeight: FontWeight.w700, fontSize: 11, color: textFaint, letterSpacing: 0.5),
dataTextStyle: TextStyle(fontFamily: 'Inter', fontSize: 13, color: textColor),
dividerThickness: 1,
),
extensions: [
isDark ? StatusColors.dark : StatusColors.light,
AppDataStyles.resolve(textColor: textColor, mutedColor: textMuted),
],
);
}
}
+16
View File
@@ -0,0 +1,16 @@
/// Spacing scale used across the app. Plain constants rather than a
/// [ThemeExtension] — spacing doesn't change between light/dark, so there's
/// no lerp behavior to gain from routing it through the theme mechanism.
abstract final class Spacing {
static const xs = 4.0;
static const sm = 8.0;
static const md = 12.0;
static const lg = 16.0;
static const xl = 20.0;
static const xxl = 28.0;
static const xxxl = 40.0;
static const radiusSm = 6.0;
static const radiusMd = 8.0;
static const radiusLg = 10.0;
}
+82
View File
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
/// Semantic status colors (good/warning/serious/critical) used for service
/// up/down/unknown pills, backup/update success/fail pills, and stat-tile
/// severity stripes. Kept out of [ColorScheme] because Material only has a
/// single `error` role and these four are a distinct, reserved set that
/// must never be reused for anything else (e.g. a 4th categorical series).
@immutable
class StatusColors extends ThemeExtension<StatusColors> {
const StatusColors({
required this.good,
required this.warning,
required this.serious,
required this.critical,
});
final Color good;
final Color warning;
final Color serious;
final Color critical;
static const light = StatusColors(
good: AppColors.good,
warning: AppColors.warningLight,
serious: AppColors.serious,
critical: AppColors.critical,
);
static const dark = StatusColors(
good: AppColors.good,
warning: AppColors.warningDark,
serious: AppColors.serious,
critical: AppColors.critical,
);
@override
StatusColors copyWith({
Color? good,
Color? warning,
Color? serious,
Color? critical,
}) {
return StatusColors(
good: good ?? this.good,
warning: warning ?? this.warning,
serious: serious ?? this.serious,
critical: critical ?? this.critical,
);
}
@override
StatusColors lerp(ThemeExtension<StatusColors>? other, double t) {
if (other is! StatusColors) return this;
return StatusColors(
good: Color.lerp(good, other.good, t)!,
warning: Color.lerp(warning, other.warning, t)!,
serious: Color.lerp(serious, other.serious, t)!,
critical: Color.lerp(critical, other.critical, t)!,
);
}
}
/// The four states a service / backup execution / update record / remote
/// node can render as a [StatusPill]. Kept as one enum shared across
/// features rather than per-feature status strings, so pill color mapping
/// lives in exactly one place.
enum Severity { good, warning, serious, critical, neutral }
extension SeverityColor on Severity {
/// Resolves to a status color, or [muted] for [Severity.neutral] (which
/// has no reserved status hue — callers pass their theme's muted/outline
/// color for the "no data" / "not configured" case).
Color resolve(StatusColors colors, {required Color muted}) => switch (this) {
Severity.good => colors.good,
Severity.warning => colors.warning,
Severity.serious => colors.serious,
Severity.critical => colors.critical,
Severity.neutral => muted,
};
}
+26
View File
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../utils/shared_preferences_provider.dart';
part 'theme_mode_provider.g.dart';
const _themeModeKey = 'theme_mode';
@Riverpod(keepAlive: true)
class ThemeModeController extends _$ThemeModeController {
@override
ThemeMode build() {
final stored = ref.watch(sharedPreferencesProvider).getString(_themeModeKey);
return switch (stored) {
'light' => ThemeMode.light,
'dark' => ThemeMode.dark,
_ => ThemeMode.system,
};
}
Future<void> setThemeMode(ThemeMode mode) async {
state = mode;
await ref.read(sharedPreferencesProvider).setString(_themeModeKey, mode.name);
}
}
+64
View File
@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'theme_mode_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ThemeModeController)
const themeModeControllerProvider = ThemeModeControllerProvider._();
final class ThemeModeControllerProvider
extends $NotifierProvider<ThemeModeController, ThemeMode> {
const ThemeModeControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'themeModeControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$themeModeControllerHash();
@$internal
@override
ThemeModeController create() => ThemeModeController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ThemeMode value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ThemeMode>(value),
);
}
}
String _$themeModeControllerHash() =>
r'521a0916c167062954c4b944c327fbeca64c294c';
abstract class _$ThemeModeController extends $Notifier<ThemeMode> {
ThemeMode build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<ThemeMode, ThemeMode>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ThemeMode, ThemeMode>,
ThemeMode,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
import 'app_data_styles.dart';
import 'status_colors.dart';
/// Shorthand accessors so screens read `context.status.good` /
/// `context.dataStyles.dataMono` instead of the verbose
/// `Theme.of(context).extension<...>()!` at every call site.
extension ThemeX on BuildContext {
ThemeData get theme => Theme.of(this);
ColorScheme get colors => Theme.of(this).colorScheme;
TextTheme get text => Theme.of(this).textTheme;
StatusColors get status => Theme.of(this).extension<StatusColors>()!;
AppDataStyles get dataStyles => Theme.of(this).extension<AppDataStyles>()!;
}
+12
View File
@@ -0,0 +1,12 @@
/// Formats a past [DateTime] as a short relative string ("2h ago",
/// "5d ago") for activity feeds and stat-tile captions — full timestamps
/// belong in tables (as [MonoText]), not here.
String formatRelative(DateTime dateTime) {
final diff = DateTime.now().difference(dateTime);
if (diff.inSeconds < 45) return 'just now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}h ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
if (diff.inDays < 30) return '${(diff.inDays / 7).floor()}w ago';
return '${(diff.inDays / 30).floor()}mo ago';
}
@@ -0,0 +1,12 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Overridden in `main()` with an instance obtained via
/// `SharedPreferences.getInstance()` before `runApp`. Left unimplemented by
/// default so a missing override fails loudly instead of silently using a
/// throwaway in-memory instance.
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError(
'sharedPreferencesProvider must be overridden in main() with a loaded SharedPreferences instance.',
);
});
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import '../models/models.dart';
import '../network/api_exception.dart';
const _methods = BackupMethod.values;
/// Add/edit dialog for a single [BackupTarget] — shared by the node Backups
/// screen and the Services detail panel, which otherwise only differ in
/// which repository [onSave] calls. Per-target `params` editing stays out
/// of scope here, same reasoning as the rest of the app: a power-user
/// escape hatch, not worth a nested key/value editor in v1.
Future<void> showBackupTargetFormDialog(
BuildContext context, {
BackupTarget? existing,
required Future<void> Function(BackupTarget draft) onSave,
}) {
return showDialog<void>(
context: context,
builder: (context) => _BackupTargetFormDialog(existing: existing, onSave: onSave),
);
}
class _BackupTargetFormDialog extends StatefulWidget {
const _BackupTargetFormDialog({this.existing, required this.onSave});
final BackupTarget? existing;
final Future<void> Function(BackupTarget draft) onSave;
@override
State<_BackupTargetFormDialog> createState() => _BackupTargetFormDialogState();
}
class _BackupTargetFormDialogState extends State<_BackupTargetFormDialog> {
final _formKey = GlobalKey<FormState>();
late final _nameController = TextEditingController(text: widget.existing?.name);
late final _remoteController = TextEditingController(text: widget.existing?.remote);
late final _scheduleController = TextEditingController(text: widget.existing?.schedule);
late BackupMethod _method = widget.existing?.method ?? BackupMethod.restic;
bool _saving = false;
String? _error;
@override
void dispose() {
_nameController.dispose();
_remoteController.dispose();
_scheduleController.dispose();
super.dispose();
}
Future<void> _save() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() {
_saving = true;
_error = null;
});
final schedule = _scheduleController.text.trim();
final draft = (widget.existing ?? const BackupTarget(name: '', method: BackupMethod.restic, remote: '')).copyWith(
name: _nameController.text.trim(),
method: _method,
remote: _remoteController.text.trim(),
schedule: schedule.isEmpty ? null : schedule,
);
try {
await widget.onSave(draft);
if (mounted) Navigator.of(context).pop();
} catch (e) {
setState(() {
_saving = false;
_error = e is ApiException ? e.userMessage : e.toString();
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.existing == null ? 'Add backup target' : 'Edit backup target'),
content: Form(
key: _formKey,
child: SizedBox(
width: 420,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required.' : null,
),
const SizedBox(height: 14),
DropdownButtonFormField<BackupMethod>(
initialValue: _method,
decoration: const InputDecoration(labelText: 'Method'),
items: [for (final m in _methods) DropdownMenuItem(value: m, child: Text(m.name))],
onChanged: (v) => setState(() => _method = v ?? _method),
),
const SizedBox(height: 14),
TextFormField(
controller: _remoteController,
decoration: InputDecoration(
labelText: 'Remote',
hintText: switch (_method) {
BackupMethod.restic => '/mnt/backup/repo or s3:s3.amazonaws.com/bucket/path or sftp:user@host:/path',
BackupMethod.rsync => 'user@host:/path',
BackupMethod.external => 'informational only — backup is managed outside the API',
},
helperText: _method == BackupMethod.restic
? 'Restic repository — encrypted with this node\'s restic password (Settings).'
: null,
),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required.' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: _scheduleController,
decoration: const InputDecoration(
labelText: 'Schedule (optional)',
hintText: '0 2 * * * — standard 5-field cron; empty = manual only',
),
),
if (_error != null) ...[
const SizedBox(height: 12),
Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
],
],
),
),
),
actions: [
TextButton(
onPressed: _saving ? null : () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: _saving ? null : _save,
child: _saving
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('Save'),
),
],
);
}
}
+190
View File
@@ -0,0 +1,190 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/models.dart';
import '../network/api_exception.dart';
import '../theme/theme_x.dart';
import 'mono_text.dart';
/// One backup target: identity + schedule/last-run caption, and either
/// Run/Edit/Delete actions or — while a run it triggered is in flight — a
/// live progress bar polled via [fetchProgress]. Polling is a plain local
/// [Timer] scoped to this row's lifetime, not a Riverpod provider: progress
/// only matters while this exact row is on screen and something is
/// actively running, so there's nothing to share or keep alive beyond that.
class BackupTargetRow extends StatefulWidget {
const BackupTargetRow({
super.key,
required this.target,
required this.onRun,
required this.fetchProgress,
required this.onSettled,
required this.onEdit,
required this.onDelete,
});
final BackupTarget target;
final Future<void> Function() onRun;
final Future<RunState> Function() fetchProgress;
final void Function(RunState finalState) onSettled;
final VoidCallback onEdit;
final VoidCallback onDelete;
@override
State<BackupTargetRow> createState() => _BackupTargetRowState();
}
class _BackupTargetRowState extends State<BackupTargetRow> {
Timer? _timer;
RunState? _runState;
bool _starting = false;
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
Future<void> _handleRun() async {
setState(() => _starting = true);
try {
await widget.onRun();
setState(() {
_starting = false;
_runState = const RunState(running: true);
});
_poll();
_timer = Timer.periodic(const Duration(seconds: 2), (_) => _poll());
} catch (e) {
if (!mounted) return;
setState(() => _starting = false);
final message = e is ApiException ? e.userMessage : e.toString();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
}
Future<void> _poll() async {
final state = await widget.fetchProgress();
if (!mounted) return;
setState(() => _runState = state);
if (!state.running) {
_timer?.cancel();
_timer = null;
widget.onSettled(state);
}
}
@override
Widget build(BuildContext context) {
final target = widget.target;
final running = _runState?.running ?? false;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Text(target.name, style: context.text.labelLarge),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
child: Text(target.method.name, style: context.dataStyles.dataMonoSmall),
),
],
),
const SizedBox(height: 3),
MonoText(target.remote, small: true),
const SizedBox(height: 3),
Text(_caption(target), style: context.text.bodySmall),
],
),
),
const SizedBox(width: 8),
if (running)
const SizedBox.shrink()
else ...[
_starting
? const Padding(
padding: EdgeInsets.all(8),
child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)),
)
: IconButton(
tooltip: 'Run now',
icon: const Icon(Icons.play_arrow_rounded, size: 20),
onPressed: _handleRun,
),
IconButton(
tooltip: 'Edit',
icon: const Icon(Icons.edit_outlined, size: 18),
onPressed: widget.onEdit,
),
IconButton(
tooltip: 'Remove',
icon: const Icon(Icons.delete_outline, size: 18),
onPressed: widget.onDelete,
),
],
],
),
if (running) ...[
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (_runState!.percent > 0 && _runState!.percent <= 100)
? _runState!.percent / 100
: null,
minHeight: 5,
),
),
const SizedBox(height: 4),
Row(
children: [
Text(
'${_runState!.percent.toStringAsFixed(0)}%',
style: context.dataStyles.numericTabular.copyWith(fontSize: 12),
),
if (_runState!.eta != null) ...[
const SizedBox(width: 8),
Text('ETA ${_runState!.eta}', style: context.text.bodySmall),
],
if (_runState!.message?.isNotEmpty == true) ...[
const SizedBox(width: 8),
Expanded(
child: MonoText(_runState!.message!, small: true, maxLines: 1),
),
],
],
),
],
],
),
);
}
String _caption(BackupTarget target) {
final dateFormat = DateFormat('MMM d, HH:mm');
final parts = <String>[
target.schedule?.isNotEmpty == true ? 'Schedule: ${target.schedule}' : 'Manual only',
if (target.lastRun != null) 'last ${dateFormat.format(target.lastRun!.toLocal())}',
if (target.nextRun != null) 'next ${dateFormat.format(target.nextRun!.toLocal())}',
];
return parts.join(' · ');
}
}
+61
View File
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import '../models/models.dart';
import '../theme/theme_x.dart';
import 'backup_target_row.dart';
/// A list of [BackupTargetRow]s plus an "Add target" action — shared by the
/// node Backups screen and the Services detail panel.
class BackupTargetsList extends StatelessWidget {
const BackupTargetsList({
super.key,
required this.targets,
required this.onAdd,
required this.onEdit,
required this.onDelete,
required this.onRun,
required this.fetchProgress,
required this.onSettled,
});
final List<BackupTarget> targets;
final VoidCallback onAdd;
final void Function(BackupTarget target) onEdit;
final void Function(BackupTarget target) onDelete;
final Future<void> Function(BackupTarget target) onRun;
final Future<RunState> Function(BackupTarget target) fetchProgress;
final void Function(BackupTarget target, RunState finalState) onSettled;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (targets.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text('No backup targets configured.', style: context.text.bodySmall),
)
else
for (var i = 0; i < targets.length; i++) ...[
if (i > 0) const Divider(height: 1),
BackupTargetRow(
key: ValueKey(targets[i].id),
target: targets[i],
onRun: () => onRun(targets[i]),
fetchProgress: () => fetchProgress(targets[i]),
onSettled: (state) => onSettled(targets[i], state),
onEdit: () => onEdit(targets[i]),
onDelete: () => onDelete(targets[i]),
),
],
const SizedBox(height: 8),
OutlinedButton.icon(
onPressed: onAdd,
icon: const Icon(Icons.add, size: 16),
label: const Text('Add target'),
),
],
);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import '../theme/theme_x.dart';
/// The bordered, horizontally-scrollable card wrapper used by every dense
/// table in the app (Services, Fleet, Updates, Backup history). A thin
/// shell around [DataTable] — column/row content stays screen-specific,
/// this just guarantees the same card chrome, spacing, and empty-state
/// fallback everywhere.
class DataTableScaffold extends StatelessWidget {
const DataTableScaffold({
super.key,
required this.columns,
required this.rows,
this.empty,
this.columnSpacing = 28,
this.horizontalMargin = 16,
});
final List<DataColumn> columns;
final List<DataRow> rows;
final Widget? empty;
final double columnSpacing;
final double horizontalMargin;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: context.colors.surface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: context.colors.outlineVariant),
),
clipBehavior: Clip.antiAlias,
child: rows.isEmpty && empty != null
? empty
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: MediaQuery.sizeOf(context).width),
child: DataTable(
showCheckboxColumn: false,
columns: columns,
rows: rows,
columnSpacing: columnSpacing,
horizontalMargin: horizontalMargin,
headingRowHeight: 38,
dataRowMinHeight: 46,
dataRowMaxHeight: 58,
dividerThickness: 1,
),
),
),
);
}
}
+50
View File
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import '../theme/theme_x.dart';
/// Centered "nothing here yet" placeholder — no services discovered, no
/// connections saved, no update history. An optional action gives the user
/// the next step rather than a dead end.
class EmptyState extends StatelessWidget {
const EmptyState({
super.key,
required this.icon,
required this.title,
this.message,
this.actionLabel,
this.onAction,
});
final IconData icon;
final String title;
final String? message;
final String? actionLabel;
final VoidCallback? onAction;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 30, color: context.colors.outline),
const SizedBox(height: 12),
Text(title, style: context.text.titleMedium, textAlign: TextAlign.center),
if (message != null) ...[
const SizedBox(height: 4),
Text(
message!,
style: context.text.bodySmall,
textAlign: TextAlign.center,
),
],
if (actionLabel != null && onAction != null) ...[
const SizedBox(height: 16),
OutlinedButton(onPressed: onAction, child: Text(actionLabel!)),
],
],
),
);
}
}
+38
View File
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import '../theme/theme_x.dart';
/// Inline error state for a failed fetch — an `AsyncValue.error` case in a
/// screen body. Deliberately takes a plain [message] string rather than an
/// `ApiException` so `core/widgets` has no dependency on `core/network`;
/// callers resolve `exception.userMessage` before passing it in.
class ErrorBanner extends StatelessWidget {
const ErrorBanner({super.key, required this.message, this.onRetry});
final String message;
final VoidCallback? onRetry;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Color.alphaBlend(context.status.critical.withValues(alpha: 0.08), context.colors.surface),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: context.status.critical.withValues(alpha: 0.4)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.error_outline, size: 18, color: context.status.critical),
const SizedBox(width: 12),
Expanded(child: Text(message, style: context.text.bodyMedium)),
if (onRetry != null) ...[
const SizedBox(width: 12),
TextButton(onPressed: onRetry, child: const Text('Retry')),
],
],
),
);
}
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import '../theme/theme_x.dart';
/// A technical value — compose file path, hostname, id, URL — rendered in
/// the monospace data face so it reads as "an exact string", distinct from
/// prose. Use for anything a user might copy-paste or match against a log.
class MonoText extends StatelessWidget {
const MonoText(
this.value, {
super.key,
this.small = false,
this.color,
this.maxLines = 1,
this.overflow = TextOverflow.ellipsis,
});
final String value;
final bool small;
final Color? color;
final int? maxLines;
final TextOverflow overflow;
@override
Widget build(BuildContext context) {
final base = small ? context.dataStyles.dataMonoSmall : context.dataStyles.dataMono;
return Text(
value,
maxLines: maxLines,
overflow: overflow,
style: color == null ? base : base.copyWith(color: color),
);
}
}
+295
View File
@@ -0,0 +1,295 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../connections/connection_providers.dart';
import '../routing/route_paths.dart';
import '../theme/theme_mode_provider.dart';
import '../theme/theme_x.dart';
class _NavItem {
const _NavItem(this.path, this.label, this.icon, this.selectedIcon);
final String path;
final String label;
final IconData icon;
final IconData selectedIcon;
}
const _navItems = [
_NavItem(RoutePaths.dashboard, 'Dashboard', Icons.grid_view_outlined, Icons.grid_view_rounded),
_NavItem(RoutePaths.services, 'Services', Icons.layers_outlined, Icons.layers_rounded),
_NavItem(RoutePaths.backups, 'Backups', Icons.cloud_outlined, Icons.cloud_rounded),
_NavItem(RoutePaths.fleet, 'Fleet', Icons.hub_outlined, Icons.hub_rounded),
_NavItem(RoutePaths.updates, 'Updates', Icons.history_outlined, Icons.history_rounded),
_NavItem(RoutePaths.settings, 'Settings', Icons.tune_outlined, Icons.tune_rounded),
];
/// The persistent left nav rail + top bar frame wrapping every screen, per
/// the approved mockup. Wraps go_router's `ShellRoute` child.
class NavRailShell extends ConsumerWidget {
const NavRailShell({super.key, required this.currentPath, required this.child});
final String currentPath;
final Widget child;
int get _selectedIndex {
final index = _navItems.indexWhere((i) => currentPath.startsWith(i.path));
return index == -1 ? 0 : index;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final narrow = MediaQuery.sizeOf(context).width < 860;
return Scaffold(
body: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Rail(selectedIndex: _selectedIndex, narrow: narrow),
const VerticalDivider(width: 1, thickness: 1),
Expanded(
child: Column(
children: [
_TopBar(currentPath: currentPath),
const Divider(height: 1, thickness: 1),
Expanded(child: child),
],
),
),
],
),
);
}
}
class _Rail extends ConsumerWidget {
const _Rail({required this.selectedIndex, required this.narrow});
final int selectedIndex;
final bool narrow;
@override
Widget build(BuildContext context, WidgetRef ref) {
final activeConnection = ref.watch(activeConnectionProvider);
return SizedBox(
width: narrow ? 72 : 226,
child: Container(
color: context.theme.navigationRailTheme.backgroundColor,
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(14, 18, 14, 20),
child: Row(
mainAxisAlignment: narrow ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: context.colors.primary,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.hub_rounded, size: 16, color: context.colors.onPrimary),
),
if (!narrow) ...[
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('NodeMaster', style: context.text.titleMedium?.copyWith(fontSize: 14.5)),
Text('Manager', style: context.text.bodySmall),
],
),
),
],
],
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8),
children: [
for (var i = 0; i < _navItems.length; i++)
_RailButton(item: _navItems[i], selected: i == selectedIndex, narrow: narrow),
],
),
),
Padding(
padding: const EdgeInsets.all(12),
child: _ConnectionChip(name: activeConnection?.name, url: activeConnection?.baseUrl, narrow: narrow),
),
],
),
),
),
);
}
}
class _RailButton extends StatelessWidget {
const _RailButton({required this.item, required this.selected, required this.narrow});
final _NavItem item;
final bool selected;
final bool narrow;
@override
Widget build(BuildContext context) {
final fg = selected ? context.colors.secondary : context.text.bodyMedium?.color;
final bg = selected ? context.colors.primary.withValues(alpha: 0.14) : Colors.transparent;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 1.5),
child: Material(
color: bg,
borderRadius: BorderRadius.circular(7),
child: InkWell(
borderRadius: BorderRadius.circular(7),
onTap: () => context.go(item.path),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: narrow ? 0 : 10, vertical: 9),
child: Row(
mainAxisAlignment: narrow ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Icon(selected ? item.selectedIcon : item.icon, size: 18, color: fg),
if (!narrow) ...[
const SizedBox(width: 10),
Text(
item.label,
style: context.text.bodyMedium?.copyWith(
color: fg,
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
),
),
],
],
),
),
),
),
);
}
}
class _ConnectionChip extends StatelessWidget {
const _ConnectionChip({required this.name, required this.url, required this.narrow});
final String? name;
final String? url;
final bool narrow;
@override
Widget build(BuildContext context) {
final dot = Container(
width: 7,
height: 7,
decoration: BoxDecoration(
color: name == null ? context.colors.outline : context.status.good,
shape: BoxShape.circle,
),
);
if (narrow) return Center(child: dot);
return Container(
padding: const EdgeInsets.all(9),
decoration: BoxDecoration(
color: context.colors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: context.colors.outlineVariant),
),
child: Row(
children: [
dot,
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
name ?? 'No connection',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.text.labelLarge?.copyWith(fontSize: 12),
),
if (url != null)
Text(
url!.replaceFirst(RegExp(r'^https?://'), ''),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.dataStyles.dataMonoSmall,
),
],
),
),
],
),
);
}
}
const _titles = <String, (String, String Function(WidgetRef))>{
RoutePaths.dashboard: ('Dashboard', _connSubtitle),
RoutePaths.services: ('Services', _connSubtitle),
RoutePaths.backups: ('Backups', _connSubtitle),
RoutePaths.fleet: ('Fleet', _connSubtitle),
RoutePaths.updates: ('Updates', _connSubtitle),
RoutePaths.settings: ('Settings', _connSubtitle),
};
String _connSubtitle(WidgetRef ref) {
final connection = ref.watch(activeConnectionProvider);
return connection == null ? 'No connection configured' : 'Connected to ${connection.name}';
}
class _TopBar extends ConsumerWidget {
const _TopBar({required this.currentPath});
final String currentPath;
@override
Widget build(BuildContext context, WidgetRef ref) {
final entry = _titles.entries.firstWhere(
(e) => currentPath.startsWith(e.key),
orElse: () => _titles.entries.first,
);
final title = entry.value.$1;
final subtitle = entry.value.$2(ref);
final mode = ref.watch(themeModeControllerProvider);
final effectiveDark = switch (mode) {
ThemeMode.dark => true,
ThemeMode.light => false,
ThemeMode.system => MediaQuery.platformBrightnessOf(context) == Brightness.dark,
};
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: context.text.titleLarge),
Text(subtitle, style: context.text.bodySmall),
],
),
),
IconButton(
tooltip: effectiveDark ? 'Switch to light theme' : 'Switch to dark theme',
icon: Icon(effectiveDark ? Icons.dark_mode_outlined : Icons.light_mode_outlined),
onPressed: () => ref
.read(themeModeControllerProvider.notifier)
.setThemeMode(effectiveDark ? ThemeMode.light : ThemeMode.dark),
),
],
),
);
}
}
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import '../theme/theme_x.dart';
/// A right-anchored detail panel over the still-visible list behind it —
/// the "inspect a record without losing list context" pattern used by
/// Services (and reused by Fleet's per-node cards). Place as the last child
/// of a [Stack] alongside the screen's main content; it fills the stack
/// itself via [Positioned.fill] and no-ops when [open] is false.
class SlideOverPanel extends StatelessWidget {
const SlideOverPanel({
super.key,
required this.open,
required this.onClose,
required this.title,
this.subtitle,
required this.body,
this.actions = const [],
this.headerActions = const [],
this.width = 420,
});
final bool open;
final VoidCallback onClose;
final String title;
final Widget? subtitle;
final Widget body;
final List<Widget> actions;
/// Icon buttons shown before the close button — for actions that belong
/// with the record identity (e.g. Edit) rather than the primary action
/// row at the bottom, which stays reserved for the few actions that need
/// full label width.
final List<Widget> headerActions;
final double width;
@override
Widget build(BuildContext context) {
final reduceMotion = MediaQuery.disableAnimationsOf(context);
final duration = reduceMotion ? Duration.zero : const Duration(milliseconds: 200);
return Positioned.fill(
child: IgnorePointer(
ignoring: !open,
child: Stack(
children: [
AnimatedOpacity(
opacity: open ? 1 : 0,
duration: duration,
child: GestureDetector(
onTap: onClose,
child: Container(color: Colors.black.withValues(alpha: 0.35)),
),
),
Align(
alignment: Alignment.centerRight,
child: ClipRect(
child: AnimatedSlide(
offset: open ? Offset.zero : const Offset(1, 0),
duration: duration,
curve: Curves.easeOutCubic,
child: Container(
width: width,
constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.92),
height: double.infinity,
decoration: BoxDecoration(
color: context.colors.surface,
border: Border(left: BorderSide(color: context.colors.outlineVariant)),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.18), blurRadius: 48, offset: const Offset(-8, 0)),
],
),
child: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 12, 18),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: context.text.titleLarge),
if (subtitle != null) ...[
const SizedBox(height: 6),
subtitle!,
],
],
),
),
...headerActions,
IconButton(
onPressed: onClose,
icon: const Icon(Icons.close, size: 19),
tooltip: 'Close',
),
],
),
),
const Divider(height: 1),
Expanded(child: body),
if (actions.isNotEmpty) ...[
const Divider(height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
for (var i = 0; i < actions.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
Expanded(child: actions[i]),
],
],
),
),
],
],
),
),
),
),
),
),
],
),
),
);
}
}
+106
View File
@@ -0,0 +1,106 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../theme/theme_x.dart';
/// A single-series trend line — currently just backup-duration history.
/// Hand-rolled `CustomPainter` rather than a charting dependency: one chart
/// in the whole app, a specific visual spec (faint grid, thin line,
/// emphasized endpoint), full control for near-zero cost.
class Sparkline extends StatelessWidget {
const Sparkline({super.key, required this.values, this.width = 240, this.height = 52});
final List<double> values;
final double width;
final double height;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: CustomPaint(
painter: _SparklinePainter(
values: values,
lineColor: context.colors.primary,
gridColor: context.colors.outlineVariant,
),
),
);
}
}
class _SparklinePainter extends CustomPainter {
_SparklinePainter({required this.values, required this.lineColor, required this.gridColor});
final List<double> values;
final Color lineColor;
final Color gridColor;
static const _topPad = 6.0;
static const _bottomPad = 6.0;
@override
void paint(Canvas canvas, Size size) {
if (values.isEmpty) return;
final minV = values.reduce(math.min);
final maxV = values.reduce(math.max);
final range = (maxV - minV).abs() < 1e-9 ? 1.0 : maxV - minV;
final plotHeight = size.height - _topPad - _bottomPad;
final gridPaint = Paint()
..color = gridColor
..strokeWidth = 1;
canvas.drawLine(Offset(0, _topPad), Offset(size.width, _topPad), gridPaint);
canvas.drawLine(
Offset(0, size.height - _bottomPad),
Offset(size.width, size.height - _bottomPad),
gridPaint,
);
if (values.length == 1) {
final y = _topPad + plotHeight / 2;
canvas.drawCircle(Offset(size.width, y), 3.4, Paint()..color = lineColor);
return;
}
final dx = size.width / (values.length - 1);
final points = [
for (var i = 0; i < values.length; i++)
Offset(i * dx, _topPad + plotHeight - ((values[i] - minV) / range) * plotHeight),
];
final fillPath = Path()..moveTo(points.first.dx, size.height - _bottomPad);
for (final p in points) {
fillPath.lineTo(p.dx, p.dy);
}
fillPath
..lineTo(points.last.dx, size.height - _bottomPad)
..close();
canvas.drawPath(fillPath, Paint()..color = lineColor.withValues(alpha: 0.14));
final linePath = Path()..moveTo(points.first.dx, points.first.dy);
for (final p in points.skip(1)) {
linePath.lineTo(p.dx, p.dy);
}
canvas.drawPath(
linePath,
Paint()
..color = lineColor
..style = PaintingStyle.stroke
..strokeWidth = 2
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round,
);
canvas.drawCircle(points.last, 3.4, Paint()..color = lineColor);
}
@override
bool shouldRepaint(covariant _SparklinePainter oldDelegate) =>
oldDelegate.values != values ||
oldDelegate.lineColor != lineColor ||
oldDelegate.gridColor != gridColor;
}
+88
View File
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import '../theme/status_colors.dart';
import '../theme/theme_x.dart';
/// A dashboard summary tile: an uppercase label, a large tabular-nums value
/// (with an optional muted trailing qualifier, e.g. "7 / 9"), a caption, and
/// a left severity stripe communicating worst-case state at a glance —
/// severity is a first-class visual channel here, not just the number.
class StatTile extends StatelessWidget {
const StatTile({
super.key,
required this.label,
required this.value,
this.valueQualifier,
this.caption,
this.severity = Severity.good,
});
final String label;
final String value;
final String? valueQualifier;
final String? caption;
final Severity severity;
@override
Widget build(BuildContext context) {
final stripeColor = severity.resolve(context.status, muted: context.colors.outline);
return Container(
decoration: BoxDecoration(
color: context.colors.surface,
borderRadius: BorderRadius.circular(9),
border: Border.all(color: context.colors.outlineVariant),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 2, offset: const Offset(0, 1)),
],
),
clipBehavior: Clip.antiAlias,
child: IntrinsicHeight(
child: Row(
children: [
Container(width: 3, color: stripeColor),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(13, 14, 13, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(label, style: context.text.titleSmall),
const SizedBox(height: 8),
Text.rich(
TextSpan(
style: context.dataStyles.numericTabular.copyWith(
fontSize: 24,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
),
children: [
TextSpan(text: value),
if (valueQualifier != null)
TextSpan(
text: ' $valueQualifier',
style: context.text.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
),
],
),
),
if (caption != null) ...[
const SizedBox(height: 4),
Text(
caption!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.text.bodySmall,
),
],
],
),
),
),
],
),
),
);
}
}
+57
View File
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import '../theme/status_colors.dart';
import '../theme/theme_x.dart';
/// A dot + label badge encoding state — service up/down/unknown, backup or
/// update success/fail, node reachable/unreachable. The same component
/// everywhere a status appears so a given [Severity] always reads the same
/// color, matching the design rule that status color is reserved and never
/// doubles as the brand accent.
class StatusPill extends StatelessWidget {
const StatusPill({super.key, required this.label, required this.severity});
final String label;
final Severity severity;
@override
Widget build(BuildContext context) {
final color = severity.resolve(context.status, muted: context.colors.outline);
return Container(
padding: const EdgeInsets.fromLTRB(7, 3, 9, 3),
decoration: BoxDecoration(
// 8% tint, not 14% — measured against the dark surface, a same-hue
// tint background caps critical's text contrast around ~3.5:1 no
// matter the blend ratio (text and background converge toward the
// same hue as alpha rises, and approach the plain-surface ceiling
// as it falls); 8% is close to that ceiling while keeping
// good/warning/serious comfortably at 4.5:1+. Below AA-for-text on
// its own, mitigated the same way the design system's status
// palette always mitigates this: label text, never color alone.
color: severity == Severity.neutral
? context.colors.surfaceContainerHighest
: Color.alphaBlend(color.withValues(alpha: 0.08), context.colors.surface),
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
label,
style: context.text.labelMedium?.copyWith(
color: severity == Severity.neutral ? context.colors.onSurfaceVariant : color,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}