From 09431a0b081903c354ba4a127c457a0b6f635f5d Mon Sep 17 00:00:00 2001 From: GT610 <79314033+GT-610@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:25:48 +0800 Subject: [PATCH] fix(pve): Fix connection issues and add more error handlings (#1081) * feat(PVE): Added display of PVE connection loading steps Added a detailed display of loading steps during the PVE connection process, including stages such as establishing an SSH tunnel, authentication, and data retrieval Also optimized the sorting of PVE storage content and the logic for handling connection errors * feat(pve): Added error handling and prompts for PVE two-factor authentication Added error handling for PVE servers when two-factor authentication is enabled, along with relevant error types and localized prompts * feat(PVE): Added support for PVE passwords during key-based authentication - Added the `pvePwd` field to the `ServerCustom` model - Added a PVE password input field to the edit page (displayed only during key-based authentication) - Updated multilingual files to support PVE-related loading states and password prompts - Optimized PVE connection logic to support password verification during key-based authentication --- lib/data/model/app/error.dart | 2 +- lib/data/model/server/custom.dart | 5 + lib/data/model/server/custom.g.dart | 2 + lib/data/model/server/pve.dart | 6 +- lib/data/provider/container.g.dart | 2 +- lib/data/provider/pve.dart | 161 +++++++++++++++---------- lib/data/provider/pve.freezed.dart | 43 ++++--- lib/data/provider/pve.g.dart | 2 +- lib/data/provider/server/all.g.dart | 2 +- lib/data/provider/server/single.g.dart | 2 +- lib/data/provider/systemd.g.dart | 2 +- lib/data/res/github_id.dart | 1 + lib/generated/l10n/l10n.dart | 42 +++++++ lib/generated/l10n/l10n_de.dart | 23 ++++ lib/generated/l10n/l10n_en.dart | 23 ++++ lib/generated/l10n/l10n_es.dart | 23 ++++ lib/generated/l10n/l10n_fr.dart | 23 ++++ lib/generated/l10n/l10n_id.dart | 23 ++++ lib/generated/l10n/l10n_it.dart | 23 ++++ lib/generated/l10n/l10n_ja.dart | 23 ++++ lib/generated/l10n/l10n_ko.dart | 23 ++++ lib/generated/l10n/l10n_nl.dart | 23 ++++ lib/generated/l10n/l10n_pt.dart | 23 ++++ lib/generated/l10n/l10n_ru.dart | 23 ++++ lib/generated/l10n/l10n_tr.dart | 23 ++++ lib/generated/l10n/l10n_uk.dart | 23 ++++ lib/generated/l10n/l10n_zh.dart | 42 +++++++ lib/hive/hive_adapters.g.dart | 7 +- lib/hive/hive_adapters.g.yaml | 4 +- lib/l10n/app_en.arb | 7 ++ lib/l10n/app_zh.arb | 7 ++ lib/l10n/app_zh_tw.arb | 7 ++ lib/view/page/pve.dart | 40 ++++-- lib/view/page/server/edit/actions.dart | 2 + lib/view/page/server/edit/edit.dart | 2 + lib/view/page/server/edit/widget.dart | 63 ++++++---- pubspec.lock | 28 ++--- 37 files changed, 641 insertions(+), 139 deletions(-) diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart index ad6ecf75..01ec58c4 100644 --- a/lib/data/model/app/error.dart +++ b/lib/data/model/app/error.dart @@ -56,7 +56,7 @@ class WebdavErr extends Err { String? get solution => null; } -enum PveErrType { unknown, net, loginFailed } +enum PveErrType { unknown, net, loginFailed, needTfa } class PveErr extends Err { const PveErr({required super.type, super.message}); diff --git a/lib/data/model/server/custom.dart b/lib/data/model/server/custom.dart index 0edf4db7..10ec85ae 100644 --- a/lib/data/model/server/custom.dart +++ b/lib/data/model/server/custom.dart @@ -11,6 +11,8 @@ final class ServerCustom { final bool pveIgnoreCert; + final String? pvePwd; + /// {"title": "cmd"} final Map? cmds; @@ -28,6 +30,7 @@ final class ServerCustom { //this.temperature, this.pveAddr, this.pveIgnoreCert = false, + this.pvePwd, this.cmds, this.preferTempDev, this.logoUrl, @@ -45,6 +48,7 @@ final class ServerCustom { //other.temperature == temperature && other.pveAddr == pveAddr && other.pveIgnoreCert == pveIgnoreCert && + other.pvePwd == pvePwd && other.cmds == cmds && other.preferTempDev == preferTempDev && other.logoUrl == logoUrl && @@ -57,6 +61,7 @@ final class ServerCustom { //temperature.hashCode ^ pveAddr.hashCode ^ pveIgnoreCert.hashCode ^ + pvePwd.hashCode ^ cmds.hashCode ^ preferTempDev.hashCode ^ logoUrl.hashCode ^ diff --git a/lib/data/model/server/custom.g.dart b/lib/data/model/server/custom.g.dart index c67bbeb4..1619d62d 100644 --- a/lib/data/model/server/custom.g.dart +++ b/lib/data/model/server/custom.g.dart @@ -9,6 +9,7 @@ part of 'custom.dart'; ServerCustom _$ServerCustomFromJson(Map json) => ServerCustom( pveAddr: json['pveAddr'] as String?, pveIgnoreCert: json['pveIgnoreCert'] as bool? ?? false, + pvePwd: json['pvePwd'] as String?, cmds: (json['cmds'] as Map?)?.map( (k, e) => MapEntry(k, e as String), ), @@ -22,6 +23,7 @@ Map _$ServerCustomToJson(ServerCustom instance) => { 'pveAddr': ?instance.pveAddr, 'pveIgnoreCert': instance.pveIgnoreCert, + 'pvePwd': ?instance.pvePwd, 'cmds': ?instance.cmds, 'preferTempDev': ?instance.preferTempDev, 'logoUrl': ?instance.logoUrl, diff --git a/lib/data/model/server/pve.dart b/lib/data/model/server/pve.dart index 077a551d..0c81e88d 100644 --- a/lib/data/model/server/pve.dart +++ b/lib/data/model/server/pve.dart @@ -293,6 +293,10 @@ final class PveStorage extends PveResIface implements PveCtrlIface { }); static PveStorage fromJson(Map json) { + final rawContent = json['content'] as String?; + final contentParts = rawContent?.split(','); + contentParts?.sort(); + final content = contentParts?.join(',') ?? rawContent ?? ''; return PveStorage( id: json['id'], type: PveResType.storage, @@ -300,7 +304,7 @@ final class PveStorage extends PveResIface implements PveCtrlIface { node: json['node'], status: json['status'], plugintype: json['plugintype'], - content: json['content'], + content: content, shared: json['shared'], disk: json['disk'], maxdisk: json['maxdisk'], diff --git a/lib/data/provider/container.g.dart b/lib/data/provider/container.g.dart index 77647101..8254ac89 100644 --- a/lib/data/provider/container.g.dart +++ b/lib/data/provider/container.g.dart @@ -58,7 +58,7 @@ final class ContainerNotifierProvider } } -String _$containerNotifierHash() => r'85457ec75264199c284572ee45beeaccba2044a1'; +String _$containerNotifierHash() => r'2f8eb969f0e66e28f60b6fc11169e8f28315ed32'; final class ContainerNotifierFamily extends $Family with diff --git a/lib/data/provider/pve.dart b/lib/data/provider/pve.dart index 746fc998..c40d1ddc 100644 --- a/lib/data/provider/pve.dart +++ b/lib/data/provider/pve.dart @@ -8,7 +8,6 @@ import 'package:dio/io.dart'; import 'package:fl_lib/fl_lib.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/pve.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; @@ -19,6 +18,13 @@ part 'pve.g.dart'; typedef PveCtrlFunc = Future Function(String node, String id); +enum PveLoadingStep { + none, + forwarding, + loggingIn, + fetchingData, +} + @freezed abstract class PveState with _$PveState { const factory PveState({ @@ -27,43 +33,51 @@ abstract class PveState with _$PveState { @Default(null) String? release, @Default(false) bool isBusy, @Default(false) bool isConnected, + @Default(PveLoadingStep.none) PveLoadingStep loadingStep, }) = _PveState; } @riverpod class PveNotifier extends _$PveNotifier { - late final Spi spi; - late String addr; - late final SSHClient _client; - late final ServerSocket _serverSocket; + String? addr; + ServerSocket? _serverSocket; final List _forwards = []; int _localPort = 0; - late final Dio session; - late final bool _ignoreCert; + Dio? _session; + bool _ignoreCert = false; + + Dio get session => _session!; + String get addrValue => addr!; + + SSHClient get _client { + final serverState = ref.read(serverProvider(spiParam.id)); + final client = serverState.client; + if (client == null) { + throw PveErr(type: PveErrType.net, message: 'Server client is null'); + } + return client; + } @override PveState build(Spi spiParam) { - spi = spiParam; - final serverState = ref.watch(serverProvider(spi.id)); - final client = serverState.client; - if (client == null) { - return const PveState(error: PveErr(type: PveErrType.net, message: 'Server client is null')); + ref.onDispose(() => dispose()); + final serverState = ref.watch(serverProvider(spiParam.id)); + if (serverState.client == null) { + return const PveState(loadingStep: PveLoadingStep.forwarding); } - _client = client; - final addr = spi.custom?.pveAddr; - if (addr == null) { + final pveAddr = spiParam.custom?.pveAddr; + if (pveAddr == null) { return PveState(error: PveErr(type: PveErrType.net, message: 'PVE address is null')); } - this.addr = addr; - _ignoreCert = spi.custom?.pveIgnoreCert ?? false; + addr = pveAddr; + _ignoreCert = spiParam.custom?.pveIgnoreCert ?? false; _initSession(); - // Async initialization Future.microtask(() => _init()); - return const PveState(); + return const PveState(loadingStep: PveLoadingStep.forwarding); } void _initSession() { - session = Dio() + _session = Dio() ..httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final client = HttpClient(); @@ -79,35 +93,53 @@ class PveNotifier extends _$PveNotifier { bool get onlyOneNode => state.data?.nodes.length == 1; + Future reconnect() async { + state = state.copyWith(error: null, isConnected: false, loadingStep: PveLoadingStep.forwarding); + await _init(); + } + Future _init() async { try { + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.forwarding); await _forward(); + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.loggingIn); await _login(); + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.fetchingData); await _getRelease(); + if (!ref.mounted) return; state = state.copyWith(isConnected: true); - } on PveErr { - state = state.copyWith(error: PveErr(type: PveErrType.loginFailed, message: l10n.pveLoginFailed)); + await list(); + if (!ref.mounted) return; + state = state.copyWith(loadingStep: PveLoadingStep.none); + } on PveErr catch (e) { + if (!ref.mounted) return; + state = state.copyWith(error: e, loadingStep: PveLoadingStep.none); } catch (e, s) { + if (!ref.mounted) return; Loggers.app.warning('PVE init failed', e, s); - state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString())); + state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString()), loadingStep: PveLoadingStep.none); } } Future _forward() async { - final url = Uri.parse(addr); + final url = Uri.parse(addrValue); if (_localPort == 0) { _serverSocket = await ServerSocket.bind('localhost', 0); - _localPort = _serverSocket.port; - _serverSocket.listen((socket) async { - final forward = await _client.forwardLocal(url.host, url.port); - _forwards.add(forward); - forward.stream.cast>().pipe(socket); - socket.cast>().pipe(forward.sink); + _localPort = _serverSocket!.port; + _serverSocket!.listen((socket) async { + try { + final forward = await _client.forwardLocal(url.host, url.port); + _forwards.add(forward); + forward.stream.cast>().pipe(socket); + socket.cast>().pipe(forward.sink); + } catch (e, s) { + Loggers.app.warning('PVE forward failed', e, s); + socket.destroy(); + } }); - final newUrl = Uri.parse( - addr, - ).replace(host: 'localhost', port: _localPort).toString(); - dprint('Forwarding $newUrl to $addr'); } } @@ -116,15 +148,6 @@ class PveNotifier extends _$PveNotifier { String? proxyHost, int? proxyPort, ) async { - /* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0); - final _localPort = serverSocket.port; - serverSocket.listen((socket) async { - final forward = await _client.forwardLocal(url.host, url.port); - forwards.add(forward); - forward.stream.cast>().pipe(socket); - socket.cast>().pipe(forward.sink); - });*/ - if (url.isScheme('https')) { return SecureSocket.startConnect( 'localhost', @@ -137,11 +160,16 @@ class PveNotifier extends _$PveNotifier { } Future _login() async { + final useKeyAuth = spiParam.keyId != null; + final password = useKeyAuth ? spiParam.custom?.pvePwd : spiParam.pwd; + if (password == null) { + throw PveErr(type: PveErrType.loginFailed, message: 'PVE password is required. Please set it in server settings.'); + } final resp = await session.post( - '$addr/api2/extjs/access/ticket', + '$addrValue/api2/extjs/access/ticket', data: { - 'username': spi.user, - 'password': spi.pwd, + 'username': spiParam.user, + 'password': password, 'realm': 'pam', 'new-format': '1', }, @@ -149,21 +177,26 @@ class PveNotifier extends _$PveNotifier { headers: {HttpHeaders.contentTypeHeader: Headers.jsonContentType}, ), ); - try { - final ticket = resp.data['data']['ticket']; - session.options.headers['CSRFPreventionToken'] = - resp.data['data']['CSRFPreventionToken']; - session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket'; - } catch (e) { - throw PveErr(type: PveErrType.loginFailed, message: e.toString()); + + final data = resp.data['data']; + if (data['NeedTFA'] == 1) { + throw PveErr(type: PveErrType.needTfa, message: 'Two-factor authentication is not supported yet. Please disable OTP on the PVE server and try again.'); } + + _setAuthHeaders(data); + } + + void _setAuthHeaders(Map data) { + final ticket = data['ticket']; + session.options.headers['CSRFPreventionToken'] = data['CSRFPreventionToken']; + session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket'; } /// Returns true if the PVE version is 8.0 or later Future _getRelease() async { - final resp = await session.get('$addr/api2/extjs/version'); + final resp = await session.get('$addrValue/api2/extjs/version'); final version = resp.data['data']['release'] as String?; - if (version != null) { + if (version != null && ref.mounted) { state = state.copyWith(release: version); } } @@ -172,25 +205,29 @@ class PveNotifier extends _$PveNotifier { if (!state.isConnected || state.isBusy) return; state = state.copyWith(isBusy: true); try { - final resp = await session.get('$addr/api2/json/cluster/resources'); + final resp = await session.get('$addrValue/api2/json/cluster/resources'); final res = resp.data['data'] as List; final result = await Computer.shared.start(PveRes.parse, ( res, state.data, )); + if (!ref.mounted) return; state = state.copyWith(data: result, error: null); } catch (e) { + if (!ref.mounted) return; Loggers.app.warning('PVE list failed', e); state = state.copyWith(error: PveErr(type: PveErrType.unknown, message: e.toString())); } finally { - state = state.copyWith(isBusy: false); + if (ref.mounted) { + state = state.copyWith(isBusy: false); + } } } Future reboot(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/reboot', + '$addrValue/api2/json/nodes/$node/$id/status/reboot', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -200,7 +237,7 @@ class PveNotifier extends _$PveNotifier { Future start(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/start', + '$addrValue/api2/json/nodes/$node/$id/status/start', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -210,7 +247,7 @@ class PveNotifier extends _$PveNotifier { Future stop(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/stop', + '$addrValue/api2/json/nodes/$node/$id/status/stop', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -220,7 +257,7 @@ class PveNotifier extends _$PveNotifier { Future shutdown(String node, String id) async { if (!state.isConnected) return false; final resp = await session.post( - '$addr/api2/json/nodes/$node/$id/status/shutdown', + '$addrValue/api2/json/nodes/$node/$id/status/shutdown', ); final success = _isCtrlSuc(resp); if (success) await list(); // Refresh data @@ -233,7 +270,7 @@ class PveNotifier extends _$PveNotifier { Future dispose() async { try { - await _serverSocket.close(); + await _serverSocket?.close(); } catch (e, s) { Loggers.app.warning('Failed to close server socket', e, s); } diff --git a/lib/data/provider/pve.freezed.dart b/lib/data/provider/pve.freezed.dart index dc07269a..884962b0 100644 --- a/lib/data/provider/pve.freezed.dart +++ b/lib/data/provider/pve.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$PveState { - PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected; + PveErr? get error; PveRes? get data; String? get release; bool get isBusy; bool get isConnected; PveLoadingStep get loadingStep; /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $PveStateCopyWith get copyWith => _$PveStateCopyWithImpl(thi @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.loadingStep, loadingStep) || other.loadingStep == loadingStep)); } @override -int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected); +int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected,loadingStep); @override String toString() { - return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)'; + return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected, loadingStep: $loadingStep)'; } @@ -45,7 +45,7 @@ abstract mixin class $PveStateCopyWith<$Res> { factory $PveStateCopyWith(PveState value, $Res Function(PveState) _then) = _$PveStateCopyWithImpl; @useResult $Res call({ - PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected + PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep }); @@ -62,14 +62,15 @@ class _$PveStateCopyWithImpl<$Res> /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,Object? loadingStep = null,}) { return _then(_self.copyWith( error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable -as bool, +as bool,loadingStep: null == loadingStep ? _self.loadingStep : loadingStep // ignore: cast_nullable_to_non_nullable +as PveLoadingStep, )); } @@ -154,10 +155,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _PveState() when $default != null: -return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected,_that.loadingStep);case _: return orElse(); } @@ -175,10 +176,10 @@ return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnec /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep) $default,) {final _that = this; switch (_that) { case _PveState(): -return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected,_that.loadingStep);case _: throw StateError('Unexpected subclass'); } @@ -195,10 +196,10 @@ return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnec /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep)? $default,) {final _that = this; switch (_that) { case _PveState() when $default != null: -return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected);case _: +return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnected,_that.loadingStep);case _: return null; } @@ -210,7 +211,7 @@ return $default(_that.error,_that.data,_that.release,_that.isBusy,_that.isConnec class _PveState implements PveState { - const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false}); + const _PveState({this.error = null, this.data = null, this.release = null, this.isBusy = false, this.isConnected = false, this.loadingStep = PveLoadingStep.none}); @override@JsonKey() final PveErr? error; @@ -218,6 +219,7 @@ class _PveState implements PveState { @override@JsonKey() final String? release; @override@JsonKey() final bool isBusy; @override@JsonKey() final bool isConnected; +@override@JsonKey() final PveLoadingStep loadingStep; /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. @@ -229,16 +231,16 @@ _$PveStateCopyWith<_PveState> get copyWith => __$PveStateCopyWithImpl<_PveState> @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PveState&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)&&(identical(other.release, release) || other.release == release)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)&&(identical(other.isConnected, isConnected) || other.isConnected == isConnected)&&(identical(other.loadingStep, loadingStep) || other.loadingStep == loadingStep)); } @override -int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected); +int get hashCode => Object.hash(runtimeType,error,data,release,isBusy,isConnected,loadingStep); @override String toString() { - return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected)'; + return 'PveState(error: $error, data: $data, release: $release, isBusy: $isBusy, isConnected: $isConnected, loadingStep: $loadingStep)'; } @@ -249,7 +251,7 @@ abstract mixin class _$PveStateCopyWith<$Res> implements $PveStateCopyWith<$Res> factory _$PveStateCopyWith(_PveState value, $Res Function(_PveState) _then) = __$PveStateCopyWithImpl; @override @useResult $Res call({ - PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected + PveErr? error, PveRes? data, String? release, bool isBusy, bool isConnected, PveLoadingStep loadingStep }); @@ -266,14 +268,15 @@ class __$PveStateCopyWithImpl<$Res> /// Create a copy of PveState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? error = freezed,Object? data = freezed,Object? release = freezed,Object? isBusy = null,Object? isConnected = null,Object? loadingStep = null,}) { return _then(_PveState( error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as PveErr?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as PveRes?,release: freezed == release ? _self.release : release // ignore: cast_nullable_to_non_nullable as String?,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool,isConnected: null == isConnected ? _self.isConnected : isConnected // ignore: cast_nullable_to_non_nullable -as bool, +as bool,loadingStep: null == loadingStep ? _self.loadingStep : loadingStep // ignore: cast_nullable_to_non_nullable +as PveLoadingStep, )); } diff --git a/lib/data/provider/pve.g.dart b/lib/data/provider/pve.g.dart index cad05f89..476c92d1 100644 --- a/lib/data/provider/pve.g.dart +++ b/lib/data/provider/pve.g.dart @@ -58,7 +58,7 @@ final class PveNotifierProvider } } -String _$pveNotifierHash() => r'1e71faadee074b9c07bee731ef4ae6505e791967'; +String _$pveNotifierHash() => r'a66699f64eae680064a1904f475d0a241d6cb3f8'; final class PveNotifierFamily extends $Family with $ClassFamilyOverride { diff --git a/lib/data/provider/server/all.g.dart b/lib/data/provider/server/all.g.dart index 2fefa053..bf1fc413 100644 --- a/lib/data/provider/server/all.g.dart +++ b/lib/data/provider/server/all.g.dart @@ -41,7 +41,7 @@ final class ServersNotifierProvider } } -String _$serversNotifierHash() => r'dc5da44f9bd8d8dcfba3e6e932cca3e2f379e582'; +String _$serversNotifierHash() => r'277d1b219235f14bcc1b82a1e16260c2f28decdb'; abstract class _$ServersNotifier extends $Notifier { ServersState build(); diff --git a/lib/data/provider/server/single.g.dart b/lib/data/provider/server/single.g.dart index bfc9f2bb..3b0d4dc1 100644 --- a/lib/data/provider/server/single.g.dart +++ b/lib/data/provider/server/single.g.dart @@ -58,7 +58,7 @@ final class ServerNotifierProvider } } -String _$serverNotifierHash() => r'04b1beef4d96242fd10d5b523c6f5f17eb774bae'; +String _$serverNotifierHash() => r'1bda6d0a9688ab843cf30803dafe3400379dc5c3'; final class ServerNotifierFamily extends $Family with diff --git a/lib/data/provider/systemd.g.dart b/lib/data/provider/systemd.g.dart index 8ef2a6e0..1401c48f 100644 --- a/lib/data/provider/systemd.g.dart +++ b/lib/data/provider/systemd.g.dart @@ -58,7 +58,7 @@ final class SystemdNotifierProvider } } -String _$systemdNotifierHash() => r'030d556efc3d897419cd3462d37cb705813e24c7'; +String _$systemdNotifierHash() => r'd8b36c60dff036e98196ad4d084e4b2ae3a65e32'; final class SystemdNotifierFamily extends $Family with diff --git a/lib/data/res/github_id.dart b/lib/data/res/github_id.dart index 72269dea..5ab5ee4e 100644 --- a/lib/data/res/github_id.dart +++ b/lib/data/res/github_id.dart @@ -151,6 +151,7 @@ abstract final class GithubIds { 'Yinhono', 'kuvaldini', 'aliferne', + 'canronglan', }; } diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 6143f63c..136d462f 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1050,12 +1050,54 @@ abstract class AppLocalizations { /// **'Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.'** String get pveLoginFailed; + /// No description provided for @pveOtpRequired. + /// + /// In en, this message translates to: + /// **'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'** + String get pveOtpRequired; + /// No description provided for @pveVersionLow. /// /// In en, this message translates to: /// **'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.'** String get pveVersionLow; + /// No description provided for @pveLoadingForwarding. + /// + /// In en, this message translates to: + /// **'Establishing SSH tunnel...'** + String get pveLoadingForwarding; + + /// No description provided for @pveLoadingLogin. + /// + /// In en, this message translates to: + /// **'Authenticating with PVE...'** + String get pveLoadingLogin; + + /// No description provided for @pveLoadingData. + /// + /// In en, this message translates to: + /// **'Fetching cluster data...'** + String get pveLoadingData; + + /// No description provided for @pveLoadingConnect. + /// + /// In en, this message translates to: + /// **'Connecting...'** + String get pveLoadingConnect; + + /// No description provided for @pvePassword. + /// + /// In en, this message translates to: + /// **'PVE Password'** + String get pvePassword; + + /// No description provided for @pvePasswordHint. + /// + /// In en, this message translates to: + /// **'Required when using key-based SSH authentication'** + String get pvePasswordHint; + /// No description provided for @read. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index 0add8880..42785d35 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -538,10 +538,33 @@ class AppLocalizationsDe extends AppLocalizations { String get pveLoginFailed => 'Anmeldung fehlgeschlagen. Kann nicht mit Benutzername/Passwort aus der Serverkonfiguration angemeldet werden, um sich über Linux PAM anzumelden.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Diese Funktion befindet sich derzeit in der Testphase und wurde nur auf PVE 8+ getestet. Bitte verwenden Sie sie mit Vorsicht.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Lesen'; diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 43ee9a28..a2979309 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -535,10 +535,33 @@ class AppLocalizationsEn extends AppLocalizations { String get pveLoginFailed => 'Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Read'; diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 308c6812..2bfd80a3 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -540,10 +540,33 @@ class AppLocalizationsEs extends AppLocalizations { String get pveLoginFailed => 'Fallo al iniciar sesión. No se puede autenticar con el nombre de usuario/contraseña de la configuración del servidor para el inicio de sesión de Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Esta función está actualmente en fase de prueba y solo se ha probado en PVE 8+. Úsela con precaución.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Leer'; diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index fbabaf75..2f39be8c 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -542,10 +542,33 @@ class AppLocalizationsFr extends AppLocalizations { String get pveLoginFailed => 'Échec de la connexion. Impossible d\'authentifier avec le nom d\'utilisateur / mot de passe de la configuration du serveur pour la connexion Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Cette fonctionnalité est actuellement en phase de test et n\'a été testée que sur PVE 8+. Veuillez l\'utiliser avec prudence.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Lire'; diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 9718077f..22bc3978 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -535,10 +535,33 @@ class AppLocalizationsId extends AppLocalizations { String get pveLoginFailed => 'Login gagal. Tidak dapat mengautentikasi dengan nama pengguna/kata sandi dari konfigurasi server untuk login Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Fitur ini saat ini sedang dalam tahap pengujian dan hanya diuji pada PVE 8+. Gunakan dengan hati-hati.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Baca'; diff --git a/lib/generated/l10n/l10n_it.dart b/lib/generated/l10n/l10n_it.dart index 4e8bf6b4..e9c10586 100644 --- a/lib/generated/l10n/l10n_it.dart +++ b/lib/generated/l10n/l10n_it.dart @@ -536,10 +536,33 @@ class AppLocalizationsIt extends AppLocalizations { String get pveLoginFailed => 'Accesso fallito. Impossibile autenticarsi con nome utente/password dalla configurazione del server per l\'accesso Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Questa funzionalità è attualmente nella fase di test ed è stata testata solo su PVE 8+. Usala con cautela.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Leggi'; diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index ef2eb38e..47a7697d 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -517,9 +517,32 @@ class AppLocalizationsJa extends AppLocalizations { String get pveLoginFailed => 'ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'この機能は現在テスト段階にあり、PVE 8+でのみテストされています。ご利用の際は慎重に。'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => '読み取り'; diff --git a/lib/generated/l10n/l10n_ko.dart b/lib/generated/l10n/l10n_ko.dart index b01e364a..25c5376c 100644 --- a/lib/generated/l10n/l10n_ko.dart +++ b/lib/generated/l10n/l10n_ko.dart @@ -514,10 +514,33 @@ class AppLocalizationsKo extends AppLocalizations { String get pveLoginFailed => '로그인에 실패했습니다. 서버 설정의 사용자 이름/비밀번호로 Linux PAM 인증을 할 수 없습니다.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => '이 기능은 현재 테스트 단계이며 PVE 8+에서만 테스트되었습니다. 주의하여 사용해 주세요.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => '읽기'; diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 2ffbbc2a..86396c90 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -537,10 +537,33 @@ class AppLocalizationsNl extends AppLocalizations { String get pveLoginFailed => 'Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Deze functie bevindt zich momenteel in de testfase en is alleen getest op PVE 8+. Gebruik het met voorzichtigheid.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Lezen'; diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index b5fefa23..d2cbc7cc 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -535,10 +535,33 @@ class AppLocalizationsPt extends AppLocalizations { String get pveLoginFailed => 'Falha no login. Não é possível autenticar com o nome de usuário/senha da configuração do servidor para login no Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Esta funcionalidade está atualmente em fase de teste e foi testada apenas no PVE 8+. Por favor, use com cautela.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Leitura'; diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index cd3c553f..cda82788 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -538,10 +538,33 @@ class AppLocalizationsRu extends AppLocalizations { String get pveLoginFailed => 'Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Эта функция в настоящее время находится на стадии тестирования и была протестирована только на PVE 8+. Используйте ее с осторожностью.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Чтение'; diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index 145acfd2..c8f5f918 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -535,10 +535,33 @@ class AppLocalizationsTr extends AppLocalizations { String get pveLoginFailed => 'Giriş başarısız. Linux PAM girişi için sunucu yapılandırmasındaki kullanıcı adı/şifre ile kimlik doğrulama yapılamadı.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Bu özellik şu anda test aşamasında ve yalnızca PVE 8+ üzerinde test edildi. Lütfen dikkatli kullanın.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Oku'; diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 3dabdb67..b53c172e 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -539,10 +539,33 @@ class AppLocalizationsUk extends AppLocalizations { String get pveLoginFailed => 'Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.'; + @override + String get pveOtpRequired => + 'Two-factor authentication is enabled on this PVE server. Please enter the OTP code.'; + @override String get pveVersionLow => 'Ця функція наразі перебуває на стадії тестування та випробувалася лише на PVE 8+. Будь ласка, використовуйте її з обережністю.'; + @override + String get pveLoadingForwarding => 'Establishing SSH tunnel...'; + + @override + String get pveLoadingLogin => 'Authenticating with PVE...'; + + @override + String get pveLoadingData => 'Fetching cluster data...'; + + @override + String get pveLoadingConnect => 'Connecting...'; + + @override + String get pvePassword => 'PVE Password'; + + @override + String get pvePasswordHint => + 'Required when using key-based SSH authentication'; + @override String get read => 'Читати'; diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 12e40a36..e2758aa4 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -506,9 +506,30 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pveLoginFailed => '登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。'; + @override + String get pveOtpRequired => '此 PVE 服务器已启用双因素认证,请输入 OTP 验证码。'; + @override String get pveVersionLow => '当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用'; + @override + String get pveLoadingForwarding => '正在建立 SSH 隧道...'; + + @override + String get pveLoadingLogin => '正在认证 PVE...'; + + @override + String get pveLoadingData => '正在获取集群数据...'; + + @override + String get pveLoadingConnect => '正在连接...'; + + @override + String get pvePassword => 'PVE 密码'; + + @override + String get pvePasswordHint => '使用密钥认证时需要填写'; + @override String get read => '读'; @@ -1315,9 +1336,30 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get pveLoginFailed => '登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。'; + @override + String get pveOtpRequired => '此 PVE 伺服器已啟用雙因素認證,請輸入 OTP 驗證碼。'; + @override String get pveVersionLow => '此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。'; + @override + String get pveLoadingForwarding => '正在建立 SSH 隧道...'; + + @override + String get pveLoadingLogin => '正在認證 PVE...'; + + @override + String get pveLoadingData => '正在獲取集群數據...'; + + @override + String get pveLoadingConnect => '正在連接...'; + + @override + String get pvePassword => 'PVE 密碼'; + + @override + String get pvePasswordHint => '使用密鑰認證時需要填寫'; + @override String get read => '讀取'; diff --git a/lib/hive/hive_adapters.g.dart b/lib/hive/hive_adapters.g.dart index c456396b..630a06f0 100644 --- a/lib/hive/hive_adapters.g.dart +++ b/lib/hive/hive_adapters.g.dart @@ -485,6 +485,7 @@ class ServerCustomAdapter extends TypeAdapter { return ServerCustom( pveAddr: fields[1] as String?, pveIgnoreCert: fields[2] == null ? false : fields[2] as bool, + pvePwd: fields[8] as String?, cmds: (fields[3] as Map?)?.cast(), preferTempDev: fields[4] as String?, logoUrl: fields[5] as String?, @@ -496,7 +497,7 @@ class ServerCustomAdapter extends TypeAdapter { @override void write(BinaryWriter writer, ServerCustom obj) { writer - ..writeByte(7) + ..writeByte(8) ..writeByte(1) ..write(obj.pveAddr) ..writeByte(2) @@ -510,7 +511,9 @@ class ServerCustomAdapter extends TypeAdapter { ..writeByte(6) ..write(obj.netDev) ..writeByte(7) - ..write(obj.scriptDir); + ..write(obj.scriptDir) + ..writeByte(8) + ..write(obj.pvePwd); } @override diff --git a/lib/hive/hive_adapters.g.yaml b/lib/hive/hive_adapters.g.yaml index 94d426fe..f5439221 100644 --- a/lib/hive/hive_adapters.g.yaml +++ b/lib/hive/hive_adapters.g.yaml @@ -185,7 +185,7 @@ types: index: 8 ServerCustom: typeId: 7 - nextIndex: 8 + nextIndex: 9 fields: pveAddr: index: 1 @@ -201,6 +201,8 @@ types: index: 6 scriptDir: index: 7 + pvePwd: + index: 8 WakeOnLanCfg: typeId: 8 nextIndex: 3 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 99a71f51..e58c8ba0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -154,7 +154,14 @@ "pushToken": "Push token", "pveIgnoreCertTip": "Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.", "pveLoginFailed": "Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.", + "pveOtpRequired": "Two-factor authentication is enabled on this PVE server. Please enter the OTP code.", "pveVersionLow": "This feature is currently in the testing phase and has only been tested on PVE 8+. Please use it with caution.", + "pveLoadingForwarding": "Establishing SSH tunnel...", + "pveLoadingLogin": "Authenticating with PVE...", + "pveLoadingData": "Fetching cluster data...", + "pveLoadingConnect": "Connecting...", + "pvePassword": "PVE Password", + "pvePasswordHint": "Required when using key-based SSH authentication", "read": "Read", "recentConnections": "Recent Connections", "rememberPwdInMem": "Remember password in memory", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1b96d475..608d9465 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -154,7 +154,14 @@ "pushToken": "消息推送 Token", "pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项", "pveLoginFailed": "登录失败。无法使用服务器配置中的用户名或密码通过 Linux PAM 方式认证。", + "pveOtpRequired": "此 PVE 服务器已启用双因素认证,请输入 OTP 验证码。", "pveVersionLow": "当前该功能处于测试阶段,仅在 PVE 8+ 上测试过,请谨慎使用", + "pveLoadingForwarding": "正在建立 SSH 隧道...", + "pveLoadingLogin": "正在认证 PVE...", + "pveLoadingData": "正在获取集群数据...", + "pveLoadingConnect": "正在连接...", + "pvePassword": "PVE 密码", + "pvePasswordHint": "使用密钥认证时需要填写", "read": "读", "recentConnections": "最近连接记录", "rememberPwdInMem": "在内存中记住密码", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 475f996d..aa9b3f52 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -154,7 +154,14 @@ "pushToken": "消息推送 Token", "pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的預設憑證,則需要啟用此選項。", "pveLoginFailed": "登入失敗。無法使用伺服器設定中的使用者名稱或密碼透過 Linux PAM 方式認證。", + "pveOtpRequired": "此 PVE 伺服器已啟用雙因素認證,請輸入 OTP 驗證碼。", "pveVersionLow": "此功能目前處於測試階段,僅在 PVE 8+ 上進行過測試。請謹慎使用。", + "pveLoadingForwarding": "正在建立 SSH 隧道...", + "pveLoadingLogin": "正在認證 PVE...", + "pveLoadingData": "正在獲取集群數據...", + "pveLoadingConnect": "正在連接...", + "pvePassword": "PVE 密碼", + "pvePasswordHint": "使用密鑰認證時需要填寫", "read": "讀取", "recentConnections": "最近連線記錄", "rememberPwdInMem": "在記憶體中記住密碼", diff --git a/lib/view/page/pve.dart b/lib/view/page/pve.dart index 1ecbe24a..6b655526 100644 --- a/lib/view/page/pve.dart +++ b/lib/view/page/pve.dart @@ -4,6 +4,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:server_box/core/extension/context/locale.dart'; +import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/server/pve.dart'; import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/provider/pve.dart'; @@ -73,24 +74,28 @@ final class _PvePageState extends ConsumerState { : Btn.icon( icon: const Icon(Icons.refresh), onTap: () { - _notifier.list(); + _notifier.reconnect(); _initRefreshTimer(); }, ), ], ), body: pveState.error != null - ? Padding( - padding: const EdgeInsets.all(13), - child: Center(child: Text(pveState.error.toString())), - ) - : _buildBody(pveState.data), + ? _buildError(pveState.error!) + : _buildBody(pveState.data, pveState.loadingStep), ); } - Widget _buildBody(PveRes? data) { + Widget _buildError(PveErr error) { + return Padding( + padding: const EdgeInsets.all(13), + child: Center(child: Text(error.toString())), + ); + } + + Widget _buildBody(PveRes? data, PveLoadingStep loadingStep) { if (data == null) { - return UIs.centerLoading; + return _buildLoading(loadingStep); } PveResType? lastType; @@ -134,6 +139,25 @@ final class _PvePageState extends ConsumerState { ); } + Widget _buildLoading(PveLoadingStep step) { + final String message = switch (step) { + PveLoadingStep.forwarding => l10n.pveLoadingForwarding, + PveLoadingStep.loggingIn => l10n.pveLoadingLogin, + PveLoadingStep.fetchingData => l10n.pveLoadingData, + _ => l10n.pveLoadingConnect, + }; + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 17), + Text(message, style: UIs.text13Grey), + ], + ), + ); + } + Widget _buildNode(PveNode item) { final valueAnim = AlwaysStoppedAnimation(UIs.primaryColor); return Padding( diff --git a/lib/view/page/server/edit/actions.dart b/lib/view/page/server/edit/actions.dart index 7aabd7d5..2a838c87 100644 --- a/lib/view/page/server/edit/actions.dart +++ b/lib/view/page/server/edit/actions.dart @@ -71,6 +71,7 @@ extension _Actions on _ServerEditPageState { final custom = ServerCustom( pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull, pveIgnoreCert: _pveIgnoreCert.value, + pvePwd: _pvePwdCtrl.text.selfNotEmptyOrNull, cmds: customCmds.isEmpty ? null : customCmds, preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull, logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull, @@ -266,6 +267,7 @@ extension _Utils on _ServerEditPageState { if (custom != null) { _pveAddrCtrl.text = custom.pveAddr ?? ''; _pveIgnoreCert.value = custom.pveIgnoreCert; + _pvePwdCtrl.text = custom.pvePwd ?? ''; _customCmds.value = custom.cmds ?? {}; _preferTempDevCtrl.text = custom.preferTempDev ?? ''; _logoUrlCtrl.text = custom.logoUrl ?? ''; diff --git a/lib/view/page/server/edit/edit.dart b/lib/view/page/server/edit/edit.dart index 22ab507e..aab97500 100644 --- a/lib/view/page/server/edit/edit.dart +++ b/lib/view/page/server/edit/edit.dart @@ -48,6 +48,7 @@ class _ServerEditPageState extends ConsumerState final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _pveAddrCtrl = TextEditingController(); + final _pvePwdCtrl = TextEditingController(); final _preferTempDevCtrl = TextEditingController(); final _logoUrlCtrl = TextEditingController(); final _wolMacCtrl = TextEditingController(); @@ -98,6 +99,7 @@ class _ServerEditPageState extends ConsumerState _portFocus.dispose(); _usernameFocus.dispose(); _pveAddrCtrl.dispose(); + _pvePwdCtrl.dispose(); _keyIdx.dispose(); _autoConnect.dispose(); diff --git a/lib/view/page/server/edit/widget.dart b/lib/view/page/server/edit/widget.dart index 571ab67d..58769ff5 100644 --- a/lib/view/page/server/edit/widget.dart +++ b/lib/view/page/server/edit/widget.dart @@ -230,32 +230,45 @@ extension _Widgets on _ServerEditPageState { Widget _buildPVEs() { const addr = 'https://127.0.0.1:8006'; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CenterGreyTitle('PVE'), - Input( - controller: _pveAddrCtrl, - type: TextInputType.url, - icon: MingCute.web_line, - label: 'URL', - hint: addr, - suggestion: false, - ), - ListTile( - leading: const Icon(MingCute.certificate_line), - title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip), - trailing: _pveIgnoreCert.listenVal( - (v) => Switch( - value: v, - onChanged: (val) { - _pveIgnoreCert.value = val; - }, - ), + return _keyIdx.listenVal((v) { + final useKeyAuth = v != null && v >= 0; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CenterGreyTitle('PVE'), + Input( + controller: _pveAddrCtrl, + type: TextInputType.url, + icon: MingCute.web_line, + label: 'URL', + hint: addr, + suggestion: false, ), - ).cardx, - ], - ); + if (useKeyAuth) + Input( + controller: _pvePwdCtrl, + type: TextInputType.visiblePassword, + icon: MingCute.lock_line, + label: l10n.pvePassword, + hint: l10n.pvePasswordHint, + obscureText: true, + suggestion: false, + ), + ListTile( + leading: const Icon(MingCute.certificate_line), + title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip), + trailing: _pveIgnoreCert.listenVal( + (v) => Switch( + value: v, + onChanged: (val) { + _pveIgnoreCert.value = val; + }, + ), + ), + ).cardx, + ], + ); + }); } Widget _buildCustomCmds() { diff --git a/pubspec.lock b/pubspec.lock index f02dbcbd..94547b65 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: build - sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" + sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.0.5" build_config: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "3552e5c2874ed47cf9ed9d6813ac71b2276ee07622f48530468b8013f1767e3f" + sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" built_collection: dependency: transitive description: @@ -189,10 +189,10 @@ packages: dependency: transitive description: name: camera_avfoundation - sha256: "4e47c2796dab3f21fdfe1d15151bf628519093b171307cb64a71ba8e451697b5" + sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b" url: "https://pub.dev" source: hosted - version: "0.9.23" + version: "0.9.23+2" camera_platform_interface: dependency: transitive description: @@ -460,10 +460,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: d974b6ba2606371ac71dd94254beefb6fa81185bde0b59bdc1df09885da85fde + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" url: "https://pub.dev" source: hosted - version: "10.3.8" + version: "10.3.10" fixnum: dependency: transitive description: @@ -927,10 +927,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: dc9663a7bc8ac33d7d988e63901974f63d527ebef260eabd19c479447cc9c911 + sha256: b41970749c2d43791790724b76917eeee1e90de76e6b0eec3edca03a329bf44c url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" local_auth_darwin: dependency: transitive description: @@ -975,10 +975,10 @@ packages: dependency: transitive description: name: markdown - sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.1" matcher: dependency: transitive description: @@ -1491,10 +1491,10 @@ packages: dependency: transitive description: name: source_gen - sha256: adc962c96fffb2de1728ef396a995aaedcafbe635abdca13d2a987ce17e57751 + sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" source_helper: dependency: transitive description: