diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n.dart b/.dart_tool/flutter_gen/gen_l10n/l10n.dart index cb9b6581..80e1e0c5 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n.dart @@ -328,6 +328,12 @@ abstract class S { /// **'Connected'** String get connected; + /// No description provided for @container. + /// + /// In en, this message translates to: + /// **'Container'** + String get container; + /// No description provided for @containerName. /// /// In en, this message translates to: @@ -1420,6 +1426,12 @@ abstract class S { /// **'The suspend function requires root privileges and systemd support.'** String get suspendTip; + /// No description provided for @switchTo. + /// + /// In en, this message translates to: + /// **'Switch to {val}'** + String switchTo(Object val); + /// No description provided for @syncTip. /// /// In en, this message translates to: diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart index 63256704..1632c3ed 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_de.dart @@ -120,6 +120,9 @@ class SDe extends S { @override String get connected => 'in Verbindung gebracht'; + @override + String get container => 'Container'; + @override String get containerName => 'Container Name'; @@ -694,6 +697,11 @@ class SDe extends S { @override String get suspendTip => 'Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.'; + @override + String switchTo(Object val) { + return 'Wechseln zu $val'; + } + @override String get syncTip => 'Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart index ca8998e1..7f081207 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_en.dart @@ -120,6 +120,9 @@ class SEn extends S { @override String get connected => 'Connected'; + @override + String get container => 'Container'; + @override String get containerName => 'Container name'; @@ -694,6 +697,11 @@ class SEn extends S { @override String get suspendTip => 'The suspend function requires root privileges and systemd support.'; + @override + String switchTo(Object val) { + return 'Switch to $val'; + } + @override String get syncTip => 'A restart may be required for some changes to take effect.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart index 540870e5..6262752c 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_fr.dart @@ -120,6 +120,9 @@ class SFr extends S { @override String get connected => 'Connecté'; + @override + String get container => 'Conteneurs'; + @override String get containerName => 'Nom du conteneur'; @@ -694,6 +697,11 @@ class SFr extends S { @override String get suspendTip => 'La fonction de suspension nécessite des privilèges root et la prise en charge de systemd.'; + @override + String switchTo(Object val) { + return 'Passer à $val'; + } + @override String get syncTip => 'Un redémarrage peut être nécessaire pour que certains changements prennent effet.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart index 439d097c..b5315470 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_id.dart @@ -120,6 +120,9 @@ class SId extends S { @override String get connected => 'Terhubung'; + @override + String get container => 'Wadah'; + @override String get containerName => 'Nama kontainer'; @@ -694,6 +697,11 @@ class SId extends S { @override String get suspendTip => 'Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.'; + @override + String switchTo(Object val) { + return 'Beralih ke $val'; + } + @override String get syncTip => 'Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.'; diff --git a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart index 923df7a7..534229e8 100644 --- a/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart +++ b/.dart_tool/flutter_gen/gen_l10n/l10n_zh.dart @@ -120,6 +120,9 @@ class SZh extends S { @override String get connected => '已连接'; + @override + String get container => '容器'; + @override String get containerName => '容器名'; @@ -694,6 +697,11 @@ class SZh extends S { @override String get suspendTip => 'suspend 功能需要 root 权限及 systemd 支持。'; + @override + String switchTo(Object val) { + return '切换到 $val'; + } + @override String get syncTip => '可能需要重新启动,某些更改才能生效。'; @@ -948,6 +956,9 @@ class SZhTw extends SZh { @override String get connected => '已連接'; + @override + String get container => '容器'; + @override String get containerName => '容器名稱'; @@ -1522,6 +1533,11 @@ class SZhTw extends SZh { @override String get suspendTip => 'suspend 功能需要 root 權限及 systemd 支持。'; + @override + String switchTo(Object val) { + return '切換到 $val'; + } + @override String get syncTip => '可能需要重新啟動,某些更改才能生效。'; diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3a262ef4..71d16916 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -586,7 +586,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -596,7 +596,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -720,7 +720,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -730,7 +730,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -748,7 +748,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -758,7 +758,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -779,7 +779,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -792,7 +792,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; @@ -818,7 +818,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -831,7 +831,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -854,7 +854,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_TEAM = BA88US33G6; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -867,7 +867,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.StatusWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -890,7 +890,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -902,7 +902,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; @@ -931,7 +931,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -943,7 +943,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; @@ -969,7 +969,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 707; + CURRENT_PROJECT_VERSION = 709; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_PREVIEWS = YES; @@ -981,7 +981,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.707; + MARKETING_VERSION = 1.0.709; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox.WatchEnd; PRODUCT_NAME = ServerBox; diff --git a/lib/core/route.dart b/lib/core/route.dart index cba00e6a..f7b84f1c 100644 --- a/lib/core/route.dart +++ b/lib/core/route.dart @@ -3,7 +3,7 @@ import 'package:toolbox/core/analysis.dart'; import 'package:toolbox/data/model/server/private_key_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/view/page/backup.dart'; -import 'package:toolbox/view/page/docker.dart'; +import 'package:toolbox/view/page/container.dart'; import 'package:toolbox/view/page/home.dart'; import 'package:toolbox/view/page/ping.dart'; import 'package:toolbox/view/page/private_key/edit.dart'; @@ -150,7 +150,7 @@ class AppRoute { } static AppRoute docker({Key? key, required ServerPrivateInfo spi}) { - return AppRoute(DockerManagePage(key: key, spi: spi), 'docker'); + return AppRoute(ContainerPage(key: key, spi: spi), 'docker'); } /// - Pop true if the text is changed & [path] is not null diff --git a/lib/data/model/app/backup.dart b/lib/data/model/app/backup.dart index 2b43cee2..e119c9cf 100644 --- a/lib/data/model/app/backup.dart +++ b/lib/data/model/app/backup.dart @@ -20,7 +20,7 @@ class Backup { final List spis; final List snippets; final List keys; - final Map dockerHosts; + final Map container; final Map settings; final Map history; final int? lastModTime; @@ -31,7 +31,7 @@ class Backup { required this.spis, required this.snippets, required this.keys, - required this.dockerHosts, + required this.container, required this.settings, required this.history, this.lastModTime, @@ -48,7 +48,7 @@ class Backup { keys = (json['keys'] as List) .map((e) => PrivateKeyInfo.fromJson(e)) .toList(), - dockerHosts = json['dockerHosts'] ?? {}, + container = json['container'] ?? {}, settings = json['settings'] ?? {}, lastModTime = json['lastModTime'], history = json['history'] ?? {}; @@ -59,7 +59,7 @@ class Backup { 'spis': spis, 'snippets': snippets, 'keys': keys, - 'dockerHosts': dockerHosts, + 'container': container, 'settings': settings, 'lastModTime': lastModTime, 'history': history, @@ -71,7 +71,7 @@ class Backup { spis = Stores.server.fetch(), snippets = Stores.snippet.fetch(), keys = Stores.key.fetch(), - dockerHosts = Stores.docker.box.toJson(), + container = Stores.docker.box.toJson(), settings = Stores.setting.box.toJson(), lastModTime = Stores.lastModTime, history = Stores.history.box.toJson(); @@ -110,8 +110,8 @@ class Backup { for (final s in history.keys) { Stores.history.box.put(s, history[s]); } - for (final k in dockerHosts.keys) { - final val = dockerHosts[k]; + for (final k in container.keys) { + final val = container[k]; if (val != null && val is String && val.isNotEmpty) { Stores.docker.put(k, val); } diff --git a/lib/data/model/app/error.dart b/lib/data/model/app/error.dart index e57b9819..a0e7ad94 100644 --- a/lib/data/model/app/error.dart +++ b/lib/data/model/app/error.dart @@ -33,7 +33,7 @@ class SSHErr extends Err { } } -enum DockerErrType { +enum ContainerErrType { unknown, noClient, notInstalled, @@ -45,12 +45,12 @@ enum DockerErrType { parseStats, } -class DockerErr extends Err { - DockerErr({required super.type, super.message}) : super(from: ErrFrom.docker); +class ContainerErr extends Err { + ContainerErr({required super.type, super.message}) : super(from: ErrFrom.docker); @override String toString() { - return 'DockerErr<$type>: $message'; + return 'ContainerErr<$type>: $message'; } } diff --git a/lib/data/model/app/menu.dart b/lib/data/model/app/menu.dart index b2e41d01..ccf104f9 100644 --- a/lib/data/model/app/menu.dart +++ b/lib/data/model/app/menu.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:toolbox/core/extension/context/locale.dart'; -enum ServerTabMenuType { +enum ServerTabMenu { terminal, sftp, - docker, + container, process, pkg, //snippet, @@ -12,40 +12,40 @@ enum ServerTabMenuType { IconData get icon { switch (this) { - case ServerTabMenuType.sftp: + case ServerTabMenu.sftp: return Icons.insert_drive_file; //case ServerTabMenuType.snippet: //return Icons.code; - case ServerTabMenuType.pkg: + case ServerTabMenu.pkg: return Icons.system_security_update; - case ServerTabMenuType.docker: + case ServerTabMenu.container: return Icons.view_agenda; - case ServerTabMenuType.process: + case ServerTabMenu.process: return Icons.list_alt_outlined; - case ServerTabMenuType.terminal: + case ServerTabMenu.terminal: return Icons.terminal; } } String get toStr { switch (this) { - case ServerTabMenuType.sftp: + case ServerTabMenu.sftp: return 'SFTP'; //case ServerTabMenuType.snippet: //return l10n.snippet; - case ServerTabMenuType.pkg: + case ServerTabMenu.pkg: return l10n.pkg; - case ServerTabMenuType.docker: - return 'Docker'; - case ServerTabMenuType.process: + case ServerTabMenu.container: + return l10n.container; + case ServerTabMenu.process: return l10n.process; - case ServerTabMenuType.terminal: + case ServerTabMenu.terminal: return l10n.terminal; } } } -enum DockerMenuType { +enum ContainerMenu { start, stop, restart, @@ -55,7 +55,7 @@ enum DockerMenuType { //stats, ; - static List items(bool running) { + static List items(bool running) { if (running) { return [ stop, @@ -72,17 +72,17 @@ enum DockerMenuType { IconData get icon { switch (this) { - case DockerMenuType.start: + case ContainerMenu.start: return Icons.play_arrow; - case DockerMenuType.stop: + case ContainerMenu.stop: return Icons.stop; - case DockerMenuType.restart: + case ContainerMenu.restart: return Icons.restart_alt; - case DockerMenuType.rm: + case ContainerMenu.rm: return Icons.delete; - case DockerMenuType.logs: + case ContainerMenu.logs: return Icons.logo_dev; - case DockerMenuType.terminal: + case ContainerMenu.terminal: return Icons.terminal; // case DockerMenuType.stats: // return Icons.bar_chart; @@ -91,24 +91,24 @@ enum DockerMenuType { String get toStr { switch (this) { - case DockerMenuType.start: + case ContainerMenu.start: return l10n.start; - case DockerMenuType.stop: + case ContainerMenu.stop: return l10n.stop; - case DockerMenuType.restart: + case ContainerMenu.restart: return l10n.restart; - case DockerMenuType.rm: + case ContainerMenu.rm: return l10n.delete; - case DockerMenuType.logs: + case ContainerMenu.logs: return l10n.log; - case DockerMenuType.terminal: + case ContainerMenu.terminal: return l10n.terminal; // case DockerMenuType.stats: // return s.stats; } } - PopupMenuItem get widget => _build(this, icon, toStr); + PopupMenuItem get widget => _build(this, icon, toStr); } PopupMenuItem _build(T t, IconData icon, String text) { diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index ab85fd8f..c0f98411 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -224,30 +224,6 @@ const _statusCmds = [ 'nvidia-smi -q -x', ]; -enum DockerCmdType { - version, - ps, - //stats, - images, - ; - - String get exec { - switch (this) { - case DockerCmdType.version: - return 'docker version'; - case DockerCmdType.ps: - return 'docker ps -a'; - // case DockerCmdType.stats: - // return 'docker stats --no-stream'; - case DockerCmdType.images: - return 'docker image ls'; - } - } - - static final execAll = - values.map((e) => e.exec).join(' && echo $seperator && '); -} - enum BSDStatusCmdType { echo, time, diff --git a/lib/data/model/container/image.dart b/lib/data/model/container/image.dart new file mode 100644 index 00000000..b3a8a5f2 --- /dev/null +++ b/lib/data/model/container/image.dart @@ -0,0 +1,115 @@ +import 'dart:convert'; + +import 'package:toolbox/core/extension/numx.dart'; +import 'package:toolbox/data/model/container/type.dart'; + +abstract final class ContainerImg { + final String? repository = null; + final String? tag = null; + final String? id = null; + String? get sizeMB; + int? get containersCount; + + factory ContainerImg.fromRawJson(String s, ContainerType typ) => typ.img(s); +} + +final class PodmanImg implements ContainerImg { + @override + final String? repository; + @override + final String? tag; + @override + final String? id; + final int? created; + final int? size; + final int? containers; + + PodmanImg({ + this.repository, + this.tag, + this.id, + this.created, + this.size, + this.containers, + }); + + @override + String? get sizeMB => size?.convertBytes; + + @override + int? get containersCount => containers; + + factory PodmanImg.fromRawJson(String str) => + PodmanImg.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory PodmanImg.fromJson(Map json) => PodmanImg( + repository: json["repository"], + tag: json["tag"], + id: json["Id"], + created: json["Created"], + size: json["Size"], + containers: json["Containers"], + ); + + Map toJson() => { + "repository": repository, + "tag": tag, + "Id": id, + "Created": created, + "Size": size, + "Containers": containers, + }; +} + +final class DockerImg implements ContainerImg { + final String containers; + final String createdAt; + @override + final String id; + @override + final String repository; + final String size; + @override + final String tag; + + DockerImg({ + required this.containers, + required this.createdAt, + required this.id, + required this.repository, + required this.size, + required this.tag, + }); + + @override + String? get sizeMB => size; + + @override + int? get containersCount => + containers == 'N/A' ? 0 : int.tryParse(containers); + + factory DockerImg.fromRawJson(String str) => + DockerImg.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory DockerImg.fromJson(Map json) => DockerImg( + containers: json["Containers"], + createdAt: json["CreatedAt"], + id: json["ID"], + repository: json["Repository"], + size: json["Size"], + tag: json["Tag"], + ); + + Map toJson() => { + "Containers": containers, + "CreatedAt": createdAt, + "ID": id, + "Repository": repository, + "Size": size, + "Tag": tag, + }; +} diff --git a/lib/data/model/container/ps.dart b/lib/data/model/container/ps.dart new file mode 100644 index 00000000..5ba085e9 --- /dev/null +++ b/lib/data/model/container/ps.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; + +import 'package:toolbox/data/model/container/type.dart'; + +abstract final class ContainerPs { + final String? id = null; + final String? image = null; + String? get name; + String? get cmd; + bool get running; + + factory ContainerPs.fromRawJson(String s, ContainerType typ) => typ.ps(s); +} + +final class PodmanPs implements ContainerPs { + final List? command; + final DateTime? created; + final bool? exited; + @override + final String? id; + @override + final String? image; + final List? names; + final int? startedAt; + + PodmanPs({ + this.command, + this.created, + this.exited, + this.id, + this.image, + this.names, + this.startedAt, + }); + + @override + String? get name => names?.firstOrNull; + + @override + String? get cmd => command?.firstOrNull; + + @override + bool get running => exited != true; + + factory PodmanPs.fromRawJson(String str) => + PodmanPs.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory PodmanPs.fromJson(Map json) => PodmanPs( + command: json["Command"] == null + ? [] + : List.from(json["Command"]!.map((x) => x)), + created: + json["Created"] == null ? null : DateTime.parse(json["Created"]), + exited: json["Exited"], + id: json["Id"], + image: json["Image"], + names: json["Names"] == null + ? [] + : List.from(json["Names"]!.map((x) => x)), + startedAt: json["StartedAt"], + ); + + Map toJson() => { + "Command": + command == null ? [] : List.from(command!.map((x) => x)), + "Created": created?.toIso8601String(), + "Exited": exited, + "Id": id, + "Image": image, + "Names": names == null ? [] : List.from(names!.map((x) => x)), + "StartedAt": startedAt, + }; +} + +final class DockerPs implements ContainerPs { + final String? command; + final String? createdAt; + @override + final String? id; + @override + final String? image; + final String? names; + final String? state; + + DockerPs({ + this.command, + this.createdAt, + this.id, + this.image, + this.names, + this.state, + }); + + @override + String? get name => names; + + @override + String? get cmd => command; + + @override + bool get running => state == 'running'; + + factory DockerPs.fromRawJson(String str) => + DockerPs.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory DockerPs.fromJson(Map json) => DockerPs( + command: json["Command"], + createdAt: json["CreatedAt"], + id: json["ID"], + image: json["Image"], + names: json["Names"], + state: json["State"], + ); + + Map toJson() => { + "Command": command, + "CreatedAt": createdAt, + "ID": id, + "Image": image, + "Names": names, + "State": state, + }; +} diff --git a/lib/data/model/container/type.dart b/lib/data/model/container/type.dart new file mode 100644 index 00000000..e1c671ae --- /dev/null +++ b/lib/data/model/container/type.dart @@ -0,0 +1,18 @@ +import 'package:toolbox/data/model/container/image.dart'; +import 'package:toolbox/data/model/container/ps.dart'; + +enum ContainerType { + docker, + podman, + ; + + ContainerPs Function(String str) get ps => switch (this) { + ContainerType.docker => DockerPs.fromRawJson, + ContainerType.podman => PodmanPs.fromRawJson, + }; + + ContainerImg Function(String str) get img => switch (this) { + ContainerType.docker => DockerImg.fromRawJson, + ContainerType.podman => PodmanImg.fromRawJson, + }; +} \ No newline at end of file diff --git a/lib/data/model/container/version.dart b/lib/data/model/container/version.dart new file mode 100644 index 00000000..8eee2aaf --- /dev/null +++ b/lib/data/model/container/version.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; + +class Containerd { + final ContainerdClient client; + + Containerd({ + required this.client, + }); + + factory Containerd.fromRawJson(String str) => Containerd.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory Containerd.fromJson(Map json) => Containerd( + client: ContainerdClient.fromJson(json["Client"]), + ); + + Map toJson() => { + "Client": client.toJson(), + }; +} + +class ContainerdClient { + final String apiVersion; + final String version; + final String goVersion; + final String gitCommit; + final String builtTime; + final int built; + final String osArch; + final String os; + + ContainerdClient({ + required this.apiVersion, + required this.version, + required this.goVersion, + required this.gitCommit, + required this.builtTime, + required this.built, + required this.osArch, + required this.os, + }); + + factory ContainerdClient.fromRawJson(String str) => ContainerdClient.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory ContainerdClient.fromJson(Map json) => ContainerdClient( + apiVersion: json["APIVersion"], + version: json["Version"], + goVersion: json["GoVersion"], + gitCommit: json["GitCommit"], + builtTime: json["BuiltTime"], + built: json["Built"], + osArch: json["OsArch"], + os: json["Os"], + ); + + Map toJson() => { + "APIVersion": apiVersion, + "Version": version, + "GoVersion": goVersion, + "GitCommit": gitCommit, + "BuiltTime": builtTime, + "Built": built, + "OsArch": osArch, + "Os": os, + }; +} diff --git a/lib/data/model/docker/image.dart b/lib/data/model/docker/image.dart deleted file mode 100644 index 143d0a08..00000000 --- a/lib/data/model/docker/image.dart +++ /dev/null @@ -1,47 +0,0 @@ -final _dockerImageReg = RegExp(r'(\S+) +(\S+) +(\S+) +(.+) +(\S+)'); - -class DockerImage { - final String repo; - final String tag; - final String id; - final String created; - final String size; - - static final Map _cache = {}; - - DockerImage({ - required this.repo, - required this.tag, - required this.id, - required this.created, - required this.size, - }); - - Map toJson() { - return { - 'repo': repo, - 'tag': tag, - 'id': id, - 'created': created, - 'size': size, - }; - } - - factory DockerImage.fromRawStr(String raw) { - return _cache.putIfAbsent(raw, () => _parse(raw)); - } - - static DockerImage _parse(String raw) { - final match = _dockerImageReg.firstMatch(raw); - if (match == null) { - throw Exception('Invalid docker image: $raw'); - } - return DockerImage( - repo: match.group(1)!, - tag: match.group(2)!, - id: match.group(3)!, - created: match.group(4)!, - size: match.group(5)!, - ); - } -} diff --git a/lib/data/model/docker/ps.dart b/lib/data/model/docker/ps.dart deleted file mode 100644 index 90350b3c..00000000 --- a/lib/data/model/docker/ps.dart +++ /dev/null @@ -1,64 +0,0 @@ -final _seperator = RegExp(' +'); - -class DockerPsItem { - late String containerId; - late String image; - late String command; - late String created; - late String status; - late String ports; - late String name; - // String? cpu; - // String? mem; - // String? net; - // String? disk; - - DockerPsItem( - this.containerId, - this.image, - this.command, - this.created, - this.status, - this.ports, - this.name, - ); - - DockerPsItem.fromRawString(String rawString) { - List parts = rawString.split(_seperator); - parts = parts.map((e) => e.trim()).toList(); - - containerId = parts[0]; - image = parts[1]; - command = parts[2].trim(); - created = parts[3]; - status = parts[4]; - if (running && parts.length > 6) { - ports = parts[5]; - name = parts[6]; - } else { - ports = ''; - name = parts[5]; - } - } - - // void parseStats(String rawString) { - // if (rawString.isEmpty) { - // return; - // } - // final parts = rawString.split(_seperator); - // if (parts.length != 8) { - // return; - // } - // cpu = parts[2]; - // mem = parts[3]; - // net = parts[5]; - // disk = parts[6]; - // } - - bool get running => status.contains('Up '); - - @override - String toString() { - return 'DockerPsItem<$containerId@$name>'; - } -} diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart new file mode 100644 index 00000000..819e1f33 --- /dev/null +++ b/lib/data/provider/container.dart @@ -0,0 +1,213 @@ +import 'dart:async'; + +import 'package:dartssh2/dartssh2.dart'; +import 'package:flutter/material.dart'; +import 'package:toolbox/core/extension/ssh_client.dart'; +import 'package:toolbox/data/model/app/shell_func.dart'; +import 'package:toolbox/data/model/container/image.dart'; +import 'package:toolbox/data/model/container/ps.dart'; +import 'package:toolbox/data/model/app/error.dart'; +import 'package:toolbox/data/model/container/type.dart'; +import 'package:toolbox/data/model/container/version.dart'; +import 'package:toolbox/data/res/logger.dart'; +import 'package:toolbox/data/res/store.dart'; + +final _dockerNotFound = RegExp(r'command not found|Unknown command'); + +class ContainerProvider extends ChangeNotifier { + SSHClient? client; + String? userName; + List? items; + List? images; + String? version; + ContainerErr? error; + String? hostId; + String? runLog; + BuildContext? context; + ContainerType type; + + ContainerProvider({ + this.client, + this.userName, + this.hostId, + this.context, + }) : type = Stores.docker.getType(hostId) { + refresh(); + } + + Future setType(ContainerType type) async { + this.type = type; + Stores.docker.setType(hostId, type); + error = runLog = items = images = version = null; + notifyListeners(); + await refresh(); + } + + Future refresh() async { + var raw = ''; + await client?.execWithPwd( + _wrap(ContainerCmdType.execAll(type)), + context: context, + onStdout: (data, _) => raw = '$raw$data', + ); + + if (raw.contains(_dockerNotFound)) { + error = ContainerErr(type: ContainerErrType.notInstalled); + notifyListeners(); + return; + } + + // Check result segments count + final segments = raw.split(seperator); + if (segments.length != ContainerCmdType.values.length) { + error = ContainerErr( + type: ContainerErrType.segmentsNotMatch, + message: 'Container segments: ${segments.length}', + ); + Loggers.parse.warning('Container segments: ${segments.length}\n$raw'); + notifyListeners(); + return; + } + + // Parse docker version + final verRaw = ContainerCmdType.version.find(segments); + try { + final containerVersion = Containerd.fromRawJson(verRaw); + version = containerVersion.client.version; + } catch (e, trace) { + error = ContainerErr( + type: ContainerErrType.invalidVersion, + message: '$e', + ); + Loggers.parse.warning('Container version failed', e, trace); + } finally { + notifyListeners(); + } + + // Parse docker ps + final psRaw = ContainerCmdType.ps.find(segments); + + final lines = psRaw.split('\n'); + lines.removeWhere((element) => element.isEmpty); + if (lines.isNotEmpty) lines.removeAt(0); + items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList(); + + // Parse docker images + final imageRaw = ContainerCmdType.images.find(segments); + try { + final imgLines = imageRaw.split('\n'); + imgLines.removeWhere((element) => element.isEmpty); + if (imgLines.isNotEmpty) imgLines.removeAt(0); + images = imgLines.map((e) => ContainerImg.fromRawJson(e, type)).toList(); + } catch (e, trace) { + error = ContainerErr( + type: ContainerErrType.parseImages, + message: '$e', + ); + Loggers.parse.warning('Container images failed', e, trace); + } finally { + notifyListeners(); + } + + // Parse docker stats + // final statsRaw = DockerCmdType.stats.find(segments); + // try { + // final statsLines = statsRaw.split('\n'); + // statsLines.removeWhere((element) => element.isEmpty); + // if (statsLines.isNotEmpty) statsLines.removeAt(0); + // for (var item in items!) { + // final statsLine = statsLines.firstWhere( + // (element) => element.contains(item.containerId), + // orElse: () => '', + // ); + // if (statsLine.isEmpty) continue; + // item.parseStats(statsLine); + // } + // } catch (e, trace) { + // error = DockerErr( + // type: DockerErrType.parseStats, + // message: '$e', + // ); + // _logger.warning('Parse docker stats: $statsRaw', e, trace); + // } finally { + // notifyListeners(); + // } + } + + Future stop(String id) async => await run('stop $id'); + + Future start(String id) async => await run('start $id'); + + Future delete(String id, bool force) async { + if (force) { + return await run('rm -f $id'); + } + return await run('rm $id'); + } + + Future restart(String id) async => await run('restart $id'); + + Future run(String cmd) async { + cmd = switch (type) { + ContainerType.docker => 'docker $cmd', + ContainerType.podman => 'podman $cmd', + }; + + runLog = ''; + final errs = []; + final code = await client?.execWithPwd( + _wrap(cmd), + context: context, + onStdout: (data, _) { + runLog = '$runLog$data'; + notifyListeners(); + }, + onStderr: (data, _) => errs.add(data), + ); + runLog = null; + notifyListeners(); + + if (code != 0) { + return ContainerErr( + type: ContainerErrType.unknown, + message: errs.join('\n').trim(), + ); + } + await refresh(); + return null; + } + + /// wrap cmd with `docker host` + String _wrap(String cmd) { + final dockerHost = Stores.docker.fetch(hostId); + cmd = 'export LANG=en_US.UTF-8 && $cmd'; + final noDockerHost = dockerHost?.isEmpty ?? true; + if (!noDockerHost) { + cmd = 'export DOCKER_HOST=$dockerHost && $cmd'; + } + return cmd; + } +} + +const _jsonFmt = '--format "{{json .}}"'; + +enum ContainerCmdType { + version, + ps, + //stats, + images, + ; + + String exec(ContainerType type) { + final prefix = type.name; + return switch (this) { + ContainerCmdType.version => '$prefix version $_jsonFmt', + ContainerCmdType.ps => '$prefix ps -a $_jsonFmt', + // DockerCmdType.stats => '$prefix stats --no-stream'; + ContainerCmdType.images => '$prefix image ls $_jsonFmt', + }; + } + + static String execAll(ContainerType type) => + values.map((e) => e.exec(type)).join(' && echo $seperator && '); +} diff --git a/lib/data/provider/docker.dart b/lib/data/provider/docker.dart deleted file mode 100644 index f086d3a7..00000000 --- a/lib/data/provider/docker.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'dart:async'; - -import 'package:dartssh2/dartssh2.dart'; -import 'package:flutter/material.dart'; -import 'package:toolbox/core/extension/ssh_client.dart'; -import 'package:toolbox/data/model/app/shell_func.dart'; -import 'package:toolbox/data/model/docker/image.dart'; -import 'package:toolbox/data/model/docker/ps.dart'; -import 'package:toolbox/data/model/app/error.dart'; -import 'package:toolbox/data/res/logger.dart'; -import 'package:toolbox/data/res/store.dart'; - -final _dockerNotFound = RegExp(r'command not found|Unknown command'); -final _versionReg = RegExp(r'(Version:)\s+([0-9]+\.[0-9]+\.[0-9]+)'); -// eg: `Docker Engine - Community` -final _editionReg = RegExp(r'Docker Engine - [a-zA-Z]+'); -final _dockerPrefixReg = RegExp(r'(sudo )?docker '); - -class DockerProvider extends ChangeNotifier { - SSHClient? client; - String? userName; - List? items; - List? images; - String? version; - String? edition; - DockerErr? error; - String? hostId; - String? runLog; - BuildContext? context; - - void init( - SSHClient client, - String userName, - PwdRequestFunc onPwdReq, - String hostId, - BuildContext context, - ) { - this.client = client; - this.userName = userName; - this.context = context; - this.hostId = hostId; - } - - void clear() { - client = userName = error = items = version = edition = context = null; - hostId = runLog = images = null; - } - - Future refresh() async { - var raw = ''; - await client?.execWithPwd( - _wrap(DockerCmdType.execAll), - context: context, - onStdout: (data, _) => raw = '$raw$data', - ); - - if (raw.contains(_dockerNotFound)) { - error = DockerErr(type: DockerErrType.notInstalled); - notifyListeners(); - return; - } - - // Check result segments count - final segments = raw.split(seperator); - if (segments.length != DockerCmdType.values.length) { - error = DockerErr(type: DockerErrType.segmentsNotMatch); - Loggers.parse.warning('Docker segments: ${segments.length}\n$raw'); - notifyListeners(); - return; - } - - // Parse docker version - final verRaw = DockerCmdType.version.find(segments); - version = _versionReg.firstMatch(verRaw)?.group(2); - edition = _editionReg.firstMatch(verRaw)?.group(0); - - // Parse docker ps - final psRaw = DockerCmdType.ps.find(segments); - try { - final lines = psRaw.split('\n'); - lines.removeWhere((element) => element.isEmpty); - if (lines.isNotEmpty) lines.removeAt(0); - items = lines.map((e) => DockerPsItem.fromRawString(e)).toList(); - } catch (e, trace) { - error = DockerErr( - type: DockerErrType.parsePsItem, - message: '$psRaw\n-\n$e', - ); - Loggers.parse.warning('Docker ps failed', e, trace); - } finally { - notifyListeners(); - } - - // Parse docker images - final imageRaw = DockerCmdType.images.find(segments); - try { - final imageLines = imageRaw.split('\n'); - imageLines.removeWhere((element) => element.isEmpty); - if (imageLines.isNotEmpty) imageLines.removeAt(0); - images = imageLines.map((e) => DockerImage.fromRawStr(e)).toList(); - } catch (e, trace) { - error = DockerErr( - type: DockerErrType.parseImages, - message: '$imageRaw\n-\n$e', - ); - Loggers.parse.warning('Docker images failed', e, trace); - } finally { - notifyListeners(); - } - - // Parse docker stats - // final statsRaw = DockerCmdType.stats.find(segments); - // try { - // final statsLines = statsRaw.split('\n'); - // statsLines.removeWhere((element) => element.isEmpty); - // if (statsLines.isNotEmpty) statsLines.removeAt(0); - // for (var item in items!) { - // final statsLine = statsLines.firstWhere( - // (element) => element.contains(item.containerId), - // orElse: () => '', - // ); - // if (statsLine.isEmpty) continue; - // item.parseStats(statsLine); - // } - // } catch (e, trace) { - // error = DockerErr( - // type: DockerErrType.parseStats, - // message: '$statsRaw\n-\n$e', - // ); - // _logger.warning('Parse docker stats: $statsRaw', e, trace); - // } finally { - // notifyListeners(); - // } - } - - Future stop(String id) async => await run('docker stop $id'); - - Future start(String id) async => await run('docker start $id'); - - Future delete(String id, bool force) async { - if (force) { - return await run('docker rm -f $id'); - } - return await run('docker rm $id'); - } - - Future restart(String id) async => - await run('docker restart $id'); - - Future run(String cmd) async { - if (!cmd.startsWith(_dockerPrefixReg)) { - return DockerErr(type: DockerErrType.cmdNoPrefix); - } - - runLog = ''; - final errs = []; - final code = await client?.execWithPwd( - _wrap(cmd), - context: context, - onStdout: (data, _) { - runLog = '$runLog$data'; - notifyListeners(); - }, - onStderr: (data, _) => errs.add(data), - ); - runLog = null; - notifyListeners(); - - if (code != 0) { - return DockerErr( - type: DockerErrType.unknown, - message: errs.join('\n').trim(), - ); - } - await refresh(); - return null; - } - - /// wrap cmd with `docker host` - String _wrap(String cmd) { - final dockerHost = Stores.docker.fetch(hostId); - cmd = 'export LANG=en_US.UTF-8 && $cmd'; - final noDockerHost = dockerHost?.isEmpty ?? true; - if (!noDockerHost) { - cmd = 'export DOCKER_HOST=$dockerHost && $cmd'; - } - return cmd; - } -} diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index a1de44d9..0638fd86 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -2,9 +2,9 @@ class BuildData { static const String name = "ServerBox"; - static const int build = 707; + static const int build = 709; static const String engine = "3.16.7"; - static const String buildAt = "2024-01-16 12:17:21"; - static const int modifications = 1; + static const String buildAt = "2024-01-19 17:32:15"; + static const int modifications = 2; static const int script = 34; } diff --git a/lib/data/res/provider.dart b/lib/data/res/provider.dart index f12b4631..ab84f30b 100644 --- a/lib/data/res/provider.dart +++ b/lib/data/res/provider.dart @@ -1,6 +1,5 @@ import 'package:toolbox/data/provider/app.dart'; import 'package:toolbox/data/provider/debug.dart'; -import 'package:toolbox/data/provider/docker.dart'; import 'package:toolbox/data/provider/private_key.dart'; import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/sftp.dart'; @@ -10,7 +9,6 @@ import 'package:toolbox/locator.dart'; abstract final class Pros { static final app = locator(); static final debug = locator(); - static final docker = locator(); static final key = locator(); static final server = locator(); static final sftp = locator(); diff --git a/lib/data/res/store.dart b/lib/data/res/store.dart index a6791723..f3db72c4 100644 --- a/lib/data/res/store.dart +++ b/lib/data/res/store.dart @@ -1,5 +1,5 @@ import 'package:toolbox/core/persistant_store.dart'; -import 'package:toolbox/data/store/docker.dart'; +import 'package:toolbox/data/store/container.dart'; import 'package:toolbox/data/store/history.dart'; import 'package:toolbox/data/store/private_key.dart'; import 'package:toolbox/data/store/server.dart'; diff --git a/lib/data/store/container.dart b/lib/data/store/container.dart new file mode 100644 index 00000000..a1724d29 --- /dev/null +++ b/lib/data/store/container.dart @@ -0,0 +1,30 @@ +import 'package:toolbox/data/model/container/type.dart'; + +import '../../core/persistant_store.dart'; + +const _keyConfig = 'providerConfig'; + +class DockerStore extends PersistentStore { + DockerStore() : super('docker'); + + String? fetch(String? id) { + return box.get(id); + } + + void put(String id, String host) { + box.put(id, host); + } + + ContainerType getType([String? id]) { + final cfg = box.get(_keyConfig + (id ?? '')); + if (cfg == null) { + return ContainerType.docker; + } else { + return ContainerType.values.firstWhere((e) => e.toString() == cfg); + } + } + + void setType(String? id, ContainerType type) { + box.put(_keyConfig + (id ?? ''), type.toString()); + } +} diff --git a/lib/data/store/docker.dart b/lib/data/store/docker.dart deleted file mode 100644 index 186800c6..00000000 --- a/lib/data/store/docker.dart +++ /dev/null @@ -1,13 +0,0 @@ -import '../../core/persistant_store.dart'; - -class DockerStore extends PersistentStore { - DockerStore() : super('docker'); - - String? fetch(String? id) { - return box.get(id); - } - - void put(String id, String host) { - box.put(id, host); - } -} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c3f466ec..4b4ea916 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -38,6 +38,7 @@ "collapseUITip": "Ob lange Listen in der Benutzeroberfläche standardmäßig eingeklappt werden sollen oder nicht", "conn": "Verbindung", "connected": "in Verbindung gebracht", + "container": "Container", "containerName": "Container Name", "containerStatus": "Container Status", "convert": "Konvertieren", @@ -220,6 +221,7 @@ "success": "Erfolgreich", "suspend": "Suspend", "suspendTip": "Die Suspend-Funktion erfordert Root-Rechte und systemd-Unterstützung.", + "switchTo": "Wechseln zu {val}", "syncTip": "Damit einige Änderungen wirksam werden, kann ein Neustart erforderlich sein.", "system": "Systeme", "tag": "Tags", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d405c006..3cce4393 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -38,6 +38,7 @@ "collapseUITip": "Whether to collapse long lists present in the UI by default", "conn": "Connection", "connected": "Connected", + "container": "Container", "containerName": "Container name", "containerStatus": "Container status", "convert": "Convert", @@ -220,6 +221,7 @@ "success": "Success", "suspend": "Suspend", "suspendTip": "The suspend function requires root privileges and systemd support.", + "switchTo": "Switch to {val}", "syncTip": "A restart may be required for some changes to take effect.", "system": "System", "tag": "Tags", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b283c369..6d086ac6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -38,6 +38,7 @@ "collapseUITip": "Réduction ou non des longues listes présentes dans l'interface utilisateur par défaut", "conn": "Connexion", "connected": "Connecté", + "container": "Conteneurs", "containerName": "Nom du conteneur", "containerStatus": "Statut du conteneur", "convert": "Convertir", @@ -220,6 +221,7 @@ "success": "Succès", "suspend": "Suspendre", "suspendTip": "La fonction de suspension nécessite des privilèges root et la prise en charge de systemd.", + "switchTo": "Passer à {val}", "syncTip": "Un redémarrage peut être nécessaire pour que certains changements prennent effet.", "system": "Système", "tag": "Étiquettes", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 5eafbf74..b3903580 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -38,6 +38,7 @@ "collapseUITip": "Apakah akan menciutkan daftar panjang yang ada di UI secara default atau tidak", "conn": "Koneksi", "connected": "Terhubung", + "container": "Wadah", "containerName": "Nama kontainer", "containerStatus": "Status wadah", "convert": "Mengubah", @@ -220,6 +221,7 @@ "success": "Kesuksesan", "suspend": "Suspend", "suspendTip": "Fungsi penangguhan memerlukan hak akses root dan dukungan systemd.", + "switchTo": "Beralih ke {val}", "syncTip": "Pengaktifan ulang mungkin diperlukan agar beberapa perubahan dapat diterapkan.", "system": "Sistem", "tag": "Tag", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 2af2911d..70da75d1 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -38,6 +38,7 @@ "collapseUITip": "是否默认折叠UI中存在的长列表", "conn": "连接", "connected": "已连接", + "container": "容器", "containerName": "容器名", "containerStatus": "容器状态", "convert": "转换", @@ -220,6 +221,7 @@ "success": "成功", "suspend": "挂起", "suspendTip": "suspend 功能需要 root 权限及 systemd 支持。", + "switchTo": "切换到 {val}", "syncTip": "可能需要重新启动,某些更改才能生效。", "system": "系统", "tag": "标签", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index a5b46320..16dbd23b 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -38,6 +38,7 @@ "collapseUITip": "是否預設折疊UI中存在的長列表", "conn": "連接", "connected": "已連接", + "container": "容器", "containerName": "容器名稱", "containerStatus": "容器狀態", "convert": "轉換", @@ -220,6 +221,7 @@ "success": "成功", "suspend": "挂起", "suspendTip": "suspend 功能需要 root 權限及 systemd 支持。", + "switchTo": "切換到 {val}", "syncTip": "可能需要重新啟動,某些更改才能生效。", "system": "系統", "tag": "标签", diff --git a/lib/locator.dart b/lib/locator.dart index 652a3397..9684dcff 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -2,14 +2,14 @@ import 'package:get_it/get_it.dart'; import 'data/provider/app.dart'; import 'data/provider/debug.dart'; -import 'data/provider/docker.dart'; +import 'data/provider/container.dart'; import 'data/provider/private_key.dart'; import 'data/provider/server.dart'; import 'data/provider/sftp.dart'; import 'data/provider/snippet.dart'; import 'data/provider/virtual_keyboard.dart'; import 'data/service/app.dart'; -import 'data/store/docker.dart'; +import 'data/store/container.dart'; import 'data/store/history.dart'; import 'data/store/private_key.dart'; import 'data/store/server.dart'; @@ -25,7 +25,7 @@ void _setupLocatorForServices() { void _setupLocatorForProviders() { locator.registerSingleton(AppProvider()); locator.registerSingleton(DebugProvider()); - locator.registerSingleton(DockerProvider()); + locator.registerSingleton(ContainerProvider()); locator.registerSingleton(ServerProvider()); locator.registerSingleton(VirtKeyProvider()); locator.registerSingleton(SnippetProvider()); diff --git a/lib/main.dart b/lib/main.dart index 7d3c6cb9..26f4e772 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,6 @@ import 'data/model/server/snippet.dart'; import 'data/model/ssh/virtual_key.dart'; import 'data/provider/app.dart'; import 'data/provider/debug.dart'; -import 'data/provider/docker.dart'; import 'data/provider/private_key.dart'; import 'data/provider/server.dart'; import 'data/provider/sftp.dart'; @@ -43,7 +42,6 @@ Future main() async { providers: [ ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), - ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), ChangeNotifierProvider(create: (_) => locator()), diff --git a/lib/view/page/docker.dart b/lib/view/page/container.dart similarity index 50% rename from lib/view/page/docker.dart rename to lib/view/page/container.dart index b78f68ee..75e3940f 100644 --- a/lib/view/page/docker.dart +++ b/lib/view/page/container.dart @@ -4,85 +4,79 @@ import 'package:toolbox/core/extension/context/common.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/snackbar.dart'; +import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/route.dart'; -import 'package:toolbox/data/model/docker/image.dart'; -import 'package:toolbox/data/res/provider.dart'; +import 'package:toolbox/data/model/container/image.dart'; +import 'package:toolbox/data/model/container/type.dart'; import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/view/widget/expand_tile.dart'; import 'package:toolbox/view/widget/input_field.dart'; -import '../../data/model/docker/ps.dart'; +import '../../data/model/container/ps.dart'; import '../../data/model/server/server_private_info.dart'; -import '../../data/provider/docker.dart'; -import '../../data/model/app/error.dart'; +import '../../data/provider/container.dart'; import '../../data/model/app/menu.dart'; import '../../data/res/ui.dart'; -import '../../data/res/url.dart'; import '../widget/appbar.dart'; import '../widget/popup_menu.dart'; import '../widget/cardx.dart'; import '../widget/two_line_text.dart'; -import '../widget/url_text.dart'; -class DockerManagePage extends StatefulWidget { +class ContainerPage extends StatefulWidget { final ServerPrivateInfo spi; - const DockerManagePage({required this.spi, super.key}); + const ContainerPage({required this.spi, super.key}); @override - State createState() => _DockerManagePageState(); + State createState() => _ContainerPageState(); } -class _DockerManagePageState extends State { +class _ContainerPageState extends State { final _textController = TextEditingController(); - final _docker = Pros.docker; + late final _container = ContainerProvider( + client: widget.spi.server?.client, + userName: widget.spi.user, + hostId: widget.spi.id, + context: context, + ); @override void dispose() { super.dispose(); - _docker.clear(); _textController.dispose(); } @override void initState() { super.initState(); - final client = widget.spi.server?.client; - if (client == null) { - return; - } - _docker - ..init( - client, - widget.spi.user, - (user) async => await context.showPwdDialog(user), - widget.spi.id, - context, - ) - ..refresh(); } @override Widget build(BuildContext context) { - return Consumer(builder: (_, ___, __) { - return Scaffold( - appBar: CustomAppBar( - centerTitle: true, - title: TwoLineText(up: 'Docker', down: widget.spi.name), - actions: [ - IconButton( - onPressed: () async { - context.showLoadingDialog(); - await _docker.refresh(); - context.pop(); - }, - icon: const Icon(Icons.refresh), - ) - ], - ), - body: _buildMain(), - floatingActionButton: _docker.error == null ? _buildFAB() : null, - ); - }); + return ChangeNotifierProvider( + create: (_) => _container, + builder: (_, __) => Consumer( + builder: (_, ___, __) { + return Scaffold( + appBar: CustomAppBar( + centerTitle: true, + title: TwoLineText(up: 'Container', down: widget.spi.name), + actions: [ + IconButton( + onPressed: () async { + context.showLoadingDialog(); + await _container.refresh(); + context.pop(); + }, + icon: const Icon(Icons.refresh), + ) + ], + ), + body: _buildMain(), + floatingActionButton: _container.error == null ? _buildFAB() : null, + ); + }, + ), + ); } Widget _buildFAB() { @@ -92,6 +86,196 @@ class _DockerManagePageState extends State { ); } + Widget _buildMain() { + if (_container.error != null && _container.items == null) { + return SizedBox.expand( + child: Column( + children: [ + const Spacer(), + const Icon( + Icons.error, + size: 37, + ), + UIs.height13, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 23), + child: Text(_container.error?.toString() ?? l10n.unknownError), + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildEditHost(), + _buildSwitchProvider(), + ], + ), + UIs.height13, + ], + ), + ); + } + if (_container.items == null || _container.images == null) { + return UIs.centerLoading; + } + + final items = [ + _buildLoading(), + _buildVersion(), + _buildPs(), + _buildImage(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildEditHost(), + _buildSwitchProvider(), + ], + ), + ].map((e) => CardX(child: e)); + return ListView( + padding: const EdgeInsets.all(7), + children: items.toList(), + ); + } + + Widget _buildImage() { + return ExpandTile( + title: Text(l10n.imagesList), + subtitle: Text( + l10n.dockerImagesFmt(_container.images!.length), + style: UIs.textGrey, + ), + initiallyExpanded: (_container.images?.length ?? 0) <= 3, + children: _container.images?.map(_buildImageItem).toList() ?? [], + ); + } + + Widget _buildImageItem(ContainerImg e) { + return ListTile( + title: Text(e.repository ?? l10n.unknown), + subtitle: Text('${e.tag} - ${e.sizeMB}', style: UIs.textGrey), + trailing: IconButton( + padding: EdgeInsets.zero, + alignment: Alignment.centerRight, + icon: const Icon(Icons.delete), + onPressed: () => _showImageRmDialog(e), + ), + ); + } + + Widget _buildLoading() { + if (_container.runLog == null) return UIs.placeholder; + return Padding( + padding: const EdgeInsets.all(17), + child: Column( + children: [ + const Center( + child: CircularProgressIndicator(), + ), + UIs.height13, + Text(_container.runLog ?? '...'), + ], + ), + ); + } + + Widget _buildVersion() { + return Padding( + padding: const EdgeInsets.all(17), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(_container.type.name.upperFirst), + Text(_container.version ?? l10n.unknown), + ], + ), + ); + } + + Widget _buildPs() { + final items = _container.items; + if (items == null) return UIs.placeholder; + return ExpandTile( + title: Text(l10n.containerStatus), + subtitle: Text( + _buildPsCardSubtitle(items), + style: UIs.textGrey, + ), + initiallyExpanded: items.length <= 7, + children: items.map(_buildPsItem).toList(), + ); + } + + Widget _buildPsItem(ContainerPs item) { + return ListTile( + title: Text(item.name ?? l10n.unknown), + subtitle: Text( + item.image ?? l10n.unknown, + style: UIs.text13Grey, + ), + trailing: _buildMoreBtn(item), + ); + } + + Widget _buildMoreBtn(ContainerPs dItem) { + return PopupMenu( + items: ContainerMenu.items(dItem.running).map((e) => e.widget).toList(), + onSelected: (item) => _onTapMoreBtn(item, dItem), + ); + } + + String _buildPsCardSubtitle(List running) { + final runningCount = running.where((element) => element.running).length; + final stoped = running.length - runningCount; + if (stoped == 0) { + return l10n.dockerStatusRunningFmt(runningCount); + } + return l10n.dockerStatusRunningAndStoppedFmt(runningCount, stoped); + } + + Widget _buildEditHost() { + final children = []; + final emptyImgs = _container.images?.isEmpty ?? false; + final emptyPs = _container.items?.isEmpty ?? false; + if (emptyPs && emptyImgs) { + children.add(Padding( + padding: const EdgeInsets.fromLTRB(17, 17, 17, 0), + child: Text( + l10n.dockerEmptyRunningItems, + textAlign: TextAlign.center, + ), + )); + } + children.add( + TextButton( + onPressed: _showEditHostDialog, + child: Text(l10n.dockerEditHost), + ), + ); + return Column( + children: children, + ); + } + + Widget _buildSwitchProvider() { + late final Widget child; + if (_container.type == ContainerType.podman) { + child = TextButton( + onPressed: () { + _container.setType(ContainerType.docker); + }, + child: Text(l10n.switchTo('Docker')), + ); + } else { + child = TextButton( + onPressed: () { + _container.setType(ContainerType.podman); + }, + child: Text(l10n.switchTo('Podman')), + ); + } + return child; + } + Future _showAddFAB() async { final imageCtrl = TextEditingController(); final nameCtrl = TextEditingController(); @@ -157,7 +341,7 @@ class _DockerManagePageState extends State { onPressed: () async { context.pop(); context.showLoadingDialog(); - final result = await _docker.run(cmd); + final result = await _container.run(cmd); context.pop(); if (result != null) { context.showSnackBar(result.message ?? l10n.unknownError); @@ -177,336 +361,9 @@ class _DockerManagePageState extends State { suffix = '$args $image'; } if (name.isEmpty) { - return 'docker run -itd $suffix'; + return 'run -itd $suffix'; } - return 'docker run -itd --name $name $suffix'; - } - - Widget _buildMain() { - if (_docker.error != null && _docker.items == null) { - return SizedBox.expand( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.error, - size: 37, - ), - const SizedBox(height: 27), - Text(_docker.error?.message ?? l10n.unknownError), - const SizedBox(height: 27), - Padding( - padding: const EdgeInsets.all(17), - child: _buildSolution(_docker.error!), - ), - _buildEditHost(), - ], - ), - ); - } - if (_docker.items == null || _docker.images == null) { - return UIs.centerLoading; - } - - final items = [ - _buildLoading(), - _buildVersion(), - _buildPs(), - _buildImage(), - _buildEditHost(), - ].map((e) => CardX(child: e)); - return ListView( - padding: const EdgeInsets.all(7), - children: items.toList(), - ); - } - - Widget _buildImage() { - return ExpandTile( - title: Text(l10n.imagesList), - subtitle: Text( - l10n.dockerImagesFmt(_docker.images!.length), - style: UIs.textGrey, - ), - initiallyExpanded: (_docker.images?.length ?? 0) <= 3, - children: _docker.images?.map(_buildImageItem).toList() ?? [], - ); - } - - Widget _buildImageItem(DockerImage e) { - return ListTile( - title: Text(e.repo), - subtitle: Text('${e.tag} - ${e.size}', style: UIs.textGrey), - trailing: IconButton( - padding: EdgeInsets.zero, - alignment: Alignment.centerRight, - icon: const Icon(Icons.delete), - onPressed: () => _showImageRmDialog(e), - ), - ); - } - - void _showImageRmDialog(DockerImage e) { - context.showRoundDialog( - title: Text(l10n.attention), - child: Text(l10n.askContinue('${l10n.delete} Image(${e.repo})')), - actions: [ - TextButton( - onPressed: () => context.pop(), - child: Text(l10n.cancel), - ), - TextButton( - onPressed: () async { - context.pop(); - final result = await Pros.docker.run( - 'docker rmi ${e.id} -f', - ); - if (result != null) { - context.showSnackBar(result.message ?? l10n.unknownError); - } - }, - child: Text(l10n.ok, style: UIs.textRed), - ), - ], - ); - } - - Widget _buildLoading() { - if (Pros.docker.runLog == null) return UIs.placeholder; - return Padding( - padding: const EdgeInsets.all(17), - child: Column( - children: [ - const Center( - child: CircularProgressIndicator(), - ), - UIs.height13, - Text(_docker.runLog ?? '...'), - ], - ), - ); - } - - Widget _buildSolution(DockerErr err) { - switch (err.type) { - case DockerErrType.notInstalled: - return UrlText( - text: l10n.installDockerWithUrl, - replace: l10n.install, - ); - case DockerErrType.noClient: - return Text(l10n.waitConnection); - case DockerErrType.invalidVersion: - return UrlText( - text: l10n.invalidVersionHelp(Urls.appHelp), - replace: 'Github', - ); - case DockerErrType.parseImages: - return const Text('Parse images error'); - case DockerErrType.parsePsItem: - return const Text('Parse ps item error'); - case DockerErrType.parseStats: - return const Text('Parse stats error'); - case DockerErrType.unknown: - return const Text('Unknown error'); - case DockerErrType.cmdNoPrefix: - return const Text('Cmd no prefix'); - case DockerErrType.segmentsNotMatch: - return const Text('Segments not match'); - } - } - - Widget _buildVersion() { - return Padding( - padding: const EdgeInsets.all(17), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(_docker.edition ?? l10n.unknown), - Text(_docker.version ?? l10n.unknown), - ], - ), - ); - } - - Widget _buildPs() { - final items = Pros.docker.items; - if (items == null) return UIs.placeholder; - return ExpandTile( - title: Text(l10n.containerStatus), - subtitle: Text( - _buildPsCardSubtitle(items), - style: UIs.textGrey, - ), - initiallyExpanded: items.length <= 7, - children: items.map(_buildPsItem).toList(), - ); - } - - Widget _buildPsItem(DockerPsItem item) { - return ListTile( - title: Text(item.image), - subtitle: Text( - '${item.name} - ${item.status}', - style: UIs.text13Grey, - ), - trailing: _buildMoreBtn(item), - ); - } - - Widget _buildMoreBtn(DockerPsItem dItem) { - return PopupMenu( - items: DockerMenuType.items(dItem.running).map((e) => e.widget).toList(), - onSelected: (item) async { - switch (item) { - case DockerMenuType.rm: - var force = false; - context.showRoundDialog( - title: Text(l10n.attention), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.askContinue( - '${l10n.delete} Container(${dItem.name})', - )), - UIs.height13, - Row( - children: [ - StatefulBuilder(builder: (_, setState) { - return Checkbox( - value: force, - onChanged: (val) => setState( - () => force = val ?? false, - ), - ); - }), - Text(l10n.force), - ], - ) - ], - ), - actions: [ - TextButton( - onPressed: () async { - context.pop(); - context.showLoadingDialog(); - final result = await _docker.delete( - dItem.containerId, - force, - ); - context.pop(); - if (result != null) { - context.showRoundDialog( - title: Text(l10n.error), - child: Text(result.message ?? l10n.unknownError), - ); - } - }, - child: Text(l10n.ok), - ) - ], - ); - break; - case DockerMenuType.start: - context.showLoadingDialog(); - final result = await _docker.start(dItem.containerId); - context.pop(); - if (result != null) { - context.showRoundDialog( - title: Text(l10n.error), - child: Text(result.message ?? l10n.unknownError), - ); - } - break; - case DockerMenuType.stop: - context.showLoadingDialog(); - final result = await _docker.stop(dItem.containerId); - context.pop(); - if (result != null) { - context.showRoundDialog( - title: Text(l10n.error), - child: Text(result.message ?? l10n.unknownError), - ); - } - break; - case DockerMenuType.restart: - context.showLoadingDialog(); - final result = await _docker.restart(dItem.containerId); - context.pop(); - if (result != null) { - context.showRoundDialog( - title: Text(l10n.error), - child: Text(result.message ?? l10n.unknownError), - ); - } - break; - case DockerMenuType.logs: - AppRoute.ssh( - spi: widget.spi, - initCmd: 'docker logs -f --tail 100 ${dItem.containerId}', - ).go(context); - break; - case DockerMenuType.terminal: - AppRoute.ssh( - spi: widget.spi, - initCmd: 'docker exec -it ${dItem.containerId} sh', - ).go(context); - break; - // case DockerMenuType.stats: - // showRoundDialog( - // context: context, - // title: Text(l10n.stats), - // child: Text( - // 'CPU: ${dItem.cpu}\n' - // 'Mem: ${dItem.mem}\n' - // 'Net: ${dItem.net}\n' - // 'Block: ${dItem.disk}', - // ), - // actions: [ - // TextButton( - // onPressed: () => context.pop(), - // child: Text(l10n.ok), - // ), - // ], - // ); - // break; - } - }, - ); - } - - String _buildPsCardSubtitle(List running) { - final runningCount = running.where((element) => element.running).length; - final stoped = running.length - runningCount; - if (stoped == 0) { - return l10n.dockerStatusRunningFmt(runningCount); - } - return l10n.dockerStatusRunningAndStoppedFmt(runningCount, stoped); - } - - Widget _buildEditHost() { - final children = []; - final emptyImgs = _docker.images?.isEmpty ?? false; - final emptyPs = _docker.items?.isEmpty ?? false; - if (emptyPs && emptyImgs) { - children.add(Padding( - padding: const EdgeInsets.fromLTRB(17, 17, 17, 0), - child: Text( - l10n.dockerEmptyRunningItems, - textAlign: TextAlign.center, - ), - )); - } - children.add( - TextButton( - onPressed: _showEditHostDialog, - child: Text(l10n.dockerEditHost), - ), - ); - return Column( - children: children, - ); + return 'run -itd --name $name $suffix'; } Future _showEditHostDialog() async { @@ -533,6 +390,147 @@ class _DockerManagePageState extends State { void _onSaveDockerHost(String val) { context.pop(); Stores.docker.put(widget.spi.id, val.trim()); - _docker.refresh(); + _container.refresh(); + } + + void _showImageRmDialog(ContainerImg e) { + context.showRoundDialog( + title: Text(l10n.attention), + child: Text(l10n.askContinue('${l10n.delete} Image(${e.repository})')), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () async { + context.pop(); + final result = await _container.run('rmi ${e.id} -f'); + if (result != null) { + context.showSnackBar(result.message ?? l10n.unknownError); + } + }, + child: Text(l10n.ok, style: UIs.textRed), + ), + ], + ); + } + + void _onTapMoreBtn(ContainerMenu item, ContainerPs dItem) async { + final id = dItem.id; + if (id == null) { + context.showSnackBar('Id is null'); + return; + } + switch (item) { + case ContainerMenu.rm: + var force = false; + context.showRoundDialog( + title: Text(l10n.attention), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.askContinue( + '${l10n.delete} Container(${dItem.name})', + )), + UIs.height13, + Row( + children: [ + StatefulBuilder(builder: (_, setState) { + return Checkbox( + value: force, + onChanged: (val) => setState( + () => force = val ?? false, + ), + ); + }), + Text(l10n.force), + ], + ) + ], + ), + actions: [ + TextButton( + onPressed: () async { + context.pop(); + context.showLoadingDialog(); + final result = await _container.delete(id, force); + context.pop(); + if (result != null) { + context.showRoundDialog( + title: Text(l10n.error), + child: Text(result.message ?? l10n.unknownError), + ); + } + }, + child: Text(l10n.ok), + ) + ], + ); + break; + case ContainerMenu.start: + context.showLoadingDialog(); + final result = await _container.start(id); + context.pop(); + if (result != null) { + context.showRoundDialog( + title: Text(l10n.error), + child: Text(result.message ?? l10n.unknownError), + ); + } + break; + case ContainerMenu.stop: + context.showLoadingDialog(); + final result = await _container.stop(id); + context.pop(); + if (result != null) { + context.showRoundDialog( + title: Text(l10n.error), + child: Text(result.message ?? l10n.unknownError), + ); + } + break; + case ContainerMenu.restart: + context.showLoadingDialog(); + final result = await _container.restart(id); + context.pop(); + if (result != null) { + context.showRoundDialog( + title: Text(l10n.error), + child: Text(result.message ?? l10n.unknownError), + ); + } + break; + case ContainerMenu.logs: + AppRoute.ssh( + spi: widget.spi, + initCmd: 'docker logs -f --tail 100 ${dItem.id}', + ).go(context); + break; + case ContainerMenu.terminal: + AppRoute.ssh( + spi: widget.spi, + initCmd: 'docker exec -it ${dItem.id} sh', + ).go(context); + break; + // case DockerMenuType.stats: + // showRoundDialog( + // context: context, + // title: Text(l10n.stats), + // child: Text( + // 'CPU: ${dItem.cpu}\n' + // 'Mem: ${dItem.mem}\n' + // 'Net: ${dItem.net}\n' + // 'Block: ${dItem.disk}', + // ), + // actions: [ + // TextButton( + // onPressed: () => context.pop(), + // child: Text(l10n.ok), + // ), + // ], + // ); + // break; + } } } diff --git a/lib/view/page/setting/srv_detail_seq.dart b/lib/view/page/setting/srv_detail_seq.dart index a92587e2..1f214a16 100644 --- a/lib/view/page/setting/srv_detail_seq.dart +++ b/lib/view/page/setting/srv_detail_seq.dart @@ -33,7 +33,8 @@ class _ServerDetailOrderPageState extends State { for (final key in keys_) { keys.add(key); } - final disabled = Defaults.detailCardOrder.where((e) => !keys.contains(e)).toList(); + final disabled = + Defaults.detailCardOrder.where((e) => !keys.contains(e)).toList(); final allKeys = [...keys, ...disabled]; return ReorderableListView.builder( padding: const EdgeInsets.all(7), diff --git a/lib/view/widget/server_func_btns.dart b/lib/view/widget/server_func_btns.dart index 468fa7bf..d5487be9 100644 --- a/lib/view/widget/server_func_btns.dart +++ b/lib/view/widget/server_func_btns.dart @@ -32,9 +32,9 @@ class ServerFuncBtnsTopRight extends StatelessWidget { @override Widget build(BuildContext context) { - return PopupMenu( - items: ServerTabMenuType.values - .map((e) => PopupMenuItem( + return PopupMenu( + items: ServerTabMenu.values + .map((e) => PopupMenuItem( value: e, child: Row( children: [ @@ -93,7 +93,7 @@ class ServerFuncBtns extends StatelessWidget { // ); return Row( mainAxisAlignment: MainAxisAlignment.spaceAround, - children: ServerTabMenuType.values + children: ServerTabMenu.values .map( (e) => IconButton( onPressed: () => _onTapMoreBtns(e, spi, context), @@ -108,15 +108,15 @@ class ServerFuncBtns extends StatelessWidget { } void _onTapMoreBtns( - ServerTabMenuType value, + ServerTabMenu value, ServerPrivateInfo spi, BuildContext context, ) async { switch (value) { - case ServerTabMenuType.pkg: + case ServerTabMenu.pkg: _onPkg(context, spi); break; - case ServerTabMenuType.sftp: + case ServerTabMenu.sftp: AppRoute.sftp(spi: spi).checkGo( context: context, check: () => _checkClient(context, spi.id), @@ -145,19 +145,19 @@ void _onTapMoreBtns( // ); // } // break; - case ServerTabMenuType.docker: + case ServerTabMenu.container: AppRoute.docker(spi: spi).checkGo( context: context, check: () => _checkClient(context, spi.id), ); break; - case ServerTabMenuType.process: + case ServerTabMenu.process: AppRoute.process(spi: spi).checkGo( context: context, check: () => _checkClient(context, spi.id), ); break; - case ServerTabMenuType.terminal: + case ServerTabMenu.terminal: _gotoSSH(spi, context); break; }