Files
GT610 3c592baf2c fix: Use latest dartssh2 and add a switch for temperature between celsius and millicelsius (#1095)
* refactor(sftp): Optimize file download logic and SSH command execution handling

Replace manual chunked downloads with a more concise `sftp.download` method
Consistently use `utf8.decode` to process SSH command output

Remove redundant code and comments, and simplify the logic

* chore: Update `dartssh2` submodule

* feat (Temperature Display): Added an option to switch between degrees Celsius and millicelsius

Allows users to switch temperature units in server settings, resolving the issue of incorrect temperature display on some devices

* chore: Add a participnt

* fix(sftp): Fixed a resource leak issue during file downloads and SSH command execution

Ensured that remote and local file handles are properly closed during file downloads to prevent resource leaks. Additionally, improved error handling during SSH command execution to ensure that all streams are either successfully completed or properly handled in the event of an error.
2026-04-01 11:27:58 +08:00

461 lines
13 KiB
Dart

part of 'edit.dart';
extension _Widgets on _ServerEditPageState {
Widget _buildAuth() {
final switch_ = ListTile(
title: Text(l10n.keyAuth),
trailing: _keyIdx.listenVal(
(v) => Switch(
value: v != null,
onChanged: (val) {
if (val) {
_keyIdx.value = -1;
} else {
_keyIdx.value = null;
}
},
),
),
);
/// Put [switch_] out of [ValueBuilder] to avoid rebuild
return _keyIdx.listenVal((v) {
final children = <Widget>[switch_];
if (v != null) {
children.add(_buildKeyAuth());
} else {
children.add(
Input(
controller: _passwordController,
obscureText: true,
type: TextInputType.text,
label: libL10n.pwd,
icon: Icons.password,
suggestion: false,
onSubmitted: (_) => _onSave(),
),
);
}
return Column(children: children);
});
}
Widget _buildKeyAuth() {
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
final privateKeyState = ref.watch(privateKeyProvider);
final pkis = privateKeyState.keys;
final choice = _keyIdx.listenVal((val) {
final selectedPki = val != null && val >= 0 && val < pkis.length
? pkis[val]
: null;
return Choice<int>(
multiple: false,
clearable: true,
value: selectedPki != null ? [val!] : [],
builder: (state, _) => Column(
children: [
Wrap(
children: List<Widget>.generate(pkis.length, (index) {
final item = pkis[index];
return ChoiceChipX<int>(
key: ValueKey(index),
label: item.id,
state: state,
value: index,
onSelected: (idx, on) {
if (on) {
_keyIdx.value = idx;
} else {
_keyIdx.value = -1;
}
},
);
}),
),
UIs.height7,
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (selectedPki != null)
Btn.icon(
icon: const Icon(Icons.edit, size: 20),
text: libL10n.edit,
onTap: () => PrivateKeyEditPage.route.go(
context,
args: PrivateKeyEditPageArgs(pki: selectedPki),
),
),
Btn.icon(
icon: const Icon(Icons.add, size: 20),
text: libL10n.add,
onTap: () => PrivateKeyEditPage.route.go(context),
),
],
),
],
),
);
});
return ExpandTile(
leading: const Icon(Icons.key),
initiallyExpanded: _keyIdx.value != null && _keyIdx.value! >= 0,
childrenPadding: padding,
title: Text(l10n.privateKey),
children: [choice],
).cardx;
}
Widget _buildEnvs() {
return _env.listenVal((val) {
final subtitle = val.isEmpty
? null
: Text(val.keys.join(','), style: UIs.textGrey);
return ListTile(
leading: const Icon(HeroIcons.variable),
subtitle: subtitle,
title: Text(l10n.envVars),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {
final res = await KvEditor.route.go(
context,
KvEditorArgs(data: spi?.envs ?? {}),
);
if (res == null) return;
_env.value = res;
},
).cardx;
});
}
Widget _buildMore() {
return ExpandTile(
title: Text(l10n.more),
children: [
Input(
controller: _logoUrlCtrl,
type: TextInputType.url,
icon: Icons.image,
label: 'Logo URL',
hint: 'https://example.com/logo.png',
suggestion: false,
),
_buildAltUrl(),
_buildScriptDir(),
_buildEnvs(),
_buildPVEs(),
_buildCustomCmds(),
_buildDisabledCmdTypes(),
_buildCustomDev(),
_buildWOLs(),
],
);
}
Widget _buildScriptDir() {
return Input(
controller: _scriptDirCtrl,
type: TextInputType.text,
label: '${l10n.remotePath} (Shell ${libL10n.install})',
icon: Icons.folder,
hint: '~/.config/server_box',
suggestion: false,
);
}
Widget _buildCustomDev() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CenterGreyTitle(l10n.specifyDev),
ListTile(
leading: const Icon(MingCute.question_line),
title: TipText(libL10n.note, l10n.specifyDevTip),
).cardx,
Input(
controller: _preferTempDevCtrl,
type: TextInputType.text,
label: libL10n.temperature,
icon: MingCute.low_temperature_line,
hint: 'nvme-pci-0400',
suggestion: false,
),
ListTile(
leading: const Icon(MingCute.question_line),
title: TipText('${libL10n.temperature} (°C)', l10n.tempIsCelsiusTip),
trailing: _tempIsCelsius.listenVal(
(v) => Switch(
value: v,
onChanged: (val) {
_tempIsCelsius.value = val;
},
),
),
).cardx,
Input(
controller: _netDevCtrl,
type: TextInputType.text,
label: libL10n.net,
icon: ZondIcons.network,
hint: 'eth0',
suggestion: false,
),
],
);
}
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,
type: TextInputType.url,
node: _alterUrlFocus,
label: l10n.fallbackSshDest,
icon: MingCute.link_line,
hint: 'user@ip:port',
suggestion: false,
);
}
Widget _buildPVEs() {
const addr = 'https://127.0.0.1:8006';
return _keyIdx.listenVal((v) {
final useKeyAuth = v != null && v >= 0;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CenterGreyTitle('PVE'),
Input(
controller: _pveAddrCtrl,
type: TextInputType.url,
icon: MingCute.web_line,
label: 'URL',
hint: addr,
suggestion: false,
),
if (useKeyAuth)
Input(
controller: _pvePwdCtrl,
type: TextInputType.visiblePassword,
icon: MingCute.lock_line,
label: l10n.pvePassword,
hint: l10n.pvePasswordHint,
obscureText: true,
suggestion: false,
),
ListTile(
leading: const Icon(MingCute.certificate_line),
title: TipText('PVE ${l10n.ignoreCert}', l10n.pveIgnoreCertTip),
trailing: _pveIgnoreCert.listenVal(
(v) => Switch(
value: v,
onChanged: (val) {
_pveIgnoreCert.value = val;
},
),
),
).cardx,
],
);
});
}
Widget _buildCustomCmds() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CenterGreyTitle(l10n.customCmd),
_customCmds.listenVal((vals) {
return ListTile(
leading: const Icon(BoxIcons.bxs_file_json),
title: const Text('JSON'),
subtitle: vals.isEmpty
? null
: Text(vals.keys.join(','), style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: _onTapCustomItem,
);
}).cardx,
ListTile(
leading: const Icon(MingCute.doc_line),
title: Text(libL10n.doc),
trailing: const Icon(Icons.open_in_new, size: 17),
onTap: libL10n.customCmdDocUrl.launchUrl,
).cardx,
],
);
}
Widget _buildDisabledCmdTypes() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CenterGreyTitle('${libL10n.disabled} ${libL10n.cmd}'),
_disabledCmdTypes.listenVal((disabled) {
return ListTile(
leading: const Icon(Icons.disabled_by_default),
title: Text('${libL10n.disabled} ${libL10n.cmd}'),
subtitle: disabled.isEmpty
? null
: Text(disabled.join(', '), style: UIs.textGrey),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: _onTapDisabledCmdTypes,
);
}).cardx,
],
);
}
Widget _buildWOLs() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CenterGreyTitle('Wake On LAN (beta)'),
ListTile(
leading: const Icon(BoxIcons.bxs_help_circle),
title: TipText(libL10n.about, l10n.wolTip),
).cardx,
Input(
controller: _wolMacCtrl,
type: TextInputType.text,
label: 'MAC ${l10n.addr}',
icon: Icons.computer,
hint: '00:11:22:33:44:55',
suggestion: false,
),
Input(
controller: _wolIpCtrl,
type: TextInputType.text,
label: 'IP ${l10n.addr}',
icon: ZondIcons.network,
hint: '192.168.1.x',
suggestion: false,
),
Input(
controller: _wolPwdCtrl,
type: TextInputType.text,
obscureText: true,
label: libL10n.pwd,
icon: Icons.password,
suggestion: false,
),
],
);
}
Widget _buildFAB() {
return FloatingActionButton(
onPressed: _onSave,
child: const Icon(Icons.save),
);
}
Widget _buildJumpServer() {
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
final srvs = ref
.watch(serversProvider)
.servers
.values
.where((e) => e.id != spi?.id)
.where((e) => !_isInvalidJumpSelection(e.id))
.toList();
final choice = _jumpServer.listenVal((val) {
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
return Choice<Spi>(
multiple: false,
clearable: true,
value: srv != null ? [srv] : [],
builder: (state, _) => Wrap(
children: List<Widget>.generate(srvs.length, (index) {
final item = srvs[index];
return ChoiceChipX<Spi>(
key: ValueKey(item),
label: item.name,
state: state,
value: item,
onSelected: (srv, on) {
if (on) {
_jumpServer.value = srv.id;
} else {
_jumpServer.value = null;
}
},
);
}),
),
);
});
return ExpandTile(
leading: const Icon(Icons.map),
initiallyExpanded: _jumpServer.value != null,
childrenPadding: padding,
title: Text(l10n.jumpServer),
children: [choice],
).cardx;
}
Widget _buildWriteScriptTip() {
return Btn.tile(
text: libL10n.attention,
icon: const Icon(Icons.tips_and_updates, color: Colors.grey),
onTap: () {
context.showRoundDialog(
title: libL10n.attention,
child: SimpleMarkdown(data: l10n.writeScriptTip),
actions: Btnx.oks,
);
},
textStyle: UIs.textGrey,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildDelBtn() {
return IconButton(
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(
libL10n.askContinue(
'${libL10n.delete} ${libL10n.server}(${spi!.name})',
),
),
actions: Btn.ok(
onTap: () async {
context.pop();
await ref.read(serversProvider.notifier).delServer(spi!.id);
if (!mounted) return;
context.pop(true);
},
red: true,
).toList,
);
},
icon: const Icon(Icons.delete),
);
}
}