From e6db2db320ea996d11e390d8e619d6377f9bc4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:53:43 +0800 Subject: [PATCH] fix: container not working (#787) --- lib/data/provider/container.dart | 19 + lib/data/store/setting.dart | 4 +- lib/generated/l10n/l10n.dart | 6 + lib/generated/l10n/l10n_de.dart | 3 + lib/generated/l10n/l10n_en.dart | 3 + lib/generated/l10n/l10n_es.dart | 3 + lib/generated/l10n/l10n_fr.dart | 3 + lib/generated/l10n/l10n_id.dart | 3 + lib/generated/l10n/l10n_ja.dart | 3 + lib/generated/l10n/l10n_nl.dart | 3 + lib/generated/l10n/l10n_pt.dart | 3 + lib/generated/l10n/l10n_ru.dart | 3 + lib/generated/l10n/l10n_tr.dart | 3 + lib/generated/l10n/l10n_uk.dart | 3 + lib/generated/l10n/l10n_zh.dart | 6 + lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_nl.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_tr.arb | 1 + lib/l10n/app_uk.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/app_zh_tw.arb | 1 + lib/view/page/container.dart | 591 ------------------------- lib/view/page/container/actions.dart | 232 ++++++++++ lib/view/page/container/container.dart | 370 ++++++++++++++++ lib/view/page/container/types.dart | 18 + lib/view/widget/server_func_btns.dart | 2 +- 33 files changed, 700 insertions(+), 594 deletions(-) delete mode 100644 lib/view/page/container.dart create mode 100644 lib/view/page/container/actions.dart create mode 100644 lib/view/page/container/container.dart create mode 100644 lib/view/page/container/types.dart diff --git a/lib/data/provider/container.dart b/lib/data/provider/container.dart index f2977e67..f5e705ae 100644 --- a/lib/data/provider/container.dart +++ b/lib/data/provider/container.dart @@ -222,6 +222,23 @@ class ContainerProvider extends ChangeNotifier { Future restart(String id) async => await run('restart $id'); + Future pruneImages({bool all = true}) async { + final cmd = 'image prune${all ? " -a" : ""} -f'; + return await run(cmd); + } + + Future pruneContainers() async { + return await run('container prune -f'); + } + + Future pruneVolumes() async { + return await run('volume prune -f'); + } + + Future pruneSystem() async { + return await run('system prune -a -f --volumes'); + } + Future run(String cmd, {bool autoRefresh = true}) async { cmd = switch (type) { ContainerType.docker => 'docker $cmd', @@ -272,6 +289,8 @@ enum ContainerCmdType { ps, stats, images, + // No specific commands needed for prune actions as they are simple + // and don't require splitting output with ShellFunc.seperator ; String exec( diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 881c92bc..6ff84dca 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -176,8 +176,8 @@ class SettingStore extends HiveStore { late final containerParseStat = propertyDefault('containerParseStat', true); /// Auto refresh container status - late final contaienrAutoRefresh = propertyDefault( - 'contaienrAutoRefresh', + late final containerAutoRefresh = propertyDefault( + 'containerAutoRefresh', true, ); diff --git a/lib/generated/l10n/l10n.dart b/lib/generated/l10n/l10n.dart index 0b0d4895..ca36dc89 100644 --- a/lib/generated/l10n/l10n.dart +++ b/lib/generated/l10n/l10n.dart @@ -914,6 +914,12 @@ abstract class AppLocalizations { /// **'Process'** String get process; + /// No description provided for @prune. + /// + /// In en, this message translates to: + /// **'Prune'** + String get prune; + /// No description provided for @pushToken. /// /// In en, this message translates to: diff --git a/lib/generated/l10n/l10n_de.dart b/lib/generated/l10n/l10n_de.dart index 7ad5821a..5b7dd467 100644 --- a/lib/generated/l10n/l10n_de.dart +++ b/lib/generated/l10n/l10n_de.dart @@ -452,6 +452,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get process => 'Prozess'; + @override + String get prune => 'Beschneiden'; + @override String get pushToken => 'Push Token'; diff --git a/lib/generated/l10n/l10n_en.dart b/lib/generated/l10n/l10n_en.dart index 72de7649..708fd5a4 100644 --- a/lib/generated/l10n/l10n_en.dart +++ b/lib/generated/l10n/l10n_en.dart @@ -450,6 +450,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get process => 'Process'; + @override + String get prune => 'Prune'; + @override String get pushToken => 'Push token'; diff --git a/lib/generated/l10n/l10n_es.dart b/lib/generated/l10n/l10n_es.dart index 51e551d9..b537c797 100644 --- a/lib/generated/l10n/l10n_es.dart +++ b/lib/generated/l10n/l10n_es.dart @@ -454,6 +454,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get process => 'Proceso'; + @override + String get prune => 'Podar'; + @override String get pushToken => 'Token de notificaciones'; diff --git a/lib/generated/l10n/l10n_fr.dart b/lib/generated/l10n/l10n_fr.dart index 5c982d97..2762fc96 100644 --- a/lib/generated/l10n/l10n_fr.dart +++ b/lib/generated/l10n/l10n_fr.dart @@ -455,6 +455,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get process => 'Processus'; + @override + String get prune => 'Élaguer'; + @override String get pushToken => 'Jeton d\'identification'; diff --git a/lib/generated/l10n/l10n_id.dart b/lib/generated/l10n/l10n_id.dart index 815581cf..5bf08add 100644 --- a/lib/generated/l10n/l10n_id.dart +++ b/lib/generated/l10n/l10n_id.dart @@ -450,6 +450,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get process => 'Proses'; + @override + String get prune => 'Pangkas'; + @override String get pushToken => 'Dorong token'; diff --git a/lib/generated/l10n/l10n_ja.dart b/lib/generated/l10n/l10n_ja.dart index 797a50df..fcf776c0 100644 --- a/lib/generated/l10n/l10n_ja.dart +++ b/lib/generated/l10n/l10n_ja.dart @@ -436,6 +436,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get process => 'プロセス'; + @override + String get prune => '剪定する'; + @override String get pushToken => 'プッシュトークン'; diff --git a/lib/generated/l10n/l10n_nl.dart b/lib/generated/l10n/l10n_nl.dart index 14552bbc..08072d07 100644 --- a/lib/generated/l10n/l10n_nl.dart +++ b/lib/generated/l10n/l10n_nl.dart @@ -451,6 +451,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get process => 'Proces'; + @override + String get prune => 'Snoeien'; + @override String get pushToken => 'Push-token'; diff --git a/lib/generated/l10n/l10n_pt.dart b/lib/generated/l10n/l10n_pt.dart index fccdfb49..018ab98b 100644 --- a/lib/generated/l10n/l10n_pt.dart +++ b/lib/generated/l10n/l10n_pt.dart @@ -451,6 +451,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get process => 'Processo'; + @override + String get prune => 'Podar'; + @override String get pushToken => 'Token de notificação push'; diff --git a/lib/generated/l10n/l10n_ru.dart b/lib/generated/l10n/l10n_ru.dart index 88b54e32..0f155f1c 100644 --- a/lib/generated/l10n/l10n_ru.dart +++ b/lib/generated/l10n/l10n_ru.dart @@ -452,6 +452,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get process => 'Процесс'; + @override + String get prune => 'Обрезать'; + @override String get pushToken => 'Токен уведомлений'; diff --git a/lib/generated/l10n/l10n_tr.dart b/lib/generated/l10n/l10n_tr.dart index 411d2197..d5a606ce 100644 --- a/lib/generated/l10n/l10n_tr.dart +++ b/lib/generated/l10n/l10n_tr.dart @@ -449,6 +449,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get process => 'İşlem'; + @override + String get prune => 'Budamak'; + @override String get pushToken => 'Push belirteci'; diff --git a/lib/generated/l10n/l10n_uk.dart b/lib/generated/l10n/l10n_uk.dart index 4a6b334d..721f7a45 100644 --- a/lib/generated/l10n/l10n_uk.dart +++ b/lib/generated/l10n/l10n_uk.dart @@ -454,6 +454,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get process => 'Процес'; + @override + String get prune => 'Обрізати'; + @override String get pushToken => 'Надіслати токен'; diff --git a/lib/generated/l10n/l10n_zh.dart b/lib/generated/l10n/l10n_zh.dart index aedbe39c..d4f1c31a 100644 --- a/lib/generated/l10n/l10n_zh.dart +++ b/lib/generated/l10n/l10n_zh.dart @@ -431,6 +431,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get process => '进程'; + @override + String get prune => '修剪'; + @override String get pushToken => '消息推送 Token'; @@ -1162,6 +1165,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get process => '行程'; + @override + String get prune => '修剪'; + @override String get pushToken => '消息推送 Token'; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2fe04bb4..d7a3799f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -132,6 +132,7 @@ "preview": "Vorschau", "privateKey": "Private Key", "process": "Prozess", + "prune": "Beschneiden", "pushToken": "Push Token", "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.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6faa091b..f3666b48 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -132,6 +132,7 @@ "preview": "Preview", "privateKey": "Private Key", "process": "Process", + "prune": "Prune", "pushToken": "Push token", "pveIgnoreCertTip": "Not recommended to enable, beware of security risks! If you are using the default certificate from PVE, you need to enable this option.", "pveLoginFailed": "Login failed. Unable to authenticate with username/password from server configuration for Linux PAM login.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a369fe43..901d0424 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -132,6 +132,7 @@ "preview": "Vista previa", "privateKey": "Llave privada", "process": "Proceso", + "prune": "Podar", "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.", "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.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7ca6cd9b..e18fefc4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -132,6 +132,7 @@ "preview": "Aperçu", "privateKey": "Clé privée", "process": "Processus", + "prune": "Élaguer", "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.", "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.", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index cd90a80e..59769546 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -132,6 +132,7 @@ "preview": "Pratinjau", "privateKey": "Kunci Pribadi", "process": "Proses", + "prune": "Pangkas", "pushToken": "Dorong token", "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.", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index a441ba30..242215b3 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -132,6 +132,7 @@ "preview": "プレビュー", "privateKey": "秘密鍵", "process": "プロセス", + "prune": "剪定する", "pushToken": "プッシュトークン", "pveIgnoreCertTip": "オプションを有効にすることは推奨されません、セキュリティリスクに注意してください!PVEのデフォルト証明書を使用している場合は、このオプションを有効にする必要があります。", "pveLoginFailed": "ログインに失敗しました。Linux PAMログインのためにサーバー構成からのユーザー名/パスワードで認証できません。", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index dd5e75ad..24d1f8ab 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -132,6 +132,7 @@ "preview": "Voorbeeld", "privateKey": "Privésleutel", "process": "Proces", + "prune": "Snoeien", "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.", "pveLoginFailed": "Aanmelden mislukt. Kan niet authenticeren met gebruikersnaam/wachtwoord van serverconfiguratie voor Linux PAM-login.", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index eb602d03..e19ed28d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -132,6 +132,7 @@ "preview": "Pré-visualização", "privateKey": "Chave privada", "process": "Processo", + "prune": "Podar", "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.", "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.", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index d1698925..d360d1cc 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -132,6 +132,7 @@ "preview": "Предпросмотр", "privateKey": "Приватный ключ", "process": "Процесс", + "prune": "Обрезать", "pushToken": "Токен уведомлений", "pveIgnoreCertTip": "Не рекомендуется включать, обратите внимание на риски безопасности! Если вы используете стандартный сертификат от PVE, вам нужно включить эту опцию.", "pveLoginFailed": "Ошибка входа. Невозможно аутентифицироваться с помощью имени пользователя/пароля из конфигурации сервера для входа в Linux PAM.", diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 5d573a67..d3fe2b22 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -132,6 +132,7 @@ "preview": "Önizleme", "privateKey": "Özel Anahtar", "process": "İşlem", + "prune": "Budamak", "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.", "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ı.", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 27fe31ed..f73a106f 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -132,6 +132,7 @@ "preview": "Попередній перегляд", "privateKey": "Приватний ключ", "process": "Процес", + "prune": "Обрізати", "pushToken": "Надіслати токен", "pveIgnoreCertTip": "Не рекомендується включати, будьте обережні з ризиками безпеки! Якщо ви використовуєте стандартний сертифікат від PVE, вам потрібно увімкнути цю опцію.", "pveLoginFailed": "Не вдалося увійти. Неможливо пройти аутентифікацію за допомогою імені користувача/пароля з конфігурації сервера для входу Linux PAM.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 71dbb0b1..b12142b0 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -132,6 +132,7 @@ "preview": "预览", "privateKey": "私钥", "process": "进程", + "prune": "修剪", "pushToken": "消息推送 Token", "pveIgnoreCertTip": "不推荐开启,注意安全隐患!如果你使用的 PVE 默认证书,需要开启该选项", "pveLoginFailed": "登录失败。无法使用服务器配置内的用户/密码,以 Linux PAM 方式登录。", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 2e2de7e4..660cdc8f 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -132,6 +132,7 @@ "preview": "預覽", "privateKey": "私鑰", "process": "行程", + "prune": "修剪", "pushToken": "消息推送 Token", "pveIgnoreCertTip": "不建議啟用,請注意安全風險!如果您使用的是 PVE 的默認證書,則需要啟用此選項。", "pveLoginFailed": "登錄失敗。無法使用伺服器配置中的使用者名稱/密碼以 Linux PAM 方式登錄。", diff --git a/lib/view/page/container.dart b/lib/view/page/container.dart deleted file mode 100644 index f3a70a59..00000000 --- a/lib/view/page/container.dart +++ /dev/null @@ -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 createState() => _ContainerPageState(); - - static const route = AppRouteArg( - page: ContainerPage.new, - path: '/container', - ); -} - -class _ContainerPageState extends State { - 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( - 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: [ - _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 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: 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 _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 _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 _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(); - } - }, - ); - } - } -} diff --git a/lib/view/page/container/actions.dart b/lib/view/page/container/actions.dart new file mode 100644 index 00000000..d44dcac6 --- /dev/null +++ b/lib/view/page/container/actions.dart @@ -0,0 +1,232 @@ +part of 'container.dart'; + +extension on _ContainerPageState { + Future _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 _showPruneDialog({ + required String title, + String? message, + required Future 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 _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 _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(); + } + }); + } + } +} diff --git a/lib/view/page/container/container.dart b/lib/view/page/container/container.dart new file mode 100644 index 00000000..38f7ddb2 --- /dev/null +++ b/lib/view/page/container/container.dart @@ -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 createState() => _ContainerPageState(); + + static const route = AppRouteArg(page: ContainerPage.new, path: '/container'); +} + +class _ContainerPageState extends State { + 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( + 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: [ + _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), + ); + } +} diff --git a/lib/view/page/container/types.dart b/lib/view/page/container/types.dart new file mode 100644 index 00000000..ca8ebd64 --- /dev/null +++ b/lib/view/page/container/types.dart @@ -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, + }; + } +} \ No newline at end of file diff --git a/lib/view/widget/server_func_btns.dart b/lib/view/widget/server_func_btns.dart index 12fd4c2a..52766b48 100644 --- a/lib/view/widget/server_func_btns.dart +++ b/lib/view/widget/server_func_btns.dart @@ -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/snippet.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/process.dart'; import 'package:server_box/view/page/ssh/page/page.dart';