optimization: desktop UI (#747)
This commit is contained in:
@@ -1,5 +1,83 @@
|
||||
part of 'view.dart';
|
||||
|
||||
extension on _ServerDetailPageState {
|
||||
void _onTapGpuItem(NvidiaSmiItem item) {
|
||||
final processes = item.memory.processes;
|
||||
final displayCount = processes.length > 5 ? 5 : processes.length;
|
||||
final height = displayCount * 47.0;
|
||||
context.showRoundDialog(
|
||||
title: item.name,
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: height,
|
||||
child: ListView.builder(
|
||||
itemCount: processes.length,
|
||||
itemBuilder: (_, idx) => _buildGpuProcessItem(processes[idx]),
|
||||
),
|
||||
),
|
||||
actions: Btnx.oks,
|
||||
);
|
||||
}
|
||||
|
||||
void _nTapGpuProcessItem(NvidiaSmiMemProcess process) {
|
||||
context.showRoundDialog(
|
||||
title: '${process.pid}',
|
||||
titleMaxLines: 1,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UIs.height13,
|
||||
Text('Memory: ${process.memory} MiB'),
|
||||
UIs.height13,
|
||||
Text('Process: ${process.name}')
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(libL10n.close),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapCustomItem(MapEntry<String, String> cmd) {
|
||||
context.showRoundDialog(
|
||||
title: cmd.key,
|
||||
child: SingleChildScrollView(
|
||||
child: Text(cmd.value, style: UIs.text13Grey),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(libL10n.close),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapSensorItem(SensorItem si) {
|
||||
context.showRoundDialog(
|
||||
title: si.device,
|
||||
child: SingleChildScrollView(
|
||||
child: SimpleMarkdown(
|
||||
data: si.toMarkdown,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
tableBorder: TableBorder.all(color: Colors.grey),
|
||||
tableHead: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapTemperatureItem(String key) {
|
||||
Pfs.copy(key);
|
||||
context.showSnackBar('${libL10n.copy} ${libL10n.success}');
|
||||
}
|
||||
}
|
||||
|
||||
enum _NetSortType {
|
||||
device,
|
||||
trans,
|
||||
@@ -26,13 +104,9 @@ enum _NetSortType {
|
||||
case _NetSortType.device:
|
||||
return (b, a) => a.compareTo(b);
|
||||
case _NetSortType.recv:
|
||||
return (b, a) => ns
|
||||
.speedInBytes(ns.deviceIdx(a))
|
||||
.compareTo(ns.speedInBytes(ns.deviceIdx(b)));
|
||||
return (b, a) => ns.speedInBytes(ns.deviceIdx(a)).compareTo(ns.speedInBytes(ns.deviceIdx(b)));
|
||||
case _NetSortType.trans:
|
||||
return (b, a) => ns
|
||||
.speedOutBytes(ns.deviceIdx(a))
|
||||
.compareTo(ns.speedOutBytes(ns.deviceIdx(b)));
|
||||
return (b, a) => ns.speedOutBytes(ns.deviceIdx(a)).compareTo(ns.speedOutBytes(ns.deviceIdx(b)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/server_detail_card.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/battery.dart';
|
||||
@@ -25,16 +26,19 @@ import 'package:server_box/data/model/server/server.dart';
|
||||
part 'misc.dart';
|
||||
|
||||
class ServerDetailPage extends StatefulWidget {
|
||||
const ServerDetailPage({super.key, required this.spi});
|
||||
|
||||
final Spi spi;
|
||||
final SpiRequiredArgs args;
|
||||
const ServerDetailPage({super.key, required this.args});
|
||||
|
||||
@override
|
||||
State<ServerDetailPage> createState() => _ServerDetailPageState();
|
||||
|
||||
static const route = AppRouteArg(
|
||||
page: ServerDetailPage.new,
|
||||
path: '/servers/detail',
|
||||
);
|
||||
}
|
||||
|
||||
class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
class _ServerDetailPageState extends State<ServerDetailPage> with SingleTickerProviderStateMixin {
|
||||
late final _cardBuildMap = Map.fromIterables(
|
||||
ServerDetailCards.names,
|
||||
[
|
||||
@@ -49,7 +53,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
_buildTemperature,
|
||||
_buildBatteries,
|
||||
_buildPve,
|
||||
_buildCustom,
|
||||
_buildCustomCmd,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -83,7 +87,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = widget.spi.server;
|
||||
final s = widget.args.spi.server;
|
||||
if (s == null) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(),
|
||||
@@ -106,14 +110,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
children.add(buildFunc(si));
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: _buildAppBar(si),
|
||||
body: ListView(
|
||||
padding: EdgeInsets.only(
|
||||
left: 13,
|
||||
right: 13,
|
||||
bottom: _media.padding.bottom + 77,
|
||||
),
|
||||
body: AutoMultiList(
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
@@ -121,18 +121,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
|
||||
CustomAppBar _buildAppBar(Server si) {
|
||||
return CustomAppBar(
|
||||
title: Hero(
|
||||
tag: 'home_card_title_${si.spi.id}',
|
||||
transitionOnUserGestures: true,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
si.spi.name,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: context.isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
si.spi.name,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: context.isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
@@ -144,7 +137,10 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () async {
|
||||
final delete = await ServerEditPage.route.go(context, args: si.spi);
|
||||
final delete = await ServerEditPage.route.go(
|
||||
context,
|
||||
args: SpiRequiredArgs(si.spi),
|
||||
);
|
||||
if (delete == true) {
|
||||
context.pop();
|
||||
}
|
||||
@@ -155,16 +151,14 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
}
|
||||
|
||||
Widget _buildLogo(Server si) {
|
||||
var logoUrl = si.spi.custom?.logoUrl ??
|
||||
_settings.serverLogoUrl.fetch().selfNotEmptyOrNull;
|
||||
var logoUrl = si.spi.custom?.logoUrl ?? _settings.serverLogoUrl.fetch().selfNotEmptyOrNull;
|
||||
if (logoUrl == null) return UIs.placeholder;
|
||||
|
||||
final dist = si.status.more[StatusCmdType.sys]?.dist;
|
||||
if (dist != null) {
|
||||
logoUrl = logoUrl.replaceFirst('{DIST}', dist.name);
|
||||
}
|
||||
logoUrl =
|
||||
logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
|
||||
logoUrl = logoUrl.replaceFirst('{BRIGHT}', context.isDark ? 'dark' : 'light');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
@@ -194,8 +188,16 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(e.key.i18n, style: UIs.text13),
|
||||
Text(e.value, style: UIs.text13Grey)
|
||||
Text(
|
||||
e.key.i18n,
|
||||
style: UIs.text13,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
e.value,
|
||||
style: UIs.text13Grey,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -267,15 +269,15 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
}
|
||||
|
||||
Widget _buildCpuModelItem(MapEntry<String, int> e) {
|
||||
final name = e.key
|
||||
.replaceFirst('Intel(R)', '')
|
||||
.replaceFirst('AMD', '')
|
||||
.replaceFirst('with Radeon Graphics', '');
|
||||
final name =
|
||||
e.key.replaceFirst('Intel(R)', '').replaceFirst('AMD', '').replaceFirst('with Radeon Graphics', '');
|
||||
final child = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _media.size.width * .7,
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _media.size.width * .7,
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
style: UIs.text13,
|
||||
@@ -283,7 +285,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Text('x ${e.value}', style: UIs.text13Grey),
|
||||
Text('x ${e.value}', style: UIs.text13Grey, overflow: TextOverflow.clip),
|
||||
],
|
||||
);
|
||||
return child.paddingSymmetric(horizontal: 17);
|
||||
@@ -312,41 +314,47 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
List<Widget> _buildCPUProgress(Cpus cs) {
|
||||
const kMaxColumn = 2;
|
||||
const kRowThreshold = 4;
|
||||
const kCoresCount = kMaxColumn * kRowThreshold;
|
||||
const kCoresCountThreshold = kMaxColumn * kRowThreshold;
|
||||
final children = <Widget>[];
|
||||
final displayCpuIndexSetting = Stores.setting.displayCpuIndex.fetch();
|
||||
|
||||
if (cs.coresCount > kCoresCount) {
|
||||
final rows = cs.coresCount ~/ kMaxColumn;
|
||||
for (var i = 0; i < rows; i++) {
|
||||
if (cs.coresCount > kCoresCountThreshold) {
|
||||
final numCoresToDisplay = cs.coresCount - 1;
|
||||
final numRows = (numCoresToDisplay + kMaxColumn - 1) ~/ kMaxColumn;
|
||||
|
||||
for (var i = 0; i < numRows; i++) {
|
||||
final rowChildren = <Widget>[];
|
||||
for (var j = 0; j < kMaxColumn; j++) {
|
||||
final idx = i * kMaxColumn + j + 1;
|
||||
if (idx >= cs.coresCount) break;
|
||||
if (Stores.setting.displayCpuIndex.fetch()) {
|
||||
rowChildren.add(Text('$idx', style: UIs.text13Grey));
|
||||
final coreListIndex = i * kMaxColumn + j;
|
||||
if (coreListIndex >= numCoresToDisplay) break;
|
||||
|
||||
final coreNumberOneBased = coreListIndex + 1;
|
||||
|
||||
if (displayCpuIndexSetting) {
|
||||
rowChildren.add(Text('$coreNumberOneBased', style: UIs.text13Grey));
|
||||
}
|
||||
rowChildren.add(
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: _buildProgress(cs.usedPercent(coreIdx: idx)),
|
||||
child: _buildProgress(cs.usedPercent(coreIdx: coreNumberOneBased)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
rowChildren.joinWith(UIs.width7);
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: Row(
|
||||
children: rowChildren,
|
||||
if (rowChildren.isNotEmpty) {
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17),
|
||||
child: Row(
|
||||
children: rowChildren.joinWith(UIs.width7).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < cs.coresCount; i++) {
|
||||
if (i == 0) continue;
|
||||
for (var i = 1; i < cs.coresCount; i++) {
|
||||
children.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 17),
|
||||
@@ -377,6 +385,17 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
final used = ss.mem.usedPercent * 100;
|
||||
final usedStr = used.toStringAsFixed(0);
|
||||
|
||||
final percentW = Row(
|
||||
children: [
|
||||
_buildAnimatedText(ValueKey(usedStr), '$usedStr%', UIs.text27),
|
||||
UIs.width7,
|
||||
Text(
|
||||
'of ${(ss.mem.total * 1024).bytes2Str}',
|
||||
style: UIs.text13Grey,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
return CardX(
|
||||
child: Padding(
|
||||
padding: UIs.roundRectCardPadding,
|
||||
@@ -387,20 +406,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
_buildAnimatedText(
|
||||
ValueKey(usedStr),
|
||||
'$usedStr%',
|
||||
UIs.text27,
|
||||
),
|
||||
UIs.width7,
|
||||
Text(
|
||||
'of ${(ss.mem.total * 1024).bytes2Str}',
|
||||
style: UIs.text13Grey,
|
||||
)
|
||||
],
|
||||
),
|
||||
percentW,
|
||||
Row(
|
||||
children: [
|
||||
_buildDetailPercent(free, 'free'),
|
||||
@@ -423,6 +429,18 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
if (ss.swap.total == 0) return UIs.placeholder;
|
||||
final used = ss.swap.usedPercent * 100;
|
||||
final cached = ss.swap.cached / ss.swap.total * 100;
|
||||
|
||||
final percentW = Row(
|
||||
children: [
|
||||
Text('${used.toStringAsFixed(0)}%', style: UIs.text27),
|
||||
UIs.width7,
|
||||
Text(
|
||||
'of ${(ss.swap.total * 1024).bytes2Str} ',
|
||||
style: UIs.text13Grey,
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
return CardX(
|
||||
child: Padding(
|
||||
padding: UIs.roundRectCardPadding,
|
||||
@@ -433,16 +451,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('${used.toStringAsFixed(0)}%', style: UIs.text27),
|
||||
UIs.width7,
|
||||
Text(
|
||||
'of ${(ss.swap.total * 1024).bytes2Str} ',
|
||||
style: UIs.text13Grey,
|
||||
)
|
||||
],
|
||||
),
|
||||
percentW,
|
||||
_buildDetailPercent(cached, 'cached'),
|
||||
],
|
||||
),
|
||||
@@ -470,7 +479,6 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
|
||||
Widget _buildGpuItem(NvidiaSmiItem item) {
|
||||
final mem = item.memory;
|
||||
final processes = mem.processes;
|
||||
return ListTile(
|
||||
title: Text(item.name, style: UIs.text13),
|
||||
leading: Text(
|
||||
@@ -490,32 +498,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final height = () {
|
||||
if (processes.length > 5) {
|
||||
return 5 * 47.0;
|
||||
}
|
||||
return processes.length * 47.0;
|
||||
}();
|
||||
context.showRoundDialog(
|
||||
title: item.name,
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: height,
|
||||
child: ListView.builder(
|
||||
itemCount: processes.length,
|
||||
itemBuilder: (_, idx) =>
|
||||
_buildGpuProcessItem(processes[idx]),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(libL10n.close),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
onPressed: () => _onTapGpuItem(item),
|
||||
icon: const Icon(Icons.info_outline, size: 17),
|
||||
),
|
||||
],
|
||||
@@ -538,28 +521,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
textScaler: _textFactor,
|
||||
),
|
||||
trailing: InkWell(
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: '${process.pid}',
|
||||
titleMaxLines: 1,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UIs.height13,
|
||||
Text('Memory: ${process.memory} MiB'),
|
||||
UIs.height13,
|
||||
Text('Process: ${process.name}')
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(libL10n.close),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
onTap: () => _nTapGpuProcessItem(process),
|
||||
child: const Icon(Icons.info_outline, size: 17),
|
||||
),
|
||||
);
|
||||
@@ -567,8 +529,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
|
||||
Widget _buildDiskView(Server si) {
|
||||
final ss = si.status;
|
||||
final children = List.generate(
|
||||
ss.disk.length, (idx) => _buildDiskItem(ss.disk[idx], ss));
|
||||
final children = List.generate(ss.disk.length, (idx) => _buildDiskItem(ss.disk[idx], ss));
|
||||
return CardX(
|
||||
child: ExpandTile(
|
||||
title: Text(l10n.disk),
|
||||
@@ -587,6 +548,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
if (read == null || write == null) return use;
|
||||
return '$use\n${l10n.read} $read | ${l10n.write} $write';
|
||||
}();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 5),
|
||||
child: Row(
|
||||
@@ -638,43 +600,40 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
devices.sort(_netSortType.value.getSortFunc(ns));
|
||||
children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e)));
|
||||
|
||||
return CardX(
|
||||
child: ExpandTile(
|
||||
leading: Icon(ServerDetailCards.net.icon, size: 17),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(l10n.net),
|
||||
UIs.width13,
|
||||
ValBuilder(
|
||||
listenable: _netSortType,
|
||||
builder: (val) => InkWell(
|
||||
onTap: () => _netSortType.value = val.next,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.sort, size: 17),
|
||||
UIs.width7,
|
||||
Text(
|
||||
val.name,
|
||||
style: UIs.text13Grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
return ExpandTile(
|
||||
leading: Icon(ServerDetailCards.net.icon, size: 17),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(l10n.net),
|
||||
UIs.width13,
|
||||
_netSortType.listenVal(
|
||||
(val) => InkWell(
|
||||
onTap: () => _netSortType.value = val.next,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.sort, size: 17),
|
||||
UIs.width7,
|
||||
Text(
|
||||
val.name,
|
||||
style: UIs.text13Grey,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
childrenPadding: const EdgeInsets.only(bottom: 11),
|
||||
initiallyExpanded: _getInitExpand(children.length),
|
||||
children: children,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
childrenPadding: const EdgeInsets.only(bottom: 11),
|
||||
initiallyExpanded: _getInitExpand(children.length),
|
||||
children: children,
|
||||
).cardx;
|
||||
}
|
||||
|
||||
Widget _buildNetSpeedItem(NetSpeed ns, String device) {
|
||||
@@ -725,9 +684,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
leading: const Icon(Icons.ac_unit, size: 20),
|
||||
initiallyExpanded: _getInitExpand(ss.temps.devices.length),
|
||||
childrenPadding: const EdgeInsets.only(bottom: 7),
|
||||
children: ss.temps.devices
|
||||
.map((key) => _buildTemperatureItem(key, ss.temps.get(key)))
|
||||
.toList(),
|
||||
children: ss.temps.devices.map((key) => _buildTemperatureItem(key, ss.temps.get(key))).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -738,12 +695,11 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(key, style: UIs.text15).paddingSymmetric(horizontal: 5).tap(
|
||||
onTap: () {
|
||||
Pfs.copy(key);
|
||||
context.showSnackBar('${libL10n.copy} ${libL10n.success}');
|
||||
},
|
||||
),
|
||||
Btn.text(
|
||||
text: key,
|
||||
textStyle: UIs.text15,
|
||||
onTap: () => _onTapTemperatureItem(key),
|
||||
).paddingSymmetric(horizontal: 5),
|
||||
Text('${val?.toStringAsFixed(1)}°C', style: UIs.text13Grey),
|
||||
],
|
||||
),
|
||||
@@ -813,41 +769,31 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
child: Text(si.device),
|
||||
);
|
||||
}
|
||||
|
||||
final itemW = Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(si.device, style: UIs.text15Bold),
|
||||
UIs.width7,
|
||||
Text('(${si.adapter.raw})', style: UIs.text13Grey),
|
||||
],
|
||||
),
|
||||
Text(si.summary ?? '', style: UIs.text13Grey),
|
||||
],
|
||||
));
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: si.device,
|
||||
child: SingleChildScrollView(
|
||||
child: SimpleMarkdown(
|
||||
data: si.toMarkdown,
|
||||
styleSheet: MarkdownStyleSheet(
|
||||
tableBorder: TableBorder.all(color: Colors.grey),
|
||||
tableHead: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => _onTapSensorItem(si),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(si.device, style: UIs.text15Bold),
|
||||
UIs.width7,
|
||||
Text('(${si.adapter.raw})', style: UIs.text13Grey),
|
||||
],
|
||||
),
|
||||
Text(si.summary ?? '', style: UIs.text13Grey),
|
||||
],
|
||||
)),
|
||||
itemW,
|
||||
UIs.width7,
|
||||
const Icon(Icons.keyboard_arrow_right, color: Colors.grey),
|
||||
],
|
||||
@@ -869,7 +815,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustom(Server si) {
|
||||
Widget _buildCustomCmd(Server si) {
|
||||
final ss = si.status;
|
||||
if (ss.customCmds.isEmpty) return UIs.placeholder;
|
||||
return CardX(
|
||||
@@ -877,12 +823,12 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
leading: const Icon(MingCute.command_line, size: 17),
|
||||
title: Text(l10n.customCmd),
|
||||
initiallyExpanded: _getInitExpand(ss.customCmds.length),
|
||||
children: ss.customCmds.entries.map(_buildCustomItem).toList(),
|
||||
children: ss.customCmds.entries.map(_buildCustomCmdItem).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomItem(MapEntry<String, String> cmd) {
|
||||
Widget _buildCustomCmdItem(MapEntry<String, String> cmd) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7),
|
||||
child: KvRow(
|
||||
@@ -891,20 +837,7 @@ class _ServerDetailPageState extends State<ServerDetailPage>
|
||||
vBuilder: () {
|
||||
if (!cmd.value.contains('\n')) return null;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.showRoundDialog(
|
||||
title: cmd.key,
|
||||
child: SingleChildScrollView(
|
||||
child: Text(cmd.value, style: UIs.text13Grey),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: Text(libL10n.close),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
onTap: () => _onTapCustomItem(cmd),
|
||||
child: const Icon(
|
||||
Icons.info_outline,
|
||||
size: 17,
|
||||
|
||||
@@ -5,24 +5,25 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/server/custom.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/private_key.dart';
|
||||
import 'package:server_box/data/store/server.dart';
|
||||
import 'package:server_box/view/page/private_key/edit.dart';
|
||||
|
||||
class ServerEditPage extends StatefulWidget {
|
||||
final Spi? args;
|
||||
final SpiRequiredArgs? args;
|
||||
|
||||
const ServerEditPage({super.key, this.args});
|
||||
|
||||
static const route = AppRoute<bool, Spi>(
|
||||
static const route = AppRoute<bool, SpiRequiredArgs>(
|
||||
page: ServerEditPage.new,
|
||||
path: '/server_edit',
|
||||
path: '/servers/edit',
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -30,7 +31,7 @@ class ServerEditPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
late final spi = widget.args;
|
||||
late final spi = widget.args?.spi;
|
||||
final _nameController = TextEditingController();
|
||||
final _ipController = TextEditingController();
|
||||
final _altUrlController = TextEditingController();
|
||||
@@ -187,14 +188,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
_buildJumpServer(),
|
||||
_buildMore(),
|
||||
];
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(17, 7, 17, 47),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
return AutoMultiList(children: children);
|
||||
}
|
||||
|
||||
Widget _buildAuth() {
|
||||
@@ -259,7 +253,10 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
),
|
||||
trailing: Btn.icon(
|
||||
icon: const Icon(Icons.edit),
|
||||
onTap: () => AppRoutes.keyEdit(pki: e).go(context),
|
||||
onTap: () => PrivateKeyEditPage.route.go(
|
||||
context,
|
||||
args: PrivateKeyEditPageArgs(pki: e),
|
||||
),
|
||||
),
|
||||
onTap: () => _keyIdx.value = index,
|
||||
);
|
||||
@@ -269,23 +266,17 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
title: Text(libL10n.add),
|
||||
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
||||
trailing: const Icon(Icons.add),
|
||||
onTap: () => AppRoutes.keyEdit().go(context),
|
||||
),
|
||||
);
|
||||
return CardX(
|
||||
child: ListenableBuilder(
|
||||
listenable: _keyIdx,
|
||||
builder: (_, __) => Column(children: tiles),
|
||||
onTap: () => PrivateKeyEditPage.route.go(context),
|
||||
),
|
||||
);
|
||||
return _keyIdx.listenVal((_) => Column(children: tiles)).cardx;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEnvs() {
|
||||
return _env.listenVal((val) {
|
||||
final subtitle =
|
||||
val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
|
||||
final subtitle = val.isEmpty ? null : Text(val.keys.join(','), style: UIs.textGrey);
|
||||
return ListTile(
|
||||
leading: const Icon(HeroIcons.variable),
|
||||
subtitle: subtitle,
|
||||
@@ -419,18 +410,9 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
return ListTile(
|
||||
leading: const Icon(BoxIcons.bxs_file_json),
|
||||
title: const Text('JSON'),
|
||||
subtitle: vals.isEmpty
|
||||
? null
|
||||
: Text(vals.keys.join(','), style: UIs.textGrey),
|
||||
subtitle: vals.isEmpty ? null : Text(vals.keys.join(','), style: UIs.textGrey),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onTap: () async {
|
||||
final res = await KvEditor.route.go(
|
||||
context,
|
||||
KvEditorArgs(data: _customCmds.value),
|
||||
);
|
||||
if (res == null) return;
|
||||
_customCmds.value = res;
|
||||
},
|
||||
onTap: _onTapCustomItem,
|
||||
);
|
||||
},
|
||||
).cardx,
|
||||
@@ -535,153 +517,6 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
).cardx;
|
||||
}
|
||||
|
||||
void _onSave() async {
|
||||
if (_ipController.text.isEmpty) {
|
||||
context.showSnackBar('${libL10n.empty} ${l10n.host}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||
final cancel = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(l10n.useNoPwd)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(false),
|
||||
child: Text(libL10n.ok),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(true),
|
||||
child: Text(libL10n.cancel),
|
||||
)
|
||||
],
|
||||
);
|
||||
if (cancel != false) return;
|
||||
}
|
||||
|
||||
// If [_pubKeyIndex] is -1, it means that the user has not selected
|
||||
if (_keyIdx.value == -1) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
if (_usernameController.text.isEmpty) {
|
||||
_usernameController.text = 'root';
|
||||
}
|
||||
if (_portController.text.isEmpty) {
|
||||
_portController.text = '22';
|
||||
}
|
||||
final customCmds = _customCmds.value;
|
||||
final custom = ServerCustom(
|
||||
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
||||
pveIgnoreCert: _pveIgnoreCert.value,
|
||||
cmds: customCmds.isEmpty ? null : customCmds,
|
||||
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
||||
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
|
||||
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
|
||||
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
|
||||
);
|
||||
|
||||
final wolEmpty = _wolMacCtrl.text.isEmpty &&
|
||||
_wolIpCtrl.text.isEmpty &&
|
||||
_wolPwdCtrl.text.isEmpty;
|
||||
final wol = wolEmpty
|
||||
? null
|
||||
: WakeOnLanCfg(
|
||||
mac: _wolMacCtrl.text,
|
||||
ip: _wolIpCtrl.text,
|
||||
pwd: _wolPwdCtrl.text.selfNotEmptyOrNull,
|
||||
);
|
||||
if (wol != null) {
|
||||
final wolValidation = wol.validate();
|
||||
if (!wolValidation.$2) {
|
||||
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final spi = Spi(
|
||||
name: _nameController.text.isEmpty
|
||||
? _ipController.text
|
||||
: _nameController.text,
|
||||
ip: _ipController.text,
|
||||
port: int.parse(_portController.text),
|
||||
user: _usernameController.text,
|
||||
pwd: _passwordController.text.selfNotEmptyOrNull,
|
||||
keyId: _keyIdx.value != null
|
||||
? PrivateKeyProvider.pkis.value.elementAt(_keyIdx.value!).id
|
||||
: null,
|
||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||
autoConnect: _autoConnect.value,
|
||||
jumpId: _jumpServer.value,
|
||||
custom: custom,
|
||||
wolCfg: wol,
|
||||
envs: _env.value.isEmpty ? null : _env.value,
|
||||
);
|
||||
|
||||
if (this.spi == null) {
|
||||
final existsIds = ServerStore.instance.box.keys;
|
||||
if (existsIds.contains(spi.id)) {
|
||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||
return;
|
||||
}
|
||||
ServerProvider.addServer(spi);
|
||||
} else {
|
||||
ServerProvider.updateServer(this.spi!, spi);
|
||||
}
|
||||
|
||||
context.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
if (spi != null) {
|
||||
_initWithSpi(spi!);
|
||||
}
|
||||
}
|
||||
|
||||
void _initWithSpi(Spi spi) {
|
||||
_nameController.text = spi.name;
|
||||
_ipController.text = spi.ip;
|
||||
_portController.text = spi.port.toString();
|
||||
_usernameController.text = spi.user;
|
||||
if (spi.keyId == null) {
|
||||
_passwordController.text = spi.pwd ?? '';
|
||||
} else {
|
||||
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere(
|
||||
(e) => e.id == spi.keyId,
|
||||
);
|
||||
}
|
||||
|
||||
/// List in dart is passed by pointer, so you need to copy it here
|
||||
_tags.value = spi.tags?.toSet() ?? {};
|
||||
|
||||
_altUrlController.text = spi.alterUrl ?? '';
|
||||
_autoConnect.value = spi.autoConnect;
|
||||
_jumpServer.value = spi.jumpId;
|
||||
|
||||
final custom = spi.custom;
|
||||
if (custom != null) {
|
||||
_pveAddrCtrl.text = custom.pveAddr ?? '';
|
||||
_pveIgnoreCert.value = custom.pveIgnoreCert;
|
||||
_customCmds.value = custom.cmds ?? {};
|
||||
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
||||
_logoUrlCtrl.text = custom.logoUrl ?? '';
|
||||
}
|
||||
|
||||
final wol = spi.wolCfg;
|
||||
if (wol != null) {
|
||||
_wolMacCtrl.text = wol.mac;
|
||||
_wolIpCtrl.text = wol.ip;
|
||||
_wolPwdCtrl.text = wol.pwd ?? '';
|
||||
}
|
||||
|
||||
_env.value = spi.envs ?? {};
|
||||
|
||||
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
||||
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
||||
}
|
||||
|
||||
Widget _buildWriteScriptTip() {
|
||||
return Btn.tile(
|
||||
text: libL10n.attention,
|
||||
@@ -742,4 +577,156 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
|
||||
icon: const Icon(Icons.delete),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void afterFirstLayout(BuildContext context) {
|
||||
if (spi != null) {
|
||||
_initWithSpi(spi!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on _ServerEditPageState {
|
||||
void _onTapCustomItem() async {
|
||||
final res = await KvEditor.route.go(
|
||||
context,
|
||||
KvEditorArgs(data: _customCmds.value),
|
||||
);
|
||||
if (res == null) return;
|
||||
_customCmds.value = res;
|
||||
}
|
||||
|
||||
void _onSave() async {
|
||||
if (_ipController.text.isEmpty) {
|
||||
context.showSnackBar('${libL10n.empty} ${l10n.host}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||
final cancel = await context.showRoundDialog<bool>(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue(l10n.useNoPwd)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(false),
|
||||
child: Text(libL10n.ok),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.pop(true),
|
||||
child: Text(libL10n.cancel),
|
||||
)
|
||||
],
|
||||
);
|
||||
if (cancel != false) return;
|
||||
}
|
||||
|
||||
// If [_pubKeyIndex] is -1, it means that the user has not selected
|
||||
if (_keyIdx.value == -1) {
|
||||
context.showSnackBar(libL10n.empty);
|
||||
return;
|
||||
}
|
||||
if (_usernameController.text.isEmpty) {
|
||||
_usernameController.text = 'root';
|
||||
}
|
||||
if (_portController.text.isEmpty) {
|
||||
_portController.text = '22';
|
||||
}
|
||||
final customCmds = _customCmds.value;
|
||||
final custom = ServerCustom(
|
||||
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
||||
pveIgnoreCert: _pveIgnoreCert.value,
|
||||
cmds: customCmds.isEmpty ? null : customCmds,
|
||||
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
||||
logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull,
|
||||
netDev: _netDevCtrl.text.selfNotEmptyOrNull,
|
||||
scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull,
|
||||
);
|
||||
|
||||
final wolEmpty = _wolMacCtrl.text.isEmpty && _wolIpCtrl.text.isEmpty && _wolPwdCtrl.text.isEmpty;
|
||||
final wol = wolEmpty
|
||||
? null
|
||||
: WakeOnLanCfg(
|
||||
mac: _wolMacCtrl.text,
|
||||
ip: _wolIpCtrl.text,
|
||||
pwd: _wolPwdCtrl.text.selfNotEmptyOrNull,
|
||||
);
|
||||
if (wol != null) {
|
||||
final wolValidation = wol.validate();
|
||||
if (!wolValidation.$2) {
|
||||
context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final spi = Spi(
|
||||
name: _nameController.text.isEmpty ? _ipController.text : _nameController.text,
|
||||
ip: _ipController.text,
|
||||
port: int.parse(_portController.text),
|
||||
user: _usernameController.text,
|
||||
pwd: _passwordController.text.selfNotEmptyOrNull,
|
||||
keyId: _keyIdx.value != null ? PrivateKeyProvider.pkis.value.elementAt(_keyIdx.value!).id : null,
|
||||
tags: _tags.value.isEmpty ? null : _tags.value.toList(),
|
||||
alterUrl: _altUrlController.text.selfNotEmptyOrNull,
|
||||
autoConnect: _autoConnect.value,
|
||||
jumpId: _jumpServer.value,
|
||||
custom: custom,
|
||||
wolCfg: wol,
|
||||
envs: _env.value.isEmpty ? null : _env.value,
|
||||
);
|
||||
|
||||
if (this.spi == null) {
|
||||
final existsIds = ServerStore.instance.box.keys;
|
||||
if (existsIds.contains(spi.id)) {
|
||||
context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}');
|
||||
return;
|
||||
}
|
||||
ServerProvider.addServer(spi);
|
||||
} else {
|
||||
ServerProvider.updateServer(this.spi!, spi);
|
||||
}
|
||||
|
||||
context.pop();
|
||||
}
|
||||
|
||||
void _initWithSpi(Spi spi) {
|
||||
_nameController.text = spi.name;
|
||||
_ipController.text = spi.ip;
|
||||
_portController.text = spi.port.toString();
|
||||
_usernameController.text = spi.user;
|
||||
if (spi.keyId == null) {
|
||||
_passwordController.text = spi.pwd ?? '';
|
||||
} else {
|
||||
_keyIdx.value = PrivateKeyProvider.pkis.value.indexWhere(
|
||||
(e) => e.id == spi.keyId,
|
||||
);
|
||||
}
|
||||
|
||||
/// List in dart is passed by pointer, so you need to copy it here
|
||||
_tags.value = spi.tags?.toSet() ?? {};
|
||||
|
||||
_altUrlController.text = spi.alterUrl ?? '';
|
||||
_autoConnect.value = spi.autoConnect;
|
||||
_jumpServer.value = spi.jumpId;
|
||||
|
||||
final custom = spi.custom;
|
||||
if (custom != null) {
|
||||
_pveAddrCtrl.text = custom.pveAddr ?? '';
|
||||
_pveIgnoreCert.value = custom.pveIgnoreCert;
|
||||
_customCmds.value = custom.cmds ?? {};
|
||||
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
||||
_logoUrlCtrl.text = custom.logoUrl ?? '';
|
||||
}
|
||||
|
||||
final wol = spi.wolCfg;
|
||||
if (wol != null) {
|
||||
_wolMacCtrl.text = wol.mac;
|
||||
_wolIpCtrl.text = wol.ip;
|
||||
_wolPwdCtrl.text = wol.pwd ?? '';
|
||||
}
|
||||
|
||||
_env.value = spi.envs ?? {};
|
||||
|
||||
_netDevCtrl.text = spi.custom?.netDev ?? '';
|
||||
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,802 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/setting/entry.dart';
|
||||
import 'package:server_box/view/widget/percent_circle.dart';
|
||||
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||
|
||||
part 'top_bar.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
const ServerPage({super.key});
|
||||
|
||||
@override
|
||||
State<ServerPage> createState() => _ServerPageState();
|
||||
}
|
||||
|
||||
const _cardPad = 74.0;
|
||||
const _cardPadSingle = 13.0;
|
||||
|
||||
class _ServerPageState extends State<ServerPage>
|
||||
with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||
late MediaQueryData _media;
|
||||
|
||||
late double _textFactorDouble;
|
||||
double _offset = 1;
|
||||
late TextScaler _textFactor;
|
||||
|
||||
final _cardsStatus = <String, _CardNotifier>{};
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
final _tag = ''.vn;
|
||||
bool _useDoubleColumn = false;
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
final _autoHideCtrl = AutoHideController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer?.cancel();
|
||||
_scrollController.dispose();
|
||||
_autoHideCtrl.dispose();
|
||||
_tag.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!Stores.setting.fullScreenJitter.fetch()) return;
|
||||
_timer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
if (mounted) {
|
||||
_updateOffset();
|
||||
setState(() {});
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
_updateOffset();
|
||||
_updateTextScaler();
|
||||
_useDoubleColumn = _media.size.width > 639 &&
|
||||
Stores.setting.doubleColumnServersPage.fetch();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return OrientationBuilder(builder: (_, orientation) {
|
||||
if (orientation == Orientation.landscape) {
|
||||
final useFullScreen = Stores.setting.fullScreen.fetch();
|
||||
if (useFullScreen) return _buildLandscape();
|
||||
}
|
||||
return _buildPortrait();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPortrait() {
|
||||
return Scaffold(
|
||||
appBar: _TopBar(
|
||||
tags: ServerProvider.tags,
|
||||
onTagChanged: (p0) => _tag.value = p0,
|
||||
initTag: _tag.value,
|
||||
),
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _autoHideCtrl.show(),
|
||||
child: ListenableBuilder(
|
||||
listenable: Stores.setting.textFactor.listenable(),
|
||||
builder: (_, __) {
|
||||
_updateTextScaler();
|
||||
return _buildBody();
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: AutoHide(
|
||||
direction: AxisDirection.right,
|
||||
offset: 75,
|
||||
scrollController: _scrollController,
|
||||
hideController: _autoHideCtrl,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'addServer',
|
||||
onPressed: () => ServerEditPage.route.go(context),
|
||||
tooltip: libL10n.add,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLandscape() {
|
||||
final offset = Offset(_offset, _offset);
|
||||
return Padding(
|
||||
// Avoid display cutout
|
||||
padding: EdgeInsets.all(_offset.abs()),
|
||||
child: Transform.translate(
|
||||
offset: offset,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildLandscapeBody(),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => SettingsPage.route.go(context),
|
||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLandscapeBody() {
|
||||
return ServerProvider.serverOrder.listenVal((order) {
|
||||
if (order.isEmpty) {
|
||||
return Center(
|
||||
child: Text(libL10n.empty, textAlign: TextAlign.center),
|
||||
);
|
||||
}
|
||||
|
||||
return PageView.builder(
|
||||
itemCount: order.length,
|
||||
itemBuilder: (_, idx) {
|
||||
final id = order[idx];
|
||||
final srv = ServerProvider.pick(id: id);
|
||||
if (srv == null) return UIs.placeholder;
|
||||
|
||||
return srv.listenVal((srv) {
|
||||
final title = _buildServerCardTitle(srv);
|
||||
final List<Widget> children = [
|
||||
title,
|
||||
..._buildNormalCard(srv.status, srv.spi).joinWith(SizedBox(
|
||||
height: _media.size.height / 10,
|
||||
))
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: _media.padding,
|
||||
child: ListenableBuilder(
|
||||
listenable: _getCardNoti(id),
|
||||
builder: (_, __) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return ServerProvider.serverOrder.listenVal(
|
||||
(order) {
|
||||
if (order.isEmpty) {
|
||||
return Center(
|
||||
child: Text(libL10n.empty, textAlign: TextAlign.center),
|
||||
);
|
||||
}
|
||||
|
||||
return _tag.listenVal(
|
||||
(val) {
|
||||
final filtered = _filterServers(order);
|
||||
if (_useDoubleColumn &&
|
||||
Stores.setting.doubleColumnServersPage.fetch()) {
|
||||
return _buildBodyMedium(filtered);
|
||||
}
|
||||
return _buildBodySmall(filtered: filtered);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodySmall({
|
||||
required List<String> filtered,
|
||||
EdgeInsets? padding = const EdgeInsets.fromLTRB(7, 0, 7, 7),
|
||||
}) {
|
||||
final count = filtered.length + 1;
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: padding,
|
||||
itemCount: count,
|
||||
itemBuilder: (_, index) {
|
||||
// Issue #130
|
||||
if (index == count - 1) return UIs.height77;
|
||||
final vnode = ServerProvider.pick(id: filtered[index]);
|
||||
if (vnode == null) return UIs.placeholder;
|
||||
return vnode.listenVal(_buildEachServerCard);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodyMedium(List<String> filtered) {
|
||||
final mid = (filtered.length / 2).ceil();
|
||||
final filteredLeft = filtered.sublist(0, mid);
|
||||
final filteredRight = filtered.sublist(mid);
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildBodySmall(
|
||||
filtered: filteredLeft,
|
||||
padding: const EdgeInsets.only(left: 7),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildBodySmall(
|
||||
filtered: filteredRight,
|
||||
padding: const EdgeInsets.only(right: 7),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(Server? srv) {
|
||||
if (srv == null) {
|
||||
return UIs.placeholder;
|
||||
}
|
||||
|
||||
return CardX(
|
||||
key: Key(srv.spi.id + _tag.value),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (srv.canViewDetails) {
|
||||
AppRoutes.serverDetail(spi: srv.spi).go(context);
|
||||
} else {
|
||||
ServerEditPage.route.go(context, args: srv.spi);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (srv.conn == ServerConn.finished) {
|
||||
final id = srv.spi.id;
|
||||
final cardStatus = _getCardNoti(id);
|
||||
cardStatus.value = cardStatus.value.copyWith(
|
||||
flip: !cardStatus.value.flip,
|
||||
);
|
||||
} else {
|
||||
ServerEditPage.route.go(context, args: srv.spi);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: _cardPadSingle,
|
||||
right: 3,
|
||||
top: _cardPadSingle,
|
||||
bottom: _cardPadSingle,
|
||||
),
|
||||
child: _buildRealServerCard(srv),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The child's width mat not equal to 1/4 of the screen width,
|
||||
/// so we need to wrap it with a SizedBox.
|
||||
Widget _wrapWithSizedbox(Widget child, [bool circle = false]) {
|
||||
var width = (_media.size.width - _cardPad) / (circle ? 4 : 4.3);
|
||||
if (_useDoubleColumn) width /= 2;
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRealServerCard(Server srv) {
|
||||
final id = srv.spi.id;
|
||||
final cardStatus = _getCardNoti(id);
|
||||
final title = _buildServerCardTitle(srv);
|
||||
|
||||
return cardStatus.listenVal((_) {
|
||||
final List<Widget> children = [title];
|
||||
if (srv.conn == ServerConn.finished) {
|
||||
if (cardStatus.value.flip) {
|
||||
children.add(_buildFlippedCard(srv));
|
||||
} else {
|
||||
children.addAll(_buildNormalCard(srv.status, srv.spi));
|
||||
}
|
||||
}
|
||||
|
||||
final height = _calcCardHeight(srv.conn, cardStatus.value.flip);
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: height,
|
||||
// Use [OverflowBox] to dismiss the warning of [Column] overflow.
|
||||
child: OverflowBox(
|
||||
// If `height == _kCardHeightMin`, the `maxHeight` will be ignored.
|
||||
//
|
||||
// You can comment the `maxHeight` then connect&disconnect the server
|
||||
// to see the difference.
|
||||
maxHeight: height != _kCardHeightMin ? height : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFlippedCard(Server srv) {
|
||||
const color = Colors.grey;
|
||||
const textStyle = TextStyle(fontSize: 13, color: color);
|
||||
final children = [
|
||||
Btn.column(
|
||||
onTap: () => _askFor(
|
||||
func: () async {
|
||||
if (Stores.setting.showSuspendTip.fetch()) {
|
||||
await context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(l10n.suspendTip),
|
||||
);
|
||||
Stores.setting.showSuspendTip.put(false);
|
||||
}
|
||||
srv.client?.execWithPwd(
|
||||
ShellFunc.suspend.exec(srv.spi.id),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
);
|
||||
},
|
||||
typ: l10n.suspend,
|
||||
name: srv.spi.name,
|
||||
),
|
||||
icon: const Icon(Icons.stop, color: color),
|
||||
text: l10n.suspend,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
Btn.column(
|
||||
onTap: () => _askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.shutdown.exec(srv.spi.id),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
typ: l10n.shutdown,
|
||||
name: srv.spi.name,
|
||||
),
|
||||
icon: const Icon(Icons.power_off, color: color),
|
||||
text: l10n.shutdown,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
Btn.column(
|
||||
onTap: () => _askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.reboot.exec(srv.spi.id),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
typ: l10n.reboot,
|
||||
name: srv.spi.name,
|
||||
),
|
||||
icon: const Icon(Icons.restart_alt, color: color),
|
||||
text: l10n.reboot,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
Btn.column(
|
||||
onTap: () => ServerEditPage.route.go(context, args: srv.spi),
|
||||
icon: const Icon(Icons.edit, color: color),
|
||||
text: libL10n.edit,
|
||||
textStyle: textStyle,
|
||||
)
|
||||
];
|
||||
|
||||
final width = (_media.size.width - _cardPad) / children.length;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 9),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: children.map((e) {
|
||||
if (width == 0) return e;
|
||||
return SizedBox(width: width, child: e);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildNormalCard(ServerStatus ss, Spi spi) {
|
||||
return [
|
||||
UIs.height13,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), true),
|
||||
_wrapWithSizedbox(
|
||||
PercentCircle(percent: ss.mem.usedPercent * 100),
|
||||
true,
|
||||
),
|
||||
_wrapWithSizedbox(_buildNet(ss, spi.id)),
|
||||
_wrapWithSizedbox(_buildDisk(ss, spi.id)),
|
||||
],
|
||||
),
|
||||
UIs.height13,
|
||||
if (Stores.setting.moveServerFuncs.fetch() &&
|
||||
// Discussion #146
|
||||
!Stores.setting.serverTabUseOldUI.fetch())
|
||||
SizedBox(
|
||||
height: 27,
|
||||
child: ServerFuncBtns(spi: spi),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildServerCardTitle(Server s) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 7, right: 13),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: _media.size.width / 2.3),
|
||||
child: Hero(
|
||||
tag: 'home_card_title_${s.spi.id}',
|
||||
transitionOnUserGestures: true,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
s.spi.name,
|
||||
style: UIs.text13Bold.copyWith(
|
||||
color: context.isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
size: 17,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildTopRightText(s),
|
||||
_buildTopRightWidget(s),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopRightWidget(Server s) {
|
||||
final (child, onTap) = switch (s.conn) {
|
||||
ServerConn.connecting || ServerConn.loading || ServerConn.connected => (
|
||||
SizedBox(
|
||||
width: 19,
|
||||
height: 19,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(UIs.primaryColor),
|
||||
),
|
||||
),
|
||||
null,
|
||||
),
|
||||
ServerConn.failed => (
|
||||
const Icon(Icons.refresh, size: 21, color: Colors.grey),
|
||||
() {
|
||||
TryLimiter.reset(s.spi.id);
|
||||
ServerProvider.refresh(spi: s.spi);
|
||||
},
|
||||
),
|
||||
ServerConn.disconnected => (
|
||||
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
|
||||
() => ServerProvider.refresh(spi: s.spi)
|
||||
),
|
||||
ServerConn.finished => (
|
||||
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
|
||||
() => ServerProvider.closeServer(id: s.spi.id),
|
||||
),
|
||||
};
|
||||
|
||||
// Or the loading icon will be rescaled.
|
||||
final wrapped = child is SizedBox
|
||||
? child
|
||||
: SizedBox(height: _kCardHeightMin, width: 27, child: child);
|
||||
if (onTap == null) return wrapped.paddingOnly(left: 10);
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
onTap: onTap,
|
||||
child: wrapped,
|
||||
).paddingOnly(left: 5);
|
||||
}
|
||||
|
||||
Widget _buildTopRightText(Server s) {
|
||||
final hasErr = s.conn == ServerConn.failed && s.status.err != null;
|
||||
final str = s.getTopRightStr(s.spi);
|
||||
if (str == null) return UIs.placeholder;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!hasErr) return;
|
||||
_showFailReason(s.status);
|
||||
},
|
||||
child: Text(str, style: UIs.text13Grey),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFailReason(ServerStatus ss) {
|
||||
final md = '''
|
||||
${ss.err?.solution ?? l10n.unknown}
|
||||
|
||||
```sh
|
||||
${ss.err?.message ?? 'null'}
|
||||
''';
|
||||
context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: SingleChildScrollView(child: SimpleMarkdown(data: md)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Pfs.copy(md),
|
||||
child: Text(libL10n.copy),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisk(ServerStatus ss, String id) {
|
||||
final cardNoti = _getCardNoti(id);
|
||||
return ListenableBuilder(
|
||||
listenable: cardNoti,
|
||||
builder: (_, __) {
|
||||
final isSpeed = cardNoti.value.diskIO ??
|
||||
!Stores.setting.serverTabPreferDiskAmount.fetch();
|
||||
|
||||
final (r, w) = ss.diskIO.cachedAllSpeed;
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
child: _buildIOData(
|
||||
isSpeed
|
||||
? '${l10n.read}:\n$r'
|
||||
: 'Total:\n${ss.diskUsage?.size.kb2Str}',
|
||||
isSpeed
|
||||
? '${l10n.write}:\n$w'
|
||||
: 'Used:\n${ss.diskUsage?.used.kb2Str}',
|
||||
onTap: () {
|
||||
cardNoti.value = cardNoti.value.copyWith(diskIO: !isSpeed);
|
||||
},
|
||||
key: ValueKey(isSpeed),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNet(ServerStatus ss, String id) {
|
||||
final cardNoti = _getCardNoti(id);
|
||||
final type = cardNoti.value.net ?? Stores.setting.netViewType.fetch();
|
||||
final device = ServerProvider.pick(id: id)?.value.spi.custom?.netDev;
|
||||
final (a, b) = type.build(ss, dev: device);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
transitionBuilder: (c, anim) => FadeTransition(opacity: anim, child: c),
|
||||
child: _buildIOData(
|
||||
a,
|
||||
b,
|
||||
onTap: () => cardNoti.value = cardNoti.value.copyWith(net: type.next),
|
||||
key: ValueKey(type),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIOData(
|
||||
String up,
|
||||
String down, {
|
||||
void Function()? onTap,
|
||||
Key? key,
|
||||
}) {
|
||||
final child = Column(
|
||||
children: [
|
||||
Text(
|
||||
up,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
textScaler: _textFactor,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
down,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
textScaler: _textFactor,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (onTap == null) return child;
|
||||
return IconButton(
|
||||
key: key,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
onPressed: onTap,
|
||||
icon: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
ServerProvider.refresh();
|
||||
ServerProvider.startAutoRefresh();
|
||||
}
|
||||
|
||||
List<String> _filterServers(List<String> order) {
|
||||
final tag = _tag.value;
|
||||
if (tag == TagSwitcher.kDefaultTag) return order;
|
||||
return order.where((e) {
|
||||
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
||||
if (tags == null) return false;
|
||||
return tags.contains(tag);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static const _kCardHeightMin = 23.0;
|
||||
static const _kCardHeightFlip = 99.0;
|
||||
static const _kCardHeightNormal = 108.0;
|
||||
static const _kCardHeightMoveOutFuncs = 135.0;
|
||||
|
||||
double? _calcCardHeight(ServerConn cs, bool flip) {
|
||||
if (_textFactorDouble != 1.0) return null;
|
||||
if (cs != ServerConn.finished) {
|
||||
return _kCardHeightMin;
|
||||
}
|
||||
if (flip) {
|
||||
return _kCardHeightFlip;
|
||||
}
|
||||
if (Stores.setting.moveServerFuncs.fetch() &&
|
||||
// Discussion #146
|
||||
!Stores.setting.serverTabUseOldUI.fetch()) {
|
||||
return _kCardHeightMoveOutFuncs;
|
||||
}
|
||||
return _kCardHeightNormal;
|
||||
}
|
||||
|
||||
void _askFor({
|
||||
required void Function() func,
|
||||
required String typ,
|
||||
required String name,
|
||||
}) {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue('$typ ${l10n.server}($name)')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
func();
|
||||
},
|
||||
).toList,
|
||||
);
|
||||
}
|
||||
|
||||
_CardNotifier _getCardNoti(String id) => _cardsStatus.putIfAbsent(
|
||||
id,
|
||||
() => _CardNotifier(const _CardStatus()),
|
||||
);
|
||||
|
||||
void _updateOffset() {
|
||||
if (!Stores.setting.fullScreenJitter.fetch()) return;
|
||||
final x = _media.size.height * 0.03;
|
||||
final r = math.Random().nextDouble();
|
||||
final n = math.Random().nextBool() ? 1 : -1;
|
||||
_offset = x * r * n;
|
||||
}
|
||||
|
||||
void _updateTextScaler() {
|
||||
_textFactorDouble = Stores.setting.textFactor.fetch();
|
||||
_textFactor = TextScaler.linear(_textFactorDouble);
|
||||
}
|
||||
}
|
||||
|
||||
typedef _CardNotifier = ValueNotifier<_CardStatus>;
|
||||
|
||||
class _CardStatus {
|
||||
final bool flip;
|
||||
final bool? diskIO;
|
||||
final NetViewType? net;
|
||||
|
||||
const _CardStatus({
|
||||
this.flip = false,
|
||||
this.diskIO,
|
||||
this.net,
|
||||
});
|
||||
|
||||
_CardStatus copyWith({
|
||||
bool? flip,
|
||||
bool? diskIO,
|
||||
NetViewType? net,
|
||||
}) {
|
||||
return _CardStatus(
|
||||
flip: flip ?? this.flip,
|
||||
diskIO: diskIO ?? this.diskIO,
|
||||
net: net ?? this.net,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension _ServerX on Server {
|
||||
String? getTopRightStr(Spi spi) {
|
||||
switch (conn) {
|
||||
case ServerConn.disconnected:
|
||||
return null;
|
||||
case ServerConn.finished:
|
||||
// Highest priority of temperature display
|
||||
final cmdTemp = () {
|
||||
final val = status.customCmds['server_card_top_right'];
|
||||
if (val == null) return null;
|
||||
// This returned value is used on server card top right, so it should
|
||||
// be a single line string.
|
||||
return val.split('\n').lastOrNull;
|
||||
}();
|
||||
final temperatureVal = () {
|
||||
// Second priority
|
||||
final preferTempDev = spi.custom?.preferTempDev;
|
||||
if (preferTempDev != null) {
|
||||
final preferTemp = status.sensors
|
||||
.firstWhereOrNull((e) => e.device == preferTempDev)
|
||||
?.summary
|
||||
?.split(' ')
|
||||
.firstOrNull;
|
||||
if (preferTemp != null) {
|
||||
return double.tryParse(preferTemp.replaceFirst('°C', ''));
|
||||
}
|
||||
}
|
||||
// Last priority
|
||||
final temp = status.temps.first;
|
||||
if (temp != null) {
|
||||
return temp;
|
||||
}
|
||||
return null;
|
||||
}();
|
||||
final upTime = status.more[StatusCmdType.uptime];
|
||||
final items = [
|
||||
cmdTemp ??
|
||||
(temperatureVal != null
|
||||
? '${temperatureVal.toStringAsFixed(1)}°C'
|
||||
: null),
|
||||
upTime
|
||||
];
|
||||
final str = items.where((e) => e != null && e.isNotEmpty).join(' | ');
|
||||
if (str.isEmpty) return libL10n.empty;
|
||||
return str;
|
||||
case ServerConn.loading:
|
||||
return null;
|
||||
case ServerConn.connected:
|
||||
return null;
|
||||
case ServerConn.connecting:
|
||||
return null;
|
||||
case ServerConn.failed:
|
||||
return status.err != null ? l10n.viewErr : libL10n.fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
lib/view/page/server/tab/card_stat.dart
Normal file
27
lib/view/page/server/tab/card_stat.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
part of 'tab.dart';
|
||||
|
||||
typedef _CardNotifier = ValueNotifier<_CardStatus>;
|
||||
|
||||
class _CardStatus {
|
||||
final bool flip;
|
||||
final bool? diskIO;
|
||||
final NetViewType? net;
|
||||
|
||||
const _CardStatus({
|
||||
this.flip = false,
|
||||
this.diskIO,
|
||||
this.net,
|
||||
});
|
||||
|
||||
_CardStatus copyWith({
|
||||
bool? flip,
|
||||
bool? diskIO,
|
||||
NetViewType? net,
|
||||
}) {
|
||||
return _CardStatus(
|
||||
flip: flip ?? this.flip,
|
||||
diskIO: diskIO ?? this.diskIO,
|
||||
net: net ?? this.net,
|
||||
);
|
||||
}
|
||||
}
|
||||
195
lib/view/page/server/tab/content.dart
Normal file
195
lib/view/page/server/tab/content.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
part of 'tab.dart';
|
||||
|
||||
extension on _ServerPageState {
|
||||
Widget _buildServerCardTitle(Server s) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 7, right: 13),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: _media.size.width / 2.3),
|
||||
child: Hero(
|
||||
tag: 'home_card_title_${s.spi.id}',
|
||||
transitionOnUserGestures: true,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
s.spi.name,
|
||||
style: UIs.text13Bold.copyWith(
|
||||
color: context.isDark ? Colors.white : Colors.black,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.keyboard_arrow_right,
|
||||
size: 17,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildTopRightText(s),
|
||||
_buildTopRightWidget(s),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopRightWidget(Server s) {
|
||||
final (child, onTap) = switch (s.conn) {
|
||||
ServerConn.connecting || ServerConn.loading || ServerConn.connected => (
|
||||
SizedBox(
|
||||
width: 19,
|
||||
height: 19,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(UIs.primaryColor),
|
||||
),
|
||||
),
|
||||
null,
|
||||
),
|
||||
ServerConn.failed => (
|
||||
const Icon(Icons.refresh, size: 21, color: Colors.grey),
|
||||
() {
|
||||
TryLimiter.reset(s.spi.id);
|
||||
ServerProvider.refresh(spi: s.spi);
|
||||
},
|
||||
),
|
||||
ServerConn.disconnected => (
|
||||
const Icon(MingCute.link_3_line, size: 19, color: Colors.grey),
|
||||
() => ServerProvider.refresh(spi: s.spi)
|
||||
),
|
||||
ServerConn.finished => (
|
||||
const Icon(MingCute.unlink_2_line, size: 17, color: Colors.grey),
|
||||
() => ServerProvider.closeServer(id: s.spi.id),
|
||||
),
|
||||
};
|
||||
|
||||
// Or the loading icon will be rescaled.
|
||||
final wrapped = child is SizedBox
|
||||
? child
|
||||
: SizedBox(height: _ServerPageState._kCardHeightMin, width: 27, child: child);
|
||||
if (onTap == null) return wrapped.paddingOnly(left: 10);
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
onTap: onTap,
|
||||
child: wrapped,
|
||||
).paddingOnly(left: 5);
|
||||
}
|
||||
|
||||
Widget _buildTopRightText(Server s) {
|
||||
final hasErr = s.conn == ServerConn.failed && s.status.err != null;
|
||||
final str = s._getTopRightStr(s.spi);
|
||||
if (str == null) return UIs.placeholder;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!hasErr) return;
|
||||
_showFailReason(s.status);
|
||||
},
|
||||
child: Text(str, style: UIs.text13Grey),
|
||||
);
|
||||
}
|
||||
|
||||
void _showFailReason(ServerStatus ss) {
|
||||
final md = '''
|
||||
${ss.err?.solution ?? l10n.unknown}
|
||||
|
||||
```sh
|
||||
${ss.err?.message ?? 'null'}
|
||||
''';
|
||||
context.showRoundDialog(
|
||||
title: libL10n.error,
|
||||
child: SingleChildScrollView(child: SimpleMarkdown(data: md)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Pfs.copy(md),
|
||||
child: Text(libL10n.copy),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisk(ServerStatus ss, String id) {
|
||||
final cardNoti = _getCardNoti(id);
|
||||
return ListenableBuilder(
|
||||
listenable: cardNoti,
|
||||
builder: (_, __) {
|
||||
final isSpeed = cardNoti.value.diskIO ?? !Stores.setting.serverTabPreferDiskAmount.fetch();
|
||||
|
||||
final (r, w) = ss.diskIO.cachedAllSpeed;
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
child: _buildIOData(
|
||||
isSpeed ? '${l10n.read}:\n$r' : 'Total:\n${ss.diskUsage?.size.kb2Str}',
|
||||
isSpeed ? '${l10n.write}:\n$w' : 'Used:\n${ss.diskUsage?.used.kb2Str}',
|
||||
onTap: () {
|
||||
cardNoti.value = cardNoti.value.copyWith(diskIO: !isSpeed);
|
||||
},
|
||||
key: ValueKey(isSpeed),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNet(ServerStatus ss, String id) {
|
||||
final cardNoti = _getCardNoti(id);
|
||||
final type = cardNoti.value.net ?? Stores.setting.netViewType.fetch();
|
||||
final device = ServerProvider.pick(id: id)?.value.spi.custom?.netDev;
|
||||
final (a, b) = type.build(ss, dev: device);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
transitionBuilder: (c, anim) => FadeTransition(opacity: anim, child: c),
|
||||
child: _buildIOData(
|
||||
a,
|
||||
b,
|
||||
onTap: () => cardNoti.value = cardNoti.value.copyWith(net: type.next),
|
||||
key: ValueKey(type),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIOData(
|
||||
String up,
|
||||
String down, {
|
||||
void Function()? onTap,
|
||||
Key? key,
|
||||
int maxLines = 2
|
||||
}) {
|
||||
final child = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
up,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
textScaler: _textFactor,
|
||||
maxLines: maxLines,
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
down,
|
||||
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
textScaler: _textFactor,
|
||||
maxLines: maxLines,
|
||||
)
|
||||
],
|
||||
);
|
||||
if (onTap == null) return child;
|
||||
return IconButton(
|
||||
key: key,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
onPressed: onTap,
|
||||
icon: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/view/page/server/tab/landscape.dart
Normal file
69
lib/view/page/server/tab/landscape.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
part of 'tab.dart';
|
||||
|
||||
extension on _ServerPageState {
|
||||
Widget _buildLandscape() {
|
||||
final offset = Offset(_offset, _offset);
|
||||
return Padding(
|
||||
// Avoid display cutout
|
||||
padding: EdgeInsets.all(_offset.abs()),
|
||||
child: Transform.translate(
|
||||
offset: offset,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildLandscapeBody(),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: IconButton(
|
||||
onPressed: () => SettingsPage.route.go(context),
|
||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLandscapeBody() {
|
||||
return ServerProvider.serverOrder.listenVal((order) {
|
||||
if (order.isEmpty) {
|
||||
return Center(
|
||||
child: Text(libL10n.empty, textAlign: TextAlign.center),
|
||||
);
|
||||
}
|
||||
|
||||
return PageView.builder(
|
||||
itemCount: order.length,
|
||||
itemBuilder: (_, idx) {
|
||||
final id = order[idx];
|
||||
final srv = ServerProvider.pick(id: id);
|
||||
if (srv == null) return UIs.placeholder;
|
||||
|
||||
return srv.listenVal((srv) {
|
||||
final title = _buildServerCardTitle(srv);
|
||||
final List<Widget> children = [
|
||||
title,
|
||||
_buildNormalCard(srv.status, srv.spi),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: _media.padding,
|
||||
child: ListenableBuilder(
|
||||
listenable: _getCardNoti(id),
|
||||
builder: (_, __) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
369
lib/view/page/server/tab/tab.dart
Normal file
369
lib/view/page/server/tab/tab.dart
Normal file
@@ -0,0 +1,369 @@
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/core/route.dart';
|
||||
import 'package:server_box/data/model/app/shell_func.dart';
|
||||
import 'package:server_box/data/model/server/try_limiter.dart';
|
||||
import 'package:server_box/data/res/build_data.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
import 'package:server_box/view/page/server/detail/view.dart';
|
||||
import 'package:server_box/view/page/server/edit.dart';
|
||||
import 'package:server_box/view/page/setting/entry.dart';
|
||||
import 'package:server_box/view/widget/percent_circle.dart';
|
||||
|
||||
import 'package:server_box/data/model/app/net_view.dart';
|
||||
import 'package:server_box/data/model/server/server.dart';
|
||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||
import 'package:server_box/data/provider/server.dart';
|
||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||
|
||||
part 'top_bar.dart';
|
||||
part 'card_stat.dart';
|
||||
part 'utils.dart';
|
||||
part 'content.dart';
|
||||
part 'landscape.dart';
|
||||
|
||||
class ServerPage extends StatefulWidget {
|
||||
const ServerPage({super.key});
|
||||
|
||||
@override
|
||||
State<ServerPage> createState() => _ServerPageState();
|
||||
|
||||
static const route = AppRouteNoArg(
|
||||
page: ServerPage.new,
|
||||
path: '/servers',
|
||||
);
|
||||
}
|
||||
|
||||
const _cardPad = 74.0;
|
||||
const _cardPadSingle = 13.0;
|
||||
|
||||
class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMixin, AfterLayoutMixin {
|
||||
late MediaQueryData _media;
|
||||
|
||||
late double _textFactorDouble;
|
||||
double _offset = 1;
|
||||
late TextScaler _textFactor;
|
||||
|
||||
final _cardsStatus = <String, _CardNotifier>{};
|
||||
|
||||
Timer? _timer;
|
||||
|
||||
final _tag = ''.vn;
|
||||
|
||||
final _scrollController = ScrollController();
|
||||
final _autoHideCtrl = AutoHideController();
|
||||
final _splitViewCtrl = SplitViewController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer?.cancel();
|
||||
_scrollController.dispose();
|
||||
_autoHideCtrl.dispose();
|
||||
_tag.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startAvoidJitterTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_media = MediaQuery.of(context);
|
||||
_updateOffset();
|
||||
_updateTextScaler();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return OrientationBuilder(builder: (_, orientation) {
|
||||
if (orientation == Orientation.landscape) {
|
||||
final useFullScreen = Stores.setting.fullScreen.fetch();
|
||||
// Only enter landscape mode when the screen is wide enough and the
|
||||
// full screen mode is enabled.
|
||||
if (useFullScreen) return _buildLandscape();
|
||||
}
|
||||
return _buildPortrait();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildScaffold(Widget child) {
|
||||
return Scaffold(
|
||||
appBar: _TopBar(
|
||||
tags: ServerProvider.tags,
|
||||
onTagChanged: (p0) => _tag.value = p0,
|
||||
initTag: _tag.value,
|
||||
),
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _autoHideCtrl.show(),
|
||||
child: ListenableBuilder(
|
||||
listenable: Stores.setting.textFactor.listenable(),
|
||||
builder: (_, __) {
|
||||
_updateTextScaler();
|
||||
return child;
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: AutoHide(
|
||||
direction: AxisDirection.right,
|
||||
offset: 75,
|
||||
scrollController: _scrollController,
|
||||
hideController: _autoHideCtrl,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'addServer',
|
||||
onPressed: _onTapAddServer,
|
||||
tooltip: libL10n.add,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPortrait() {
|
||||
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||
return ServerProvider.serverOrder.listenVal(
|
||||
(order) {
|
||||
return _tag.listenVal(
|
||||
(val) {
|
||||
final filtered = _filterServers(order);
|
||||
final child = _buildScaffold(_buildBodySmall(filtered: filtered));
|
||||
// if (isMobile) {
|
||||
return child;
|
||||
// }
|
||||
|
||||
// return SplitView(
|
||||
// controller: _splitViewCtrl,
|
||||
// leftWeight: 1,
|
||||
// rightWeight: 1.3,
|
||||
// initialRight: Center(child: CircularProgressIndicator()),
|
||||
// leftBuilder: (_, __) => child,
|
||||
// );
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBodySmall({
|
||||
required List<String> filtered,
|
||||
EdgeInsets? padding = const EdgeInsets.fromLTRB(7, 0, 7, 7),
|
||||
}) {
|
||||
if (filtered.isEmpty) {
|
||||
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
|
||||
}
|
||||
|
||||
// Calculate number of columns based on available width
|
||||
final columnsCount = math.max(1, (_media.size.width / UIs.columnWidth).floor());
|
||||
|
||||
// Calculate number of rows needed
|
||||
final rowCount = (filtered.length + columnsCount - 1) ~/ columnsCount;
|
||||
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: padding,
|
||||
itemCount: rowCount + 1, // +1 for the bottom space
|
||||
itemBuilder: (_, rowIndex) {
|
||||
// Bottom space
|
||||
if (rowIndex == rowCount) return UIs.height77;
|
||||
|
||||
// Create a row of server cards
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: List.generate(columnsCount, (colIndex) {
|
||||
final index = rowIndex * columnsCount + colIndex;
|
||||
if (index >= filtered.length) return Expanded(child: Container());
|
||||
|
||||
final vnode = ServerProvider.pick(id: filtered[index]);
|
||||
if (vnode == null) return Expanded(child: UIs.placeholder);
|
||||
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: vnode.listenVal(_buildEachServerCard),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEachServerCard(Server? srv) {
|
||||
if (srv == null) {
|
||||
return UIs.placeholder;
|
||||
}
|
||||
|
||||
return CardX(
|
||||
key: Key(srv.spi.id + _tag.value),
|
||||
child: InkWell(
|
||||
onTap: () => _onTapCard(srv),
|
||||
onLongPress: () => _onLongPressCard(srv),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: _cardPadSingle,
|
||||
right: 3,
|
||||
top: _cardPadSingle,
|
||||
bottom: _cardPadSingle,
|
||||
),
|
||||
child: _buildRealServerCard(srv),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The child's width mat not equal to 1/4 of the screen width,
|
||||
/// so we need to wrap it with a SizedBox.
|
||||
Widget _wrapWithSizedbox(Widget child, double maxWidth, [bool circle = false]) {
|
||||
return LayoutBuilder(builder: (_, cons) {
|
||||
final width = (maxWidth - _cardPad) / 4;
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildRealServerCard(Server srv) {
|
||||
final id = srv.spi.id;
|
||||
final cardStatus = _getCardNoti(id);
|
||||
final title = _buildServerCardTitle(srv);
|
||||
|
||||
return cardStatus.listenVal((_) {
|
||||
final List<Widget> children = [title];
|
||||
if (srv.conn == ServerConn.finished) {
|
||||
if (cardStatus.value.flip) {
|
||||
children.add(_buildFlippedCard(srv));
|
||||
} else {
|
||||
children.add(_buildNormalCard(srv.status, srv.spi));
|
||||
}
|
||||
}
|
||||
|
||||
final height = _calcCardHeight(srv.conn, cardStatus.value.flip);
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 377),
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: height,
|
||||
// Use [OverflowBox] to dismiss the warning of [Column] overflow.
|
||||
child: OverflowBox(
|
||||
// If `height == _kCardHeightMin`, the `maxHeight` will be ignored.
|
||||
//
|
||||
// You can comment the `maxHeight` then connect&disconnect the server
|
||||
// to see the difference.
|
||||
maxHeight: height != _kCardHeightMin ? height : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFlippedCard(Server srv) {
|
||||
const color = Colors.grey;
|
||||
const textStyle = TextStyle(fontSize: 13, color: color);
|
||||
final children = [
|
||||
Btn.column(
|
||||
onTap: () => _onTapSuspend(srv),
|
||||
icon: const Icon(Icons.stop, color: color),
|
||||
text: l10n.suspend,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
Btn.column(
|
||||
onTap: () => _onTapShutdown(srv),
|
||||
icon: const Icon(Icons.power_off, color: color),
|
||||
text: l10n.shutdown,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
Btn.column(
|
||||
onTap: () => _onTapReboot(srv),
|
||||
icon: const Icon(Icons.restart_alt, color: color),
|
||||
text: l10n.reboot,
|
||||
textStyle: textStyle,
|
||||
),
|
||||
Btn.column(
|
||||
onTap: () => _onTapEdit(srv),
|
||||
icon: const Icon(Icons.edit, color: color),
|
||||
text: libL10n.edit,
|
||||
textStyle: textStyle,
|
||||
)
|
||||
];
|
||||
|
||||
final width = (_media.size.width - _cardPad) / children.length;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 9),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: children.map((e) {
|
||||
if (width == 0) return e;
|
||||
return SizedBox(width: width, child: e);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNormalCard(ServerStatus ss, Spi spi) {
|
||||
return LayoutBuilder(builder: (_, cons) {
|
||||
final maxWidth = cons.maxWidth;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
UIs.height13,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_wrapWithSizedbox(PercentCircle(percent: ss.cpu.usedPercent()), maxWidth, true),
|
||||
_wrapWithSizedbox(
|
||||
PercentCircle(percent: ss.mem.usedPercent * 100),
|
||||
maxWidth,
|
||||
true,
|
||||
),
|
||||
_wrapWithSizedbox(_buildNet(ss, spi.id), maxWidth),
|
||||
_wrapWithSizedbox(_buildDisk(ss, spi.id), maxWidth),
|
||||
],
|
||||
),
|
||||
UIs.height13,
|
||||
if (Stores.setting.moveServerFuncs.fetch() &&
|
||||
// Discussion #146
|
||||
!Stores.setting.serverTabUseOldUI.fetch())
|
||||
SizedBox(
|
||||
height: 27,
|
||||
child: ServerFuncBtns(spi: spi),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Future<void> afterFirstLayout(BuildContext context) async {
|
||||
ServerProvider.refresh();
|
||||
ServerProvider.startAutoRefresh();
|
||||
}
|
||||
|
||||
static const _kCardHeightMin = 23.0;
|
||||
static const _kCardHeightFlip = 99.0;
|
||||
static const _kCardHeightNormal = 108.0;
|
||||
static const _kCardHeightMoveOutFuncs = 135.0;
|
||||
}
|
||||
@@ -13,6 +13,9 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||
if (!isMobile) return UIs.placeholder;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Row(
|
||||
243
lib/view/page/server/tab/utils.dart
Normal file
243
lib/view/page/server/tab/utils.dart
Normal file
@@ -0,0 +1,243 @@
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
part of 'tab.dart';
|
||||
|
||||
extension _Actions on _ServerPageState {
|
||||
void _onTapCard(Server srv) {
|
||||
if (srv.canViewDetails) {
|
||||
// _splitViewCtrl.replace(ServerDetailPage(
|
||||
// key: ValueKey(srv.spi.id),
|
||||
// args: SpiRequiredArgs(srv.spi),
|
||||
// ));
|
||||
ServerDetailPage.route.go(
|
||||
context,
|
||||
SpiRequiredArgs(srv.spi),
|
||||
);
|
||||
} else {
|
||||
// _splitViewCtrl.replace(ServerEditPage(
|
||||
// key: ValueKey(srv.spi.id),
|
||||
// args: SpiRequiredArgs(srv.spi),
|
||||
// ));
|
||||
ServerEditPage.route.go(
|
||||
context,
|
||||
args: SpiRequiredArgs(srv.spi),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onLongPressCard(Server srv) {
|
||||
if (srv.conn == ServerConn.finished) {
|
||||
final id = srv.spi.id;
|
||||
final cardStatus = _getCardNoti(id);
|
||||
cardStatus.value = cardStatus.value.copyWith(
|
||||
flip: !cardStatus.value.flip,
|
||||
);
|
||||
} else {
|
||||
_splitViewCtrl.replace(ServerEditPage(
|
||||
key: ValueKey(srv.spi.id),
|
||||
args: SpiRequiredArgs(srv.spi),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapAddServer() {
|
||||
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
|
||||
// if (isMobile) {
|
||||
ServerEditPage.route.go(context);
|
||||
// } else {
|
||||
// _splitViewCtrl.replace(const ServerEditPage(
|
||||
// key: ValueKey('addServer'),
|
||||
// ));
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
extension _Operation on _ServerPageState {
|
||||
void _onTapSuspend(Server srv) {
|
||||
_askFor(
|
||||
func: () async {
|
||||
if (Stores.setting.showSuspendTip.fetch()) {
|
||||
await context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(l10n.suspendTip),
|
||||
);
|
||||
Stores.setting.showSuspendTip.put(false);
|
||||
}
|
||||
srv.client?.execWithPwd(
|
||||
ShellFunc.suspend.exec(srv.spi.id),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
);
|
||||
},
|
||||
typ: l10n.suspend,
|
||||
name: srv.spi.name,
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapShutdown(Server srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.shutdown.exec(srv.spi.id),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
typ: l10n.shutdown,
|
||||
name: srv.spi.name,
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapReboot(Server srv) {
|
||||
_askFor(
|
||||
func: () => srv.client?.execWithPwd(
|
||||
ShellFunc.reboot.exec(srv.spi.id),
|
||||
context: context,
|
||||
id: srv.id,
|
||||
),
|
||||
typ: l10n.reboot,
|
||||
name: srv.spi.name,
|
||||
);
|
||||
}
|
||||
|
||||
void _onTapEdit(Server srv) {
|
||||
if (srv.canViewDetails) {
|
||||
_splitViewCtrl.replace(ServerDetailPage(
|
||||
key: ValueKey(srv.spi.id),
|
||||
args: SpiRequiredArgs(srv.spi),
|
||||
));
|
||||
} else {
|
||||
_splitViewCtrl.replace(ServerEditPage(
|
||||
key: ValueKey(srv.spi.id),
|
||||
args: SpiRequiredArgs(srv.spi),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _Utils on _ServerPageState {
|
||||
List<String> _filterServers(List<String> order) {
|
||||
final tag = _tag.value;
|
||||
if (tag == TagSwitcher.kDefaultTag) return order;
|
||||
return order.where((e) {
|
||||
final tags = ServerProvider.pick(id: e)?.value.spi.tags;
|
||||
if (tags == null) return false;
|
||||
return tags.contains(tag);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
double? _calcCardHeight(ServerConn cs, bool flip) {
|
||||
if (_textFactorDouble != 1.0) return null;
|
||||
if (cs != ServerConn.finished) {
|
||||
return _ServerPageState._kCardHeightMin;
|
||||
}
|
||||
if (flip) {
|
||||
return _ServerPageState._kCardHeightFlip;
|
||||
}
|
||||
if (Stores.setting.moveServerFuncs.fetch() &&
|
||||
// Discussion #146
|
||||
!Stores.setting.serverTabUseOldUI.fetch()) {
|
||||
return _ServerPageState._kCardHeightMoveOutFuncs;
|
||||
}
|
||||
return _ServerPageState._kCardHeightNormal;
|
||||
}
|
||||
|
||||
void _askFor({
|
||||
required void Function() func,
|
||||
required String typ,
|
||||
required String name,
|
||||
}) {
|
||||
context.showRoundDialog(
|
||||
title: libL10n.attention,
|
||||
child: Text(libL10n.askContinue('$typ ${l10n.server}($name)')),
|
||||
actions: Btn.ok(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
func();
|
||||
},
|
||||
).toList,
|
||||
);
|
||||
}
|
||||
|
||||
_CardNotifier _getCardNoti(String id) => _cardsStatus.putIfAbsent(
|
||||
id,
|
||||
() => _CardNotifier(const _CardStatus()),
|
||||
);
|
||||
|
||||
void _updateOffset() {
|
||||
if (!Stores.setting.fullScreenJitter.fetch()) return;
|
||||
final x = _media.size.height * 0.03;
|
||||
final r = math.Random().nextDouble();
|
||||
final n = math.Random().nextBool() ? 1 : -1;
|
||||
_offset = x * r * n;
|
||||
}
|
||||
|
||||
void _updateTextScaler() {
|
||||
_textFactorDouble = Stores.setting.textFactor.fetch();
|
||||
_textFactor = TextScaler.linear(_textFactorDouble);
|
||||
}
|
||||
|
||||
void _startAvoidJitterTimer() {
|
||||
if (!Stores.setting.fullScreenJitter.fetch()) return;
|
||||
_timer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
if (mounted) {
|
||||
_updateOffset();
|
||||
setState(() {});
|
||||
} else {
|
||||
_timer?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension _ServerX on Server {
|
||||
String? _getTopRightStr(Spi spi) {
|
||||
switch (conn) {
|
||||
case ServerConn.disconnected:
|
||||
return null;
|
||||
case ServerConn.finished:
|
||||
// Highest priority of temperature display
|
||||
final cmdTemp = () {
|
||||
final val = status.customCmds['server_card_top_right'];
|
||||
if (val == null) return null;
|
||||
// This returned value is used on server card top right, so it should
|
||||
// be a single line string.
|
||||
return val.split('\n').lastOrNull;
|
||||
}();
|
||||
final temperatureVal = () {
|
||||
// Second priority
|
||||
final preferTempDev = spi.custom?.preferTempDev;
|
||||
if (preferTempDev != null) {
|
||||
final preferTemp = status.sensors
|
||||
.firstWhereOrNull((e) => e.device == preferTempDev)
|
||||
?.summary
|
||||
?.split(' ')
|
||||
.firstOrNull;
|
||||
if (preferTemp != null) {
|
||||
return double.tryParse(preferTemp.replaceFirst('°C', ''));
|
||||
}
|
||||
}
|
||||
// Last priority
|
||||
final temp = status.temps.first;
|
||||
if (temp != null) {
|
||||
return temp;
|
||||
}
|
||||
return null;
|
||||
}();
|
||||
final upTime = status.more[StatusCmdType.uptime];
|
||||
final items = [
|
||||
cmdTemp ?? (temperatureVal != null ? '${temperatureVal.toStringAsFixed(1)}°C' : null),
|
||||
upTime
|
||||
];
|
||||
final str = items.where((e) => e != null && e.isNotEmpty).join(' | ');
|
||||
if (str.isEmpty) return libL10n.empty;
|
||||
return str;
|
||||
case ServerConn.loading:
|
||||
return null;
|
||||
case ServerConn.connected:
|
||||
return null;
|
||||
case ServerConn.connecting:
|
||||
return null;
|
||||
case ServerConn.failed:
|
||||
return status.err != null ? l10n.viewErr : libL10n.fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user