diff --git a/lib/data/model/server/pve.dart b/lib/data/model/server/pve.dart index 29d08828..917a5638 100644 --- a/lib/data/model/server/pve.dart +++ b/lib/data/model/server/pve.dart @@ -58,12 +58,20 @@ sealed class PveResIface { } } -final class PveLxc extends PveResIface { +abstract interface class PveCtrlIface { + String get node; + String get id; + bool get isRunning; + String get summary; +} + +final class PveLxc extends PveResIface implements PveCtrlIface { @override final String id; @override final PveResType type; final int vmid; + @override final String node; final String name; @override @@ -122,9 +130,11 @@ final class PveLxc extends PveResIface { ); } + @override bool get isRunning => status == 'running'; - String get topRight { + @override + String get summary { if (isRunning) { return uptime.secondsToDuration().toStr; } @@ -132,12 +142,13 @@ final class PveLxc extends PveResIface { } } -final class PveQemu extends PveResIface { +final class PveQemu extends PveResIface implements PveCtrlIface { @override final String id; @override final PveResType type; final int vmid; + @override final String node; final String name; @override @@ -196,9 +207,11 @@ final class PveQemu extends PveResIface { ); } + @override bool get isRunning => status == 'running'; - String get topRight { + @override + String get summary { if (isRunning) { return uptime.secondsToDuration().toStr; } diff --git a/lib/data/provider/pve.dart b/lib/data/provider/pve.dart index dc1816af..6659b3cd 100644 --- a/lib/data/provider/pve.dart +++ b/lib/data/provider/pve.dart @@ -135,4 +135,36 @@ final class PveProvider extends ChangeNotifier { data.value = res; return res; } + + Future reboot(String node, String id) async { + await connected.future; + final resp = + await session.post('$addr/api2/json/nodes/$node/$id/status/reboot'); + return _isCtrlSuc(resp); + } + + Future start(String node, String id) async { + await connected.future; + final resp = + await session.post('$addr/api2/json/nodes/$node/$id/status/start'); + return _isCtrlSuc(resp); + } + + Future stop(String node, String id) async { + await connected.future; + final resp = + await session.post('$addr/api2/json/nodes/$node/$id/status/stop'); + return _isCtrlSuc(resp); + } + + Future shutdown(String node, String id) async { + await connected.future; + final resp = + await session.post('$addr/api2/json/nodes/$node/$id/status/shutdown'); + return _isCtrlSuc(resp); + } + + bool _isCtrlSuc(Response resp) { + return resp.statusCode == 200; + } } diff --git a/lib/view/page/pve.dart b/lib/view/page/pve.dart index 9c8ba9dd..66d789c3 100644 --- a/lib/view/page/pve.dart +++ b/lib/view/page/pve.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:toolbox/core/extension/context/common.dart'; +import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; +import 'package:toolbox/core/extension/context/snackbar.dart'; import 'package:toolbox/core/extension/numx.dart'; import 'package:toolbox/core/extension/widget.dart'; import 'package:toolbox/data/model/server/pve.dart'; @@ -11,6 +14,7 @@ import 'package:toolbox/data/res/color.dart'; import 'package:toolbox/data/res/store.dart'; import 'package:toolbox/data/res/ui.dart'; import 'package:toolbox/view/widget/appbar.dart'; +import 'package:toolbox/view/widget/icon_btn.dart'; import 'package:toolbox/view/widget/kv_row.dart'; import 'package:toolbox/view/widget/percent_circle.dart'; import 'package:toolbox/view/widget/two_line_text.dart'; @@ -189,132 +193,174 @@ final class _PvePageState extends State { } Widget _buildQemu(PveQemu item) { + if (!item.isRunning) { + return ListTile( + title: Text(item.name, style: UIs.text13Bold), + trailing: _buildCtrlBtns(item), + ).card; + } + final children = [ + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(width: 15), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: item.name, + style: UIs.text13Bold, + ), + TextSpan( + text: ' / ${item.summary}', + style: UIs.text12Grey, + ), + ], + ), + ), + const Spacer(), + _buildCtrlBtns(item), + UIs.width13, + ], + ), + UIs.height7, + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4), + _wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4), + _wrap( + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${l10n.read}:\n${item.diskread.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ), + const SizedBox(height: 3), + Text( + '${l10n.write}:\n${item.diskwrite.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ) + ], + ), + 4), + _wrap( + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '↓:\n${item.netin.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ), + const SizedBox(height: 3), + Text( + '↑:\n${item.netout.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ) + ], + ), + 4), + ], + ), + const SizedBox(height: 21) + ]; 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, - ], - ), - 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, - ], + children: children, ).card; } Widget _buildLxc(PveLxc item) { + if (!item.isRunning) { + return ListTile( + title: Text(item.name, style: UIs.text13Bold), + trailing: _buildCtrlBtns(item), + ).card; + } + final children = [ + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(width: 15), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: item.name, + style: UIs.text13Bold, + ), + TextSpan( + text: ' / ${item.summary}', + style: UIs.text12Grey, + ), + ], + ), + ), + const Spacer(), + _buildCtrlBtns(item), + UIs.width13, + ], + ), + UIs.height7, + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4), + _wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4), + _wrap( + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${l10n.read}:\n${item.diskread.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ), + const SizedBox(height: 3), + Text( + '${l10n.write}:\n${item.diskwrite.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ) + ], + ), + 4), + _wrap( + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '↓:\n${item.netin.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ), + const SizedBox(height: 3), + Text( + '↑:\n${item.netout.bytes2Str}', + style: UIs.text11Grey, + textAlign: TextAlign.center, + ) + ], + ), + 4), + ], + ), + const SizedBox(height: 21) + ]; return Column( mainAxisSize: MainAxisSize.min, - children: [ - UIs.height13, - Row( - children: [ - UIs.width13, - Text(item.name, style: UIs.text13Bold), - const Spacer(), - Text(item.topRight, style: UIs.text12Grey), - UIs.width13, - ], - ), - UIs.height7, - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _wrap(PercentCircle(percent: (item.cpu / item.maxcpu) * 100), 4), - _wrap(PercentCircle(percent: (item.mem / item.maxmem) * 100), 4), - _wrap( - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '${l10n.read}:\n${item.diskread.bytes2Str}', - style: UIs.text11Grey, - textAlign: TextAlign.center, - ), - const SizedBox(height: 3), - Text( - '${l10n.write}:\n${item.diskwrite.bytes2Str}', - style: UIs.text11Grey, - textAlign: TextAlign.center, - ) - ], - ), - 4), - _wrap( - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '↓:\n${item.netin.bytes2Str}', - style: UIs.text11Grey, - textAlign: TextAlign.center, - ), - const SizedBox(height: 3), - Text( - '↑:\n${item.netout.bytes2Str}', - style: UIs.text11Grey, - textAlign: TextAlign.center, - ) - ], - ), - 4), - ], - ), - UIs.height13, - ], + children: children, ).card; } @@ -346,9 +392,98 @@ final class _PvePageState extends State { ).card; } + Widget _buildCtrlBtns(PveCtrlIface item) { + if (!item.isRunning) { + return IconBtn( + icon: Icons.play_arrow, + color: Colors.grey, + onTap: () async { + bool? suc; + await context.showLoadingDialog(fn: () async { + suc = await _pve.start(item.node, item.id); + }); + if (suc == true) { + context.showSnackBar(l10n.success); + } else { + context.showSnackBar(l10n.failed); + } + }, + ); + } + return Row( + children: [ + IconBtn( + icon: Icons.stop, + color: Colors.grey, + onTap: () async { + final sure = await _ask(l10n.stop, item.id); + if (!sure) return; + bool? suc; + await context.showLoadingDialog(fn: () async { + suc = await _pve.stop(item.node, item.id); + }); + if (suc == true) { + context.showSnackBar(l10n.success); + } else { + context.showSnackBar(l10n.failed); + } + }, + ), + IconBtn( + icon: Icons.refresh, + color: Colors.grey, + onTap: () async { + final sure = await _ask(l10n.reboot, item.id); + if (!sure) return; + bool? suc; + await context.showLoadingDialog(fn: () async { + suc = await _pve.reboot(item.node, item.id); + }); + if (suc == true) { + context.showSnackBar(l10n.success); + } else { + context.showSnackBar(l10n.failed); + } + }, + ), + IconBtn( + icon: Icons.power_off, + color: Colors.grey, + onTap: () async { + final sure = await _ask(l10n.shutdown, item.id); + if (!sure) return; + bool? suc; + await context.showLoadingDialog(fn: () async { + suc = await _pve.shutdown(item.node, item.id); + }); + if (suc == true) { + context.showSnackBar(l10n.success); + } else { + context.showSnackBar(l10n.failed); + } + }, + ), + ], + ); + } + + Future _ask(String action, String id) async { + final sure = await context.showRoundDialog( + title: Text(l10n.attention), + child: Text(l10n.askContinue('$action $id')), + actions: [ + TextButton( + onPressed: () => context.pop(true), + child: Text(l10n.ok, style: UIs.textRed), + ), + ], + ); + return sure == true; + } + Widget _wrap(Widget child, int count) { return SizedBox( - height: (_media.size.width - 2 * _kHorziPadding) / count, + width: (_media.size.width - 2 * _kHorziPadding - 26) / count, child: child, ); } diff --git a/lib/view/widget/icon_btn.dart b/lib/view/widget/icon_btn.dart new file mode 100644 index 00000000..dbf2b581 --- /dev/null +++ b/lib/view/widget/icon_btn.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +final class IconBtn extends StatelessWidget { + final IconData icon; + final double size; + final Color? color; + final void Function() onTap; + + const IconBtn({ + super.key, + required this.icon, + required this.onTap, + this.size = 17, + this.color, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(17), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon(icon, size: size, color: color), + ), + ); + } +}