From 2597f995712ec94029bcf03623c849e2dd28aa5b Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Mon, 18 Mar 2024 23:11:30 -0600 Subject: [PATCH] new: pve dashboard (#307) --- lib/data/model/server/pve.dart | 32 ++- lib/data/provider/pve.dart | 38 +++- lib/data/res/ui.dart | 5 + lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 2 + lib/l10n/app_fr.arb | 2 + lib/l10n/app_id.arb | 2 + lib/l10n/app_ja.arb | 2 + lib/l10n/app_pt.arb | 2 + lib/l10n/app_ru.arb | 2 + lib/l10n/app_zh.arb | 2 + lib/l10n/app_zh_tw.arb | 2 + lib/view/page/pve.dart | 370 ++++++++++++++++++++++++--------- lib/view/widget/kv_row.dart | 39 ++++ 15 files changed, 390 insertions(+), 114 deletions(-) create mode 100644 lib/view/widget/kv_row.dart diff --git a/lib/data/model/server/pve.dart b/lib/data/model/server/pve.dart index e673312b3..29d08828d 100644 --- a/lib/data/model/server/pve.dart +++ b/lib/data/model/server/pve.dart @@ -28,12 +28,12 @@ enum PveResType { } String get toStr => switch (this) { - PveResType.node => l10n.node, - PveResType.qemu => 'QEMU', - PveResType.lxc => 'LXC', - PveResType.storage => l10n.storage, - PveResType.sdn => 'SDN', - }; + PveResType.node => l10n.node, + PveResType.qemu => 'QEMU', + PveResType.lxc => 'LXC', + PveResType.storage => l10n.storage, + PveResType.sdn => 'SDN', + }; } sealed class PveResIface { @@ -121,6 +121,15 @@ final class PveLxc extends PveResIface { netout: json['netout'], ); } + + bool get isRunning => status == 'running'; + + String get topRight { + if (isRunning) { + return uptime.secondsToDuration().toStr; + } + return l10n.stopped; + } } final class PveQemu extends PveResIface { @@ -190,7 +199,7 @@ final class PveQemu extends PveResIface { bool get isRunning => status == 'running'; String get topRight { - if (!isRunning) { + if (isRunning) { return uptime.secondsToDuration().toStr; } return l10n.stopped; @@ -236,6 +245,15 @@ final class PveNode extends PveResIface { maxcpu: json['maxcpu'], ); } + + bool get isRunning => status == 'online'; + + String get topRight { + if (isRunning) { + return uptime.secondsToDuration().toStr; + } + return l10n.stopped; + } } final class PveStorage extends PveResIface { diff --git a/lib/data/provider/pve.dart b/lib/data/provider/pve.dart index 56058fc1a..dc1816af3 100644 --- a/lib/data/provider/pve.dart +++ b/lib/data/provider/pve.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; +import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/data/model/server/pve.dart'; import 'package:toolbox/data/model/server/server_private_info.dart'; @@ -10,6 +11,8 @@ final class PveProvider extends ChangeNotifier { late final String addr; //late final SSHClient _client; + final data = ValueNotifier(null); + PveProvider({ required this.spi, }) { @@ -77,11 +80,12 @@ final class PveProvider extends ChangeNotifier { final resp = await session.get('$addr/api2/json/cluster/resources'); final list = resp.data['data'] as List; final items = list.map((e) => PveResIface.fromJson(e)).toList(); - final qemus = []; - final lxcs = []; - final nodes = []; - final storages = []; - final sdns = []; + + final Order qemus = []; + final Order lxcs = []; + final Order nodes = []; + final Order storages = []; + final Order sdns = []; for (final item in items) { switch (item.type) { case PveResType.lxc: @@ -101,12 +105,34 @@ final class PveProvider extends ChangeNotifier { break; } } - return PveRes( + + final old = data.value; + if (old != null) { + qemus.reorder( + order: old.qemus.map((e) => e.id).toList(), + finder: (e, s) => e.id == s); + lxcs.reorder( + order: old.lxcs.map((e) => e.id).toList(), + finder: (e, s) => e.id == s); + nodes.reorder( + order: old.nodes.map((e) => e.id).toList(), + finder: (e, s) => e.id == s); + storages.reorder( + order: old.storages.map((e) => e.id).toList(), + finder: (e, s) => e.id == s); + sdns.reorder( + order: old.sdns.map((e) => e.id).toList(), + finder: (e, s) => e.id == s); + } + + final res = PveRes( qemus: qemus, lxcs: lxcs, nodes: nodes, storages: storages, sdns: sdns, ); + data.value = res; + return res; } } diff --git a/lib/data/res/ui.dart b/lib/data/res/ui.dart index ecb54a686..c09a5c6a4 100644 --- a/lib/data/res/ui.dart +++ b/lib/data/res/ui.dart @@ -22,6 +22,10 @@ abstract final class UIs { ); static const text13Grey = TextStyle(color: Colors.grey, fontSize: 13); static const text15 = TextStyle(fontSize: 15); + static const text15Bold = TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ); static const text18 = TextStyle(fontSize: 18); static const text27 = TextStyle(fontSize: 27); static const textGrey = TextStyle(color: Colors.grey); @@ -39,6 +43,7 @@ abstract final class UIs { /// SizedBox static const placeholder = SizedBox(); + static const height7 = SizedBox(height: 7); static const height13 = SizedBox(height: 13); static const height77 = SizedBox(height: 77); static const width13 = SizedBox(width: 13); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2ecd4622c..5e734a058 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -48,6 +48,7 @@ "containerName": "Container Name", "containerStatus": "Container Status", "containerTrySudoTip": "Zum Beispiel: In der App ist der Benutzer auf aaa eingestellt, aber Docker ist unter dem Root-Benutzer installiert. In diesem Fall müssen Sie diese Option aktivieren", + "content": "Inhalt", "convert": "Konvertieren", "copy": "Kopieren", "copyPath": "Pfad kopieren", @@ -185,6 +186,7 @@ "pingNoServer": "Kein Server zum Anpingen.\nBitte füge einen Server hinzu.", "pkg": "Pkg", "platformNotSupportUpdate": "Die aktuelle Plattform unterstützt keine In-App-Updates.\nBitte kompiliere vom Quellcode und installiere sie.", + "plugInType": "Einfügetyp", "plzEnterHost": "Bitte Host eingeben.", "plzSelectKey": "Wähle einen Key.", "port": "Port", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a2eb06f62..de365bdc0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -48,6 +48,7 @@ "containerName": "Container name", "containerStatus": "Container status", "containerTrySudoTip": "For example: In the app, the user is set to aaa, but Docker is installed under the root user. In this case, you need to enable this option.", + "content": "Content", "convert": "Convert", "copy": "Copy", "copyPath": "Copy path", @@ -185,6 +186,7 @@ "pingNoServer": "No server to ping.\nPlease add a server in server tab.", "pkg": "Pkg", "platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.", + "plugInType": "Insertion Type", "plzEnterHost": "Please enter host.", "plzSelectKey": "Please select a key.", "port": "Port", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index c7c9d2f12..1591df536 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -48,6 +48,7 @@ "containerName": "Nombre del contenedor", "containerStatus": "Estado del contenedor", "containerTrySudoTip": "Por ejemplo: si configuras el usuario dentro de la app como aaa, pero Docker está instalado bajo el usuario root, entonces necesitarás habilitar esta opción", + "content": "Contenido", "convert": "Convertir", "copy": "Copiar", "copyPath": "Copiar ruta", @@ -185,6 +186,7 @@ "pingNoServer": "No hay servidores disponibles para hacer Ping\nPor favor, añade un servidor en la pestaña de servidores y vuelve a intentarlo", "pkg": "Gestión de paquetes", "platformNotSupportUpdate": "La plataforma actual no soporta actualizaciones, por favor instala manualmente la última versión del código fuente", + "plugInType": "Tipo de inserción", "plzEnterHost": "Por favor, introduce el host", "plzSelectKey": "Por favor, selecciona una llave privada", "port": "Puerto", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f9b47897d..99c4ce751 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -48,6 +48,7 @@ "containerName": "Nom du conteneur", "containerStatus": "Statut du conteneur", "containerTrySudoTip": "Par exemple : dans l'application, l'utilisateur est défini comme aaa, mais Docker est installé en tant qu'utilisateur root. Dans ce cas, vous devez activer cette option.", + "content": "Contenu", "convert": "Convertir", "copy": "Copier", "copyPath": "Copier le chemin", @@ -185,6 +186,7 @@ "pingNoServer": "Aucun serveur pour ping.\nVeuillez ajouter un serveur dans l'onglet serveur.", "pkg": "Pkg", "platformNotSupportUpdate": "La plateforme actuelle ne prend pas en charge la mise à jour dans l'application. \nVeuillez le compiler à partir de la source et l'installer.", + "plugInType": "Type d'insertion", "plzEnterHost": "Veuillez saisir l'hôte.", "plzSelectKey": "Veuillez sélectionner une clé.", "port": "Port", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 43e819faf..5885e0c46 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -48,6 +48,7 @@ "containerName": "Nama kontainer", "containerStatus": "Status wadah", "containerTrySudoTip": "Contohnya: Di dalam aplikasi, pengguna diatur sebagai aaa, tetapi Docker diinstal di bawah pengguna root. Dalam kasus ini, Anda perlu mengaktifkan opsi ini.", + "content": "Konten", "convert": "Mengubah", "copy": "Menyalin", "copyPath": "Path Copy", @@ -185,6 +186,7 @@ "pingNoServer": "Tidak ada server untuk melakukan ping.\nHarap tambahkan server di tab Server.", "pkg": "Pkg", "platformNotSupportUpdate": "Platform saat ini tidak mendukung pembaruan aplikasi.\nSilakan bangun dari sumber dan instal.", + "plugInType": "Jenis Penyisipan", "plzEnterHost": "Harap masukkan host.", "plzSelectKey": "Pilih kunci.", "port": "Port", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 4d972f039..2cb3a2379 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -48,6 +48,7 @@ "containerName": "コンテナ名", "containerStatus": "コンテナの状態", "containerTrySudoTip": "例:アプリ内でユーザーをaaaに設定しているが、Dockerがrootユーザーでインストールされている場合、このオプションを有効にする必要があります", + "content": "コンテンツ", "convert": "変換", "copy": "コピー", "copyPath": "パスをコピー", @@ -185,6 +186,7 @@ "pingNoServer": "Pingに使用するサーバーがありません\nサーバータブでサーバーを追加してから再試行してください", "pkg": "パッケージ管理", "platformNotSupportUpdate": "現在のプラットフォームは更新をサポートしていません。最新のソースコードをコンパイルして手動でインストールしてください", + "plugInType": "挿入タイプ", "plzEnterHost": "ホストを入力してください", "plzSelectKey": "プライベートキーを選択してください", "port": "ポート", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 11c9dd6c9..5b01a663a 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -48,6 +48,7 @@ "containerName": "Nome do contêiner", "containerStatus": "Estado do contêiner", "containerTrySudoTip": "Por exemplo: se o usuário for definido como aaa dentro do app, mas o Docker estiver instalado sob o usuário root, esta opção precisará ser ativada", + "content": "Conteúdo", "convert": "Converter", "copy": "Copiar", "copyPath": "Copiar caminho", @@ -185,6 +186,7 @@ "pingNoServer": "Nenhum servidor disponível para Ping\nPor favor, adicione um servidor na aba de servidores e tente novamente", "pkg": "Gerenciamento de pacotes", "platformNotSupportUpdate": "Atualização não suportada na plataforma atual, por favor, instale manualmente a versão mais recente do código-fonte", + "plugInType": "Tipo de Inserção", "plzEnterHost": "Por favor, insira o host", "plzSelectKey": "Por favor, selecione uma chave privada", "port": "Porta", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index a5feb3d7e..701f26f66 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -48,6 +48,7 @@ "containerName": "имя контейнера", "containerStatus": "статус контейнера", "containerTrySudoTip": "Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию", + "content": "Содержимое", "convert": "конвертировать", "copy": "копировать", "copyPath": "копировать путь", @@ -185,6 +186,7 @@ "pingNoServer": "Нет доступных серверов для Ping\nПожалуйста, добавьте серверы на вкладке серверов и попробуйте снова", "pkg": "менеджер пакетов", "platformNotSupportUpdate": "Текущая платформа не поддерживает обновления, пожалуйста, вручную установите последнюю версию из исходного кода", + "plugInType": "Тип вставки", "plzEnterHost": "Пожалуйста, введите хост", "plzSelectKey": "Пожалуйста, выберите ключ", "port": "порт", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 0181ee492..5eac51e45 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -48,6 +48,7 @@ "containerName": "容器名", "containerStatus": "容器状态", "containerTrySudoTip": "例如:在应用内将用户设置为aaa,但是Docker安装在root用户下,这时就需要启用此选项", + "content": "内容", "convert": "转换", "copy": "复制", "copyPath": "复制路径", @@ -185,6 +186,7 @@ "pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试", "pkg": "包管理", "platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装", + "plugInType": "插入类型", "plzEnterHost": "请输入主机", "plzSelectKey": "请选择私钥", "port": "端口", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 7626d5540..58ebfb3a2 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -48,6 +48,7 @@ "containerName": "容器名稱", "containerStatus": "容器狀態", "containerTrySudoTip": "例如:App内设置用户为aaa,但是Docker安装在root用户,这时就需要开启此选项", + "content": "內容", "convert": "轉換", "copy": "複製", "copyPath": "複製路徑", @@ -185,6 +186,7 @@ "pingNoServer": "沒有服務器可用於Ping\n請在服務器tab新增服務器後再試", "pkg": "包管理", "platformNotSupportUpdate": "當前平台不支持更新,請編譯最新源碼後手動安裝", + "plugInType": "插入類型", "plzEnterHost": "請輸入主機", "plzSelectKey": "請選擇私鑰", "port": "端口", diff --git a/lib/view/page/pve.dart b/lib/view/page/pve.dart index ed902c008..9c8ba9dd5 100644 --- a/lib/view/page/pve.dart +++ b/lib/view/page/pve.dart @@ -1,13 +1,19 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/numx.dart'; import 'package:toolbox/core/extension/widget.dart'; import 'package:toolbox/data/model/server/pve.dart'; import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/provider/pve.dart'; +import 'package:toolbox/data/res/color.dart'; +import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/ui.dart'; import 'package:toolbox/view/widget/appbar.dart'; -import 'package:toolbox/view/widget/future_widget.dart'; +import 'package:toolbox/view/widget/kv_row.dart'; import 'package:toolbox/view/widget/percent_circle.dart'; +import 'package:toolbox/view/widget/two_line_text.dart'; final class PvePage extends StatefulWidget { final ServerPrivateInfo spi; @@ -24,8 +30,9 @@ final class PvePage extends StatefulWidget { const _kHorziPadding = 11.0; final class _PvePageState extends State { - late final pve = PveProvider(spi: widget.spi); + late final _pve = PveProvider(spi: widget.spi); late MediaQueryData _media; + Timer? _timer; @override void didChangeDependencies() { @@ -33,121 +40,272 @@ final class _PvePageState extends State { _media = MediaQuery.of(context); } + @override + void initState() { + super.initState(); + _initRefreshTimer(); + } + + @override + void dispose() { + super.dispose(); + _timer?.cancel(); + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: const CustomAppBar( - title: Text('PVE'), + appBar: CustomAppBar( + title: TwoLineText(up: 'PVE', down: widget.spi.name), + ), + body: ValueListenableBuilder( + valueListenable: _pve.data, + builder: (_, val, __) { + return _buildBody(val); + }, ), - body: _buildBody(), ); } - Widget _buildBody() { - if (pve.err.value != null) { + Widget _buildBody(PveRes? data) { + if (_pve.err.value != null) { return Center( - child: Text('Failed to connect to PVE: ${pve.err.value}'), + child: Text('Failed to connect to PVE: ${_pve.err.value}'), ); } - return FutureWidget( - future: pve.list(), - error: (e, _) { - return Center( - child: Text('Failed to list PVE: $e'), - ); - }, - loading: UIs.centerLoading, - success: (data) { - if (data == null) { - return const Center( - child: Text('No PVE Resource found'), - ); - } - PveResType? lastType; - return ListView.separated( - padding: const EdgeInsets.symmetric( - horizontal: _kHorziPadding, - vertical: 7, - ), - itemCount: data.length + 1, - separatorBuilder: (context, index) { - final type = switch (data[index]) { - final PveNode _ => PveResType.node, - final PveQemu _ => PveResType.qemu, - final PveLxc _ => PveResType.lxc, - final PveStorage _ => PveResType.storage, - final PveSdn _ => PveResType.sdn, - }; - if (type == lastType) { - return UIs.placeholder; - } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 7), - child: Align( - alignment: Alignment.center, - child: Text( - type.toStr, - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.grey, - ), + if (data == null) { + return UIs.centerLoading; + } + + PveResType? lastType; + return ListView.builder( + padding: const EdgeInsets.symmetric( + horizontal: _kHorziPadding, + vertical: 7, + ), + itemCount: data.length * 2, + itemBuilder: (context, index) { + final item = data[index ~/ 2]; + if (index % 2 == 0) { + final type = switch (item) { + final PveNode _ => PveResType.node, + final PveQemu _ => PveResType.qemu, + final PveLxc _ => PveResType.lxc, + final PveStorage _ => PveResType.storage, + final PveSdn _ => PveResType.sdn, + }; + if (type == lastType) { + return UIs.placeholder; + } + lastType = type; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 7), + child: Align( + alignment: Alignment.center, + child: Text( + type.toStr, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey, ), ), - ); - }, - itemBuilder: (context, index) { - if (index == 0) return UIs.placeholder; - final item = data[index - 1]; - switch (item) { - case final PveNode item: - lastType = PveResType.node; - return _buildNode(item); - case final PveQemu item: - lastType = PveResType.qemu; - return _buildQemu(item); - case final PveLxc item: - lastType = PveResType.lxc; - return _buildLxc(item); - case final PveStorage item: - lastType = PveResType.storage; - return _buildStorage(item); - case final PveSdn item: - lastType = PveResType.sdn; - return _buildSdn(item); - } - }, - ); + ), + ); + } + return switch (item) { + final PveNode _ => _buildNode(item), + final PveQemu _ => _buildQemu(item), + final PveLxc _ => _buildLxc(item), + final PveStorage _ => _buildStorage(item), + final PveSdn _ => _buildSdn(item), + }; }, ); } + Widget _buildNode(PveNode item) { + final valueAnim = AlwaysStoppedAnimation(primaryColor); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 13), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text(item.node, style: UIs.text15Bold), + const Spacer(), + Text(item.topRight, style: UIs.text12Grey), + ], + ), + UIs.height13, + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceAround, + // children: [ + // _wrap(PercentCircle(percent: item.cpu / item.maxcpu), 3), + // _wrap(PercentCircle(percent: item.mem / item.maxmem), 3), + // ], + // ), + Row( + children: [ + const Icon(Icons.memory, size: 13, color: Colors.grey), + UIs.width7, + const Text('CPU', style: UIs.text12Grey), + const Spacer(), + Text( + '${(item.cpu * 100).toStringAsFixed(1)} %', + style: UIs.text12Grey, + ), + ], + ), + const SizedBox(height: 3), + LinearProgressIndicator( + value: item.cpu / item.maxcpu, + minHeight: 7, + valueColor: valueAnim, + ), + UIs.height7, + Row( + children: [ + const Icon(Icons.view_agenda, size: 13, color: Colors.grey), + UIs.width7, + const Text('RAM', style: UIs.text12Grey), + const Spacer(), + Text( + '${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}', + style: UIs.text12Grey, + ), + ], + ), + const SizedBox(height: 3), + LinearProgressIndicator( + value: item.mem / item.maxmem, + minHeight: 7, + valueColor: valueAnim, + ), + ], + ), + ).card; + } + Widget _buildQemu(PveQemu item) { return Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - title: Text(item.name), - trailing: Text(item.topRight), + UIs.height13, + Row( + children: [ + UIs.width13, + Text(item.name, style: UIs.text13Bold), + const Spacer(), + Text(item.topRight, style: UIs.text12Grey), + UIs.width13, + ], ), - if (item.isRunning) Row( + if (item.isRunning) + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4), + _wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4), + _wrap( + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${l10n.read}:\n${item.diskread.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ), + const SizedBox(height: 3), + Text( + '${l10n.write}:\n${item.diskwrite.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ) + ], + ), + 4), + _wrap( + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '↓:\n${item.netin.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ), + const SizedBox(height: 3), + Text( + '↑:\n${item.netout.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ) + ], + ), + 4), + ], + ), + UIs.height13, + ], + ).card; + } + + Widget _buildLxc(PveLxc item) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + UIs.height13, + Row( + children: [ + UIs.width13, + Text(item.name, style: UIs.text13Bold), + const Spacer(), + Text(item.topRight, style: UIs.text12Grey), + UIs.width13, + ], + ), + UIs.height7, + Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4), _wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4), - _wrap(PercentCircle(percent: (item.disk / item.maxdisk) * 100), 4), _wrap( Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${l10n.read}:\n${item.diskread.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ), + const SizedBox(height: 3), + Text( + '${l10n.write}:\n${item.diskwrite.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ) + ], + ), + 4), + _wrap( + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - item.netin.bytes2Str, - style: const TextStyle(fontSize: 10, color: Colors.grey), + '↓:\n${item.netin.bytes2Str}', + style: UIs.text11Grey, textAlign: TextAlign.center, ), const SizedBox(height: 3), Text( - item.netout.bytes2Str, - style: const TextStyle(fontSize: 10, color: Colors.grey), + '↑:\n${item.netout.bytes2Str}', + style: UIs.text11Grey, textAlign: TextAlign.center, ) ], @@ -155,29 +313,29 @@ final class _PvePageState extends State { 4), ], ), - if (item.isRunning) UIs.height13, + UIs.height13, ], ).card; } - Widget _buildLxc(PveLxc item) { - return ListTile( - title: Text(item.name), - trailing: Text(item.status), - ).card; - } - - Widget _buildNode(PveNode item) { - return ListTile( - title: Text(item.node), - trailing: Text(item.status), - ).card; - } - Widget _buildStorage(PveStorage item) { - return ListTile( - title: Text(item.storage), - trailing: Text(item.status), + return Padding( + padding: const EdgeInsets.all(13), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text(item.storage, style: UIs.text13Bold), + const Spacer(), + Text(item.status, style: UIs.text12Grey), + ], + ), + UIs.height7, + KvRow(k: l10n.content, v: item.content), + KvRow(k: l10n.plugInType, v: item.plugintype), + ], + ), ).card; } @@ -194,4 +352,14 @@ final class _PvePageState extends State { child: child, ); } + + void _initRefreshTimer() { + _timer = Timer.periodic( + Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()), + (_) { + if (mounted) { + _pve.list(); + } + }); + } } diff --git a/lib/view/widget/kv_row.dart b/lib/view/widget/kv_row.dart new file mode 100644 index 000000000..fb098c933 --- /dev/null +++ b/lib/view/widget/kv_row.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:toolbox/data/res/ui.dart'; + +final class KvRow extends StatelessWidget { + final String k; + final String v; + final void Function()? onTap; + + const KvRow({ + super.key, + required this.k, + required this.v, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(k, style: UIs.text12), + UIs.width7, + Text( + v, + style: UIs.text11Grey, + overflow: TextOverflow.ellipsis, + ), + if (onTap != null) UIs.width7, + if (onTap != null) const Icon(Icons.keyboard_arrow_right, size: 16), + ], + ), + ), + ); + } +}