mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
794 lines
22 KiB
Dart
794 lines
22 KiB
Dart
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/store.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';
|
|
|
|
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 _autoHideKey = GlobalKey<AutoHideState>();
|
|
|
|
@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: TagSwitcher(
|
|
tags: ServerProvider.tags,
|
|
onTagChanged: (p0) => _tag.value = p0,
|
|
initTag: _tag.value,
|
|
),
|
|
body: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => _autoHideKey.currentState?.show(),
|
|
child: ListenableBuilder(
|
|
listenable: Stores.setting.textFactor.listenable(),
|
|
builder: (_, __) {
|
|
_updateTextScaler();
|
|
return _buildBody();
|
|
},
|
|
),
|
|
),
|
|
floatingActionButton: AutoHide(
|
|
key: _autoHideKey,
|
|
direction: AxisDirection.right,
|
|
offset: 75,
|
|
controller: _scrollController,
|
|
child: FloatingActionButton(
|
|
heroTag: 'addServer',
|
|
onPressed: () => AppRoutes.serverEdit().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: () => AppRoutes.settings().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 {
|
|
AppRoutes.serverEdit(spi: srv.spi).go(context);
|
|
}
|
|
},
|
|
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 {
|
|
AppRoutes.serverEdit(spi: srv.spi).go(context);
|
|
}
|
|
},
|
|
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 ListenableBuilder(
|
|
listenable: cardStatus,
|
|
builder: (_, __) {
|
|
final List<Widget> children = [title];
|
|
if (srv.conn == ServerConn.finished) {
|
|
if (cardStatus.value.flip) {
|
|
children.addAll(_buildFlippedCard(srv));
|
|
} else {
|
|
children.addAll(_buildNormalCard(srv.status, srv.spi));
|
|
}
|
|
}
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 377),
|
|
curve: Curves.fastEaseInToSlowEaseOut,
|
|
height: _calcCardHeight(srv.conn, cardStatus.value.flip),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: children,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildFlippedCard(Server srv) {
|
|
const textStyle = TextStyle(color: Colors.grey);
|
|
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: Colors.grey),
|
|
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: Colors.grey),
|
|
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: Colors.grey),
|
|
text: l10n.reboot,
|
|
textStyle: textStyle,
|
|
),
|
|
Btn.column(
|
|
onTap: () => AppRoutes.serverEdit(spi: srv.spi).go(context),
|
|
icon: const Icon(Icons.edit, color: Colors.grey),
|
|
text: libL10n.edit,
|
|
textStyle: textStyle,
|
|
)
|
|
];
|
|
|
|
final width = (_media.size.width - _cardPad) / children.length;
|
|
return [
|
|
UIs.height13,
|
|
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: Text(
|
|
s.spi.name,
|
|
style: UIs.text13Bold,
|
|
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),
|
|
),
|
|
_ when Stores.setting.serverTabUseOldUI.fetch() => (
|
|
ServerFuncBtnsTopRight(spi: s.spi),
|
|
null,
|
|
),
|
|
};
|
|
|
|
// 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 == 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;
|
|
}
|
|
}
|
|
}
|