feat: Windows compatibility (#836)

* feat: win compatibility

* fix

* fix: uptime parse

* opt.: linux uptime accuracy

* fix: windows temperature fetching

* opt.

* opt.: powershell exec

* refactor: address PR review feedback and improve code quality

### Major Improvements:
- **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability
- **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider
- **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions

### Bug Fixes:
- **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages)
- **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging
- **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation
- **Fixed division by zero**: Added safety checks in Swap.usedPercent getter

### Code Quality Enhancements:
- **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach
- **Strengthened error handling**: Added detailed logging and validation throughout parsing methods
- **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings
- **Better separation of concerns**: Each parsing method now has single responsibility

### Files Changed:
- `lib/data/helper/system_detector.dart` (new): System detection helper
- `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation
- `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection
- `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods
- `lib/data/provider/server.dart`: Improved synchronization and extracted system detection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: parse & shell fn struct

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-08 16:56:36 +08:00
committed by GitHub
parent 46a12bc844
commit 3a615449e3
103 changed files with 9591 additions and 1906 deletions

View File

@@ -76,9 +76,9 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
initiallyExpanded: false,
children: [
ListTile(
title: Text(libL10n.backup),
trailing: const Icon(Icons.save),
onTap: () => BackupService.backup(context, FileBackupSource())
title: Text(libL10n.backup),
trailing: const Icon(Icons.save),
onTap: () => BackupService.backup(context, FileBackupSource()),
),
ListTile(
trailing: const Icon(Icons.restore),
@@ -264,7 +264,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
).cardx;
}
Future<void> _onTapWebdavDl(BuildContext context) async {
webdavLoading.value = true;
try {
@@ -357,7 +356,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
}
}
void _onBulkImportServers(BuildContext context) async {
final data = await context.showImportDialog(title: l10n.server, modelDef: Spix.example.toJson());
if (data == null) return;
@@ -394,11 +392,6 @@ final class _BackupPageState extends State<BackupPage> with AutomaticKeepAliveCl
}
}
@override
bool get wantKeepAlive => true;
}

View File

@@ -15,4 +15,4 @@ enum _PruneTypes {
_ => null,
};
}
}
}

View File

@@ -17,10 +17,7 @@ class HomePage extends StatefulWidget {
@override
State<HomePage> createState() => _HomePageState();
static const route = AppRouteNoArg(
page: HomePage.new,
path: '/',
);
static const route = AppRouteNoArg(page: HomePage.new, path: '/');
}
class _HomePageState extends State<HomePage>
@@ -181,11 +178,7 @@ class _HomePageState extends State<HomePage>
//_reqNotiPerm();
if (Stores.setting.autoCheckAppUpdate.fetch()) {
AppUpdateIface.doUpdate(
build: BuildData.build,
url: Urls.updateCfg,
context: context,
);
AppUpdateIface.doUpdate(build: BuildData.build, url: Urls.updateCfg, context: context);
}
MethodChans.updateHomeWidget();
await ServerProvider.refresh();
@@ -216,10 +209,7 @@ class _HomePageState extends State<HomePage>
void _goAuth() {
if (Stores.setting.useBioAuth.fetch()) {
if (LocalAuthPage.route.alreadyIn) return;
LocalAuthPage.route.go(
context,
args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false),
);
LocalAuthPage.route.go(context, args: LocalAuthPageArgs(onAuthSuccess: () => _shouldAuth = false));
}
}
@@ -245,9 +235,7 @@ final class _AppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: preferredSize.height,
);
return SizedBox(height: preferredSize.height);
}
@override

View File

@@ -4,7 +4,6 @@ import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/view/page/ssh/page/page.dart';
class IPerfPage extends StatefulWidget {
final SpiRequiredArgs args;
@@ -13,10 +12,7 @@ class IPerfPage extends StatefulWidget {
@override
State<IPerfPage> createState() => _IPerfPageState();
static const route = AppRouteArg<void, SpiRequiredArgs>(
page: IPerfPage.new,
path: '/iperf',
);
static const route = AppRouteArg<void, SpiRequiredArgs>(page: IPerfPage.new, path: '/iperf');
}
class _IPerfPageState extends State<IPerfPage> {
@@ -33,9 +29,7 @@ class _IPerfPageState extends State<IPerfPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: const Text('iperf'),
),
appBar: CustomAppBar(title: const Text('iperf')),
body: _buildBody(),
floatingActionButton: _buildFAB(),
);
@@ -63,12 +57,7 @@ class _IPerfPageState extends State<IPerfPage> {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 17),
children: [
Input(
controller: _hostCtrl,
label: l10n.host,
icon: Icons.computer,
suggestion: false,
),
Input(controller: _hostCtrl, label: l10n.host, icon: Icons.computer, suggestion: false),
Input(
controller: _portCtrl,
label: l10n.port,

View File

@@ -24,10 +24,7 @@ class PrivateKeyEditPage extends StatefulWidget {
@override
State<PrivateKeyEditPage> createState() => _PrivateKeyEditPageState();
static const route = AppRoute(
page: PrivateKeyEditPage.new,
path: '/private_key/edit',
);
static const route = AppRoute(page: PrivateKeyEditPage.new, path: '/private_key/edit');
}
class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
@@ -82,11 +79,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: _buildBody(),
floatingActionButton: _buildFAB(),
);
return Scaffold(appBar: _buildAppBar(), body: _buildBody(), floatingActionButton: _buildFAB());
}
CustomAppBar _buildAppBar() {
@@ -98,9 +91,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.privateKey}(${pki.id})',
)),
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.privateKey}(${pki.id})')),
actions: Btn.ok(
onTap: () {
PrivateKeyProvider.delete(pki);
@@ -112,13 +103,10 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
);
},
icon: const Icon(Icons.delete),
)
),
]
: null;
return CustomAppBar(
title: Text(libL10n.edit),
actions: actions,
);
return CustomAppBar(title: Text(libL10n.edit), actions: actions);
}
String _standardizeLineSeparators(String value) {
@@ -126,11 +114,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
}
Widget _buildFAB() {
return FloatingActionButton(
tooltip: l10n.save,
onPressed: _onTapSave,
child: const Icon(Icons.save),
);
return FloatingActionButton(tooltip: l10n.save, onPressed: _onTapSave, child: const Icon(Icons.save));
}
Widget _buildBody() {
@@ -170,11 +154,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
final size = (await file.stat()).size;
if (size > Miscs.privateKeyMaxSize) {
context.showSnackBar(
l10n.fileTooLarge(
path,
size.bytes2Str,
Miscs.privateKeyMaxSize.bytes2Str,
),
l10n.fileTooLarge(path, size.bytes2Str, Miscs.privateKeyMaxSize.bytes2Str),
);
return;
}
@@ -196,10 +176,7 @@ class _PrivateKeyEditPageState extends State<PrivateKeyEditPage> {
onSubmitted: (_) => _onTapSave(),
),
SizedBox(height: MediaQuery.of(context).size.height * 0.1),
ValBuilder(
listenable: _loading,
builder: (val) => val ?? UIs.placeholder,
),
ValBuilder(listenable: _loading, builder: (val) => val ?? UIs.placeholder),
],
);
}

View File

@@ -15,10 +15,7 @@ class PrivateKeysListPage extends StatefulWidget {
@override
State<PrivateKeysListPage> createState() => _PrivateKeyListState();
static const route = AppRouteNoArg(
page: PrivateKeysListPage.new,
path: '/private_key',
);
static const route = AppRouteNoArg(page: PrivateKeysListPage.new, path: '/private_key');
}
class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMixin {
@@ -34,26 +31,21 @@ class _PrivateKeyListState extends State<PrivateKeysListPage> with AfterLayoutMi
}
Widget _buildBody() {
return PrivateKeyProvider.pkis.listenVal(
(pkis) {
if (pkis.isEmpty) {
return Center(child: Text(libL10n.empty));
}
return PrivateKeyProvider.pkis.listenVal((pkis) {
if (pkis.isEmpty) {
return Center(child: Text(libL10n.empty));
}
final children = pkis.map(_buildKeyItem).toList();
return AutoMultiList(children: children);
},
);
final children = pkis.map(_buildKeyItem).toList();
return AutoMultiList(children: children);
});
}
Widget _buildKeyItem(PrivateKeyInfo item) {
return ListTile(
title: Text(item.id),
subtitle: Text(item.type ?? l10n.unknown, style: UIs.textGrey),
onTap: () => PrivateKeyEditPage.route.go(
context,
args: PrivateKeyEditPageArgs(pki: item),
),
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: item)),
trailing: const Icon(Icons.edit),
).cardx;
}
@@ -72,20 +64,16 @@ extension on _PrivateKeyListState {
if (home == null) return;
final idRsaFile = File(home.joinPath('.ssh/id_rsa'));
if (!idRsaFile.existsSync()) return;
final sysPk = PrivateKeyInfo(
id: 'system',
key: await idRsaFile.readAsString(),
);
final sysPk = PrivateKeyInfo(id: 'system', key: await idRsaFile.readAsString());
context.showRoundDialog(
title: libL10n.attention,
child: Text(l10n.addSystemPrivateKeyTip),
actions: Btn.ok(onTap: () {
context.pop();
PrivateKeyEditPage.route.go(
context,
args: PrivateKeyEditPageArgs(pki: sysPk),
);
}).toList,
actions: Btn.ok(
onTap: () {
context.pop();
PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: sysPk));
},
).toList,
);
}
}

View File

@@ -12,16 +12,13 @@ import 'package:server_box/data/res/store.dart';
class ProcessPage extends StatefulWidget {
final SpiRequiredArgs args;
const ProcessPage({super.key, required this.args});
@override
State<ProcessPage> createState() => _ProcessPageState();
static const route = AppRouteArg(
page: ProcessPage.new,
path: '/process',
);
static const route = AppRouteArg(page: ProcessPage.new, path: '/process');
}
class _ProcessPageState extends State<ProcessPage> {
@@ -49,8 +46,7 @@ class _ProcessPageState extends State<ProcessPage> {
void initState() {
super.initState();
_client = widget.args.spi.server?.value.client;
final duration =
Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
_timer = Timer.periodic(duration, (_) => _refresh());
}
@@ -62,8 +58,10 @@ class _ProcessPageState extends State<ProcessPage> {
Future<void> _refresh() async {
if (mounted) {
final result =
await _client?.run(ShellFunc.process.exec(widget.args.spi.id)).string;
final systemType = widget.args.spi.server?.value.status.system;
final result = await _client
?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType))
.string;
if (result == null || result.isEmpty) {
context.showSnackBar(libL10n.empty);
return;
@@ -72,8 +70,7 @@ class _ProcessPageState extends State<ProcessPage> {
// If there are any [Proc]'s data is not complete,
// the option to sort by cpu/mem will not be available.
final isAnyProcDataNotComplete =
_result.procs.any((e) => e.cpu == null || e.mem == null);
final isAnyProcDataNotComplete = _result.procs.any((e) => e.cpu == null || e.mem == null);
if (isAnyProcDataNotComplete) {
_sortModes.removeWhere((e) => e == ProcSortMode.cpu);
_sortModes.removeWhere((e) => e == ProcSortMode.mem);
@@ -97,25 +94,20 @@ class _ProcessPageState extends State<ProcessPage> {
},
icon: const Icon(Icons.sort),
initialValue: _procSortMode,
itemBuilder: (_) => _sortModes
.map((e) => PopupMenuItem(value: e, child: Text(e.name)))
.toList(),
itemBuilder: (_) => _sortModes.map((e) => PopupMenuItem(value: e, child: Text(e.name))).toList(),
),
];
if (_result.error != null) {
actions.add(IconButton(
icon: const Icon(Icons.error),
onPressed: () => context.showRoundDialog(
title: libL10n.error,
child: SingleChildScrollView(child: Text(_result.error!)),
actions: [
TextButton(
onPressed: () => Pfs.copy(_result.error!),
child: Text(libL10n.copy),
),
],
actions.add(
IconButton(
icon: const Icon(Icons.error),
onPressed: () => context.showRoundDialog(
title: libL10n.error,
child: SingleChildScrollView(child: Text(_result.error!)),
actions: [TextButton(onPressed: () => Pfs.copy(_result.error!), child: Text(libL10n.copy))],
),
),
));
);
}
Widget child;
if (_result.procs.isEmpty) {
@@ -144,32 +136,26 @@ class _ProcessPageState extends State<ProcessPage> {
return CardX(
key: ValueKey(proc.pid),
child: ListTile(
leading: SizedBox(
width: _media.size.width / 6,
child: leading,
),
leading: SizedBox(width: _media.size.width / 6, child: leading),
title: Text(proc.binary),
subtitle: Text(
proc.command,
style: UIs.textGrey,
maxLines: 3,
overflow: TextOverflow.fade,
),
subtitle: Text(proc.command, style: UIs.textGrey, maxLines: 3, overflow: TextOverflow.fade),
trailing: _buildItemTrail(proc),
onTap: () => _lastFocusId = proc.pid,
onLongPress: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue(
'${l10n.stop} ${l10n.process}(${proc.pid})',
)),
actions: Btn.ok(onTap: () async {
context.pop();
await context.showLoadingDialog(fn: () async {
await _client?.run('kill ${proc.pid}');
await _refresh();
});
}).toList,
child: Text(libL10n.askContinue('${l10n.stop} ${l10n.process}(${proc.pid})')),
actions: Btn.ok(
onTap: () async {
context.pop();
await context.showLoadingDialog(
fn: () async {
await _client?.run('kill ${proc.pid}');
await _refresh();
},
);
},
).toList,
);
},
selected: _lastFocusId == proc.pid,
@@ -185,17 +171,9 @@ class _ProcessPageState extends State<ProcessPage> {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (proc.cpu != null)
TwoLineText(
up: proc.cpu!.toStringAsFixed(1),
down: 'cpu',
),
if (proc.cpu != null) TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'),
UIs.width13,
if (proc.mem != null)
TwoLineText(
up: proc.mem!.toStringAsFixed(1),
down: 'mem',
),
if (proc.mem != null) TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'),
],
);
}

View File

@@ -18,18 +18,12 @@ final class PvePageArgs {
final class PvePage extends StatefulWidget {
final PvePageArgs args;
const PvePage({
super.key,
required this.args,
});
const PvePage({super.key, required this.args});
@override
State<PvePage> createState() => _PvePageState();
static const route = AppRouteArg<void, PvePageArgs>(
page: PvePage.new,
path: '/pve',
);
static const route = AppRouteArg<void, PvePageArgs>(page: PvePage.new, path: '/pve');
}
const _kHorziPadding = 11.0;
@@ -87,9 +81,7 @@ final class _PvePageState extends State<PvePage> {
_timer?.cancel();
return Padding(
padding: const EdgeInsets.all(13),
child: Center(
child: Text(val),
),
child: Center(child: Text(val)),
);
}
return ValBuilder(
@@ -110,10 +102,7 @@ final class _PvePageState extends State<PvePage> {
PveResType? lastType;
return ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: _kHorziPadding,
vertical: 7,
),
padding: const EdgeInsets.symmetric(horizontal: _kHorziPadding, vertical: 7),
itemCount: data.length * 2,
itemBuilder: (context, index) {
final item = data[index ~/ 2];
@@ -135,10 +124,7 @@ final class _PvePageState extends State<PvePage> {
alignment: Alignment.center,
child: Text(
type.toStr,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.grey,
),
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
textAlign: TextAlign.start,
),
),
@@ -183,18 +169,11 @@ final class _PvePageState extends State<PvePage> {
UIs.width7,
const Text('CPU', style: UIs.text12Grey),
const Spacer(),
Text(
'${(item.cpu * 100).toStringAsFixed(1)} %',
style: UIs.text12Grey,
),
Text('${(item.cpu * 100).toStringAsFixed(1)} %', style: UIs.text12Grey),
],
),
const SizedBox(height: 3),
LinearProgressIndicator(
value: item.cpu / item.maxcpu,
minHeight: 7,
valueColor: valueAnim,
),
LinearProgressIndicator(value: item.cpu / item.maxcpu, minHeight: 7, valueColor: valueAnim),
UIs.height7,
Row(
children: [
@@ -202,18 +181,11 @@ final class _PvePageState extends State<PvePage> {
UIs.width7,
const Text('RAM', style: UIs.text12Grey),
const Spacer(),
Text(
'${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}',
style: UIs.text12Grey,
),
Text('${item.mem.bytes2Str} / ${item.maxmem.bytes2Str}', style: UIs.text12Grey),
],
),
const SizedBox(height: 3),
LinearProgressIndicator(
value: item.mem / item.maxmem,
minHeight: 7,
valueColor: valueAnim,
),
LinearProgressIndicator(value: item.mem / item.maxmem, minHeight: 7, valueColor: valueAnim),
],
),
).cardx;
@@ -232,14 +204,8 @@ final class _PvePageState extends State<PvePage> {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 15),
Text(
_wrapNodeName(item),
style: UIs.text13Bold,
),
Text(
' / ${item.summary}',
style: UIs.text12Grey,
),
Text(_wrapNodeName(item), style: UIs.text13Bold),
Text(' / ${item.summary}', style: UIs.text12Grey),
const Spacer(),
_buildCtrlBtns(item),
UIs.width13,
@@ -266,34 +232,23 @@ final class _PvePageState extends State<PvePage> {
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
)
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'↓:\n${item.netin.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
),
Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
const SizedBox(height: 3),
Text(
'↑:\n${item.netout.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
)
Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
],
),
],
),
const SizedBox(height: 21)
const SizedBox(height: 21),
];
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
).cardx;
return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
}
Widget _buildLxc(PveLxc item) {
@@ -309,14 +264,8 @@ final class _PvePageState extends State<PvePage> {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 15),
Text(
_wrapNodeName(item),
style: UIs.text13Bold,
),
Text(
' / ${item.summary}',
style: UIs.text12Grey,
),
Text(_wrapNodeName(item), style: UIs.text13Bold),
Text(' / ${item.summary}', style: UIs.text12Grey),
const Spacer(),
_buildCtrlBtns(item),
UIs.width13,
@@ -343,34 +292,23 @@ final class _PvePageState extends State<PvePage> {
'${l10n.write}:\n${item.diskwrite.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
)
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'↓:\n${item.netin.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
),
Text('↓:\n${item.netin.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
const SizedBox(height: 3),
Text(
'↑:\n${item.netout.bytes2Str}',
style: UIs.text11Grey,
textAlign: TextAlign.center,
)
Text('↑:\n${item.netout.bytes2Str}', style: UIs.text11Grey, textAlign: TextAlign.center),
],
),
],
),
const SizedBox(height: 21)
const SizedBox(height: 21),
];
return Column(
mainAxisSize: MainAxisSize.min,
children: children,
).cardx;
return Column(mainAxisSize: MainAxisSize.min, children: children).cardx;
}
Widget _buildStorage(PveStorage item) {
@@ -396,33 +334,34 @@ final class _PvePageState extends State<PvePage> {
}
Widget _buildSdn(PveSdn item) {
return ListTile(
title: Text(_wrapNodeName(item)),
trailing: Text(item.summary),
).cardx;
return ListTile(title: Text(_wrapNodeName(item)), trailing: Text(item.summary)).cardx;
}
Widget _buildCtrlBtns(PveCtrlIface item) {
const pad = EdgeInsets.symmetric(horizontal: 7, vertical: 5);
if (!item.available) {
return Btn.icon(
icon: const Icon(Icons.play_arrow, color: Colors.grey),
onTap: () => _onCtrl(_pve.start, l10n.start, item));
icon: const Icon(Icons.play_arrow, color: Colors.grey),
onTap: () => _onCtrl(_pve.start, l10n.start, item),
);
}
return Row(
children: [
Btn.icon(
icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
padding: pad,
onTap: () => _onCtrl(_pve.stop, l10n.stop, item)),
icon: const Icon(Icons.stop, color: Colors.grey, size: 20),
padding: pad,
onTap: () => _onCtrl(_pve.stop, l10n.stop, item),
),
Btn.icon(
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
padding: pad,
onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item)),
icon: const Icon(Icons.refresh, color: Colors.grey, size: 20),
padding: pad,
onTap: () => _onCtrl(_pve.reboot, l10n.reboot, item),
),
Btn.icon(
icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
padding: pad,
onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item)),
icon: const Icon(Icons.power_off, color: Colors.grey, size: 20),
padding: pad,
onTap: () => _onCtrl(_pve.shutdown, l10n.shutdown, item),
),
],
);
}
@@ -437,9 +376,7 @@ extension on _PvePageState {
);
if (sure != true) return;
final (suc, err) = await context.showLoadingDialog(
fn: () => func(item.node, item.id),
);
final (suc, err) = await context.showLoadingDialog(fn: () => func(item.node, item.id));
if (suc == true) {
context.showSnackBar(libL10n.success);
} else {

View File

@@ -61,7 +61,7 @@ extension on _ServerDetailPageState {
titleMaxLines: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UIs.height13,
Text('Memory: ${process.memory} ${process.memory > 1024 ? 'MB' : 'KB'}'),

View File

@@ -9,6 +9,7 @@ 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/server_private_info.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/provider/server.dart';
@@ -59,6 +60,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
final _env = <String, String>{}.vn;
final _customCmds = <String, String>{}.vn;
final _tags = <String>{}.vn;
final _systemType = ValueNotifier<SystemType?>(null);
@override
void dispose() {
@@ -91,6 +93,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
_env.dispose();
_customCmds.dispose();
_tags.dispose();
_systemType.dispose();
}
@override
@@ -174,6 +177,7 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
),
),
_buildAuth(),
_buildSystemType(),
_buildJumpServer(),
_buildMore(),
];
@@ -331,6 +335,26 @@ class _ServerEditPageState extends State<ServerEditPage> with AfterLayoutMixin {
);
}
Widget _buildSystemType() {
return _systemType.listenVal((val) {
return ListTile(
leading: Icon(MingCute.laptop_2_line),
title: Text(l10n.system),
trailing: PopupMenu<SystemType?>(
initialValue: val,
items: [
PopupMenuItem(value: null, child: Text(libL10n.auto)),
PopupMenuItem(value: SystemType.linux, child: Text('Linux')),
PopupMenuItem(value: SystemType.bsd, child: Text('BSD')),
PopupMenuItem(value: SystemType.windows, child: Text('Windows')),
],
onSelected: (value) => _systemType.value = value,
child: Text(val?.name ?? libL10n.auto, style: TextStyle(color: val == null ? Colors.grey : null)),
),
).cardx;
});
}
Widget _buildAltUrl() {
return Input(
controller: _altUrlController,
@@ -614,6 +638,7 @@ extension on _ServerEditPageState {
wolCfg: wol,
envs: _env.value.isEmpty ? null : _env.value,
id: widget.args?.spi.id ?? ShortId.generate(),
customSystemType: _systemType.value,
);
if (this.spi == null) {
@@ -668,5 +693,7 @@ extension on _ServerEditPageState {
_netDevCtrl.text = spi.custom?.netDev ?? '';
_scriptDirCtrl.text = spi.custom?.scriptDir ?? '';
_systemType.value = spi.customSystemType;
}
}

View File

@@ -7,21 +7,9 @@ class _CardStatus {
final bool? diskIO;
final NetViewType? net;
const _CardStatus({
this.flip = false,
this.diskIO,
this.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,
);
_CardStatus copyWith({bool? flip, bool? diskIO, NetViewType? net}) {
return _CardStatus(flip: flip ?? this.flip, diskIO: diskIO ?? this.diskIO, net: net ?? this.net);
}
}

View File

@@ -319,8 +319,7 @@ class _ServerPageState extends State<ServerPage> with AutomaticKeepAliveClientMi
],
),
UIs.height13,
if (Stores.setting.moveServerFuncs.fetch())
SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),
if (Stores.setting.moveServerFuncs.fetch()) SizedBox(height: 27, child: ServerFuncBtns(spi: spi)),
],
);
},

View File

@@ -5,11 +5,7 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
final void Function(String) onTagChanged;
final String initTag;
const _TopBar({
required this.initTag,
required this.onTagChanged,
required this.tags,
});
const _TopBar({required this.initTag, required this.onTagChanged, required this.tags});
@override
Widget build(BuildContext context) {
@@ -31,15 +27,9 @@ final class _TopBar extends StatelessWidget implements PreferredSizeWidget {
padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3),
child: Row(
children: [
Text(
BuildData.name,
style: TextStyle(fontSize: 19),
),
Text(BuildData.name, style: TextStyle(fontSize: 19)),
SizedBox(width: 5),
Icon(
Icons.settings,
size: 17,
),
Icon(Icons.settings, size: 17),
],
),
),

View File

@@ -49,7 +49,11 @@ extension _Operation on _ServerPageState {
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);
srv.client?.execWithPwd(
ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system),
context: context,
id: srv.id,
);
},
typ: l10n.suspend,
name: srv.spi.name,
@@ -58,7 +62,11 @@ extension _Operation on _ServerPageState {
void _onTapShutdown(Server srv) {
_askFor(
func: () => srv.client?.execWithPwd(ShellFunc.shutdown.exec(srv.spi.id), context: context, id: srv.id),
func: () => srv.client?.execWithPwd(
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system),
context: context,
id: srv.id,
),
typ: l10n.shutdown,
name: srv.spi.name,
);
@@ -66,7 +74,11 @@ extension _Operation on _ServerPageState {
void _onTapReboot(Server srv) {
_askFor(
func: () => srv.client?.execWithPwd(ShellFunc.reboot.exec(srv.spi.id), context: context, id: srv.id),
func: () => srv.client?.execWithPwd(
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system),
context: context,
id: srv.id,
),
typ: l10n.reboot,
name: srv.spi.name,
);

View File

@@ -7,8 +7,7 @@ final class _AppAboutPage extends StatefulWidget {
State<_AppAboutPage> createState() => _AppAboutPageState();
}
final class _AppAboutPageState extends State<_AppAboutPage>
with AutomaticKeepAliveClientMixin {
final class _AppAboutPageState extends State<_AppAboutPage> with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
@@ -16,15 +15,8 @@ final class _AppAboutPageState extends State<_AppAboutPage>
padding: const EdgeInsets.all(13),
children: [
UIs.height13,
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47),
child: UIs.appIcon,
),
const Text(
'${BuildData.name}\nv${BuildData.build}',
textAlign: TextAlign.center,
style: UIs.text15,
),
ConstrainedBox(constraints: const BoxConstraints(maxHeight: 47, maxWidth: 47), child: UIs.appIcon),
const Text('${BuildData.name}\nv${BuildData.build}', textAlign: TextAlign.center, style: UIs.text15),
UIs.height13,
SizedBox(
height: 77,
@@ -52,7 +44,8 @@ final class _AppAboutPageState extends State<_AppAboutPage>
),
UIs.height13,
SimpleMarkdown(
data: '''
data:
'''
#### Contributors
${GithubIds.contributors.map((e) => '[$e](${e.url})').join(' ')}

View File

@@ -10,10 +10,7 @@ class ServerDetailOrderPage extends StatefulWidget {
@override
State<ServerDetailOrderPage> createState() => _ServerDetailOrderPageState();
static const route = AppRouteNoArg(
page: ServerDetailOrderPage.new,
path: '/settings/order/server_detail',
);
static const route = AppRouteNoArg(page: ServerDetailOrderPage.new, path: '/settings/order/server_detail');
}
class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
@@ -31,8 +28,7 @@ class _ServerDetailOrderPageState extends State<ServerDetailOrderPage> {
return ValBuilder(
listenable: prop.listenable(),
builder: (keys) {
final disabled =
ServerDetailCards.names.where((e) => !keys.contains(e)).toList();
final disabled = ServerDetailCards.names.where((e) => !keys.contains(e)).toList();
final allKeys = [...keys, ...disabled];
return ReorderableListView.builder(
padding: const EdgeInsets.all(7),

View File

@@ -10,10 +10,7 @@ class ServerFuncBtnsOrderPage extends StatefulWidget {
@override
State<ServerFuncBtnsOrderPage> createState() => _ServerDetailOrderPageState();
static const route = AppRouteNoArg(
page: ServerFuncBtnsOrderPage.new,
path: '/setting/seq/srv_func',
);
static const route = AppRouteNoArg(page: ServerFuncBtnsOrderPage.new, path: '/setting/seq/srv_func');
}
class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
@@ -67,12 +64,7 @@ class _ServerDetailOrderPageState extends State<ServerFuncBtnsOrderPage> {
);
}
Widget _buildCheckBox(
List<int> keys,
int key,
int idx,
bool value,
) {
Widget _buildCheckBox(List<int> keys, int key, int idx, bool value) {
return Checkbox(
value: value,
onChanged: (val) {

View File

@@ -12,10 +12,7 @@ class ServerOrderPage extends StatefulWidget {
@override
State<ServerOrderPage> createState() => _ServerOrderPageState();
static const route = AppRouteNoArg(
page: ServerOrderPage.new,
path: '/settings/order/server',
);
static const route = AppRouteNoArg(page: ServerOrderPage.new, path: '/settings/order/server');
}
class _ServerOrderPageState extends State<ServerOrderPage> {
@@ -36,10 +33,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
final double scale = lerpDouble(1, 1.02, animValue)!;
return Transform.scale(
scale: scale,
child: Card(
elevation: elevation,
child: child,
),
child: Card(elevation: elevation, child: child),
);
},
child: _buildCardTile(index),
@@ -56,11 +50,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
footer: const SizedBox(height: 77),
onReorder: (oldIndex, newIndex) {
setState(() {
orders.value.move(
oldIndex,
newIndex,
property: Stores.setting.serverOrder,
);
orders.value.move(oldIndex, newIndex, property: Stores.setting.serverOrder);
});
},
padding: const EdgeInsets.all(8),
@@ -78,9 +68,7 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
index: index,
child: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: CardX(
child: _buildCardTile(index),
),
child: CardX(child: _buildCardTile(index)),
),
);
}
@@ -93,20 +81,14 @@ class _ServerOrderPageState extends State<ServerOrderPage> {
}
return ListTile(
title: Text(
spi.name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
title: Text(spi.name, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(spi.oldId, style: UIs.textGrey),
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
child: Text(spi.name[0]),
),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
trailing: ReorderableDragStartListener(index: index, child: const Icon(Icons.drag_handle)),
);
}
}

View File

@@ -19,10 +19,7 @@ class SnippetEditPage extends StatefulWidget {
@override
State<SnippetEditPage> createState() => _SnippetEditPageState();
static const route = AppRoute(
page: SnippetEditPage.new,
path: '/snippets/edit',
);
static const route = AppRoute(page: SnippetEditPage.new, path: '/snippets/edit');
}
class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin {
@@ -47,10 +44,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: Text(libL10n.edit),
actions: _buildAppBarActions(),
),
appBar: CustomAppBar(title: Text(libL10n.edit), actions: _buildAppBarActions()),
body: _buildBody(),
floatingActionButton: _buildFAB(),
);
@@ -64,9 +58,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.snippet}(${snippet.name})',
)),
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.snippet}(${snippet.name})')),
actions: Btn.ok(
onTap: () {
SnippetProvider.del(snippet);
@@ -79,7 +71,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
},
tooltip: libL10n.delete,
icon: const Icon(Icons.delete),
)
),
];
}
@@ -168,12 +160,7 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
trailing: const Icon(Icons.keyboard_arrow_right),
subtitle: subtitle == null
? null
: Text(
subtitle,
maxLines: 1,
style: UIs.textGrey,
overflow: TextOverflow.ellipsis,
),
: Text(subtitle, maxLines: 1, style: UIs.textGrey, overflow: TextOverflow.ellipsis),
onTap: () async {
vals.removeWhere((e) => !ServerProvider.serverOrder.value.contains(e));
final serverIds = await context.showPickDialog(
@@ -198,7 +185,8 @@ class _SnippetEditPageState extends State<SnippetEditPage> with AfterLayoutMixin
child: Padding(
padding: const EdgeInsets.all(13),
child: SimpleMarkdown(
data: '''
data:
'''
📌 ${l10n.supportFmtArgs}\n
${SnippetX.fmtArgs.keys.map((e) => '`$e`').join(', ')}\n
@@ -207,11 +195,7 @@ ${libL10n.example}:
- `\${ctrl+c}` (Control + C)
- `\${ctrl+b}d` (Tmux Detach)
''',
styleSheet: MarkdownStyleSheet(
codeblockDecoration: const BoxDecoration(
color: Colors.transparent,
),
),
styleSheet: MarkdownStyleSheet(codeblockDecoration: const BoxDecoration(color: Colors.transparent)),
),
),
);

View File

@@ -11,10 +11,7 @@ class SnippetListPage extends StatefulWidget {
@override
State<SnippetListPage> createState() => _SnippetListPageState();
static const route = AppRouteNoArg(
page: SnippetListPage.new,
path: '/snippets',
);
static const route = AppRouteNoArg(page: SnippetListPage.new, path: '/snippets');
}
class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAliveClientMixin {
@@ -38,24 +35,22 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
Widget _buildBody() {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
return SnippetProvider.snippets.listenVal(
(snippets) {
return _tag.listenVal((tag) {
final child = _buildScaffold(snippets, tag);
// if (isMobile) {
return child;
// }
return SnippetProvider.snippets.listenVal((snippets) {
return _tag.listenVal((tag) {
final child = _buildScaffold(snippets, tag);
// if (isMobile) {
return child;
// }
// return SplitView(
// controller: _splitViewCtrl,
// leftWeight: 1,
// rightWeight: 1.3,
// initialRight: Center(child: Text(libL10n.empty)),
// leftBuilder: (_, __) => child,
// );
});
},
);
// return SplitView(
// controller: _splitViewCtrl,
// leftWeight: 1,
// rightWeight: 1.3,
// initialRight: Center(child: Text(libL10n.empty)),
// leftBuilder: (_, __) => child,
// );
});
});
}
Widget _buildScaffold(List<Snippet> snippets, String tag) {
@@ -104,11 +99,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
Widget _buildSnippetItem(Snippet snippet) {
return ListTile(
contentPadding: const EdgeInsets.only(left: 23, right: 17),
title: Text(
snippet.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
title: Text(snippet.name, overflow: TextOverflow.ellipsis, maxLines: 1),
subtitle: Text(
snippet.note ?? snippet.script,
overflow: TextOverflow.ellipsis,
@@ -119,10 +110,7 @@ class _SnippetListPageState extends State<SnippetListPage> with AutomaticKeepAli
onTap: () {
// final isMobile = ResponsiveBreakpoints.of(context).isMobile;
// if (isMobile) {
SnippetEditPage.route.go(
context,
args: SnippetEditPageArgs(snippet: snippet),
);
SnippetEditPage.route.go(context, args: SnippetEditPageArgs(snippet: snippet));
// } else {
// _splitViewCtrl.replace(SnippetEditPage(
// args: SnippetEditPageArgs(snippet: snippet),

View File

@@ -8,10 +8,7 @@ class SnippetResultPage extends StatelessWidget {
const SnippetResultPage({super.key, required this.args});
static const route = AppRouteArg(
page: SnippetResultPage.new,
path: '/snippets/result',
);
static const route = AppRouteArg(page: SnippetResultPage.new, path: '/snippets/result');
@override
Widget build(BuildContext context) {
@@ -37,10 +34,7 @@ class SnippetResultPage extends StatelessWidget {
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 17),
scrollDirection: Axis.horizontal,
child: Text(
item.result,
textAlign: TextAlign.start,
),
child: Text(item.result, textAlign: TextAlign.start),
),
],
),

View File

@@ -13,7 +13,7 @@ extension _Keyboard on SSHPageState {
_handleEscKeyOrBackButton();
return true; // Mark as handled so it doesn't propagate
}
if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
if (event.logicalKey == LogicalKeyboardKey.shiftLeft ||
event.logicalKey == LogicalKeyboardKey.shiftRight) {
// Handle shift key press
_terminal.keyInput(TerminalKey.shift);

View File

@@ -88,10 +88,7 @@ extension _VirtKey on SSHPageState {
while (initPath == null) {
// Check if we've exceeded timeout
if (DateTime.now().difference(startTime) > timeout) {
contextSafe?.showRoundDialog(
title: libL10n.error,
child: Text(libL10n.empty),
);
contextSafe?.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty));
return;
}
@@ -119,10 +116,7 @@ extension _VirtKey on SSHPageState {
}
if (!initPath.startsWith('/')) {
context.showRoundDialog(
title: libL10n.error,
child: Text('${l10n.remotePath}: $initPath'),
);
context.showRoundDialog(title: libL10n.error, child: Text('${l10n.remotePath}: $initPath'));
return;
}
@@ -138,10 +132,7 @@ extension _VirtKey on SSHPageState {
if (text != null) {
_terminal.textInput(text);
} else {
context.showRoundDialog(
title: libL10n.error,
child: Text(libL10n.empty),
);
context.showRoundDialog(title: libL10n.error, child: Text(libL10n.empty));
}
});
}

View File

@@ -16,19 +16,14 @@ class SSHTabPage extends StatefulWidget {
@override
State<SSHTabPage> createState() => _SSHTabPageState();
static const route = AppRouteNoArg(
page: SSHTabPage.new,
path: '/ssh',
);
static const route = AppRouteNoArg(page: SSHTabPage.new, path: '/ssh');
}
typedef _TabMap = Map<String, ({Widget page, FocusNode? focus})>;
class _SSHTabPageState extends State<SSHTabPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late final _TabMap _tabMap = {
libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null),
};
late final _TabMap _tabMap = {libL10n.add: (page: _AddPage(onTapInitCard: _onTapInitCard), focus: null)};
final _pageCtrl = PageController();
final _fabVN = 0.vn;
final _tabRN = RNode();
@@ -48,12 +43,7 @@ class _SSHTabPageState extends State<SSHTabPage>
appBar: PreferredSizeListenBuilder(
listenable: _tabRN,
builder: () {
return _TabBar(
idxVN: _fabVN,
map: _tabMap,
onTap: _onTapTab,
onClose: _onTapClose,
);
return _TabBar(idxVN: _fabVN, map: _tabMap, onTap: _onTapTab, onClose: _onTapClose);
},
),
body: _buildBody(),
@@ -159,12 +149,7 @@ extension on _SSHTabPageState {
}
final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
const _TabBar({
required this.idxVN,
required this.map,
required this.onTap,
required this.onClose,
});
const _TabBar({required this.idxVN, required this.map, required this.onTap, required this.onClose});
final ValueListenable<int> idxVN;
final _TabMap map;
@@ -188,10 +173,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
itemBuilder: (_, idx) => _buildItem(idx),
separatorBuilder: (_, _) => Padding(
padding: const EdgeInsets.symmetric(vertical: 17),
child: Container(
color: const Color.fromARGB(61, 158, 158, 158),
width: 3,
),
child: Container(color: const Color.fromARGB(61, 158, 158, 158), width: 3),
),
);
},
@@ -242,10 +224,7 @@ final class _TabBar extends StatelessWidget implements PreferredSizeWidget {
width: selected ? kWideWidth : kNarrowWidth,
duration: Durations.medium3,
curve: Curves.fastEaseInToSlowEaseOut,
child: OverflowBox(
maxWidth: selected ? kWideWidth : null,
child: btn,
),
child: OverflowBox(maxWidth: selected ? kWideWidth : null, child: btn),
);
}
@@ -280,9 +259,7 @@ class _AddPage extends StatelessWidget {
return ServerProvider.serverOrder.listenVal((order) {
if (order.isEmpty) {
return Center(
child: Text(libL10n.empty, textAlign: TextAlign.center),
);
return Center(child: Text(libL10n.empty, textAlign: TextAlign.center));
}
// Custom grid
@@ -316,7 +293,7 @@ class _AddPage extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.chevron_right)
const Icon(Icons.chevron_right),
],
),
),

View File

@@ -16,10 +16,7 @@ import 'package:server_box/view/page/storage/sftp_mission.dart';
final class LocalFilePageArgs {
final bool? isPickFile;
final String? initDir;
const LocalFilePageArgs({
this.isPickFile,
this.initDir,
});
const LocalFilePageArgs({this.isPickFile, this.initDir});
}
class LocalFilePage extends StatefulWidget {
@@ -27,10 +24,7 @@ class LocalFilePage extends StatefulWidget {
const LocalFilePage({super.key, this.args});
static const route = AppRoute<String, LocalFilePageArgs>(
page: LocalFilePage.new,
path: '/files/local',
);
static const route = AppRoute<String, LocalFilePageArgs>(page: LocalFilePage.new, path: '/files/local');
@override
State<LocalFilePage> createState() => _LocalFilePageState();
@@ -98,9 +92,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
Future<List<(FileSystemEntity, FileStat)>> getEntities() async {
final files = await Directory(_path.path).list().toList();
final sorted = _sortType.value.sort(files);
final stats = await Future.wait(
sorted.map((e) async => (e, await e.stat())),
);
final stats = await Future.wait(sorted.map((e) async => (e, await e.stat())));
return stats;
}
@@ -133,12 +125,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
final stat = item.$2;
final isDir = stat.type == FileSystemEntityType.directory;
return _buildItem(
file: file,
fileName: fileName,
stat: stat,
isDir: isDir,
);
return _buildItem(file: file, fileName: fileName, stat: stat, isDir: isDir);
},
);
},
@@ -156,10 +143,7 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
leading: isDir ? const Icon(Icons.folder_open) : const Icon(Icons.insert_drive_file),
title: Text(fileName),
subtitle: isDir ? null : Text(stat.size.bytes2Str, style: UIs.textGrey),
trailing: Text(
stat.modified.ymdhms(),
style: UIs.textGrey,
),
trailing: Text(stat.modified.ymdhms(), style: UIs.textGrey),
onLongPress: () {
if (isDir) {
_showDirActionDialog(file);
@@ -187,17 +171,15 @@ class _LocalFilePageState extends State<LocalFilePage> with AutomaticKeepAliveCl
}
Widget _buildSortBtn() {
return _sortType.listenVal(
(value) {
return PopupMenuButton<_SortType>(
icon: const Icon(Icons.sort),
itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),
onSelected: (value) {
_sortType.value = value;
},
);
},
);
return _sortType.listenVal((value) {
return PopupMenuButton<_SortType>(
icon: const Icon(Icons.sort),
itemBuilder: (_) => _SortType.values.map((e) => e.menuItem).toList(),
onSelected: (value) {
_sortType.value = value;
},
);
});
}
@override
@@ -238,10 +220,12 @@ extension _Actions on _LocalFilePageState {
title: libL10n.file,
child: Text(fileName),
actions: [
Btn.ok(onTap: () {
context.pop();
context.pop(file.path);
}),
Btn.ok(
onTap: () {
context.pop();
context.pop(file.path);
},
),
],
);
return;
@@ -382,21 +366,13 @@ extension _OnTapFile on _LocalFilePageState {
);
if (spi == null) return;
final args = SftpPageArgs(
spi: spi,
isSelect: true,
);
final args = SftpPageArgs(spi: spi, isSelect: true);
final remotePath = await SftpPage.route.go(context, args);
if (remotePath == null) {
return;
}
SftpProvider.add(SftpReq(
spi,
'$remotePath/$fileName',
file.absolute.path,
SftpReqType.upload,
));
SftpProvider.add(SftpReq(spi, '$remotePath/$fileName', file.absolute.path, SftpReqType.upload));
context.showSnackBar(l10n.added2List);
}
}
@@ -404,8 +380,7 @@ extension _OnTapFile on _LocalFilePageState {
enum _SortType {
name,
size,
time,
;
time;
List<FileSystemEntity> sort(List<FileSystemEntity> files) {
switch (this) {
@@ -423,27 +398,21 @@ enum _SortType {
}
String get i18n => switch (this) {
name => libL10n.name,
size => l10n.size,
time => l10n.time,
};
name => libL10n.name,
size => l10n.size,
time => l10n.time,
};
IconData get icon => switch (this) {
name => Icons.sort_by_alpha,
size => Icons.sort,
time => Icons.access_time,
};
name => Icons.sort_by_alpha,
size => Icons.sort,
time => Icons.access_time,
};
PopupMenuItem<_SortType> get menuItem {
return PopupMenuItem(
value: this,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Icon(icon),
Text(i18n),
],
),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [Icon(icon), Text(i18n)]),
);
}
}

View File

@@ -11,19 +11,14 @@ class SftpMissionPage extends StatefulWidget {
@override
State<SftpMissionPage> createState() => _SftpMissionPageState();
static const route = AppRouteNoArg(
page: SftpMissionPage.new,
path: '/sftp/mission',
);
static const route = AppRouteNoArg(page: SftpMissionPage.new, path: '/sftp/mission');
}
class _SftpMissionPageState extends State<SftpMissionPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: Text(l10n.mission, style: UIs.text18),
),
appBar: CustomAppBar(title: Text(l10n.mission, style: UIs.text18)),
body: _buildBody(),
);
}
@@ -50,10 +45,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
status: status,
subtitle: libL10n.error,
trailing: IconButton(
onPressed: () => context.showRoundDialog(
title: libL10n.error,
child: Text(err.toString()),
),
onPressed: () => context.showRoundDialog(title: libL10n.error, child: Text(err.toString())),
icon: const Icon(Icons.error),
),
);
@@ -109,9 +101,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
Widget _buildFinished(SftpReqStatus status) {
final time = status.spentTime.toString();
final str = l10n.spentTime(
time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)),
);
final str = l10n.spentTime(time == 'null' ? l10n.unknown : (time.substring(0, time.length - 7)));
final btns = Row(
mainAxisSize: MainAxisSize.min,
@@ -120,41 +110,26 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
onPressed: () {
final idx = status.req.localPath.lastIndexOf(Pfs.seperator);
final dir = status.req.localPath.substring(0, idx);
LocalFilePage.route.go(
context,
args: LocalFilePageArgs(initDir: dir),
);
LocalFilePage.route.go(context, args: LocalFilePageArgs(initDir: dir));
},
icon: const Icon(Icons.file_open),
),
IconButton(
onPressed: () => Pfs.sharePaths(paths: [status.req.localPath]),
icon: const Icon(Icons.open_in_new),
)
),
],
);
return _wrapInCard(
status: status,
subtitle: str,
trailing: btns,
);
return _wrapInCard(status: status, subtitle: str, trailing: btns);
}
Widget _wrapInCard({
required SftpReqStatus status,
String? subtitle,
Widget? trailing,
}) {
Widget _wrapInCard({required SftpReqStatus status, String? subtitle, Widget? trailing}) {
final time = DateTime.fromMicrosecondsSinceEpoch(status.id);
return CardX(
child: ListTile(
leading: Text(time.hourMinute),
title: Text(
status.fileName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
title: Text(status.fileName, overflow: TextOverflow.ellipsis, maxLines: 1),
subtitle: subtitle == null ? null : Text(subtitle, style: UIs.textGrey),
trailing: trailing,
),
@@ -165,9 +140,7 @@ class _SftpMissionPageState extends State<SftpMissionPage> {
return IconButton(
onPressed: () => context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue(
'${libL10n.delete} ${l10n.mission}($name)',
)),
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.mission}($name)')),
actions: Btn.ok(
onTap: () {
SftpProvider.cancel(id);

View File

@@ -9,15 +9,9 @@ import 'package:server_box/view/page/ssh/page/page.dart';
final class SystemdPage extends StatefulWidget {
final SpiRequiredArgs args;
const SystemdPage({
super.key,
required this.args,
});
const SystemdPage({super.key, required this.args});
static const route = AppRouteArg<void, SpiRequiredArgs>(
page: SystemdPage.new,
path: '/systemd',
);
static const route = AppRouteArg<void, SpiRequiredArgs>(page: SystemdPage.new, path: '/systemd');
@override
State<SystemdPage> createState() => _SystemdPageState();
@@ -37,9 +31,7 @@ final class _SystemdPageState extends State<SystemdPage> {
return Scaffold(
appBar: CustomAppBar(
title: const Text('Systemd'),
actions: isDesktop
? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)]
: null,
actions: isDesktop ? [Btn.icon(icon: const Icon(Icons.refresh), onTap: _pro.getUnits)] : null,
),
body: RefreshIndicator(onRefresh: _pro.getUnits, child: _buildBody()),
);
@@ -54,9 +46,7 @@ final class _SystemdPageState extends State<SystemdPage> {
duration: Durations.medium1,
curve: Curves.fastEaseInToSlowEaseOut,
height: isBusy ? SizedLoading.medium.size : 0,
child: isBusy
? SizedLoading.medium
: const SizedBox.shrink(),
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
),
),
),
@@ -66,35 +56,24 @@ final class _SystemdPageState extends State<SystemdPage> {
}
Widget _buildUnitList(VNode<List<SystemdUnit>> units) {
return units.listenVal(
(units) {
if (units.isEmpty) {
return SliverToBoxAdapter(
child:
CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final unit = units[index];
return ListTile(
leading: _buildScopeTag(unit.scope),
title: unit.description != null
? TipText(unit.name, unit.description!)
: Text(unit.name),
subtitle: Wrap(children: [
_buildStateTag(unit.state),
_buildTypeTag(unit.type),
]).paddingOnly(top: 7),
trailing: _buildUnitFuncs(unit),
).cardx.paddingSymmetric(horizontal: 13);
},
childCount: units.length,
),
);
},
);
return units.listenVal((units) {
if (units.isEmpty) {
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
}
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final unit = units[index];
return ListTile(
leading: _buildScopeTag(unit.scope),
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
subtitle: Wrap(
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
).paddingOnly(top: 7),
trailing: _buildUnitFuncs(unit),
).cardx.paddingSymmetric(horizontal: 13);
}, childCount: units.length),
);
});
}
Widget _buildUnitFuncs(SystemdUnit unit) {
@@ -128,11 +107,7 @@ final class _SystemdPageState extends State<SystemdPage> {
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Icon(func.icon, size: 19),
const SizedBox(width: 10),
Text(func.name.capitalize),
],
children: [Icon(func.icon, size: 19), const SizedBox(width: 10), Text(func.name.capitalize)],
),
);
}
@@ -155,8 +130,7 @@ final class _SystemdPageState extends State<SystemdPage> {
color: color?.withValues(alpha: 0.7) ?? UIs.halfAlpha,
borderRadius: BorderRadius.circular(5),
),
child: Text(tag, style: UIs.text11)
.paddingSymmetric(horizontal: 5, vertical: 1),
child: Text(tag, style: UIs.text11).paddingSymmetric(horizontal: 5, vertical: 1),
).paddingOnly(right: noPad ? 0 : 5);
}
}

View File

@@ -6,47 +6,39 @@ class OmitStartText extends StatelessWidget {
final TextStyle? style;
final TextOverflow? overflow;
const OmitStartText(
this.text, {
super.key,
this.maxLines,
this.style,
this.overflow,
});
const OmitStartText(this.text, {super.key, this.maxLines, this.style, this.overflow});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, size) {
bool exceeded = false;
int len = 0;
for (; !exceeded && len < text.length; len++) {
// Build the textspan
final span = TextSpan(
text: 'A' * 7 + text.substring(text.length - len),
style: style ?? Theme.of(context).textTheme.bodyMedium,
);
return LayoutBuilder(
builder: (context, size) {
bool exceeded = false;
int len = 0;
for (; !exceeded && len < text.length; len++) {
// Build the textspan
final span = TextSpan(
text: 'A' * 7 + text.substring(text.length - len),
style: style ?? Theme.of(context).textTheme.bodyMedium,
);
// Use a textpainter to determine if it will exceed max lines
final tp = TextPainter(
// Use a textpainter to determine if it will exceed max lines
final tp = TextPainter(maxLines: maxLines ?? 1, textDirection: TextDirection.ltr, text: span);
// trigger it to layout
tp.layout(maxWidth: size.maxWidth);
// whether the text overflowed or not
exceeded = tp.didExceedMaxLines;
}
return Text(
(exceeded ? '...' : '') + text.substring(text.length - len),
overflow: overflow ?? TextOverflow.fade,
softWrap: false,
maxLines: maxLines ?? 1,
textDirection: TextDirection.ltr,
text: span,
style: style,
);
// trigger it to layout
tp.layout(maxWidth: size.maxWidth);
// whether the text overflowed or not
exceeded = tp.didExceedMaxLines;
}
return Text(
(exceeded ? '...' : '') + text.substring(text.length - len),
overflow: overflow ?? TextOverflow.fade,
softWrap: false,
maxLines: maxLines ?? 1,
style: style,
);
});
},
);
}
}

View File

@@ -5,18 +5,13 @@ import 'package:flutter/material.dart';
final class PercentCircle extends StatelessWidget {
final double percent;
const PercentCircle({
super.key,
required this.percent,
});
const PercentCircle({super.key, required this.percent});
@override
Widget build(BuildContext context) {
final percent = switch (this.percent) {
0 => 0.01,
100 => 99.9,
// NaN
final val when val.isNaN => 0.01,
<= 0.01 => 0.01,
>= 99.9 => 99.9,
_ => this.percent,
};
return Stack(

View File

@@ -6,11 +6,7 @@ final class UnixPermOp {
final bool w;
final bool x;
const UnixPermOp({
required this.r,
required this.w,
required this.x,
});
const UnixPermOp({required this.r, required this.w, required this.x});
UnixPermOp copyWith({bool? r, bool? w, bool? x}) {
return UnixPermOp(r: r ?? this.r, w: w ?? this.w, x: x ?? this.x);
@@ -24,8 +20,7 @@ final class UnixPermOp {
enum UnixPermScope {
user,
group,
other,
;
other;
String get title {
return switch (this) {
@@ -72,10 +67,10 @@ final class UnixPerm {
}
static UnixPerm get empty => const UnixPerm(
user: UnixPermOp(r: false, w: false, x: false),
group: UnixPermOp(r: false, w: false, x: false),
other: UnixPermOp(r: false, w: false, x: false),
);
user: UnixPermOp(r: false, w: false, x: false),
group: UnixPermOp(r: false, w: false, x: false),
other: UnixPermOp(r: false, w: false, x: false),
);
}
final class UnixPermEditor extends StatefulWidget {
@@ -150,9 +145,6 @@ final class _UnixPermEditorState extends State<UnixPermEditor> {
}
Widget _buildSwitch(bool value, void Function(bool) onChanged) {
return Switch(
value: value,
onChanged: onChanged,
);
return Switch(value: value, onChanged: onChanged);
}
}