mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
fix: container not working (#787)
This commit is contained in:
@@ -222,6 +222,23 @@ class ContainerProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<ContainerErr?> restart(String id) async => await run('restart $id');
|
Future<ContainerErr?> restart(String id) async => await run('restart $id');
|
||||||
|
|
||||||
|
Future<ContainerErr?> pruneImages({bool all = true}) async {
|
||||||
|
final cmd = 'image prune${all ? " -a" : ""} -f';
|
||||||
|
return await run(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ContainerErr?> pruneContainers() async {
|
||||||
|
return await run('container prune -f');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ContainerErr?> pruneVolumes() async {
|
||||||
|
return await run('volume prune -f');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ContainerErr?> pruneSystem() async {
|
||||||
|
return await run('system prune -a -f --volumes');
|
||||||
|
}
|
||||||
|
|
||||||
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
|
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
|
||||||
cmd = switch (type) {
|
cmd = switch (type) {
|
||||||
ContainerType.docker => 'docker $cmd',
|
ContainerType.docker => 'docker $cmd',
|
||||||
@@ -272,6 +289,8 @@ enum ContainerCmdType {
|
|||||||
ps,
|
ps,
|
||||||
stats,
|
stats,
|
||||||
images,
|
images,
|
||||||
|
// No specific commands needed for prune actions as they are simple
|
||||||
|
// and don't require splitting output with ShellFunc.seperator
|
||||||
;
|
;
|
||||||
|
|
||||||
String exec(
|
String exec(
|
||||||
|
|||||||
@@ -176,8 +176,8 @@ class SettingStore extends HiveStore {
|
|||||||
late final containerParseStat = propertyDefault('containerParseStat', true);
|
late final containerParseStat = propertyDefault('containerParseStat', true);
|
||||||
|
|
||||||
/// Auto refresh container status
|
/// Auto refresh container status
|
||||||
late final contaienrAutoRefresh = propertyDefault(
|
late final containerAutoRefresh = propertyDefault(
|
||||||
'contaienrAutoRefresh',
|
'containerAutoRefresh',
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -914,6 +914,12 @@ abstract class AppLocalizations {
|
|||||||
/// **'Process'**
|
/// **'Process'**
|
||||||
String get process;
|
String get process;
|
||||||
|
|
||||||
|
/// No description provided for @prune.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Prune'**
|
||||||
|
String get prune;
|
||||||
|
|
||||||
/// No description provided for @pushToken.
|
/// No description provided for @pushToken.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -452,6 +452,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Prozess';
|
String get process => 'Prozess';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Beschneiden';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Push Token';
|
String get pushToken => 'Push Token';
|
||||||
|
|
||||||
|
|||||||
@@ -450,6 +450,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Process';
|
String get process => 'Process';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Prune';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Push token';
|
String get pushToken => 'Push token';
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Proceso';
|
String get process => 'Proceso';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Podar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Token de notificaciones';
|
String get pushToken => 'Token de notificaciones';
|
||||||
|
|
||||||
|
|||||||
@@ -455,6 +455,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Processus';
|
String get process => 'Processus';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Élaguer';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Jeton d\'identification';
|
String get pushToken => 'Jeton d\'identification';
|
||||||
|
|
||||||
|
|||||||
@@ -450,6 +450,9 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Proses';
|
String get process => 'Proses';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Pangkas';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Dorong token';
|
String get pushToken => 'Dorong token';
|
||||||
|
|
||||||
|
|||||||
@@ -436,6 +436,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'プロセス';
|
String get process => 'プロセス';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => '剪定する';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'プッシュトークン';
|
String get pushToken => 'プッシュトークン';
|
||||||
|
|
||||||
|
|||||||
@@ -451,6 +451,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Proces';
|
String get process => 'Proces';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Snoeien';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Push-token';
|
String get pushToken => 'Push-token';
|
||||||
|
|
||||||
|
|||||||
@@ -451,6 +451,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Processo';
|
String get process => 'Processo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Podar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Token de notificação push';
|
String get pushToken => 'Token de notificação push';
|
||||||
|
|
||||||
|
|||||||
@@ -452,6 +452,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Процесс';
|
String get process => 'Процесс';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Обрезать';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Токен уведомлений';
|
String get pushToken => 'Токен уведомлений';
|
||||||
|
|
||||||
|
|||||||
@@ -449,6 +449,9 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'İşlem';
|
String get process => 'İşlem';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Budamak';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Push belirteci';
|
String get pushToken => 'Push belirteci';
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => 'Процес';
|
String get process => 'Процес';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => 'Обрізати';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => 'Надіслати токен';
|
String get pushToken => 'Надіслати токен';
|
||||||
|
|
||||||
|
|||||||
@@ -431,6 +431,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get process => '进程';
|
String get process => '进程';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => '修剪';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => '消息推送 Token';
|
String get pushToken => '消息推送 Token';
|
||||||
|
|
||||||
@@ -1162,6 +1165,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
|||||||
@override
|
@override
|
||||||
String get process => '行程';
|
String get process => '行程';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get prune => '修剪';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pushToken => '消息推送 Token';
|
String get pushToken => '消息推送 Token';
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"privateKey": "Private Key",
|
"privateKey": "Private Key",
|
||||||
"process": "Prozess",
|
"process": "Prozess",
|
||||||
|
"prune": "Beschneiden",
|
||||||
"pushToken": "Push Token",
|
"pushToken": "Push Token",
|
||||||
"pveIgnoreCertTip": "Nicht empfohlen, Achten Sie auf Sicherheitsrisiken! Wenn Sie das Standardzertifikat von PVE verwenden, müssen Sie diese Option aktivieren.",
|
"pveIgnoreCertTip": "Nicht empfohlen, Achten Sie auf Sicherheitsrisiken! Wenn Sie das Standardzertifikat von PVE verwenden, müssen Sie diese Option aktivieren.",
|
||||||
"pveLoginFailed": "Anmeldung fehlgeschlagen. Kann nicht mit Benutzername/Passwort aus der Serverkonfiguration angemeldet werden, um sich über Linux PAM anzumelden.",
|
"pveLoginFailed": "Anmeldung fehlgeschlagen. Kann nicht mit Benutzername/Passwort aus der Serverkonfiguration angemeldet werden, um sich über Linux PAM anzumelden.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"privateKey": "Private Key",
|
"privateKey": "Private Key",
|
||||||
"process": "Process",
|
"process": "Process",
|
||||||
|
"prune": "Prune",
|
||||||
"pushToken": "Push token",
|
"pushToken": "Push token",
|
||||||
"pveIgnoreCertTip": "Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.",
|
"pveIgnoreCertTip": "Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.",
|
||||||
"pveLoginFailed": "Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.",
|
"pveLoginFailed": "Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Vista previa",
|
"preview": "Vista previa",
|
||||||
"privateKey": "Llave privada",
|
"privateKey": "Llave privada",
|
||||||
"process": "Proceso",
|
"process": "Proceso",
|
||||||
|
"prune": "Podar",
|
||||||
"pushToken": "Token de notificaciones",
|
"pushToken": "Token de notificaciones",
|
||||||
"pveIgnoreCertTip": "No se recomienda activarlo, ¡tenga cuidado con los riesgos de seguridad! Si está utilizando el certificado predeterminado de PVE, debe habilitar esta opción.",
|
"pveIgnoreCertTip": "No se recomienda activarlo, ¡tenga cuidado con los riesgos de seguridad! Si está utilizando el certificado predeterminado de PVE, debe habilitar esta opción.",
|
||||||
"pveLoginFailed": "Fallo al iniciar sesión. No se puede autenticar con el nombre de usuario/contraseña de la configuración del servidor para el inicio de sesión de Linux PAM.",
|
"pveLoginFailed": "Fallo al iniciar sesión. No se puede autenticar con el nombre de usuario/contraseña de la configuración del servidor para el inicio de sesión de Linux PAM.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Aperçu",
|
"preview": "Aperçu",
|
||||||
"privateKey": "Clé privée",
|
"privateKey": "Clé privée",
|
||||||
"process": "Processus",
|
"process": "Processus",
|
||||||
|
"prune": "Élaguer",
|
||||||
"pushToken": "Jeton d'identification",
|
"pushToken": "Jeton d'identification",
|
||||||
"pveIgnoreCertTip": "Il n'est pas recommandé de l'activer, attention aux risques de sécurité ! Si vous utilisez le certificat par défaut de PVE, vous devez activer cette option.",
|
"pveIgnoreCertTip": "Il n'est pas recommandé de l'activer, attention aux risques de sécurité ! Si vous utilisez le certificat par défaut de PVE, vous devez activer cette option.",
|
||||||
"pveLoginFailed": "Échec de la connexion. Impossible d'authentifier avec le nom d'utilisateur / mot de passe de la configuration du serveur pour la connexion Linux PAM.",
|
"pveLoginFailed": "Échec de la connexion. Impossible d'authentifier avec le nom d'utilisateur / mot de passe de la configuration du serveur pour la connexion Linux PAM.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Pratinjau",
|
"preview": "Pratinjau",
|
||||||
"privateKey": "Kunci Pribadi",
|
"privateKey": "Kunci Pribadi",
|
||||||
"process": "Proses",
|
"process": "Proses",
|
||||||
|
"prune": "Pangkas",
|
||||||
"pushToken": "Dorong token",
|
"pushToken": "Dorong token",
|
||||||
"pveIgnoreCertTip": "Tidak disarankan untuk diaktifkan, waspadai risiko keamanan! Jika Anda menggunakan sertifikat default dari PVE, Anda perlu mengaktifkan opsi ini.",
|
"pveIgnoreCertTip": "Tidak disarankan untuk diaktifkan, waspadai risiko keamanan! Jika Anda menggunakan sertifikat default dari PVE, Anda perlu mengaktifkan opsi ini.",
|
||||||
"pveLoginFailed": "Login gagal. Tidak dapat mengautentikasi dengan nama pengguna/kata sandi dari konfigurasi server untuk login Linux PAM.",
|
"pveLoginFailed": "Login gagal. Tidak dapat mengautentikasi dengan nama pengguna/kata sandi dari konfigurasi server untuk login Linux PAM.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "プレビュー",
|
"preview": "プレビュー",
|
||||||
"privateKey": "秘密鍵",
|
"privateKey": "秘密鍵",
|
||||||
"process": "プロセス",
|
"process": "プロセス",
|
||||||
|
"prune": "剪定する",
|
||||||
"pushToken": "プッシュトークン",
|
"pushToken": "プッシュトークン",
|
||||||
"pveIgnoreCertTip": "オプションを有効にすることは推奨されません、セキュリティリスクに注意してください!PVEのデフォルト証明書を使用している場合は、このオプションを有効にする必要があります。",
|
"pveIgnoreCertTip": "オプションを有効にすることは推奨されません、セキュリティリスクに注意してください!PVEのデフォルト証明書を使用している場合は、このオプションを有効にする必要があります。",
|
||||||
"pveLoginFailed": "ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。",
|
"pveLoginFailed": "ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Voorbeeld",
|
"preview": "Voorbeeld",
|
||||||
"privateKey": "Privésleutel",
|
"privateKey": "Privésleutel",
|
||||||
"process": "Proces",
|
"process": "Proces",
|
||||||
|
"prune": "Snoeien",
|
||||||
"pushToken": "Push-token",
|
"pushToken": "Push-token",
|
||||||
"pveIgnoreCertTip": "Niet aanbevolen om in te schakelen, let op beveiligingsrisico's! Als u de standaardcertificaat van PVE gebruikt, moet u deze optie inschakelen.",
|
"pveIgnoreCertTip": "Niet aanbevolen om in te schakelen, let op beveiligingsrisico's! Als u de standaardcertificaat van PVE gebruikt, moet u deze optie inschakelen.",
|
||||||
"pveLoginFailed": "Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.",
|
"pveLoginFailed": "Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Pré-visualização",
|
"preview": "Pré-visualização",
|
||||||
"privateKey": "Chave privada",
|
"privateKey": "Chave privada",
|
||||||
"process": "Processo",
|
"process": "Processo",
|
||||||
|
"prune": "Podar",
|
||||||
"pushToken": "Token de notificação push",
|
"pushToken": "Token de notificação push",
|
||||||
"pveIgnoreCertTip": "Não recomendado para ativar, cuidado com os riscos de segurança! Se estiver usando o certificado padrão do PVE, você precisa habilitar esta opção.",
|
"pveIgnoreCertTip": "Não recomendado para ativar, cuidado com os riscos de segurança! Se estiver usando o certificado padrão do PVE, você precisa habilitar esta opção.",
|
||||||
"pveLoginFailed": "Falha no login. Não é possível autenticar com o nome de usuário/senha da configuração do servidor para login no Linux PAM.",
|
"pveLoginFailed": "Falha no login. Não é possível autenticar com o nome de usuário/senha da configuração do servidor para login no Linux PAM.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Предпросмотр",
|
"preview": "Предпросмотр",
|
||||||
"privateKey": "Приватный ключ",
|
"privateKey": "Приватный ключ",
|
||||||
"process": "Процесс",
|
"process": "Процесс",
|
||||||
|
"prune": "Обрезать",
|
||||||
"pushToken": "Токен уведомлений",
|
"pushToken": "Токен уведомлений",
|
||||||
"pveIgnoreCertTip": "Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.",
|
"pveIgnoreCertTip": "Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.",
|
||||||
"pveLoginFailed": "Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.",
|
"pveLoginFailed": "Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Önizleme",
|
"preview": "Önizleme",
|
||||||
"privateKey": "Özel Anahtar",
|
"privateKey": "Özel Anahtar",
|
||||||
"process": "İşlem",
|
"process": "İşlem",
|
||||||
|
"prune": "Budamak",
|
||||||
"pushToken": "Push belirteci",
|
"pushToken": "Push belirteci",
|
||||||
"pveIgnoreCertTip": "Etkinleştirilmesi önerilmez, güvenlik risklerine dikkat edin! PVE'den varsayılan sertifikayı kullanıyorsanız, bu seçeneği etkinleştirmeniz gerekir.",
|
"pveIgnoreCertTip": "Etkinleştirilmesi önerilmez, güvenlik risklerine dikkat edin! PVE'den varsayılan sertifikayı kullanıyorsanız, bu seçeneği etkinleştirmeniz gerekir.",
|
||||||
"pveLoginFailed": "Giriş başarısız. Linux PAM girişi için sunucu yapılandırmasındaki kullanıcı adı/şifre ile kimlik doğrulama yapılamadı.",
|
"pveLoginFailed": "Giriş başarısız. Linux PAM girişi için sunucu yapılandırmasındaki kullanıcı adı/şifre ile kimlik doğrulama yapılamadı.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "Попередній перегляд",
|
"preview": "Попередній перегляд",
|
||||||
"privateKey": "Приватний ключ",
|
"privateKey": "Приватний ключ",
|
||||||
"process": "Процес",
|
"process": "Процес",
|
||||||
|
"prune": "Обрізати",
|
||||||
"pushToken": "Надіслати токен",
|
"pushToken": "Надіслати токен",
|
||||||
"pveIgnoreCertTip": "Не рекомендується включати, будьте обережні з ризиками безпеки! Якщо ви використовуєте стандартний сертифікат від PVE, вам потрібно увімкнути цю опцію.",
|
"pveIgnoreCertTip": "Не рекомендується включати, будьте обережні з ризиками безпеки! Якщо ви використовуєте стандартний сертифікат від PVE, вам потрібно увімкнути цю опцію.",
|
||||||
"pveLoginFailed": "Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.",
|
"pveLoginFailed": "Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "预览",
|
"preview": "预览",
|
||||||
"privateKey": "私钥",
|
"privateKey": "私钥",
|
||||||
"process": "进程",
|
"process": "进程",
|
||||||
|
"prune": "修剪",
|
||||||
"pushToken": "消息推送 Token",
|
"pushToken": "消息推送 Token",
|
||||||
"pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项",
|
"pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项",
|
||||||
"pveLoginFailed": "登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。",
|
"pveLoginFailed": "登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"preview": "預覽",
|
"preview": "預覽",
|
||||||
"privateKey": "私鑰",
|
"privateKey": "私鑰",
|
||||||
"process": "行程",
|
"process": "行程",
|
||||||
|
"prune": "修剪",
|
||||||
"pushToken": "消息推送 Token",
|
"pushToken": "消息推送 Token",
|
||||||
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的默認證書,則需要啟用此選項。",
|
"pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的默認證書,則需要啟用此選項。",
|
||||||
"pveLoginFailed": "登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。",
|
"pveLoginFailed": "登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。",
|
||||||
|
|||||||
@@ -1,591 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
|
||||||
import 'package:server_box/core/route.dart';
|
|
||||||
import 'package:server_box/data/model/app/menu/base.dart';
|
|
||||||
import 'package:server_box/data/model/app/menu/container.dart';
|
|
||||||
import 'package:server_box/data/model/container/image.dart';
|
|
||||||
import 'package:server_box/data/model/container/ps.dart';
|
|
||||||
import 'package:server_box/data/model/container/type.dart';
|
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
|
||||||
import 'package:server_box/data/provider/container.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
|
||||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
|
||||||
|
|
||||||
class ContainerPage extends StatefulWidget {
|
|
||||||
final SpiRequiredArgs args;
|
|
||||||
const ContainerPage({required this.args, super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ContainerPage> createState() => _ContainerPageState();
|
|
||||||
|
|
||||||
static const route = AppRouteArg(
|
|
||||||
page: ContainerPage.new,
|
|
||||||
path: '/container',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ContainerPageState extends State<ContainerPage> {
|
|
||||||
final _textController = TextEditingController();
|
|
||||||
late final _container = ContainerProvider(
|
|
||||||
client: widget.args.spi.server?.value.client,
|
|
||||||
userName: widget.args.spi.user,
|
|
||||||
hostId: widget.args.spi.id,
|
|
||||||
context: context,
|
|
||||||
);
|
|
||||||
late Size _size;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_textController.dispose();
|
|
||||||
_container.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_initAutoRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_size = MediaQuery.of(context).size;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Consumer<ContainerProvider>(
|
|
||||||
builder: (_, _, _) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: CustomAppBar(
|
|
||||||
centerTitle: true,
|
|
||||||
title: TwoLineText(up: l10n.container, down: widget.args.spi.name),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => context.showLoadingDialog(fn: () => _container.refresh()),
|
|
||||||
icon: const Icon(Icons.refresh),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: _buildMain(),
|
|
||||||
floatingActionButton: _container.error == null ? _buildFAB() : null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFAB() {
|
|
||||||
return FloatingActionButton(
|
|
||||||
onPressed: () async => await _showAddFAB(),
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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()),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
_buildEditHost(),
|
|
||||||
_buildSwitchProvider(),
|
|
||||||
UIs.height13,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (_container.items == null || _container.images == null) {
|
|
||||||
return UIs.centerLoading;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView(
|
|
||||||
padding: const EdgeInsets.only(left: 13, right: 13, top: 13, bottom: 37),
|
|
||||||
children: <Widget>[
|
|
||||||
_buildLoading(),
|
|
||||||
_buildVersion(),
|
|
||||||
_buildPs(),
|
|
||||||
_buildImage(),
|
|
||||||
_buildEditHost(),
|
|
||||||
_buildSwitchProvider(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImage() {
|
|
||||||
return CardX(
|
|
||||||
child: 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 CardX(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(17),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(_container.type.name.capitalize),
|
|
||||||
Text(_container.version ?? l10n.unknown),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPs() {
|
|
||||||
final items = _container.items;
|
|
||||||
if (items == null) return UIs.placeholder;
|
|
||||||
return Column(
|
|
||||||
children: items.map(_buildPsItem).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPsItem(ContainerPs item) {
|
|
||||||
return CardX(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 11),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(item.name ?? l10n.unknown, style: UIs.text15),
|
|
||||||
const SizedBox(height: 3),
|
|
||||||
_buildMoreBtn(item),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${item.image ?? l10n.unknown} - ${switch (item) {
|
|
||||||
final PodmanPs ps => ps.running ? l10n.running : l10n.stopped,
|
|
||||||
final DockerPs ps => ps.state,
|
|
||||||
}}',
|
|
||||||
style: UIs.text13Grey,
|
|
||||||
),
|
|
||||||
_buildPsItemStats(item),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPsItemStats(ContainerPs item) {
|
|
||||||
if (item.cpu == null || item.mem == null) return UIs.placeholder;
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
UIs.height13,
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildPsItemStatsItem('CPU', item.cpu, Icons.memory),
|
|
||||||
UIs.width13,
|
|
||||||
_buildPsItemStatsItem('Net', item.net, Icons.network_cell),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildPsItemStatsItem('Mem', item.mem, Icons.settings_input_component),
|
|
||||||
UIs.width13,
|
|
||||||
_buildPsItemStatsItem('Disk', item.disk, Icons.storage),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPsItemStatsItem(String title, String? value, IconData icon) {
|
|
||||||
return SizedBox(
|
|
||||||
width: _size.width / 2 - 41,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 12, color: Colors.grey),
|
|
||||||
UIs.width7,
|
|
||||||
Text(value ?? l10n.unknown, style: UIs.text11Grey),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMoreBtn(ContainerPs dItem) {
|
|
||||||
return PopupMenu(
|
|
||||||
items: ContainerMenu.items(dItem.running).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
|
||||||
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// String _buildPsCardSubtitle(List<ContainerPs> 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 = <Widget>[];
|
|
||||||
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: SimpleMarkdown(data: l10n.dockerEmptyRunningItems),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
children.add(
|
|
||||||
TextButton(
|
|
||||||
onPressed: _showEditHostDialog,
|
|
||||||
child: Text('${libL10n.edit} DOCKER_HOST'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return CardX(
|
|
||||||
child: 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 CardX(child: child);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showAddFAB() async {
|
|
||||||
final imageCtrl = TextEditingController();
|
|
||||||
final nameCtrl = TextEditingController();
|
|
||||||
final argsCtrl = TextEditingController();
|
|
||||||
await context.showRoundDialog(
|
|
||||||
title: l10n.newContainer,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Input(
|
|
||||||
autoFocus: true,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: l10n.image,
|
|
||||||
hint: 'xxx:1.1',
|
|
||||||
controller: imageCtrl,
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
type: TextInputType.text,
|
|
||||||
controller: nameCtrl,
|
|
||||||
label: libL10n.name,
|
|
||||||
hint: 'xxx',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
type: TextInputType.text,
|
|
||||||
controller: argsCtrl,
|
|
||||||
label: l10n.extraArgs,
|
|
||||||
hint: '-p 2222:22 -v ~/.xxx/:/xxx',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: Btn.ok(onTap: () async {
|
|
||||||
context.pop();
|
|
||||||
await _showAddCmdPreview(
|
|
||||||
_buildAddCmd(
|
|
||||||
imageCtrl.text.trim(),
|
|
||||||
nameCtrl.text.trim(),
|
|
||||||
argsCtrl.text.trim(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showAddCmdPreview(String cmd) async {
|
|
||||||
await context.showRoundDialog(
|
|
||||||
title: l10n.preview,
|
|
||||||
child: Text(cmd),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => context.pop(),
|
|
||||||
child: Text(libL10n.cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
context.pop();
|
|
||||||
|
|
||||||
final (result, err) = await context.showLoadingDialog(
|
|
||||||
fn: () => _container.run(cmd),
|
|
||||||
);
|
|
||||||
if (err != null || result != null) {
|
|
||||||
final e = result?.message ?? err?.toString();
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(e.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(l10n.run),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _buildAddCmd(String image, String name, String args) {
|
|
||||||
var suffix = '';
|
|
||||||
if (args.isEmpty) {
|
|
||||||
suffix = image;
|
|
||||||
} else {
|
|
||||||
suffix = '$args $image';
|
|
||||||
}
|
|
||||||
if (name.isEmpty) {
|
|
||||||
return 'run -itd $suffix';
|
|
||||||
}
|
|
||||||
return 'run -itd --name $name $suffix';
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showEditHostDialog() async {
|
|
||||||
final id = widget.args.spi.id;
|
|
||||||
final host = Stores.container.fetch(id);
|
|
||||||
final ctrl = TextEditingController(text: host);
|
|
||||||
await context.showRoundDialog(
|
|
||||||
title: libL10n.edit,
|
|
||||||
child: Input(
|
|
||||||
maxLines: 2,
|
|
||||||
controller: ctrl,
|
|
||||||
onSubmitted: _onSaveDockerHost,
|
|
||||||
hint: 'unix:///run/user/1000/docker.sock',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
actions: Btn.ok(onTap: () => _onSaveDockerHost(ctrl.text)).toList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSaveDockerHost(String val) {
|
|
||||||
context.pop();
|
|
||||||
Stores.container.put(widget.args.spi.id, val.trim());
|
|
||||||
_container.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showImageRmDialog(ContainerImg e) {
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.attention,
|
|
||||||
child: Text(
|
|
||||||
libL10n.askContinue('${libL10n.delete} Image(${e.repository})'),
|
|
||||||
),
|
|
||||||
actions: Btn.ok(
|
|
||||||
onTap: () async {
|
|
||||||
context.pop();
|
|
||||||
final result = await _container.run('rmi ${e.id} -f');
|
|
||||||
if (result != null) {
|
|
||||||
context.showSnackBar(result.message ?? 'null');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
red: true,
|
|
||||||
).toList,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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: libL10n.attention,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(libL10n.askContinue(
|
|
||||||
'${libL10n.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: Btn.ok(onTap: () async {
|
|
||||||
context.pop();
|
|
||||||
|
|
||||||
final (result, err) = await context.showLoadingDialog(
|
|
||||||
fn: () => _container.delete(id, force),
|
|
||||||
);
|
|
||||||
if (err != null || result != null) {
|
|
||||||
final e = result?.message ?? err?.toString();
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(e ?? 'null'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}).toList,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case ContainerMenu.start:
|
|
||||||
final (result, err) = await context.showLoadingDialog(
|
|
||||||
fn: () => _container.start(id),
|
|
||||||
);
|
|
||||||
if (err != null || result != null) {
|
|
||||||
final e = result?.message ?? err?.toString();
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(e ?? 'null'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ContainerMenu.stop:
|
|
||||||
final (result, err) = await context.showLoadingDialog(
|
|
||||||
fn: () => _container.stop(id),
|
|
||||||
);
|
|
||||||
if (err != null || result != null) {
|
|
||||||
final e = result?.message ?? err?.toString();
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(e ?? 'null'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ContainerMenu.restart:
|
|
||||||
final (result, err) = await context.showLoadingDialog(
|
|
||||||
fn: () => _container.restart(id),
|
|
||||||
);
|
|
||||||
if (err != null || result != null) {
|
|
||||||
final e = result?.message ?? err?.toString();
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.error,
|
|
||||||
child: Text(e ?? 'null'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ContainerMenu.logs:
|
|
||||||
final args = SshPageArgs(
|
|
||||||
spi: widget.args.spi,
|
|
||||||
initCmd: '${switch (_container.type) {
|
|
||||||
ContainerType.podman => 'podman',
|
|
||||||
ContainerType.docker => 'docker',
|
|
||||||
}} logs -f --tail 100 ${dItem.id}',
|
|
||||||
);
|
|
||||||
SSHPage.route.go(context, args);
|
|
||||||
break;
|
|
||||||
case ContainerMenu.terminal:
|
|
||||||
final args = SshPageArgs(
|
|
||||||
spi: widget.args.spi,
|
|
||||||
initCmd: '${switch (_container.type) {
|
|
||||||
ContainerType.podman => 'podman',
|
|
||||||
ContainerType.docker => 'docker',
|
|
||||||
}} exec -it ${dItem.id} sh',
|
|
||||||
);
|
|
||||||
SSHPage.route.go(context, args);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initAutoRefresh() {
|
|
||||||
if (Stores.setting.contaienrAutoRefresh.fetch()) {
|
|
||||||
Timer.periodic(
|
|
||||||
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()),
|
|
||||||
(timer) {
|
|
||||||
if (mounted) {
|
|
||||||
_container.refresh(isAuto: true);
|
|
||||||
} else {
|
|
||||||
timer.cancel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
232
lib/view/page/container/actions.dart
Normal file
232
lib/view/page/container/actions.dart
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
part of 'container.dart';
|
||||||
|
|
||||||
|
extension on _ContainerPageState {
|
||||||
|
Future<void> _showAddFAB() async {
|
||||||
|
final imageCtrl = TextEditingController();
|
||||||
|
final nameCtrl = TextEditingController();
|
||||||
|
final argsCtrl = TextEditingController();
|
||||||
|
await context.showRoundDialog(
|
||||||
|
title: l10n.newContainer,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Input(
|
||||||
|
autoFocus: true,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: l10n.image,
|
||||||
|
hint: 'xxx:1.1',
|
||||||
|
controller: imageCtrl,
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
type: TextInputType.text,
|
||||||
|
controller: nameCtrl,
|
||||||
|
label: libL10n.name,
|
||||||
|
hint: 'xxx',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
type: TextInputType.text,
|
||||||
|
controller: argsCtrl,
|
||||||
|
label: l10n.extraArgs,
|
||||||
|
hint: '-p 2222:22 -v ~/.xxx/:/xxx',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: Btn.ok(
|
||||||
|
onTap: () async {
|
||||||
|
context.pop();
|
||||||
|
await _showAddCmdPreview(
|
||||||
|
_buildAddCmd(imageCtrl.text.trim(), nameCtrl.text.trim(), argsCtrl.text.trim()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showPruneDialog({
|
||||||
|
required String title,
|
||||||
|
String? message,
|
||||||
|
required Future<ContainerErr?> Function() onConfirm,
|
||||||
|
}) async {
|
||||||
|
await context.showRoundDialog(
|
||||||
|
title: title,
|
||||||
|
child: Text(message ?? libL10n.askContinue('${l10n.prune} $title')),
|
||||||
|
actions: Btn.ok(
|
||||||
|
onTap: () async {
|
||||||
|
context.pop();
|
||||||
|
final (result, err) = await context.showLoadingDialog(fn: onConfirm);
|
||||||
|
if (err != null || result != null) {
|
||||||
|
final e = result?.message ?? err?.toString();
|
||||||
|
context.showRoundDialog(title: libL10n.error, child: Text(e.toString()));
|
||||||
|
} else {
|
||||||
|
context.showSnackBar(libL10n.success);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
red: true,
|
||||||
|
).toList,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showAddCmdPreview(String cmd) async {
|
||||||
|
await context.showRoundDialog(
|
||||||
|
title: l10n.preview,
|
||||||
|
child: Text(cmd),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => context.pop(), child: Text(libL10n.cancel)),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
context.pop();
|
||||||
|
|
||||||
|
final (result, err) = await context.showLoadingDialog(fn: () => _container.run(cmd));
|
||||||
|
if (err != null || result != null) {
|
||||||
|
final e = result?.message ?? err?.toString();
|
||||||
|
context.showRoundDialog(title: libL10n.error, child: Text(e.toString()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(l10n.run),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showEditHostDialog() async {
|
||||||
|
final id = widget.args.spi.id;
|
||||||
|
final host = Stores.container.fetch(id);
|
||||||
|
final ctrl = TextEditingController(text: host);
|
||||||
|
await context.showRoundDialog(
|
||||||
|
title: libL10n.edit,
|
||||||
|
child: Input(
|
||||||
|
maxLines: 2,
|
||||||
|
controller: ctrl,
|
||||||
|
onSubmitted: _onSaveDockerHost,
|
||||||
|
hint: 'unix:///run/user/1000/docker.sock',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
actions: Btn.ok(onTap: () => _onSaveDockerHost(ctrl.text)).toList,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSaveDockerHost(String val) {
|
||||||
|
context.pop();
|
||||||
|
Stores.container.put(widget.args.spi.id, val.trim());
|
||||||
|
_container.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showImageRmDialog(ContainerImg e) {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: libL10n.attention,
|
||||||
|
child: Text(libL10n.askContinue('${libL10n.delete} Image(${e.repository})')),
|
||||||
|
actions: Btn.ok(
|
||||||
|
onTap: () async {
|
||||||
|
context.pop();
|
||||||
|
final result = await _container.run('rmi ${e.id} -f');
|
||||||
|
if (result != null) {
|
||||||
|
context.showSnackBar(result.message ?? 'null');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
red: true,
|
||||||
|
).toList,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: libL10n.attention,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(libL10n.askContinue('${libL10n.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: Btn.ok(
|
||||||
|
onTap: () async {
|
||||||
|
context.pop();
|
||||||
|
|
||||||
|
final (result, err) = await context.showLoadingDialog(fn: () => _container.delete(id, force));
|
||||||
|
if (err != null || result != null) {
|
||||||
|
final e = result?.message ?? err?.toString();
|
||||||
|
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).toList,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ContainerMenu.start:
|
||||||
|
final (result, err) = await context.showLoadingDialog(fn: () => _container.start(id));
|
||||||
|
if (err != null || result != null) {
|
||||||
|
final e = result?.message ?? err?.toString();
|
||||||
|
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ContainerMenu.stop:
|
||||||
|
final (result, err) = await context.showLoadingDialog(fn: () => _container.stop(id));
|
||||||
|
if (err != null || result != null) {
|
||||||
|
final e = result?.message ?? err?.toString();
|
||||||
|
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ContainerMenu.restart:
|
||||||
|
final (result, err) = await context.showLoadingDialog(fn: () => _container.restart(id));
|
||||||
|
if (err != null || result != null) {
|
||||||
|
final e = result?.message ?? err?.toString();
|
||||||
|
context.showRoundDialog(title: libL10n.error, child: Text(e ?? 'null'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ContainerMenu.logs:
|
||||||
|
final args = SshPageArgs(
|
||||||
|
spi: widget.args.spi,
|
||||||
|
initCmd:
|
||||||
|
'${switch (_container.type) {
|
||||||
|
ContainerType.podman => 'podman',
|
||||||
|
ContainerType.docker => 'docker',
|
||||||
|
}} logs -f --tail 100 ${dItem.id}',
|
||||||
|
);
|
||||||
|
SSHPage.route.go(context, args);
|
||||||
|
break;
|
||||||
|
case ContainerMenu.terminal:
|
||||||
|
final args = SshPageArgs(
|
||||||
|
spi: widget.args.spi,
|
||||||
|
initCmd:
|
||||||
|
'${switch (_container.type) {
|
||||||
|
ContainerType.podman => 'podman',
|
||||||
|
ContainerType.docker => 'docker',
|
||||||
|
}} exec -it ${dItem.id} sh',
|
||||||
|
);
|
||||||
|
SSHPage.route.go(context, args);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initAutoRefresh() {
|
||||||
|
if (Stores.setting.containerAutoRefresh.fetch()) {
|
||||||
|
Timer.periodic(Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), (timer) {
|
||||||
|
if (mounted) {
|
||||||
|
_container.refresh(isAuto: true);
|
||||||
|
} else {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
370
lib/view/page/container/container.dart
Normal file
370
lib/view/page/container/container.dart
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/core/route.dart';
|
||||||
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
|
import 'package:server_box/data/model/app/menu/base.dart';
|
||||||
|
import 'package:server_box/data/model/app/menu/container.dart';
|
||||||
|
import 'package:server_box/data/model/container/image.dart';
|
||||||
|
import 'package:server_box/data/model/container/ps.dart';
|
||||||
|
import 'package:server_box/data/model/container/type.dart';
|
||||||
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
|
import 'package:server_box/data/provider/container.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||||
|
|
||||||
|
part 'actions.dart';
|
||||||
|
part 'types.dart';
|
||||||
|
|
||||||
|
class ContainerPage extends StatefulWidget {
|
||||||
|
final SpiRequiredArgs args;
|
||||||
|
const ContainerPage({required this.args, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ContainerPage> createState() => _ContainerPageState();
|
||||||
|
|
||||||
|
static const route = AppRouteArg(page: ContainerPage.new, path: '/container');
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContainerPageState extends State<ContainerPage> {
|
||||||
|
final _textController = TextEditingController();
|
||||||
|
late final _container = ContainerProvider(
|
||||||
|
client: widget.args.spi.server?.value.client,
|
||||||
|
userName: widget.args.spi.user,
|
||||||
|
hostId: widget.args.spi.id,
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_textController.dispose();
|
||||||
|
_container.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProvider(
|
||||||
|
create: (_) => _container,
|
||||||
|
builder: (_, _) => Consumer<ContainerProvider>(
|
||||||
|
builder: (_, _, _) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: _buildAppBar,
|
||||||
|
body: _buildMain,
|
||||||
|
floatingActionButton: _container.error == null ? _buildFAB : null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomAppBar get _buildAppBar {
|
||||||
|
return CustomAppBar(
|
||||||
|
centerTitle: true,
|
||||||
|
title: TwoLineText(up: l10n.container, down: widget.args.spi.name),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.showLoadingDialog(fn: () => _container.refresh()),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _buildFAB {
|
||||||
|
return FloatingActionButton(onPressed: () async => await _showAddFAB(), child: const Icon(Icons.add));
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _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()),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
UIs.height13,
|
||||||
|
_buildSettingsBtns,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_container.items == null || _container.images == null) {
|
||||||
|
return UIs.centerLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: AutoMultiList(
|
||||||
|
children: <Widget>[
|
||||||
|
_buildLoading(),
|
||||||
|
_buildVersion(),
|
||||||
|
_buildPs(),
|
||||||
|
_buildImage(),
|
||||||
|
_buildEmptyStateMessage(),
|
||||||
|
_buildPruneBtns,
|
||||||
|
_buildSettingsBtns,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyStateMessage() {
|
||||||
|
final emptyImgs = _container.images?.isEmpty ?? true;
|
||||||
|
final emptyPs = _container.items?.isEmpty ?? true;
|
||||||
|
if (emptyPs && emptyImgs && _container.runLog == null) {
|
||||||
|
return CardX(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(17, 17, 17, 7),
|
||||||
|
child: SimpleMarkdown(data: l10n.dockerEmptyRunningItems),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return UIs.placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImage() {
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(MingCute.clapperboard_line),
|
||||||
|
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() ?? [],
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImageItem(ContainerImg e) {
|
||||||
|
final repoSplited = e.repository?.split('/');
|
||||||
|
final title = repoSplited?.lastOrNull ?? e.repository;
|
||||||
|
repoSplited?.removeLast();
|
||||||
|
final reg = repoSplited?.join('/');
|
||||||
|
return ListTile(
|
||||||
|
title: Text(title ?? l10n.unknown, style: UIs.text15),
|
||||||
|
subtitle: Text('${reg ?? ''} - ${e.tag} - ${e.sizeMB}', style: UIs.text13Grey),
|
||||||
|
trailing: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
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 CardX(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(17),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [Text(_container.type.name.capitalize), Text(_container.version ?? l10n.unknown)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPs() {
|
||||||
|
final items = _container.items;
|
||||||
|
if (items == null) return UIs.placeholder;
|
||||||
|
final running = items.where((e) => e.running).length;
|
||||||
|
final stopped = items.length - running;
|
||||||
|
final subtitle = stopped > 0
|
||||||
|
? l10n.dockerStatusRunningAndStoppedFmt(running, stopped)
|
||||||
|
: l10n.dockerStatusRunningFmt(running);
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(OctIcons.container, size: 22),
|
||||||
|
title: Text(l10n.container),
|
||||||
|
subtitle: Text(subtitle, style: UIs.textGrey),
|
||||||
|
initiallyExpanded: items.length < 7,
|
||||||
|
children: items.map(_buildPsItem).toList(),
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPsItem(ContainerPs item) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 11),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(item.name ?? l10n.unknown, style: UIs.text15),
|
||||||
|
const SizedBox(height: 3),
|
||||||
|
_buildMoreBtn(item),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${item.image ?? l10n.unknown} - ${switch (item) {
|
||||||
|
final PodmanPs ps => ps.running ? l10n.running : l10n.stopped,
|
||||||
|
final DockerPs ps => ps.state,
|
||||||
|
}}',
|
||||||
|
style: UIs.text13Grey,
|
||||||
|
),
|
||||||
|
_buildPsItemStats(item),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPsItemStats(ContainerPs item) {
|
||||||
|
if (item.cpu == null || item.mem == null) return UIs.placeholder;
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (_, cons) {
|
||||||
|
final width = cons.maxWidth / 2 - 41;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
UIs.height13,
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildPsItemStatsItem('CPU', item.cpu, Icons.memory, width: width),
|
||||||
|
UIs.width13,
|
||||||
|
_buildPsItemStatsItem('Net', item.net, Icons.network_cell, width: width),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildPsItemStatsItem('Mem', item.mem, Icons.settings_input_component, width: width),
|
||||||
|
UIs.width13,
|
||||||
|
_buildPsItemStatsItem('Disk', item.disk, Icons.storage, width: width),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPsItemStatsItem(String title, String? value, IconData icon, {required double width}) {
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 12, color: Colors.grey),
|
||||||
|
UIs.width7,
|
||||||
|
Text(value ?? l10n.unknown, style: UIs.text11Grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMoreBtn(ContainerPs dItem) {
|
||||||
|
return PopupMenu(
|
||||||
|
items: ContainerMenu.items(dItem.running).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(),
|
||||||
|
onSelected: (item) => _onTapMoreBtn(item, dItem),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildAddCmd(String image, String name, String args) {
|
||||||
|
var suffix = '';
|
||||||
|
if (args.isEmpty) {
|
||||||
|
suffix = image;
|
||||||
|
} else {
|
||||||
|
suffix = '$args $image';
|
||||||
|
}
|
||||||
|
if (name.isEmpty) {
|
||||||
|
return 'run -itd $suffix';
|
||||||
|
}
|
||||||
|
return 'run -itd --name $name $suffix';
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _buildPruneBtns {
|
||||||
|
final len = _PruneTypes.values.length;
|
||||||
|
if (len == 0) return UIs.placeholder;
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(Icons.delete),
|
||||||
|
title: Text(l10n.prune),
|
||||||
|
children: _PruneTypes.values.map(_buildPruneBtn).toList(),
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPruneBtn(_PruneTypes type) {
|
||||||
|
final title = type.name.capitalize;
|
||||||
|
return ListTile(
|
||||||
|
onTap: () async {
|
||||||
|
await _showPruneDialog(
|
||||||
|
title: title,
|
||||||
|
message: type.tip,
|
||||||
|
onConfirm: switch (type) {
|
||||||
|
_PruneTypes.images => _container.pruneImages,
|
||||||
|
_PruneTypes.containers => () => _container.pruneContainers(),
|
||||||
|
_PruneTypes.volumes => _container.pruneVolumes,
|
||||||
|
_PruneTypes.system => _container.pruneSystem,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
title: Text(title),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget get _buildSettingsBtns {
|
||||||
|
final len = _SettingsMenuItems.values.length;
|
||||||
|
if (len == 0) return UIs.placeholder;
|
||||||
|
return ExpandTile(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: Text(libL10n.setting),
|
||||||
|
initiallyExpanded: _container.error != null,
|
||||||
|
children: _SettingsMenuItems.values.map(_buildSettingTile).toList(),
|
||||||
|
).cardx;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettingTile(_SettingsMenuItems item) {
|
||||||
|
final String title;
|
||||||
|
switch (item) {
|
||||||
|
case _SettingsMenuItems.editDockerHost:
|
||||||
|
title = '${libL10n.edit} DOCKER_HOST';
|
||||||
|
break;
|
||||||
|
case _SettingsMenuItems.switchProvider:
|
||||||
|
title = _container.type == ContainerType.podman ? l10n.switchTo('Docker') : l10n.switchTo('Podman');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
switch (item) {
|
||||||
|
case _SettingsMenuItems.editDockerHost:
|
||||||
|
_showEditHostDialog();
|
||||||
|
break;
|
||||||
|
case _SettingsMenuItems.switchProvider:
|
||||||
|
_container.setType(
|
||||||
|
_container.type == ContainerType.docker ? ContainerType.podman : ContainerType.docker,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: Text(title),
|
||||||
|
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/view/page/container/types.dart
Normal file
18
lib/view/page/container/types.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
part of 'container.dart';
|
||||||
|
|
||||||
|
enum _SettingsMenuItems { editDockerHost, switchProvider }
|
||||||
|
|
||||||
|
enum _PruneTypes {
|
||||||
|
images,
|
||||||
|
containers,
|
||||||
|
volumes,
|
||||||
|
system;
|
||||||
|
|
||||||
|
String? get tip {
|
||||||
|
return switch (this) {
|
||||||
|
_PruneTypes.system =>
|
||||||
|
'This will remove all unused data, including images, containers, volumes, and networks.',
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import 'package:server_box/data/model/server/snippet.dart';
|
|||||||
import 'package:server_box/data/provider/server.dart';
|
import 'package:server_box/data/provider/server.dart';
|
||||||
import 'package:server_box/data/provider/snippet.dart';
|
import 'package:server_box/data/provider/snippet.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/container.dart';
|
import 'package:server_box/view/page/container/container.dart';
|
||||||
import 'package:server_box/view/page/iperf.dart';
|
import 'package:server_box/view/page/iperf.dart';
|
||||||
import 'package:server_box/view/page/process.dart';
|
import 'package:server_box/view/page/process.dart';
|
||||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user