mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-01-31 13:25:10 +01:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ enum _PruneTypes {
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'}'),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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(' ')}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user