From 9f28df0cd058c9a3dadd50ad57d3466c09d78826 Mon Sep 17 00:00:00 2001 From: GT610 Date: Sun, 11 Jan 2026 22:03:02 +0800 Subject: [PATCH 01/11] fix(ssh): Modify the return type of execWithPwd to include the output content Adjust the return type of the `execWithPwd` method to `(int?, String)` so that it can simultaneously return the exit code and output content Fix the issue in ContainerNotifier where the return result of execWithPwd is not handled correctly Ensure that server operations (shutdown/restart/suspend) are correctly pending until the command execution is completed --- lib/core/extension/ssh_client.dart | 6 +++--- lib/data/provider/container.dart | 17 ++++++++--------- lib/view/page/server/tab/utils.dart | 26 +++++++++++++++----------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 7b7210206..43e16ef89 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -112,7 +112,7 @@ extension SSHClientX on SSHClient { return (session, result.takeBytes().string); } - Future execWithPwd( + Future<(int?, String)> execWithPwd( String script, { String? entry, BuildContext? context, @@ -121,7 +121,7 @@ extension SSHClientX on SSHClient { required String id, }) async { var isRequestingPwd = false; - final (session, _) = await exec( + final (session, output) = await exec( (sess) { sess.stdin.add('$script\n'.uint8List); sess.stdin.close(); @@ -147,7 +147,7 @@ extension SSHClientX on SSHClient { onStdout: onStdout, entry: entry, ); - return session.exitCode; + return (session.exitCode, output); } Future execForOutput( diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 69a7bc675..99473b5ab 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -84,15 +84,14 @@ class ContainerNotifier extends _$ContainerNotifier { } final includeStats = Stores.setting.containerParseStat.fetch(); - var raw = ''; final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats)); - final code = await client?.execWithPwd( - cmd, - context: context, - onStdout: (data, _) => raw = '$raw$data', - id: hostId, - ); + int? code; + String raw = ''; + if (client != null) { + (code, raw) = await client!.execWithPwd(cmd, context: context, id: hostId); + } + if (!ref.mounted) return; state = state.copyWith(isBusy: false); if (!context.mounted) return; @@ -234,7 +233,7 @@ class ContainerNotifier extends _$ContainerNotifier { state = state.copyWith(runLog: ''); final errs = []; - final code = await client?.execWithPwd( + final (code, _) = await client?.execWithPwd( _wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd), context: context, onStdout: (data, _) { @@ -242,7 +241,7 @@ class ContainerNotifier extends _$ContainerNotifier { }, onStderr: (data, _) => errs.add(data), id: hostId, - ); + ) ?? (null, null); state = state.copyWith(runLog: null); if (code != 0) { diff --git a/lib/view/page/server/tab/utils.dart b/lib/view/page/server/tab/utils.dart index cd4d94eeb..5f2a6a79d 100644 --- a/lib/view/page/server/tab/utils.dart +++ b/lib/view/page/server/tab/utils.dart @@ -49,7 +49,7 @@ extension _Operation on _ServerPageState { await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip)); Stores.setting.showSuspendTip.put(false); } - srv.client?.execWithPwd( + await srv.client?.execWithPwd( ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system, customDir: null), context: context, id: srv.id, @@ -62,11 +62,13 @@ extension _Operation on _ServerPageState { void _onTapShutdown(ServerState srv) { _askFor( - func: () => srv.client?.execWithPwd( - ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null), - context: context, - id: srv.id, - ), + func: () async { + await srv.client?.execWithPwd( + ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null), + context: context, + id: srv.id, + ); + }, typ: l10n.shutdown, name: srv.spi.name, ); @@ -74,11 +76,13 @@ extension _Operation on _ServerPageState { void _onTapReboot(ServerState srv) { _askFor( - func: () => srv.client?.execWithPwd( - ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null), - context: context, - id: srv.id, - ), + func: () async { + await srv.client?.execWithPwd( + ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null), + context: context, + id: srv.id, + ); + }, typ: l10n.reboot, name: srv.spi.name, ); From a52baac387e34049459861373932bf5cdeab7185 Mon Sep 17 00:00:00 2001 From: GT610 Date: Sun, 11 Jan 2026 22:35:47 +0800 Subject: [PATCH 02/11] refactor(container): Change single error handling to multiple error lists Support the simultaneous display of multiple container operation errors, enhancing error handling capabilities --- lib/data/provider/container.dart | 26 ++++++------ lib/data/provider/container.freezed.dart | 52 +++++++++++++----------- lib/view/page/container/container.dart | 10 ++--- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 99473b5ab..a69fa5d80 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -25,7 +25,7 @@ abstract class ContainerState with _$ContainerState { @Default(null) List? items, @Default(null) List? images, @Default(null) String? version, - @Default(null) ContainerErr? error, + @Default([]) List errors, @Default(null) String? runLog, @Default(ContainerType.docker) ContainerType type, @Default(false) bool isBusy, @@ -48,7 +48,7 @@ class ContainerNotifier extends _$ContainerNotifier { } Future setType(ContainerType type) async { - state = state.copyWith(type: type, error: null, runLog: null, items: null, images: null, version: null); + state = state.copyWith(type: type, errors: [], runLog: null, items: null, images: null, version: null); Stores.container.setType(type, hostId); sudoCompleter = Completer(); await refresh(); @@ -98,7 +98,7 @@ class ContainerNotifier extends _$ContainerNotifier { /// Code 127 means command not found if (code == 127 || raw.contains(_dockerNotFound)) { - state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled)); + state = state.copyWith(errors: [ContainerErr(type: ContainerErrType.notInstalled)]); return; } @@ -106,10 +106,12 @@ class ContainerNotifier extends _$ContainerNotifier { final segments = raw.split(ScriptConstants.separator); if (segments.length != ContainerCmdType.values.length) { state = state.copyWith( - error: ContainerErr( - type: ContainerErrType.segmentsNotMatch, - message: 'Container segments: ${segments.length}', - ), + errors: [ + ContainerErr( + type: ContainerErrType.segmentsNotMatch, + message: 'Container segments: ${segments.length}', + ), + ], ); Loggers.app.warning('Container segments: ${segments.length}\n$raw'); return; @@ -119,10 +121,10 @@ class ContainerNotifier extends _$ContainerNotifier { final verRaw = ContainerCmdType.version.find(segments); try { final version = json.decode(verRaw)['Client']['Version']; - state = state.copyWith(version: version, error: null); + state = state.copyWith(version: version); } catch (e, trace) { state = state.copyWith( - error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'), + errors: [...state.errors, ContainerErr(type: ContainerErrType.invalidVersion, message: '$e')], ); Loggers.app.warning('Container version failed', e, trace); } @@ -140,7 +142,7 @@ class ContainerNotifier extends _$ContainerNotifier { state = state.copyWith(items: items); } catch (e, trace) { state = state.copyWith( - error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'), + errors: [...state.errors, ContainerErr(type: ContainerErrType.parsePs, message: '$e')], ); Loggers.app.warning('Container ps failed', e, trace); } @@ -162,7 +164,7 @@ class ContainerNotifier extends _$ContainerNotifier { state = state.copyWith(images: images); } catch (e, trace) { state = state.copyWith( - error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'), + errors: [...state.errors, ContainerErr(type: ContainerErrType.parseImages, message: '$e')], ); Loggers.app.warning('Container images failed', e, trace); } @@ -189,7 +191,7 @@ class ContainerNotifier extends _$ContainerNotifier { } } catch (e, trace) { state = state.copyWith( - error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'), + errors: [...state.errors, ContainerErr(type: ContainerErrType.parseStats, message: '$e')], ); Loggers.app.warning('Parse docker stats: $statsRaw', e, trace); } diff --git a/lib/data/provider/container.freezed.dart b/lib/data/provider/container.freezed.dart index 614e4d743..5e36e66f1 100644 --- a/lib/data/provider/container.freezed.dart +++ b/lib/data/provider/container.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$ContainerState { - List? get items; List? get images; String? get version; ContainerErr? get error; String? get runLog; ContainerType get type; bool get isBusy; + List? get items; List? get images; String? get version; List get errors; String? get runLog; ContainerType get type; bool get isBusy; /// Create a copy of ContainerState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $ContainerStateCopyWith get copyWith => _$ContainerStateCopyWith @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&const DeepCollectionEquality().equals(other.errors, errors)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,error,runLog,type,isBusy); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,const DeepCollectionEquality().hash(errors),runLog,type,isBusy); @override String toString() { - return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)'; + return 'ContainerState(items: $items, images: $images, version: $version, errors: $errors, runLog: $runLog, type: $type, isBusy: $isBusy)'; } @@ -45,7 +45,7 @@ abstract mixin class $ContainerStateCopyWith<$Res> { factory $ContainerStateCopyWith(ContainerState value, $Res Function(ContainerState) _then) = _$ContainerStateCopyWithImpl; @useResult $Res call({ - List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy + List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy }); @@ -62,13 +62,13 @@ class _$ContainerStateCopyWithImpl<$Res> /// Create a copy of ContainerState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? errors = null,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { return _then(_self.copyWith( items: freezed == items ? _self.items : items // ignore: cast_nullable_to_non_nullable as List?,images: freezed == images ? _self.images : images // ignore: cast_nullable_to_non_nullable as List?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable -as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable +as String?,errors: null == errors ? _self.errors : errors // ignore: cast_nullable_to_non_nullable +as List,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool, @@ -156,10 +156,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _ContainerState() when $default != null: -return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: +return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog,_that.type,_that.isBusy);case _: return orElse(); } @@ -177,10 +177,10 @@ return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog, /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy) $default,) {final _that = this; switch (_that) { case _ContainerState(): -return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: +return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog,_that.type,_that.isBusy);case _: throw StateError('Unexpected subclass'); } @@ -197,10 +197,10 @@ return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog, /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy)? $default,) {final _that = this; switch (_that) { case _ContainerState() when $default != null: -return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: +return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog,_that.type,_that.isBusy);case _: return null; } @@ -212,7 +212,7 @@ return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog, class _ContainerState implements ContainerState { - const _ContainerState({final List? items = null, final List? images = null, this.version = null, this.error = null, this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images; + const _ContainerState({final List? items = null, final List? images = null, this.version = null, final List errors = const [], this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images,_errors = errors; final List? _items; @@ -234,7 +234,13 @@ class _ContainerState implements ContainerState { } @override@JsonKey() final String? version; -@override@JsonKey() final ContainerErr? error; + final List _errors; +@override@JsonKey() List get errors { + if (_errors is EqualUnmodifiableListView) return _errors; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_errors); +} + @override@JsonKey() final String? runLog; @override@JsonKey() final ContainerType type; @override@JsonKey() final bool isBusy; @@ -249,16 +255,16 @@ _$ContainerStateCopyWith<_ContainerState> get copyWith => __$ContainerStateCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&const DeepCollectionEquality().equals(other._errors, _errors)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,error,runLog,type,isBusy); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,const DeepCollectionEquality().hash(_errors),runLog,type,isBusy); @override String toString() { - return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)'; + return 'ContainerState(items: $items, images: $images, version: $version, errors: $errors, runLog: $runLog, type: $type, isBusy: $isBusy)'; } @@ -269,7 +275,7 @@ abstract mixin class _$ContainerStateCopyWith<$Res> implements $ContainerStateCo factory _$ContainerStateCopyWith(_ContainerState value, $Res Function(_ContainerState) _then) = __$ContainerStateCopyWithImpl; @override @useResult $Res call({ - List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy + List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy }); @@ -286,13 +292,13 @@ class __$ContainerStateCopyWithImpl<$Res> /// Create a copy of ContainerState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? errors = null,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { return _then(_ContainerState( items: freezed == items ? _self._items : items // ignore: cast_nullable_to_non_nullable as List?,images: freezed == images ? _self._images : images // ignore: cast_nullable_to_non_nullable as List?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable -as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable +as String?,errors: null == errors ? _self._errors : errors // ignore: cast_nullable_to_non_nullable +as List,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool, diff --git a/lib/view/page/container/container.dart b/lib/view/page/container/container.dart index dab4b0a61..039a10264 100644 --- a/lib/view/page/container/container.dart +++ b/lib/view/page/container/container.dart @@ -55,12 +55,12 @@ class _ContainerPageState extends ConsumerState { @override Widget build(BuildContext context) { - final err = ref.watch(_provider.select((p) => p.error)); + final errors = ref.watch(_provider.select((p) => p.errors)); return Scaffold( appBar: _buildAppBar(), body: SafeArea(child: _buildMain()), - floatingActionButton: err == null ? _buildFAB() : null, + floatingActionButton: errors.isEmpty ? _buildFAB() : null, ); } @@ -84,7 +84,7 @@ class _ContainerPageState extends ConsumerState { Widget _buildMain() { final containerState = _containerState; - if (containerState.error != null && containerState.items == null) { + if (containerState.errors.isNotEmpty && containerState.items == null) { return SizedBox.expand( child: Column( children: [ @@ -93,7 +93,7 @@ class _ContainerPageState extends ConsumerState { UIs.height13, Padding( padding: const EdgeInsets.symmetric(horizontal: 23), - child: Text(containerState.error.toString()), + child: Text(containerState.errors.map((e) => e.toString()).join('\n')), ), const Spacer(), UIs.height13, @@ -334,7 +334,7 @@ class _ContainerPageState extends ConsumerState { return ExpandTile( leading: const Icon(Icons.settings), title: Text(libL10n.setting), - initiallyExpanded: containerState.error != null, + initiallyExpanded: containerState.errors.isNotEmpty, children: _SettingsMenuItems.values.map((item) => _buildSettingTile(item, containerState)).toList(), ).cardx; } From 89367b2ed447bff886c9c6efdd8ebbac9318f6c1 Mon Sep 17 00:00:00 2001 From: GT610 Date: Sun, 11 Jan 2026 22:45:12 +0800 Subject: [PATCH 03/11] fix(container): Adjust the layout width and optimize the handling of text overflow Adjust the width calculation for the container page layout, changing from subtracting a fixed value to subtracting a smaller value to improve the layout Add overflow ellipsis processing to the text to prevent anomalies when the text is too long --- lib/view/page/container/container.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/view/page/container/container.dart b/lib/view/page/container/container.dart index 039a10264..89be6007d 100644 --- a/lib/view/page/container/container.dart +++ b/lib/view/page/container/container.dart @@ -234,7 +234,7 @@ class _ContainerPageState extends ConsumerState { if (item.cpu == null || item.mem == null) return UIs.placeholder; return LayoutBuilder( builder: (_, cons) { - final width = cons.maxWidth / 2 - 41; + final width = cons.maxWidth / 2 - 6.5; return Column( children: [ UIs.height13, @@ -264,10 +264,17 @@ class _ContainerPageState extends ConsumerState { child: Column( children: [ Row( + mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 12, color: Colors.grey), UIs.width7, - Text(value ?? l10n.unknown, style: UIs.text11Grey), + Expanded( + child: Text( + value ?? l10n.unknown, + style: UIs.text11Grey, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ], From 3ec75ca891aee0c2c32d8aaf5b66db357cd9e9ea Mon Sep 17 00:00:00 2001 From: GT610 Date: Sun, 11 Jan 2026 22:52:00 +0800 Subject: [PATCH 04/11] Revert "refactor(container): Change single error handling to multiple error lists" This reverts commit 72aaa173f5ceabc952ab5c28e024451ac1309920. --- lib/data/provider/container.dart | 26 ++++++------ lib/data/provider/container.freezed.dart | 52 +++++++++++------------- lib/view/page/container/container.dart | 10 ++--- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index a69fa5d80..99473b5ab 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -25,7 +25,7 @@ abstract class ContainerState with _$ContainerState { @Default(null) List? items, @Default(null) List? images, @Default(null) String? version, - @Default([]) List errors, + @Default(null) ContainerErr? error, @Default(null) String? runLog, @Default(ContainerType.docker) ContainerType type, @Default(false) bool isBusy, @@ -48,7 +48,7 @@ class ContainerNotifier extends _$ContainerNotifier { } Future setType(ContainerType type) async { - state = state.copyWith(type: type, errors: [], runLog: null, items: null, images: null, version: null); + state = state.copyWith(type: type, error: null, runLog: null, items: null, images: null, version: null); Stores.container.setType(type, hostId); sudoCompleter = Completer(); await refresh(); @@ -98,7 +98,7 @@ class ContainerNotifier extends _$ContainerNotifier { /// Code 127 means command not found if (code == 127 || raw.contains(_dockerNotFound)) { - state = state.copyWith(errors: [ContainerErr(type: ContainerErrType.notInstalled)]); + state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled)); return; } @@ -106,12 +106,10 @@ class ContainerNotifier extends _$ContainerNotifier { final segments = raw.split(ScriptConstants.separator); if (segments.length != ContainerCmdType.values.length) { state = state.copyWith( - errors: [ - ContainerErr( - type: ContainerErrType.segmentsNotMatch, - message: 'Container segments: ${segments.length}', - ), - ], + error: ContainerErr( + type: ContainerErrType.segmentsNotMatch, + message: 'Container segments: ${segments.length}', + ), ); Loggers.app.warning('Container segments: ${segments.length}\n$raw'); return; @@ -121,10 +119,10 @@ class ContainerNotifier extends _$ContainerNotifier { final verRaw = ContainerCmdType.version.find(segments); try { final version = json.decode(verRaw)['Client']['Version']; - state = state.copyWith(version: version); + state = state.copyWith(version: version, error: null); } catch (e, trace) { state = state.copyWith( - errors: [...state.errors, ContainerErr(type: ContainerErrType.invalidVersion, message: '$e')], + error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'), ); Loggers.app.warning('Container version failed', e, trace); } @@ -142,7 +140,7 @@ class ContainerNotifier extends _$ContainerNotifier { state = state.copyWith(items: items); } catch (e, trace) { state = state.copyWith( - errors: [...state.errors, ContainerErr(type: ContainerErrType.parsePs, message: '$e')], + error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'), ); Loggers.app.warning('Container ps failed', e, trace); } @@ -164,7 +162,7 @@ class ContainerNotifier extends _$ContainerNotifier { state = state.copyWith(images: images); } catch (e, trace) { state = state.copyWith( - errors: [...state.errors, ContainerErr(type: ContainerErrType.parseImages, message: '$e')], + error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'), ); Loggers.app.warning('Container images failed', e, trace); } @@ -191,7 +189,7 @@ class ContainerNotifier extends _$ContainerNotifier { } } catch (e, trace) { state = state.copyWith( - errors: [...state.errors, ContainerErr(type: ContainerErrType.parseStats, message: '$e')], + error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'), ); Loggers.app.warning('Parse docker stats: $statsRaw', e, trace); } diff --git a/lib/data/provider/container.freezed.dart b/lib/data/provider/container.freezed.dart index 5e36e66f1..614e4d743 100644 --- a/lib/data/provider/container.freezed.dart +++ b/lib/data/provider/container.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$ContainerState { - List? get items; List? get images; String? get version; List get errors; String? get runLog; ContainerType get type; bool get isBusy; + List? get items; List? get images; String? get version; ContainerErr? get error; String? get runLog; ContainerType get type; bool get isBusy; /// Create a copy of ContainerState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $ContainerStateCopyWith get copyWith => _$ContainerStateCopyWith @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&const DeepCollectionEquality().equals(other.errors, errors)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ContainerState&&const DeepCollectionEquality().equals(other.items, items)&&const DeepCollectionEquality().equals(other.images, images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,const DeepCollectionEquality().hash(errors),runLog,type,isBusy); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(items),const DeepCollectionEquality().hash(images),version,error,runLog,type,isBusy); @override String toString() { - return 'ContainerState(items: $items, images: $images, version: $version, errors: $errors, runLog: $runLog, type: $type, isBusy: $isBusy)'; + return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)'; } @@ -45,7 +45,7 @@ abstract mixin class $ContainerStateCopyWith<$Res> { factory $ContainerStateCopyWith(ContainerState value, $Res Function(ContainerState) _then) = _$ContainerStateCopyWithImpl; @useResult $Res call({ - List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy + List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy }); @@ -62,13 +62,13 @@ class _$ContainerStateCopyWithImpl<$Res> /// Create a copy of ContainerState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? errors = null,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { return _then(_self.copyWith( items: freezed == items ? _self.items : items // ignore: cast_nullable_to_non_nullable as List?,images: freezed == images ? _self.images : images // ignore: cast_nullable_to_non_nullable as List?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable -as String?,errors: null == errors ? _self.errors : errors // ignore: cast_nullable_to_non_nullable -as List,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable +as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool, @@ -156,10 +156,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _ContainerState() when $default != null: -return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog,_that.type,_that.isBusy);case _: +return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: return orElse(); } @@ -177,10 +177,10 @@ return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy) $default,) {final _that = this; switch (_that) { case _ContainerState(): -return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog,_that.type,_that.isBusy);case _: +return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: throw StateError('Unexpected subclass'); } @@ -197,10 +197,10 @@ return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy)? $default,) {final _that = this; switch (_that) { case _ContainerState() when $default != null: -return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog,_that.type,_that.isBusy);case _: +return $default(_that.items,_that.images,_that.version,_that.error,_that.runLog,_that.type,_that.isBusy);case _: return null; } @@ -212,7 +212,7 @@ return $default(_that.items,_that.images,_that.version,_that.errors,_that.runLog class _ContainerState implements ContainerState { - const _ContainerState({final List? items = null, final List? images = null, this.version = null, final List errors = const [], this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images,_errors = errors; + const _ContainerState({final List? items = null, final List? images = null, this.version = null, this.error = null, this.runLog = null, this.type = ContainerType.docker, this.isBusy = false}): _items = items,_images = images; final List? _items; @@ -234,13 +234,7 @@ class _ContainerState implements ContainerState { } @override@JsonKey() final String? version; - final List _errors; -@override@JsonKey() List get errors { - if (_errors is EqualUnmodifiableListView) return _errors; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_errors); -} - +@override@JsonKey() final ContainerErr? error; @override@JsonKey() final String? runLog; @override@JsonKey() final ContainerType type; @override@JsonKey() final bool isBusy; @@ -255,16 +249,16 @@ _$ContainerStateCopyWith<_ContainerState> get copyWith => __$ContainerStateCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&const DeepCollectionEquality().equals(other._errors, _errors)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ContainerState&&const DeepCollectionEquality().equals(other._items, _items)&&const DeepCollectionEquality().equals(other._images, _images)&&(identical(other.version, version) || other.version == version)&&(identical(other.error, error) || other.error == error)&&(identical(other.runLog, runLog) || other.runLog == runLog)&&(identical(other.type, type) || other.type == type)&&(identical(other.isBusy, isBusy) || other.isBusy == isBusy)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,const DeepCollectionEquality().hash(_errors),runLog,type,isBusy); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_items),const DeepCollectionEquality().hash(_images),version,error,runLog,type,isBusy); @override String toString() { - return 'ContainerState(items: $items, images: $images, version: $version, errors: $errors, runLog: $runLog, type: $type, isBusy: $isBusy)'; + return 'ContainerState(items: $items, images: $images, version: $version, error: $error, runLog: $runLog, type: $type, isBusy: $isBusy)'; } @@ -275,7 +269,7 @@ abstract mixin class _$ContainerStateCopyWith<$Res> implements $ContainerStateCo factory _$ContainerStateCopyWith(_ContainerState value, $Res Function(_ContainerState) _then) = __$ContainerStateCopyWithImpl; @override @useResult $Res call({ - List? items, List? images, String? version, List errors, String? runLog, ContainerType type, bool isBusy + List? items, List? images, String? version, ContainerErr? error, String? runLog, ContainerType type, bool isBusy }); @@ -292,13 +286,13 @@ class __$ContainerStateCopyWithImpl<$Res> /// Create a copy of ContainerState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? errors = null,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? items = freezed,Object? images = freezed,Object? version = freezed,Object? error = freezed,Object? runLog = freezed,Object? type = null,Object? isBusy = null,}) { return _then(_ContainerState( items: freezed == items ? _self._items : items // ignore: cast_nullable_to_non_nullable as List?,images: freezed == images ? _self._images : images // ignore: cast_nullable_to_non_nullable as List?,version: freezed == version ? _self.version : version // ignore: cast_nullable_to_non_nullable -as String?,errors: null == errors ? _self._errors : errors // ignore: cast_nullable_to_non_nullable -as List,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable +as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable +as ContainerErr?,runLog: freezed == runLog ? _self.runLog : runLog // ignore: cast_nullable_to_non_nullable as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as ContainerType,isBusy: null == isBusy ? _self.isBusy : isBusy // ignore: cast_nullable_to_non_nullable as bool, diff --git a/lib/view/page/container/container.dart b/lib/view/page/container/container.dart index 89be6007d..f0006a3b5 100644 --- a/lib/view/page/container/container.dart +++ b/lib/view/page/container/container.dart @@ -55,12 +55,12 @@ class _ContainerPageState extends ConsumerState { @override Widget build(BuildContext context) { - final errors = ref.watch(_provider.select((p) => p.errors)); + final err = ref.watch(_provider.select((p) => p.error)); return Scaffold( appBar: _buildAppBar(), body: SafeArea(child: _buildMain()), - floatingActionButton: errors.isEmpty ? _buildFAB() : null, + floatingActionButton: err == null ? _buildFAB() : null, ); } @@ -84,7 +84,7 @@ class _ContainerPageState extends ConsumerState { Widget _buildMain() { final containerState = _containerState; - if (containerState.errors.isNotEmpty && containerState.items == null) { + if (containerState.error != null && containerState.items == null) { return SizedBox.expand( child: Column( children: [ @@ -93,7 +93,7 @@ class _ContainerPageState extends ConsumerState { UIs.height13, Padding( padding: const EdgeInsets.symmetric(horizontal: 23), - child: Text(containerState.errors.map((e) => e.toString()).join('\n')), + child: Text(containerState.error.toString()), ), const Spacer(), UIs.height13, @@ -341,7 +341,7 @@ class _ContainerPageState extends ConsumerState { return ExpandTile( leading: const Icon(Icons.settings), title: Text(libL10n.setting), - initiallyExpanded: containerState.errors.isNotEmpty, + initiallyExpanded: containerState.error != null, children: _SettingsMenuItems.values.map((item) => _buildSettingTile(item, containerState)).toList(), ).cardx; } From 533fa0a763065f16ac6345c30ada7f0946f2ee85 Mon Sep 17 00:00:00 2001 From: GT610 Date: Mon, 12 Jan 2026 20:14:42 +0800 Subject: [PATCH 05/11] feat(container): Add Podman Docker emulation detection function Add detection for Podman Docker emulation in the container module. When detected, a prompt message will be displayed and users will be advised to switch to Podman settings. Updated the multilingual translation files to support the new features. --- lib/data/model/app/error.dart | 1 + lib/data/provider/container.dart | 64 ++++++++++++++++++++++++++------ lib/generated/l10n/l10n.dart | 6 +++ lib/generated/l10n/l10n_de.dart | 4 ++ lib/generated/l10n/l10n_en.dart | 4 ++ lib/generated/l10n/l10n_es.dart | 4 ++ lib/generated/l10n/l10n_fr.dart | 4 ++ lib/generated/l10n/l10n_id.dart | 4 ++ lib/generated/l10n/l10n_ja.dart | 4 ++ lib/generated/l10n/l10n_nl.dart | 4 ++ lib/generated/l10n/l10n_pt.dart | 4 ++ lib/generated/l10n/l10n_ru.dart | 4 ++ lib/generated/l10n/l10n_tr.dart | 4 ++ lib/generated/l10n/l10n_uk.dart | 4 ++ lib/generated/l10n/l10n_zh.dart | 8 ++++ lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 3 +- lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_id.arb | 3 +- lib/l10n/app_ja.arb | 3 +- lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_tr.arb | 3 +- lib/l10n/app_uk.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/l10n/app_zh_tw.arb | 3 +- 28 files changed, 137 insertions(+), 25 deletions(-) diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart index 8ff3f41f6..d3d64ca8c 100644 --- a/lib/data/model/app/error.dart +++ b/lib/data/model/app/error.dart @@ -26,6 +26,7 @@ enum ContainerErrType { parsePs, parseImages, parseStats, + podmanDetected, } class ContainerErr extends Err { diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 99473b5ab..8796aee3d 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -6,6 +6,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:flutter/material.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/core/extension/ssh_client.dart'; import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/scripts/script_consts.dart'; @@ -18,6 +19,7 @@ part 'container.freezed.dart'; part 'container.g.dart'; final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found"); +final _podmanEmulationMsg = 'Emulate Docker CLI using podman'; @freezed abstract class ContainerState with _$ContainerState { @@ -121,9 +123,21 @@ class ContainerNotifier extends _$ContainerNotifier { final version = json.decode(verRaw)['Client']['Version']; state = state.copyWith(version: version, error: null); } catch (e, trace) { - state = state.copyWith( - error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'), - ); + final errorMsg = '$e'; + if (errorMsg.contains(_podmanEmulationMsg)) { + state = state.copyWith( + error: ContainerErr( + type: ContainerErrType.podmanDetected, + message: l10n.podmanDockerEmulationDetected, + ), + ); + return; + } + if (state.error == null) { + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'), + ); + } Loggers.app.warning('Container version failed', e, trace); } @@ -139,9 +153,21 @@ class ContainerNotifier extends _$ContainerNotifier { final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList(); state = state.copyWith(items: items); } catch (e, trace) { - state = state.copyWith( - error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'), - ); + final errorMsg = '$e'; + if (errorMsg.contains(_podmanEmulationMsg)) { + state = state.copyWith( + error: ContainerErr( + type: ContainerErrType.podmanDetected, + message: l10n.podmanDockerEmulationDetected, + ), + ); + return; + } + if (state.error == null) { + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'), + ); + } Loggers.app.warning('Container ps failed', e, trace); } @@ -161,9 +187,21 @@ class ContainerNotifier extends _$ContainerNotifier { } state = state.copyWith(images: images); } catch (e, trace) { - state = state.copyWith( - error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'), - ); + final errorMsg = '$e'; + if (errorMsg.contains(_podmanEmulationMsg)) { + state = state.copyWith( + error: ContainerErr( + type: ContainerErrType.podmanDetected, + message: l10n.podmanDockerEmulationDetected, + ), + ); + return; + } + if (state.error == null) { + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'), + ); + } Loggers.app.warning('Container images failed', e, trace); } @@ -188,9 +226,11 @@ class ContainerNotifier extends _$ContainerNotifier { item.parseStats(statsLine, state.version); } } catch (e, trace) { - state = state.copyWith( - error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'), - ); + if (state.error == null) { + state = state.copyWith( + error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'), + ); + } Loggers.app.warning('Parse docker stats: $statsRaw', e, trace); } } diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 072cce4e2..e796c7be9 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -1933,6 +1933,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Logs'** String get logs; + + /// No description provided for @podmanDockerEmulationDetected. + /// + /// In en, this message translates to: + /// **'Podman Docker emulation detected. Please switch to Podman in settings.'** + String get podmanDockerEmulationDetected; } class _AppLocalizationsDelegate diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index d794efdec..5a378600a 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -1031,4 +1031,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get logs => 'Protokolle'; + + @override + String get podmanDockerEmulationDetected => + 'Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman.'; } diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 94f6a2e85..977c1a054 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -1022,4 +1022,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get logs => 'Logs'; + + @override + String get podmanDockerEmulationDetected => + 'Podman Docker emulation detected. Please switch to Podman in settings.'; } diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 376cd7392..ccc304498 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -1033,4 +1033,8 @@ class AppLocalizationsEs extends AppLocalizations { @override String get logs => 'Registros'; + + @override + String get podmanDockerEmulationDetected => + 'Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración.'; } diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index 3e4f9a87c..e647ad80f 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -1036,4 +1036,8 @@ class AppLocalizationsFr extends AppLocalizations { @override String get logs => 'Journaux'; + + @override + String get podmanDockerEmulationDetected => + 'Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres.'; } diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index e6406b5a2..d1aa04093 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -1022,4 +1022,8 @@ class AppLocalizationsId extends AppLocalizations { @override String get logs => 'Log'; + + @override + String get podmanDockerEmulationDetected => + 'Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan.'; } diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index a70553e4d..cceb56092 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -992,4 +992,8 @@ class AppLocalizationsJa extends AppLocalizations { @override String get logs => 'ログ'; + + @override + String get podmanDockerEmulationDetected => + 'Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。'; } diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 805df546c..911a8dfc9 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -1029,4 +1029,8 @@ class AppLocalizationsNl extends AppLocalizations { @override String get logs => 'Logboeken'; + + @override + String get podmanDockerEmulationDetected => + 'Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen.'; } diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index 6079f1aa0..66fdd64d0 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -1024,4 +1024,8 @@ class AppLocalizationsPt extends AppLocalizations { @override String get logs => 'Logs'; + + @override + String get podmanDockerEmulationDetected => + 'Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações.'; } diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 88aa8a329..42c79304c 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -1028,4 +1028,8 @@ class AppLocalizationsRu extends AppLocalizations { @override String get logs => 'Журналы'; + + @override + String get podmanDockerEmulationDetected => + 'Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках.'; } diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index 71f554cb9..26cb52331 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -1023,4 +1023,8 @@ class AppLocalizationsTr extends AppLocalizations { @override String get logs => 'Günlükler'; + + @override + String get podmanDockerEmulationDetected => + 'Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman\'a geçin.'; } diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 935d33d01..39fe3c94c 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -1028,4 +1028,8 @@ class AppLocalizationsUk extends AppLocalizations { @override String get logs => 'Журнали'; + + @override + String get podmanDockerEmulationDetected => + 'Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях.'; } diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index 1de014192..3d38205bf 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -977,6 +977,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get logs => '日志'; + + @override + String get podmanDockerEmulationDetected => + '检测到 Podman Docker 仿真。请在设置中切换到 Podman。'; } /// The translations for Chinese, as used in Taiwan (`zh_TW`). @@ -1931,4 +1935,8 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get logs => '日誌'; + + @override + String get podmanDockerEmulationDetected => + '檢測到 Podman Docker 仿真。請在設定中切換到 Podman。'; } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f1f87701a..81ec0826a 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -294,5 +294,6 @@ "write": "Schreiben", "writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.", "writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.", - "logs": "Protokolle" + "logs": "Protokolle", + "podmanDockerEmulationDetected": "Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 53468f374..20d5d2278 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -304,5 +304,6 @@ "menuGitHubRepository": "GitHub Repository", "menuWiki": "Wiki", "menuHelp": "Help", - "logs": "Logs" + "logs": "Logs", + "podmanDockerEmulationDetected": "Podman Docker emulation detected. Please switch to Podman in settings." } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 0d2eb6d7f..9a02db126 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -294,5 +294,6 @@ "write": "Escribir", "writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.", "writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.", - "logs": "Registros" + "logs": "Registros", + "podmanDockerEmulationDetected": "Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8c6efe425..0792d7053 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -294,5 +294,6 @@ "write": "Écrire", "writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.", "writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.", - "logs": "Journaux" + "logs": "Journaux", + "podmanDockerEmulationDetected": "Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres." } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 026deffe1..532faad9d 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -294,5 +294,6 @@ "write": "Tulis", "writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.", "writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.", - "logs": "Log" + "logs": "Log", + "podmanDockerEmulationDetected": "Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan." } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index ac1414326..822071ed5 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -294,5 +294,6 @@ "write": "書き込み", "writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。", "writeScriptTip": "サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。", - "logs": "ログ" + "logs": "ログ", + "podmanDockerEmulationDetected": "Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。" } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 653572722..695b790a6 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -294,5 +294,6 @@ "write": "Schrijven", "writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.", "writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.", - "logs": "Logboeken" + "logs": "Logboeken", + "podmanDockerEmulationDetected": "Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 22ff962b3..6d2a2f049 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -294,5 +294,6 @@ "write": "Escrita", "writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.", "writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.", - "logs": "Logs" + "logs": "Logs", + "podmanDockerEmulationDetected": "Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index acb3b6e86..907f70bb7 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -294,5 +294,6 @@ "write": "Запись", "writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.", "writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.", - "logs": "Журналы" + "logs": "Журналы", + "podmanDockerEmulationDetected": "Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках." } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 8bb4d6d0e..8f4d884ad 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -294,5 +294,6 @@ "write": "Yaz", "writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.", "writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.", - "logs": "Günlükler" + "logs": "Günlükler", + "podmanDockerEmulationDetected": "Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman'a geçin." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 2a1807c2d..8416ac71f 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -294,5 +294,6 @@ "write": "Записати", "writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.", "writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.", - "logs": "Журнали" + "logs": "Журнали", + "podmanDockerEmulationDetected": "Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index bb6d42435..c6e44459d 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -301,5 +301,6 @@ "menuGitHubRepository": "GitHub 仓库", "menuWiki": "Wiki", "menuHelp": "帮助", - "logs": "日志" + "logs": "日志", + "podmanDockerEmulationDetected": "检测到 Podman Docker 仿真。请在设置中切换到 Podman。" } diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index c1ea6c368..0c81f922c 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -294,5 +294,6 @@ "write": "寫入", "writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。", "writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。", - "logs": "日誌" + "logs": "日誌", + "podmanDockerEmulationDetected": "檢測到 Podman Docker 仿真。請在設定中切換到 Podman。" } From 8218e21670e539a16c826e4dc3dba2f9c68ee68a Mon Sep 17 00:00:00 2001 From: GT610 Date: Sat, 17 Jan 2026 11:36:07 +0800 Subject: [PATCH 06/11] fix: Fix error handling in SSH client and container operations Fix the issue where the SSH client does not handle stderr when executing commands Error handling for an empty client in the container addition operation Fix the issue where null may be returned during server page operations --- lib/core/extension/ssh_client.dart | 1 + lib/data/provider/container.dart | 6 ++++++ lib/view/page/server/tab/utils.dart | 6 ++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 43e16ef89..5e46861b6 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -146,6 +146,7 @@ extension SSHClientX on SSHClient { }, onStdout: onStdout, entry: entry, + stderr: false, ); return (session.exitCode, output); } diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 8796aee3d..7c097e2fb 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -91,6 +91,12 @@ class ContainerNotifier extends _$ContainerNotifier { String raw = ''; if (client != null) { (code, raw) = await client!.execWithPwd(cmd, context: context, id: hostId); + } else { + state = state.copyWith( + isBusy: false, + error: ContainerErr(type: ContainerErrType.noClient), + ); + return; } if (!ref.mounted) return; diff --git a/lib/view/page/server/tab/utils.dart b/lib/view/page/server/tab/utils.dart index 5f2a6a79d..13056644e 100644 --- a/lib/view/page/server/tab/utils.dart +++ b/lib/view/page/server/tab/utils.dart @@ -53,7 +53,8 @@ extension _Operation on _ServerPageState { ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system, customDir: null), context: context, id: srv.id, - ); + ) ?? + (null, ''); }, typ: l10n.suspend, name: srv.spi.name, @@ -81,7 +82,8 @@ extension _Operation on _ServerPageState { ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null), context: context, id: srv.id, - ); + ) ?? + (null, ''); }, typ: l10n.reboot, name: srv.spi.name, From 4d254fde0a17fdf96b6d8cda207ab53a42371377 Mon Sep 17 00:00:00 2001 From: GT610 Date: Sat, 17 Jan 2026 11:55:55 +0800 Subject: [PATCH 07/11] fix(container): Check if client is empty before running the command When the client is null, directly return an error to avoid null pointer exception --- lib/data/provider/container.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index 7c097e2fb..c976f1307 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -272,6 +272,10 @@ class ContainerNotifier extends _$ContainerNotifier { } Future run(String cmd, {bool autoRefresh = true}) async { + if (client == null) { + return ContainerErr(type: ContainerErrType.noClient); + } + cmd = switch (state.type) { ContainerType.docker => 'docker $cmd', ContainerType.podman => 'podman $cmd', From e0bf69ebbe5665a4cd345a4a72bbbc2e6e622275 Mon Sep 17 00:00:00 2001 From: GT610 Date: Sat, 17 Jan 2026 12:23:11 +0800 Subject: [PATCH 08/11] fix: Revert `stderr` ignore --- lib/core/extension/ssh_client.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/extension/ssh_client.dart b/lib/core/extension/ssh_client.dart index 5e46861b6..43e16ef89 100644 --- a/lib/core/extension/ssh_client.dart +++ b/lib/core/extension/ssh_client.dart @@ -146,7 +146,6 @@ extension SSHClientX on SSHClient { }, onStdout: onStdout, entry: entry, - stderr: false, ); return (session.exitCode, output); } From 7a24e60dce3240e4edffdb1f4619741c4bcfa6db Mon Sep 17 00:00:00 2001 From: GT610 Date: Sat, 17 Jan 2026 13:18:49 +0800 Subject: [PATCH 09/11] fix(container): Detect Podman simulation in advance and optimize error handling Move the Podman simulation detection to the initial parsing stage to avoid redundant checks Remove duplicated error handling code and simplify the logic --- lib/data/provider/container.dart | 41 +++++++++----------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index c976f1307..b1b136085 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -110,6 +110,17 @@ class ContainerNotifier extends _$ContainerNotifier { return; } + /// Pre-parse Podman detection + if (raw.contains(_podmanEmulationMsg)) { + state = state.copyWith( + error: ContainerErr( + type: ContainerErrType.podmanDetected, + message: l10n.podmanDockerEmulationDetected, + ), + ); + return; + } + // Check result segments count final segments = raw.split(ScriptConstants.separator); if (segments.length != ContainerCmdType.values.length) { @@ -129,16 +140,6 @@ class ContainerNotifier extends _$ContainerNotifier { final version = json.decode(verRaw)['Client']['Version']; state = state.copyWith(version: version, error: null); } catch (e, trace) { - final errorMsg = '$e'; - if (errorMsg.contains(_podmanEmulationMsg)) { - state = state.copyWith( - error: ContainerErr( - type: ContainerErrType.podmanDetected, - message: l10n.podmanDockerEmulationDetected, - ), - ); - return; - } if (state.error == null) { state = state.copyWith( error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'), @@ -159,16 +160,6 @@ class ContainerNotifier extends _$ContainerNotifier { final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList(); state = state.copyWith(items: items); } catch (e, trace) { - final errorMsg = '$e'; - if (errorMsg.contains(_podmanEmulationMsg)) { - state = state.copyWith( - error: ContainerErr( - type: ContainerErrType.podmanDetected, - message: l10n.podmanDockerEmulationDetected, - ), - ); - return; - } if (state.error == null) { state = state.copyWith( error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'), @@ -193,16 +184,6 @@ class ContainerNotifier extends _$ContainerNotifier { } state = state.copyWith(images: images); } catch (e, trace) { - final errorMsg = '$e'; - if (errorMsg.contains(_podmanEmulationMsg)) { - state = state.copyWith( - error: ContainerErr( - type: ContainerErrType.podmanDetected, - message: l10n.podmanDockerEmulationDetected, - ), - ); - return; - } if (state.error == null) { state = state.copyWith( error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'), From 3fe464cc2088306f20249ee1728777f5a29402e4 Mon Sep 17 00:00:00 2001 From: GT610 Date: Sat, 17 Jan 2026 13:54:10 +0800 Subject: [PATCH 10/11] fix(container): Fix the error handling logic during container command execution Increase the inspection of error outputs, including handling situations such as sudo password prompts and Podman not being installed --- lib/data/provider/container.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index b1b136085..afaaaedac 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -89,6 +89,7 @@ class ContainerNotifier extends _$ContainerNotifier { final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats)); int? code; String raw = ''; + final errs = []; if (client != null) { (code, raw) = await client!.execWithPwd(cmd, context: context, id: hostId); } else { @@ -105,7 +106,7 @@ class ContainerNotifier extends _$ContainerNotifier { if (!context.mounted) return; /// Code 127 means command not found - if (code == 127 || raw.contains(_dockerNotFound)) { + if (code == 127 || raw.contains(_dockerNotFound) || errs.join().contains(_dockerNotFound)) { state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled)); return; } @@ -121,6 +122,19 @@ class ContainerNotifier extends _$ContainerNotifier { return; } + /// Filter out sudo password prompt from output + if (errs.any((e) => e.contains('[sudo] password'))) { + raw = raw.split('\n').where((line) => !line.contains('[sudo] password')).join('\n'); + } + + /// Detect Podman not installed when using Podman mode + if (state.type == ContainerType.podman && + (errs.any((e) => e.contains('podman: not found')) || + raw.contains('podman: not found'))) { + state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled)); + return; + } + // Check result segments count final segments = raw.split(ScriptConstants.separator); if (segments.length != ContainerCmdType.values.length) { From eb5c2320e42e32042ae1796d8494a2a0106af4a5 Mon Sep 17 00:00:00 2001 From: GT610 Date: Sat, 17 Jan 2026 13:08:43 +0800 Subject: [PATCH 11/11] refactor(macOS): Remove unused path_provider_foundation plugin --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 788ab2b9a..1058d345a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,7 +12,6 @@ import flutter_secure_storage_macos import icloud_storage import local_auth_darwin import package_info_plus -import path_provider_foundation import screen_retriever_macos import share_plus import shared_preferences_foundation @@ -28,7 +27,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))