This commit is contained in:
lollipopkit
2024-03-18 18:34:25 +08:00
parent 8bfb0eb9e0
commit 26264ecdea
31 changed files with 1114 additions and 56 deletions

View File

@@ -0,0 +1,20 @@
import 'package:toolbox/core/extension/context/locale.dart';
extension DurationX on Duration {
String get toStr {
final days = inDays;
if (days > 0) {
return '$days ${l10n.day}';
}
final hours = inHours % 24;
if (hours > 0) {
return '$hours ${l10n.hour}';
}
final minutes = inMinutes % 60;
if (minutes > 0) {
return '$minutes ${l10n.minute}';
}
final seconds = inSeconds % 60;
return '$seconds ${l10n.second}';
}
}

View File

@@ -33,3 +33,8 @@ extension BigIntX on BigInt {
String get kb2Str => (this * BigInt.from(1024)).bytes2Str; String get kb2Str => (this * BigInt.from(1024)).bytes2Str;
} }
extension IntX on int {
Duration secondsToDuration() => Duration(seconds: this);
DateTime get tsToDateTime => DateTime.fromMillisecondsSinceEpoch(this * 1000);
}

View File

@@ -1,3 +1,4 @@
import 'package:toolbox/core/extension/listx.dart';
import 'package:toolbox/core/persistant_store.dart'; import 'package:toolbox/core/persistant_store.dart';
typedef Order<T> = List<T>; typedef Order<T> = List<T>;
@@ -52,7 +53,7 @@ extension OrderX<T> on Order<T> {
move(index, newIndex, property: property, onMove: onMove); move(index, newIndex, property: property, onMove: onMove);
} }
/// order: ['d', 'b', 'e']\ /// order: ['d', 'b', 'e']
/// this: ['a', 'b', 'c', 'd']\ /// this: ['a', 'b', 'c', 'd']\
/// result: ['d', 'b', 'a', 'c']\ /// result: ['d', 'b', 'a', 'c']\
/// return: ['e'] /// return: ['e']
@@ -64,11 +65,11 @@ extension OrderX<T> on Order<T> {
final missed = <T>[]; final missed = <T>[];
final surplus = <String>[]; final surplus = <String>[];
for (final id in order.toSet()) { for (final id in order.toSet()) {
try { final item = firstWhereOrNull((element) => finder(element, id));
final item = firstWhere((e) => finder(e, id)); if (item == null) {
newOrder.add(item);
} catch (e) {
surplus.add(id); surplus.add(id);
} else {
newOrder.add(item);
} }
} }
for (final item in this) { for (final item in this) {

View File

@@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
extension WidgetX on Widget {
Widget get card {
return Card(child: this);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:toolbox/view/page/iperf.dart';
import 'package:toolbox/view/page/ping.dart'; import 'package:toolbox/view/page/ping.dart';
import 'package:toolbox/view/page/private_key/edit.dart'; import 'package:toolbox/view/page/private_key/edit.dart';
import 'package:toolbox/view/page/private_key/list.dart'; import 'package:toolbox/view/page/private_key/list.dart';
import 'package:toolbox/view/page/pve.dart';
import 'package:toolbox/view/page/server/detail.dart'; import 'package:toolbox/view/page/server/detail.dart';
import 'package:toolbox/view/page/setting/platform/android.dart'; import 'package:toolbox/view/page/setting/platform/android.dart';
import 'package:toolbox/view/page/setting/platform/ios.dart'; import 'package:toolbox/view/page/setting/platform/ios.dart';
@@ -227,4 +228,8 @@ class AppRoute {
static AppRoute serverFuncBtnsOrder({Key? key}) { static AppRoute serverFuncBtnsOrder({Key? key}) {
return AppRoute(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq'); return AppRoute(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
} }
static AppRoute pve({Key? key, required ServerPrivateInfo spi}) {
return AppRoute(PvePage(key: key, spi: spi), 'pve');
}
} }

View File

@@ -20,6 +20,8 @@ enum ServerFuncBtn {
snippet, snippet,
@HiveField(6) @HiveField(6)
iperf, iperf,
@HiveField(7)
pve,
; ;
IconData get icon => switch (this) { IconData get icon => switch (this) {
@@ -30,6 +32,7 @@ enum ServerFuncBtn {
process => Icons.list_alt_outlined, process => Icons.list_alt_outlined,
terminal => Icons.terminal, terminal => Icons.terminal,
iperf => Icons.speed, iperf => Icons.speed,
pve => Icons.computer,
}; };
String get toStr => switch (this) { String get toStr => switch (this) {
@@ -40,6 +43,7 @@ enum ServerFuncBtn {
process => l10n.process, process => l10n.process,
terminal => l10n.terminal, terminal => l10n.terminal,
iperf => 'iperf', iperf => 'iperf',
pve => 'PVE',
}; };
int toJson() => index; int toJson() => index;

View File

@@ -27,6 +27,8 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
return ServerFuncBtn.snippet; return ServerFuncBtn.snippet;
case 6: case 6:
return ServerFuncBtn.iperf; return ServerFuncBtn.iperf;
case 7:
return ServerFuncBtn.pve;
default: default:
return ServerFuncBtn.terminal; return ServerFuncBtn.terminal;
} }
@@ -56,6 +58,9 @@ class ServerFuncBtnAdapter extends TypeAdapter<ServerFuncBtn> {
case ServerFuncBtn.iperf: case ServerFuncBtn.iperf:
writer.writeByte(6); writer.writeByte(6);
break; break;
case ServerFuncBtn.pve:
writer.writeByte(7);
break;
} }
} }

View File

@@ -0,0 +1,36 @@
import 'package:hive_flutter/adapters.dart';
part 'custom.g.dart';
@HiveType(typeId: 7)
final class ServerCustom {
@HiveField(0)
final String? temperature;
@HiveField(1)
final String? pveAddr;
const ServerCustom({
this.temperature,
this.pveAddr,
});
static ServerCustom fromJson(Map<String, dynamic> json) {
final temperature = json["temperature"] as String?;
final pveAddr = json["pveAddr"] as String?;
return ServerCustom(
temperature: temperature,
pveAddr: pveAddr,
);
}
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (temperature != null) {
json["temperature"] = temperature;
}
if (pveAddr != null) {
json["pveAddr"] = pveAddr;
}
return json;
}
}

View File

@@ -0,0 +1,44 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'custom.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ServerCustomAdapter extends TypeAdapter<ServerCustom> {
@override
final int typeId = 7;
@override
ServerCustom read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerCustom(
temperature: fields[0] as String?,
pveAddr: fields[1] as String?,
);
}
@override
void write(BinaryWriter writer, ServerCustom obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.temperature)
..writeByte(1)
..write(obj.pveAddr);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerCustomAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,351 @@
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/extension/duration.dart';
import 'package:toolbox/core/extension/numx.dart';
enum PveResType {
lxc,
qemu,
node,
storage,
sdn,
;
static PveResType fromString(String type) {
switch (type) {
case 'lxc':
return PveResType.lxc;
case 'qemu':
return PveResType.qemu;
case 'node':
return PveResType.node;
case 'storage':
return PveResType.storage;
case 'sdn':
return PveResType.sdn;
default:
throw Exception('Unknown PveResType: $type');
}
}
String get toStr => switch (this) {
PveResType.node => l10n.node,
PveResType.qemu => 'QEMU',
PveResType.lxc => 'LXC',
PveResType.storage => l10n.storage,
PveResType.sdn => 'SDN',
};
}
sealed class PveResIface {
String get id;
String get status;
PveResType get type;
static PveResIface fromJson(Map<String, dynamic> json) {
final type = PveResType.fromString(json['type']);
switch (type) {
case PveResType.lxc:
return PveLxc.fromJson(json);
case PveResType.qemu:
return PveQemu.fromJson(json);
case PveResType.node:
return PveNode.fromJson(json);
case PveResType.storage:
return PveStorage.fromJson(json);
case PveResType.sdn:
return PveSdn.fromJson(json);
}
}
}
final class PveLxc extends PveResIface {
@override
final String id;
@override
final PveResType type;
final int vmid;
final String node;
final String name;
@override
final String status;
final int uptime;
final int mem;
final int maxmem;
final double cpu;
final int maxcpu;
final int disk;
final int maxdisk;
final int diskread;
final int diskwrite;
final int netin;
final int netout;
PveLxc({
required this.id,
required this.type,
required this.vmid,
required this.node,
required this.name,
required this.status,
required this.uptime,
required this.mem,
required this.maxmem,
required this.cpu,
required this.maxcpu,
required this.disk,
required this.maxdisk,
required this.diskread,
required this.diskwrite,
required this.netin,
required this.netout,
});
static PveLxc fromJson(Map<String, dynamic> json) {
return PveLxc(
id: json['id'],
type: PveResType.lxc,
vmid: json['vmid'],
node: json['node'],
name: json['name'],
status: json['status'],
uptime: json['uptime'],
mem: json['mem'],
maxmem: json['maxmem'],
cpu: (json['cpu'] as num).toDouble(),
maxcpu: json['maxcpu'],
disk: json['disk'],
maxdisk: json['maxdisk'],
diskread: json['diskread'],
diskwrite: json['diskwrite'],
netin: json['netin'],
netout: json['netout'],
);
}
}
final class PveQemu extends PveResIface {
@override
final String id;
@override
final PveResType type;
final int vmid;
final String node;
final String name;
@override
final String status;
final int uptime;
final int mem;
final int maxmem;
final double cpu;
final int maxcpu;
final int disk;
final int maxdisk;
final int diskread;
final int diskwrite;
final int netin;
final int netout;
PveQemu({
required this.id,
required this.type,
required this.vmid,
required this.node,
required this.name,
required this.status,
required this.uptime,
required this.mem,
required this.maxmem,
required this.cpu,
required this.maxcpu,
required this.disk,
required this.maxdisk,
required this.diskread,
required this.diskwrite,
required this.netin,
required this.netout,
});
static PveQemu fromJson(Map<String, dynamic> json) {
return PveQemu(
id: json['id'],
type: PveResType.qemu,
vmid: json['vmid'],
node: json['node'],
name: json['name'],
status: json['status'],
uptime: json['uptime'],
mem: json['mem'],
maxmem: json['maxmem'],
cpu: (json['cpu'] as num).toDouble(),
maxcpu: json['maxcpu'],
disk: json['disk'],
maxdisk: json['maxdisk'],
diskread: json['diskread'],
diskwrite: json['diskwrite'],
netin: json['netin'],
netout: json['netout'],
);
}
bool get isRunning => status == 'running';
String get topRight {
if (!isRunning) {
return uptime.secondsToDuration().toStr;
}
return l10n.stopped;
}
}
final class PveNode extends PveResIface {
@override
final String id;
@override
final PveResType type;
final String node;
@override
final String status;
final int uptime;
final int mem;
final int maxmem;
final double cpu;
final int maxcpu;
PveNode({
required this.id,
required this.type,
required this.node,
required this.status,
required this.uptime,
required this.mem,
required this.maxmem,
required this.cpu,
required this.maxcpu,
});
static PveNode fromJson(Map<String, dynamic> json) {
return PveNode(
id: json['id'],
type: PveResType.node,
node: json['node'],
status: json['status'],
uptime: json['uptime'],
mem: json['mem'],
maxmem: json['maxmem'],
cpu: (json['cpu'] as num).toDouble(),
maxcpu: json['maxcpu'],
);
}
}
final class PveStorage extends PveResIface {
@override
final String id;
@override
final PveResType type;
final String storage;
final String node;
@override
final String status;
final String plugintype;
final String content;
final int shared;
final int disk;
final int maxdisk;
PveStorage({
required this.id,
required this.type,
required this.storage,
required this.node,
required this.status,
required this.plugintype,
required this.content,
required this.shared,
required this.disk,
required this.maxdisk,
});
static PveStorage fromJson(Map<String, dynamic> json) {
return PveStorage(
id: json['id'],
type: PveResType.storage,
storage: json['storage'],
node: json['node'],
status: json['status'],
plugintype: json['plugintype'],
content: json['content'],
shared: json['shared'],
disk: json['disk'],
maxdisk: json['maxdisk'],
);
}
}
final class PveSdn extends PveResIface {
@override
final String id;
@override
final PveResType type;
final String sdn;
final String node;
@override
final String status;
PveSdn({
required this.id,
required this.type,
required this.sdn,
required this.node,
required this.status,
});
static PveSdn fromJson(Map<String, dynamic> json) {
return PveSdn(
id: json['id'],
type: PveResType.sdn,
sdn: json['sdn'],
node: json['node'],
status: json['status'],
);
}
}
final class PveRes {
final List<PveNode> nodes;
final List<PveQemu> qemus;
final List<PveLxc> lxcs;
final List<PveStorage> storages;
final List<PveSdn> sdns;
const PveRes({
required this.nodes,
required this.qemus,
required this.lxcs,
required this.storages,
required this.sdns,
});
int get length =>
qemus.length + lxcs.length + nodes.length + storages.length + sdns.length;
PveResIface operator [](int index) {
if (index < nodes.length) {
return nodes[index];
}
index -= nodes.length;
if (index < qemus.length) {
return qemus[index];
}
index -= qemus.length;
if (index < lxcs.length) {
return lxcs[index];
}
index -= lxcs.length;
if (index < storages.length) {
return storages[index];
}
index -= storages.length;
return sdns[index];
}
}

View File

@@ -1,4 +1,5 @@
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:toolbox/data/model/server/custom.dart';
import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server.dart';
import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/provider.dart';
@@ -6,6 +7,7 @@ import '../app/error.dart';
part 'server_private_info.g.dart'; part 'server_private_info.g.dart';
/// In former version, it's called `ServerPrivateInfo`.
@HiveType(typeId: 3) @HiveType(typeId: 3)
class ServerPrivateInfo { class ServerPrivateInfo {
@HiveField(0) @HiveField(0)
@@ -33,6 +35,9 @@ class ServerPrivateInfo {
@HiveField(9) @HiveField(9)
final String? jumpId; final String? jumpId;
@HiveField(10)
final ServerCustom? custom;
final String id; final String id;
const ServerPrivateInfo({ const ServerPrivateInfo({
@@ -46,6 +51,7 @@ class ServerPrivateInfo {
this.alterUrl, this.alterUrl,
this.autoConnect, this.autoConnect,
this.jumpId, this.jumpId,
this.custom,
}) : id = '$user@$ip:$port'; }) : id = '$user@$ip:$port';
static ServerPrivateInfo fromJson(Map<String, dynamic> json) { static ServerPrivateInfo fromJson(Map<String, dynamic> json) {
@@ -59,6 +65,9 @@ class ServerPrivateInfo {
final alterUrl = json["alterUrl"] as String?; final alterUrl = json["alterUrl"] as String?;
final autoConnect = json["autoConnect"] as bool?; final autoConnect = json["autoConnect"] as bool?;
final jumpId = json["jumpId"] as String?; final jumpId = json["jumpId"] as String?;
final custom = json["customCmd"] == null
? null
: ServerCustom.fromJson(json["custom"].cast<String, dynamic>());
return ServerPrivateInfo( return ServerPrivateInfo(
name: name, name: name,
@@ -71,6 +80,7 @@ class ServerPrivateInfo {
alterUrl: alterUrl, alterUrl: alterUrl,
autoConnect: autoConnect, autoConnect: autoConnect,
jumpId: jumpId, jumpId: jumpId,
custom: custom,
); );
} }
@@ -80,12 +90,27 @@ class ServerPrivateInfo {
data["ip"] = ip; data["ip"] = ip;
data["port"] = port; data["port"] = port;
data["user"] = user; data["user"] = user;
data["authorization"] = pwd; if (pwd != null) {
data["pubKeyId"] = keyId; data["authorization"] = pwd;
data["tags"] = tags; }
data["alterUrl"] = alterUrl; if (keyId != null) {
data["autoConnect"] = autoConnect; data["pubKeyId"] = keyId;
data["jumpId"] = jumpId; }
if (tags != null) {
data["tags"] = tags;
}
if (alterUrl != null) {
data["alterUrl"] = alterUrl;
}
if (autoConnect != null) {
data["autoConnect"] = autoConnect;
}
if (jumpId != null) {
data["jumpId"] = jumpId;
}
if (custom != null) {
data["custom"] = custom?.toJson();
}
return data; return data;
} }

View File

@@ -27,13 +27,14 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
alterUrl: fields[7] as String?, alterUrl: fields[7] as String?,
autoConnect: fields[8] as bool?, autoConnect: fields[8] as bool?,
jumpId: fields[9] as String?, jumpId: fields[9] as String?,
custom: fields[10] as ServerCustom?,
); );
} }
@override @override
void write(BinaryWriter writer, ServerPrivateInfo obj) { void write(BinaryWriter writer, ServerPrivateInfo obj) {
writer writer
..writeByte(10) ..writeByte(11)
..writeByte(0) ..writeByte(0)
..write(obj.name) ..write(obj.name)
..writeByte(1) ..writeByte(1)
@@ -53,7 +54,9 @@ class ServerPrivateInfoAdapter extends TypeAdapter<ServerPrivateInfo> {
..writeByte(8) ..writeByte(8)
..write(obj.autoConnect) ..write(obj.autoConnect)
..writeByte(9) ..writeByte(9)
..write(obj.jumpId); ..write(obj.jumpId)
..writeByte(10)
..write(obj.custom);
} }
@override @override

112
lib/data/provider/pve.dart Normal file
View File

@@ -0,0 +1,112 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/model/server/pve.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
final class PveProvider extends ChangeNotifier {
final ServerPrivateInfo spi;
late final String addr;
//late final SSHClient _client;
PveProvider({
required this.spi,
}) {
// final client = _spi.server?.client;
// if (client == null) {
// throw Exception('Server client is null');
// }
// _client = client;
final addr = spi.custom?.pveAddr;
if (addr == null) {
err.value = 'PVE address is null';
return;
}
this.addr = addr;
_init().then((_) => connected.complete());
}
final err = ValueNotifier<String?>(null);
final connected = Completer<void>();
final session = Dio();
// int _localPort = 0;
// String get addr => 'http://127.0.0.1:$_localPort';
Future<void> _init() async {
//await _forward();
await _login();
}
// Future<void> _forward() async {
// var retries = 0;
// while (retries < 3) {
// try {
// _localPort = Random().nextInt(1000) + 37000;
// print('Forwarding local port $_localPort');
// final serverSocket = await ServerSocket.bind('localhost', _localPort);
// final forward = await _client.forwardLocal('127.0.0.1', 8006);
// serverSocket.listen((socket) {
// forward.stream.cast<List<int>>().pipe(socket);
// socket.pipe(forward.sink);
// });
// return;
// } on SocketException {
// retries++;
// }
// }
// throw Exception('Failed to bind local port');
// }
Future<void> _login() async {
final resp = await session.post('$addr/api2/extjs/access/ticket', data: {
'username': spi.user,
'password': spi.pwd,
'realm': 'pam',
'new-format': '1'
});
final ticket = resp.data['data']['ticket'];
session.options.headers['CSRFPreventionToken'] =
resp.data['data']['CSRFPreventionToken'];
session.options.headers['Cookie'] = 'PVEAuthCookie=$ticket';
}
Future<PveRes> list() async {
await connected.future;
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>[];
for (final item in items) {
switch (item.type) {
case PveResType.lxc:
lxcs.add(item as PveLxc);
break;
case PveResType.qemu:
qemus.add(item as PveQemu);
break;
case PveResType.node:
nodes.add(item as PveNode);
break;
case PveResType.storage:
storages.add(item as PveStorage);
break;
case PveResType.sdn:
sdns.add(item as PveSdn);
break;
}
}
return PveRes(
qemus: qemus,
lxcs: lxcs,
nodes: nodes,
storages: storages,
sdns: sdns,
);
}
}

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "Private key hinzufügen", "addPrivateKey": "Private key hinzufügen",
"addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?", "addSystemPrivateKeyTip": "Derzeit haben Sie keinen privaten Schlüssel, fügen Sie den Schlüssel hinzu, der mit dem System geliefert wird (~/.ssh/id_rsa)?",
"added2List": "Zur Aufgabenliste hinzugefügt", "added2List": "Zur Aufgabenliste hinzugefügt",
"addr": "Adresse",
"all": "Alle", "all": "Alle",
"alreadyLastDir": "Bereits im letzten Verzeichnis.", "alreadyLastDir": "Bereits im letzten Verzeichnis.",
"alterUrl": "Url ändern", "alterUrl": "Url ändern",
@@ -54,6 +55,7 @@
"createFolder": "Ordner erstellen", "createFolder": "Ordner erstellen",
"cursorType": "Cursor-Typ", "cursorType": "Cursor-Typ",
"dark": "Dunkel", "dark": "Dunkel",
"day": "Tag",
"debug": "Debug", "debug": "Debug",
"decode": "Decode", "decode": "Decode",
"decompress": "Dekomprimieren", "decompress": "Dekomprimieren",
@@ -109,6 +111,7 @@
"highlight": "Code highlight", "highlight": "Code highlight",
"homeWidgetUrlConfig": "Home-Widget-Link konfigurieren", "homeWidgetUrlConfig": "Home-Widget-Link konfigurieren",
"host": "Host", "host": "Host",
"hour": "Stunde",
"httpFailedWithCode": "Anfrage fehlgeschlagen, Statuscode: {code}", "httpFailedWithCode": "Anfrage fehlgeschlagen, Statuscode: {code}",
"icloudSynced": "iCloud wird synchronisiert und einige Einstellungen erfordern möglicherweise einen Neustart der App, um wirksam zu werden.", "icloudSynced": "iCloud wird synchronisiert und einige Einstellungen erfordern möglicherweise einen Neustart der App, um wirksam zu werden.",
"image": "Image", "image": "Image",
@@ -143,6 +146,7 @@
"maxRetryCount": "Anzahl an Verbindungsversuchen", "maxRetryCount": "Anzahl an Verbindungsversuchen",
"maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server", "maxRetryCountEqual0": "Unbegrenzte Verbindungsversuche zum Server",
"min": "min", "min": "min",
"minute": "Minute",
"mission": "Mission", "mission": "Mission",
"more": "Mehr", "more": "Mehr",
"moveOutServerFuncBtnsHelp": "Ein: kann unter jeder Karte auf der Registerkarte \"Server\" angezeigt werden. Aus: kann oben auf der Seite \"Serverdetails\" angezeigt werden.", "moveOutServerFuncBtnsHelp": "Ein: kann unter jeder Karte auf der Registerkarte \"Server\" angezeigt werden. Aus: kann oben auf der Seite \"Serverdetails\" angezeigt werden.",
@@ -161,6 +165,7 @@
"noServerAvailable": "Kein Server verfügbar.", "noServerAvailable": "Kein Server verfügbar.",
"noTask": "Nicht fragen", "noTask": "Nicht fragen",
"noUpdateAvailable": "Kein Update verfügbar", "noUpdateAvailable": "Kein Update verfügbar",
"node": "Knoten",
"notSelected": "Nicht ausgewählt", "notSelected": "Nicht ausgewählt",
"note": "Hinweis", "note": "Hinweis",
"nullToken": "Null token", "nullToken": "Null token",
@@ -237,6 +242,7 @@
"stats": "Statistik", "stats": "Statistik",
"stop": "Stop", "stop": "Stop",
"stopped": "Ausgelaufen", "stopped": "Ausgelaufen",
"storage": "Speicher",
"success": "Erfolgreich", "success": "Erfolgreich",
"supportFmtArgs": "Die folgenden Formatierungsparameter werden unterstützt:", "supportFmtArgs": "Die folgenden Formatierungsparameter werden unterstützt:",
"suspend": "Suspend", "suspend": "Suspend",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "Add private key", "addPrivateKey": "Add private key",
"addSystemPrivateKeyTip": "Currently don't have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?", "addSystemPrivateKeyTip": "Currently don't have any private key, do you add the one that comes with the system (~/.ssh/id_rsa)?",
"added2List": "Added to task list", "added2List": "Added to task list",
"addr": "Address",
"all": "All", "all": "All",
"alreadyLastDir": "Already in last directory.", "alreadyLastDir": "Already in last directory.",
"alterUrl": "Alter url", "alterUrl": "Alter url",
@@ -54,6 +55,7 @@
"createFolder": "Create folder", "createFolder": "Create folder",
"cursorType": "Cursor type", "cursorType": "Cursor type",
"dark": "Dark", "dark": "Dark",
"day": "Day",
"debug": "Debug", "debug": "Debug",
"decode": "Decode", "decode": "Decode",
"decompress": "Decompress", "decompress": "Decompress",
@@ -109,6 +111,7 @@
"highlight": "Code highlight", "highlight": "Code highlight",
"homeWidgetUrlConfig": "Config home widget url", "homeWidgetUrlConfig": "Config home widget url",
"host": "Host", "host": "Host",
"hour": "Hour",
"httpFailedWithCode": "request failed, status code: {code}", "httpFailedWithCode": "request failed, status code: {code}",
"icloudSynced": "iCloud wird synchronisiert und einige Einstellungen erfordern möglicherweise einen Neustart der App, um wirksam zu werden.", "icloudSynced": "iCloud wird synchronisiert und einige Einstellungen erfordern möglicherweise einen Neustart der App, um wirksam zu werden.",
"image": "Image", "image": "Image",
@@ -143,6 +146,7 @@
"maxRetryCount": "Number of server reconnection", "maxRetryCount": "Number of server reconnection",
"maxRetryCountEqual0": "Will retry again and again.", "maxRetryCountEqual0": "Will retry again and again.",
"min": "min", "min": "min",
"minute": "Minute",
"mission": "Mission", "mission": "Mission",
"more": "More", "more": "More",
"moveOutServerFuncBtnsHelp": "On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.", "moveOutServerFuncBtnsHelp": "On: can be displayed below each card on the Server Tab page. Off: can be displayed at the top of the Server Details page.",
@@ -161,6 +165,7 @@
"noServerAvailable": "No server available.", "noServerAvailable": "No server available.",
"noTask": "No task", "noTask": "No task",
"noUpdateAvailable": "No update available", "noUpdateAvailable": "No update available",
"node": "Node",
"notSelected": "Not selected", "notSelected": "Not selected",
"note": "Note", "note": "Note",
"nullToken": "Null token", "nullToken": "Null token",
@@ -237,6 +242,7 @@
"stats": "Stats", "stats": "Stats",
"stop": "Stop", "stop": "Stop",
"stopped": "Stopped", "stopped": "Stopped",
"storage": "Storage",
"success": "Success", "success": "Success",
"supportFmtArgs": "The following formatting parameters are supported:", "supportFmtArgs": "The following formatting parameters are supported:",
"suspend": "Suspend", "suspend": "Suspend",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "Agregar una llave privada", "addPrivateKey": "Agregar una llave privada",
"addSystemPrivateKeyTip": "Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?", "addSystemPrivateKeyTip": "Actualmente no hay ninguna llave privada, ¿quieres agregar la que viene por defecto en el sistema (~/.ssh/id_rsa)?",
"added2List": "Añadido a la lista de tareas", "added2List": "Añadido a la lista de tareas",
"addr": "Dirección",
"all": "Todos", "all": "Todos",
"alreadyLastDir": "Ya estás en el directorio superior", "alreadyLastDir": "Ya estás en el directorio superior",
"alterUrl": "URL alternativa", "alterUrl": "URL alternativa",
@@ -54,6 +55,7 @@
"createFolder": "Crear carpeta", "createFolder": "Crear carpeta",
"cursorType": "Tipo de cursor", "cursorType": "Tipo de cursor",
"dark": "Oscuro", "dark": "Oscuro",
"day": "Día",
"debug": "Depurar", "debug": "Depurar",
"decode": "Decodificar", "decode": "Decodificar",
"decompress": "Descomprimir", "decompress": "Descomprimir",
@@ -109,6 +111,7 @@
"highlight": "Resaltar código", "highlight": "Resaltar código",
"homeWidgetUrlConfig": "Configuración de URL del widget de inicio", "homeWidgetUrlConfig": "Configuración de URL del widget de inicio",
"host": "Anfitrión", "host": "Anfitrión",
"hour": "Hora",
"httpFailedWithCode": "Fallo en la solicitud, código de estado: {code}", "httpFailedWithCode": "Fallo en la solicitud, código de estado: {code}",
"icloudSynced": "iCloud sincronizado, algunos ajustes pueden requerir reiniciar para tomar efecto.", "icloudSynced": "iCloud sincronizado, algunos ajustes pueden requerir reiniciar para tomar efecto.",
"image": "Imagen", "image": "Imagen",
@@ -143,6 +146,7 @@
"maxRetryCount": "Número máximo de reintentos de conexión al servidor", "maxRetryCount": "Número máximo de reintentos de conexión al servidor",
"maxRetryCountEqual0": "Reintentará infinitamente", "maxRetryCountEqual0": "Reintentará infinitamente",
"min": "Mínimo", "min": "Mínimo",
"minute": "Minuto",
"mission": "Misión", "mission": "Misión",
"more": "Más", "more": "Más",
"moveOutServerFuncBtnsHelp": "Activado: se mostrará debajo de cada tarjeta en la página de servidores. Desactivado: se mostrará en la parte superior de los detalles del servidor.", "moveOutServerFuncBtnsHelp": "Activado: se mostrará debajo de cada tarjeta en la página de servidores. Desactivado: se mostrará en la parte superior de los detalles del servidor.",
@@ -161,6 +165,7 @@
"noServerAvailable": "No hay servidores disponibles.", "noServerAvailable": "No hay servidores disponibles.",
"noTask": "Sin tareas", "noTask": "Sin tareas",
"noUpdateAvailable": "No hay actualizaciones disponibles", "noUpdateAvailable": "No hay actualizaciones disponibles",
"node": "Nodo",
"notSelected": "No seleccionado", "notSelected": "No seleccionado",
"note": "Nota", "note": "Nota",
"nullToken": "Token nulo", "nullToken": "Token nulo",
@@ -237,6 +242,7 @@
"stats": "Estadísticas", "stats": "Estadísticas",
"stop": "Detener", "stop": "Detener",
"stopped": "Detenido", "stopped": "Detenido",
"storage": "Almacenamiento",
"success": "Éxito", "success": "Éxito",
"supportFmtArgs": "Soporta los siguientes argumentos de formato:", "supportFmtArgs": "Soporta los siguientes argumentos de formato:",
"suspend": "Suspender", "suspend": "Suspender",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "Ajouter une clé privée", "addPrivateKey": "Ajouter une clé privée",
"addSystemPrivateKeyTip": "Actuellement, vous n'avez aucune clé privée. Voulez-vous ajouter celle qui accompagne le système (~/.ssh/id_rsa)?", "addSystemPrivateKeyTip": "Actuellement, vous n'avez aucune clé privée. Voulez-vous ajouter celle qui accompagne le système (~/.ssh/id_rsa)?",
"added2List": "Ajouté à la liste des tâches", "added2List": "Ajouté à la liste des tâches",
"addr": "Adresse",
"all": "Tous", "all": "Tous",
"alreadyLastDir": "Déjà dans le dernier répertoire.", "alreadyLastDir": "Déjà dans le dernier répertoire.",
"alterUrl": "Modifier l'URL", "alterUrl": "Modifier l'URL",
@@ -54,6 +55,7 @@
"createFolder": "Créer un dossier", "createFolder": "Créer un dossier",
"cursorType": "Type de curseur", "cursorType": "Type de curseur",
"dark": "Sombre", "dark": "Sombre",
"day": "Jour",
"debug": "Déboguer", "debug": "Déboguer",
"decode": "Décoder", "decode": "Décoder",
"decompress": "Décompresser", "decompress": "Décompresser",
@@ -109,6 +111,7 @@
"highlight": "Coloration syntaxique", "highlight": "Coloration syntaxique",
"homeWidgetUrlConfig": "Configurer l'URL du widget d'accueil", "homeWidgetUrlConfig": "Configurer l'URL du widget d'accueil",
"host": "Hôte", "host": "Hôte",
"hour": "Heure",
"httpFailedWithCode": "requête échouée, code d'état : {code}", "httpFailedWithCode": "requête échouée, code d'état : {code}",
"icloudSynced": "iCloud est synchronisé et certaines options peuvent nécessiter un redémarrage de l'application pour être effectives.", "icloudSynced": "iCloud est synchronisé et certaines options peuvent nécessiter un redémarrage de l'application pour être effectives.",
"image": "Image", "image": "Image",
@@ -143,6 +146,7 @@
"maxRetryCount": "Nombre de reconnexions du serveur", "maxRetryCount": "Nombre de reconnexions du serveur",
"maxRetryCountEqual0": "Réessayera encore et encore.", "maxRetryCountEqual0": "Réessayera encore et encore.",
"min": "min", "min": "min",
"minute": "Minute",
"mission": "Mission", "mission": "Mission",
"more": "Plus", "more": "Plus",
"moveOutServerFuncBtnsHelp": "Activé : peut être affiché sous chaque carte sur la page de l'onglet Serveur. Désactivé : peut être affiché en haut de la page Détails du serveur.", "moveOutServerFuncBtnsHelp": "Activé : peut être affiché sous chaque carte sur la page de l'onglet Serveur. Désactivé : peut être affiché en haut de la page Détails du serveur.",
@@ -161,6 +165,7 @@
"noServerAvailable": "Aucun serveur disponible.", "noServerAvailable": "Aucun serveur disponible.",
"noTask": "Aucune tâche", "noTask": "Aucune tâche",
"noUpdateAvailable": "Aucune mise à jour disponible", "noUpdateAvailable": "Aucune mise à jour disponible",
"node": "Noeud",
"notSelected": "Non sélectionné", "notSelected": "Non sélectionné",
"note": "Note", "note": "Note",
"nullToken": "Jeton nul", "nullToken": "Jeton nul",
@@ -237,6 +242,7 @@
"stats": "Statistiques", "stats": "Statistiques",
"stop": "Arrêter", "stop": "Arrêter",
"stopped": "interrompue", "stopped": "interrompue",
"storage": "Stockage",
"success": "Succès", "success": "Succès",
"supportFmtArgs": "Les paramètres de formatage suivants sont pris en charge:", "supportFmtArgs": "Les paramètres de formatage suivants sont pris en charge:",
"suspend": "Suspendre", "suspend": "Suspendre",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "Tambahkan kunci pribadi", "addPrivateKey": "Tambahkan kunci pribadi",
"addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?", "addSystemPrivateKeyTip": "Saat ini tidak memiliki kunci privat, apakah Anda menambahkan kunci yang disertakan dengan sistem (~/.ssh/id_rsa)?",
"added2List": "Ditambahkan ke Daftar Tugas", "added2List": "Ditambahkan ke Daftar Tugas",
"addr": "Alamat",
"all": "Semua", "all": "Semua",
"alreadyLastDir": "Sudah di direktori terakhir.", "alreadyLastDir": "Sudah di direktori terakhir.",
"alterUrl": "Alter url", "alterUrl": "Alter url",
@@ -54,6 +55,7 @@
"createFolder": "Membuat folder", "createFolder": "Membuat folder",
"cursorType": "Jenis kursor", "cursorType": "Jenis kursor",
"dark": "Gelap", "dark": "Gelap",
"day": "Hari",
"debug": "Debug", "debug": "Debug",
"decode": "Membaca sandi", "decode": "Membaca sandi",
"decompress": "Dekompresi", "decompress": "Dekompresi",
@@ -109,6 +111,7 @@
"highlight": "Sorotan kode", "highlight": "Sorotan kode",
"homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah", "homeWidgetUrlConfig": "Konfigurasi URL Widget Rumah",
"host": "Host", "host": "Host",
"hour": "Jam",
"httpFailedWithCode": "Permintaan gagal, kode status: {code}", "httpFailedWithCode": "Permintaan gagal, kode status: {code}",
"icloudSynced": "iCloud disinkronkan dan beberapa pengaturan mungkin memerlukan pengaktifan ulang aplikasi agar dapat diterapkan.", "icloudSynced": "iCloud disinkronkan dan beberapa pengaturan mungkin memerlukan pengaktifan ulang aplikasi agar dapat diterapkan.",
"image": "Gambar", "image": "Gambar",
@@ -143,6 +146,7 @@
"maxRetryCount": "Jumlah penyambungan kembali server", "maxRetryCount": "Jumlah penyambungan kembali server",
"maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.", "maxRetryCountEqual0": "Akan mencoba lagi lagi dan lagi.",
"min": "Min", "min": "Min",
"minute": "Menit",
"mission": "Misi", "mission": "Misi",
"more": "Lebih Banyak", "more": "Lebih Banyak",
"moveOutServerFuncBtnsHelp": "Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.", "moveOutServerFuncBtnsHelp": "Aktif: dapat ditampilkan di bawah setiap kartu pada halaman Tab Server. Nonaktif: dapat ditampilkan di bagian atas halaman Rincian Server.",
@@ -161,6 +165,7 @@
"noServerAvailable": "Tidak ada server yang tersedia.", "noServerAvailable": "Tidak ada server yang tersedia.",
"noTask": "Tidak bertanya", "noTask": "Tidak bertanya",
"noUpdateAvailable": "Tidak ada pembaruan yang tersedia", "noUpdateAvailable": "Tidak ada pembaruan yang tersedia",
"node": "Node",
"notSelected": "Tidak terpilih", "notSelected": "Tidak terpilih",
"note": "Catatan", "note": "Catatan",
"nullToken": "Token NULL", "nullToken": "Token NULL",
@@ -237,6 +242,7 @@
"stats": "Statistik", "stats": "Statistik",
"stop": "Berhenti", "stop": "Berhenti",
"stopped": "dihentikan", "stopped": "dihentikan",
"storage": "Penyimpanan",
"success": "Kesuksesan", "success": "Kesuksesan",
"supportFmtArgs": "Parameter pemformatan berikut ini didukung:", "supportFmtArgs": "Parameter pemformatan berikut ini didukung:",
"suspend": "Suspend", "suspend": "Suspend",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "プライベートキーを追加", "addPrivateKey": "プライベートキーを追加",
"addSystemPrivateKeyTip": "現在プライベートキーがありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか?", "addSystemPrivateKeyTip": "現在プライベートキーがありません。システムのデフォルト(~/.ssh/id_rsa)を追加しますか?",
"added2List": "タスクリストに追加されました", "added2List": "タスクリストに追加されました",
"addr": "住所",
"all": "すべて", "all": "すべて",
"alreadyLastDir": "すでに最上位のディレクトリです", "alreadyLastDir": "すでに最上位のディレクトリです",
"alterUrl": "代替リンク", "alterUrl": "代替リンク",
@@ -54,6 +55,7 @@
"createFolder": "フォルダーを作成", "createFolder": "フォルダーを作成",
"cursorType": "カーソルタイプ", "cursorType": "カーソルタイプ",
"dark": "ダーク", "dark": "ダーク",
"day": "日",
"debug": "デバッグ", "debug": "デバッグ",
"decode": "デコード", "decode": "デコード",
"decompress": "解凍", "decompress": "解凍",
@@ -109,6 +111,7 @@
"highlight": "コードハイライト", "highlight": "コードハイライト",
"homeWidgetUrlConfig": "ホームウィジェットURL設定", "homeWidgetUrlConfig": "ホームウィジェットURL設定",
"host": "ホスト", "host": "ホスト",
"hour": "時間",
"httpFailedWithCode": "リクエスト失敗、ステータスコード: {code}", "httpFailedWithCode": "リクエスト失敗、ステータスコード: {code}",
"icloudSynced": "iCloudが同期されました。一部の設定はアプリを再起動する必要があります。", "icloudSynced": "iCloudが同期されました。一部の設定はアプリを再起動する必要があります。",
"image": "イメージ", "image": "イメージ",
@@ -143,6 +146,7 @@
"maxRetryCount": "サーバーの再接続試行回数", "maxRetryCount": "サーバーの再接続試行回数",
"maxRetryCountEqual0": "無限に再試行します", "maxRetryCountEqual0": "無限に再試行します",
"min": "最小", "min": "最小",
"minute": "分",
"mission": "ミッション", "mission": "ミッション",
"more": "もっと", "more": "もっと",
"moveOutServerFuncBtnsHelp": "有効にする:サーバータブの各カードの下に表示されます。無効にする:サーバーの詳細ページの上部に表示されます。", "moveOutServerFuncBtnsHelp": "有効にする:サーバータブの各カードの下に表示されます。無効にする:サーバーの詳細ページの上部に表示されます。",
@@ -161,6 +165,7 @@
"noServerAvailable": "使用可能なサーバーがありません。", "noServerAvailable": "使用可能なサーバーがありません。",
"noTask": "タスクがありません", "noTask": "タスクがありません",
"noUpdateAvailable": "利用可能な更新はありません", "noUpdateAvailable": "利用可能な更新はありません",
"node": "ノード",
"notSelected": "選択されていません", "notSelected": "選択されていません",
"note": "メモ", "note": "メモ",
"nullToken": "トークンなし", "nullToken": "トークンなし",
@@ -237,6 +242,7 @@
"stats": "統計", "stats": "統計",
"stop": "停止", "stop": "停止",
"stopped": "停止しました", "stopped": "停止しました",
"storage": "ストレージ",
"success": "成功", "success": "成功",
"supportFmtArgs": "以下のフォーマット引数がサポートされています:", "supportFmtArgs": "以下のフォーマット引数がサポートされています:",
"suspend": "中断", "suspend": "中断",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "Adicionar uma chave privada", "addPrivateKey": "Adicionar uma chave privada",
"addSystemPrivateKeyTip": "Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?", "addSystemPrivateKeyTip": "Atualmente, não há nenhuma chave privada. Gostaria de adicionar a chave do sistema (~/.ssh/id_rsa)?",
"added2List": "Adicionado à lista de tarefas", "added2List": "Adicionado à lista de tarefas",
"addr": "Endereço",
"all": "Todos", "all": "Todos",
"alreadyLastDir": "Já é o diretório mais alto", "alreadyLastDir": "Já é o diretório mais alto",
"alterUrl": "URL alternativa", "alterUrl": "URL alternativa",
@@ -54,6 +55,7 @@
"createFolder": "Criar pasta", "createFolder": "Criar pasta",
"cursorType": "Tipo de cursor", "cursorType": "Tipo de cursor",
"dark": "Escuro", "dark": "Escuro",
"day": "Dia",
"debug": "Debugar", "debug": "Debugar",
"decode": "Decodificar", "decode": "Decodificar",
"decompress": "Descomprimir", "decompress": "Descomprimir",
@@ -109,6 +111,7 @@
"highlight": "Destaque de código", "highlight": "Destaque de código",
"homeWidgetUrlConfig": "Configuração de URL do widget da tela inicial", "homeWidgetUrlConfig": "Configuração de URL do widget da tela inicial",
"host": "Host", "host": "Host",
"hour": "Hora",
"httpFailedWithCode": "Falha na solicitação, código de status: {code}", "httpFailedWithCode": "Falha na solicitação, código de status: {code}",
"icloudSynced": "iCloud sincronizado, algumas configurações podem precisar de reinicialização do app para serem aplicadas.", "icloudSynced": "iCloud sincronizado, algumas configurações podem precisar de reinicialização do app para serem aplicadas.",
"image": "Imagem", "image": "Imagem",
@@ -143,6 +146,7 @@
"maxRetryCount": "Número de tentativas de reconexão com o servidor", "maxRetryCount": "Número de tentativas de reconexão com o servidor",
"maxRetryCountEqual0": "Irá tentar indefinidamente", "maxRetryCountEqual0": "Irá tentar indefinidamente",
"min": "Mínimo", "min": "Mínimo",
"minute": "Minuto",
"mission": "Missão", "mission": "Missão",
"more": "Mais", "more": "Mais",
"moveOutServerFuncBtnsHelp": "Ativado: Mostra abaixo de cada cartão na aba do servidor. Desativado: Mostra no topo da página de detalhes do servidor.", "moveOutServerFuncBtnsHelp": "Ativado: Mostra abaixo de cada cartão na aba do servidor. Desativado: Mostra no topo da página de detalhes do servidor.",
@@ -161,6 +165,7 @@
"noServerAvailable": "Nenhum servidor disponível.", "noServerAvailable": "Nenhum servidor disponível.",
"noTask": "Sem tarefas", "noTask": "Sem tarefas",
"noUpdateAvailable": "Sem atualizações disponíveis", "noUpdateAvailable": "Sem atualizações disponíveis",
"node": "Nó",
"notSelected": "Não selecionado", "notSelected": "Não selecionado",
"note": "Nota", "note": "Nota",
"nullToken": "Token nulo", "nullToken": "Token nulo",
@@ -237,6 +242,7 @@
"stats": "Estatísticas", "stats": "Estatísticas",
"stop": "Parar", "stop": "Parar",
"stopped": "Parado", "stopped": "Parado",
"storage": "Armazenamento",
"success": "Sucesso", "success": "Sucesso",
"supportFmtArgs": "Suporta os seguintes argumentos formatados:", "supportFmtArgs": "Suporta os seguintes argumentos formatados:",
"suspend": "Suspender", "suspend": "Suspender",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "добавить приватный ключ", "addPrivateKey": "добавить приватный ключ",
"addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?", "addSystemPrivateKeyTip": "В данный момент приватные ключи отсутствуют. Добавить системный приватный ключ (~/.ssh/id_rsa)?",
"added2List": "добавлено в список задач", "added2List": "добавлено в список задач",
"addr": "Адрес",
"all": "все", "all": "все",
"alreadyLastDir": "Уже в корневом каталоге", "alreadyLastDir": "Уже в корневом каталоге",
"alterUrl": "альтернативная ссылка", "alterUrl": "альтернативная ссылка",
@@ -54,6 +55,7 @@
"createFolder": "создать папку", "createFolder": "создать папку",
"cursorType": "Тип курсора", "cursorType": "Тип курсора",
"dark": "темная", "dark": "темная",
"day": "День",
"debug": "отладка", "debug": "отладка",
"decode": "декодировать", "decode": "декодировать",
"decompress": "разархивировать", "decompress": "разархивировать",
@@ -109,6 +111,7 @@
"highlight": "подсветка кода", "highlight": "подсветка кода",
"homeWidgetUrlConfig": "конфигурация URL виджета домашнего экрана", "homeWidgetUrlConfig": "конфигурация URL виджета домашнего экрана",
"host": "хост", "host": "хост",
"hour": "Час",
"httpFailedWithCode": "Ошибка запроса, код: {code}", "httpFailedWithCode": "Ошибка запроса, код: {code}",
"icloudSynced": "Синхронизация с iCloud выполнена, некоторые настройки могут потребовать перезапуска приложения для вступления в силу.", "icloudSynced": "Синхронизация с iCloud выполнена, некоторые настройки могут потребовать перезапуска приложения для вступления в силу.",
"image": "образ", "image": "образ",
@@ -143,6 +146,7 @@
"maxRetryCount": "максимальное количество попыток переподключения к серверу", "maxRetryCount": "максимальное количество попыток переподключения к серверу",
"maxRetryCountEqual0": "будет бесконечно пытаться переподключиться", "maxRetryCountEqual0": "будет бесконечно пытаться переподключиться",
"min": "минимум", "min": "минимум",
"minute": "Минута",
"mission": "задача", "mission": "задача",
"more": "больше", "more": "больше",
"moveOutServerFuncBtnsHelp": "Включено: кнопки функций сервера отображаются под каждой карточкой на вкладке сервера. Выключено: отображается в верхней части страницы деталей сервера.", "moveOutServerFuncBtnsHelp": "Включено: кнопки функций сервера отображаются под каждой карточкой на вкладке сервера. Выключено: отображается в верхней части страницы деталей сервера.",
@@ -161,6 +165,7 @@
"noServerAvailable": "Нет доступных серверов.", "noServerAvailable": "Нет доступных серверов.",
"noTask": "нет задач", "noTask": "нет задач",
"noUpdateAvailable": "нет доступных обновлений", "noUpdateAvailable": "нет доступных обновлений",
"node": "Узел",
"notSelected": "не выбрано", "notSelected": "не выбрано",
"note": "заметка", "note": "заметка",
"nullToken": "нет токена", "nullToken": "нет токена",
@@ -237,6 +242,7 @@
"stats": "статистика", "stats": "статистика",
"stop": "остановить", "stop": "остановить",
"stopped": "остановлено", "stopped": "остановлено",
"storage": "Хранение",
"success": "успех", "success": "успех",
"supportFmtArgs": "Поддерживаются следующие форматы аргументов:", "supportFmtArgs": "Поддерживаются следующие форматы аргументов:",
"suspend": "приостановить", "suspend": "приостановить",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "添加一个私钥", "addPrivateKey": "添加一个私钥",
"addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa", "addSystemPrivateKeyTip": "当前没有任何私钥,是否添加系统自带的(~/.ssh/id_rsa",
"added2List": "已添加至任务列表", "added2List": "已添加至任务列表",
"addr": "地址",
"all": "所有", "all": "所有",
"alreadyLastDir": "已经是最上层目录了", "alreadyLastDir": "已经是最上层目录了",
"alterUrl": "备选链接", "alterUrl": "备选链接",
@@ -54,6 +55,7 @@
"createFolder": "创建文件夹", "createFolder": "创建文件夹",
"cursorType": "光标类型", "cursorType": "光标类型",
"dark": "暗", "dark": "暗",
"day": "天",
"debug": "调试", "debug": "调试",
"decode": "解码", "decode": "解码",
"decompress": "解压缩", "decompress": "解压缩",
@@ -109,6 +111,7 @@
"highlight": "代码高亮", "highlight": "代码高亮",
"homeWidgetUrlConfig": "桌面部件链接配置", "homeWidgetUrlConfig": "桌面部件链接配置",
"host": "主机", "host": "主机",
"hour": "小时",
"httpFailedWithCode": "请求失败, 状态码: {code}", "httpFailedWithCode": "请求失败, 状态码: {code}",
"icloudSynced": "iCloud已同步某些设置可能需要重启才能生效。", "icloudSynced": "iCloud已同步某些设置可能需要重启才能生效。",
"image": "镜像", "image": "镜像",
@@ -143,6 +146,7 @@
"maxRetryCount": "服务器尝试重连次数", "maxRetryCount": "服务器尝试重连次数",
"maxRetryCountEqual0": "会无限重试", "maxRetryCountEqual0": "会无限重试",
"min": "最小", "min": "最小",
"minute": "分钟",
"mission": "任务", "mission": "任务",
"more": "更多", "more": "更多",
"moveOutServerFuncBtnsHelp": "开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。", "moveOutServerFuncBtnsHelp": "开启:可以在服务器 Tab 页的每个卡片下方显示。关闭:在服务器详情页顶部显示。",
@@ -161,6 +165,7 @@
"noServerAvailable": "没有可用的服务器。", "noServerAvailable": "没有可用的服务器。",
"noTask": "没有任务", "noTask": "没有任务",
"noUpdateAvailable": "没有可用更新", "noUpdateAvailable": "没有可用更新",
"node": "节点",
"notSelected": "未选择", "notSelected": "未选择",
"note": "备注", "note": "备注",
"nullToken": "无Token", "nullToken": "无Token",
@@ -237,6 +242,7 @@
"stats": "统计", "stats": "统计",
"stop": "停止", "stop": "停止",
"stopped": "已停止", "stopped": "已停止",
"storage": "存储",
"success": "成功", "success": "成功",
"supportFmtArgs": "支持以下格式化参数:", "supportFmtArgs": "支持以下格式化参数:",
"suspend": "挂起", "suspend": "挂起",

View File

@@ -7,6 +7,7 @@
"addPrivateKey": "新增一個私鑰", "addPrivateKey": "新增一個私鑰",
"addSystemPrivateKeyTip": "當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa", "addSystemPrivateKeyTip": "當前沒有任何私鑰,是否添加系統自帶的(~/.ssh/id_rsa",
"added2List": "已添加至任務列表", "added2List": "已添加至任務列表",
"addr": "地址",
"all": "所有", "all": "所有",
"alreadyLastDir": "已經是最上層目錄了", "alreadyLastDir": "已經是最上層目錄了",
"alterUrl": "備選鏈接", "alterUrl": "備選鏈接",
@@ -54,6 +55,7 @@
"createFolder": "創建文件夾", "createFolder": "創建文件夾",
"cursorType": "光標類型", "cursorType": "光標類型",
"dark": "暗", "dark": "暗",
"day": "日",
"debug": "調試", "debug": "調試",
"decode": "解碼", "decode": "解碼",
"decompress": "解壓縮", "decompress": "解壓縮",
@@ -109,6 +111,7 @@
"highlight": "代碼高亮", "highlight": "代碼高亮",
"homeWidgetUrlConfig": "桌面部件鏈接配置", "homeWidgetUrlConfig": "桌面部件鏈接配置",
"host": "主機", "host": "主機",
"hour": "小時",
"httpFailedWithCode": "請求失敗, 狀態碼: {code}", "httpFailedWithCode": "請求失敗, 狀態碼: {code}",
"icloudSynced": "iCloud已同步某些設置可能需要重啟才能生效。", "icloudSynced": "iCloud已同步某些設置可能需要重啟才能生效。",
"image": "鏡像", "image": "鏡像",
@@ -143,6 +146,7 @@
"maxRetryCount": "服務器嘗試重連次數", "maxRetryCount": "服務器嘗試重連次數",
"maxRetryCountEqual0": "會無限重試", "maxRetryCountEqual0": "會無限重試",
"min": "最小", "min": "最小",
"minute": "分鐘",
"mission": "任務", "mission": "任務",
"more": "更多", "more": "更多",
"moveOutServerFuncBtnsHelp": "開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。", "moveOutServerFuncBtnsHelp": "開啟:可以在服務器 Tab 頁的每個卡片下方顯示。關閉:在服務器詳情頁頂部顯示。",
@@ -161,6 +165,7 @@
"noServerAvailable": "沒有可用的服務器。", "noServerAvailable": "沒有可用的服務器。",
"noTask": "沒有任務", "noTask": "沒有任務",
"noUpdateAvailable": "沒有可用更新", "noUpdateAvailable": "沒有可用更新",
"node": "節點",
"notSelected": "未選擇", "notSelected": "未選擇",
"note": "備註", "note": "備註",
"nullToken": "無Token", "nullToken": "無Token",
@@ -237,6 +242,7 @@
"stats": "統計", "stats": "統計",
"stop": "停止", "stop": "停止",
"stopped": "已停止", "stopped": "已停止",
"storage": "存儲",
"success": "成功", "success": "成功",
"supportFmtArgs": "支援以下格式化參數:", "supportFmtArgs": "支援以下格式化參數:",
"suspend": "挂起", "suspend": "挂起",

View File

@@ -15,6 +15,7 @@ import 'package:toolbox/core/utils/platform/base.dart';
import 'package:toolbox/core/utils/sync/webdav.dart'; import 'package:toolbox/core/utils/sync/webdav.dart';
import 'package:toolbox/core/utils/ui.dart'; import 'package:toolbox/core/utils/ui.dart';
import 'package:toolbox/data/model/app/menu/server_func.dart'; import 'package:toolbox/data/model/app/menu/server_func.dart';
import 'package:toolbox/data/model/server/custom.dart';
import 'package:toolbox/data/res/logger.dart'; import 'package:toolbox/data/res/logger.dart';
import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
@@ -118,6 +119,7 @@ Future<void> _initDb() async {
Hive.registerAdapter(VirtKeyAdapter()); // 4 Hive.registerAdapter(VirtKeyAdapter()); // 4
Hive.registerAdapter(NetViewTypeAdapter()); // 5 Hive.registerAdapter(NetViewTypeAdapter()); // 5
Hive.registerAdapter(ServerFuncBtnAdapter()); // 6 Hive.registerAdapter(ServerFuncBtnAdapter()); // 6
Hive.registerAdapter(ServerCustomAdapter()); // 7
} }
void _setupLogger() { void _setupLogger() {

View File

@@ -223,22 +223,22 @@ class BackupPage extends StatelessWidget {
Widget _buildBulkImportServers(BuildContext context) { Widget _buildBulkImportServers(BuildContext context) {
return CardX( return CardX(
child: ListTile( child: ListTile(
title: Text(l10n.bulkImportServers), title: Text(l10n.bulkImportServers),
subtitle: MarkdownBody( subtitle: MarkdownBody(
data: l10n.bulkImportServersTip(Urls.appWiki), data: l10n.bulkImportServersTip(Urls.appWiki),
styleSheet: MarkdownStyleSheet( styleSheet: MarkdownStyleSheet(
p: UIs.textGrey, p: UIs.textGrey,
a: TextStyle( a: TextStyle(color: primaryColor),
color: primaryColor, ),
)), onTapLink: (text, href, title) {
onTapLink: (text, href, title) { if (href != null) openUrl(href);
if (href != null) openUrl(href); },
}, ),
leading: const Icon(Icons.import_export),
onTap: () => _onBulkImportServers(context),
), ),
trailing: const Icon(Icons.import_export), );
onTap: () => _onBulkImportServers(context),
));
} }
Future<void> _onTapFileRestore(BuildContext context) async { Future<void> _onTapFileRestore(BuildContext context) async {

197
lib/view/page/pve.dart Normal file
View File

@@ -0,0 +1,197 @@
import 'package:flutter/material.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/ui.dart';
import 'package:toolbox/view/widget/appbar.dart';
import 'package:toolbox/view/widget/future_widget.dart';
import 'package:toolbox/view/widget/percent_circle.dart';
final class PvePage extends StatefulWidget {
final ServerPrivateInfo spi;
const PvePage({
super.key,
required this.spi,
});
@override
_PvePageState createState() => _PvePageState();
}
const _kHorziPadding = 11.0;
final class _PvePageState extends State<PvePage> {
late final pve = PveProvider(spi: widget.spi);
late MediaQueryData _media;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(
title: Text('PVE'),
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (pve.err.value != null) {
return Center(
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,
),
),
),
);
},
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);
}
},
);
},
);
}
Widget _buildQemu(PveQemu item) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(item.name),
trailing: Text(item.topRight),
),
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(
children: [
Text(
item.netin.bytes2Str,
style: const TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 3),
Text(
item.netout.bytes2Str,
style: const TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
)
],
),
4),
],
),
if (item.isRunning) 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),
).card;
}
Widget _buildSdn(PveSdn item) {
return ListTile(
title: Text(item.sdn),
trailing: Text(item.status),
).card;
}
Widget _wrap(Widget child, int count) {
return SizedBox(
height: (_media.size.width - 2 * _kHorziPadding) / count,
child: child,
);
}
}

View File

@@ -5,7 +5,9 @@ import 'package:toolbox/core/extension/context/dialog.dart';
import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/extension/context/snackbar.dart'; import 'package:toolbox/core/extension/context/snackbar.dart';
import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/app/shell_func.dart';
import 'package:toolbox/data/model/server/custom.dart';
import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/view/widget/expand_tile.dart';
import '../../../core/route.dart'; import '../../../core/route.dart';
import '../../../data/model/server/private_key_info.dart'; import '../../../data/model/server/private_key_info.dart';
@@ -33,6 +35,8 @@ class _ServerEditPageState extends State<ServerEditPage> {
final _portController = TextEditingController(); final _portController = TextEditingController();
final _usernameController = TextEditingController(); final _usernameController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _pveAddrCtrl = TextEditingController();
final _nameFocus = FocusNode(); final _nameFocus = FocusNode();
final _ipFocus = FocusNode(); final _ipFocus = FocusNode();
final _alterUrlFocus = FocusNode(); final _alterUrlFocus = FocusNode();
@@ -71,6 +75,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
_altUrlController.text = spi.alterUrl ?? ''; _altUrlController.text = spi.alterUrl ?? '';
_autoConnect.value = spi.autoConnect ?? true; _autoConnect.value = spi.autoConnect ?? true;
_jumpServer.value = spi.jumpId; _jumpServer.value = spi.jumpId;
_pveAddrCtrl.text = spi.custom?.pveAddr ?? '';
} }
} }
@@ -221,8 +226,6 @@ class _ServerEditPageState extends State<ServerEditPage> {
allTags: [...Pros.server.tags.value], allTags: [...Pros.server.tags.value],
onRenameTag: Pros.server.renameTag, onRenameTag: Pros.server.renameTag,
), ),
_buildAuth(),
//_buildJumpServer(),
ListTile( ListTile(
title: Text(l10n.autoConnect), title: Text(l10n.autoConnect),
trailing: ListenableBuilder( trailing: ListenableBuilder(
@@ -235,6 +238,9 @@ class _ServerEditPageState extends State<ServerEditPage> {
), ),
), ),
), ),
_buildAuth(),
//_buildJumpServer(),
_buildPVE(),
]; ];
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(17, 17, 17, 47), padding: const EdgeInsets.fromLTRB(17, 17, 17, 47),
@@ -329,6 +335,17 @@ class _ServerEditPageState extends State<ServerEditPage> {
); );
} }
Widget _buildPVE() {
return ExpandTile(title: const Text('PVE'), children: [
Input(
controller: _pveAddrCtrl,
type: TextInputType.url,
label: l10n.addr,
hint: 'https://example.com:8006',
),
]);
}
Widget _buildFAB() { Widget _buildFAB() {
return FloatingActionButton( return FloatingActionButton(
heroTag: 'server', heroTag: 'server',
@@ -428,6 +445,8 @@ class _ServerEditPageState extends State<ServerEditPage> {
if (_portController.text.isEmpty) { if (_portController.text.isEmpty) {
_portController.text = '22'; _portController.text = '22';
} }
final pveAddr = _pveAddrCtrl.text.isEmpty ? null : _pveAddrCtrl.text;
final custom = pveAddr == null ? null : ServerCustom(pveAddr: pveAddr);
final spi = ServerPrivateInfo( final spi = ServerPrivateInfo(
name: _nameController.text.isEmpty name: _nameController.text.isEmpty
@@ -444,6 +463,7 @@ class _ServerEditPageState extends State<ServerEditPage> {
alterUrl: _altUrlController.text.isEmpty ? null : _altUrlController.text, alterUrl: _altUrlController.text.isEmpty ? null : _altUrlController.text,
autoConnect: _autoConnect.value, autoConnect: _autoConnect.value,
jumpId: _jumpServer.value, jumpId: _jumpServer.value,
custom: custom,
); );
if (widget.spi == null) { if (widget.spi == null) {

View File

@@ -1,5 +1,4 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:circle_chart/circle_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -16,13 +15,13 @@ import 'package:toolbox/data/model/server/sensors.dart';
import 'package:toolbox/data/model/server/try_limiter.dart'; import 'package:toolbox/data/model/server/try_limiter.dart';
import 'package:toolbox/data/res/provider.dart'; import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/view/widget/percent_circle.dart';
import '../../../core/route.dart'; import '../../../core/route.dart';
import '../../../data/model/app/net_view.dart'; import '../../../data/model/app/net_view.dart';
import '../../../data/model/server/server.dart'; import '../../../data/model/server/server.dart';
import '../../../data/model/server/server_private_info.dart'; import '../../../data/model/server/server_private_info.dart';
import '../../../data/provider/server.dart'; import '../../../data/provider/server.dart';
import '../../../data/res/color.dart';
import '../../../data/res/ui.dart'; import '../../../data/res/ui.dart';
import '../../widget/cardx.dart'; import '../../widget/cardx.dart';
import '../../widget/server_func_btns.dart'; import '../../widget/server_func_btns.dart';
@@ -312,9 +311,11 @@ class _ServerPageState extends State<ServerPage>
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_wrapWithSizedbox(_buildPercentCircle(ss.cpu.usedPercent()), true), _wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), true),
_wrapWithSizedbox( _wrapWithSizedbox(
_buildPercentCircle(ss.mem.usedPercent * 100), true), PercentCircle(percent: ss.mem.usedPercent * 100),
true,
),
_wrapWithSizedbox(_buildNet(ss, spi.id)), _wrapWithSizedbox(_buildNet(ss, spi.id)),
_wrapWithSizedbox(_buildDisk(ss, spi.id)), _wrapWithSizedbox(_buildDisk(ss, spi.id)),
], ],
@@ -509,29 +510,6 @@ class _ServerPageState extends State<ServerPage>
); );
} }
Widget _buildPercentCircle(double percent) {
if (percent <= 0) percent = 0.01;
if (percent >= 100) percent = 99.9;
return Stack(
alignment: Alignment.center,
children: [
CircleChart(
progressColor: primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 57,
height: 57,
animationDuration: const Duration(milliseconds: 777),
),
Text(
'${percent.toStringAsFixed(1)}%',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12.7),
),
],
);
}
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;

View File

@@ -0,0 +1,41 @@
import 'package:circle_chart/circle_chart.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
final class PercentCircle extends StatelessWidget {
final double percent;
const PercentCircle({
super.key,
required this.percent,
});
@override
Widget build(BuildContext context) {
final percent = switch (this.percent) {
0 => 0.01,
100 => 99.9,
// NaN
final val when val.isNaN => 0.01,
_ => this.percent,
};
return Stack(
alignment: Alignment.center,
children: [
CircleChart(
progressColor: primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 57,
height: 57,
animationDuration: const Duration(milliseconds: 777),
),
Text(
'${percent.toStringAsFixed(1)}%',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12.7),
),
],
);
}
}

View File

@@ -163,6 +163,12 @@ void _onTapMoreBtns(
check: () => _checkClient(context, spi.id), check: () => _checkClient(context, spi.id),
); );
break; break;
case ServerFuncBtn.pve:
AppRoute.pve(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
break;
} }
} }

136
test/pve_test.dart Normal file
View File

@@ -0,0 +1,136 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:toolbox/data/model/server/pve.dart';
const _raw = '''
{
"data": [
{
"maxmem": 12884901888,
"type": "lxc",
"cpu": 0.0544631947461575,
"netin": 65412250538,
"template": 0,
"diskread": 324033204224,
"maxcpu": 8,
"disk": 29767077888,
"diskwrite": 707866570752,
"node": "pve",
"vmid": 100,
"mem": 5389254656,
"status": "running",
"netout": 66898114418,
"uptime": 1204757,
"id": "lxc/100",
"maxdisk": 134145380352,
"name": "Jellyfin"
},
{
"vmid": 101,
"node": "pve",
"uptime": 0,
"netout": 0,
"status": "stopped",
"mem": 0,
"id": "qemu/101",
"name": "ubuntu",
"maxdisk": 137438953472,
"maxmem": 6442450944,
"cpu": 0,
"netin": 0,
"type": "qemu",
"disk": 0,
"diskread": 0,
"template": 0,
"maxcpu": 8,
"diskwrite": 0
},
{
"maxcpu": 4,
"template": 0,
"diskread": 23287297536,
"disk": 0,
"diskwrite": 39555984896,
"maxmem": 4294967296,
"type": "qemu",
"netin": 2190678599,
"cpu": 0.0516426831961466,
"id": "qemu/102",
"maxdisk": 0,
"name": "win",
"node": "pve",
"vmid": 102,
"mem": 1791827968,
"status": "running",
"netout": 213292068,
"uptime": 1013075
},
{
"maxcpu": 12,
"id": "node/pve",
"disk": 358415503360,
"maxdisk": 998011547648,
"cgroup-mode": 2,
"node": "pve",
"maxmem": 29287632896,
"type": "node",
"status": "online",
"mem": 11522887680,
"cpu": 0.0451634094268353,
"level": "",
"uptime": 1204771
},
{
"id": "storage/pve/DSM",
"disk": 1250082226176,
"maxdisk": 9909187887104,
"storage": "DSM",
"node": "pve",
"status": "available",
"type": "storage",
"plugintype": "cifs",
"content": "snippets,backup,images,rootdir,vztmpl,iso",
"shared": 1
},
{
"type": "storage",
"status": "available",
"plugintype": "dir",
"content": "iso,vztmpl,images,rootdir,backup,snippets",
"shared": 0,
"node": "pve",
"maxdisk": 1967847137280,
"storage": "hard",
"id": "storage/pve/hard",
"disk": 620950544384
},
{
"maxdisk": 998011547648,
"storage": "local",
"disk": 358415503360,
"id": "storage/pve/local",
"status": "available",
"type": "storage",
"plugintype": "dir",
"content": "backup,snippets,rootdir,images,vztmpl,iso",
"shared": 0,
"node": "pve"
},
{
"id": "sdn/pve/localnetwork",
"node": "pve",
"sdn": "localnetwork",
"status": "ok",
"type": "sdn"
}
]
}''';
void main() {
test('parse pve', () {
final list = json.decode(_raw)['data'] as List;
final pveItems = list.map((e) => PveResIface.fromJson(e)).toList();
expect(pveItems.length, 8);
});
}