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:
@@ -28,12 +28,12 @@ enum PveResType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get toStr => switch (this) {
|
String get toStr => switch (this) {
|
||||||
PveResType.node => l10n.node,
|
PveResType.node => l10n.node,
|
||||||
PveResType.qemu => 'QEMU',
|
PveResType.qemu => 'QEMU',
|
||||||
PveResType.lxc => 'LXC',
|
PveResType.lxc => 'LXC',
|
||||||
PveResType.storage => l10n.storage,
|
PveResType.storage => l10n.storage,
|
||||||
PveResType.sdn => 'SDN',
|
PveResType.sdn => 'SDN',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class PveResIface {
|
sealed class PveResIface {
|
||||||
@@ -121,6 +121,15 @@ final class PveLxc extends PveResIface {
|
|||||||
netout: json['netout'],
|
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 {
|
final class PveQemu extends PveResIface {
|
||||||
@@ -190,7 +199,7 @@ final class PveQemu extends PveResIface {
|
|||||||
bool get isRunning => status == 'running';
|
bool get isRunning => status == 'running';
|
||||||
|
|
||||||
String get topRight {
|
String get topRight {
|
||||||
if (!isRunning) {
|
if (isRunning) {
|
||||||
return uptime.secondsToDuration().toStr;
|
return uptime.secondsToDuration().toStr;
|
||||||
}
|
}
|
||||||
return l10n.stopped;
|
return l10n.stopped;
|
||||||
@@ -236,6 +245,15 @@ final class PveNode extends PveResIface {
|
|||||||
maxcpu: json['maxcpu'],
|
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 {
|
final class PveStorage extends PveResIface {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.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/pve.dart';
|
||||||
import 'package:toolbox/data/model/server/server_private_info.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 String addr;
|
||||||
//late final SSHClient _client;
|
//late final SSHClient _client;
|
||||||
|
|
||||||
|
final data = ValueNotifier<PveRes?>(null);
|
||||||
|
|
||||||
PveProvider({
|
PveProvider({
|
||||||
required this.spi,
|
required this.spi,
|
||||||
}) {
|
}) {
|
||||||
@@ -77,11 +80,12 @@ final class PveProvider extends ChangeNotifier {
|
|||||||
final resp = await session.get('$addr/api2/json/cluster/resources');
|
final resp = await session.get('$addr/api2/json/cluster/resources');
|
||||||
final list = resp.data['data'] as List;
|
final list = resp.data['data'] as List;
|
||||||
final items = list.map((e) => PveResIface.fromJson(e)).toList();
|
final items = list.map((e) => PveResIface.fromJson(e)).toList();
|
||||||
final qemus = <PveQemu>[];
|
|
||||||
final lxcs = <PveLxc>[];
|
final Order<PveQemu> qemus = [];
|
||||||
final nodes = <PveNode>[];
|
final Order<PveLxc> lxcs = [];
|
||||||
final storages = <PveStorage>[];
|
final Order<PveNode> nodes = [];
|
||||||
final sdns = <PveSdn>[];
|
final Order<PveStorage> storages = [];
|
||||||
|
final Order<PveSdn> sdns = [];
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case PveResType.lxc:
|
case PveResType.lxc:
|
||||||
@@ -101,12 +105,34 @@ final class PveProvider extends ChangeNotifier {
|
|||||||
break;
|
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,
|
qemus: qemus,
|
||||||
lxcs: lxcs,
|
lxcs: lxcs,
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
storages: storages,
|
storages: storages,
|
||||||
sdns: sdns,
|
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 text13Grey = TextStyle(color: Colors.grey, fontSize: 13);
|
||||||
static const text15 = TextStyle(fontSize: 15);
|
static const text15 = TextStyle(fontSize: 15);
|
||||||
|
static const text15Bold = TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
static const text18 = TextStyle(fontSize: 18);
|
static const text18 = TextStyle(fontSize: 18);
|
||||||
static const text27 = TextStyle(fontSize: 27);
|
static const text27 = TextStyle(fontSize: 27);
|
||||||
static const textGrey = TextStyle(color: Colors.grey);
|
static const textGrey = TextStyle(color: Colors.grey);
|
||||||
@@ -39,6 +43,7 @@ abstract final class UIs {
|
|||||||
/// SizedBox
|
/// SizedBox
|
||||||
|
|
||||||
static const placeholder = SizedBox();
|
static const placeholder = SizedBox();
|
||||||
|
static const height7 = SizedBox(height: 7);
|
||||||
static const height13 = SizedBox(height: 13);
|
static const height13 = SizedBox(height: 13);
|
||||||
static const height77 = SizedBox(height: 77);
|
static const height77 = SizedBox(height: 77);
|
||||||
static const width13 = SizedBox(width: 13);
|
static const width13 = SizedBox(width: 13);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "Container Name",
|
"containerName": "Container Name",
|
||||||
"containerStatus": "Container Status",
|
"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",
|
"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",
|
"convert": "Konvertieren",
|
||||||
"copy": "Kopieren",
|
"copy": "Kopieren",
|
||||||
"copyPath": "Pfad kopieren",
|
"copyPath": "Pfad kopieren",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "Kein Server zum Anpingen.\nBitte füge einen Server hinzu.",
|
"pingNoServer": "Kein Server zum Anpingen.\nBitte füge einen Server hinzu.",
|
||||||
"pkg": "Pkg",
|
"pkg": "Pkg",
|
||||||
"platformNotSupportUpdate": "Die aktuelle Plattform unterstützt keine In-App-Updates.\nBitte kompiliere vom Quellcode und installiere sie.",
|
"platformNotSupportUpdate": "Die aktuelle Plattform unterstützt keine In-App-Updates.\nBitte kompiliere vom Quellcode und installiere sie.",
|
||||||
|
"plugInType": "Einfügetyp",
|
||||||
"plzEnterHost": "Bitte Host eingeben.",
|
"plzEnterHost": "Bitte Host eingeben.",
|
||||||
"plzSelectKey": "Wähle einen Key.",
|
"plzSelectKey": "Wähle einen Key.",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "Container name",
|
"containerName": "Container name",
|
||||||
"containerStatus": "Container status",
|
"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.",
|
"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",
|
"convert": "Convert",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copyPath": "Copy path",
|
"copyPath": "Copy path",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
|
"pingNoServer": "No server to ping.\nPlease add a server in server tab.",
|
||||||
"pkg": "Pkg",
|
"pkg": "Pkg",
|
||||||
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
|
"platformNotSupportUpdate": "Current platform does not support in app update.\nPlease build from source and install it.",
|
||||||
|
"plugInType": "Insertion Type",
|
||||||
"plzEnterHost": "Please enter host.",
|
"plzEnterHost": "Please enter host.",
|
||||||
"plzSelectKey": "Please select a key.",
|
"plzSelectKey": "Please select a key.",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "Nombre del contenedor",
|
"containerName": "Nombre del contenedor",
|
||||||
"containerStatus": "Estado 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",
|
"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",
|
"convert": "Convertir",
|
||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copyPath": "Copiar ruta",
|
"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",
|
"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",
|
"pkg": "Gestión de paquetes",
|
||||||
"platformNotSupportUpdate": "La plataforma actual no soporta actualizaciones, por favor instala manualmente la última versión del código fuente",
|
"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",
|
"plzEnterHost": "Por favor, introduce el host",
|
||||||
"plzSelectKey": "Por favor, selecciona una llave privada",
|
"plzSelectKey": "Por favor, selecciona una llave privada",
|
||||||
"port": "Puerto",
|
"port": "Puerto",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "Nom du conteneur",
|
"containerName": "Nom du conteneur",
|
||||||
"containerStatus": "Statut 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.",
|
"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",
|
"convert": "Convertir",
|
||||||
"copy": "Copier",
|
"copy": "Copier",
|
||||||
"copyPath": "Copier le chemin",
|
"copyPath": "Copier le chemin",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "Aucun serveur pour ping.\nVeuillez ajouter un serveur dans l'onglet serveur.",
|
"pingNoServer": "Aucun serveur pour ping.\nVeuillez ajouter un serveur dans l'onglet serveur.",
|
||||||
"pkg": "Pkg",
|
"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.",
|
"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.",
|
"plzEnterHost": "Veuillez saisir l'hôte.",
|
||||||
"plzSelectKey": "Veuillez sélectionner une clé.",
|
"plzSelectKey": "Veuillez sélectionner une clé.",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "Nama kontainer",
|
"containerName": "Nama kontainer",
|
||||||
"containerStatus": "Status wadah",
|
"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.",
|
"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",
|
"convert": "Mengubah",
|
||||||
"copy": "Menyalin",
|
"copy": "Menyalin",
|
||||||
"copyPath": "Path Copy",
|
"copyPath": "Path Copy",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "Tidak ada server untuk melakukan ping.\nHarap tambahkan server di tab Server.",
|
"pingNoServer": "Tidak ada server untuk melakukan ping.\nHarap tambahkan server di tab Server.",
|
||||||
"pkg": "Pkg",
|
"pkg": "Pkg",
|
||||||
"platformNotSupportUpdate": "Platform saat ini tidak mendukung pembaruan aplikasi.\nSilakan bangun dari sumber dan instal.",
|
"platformNotSupportUpdate": "Platform saat ini tidak mendukung pembaruan aplikasi.\nSilakan bangun dari sumber dan instal.",
|
||||||
|
"plugInType": "Jenis Penyisipan",
|
||||||
"plzEnterHost": "Harap masukkan host.",
|
"plzEnterHost": "Harap masukkan host.",
|
||||||
"plzSelectKey": "Pilih kunci.",
|
"plzSelectKey": "Pilih kunci.",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "コンテナ名",
|
"containerName": "コンテナ名",
|
||||||
"containerStatus": "コンテナの状態",
|
"containerStatus": "コンテナの状態",
|
||||||
"containerTrySudoTip": "例:アプリ内でユーザーをaaaに設定しているが、Dockerがrootユーザーでインストールされている場合、このオプションを有効にする必要があります",
|
"containerTrySudoTip": "例:アプリ内でユーザーをaaaに設定しているが、Dockerがrootユーザーでインストールされている場合、このオプションを有効にする必要があります",
|
||||||
|
"content": "コンテンツ",
|
||||||
"convert": "変換",
|
"convert": "変換",
|
||||||
"copy": "コピー",
|
"copy": "コピー",
|
||||||
"copyPath": "パスをコピー",
|
"copyPath": "パスをコピー",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "Pingに使用するサーバーがありません\nサーバータブでサーバーを追加してから再試行してください",
|
"pingNoServer": "Pingに使用するサーバーがありません\nサーバータブでサーバーを追加してから再試行してください",
|
||||||
"pkg": "パッケージ管理",
|
"pkg": "パッケージ管理",
|
||||||
"platformNotSupportUpdate": "現在のプラットフォームは更新をサポートしていません。最新のソースコードをコンパイルして手動でインストールしてください",
|
"platformNotSupportUpdate": "現在のプラットフォームは更新をサポートしていません。最新のソースコードをコンパイルして手動でインストールしてください",
|
||||||
|
"plugInType": "挿入タイプ",
|
||||||
"plzEnterHost": "ホストを入力してください",
|
"plzEnterHost": "ホストを入力してください",
|
||||||
"plzSelectKey": "プライベートキーを選択してください",
|
"plzSelectKey": "プライベートキーを選択してください",
|
||||||
"port": "ポート",
|
"port": "ポート",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "Nome do contêiner",
|
"containerName": "Nome do contêiner",
|
||||||
"containerStatus": "Estado 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",
|
"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",
|
"convert": "Converter",
|
||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
"copyPath": "Copiar caminho",
|
"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",
|
"pingNoServer": "Nenhum servidor disponível para Ping\nPor favor, adicione um servidor na aba de servidores e tente novamente",
|
||||||
"pkg": "Gerenciamento de pacotes",
|
"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",
|
"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",
|
"plzEnterHost": "Por favor, insira o host",
|
||||||
"plzSelectKey": "Por favor, selecione uma chave privada",
|
"plzSelectKey": "Por favor, selecione uma chave privada",
|
||||||
"port": "Porta",
|
"port": "Porta",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "имя контейнера",
|
"containerName": "имя контейнера",
|
||||||
"containerStatus": "статус контейнера",
|
"containerStatus": "статус контейнера",
|
||||||
"containerTrySudoTip": "Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию",
|
"containerTrySudoTip": "Например: если пользователь в приложении установлен как aaa, но Docker установлен под пользователем root, тогда нужно включить эту опцию",
|
||||||
|
"content": "Содержимое",
|
||||||
"convert": "конвертировать",
|
"convert": "конвертировать",
|
||||||
"copy": "копировать",
|
"copy": "копировать",
|
||||||
"copyPath": "копировать путь",
|
"copyPath": "копировать путь",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "Нет доступных серверов для Ping\nПожалуйста, добавьте серверы на вкладке серверов и попробуйте снова",
|
"pingNoServer": "Нет доступных серверов для Ping\nПожалуйста, добавьте серверы на вкладке серверов и попробуйте снова",
|
||||||
"pkg": "менеджер пакетов",
|
"pkg": "менеджер пакетов",
|
||||||
"platformNotSupportUpdate": "Текущая платформа не поддерживает обновления, пожалуйста, вручную установите последнюю версию из исходного кода",
|
"platformNotSupportUpdate": "Текущая платформа не поддерживает обновления, пожалуйста, вручную установите последнюю версию из исходного кода",
|
||||||
|
"plugInType": "Тип вставки",
|
||||||
"plzEnterHost": "Пожалуйста, введите хост",
|
"plzEnterHost": "Пожалуйста, введите хост",
|
||||||
"plzSelectKey": "Пожалуйста, выберите ключ",
|
"plzSelectKey": "Пожалуйста, выберите ключ",
|
||||||
"port": "порт",
|
"port": "порт",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "容器名",
|
"containerName": "容器名",
|
||||||
"containerStatus": "容器状态",
|
"containerStatus": "容器状态",
|
||||||
"containerTrySudoTip": "例如:在应用内将用户设置为aaa,但是Docker安装在root用户下,这时就需要启用此选项",
|
"containerTrySudoTip": "例如:在应用内将用户设置为aaa,但是Docker安装在root用户下,这时就需要启用此选项",
|
||||||
|
"content": "内容",
|
||||||
"convert": "转换",
|
"convert": "转换",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"copyPath": "复制路径",
|
"copyPath": "复制路径",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
|
"pingNoServer": "没有服务器可用于Ping\n请在服务器tab添加服务器后再试",
|
||||||
"pkg": "包管理",
|
"pkg": "包管理",
|
||||||
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
|
"platformNotSupportUpdate": "当前平台不支持更新,请编译最新源码后手动安装",
|
||||||
|
"plugInType": "插入类型",
|
||||||
"plzEnterHost": "请输入主机",
|
"plzEnterHost": "请输入主机",
|
||||||
"plzSelectKey": "请选择私钥",
|
"plzSelectKey": "请选择私钥",
|
||||||
"port": "端口",
|
"port": "端口",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"containerName": "容器名稱",
|
"containerName": "容器名稱",
|
||||||
"containerStatus": "容器狀態",
|
"containerStatus": "容器狀態",
|
||||||
"containerTrySudoTip": "例如:App内设置用户为aaa,但是Docker安装在root用户,这时就需要开启此选项",
|
"containerTrySudoTip": "例如:App内设置用户为aaa,但是Docker安装在root用户,这时就需要开启此选项",
|
||||||
|
"content": "內容",
|
||||||
"convert": "轉換",
|
"convert": "轉換",
|
||||||
"copy": "複製",
|
"copy": "複製",
|
||||||
"copyPath": "複製路徑",
|
"copyPath": "複製路徑",
|
||||||
@@ -185,6 +186,7 @@
|
|||||||
"pingNoServer": "沒有服務器可用於Ping\n請在服務器tab新增服務器後再試",
|
"pingNoServer": "沒有服務器可用於Ping\n請在服務器tab新增服務器後再試",
|
||||||
"pkg": "包管理",
|
"pkg": "包管理",
|
||||||
"platformNotSupportUpdate": "當前平台不支持更新,請編譯最新源碼後手動安裝",
|
"platformNotSupportUpdate": "當前平台不支持更新,請編譯最新源碼後手動安裝",
|
||||||
|
"plugInType": "插入類型",
|
||||||
"plzEnterHost": "請輸入主機",
|
"plzEnterHost": "請輸入主機",
|
||||||
"plzSelectKey": "請選擇私鑰",
|
"plzSelectKey": "請選擇私鑰",
|
||||||
"port": "端口",
|
"port": "端口",
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/numx.dart';
|
||||||
import 'package:toolbox/core/extension/widget.dart';
|
import 'package:toolbox/core/extension/widget.dart';
|
||||||
import 'package:toolbox/data/model/server/pve.dart';
|
import 'package:toolbox/data/model/server/pve.dart';
|
||||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||||
import 'package:toolbox/data/provider/pve.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/data/res/ui.dart';
|
||||||
import 'package:toolbox/view/widget/appbar.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/percent_circle.dart';
|
||||||
|
import 'package:toolbox/view/widget/two_line_text.dart';
|
||||||
|
|
||||||
final class PvePage extends StatefulWidget {
|
final class PvePage extends StatefulWidget {
|
||||||
final ServerPrivateInfo spi;
|
final ServerPrivateInfo spi;
|
||||||
@@ -24,8 +30,9 @@ final class PvePage extends StatefulWidget {
|
|||||||
const _kHorziPadding = 11.0;
|
const _kHorziPadding = 11.0;
|
||||||
|
|
||||||
final class _PvePageState extends State<PvePage> {
|
final class _PvePageState extends State<PvePage> {
|
||||||
late final pve = PveProvider(spi: widget.spi);
|
late final _pve = PveProvider(spi: widget.spi);
|
||||||
late MediaQueryData _media;
|
late MediaQueryData _media;
|
||||||
|
Timer? _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
@@ -33,121 +40,272 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
_media = MediaQuery.of(context);
|
_media = MediaQuery.of(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_timer?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: Text('PVE'),
|
title: TwoLineText(up: 'PVE', down: widget.spi.name),
|
||||||
|
),
|
||||||
|
body: ValueListenableBuilder(
|
||||||
|
valueListenable: _pve.data,
|
||||||
|
builder: (_, val, __) {
|
||||||
|
return _buildBody(val);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
body: _buildBody(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody(PveRes? data) {
|
||||||
if (pve.err.value != null) {
|
if (_pve.err.value != null) {
|
||||||
return Center(
|
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;
|
if (data == null) {
|
||||||
return ListView.separated(
|
return UIs.centerLoading;
|
||||||
padding: const EdgeInsets.symmetric(
|
}
|
||||||
horizontal: _kHorziPadding,
|
|
||||||
vertical: 7,
|
PveResType? lastType;
|
||||||
),
|
return ListView.builder(
|
||||||
itemCount: data.length + 1,
|
padding: const EdgeInsets.symmetric(
|
||||||
separatorBuilder: (context, index) {
|
horizontal: _kHorziPadding,
|
||||||
final type = switch (data[index]) {
|
vertical: 7,
|
||||||
final PveNode _ => PveResType.node,
|
),
|
||||||
final PveQemu _ => PveResType.qemu,
|
itemCount: data.length * 2,
|
||||||
final PveLxc _ => PveResType.lxc,
|
itemBuilder: (context, index) {
|
||||||
final PveStorage _ => PveResType.storage,
|
final item = data[index ~/ 2];
|
||||||
final PveSdn _ => PveResType.sdn,
|
if (index % 2 == 0) {
|
||||||
};
|
final type = switch (item) {
|
||||||
if (type == lastType) {
|
final PveNode _ => PveResType.node,
|
||||||
return UIs.placeholder;
|
final PveQemu _ => PveResType.qemu,
|
||||||
}
|
final PveLxc _ => PveResType.lxc,
|
||||||
return Padding(
|
final PveStorage _ => PveResType.storage,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
final PveSdn _ => PveResType.sdn,
|
||||||
child: Align(
|
};
|
||||||
alignment: Alignment.center,
|
if (type == lastType) {
|
||||||
child: Text(
|
return UIs.placeholder;
|
||||||
type.toStr,
|
}
|
||||||
style: const TextStyle(
|
lastType = type;
|
||||||
fontWeight: FontWeight.bold,
|
return Padding(
|
||||||
color: Colors.grey,
|
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;
|
return switch (item) {
|
||||||
final item = data[index - 1];
|
final PveNode _ => _buildNode(item),
|
||||||
switch (item) {
|
final PveQemu _ => _buildQemu(item),
|
||||||
case final PveNode item:
|
final PveLxc _ => _buildLxc(item),
|
||||||
lastType = PveResType.node;
|
final PveStorage _ => _buildStorage(item),
|
||||||
return _buildNode(item);
|
final PveSdn _ => _buildSdn(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);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
Widget _buildQemu(PveQemu item) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
UIs.height13,
|
||||||
title: Text(item.name),
|
Row(
|
||||||
trailing: Text(item.topRight),
|
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,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4),
|
_wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4),
|
||||||
_wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4),
|
_wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4),
|
||||||
_wrap(PercentCircle(percent: (item.disk / item.maxdisk) * 100), 4),
|
|
||||||
_wrap(
|
_wrap(
|
||||||
Column(
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.netin.bytes2Str,
|
'${l10n.read}:\n${item.diskread.bytes2Str}',
|
||||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
style: UIs.text11Grey,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text(
|
||||||
item.netout.bytes2Str,
|
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
|
||||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
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,
|
textAlign: TextAlign.center,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -155,29 +313,29 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
4),
|
4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (item.isRunning) UIs.height13,
|
UIs.height13,
|
||||||
],
|
],
|
||||||
).card;
|
).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) {
|
Widget _buildStorage(PveStorage item) {
|
||||||
return ListTile(
|
return Padding(
|
||||||
title: Text(item.storage),
|
padding: const EdgeInsets.all(13),
|
||||||
trailing: Text(item.status),
|
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;
|
).card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,4 +352,14 @@ final class _PvePageState extends State<PvePage> {
|
|||||||
child: child,
|
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