mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
new: pve dashboard (#307)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<PveRes?>(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 = <PveQemu>[];
|
||||
final lxcs = <PveLxc>[];
|
||||
final nodes = <PveNode>[];
|
||||
final storages = <PveStorage>[];
|
||||
final sdns = <PveSdn>[];
|
||||
|
||||
final Order<PveQemu> qemus = [];
|
||||
final Order<PveLxc> lxcs = [];
|
||||
final Order<PveNode> nodes = [];
|
||||
final Order<PveStorage> storages = [];
|
||||
final Order<PveSdn> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ポート",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "порт",
|
||||
|
||||
@@ -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": "端口",
|
||||
|
||||
@@ -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": "端口",
|
||||
|
||||
@@ -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<PvePage> {
|
||||
late final pve = PveProvider(spi: widget.spi);
|
||||
late final _pve = PveProvider(spi: widget.spi);
|
||||
late MediaQueryData _media;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -33,46 +40,55 @@ final class _PvePageState extends State<PvePage> {
|
||||
_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'),
|
||||
);
|
||||
return UIs.centerLoading;
|
||||
}
|
||||
|
||||
PveResType? lastType;
|
||||
return ListView.separated(
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _kHorziPadding,
|
||||
vertical: 7,
|
||||
),
|
||||
itemCount: data.length + 1,
|
||||
separatorBuilder: (context, index) {
|
||||
final type = switch (data[index]) {
|
||||
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,
|
||||
@@ -82,6 +98,7 @@ final class _PvePageState extends State<PvePage> {
|
||||
if (type == lastType) {
|
||||
return UIs.placeholder;
|
||||
}
|
||||
lastType = type;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
child: Align(
|
||||
@@ -95,59 +112,135 @@ final class _PvePageState extends State<PvePage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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(PercentCircle(percent: (item.disk / item.maxdisk) * 100), 4),
|
||||
_wrap(
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item.netin.bytes2Str,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
'${l10n.read}:\n${item.diskread.bytes2Str}',
|
||||
style: UIs.text11Grey,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
item.netout.bytes2Str,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
'${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,
|
||||
)
|
||||
],
|
||||
@@ -155,29 +248,94 @@ final class _PvePageState extends State<PvePage> {
|
||||
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),
|
||||
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(
|
||||
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 _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<PvePage> {
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
void _initRefreshTimer() {
|
||||
_timer = Timer.periodic(
|
||||
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch()),
|
||||
(_) {
|
||||
if (mounted) {
|
||||
_pve.list();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
39
lib/view/widget/kv_row.dart
Normal file
39
lib/view/widget/kv_row.dart
Normal file
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user