* feat(PVE): Added display of PVE connection loading steps Added a detailed display of loading steps during the PVE connection process, including stages such as establishing an SSH tunnel, authentication, and data retrieval Also optimized the sorting of PVE storage content and the logic for handling connection errors * feat(pve): Added error handling and prompts for PVE two-factor authentication Added error handling for PVE servers when two-factor authentication is enabled, along with relevant error types and localized prompts * feat(PVE): Added support for PVE passwords during key-based authentication - Added the `pvePwd` field to the `ServerCustom` model - Added a PVE password input field to the edit page (displayed only during key-based authentication) - Updated multilingual files to support PVE-related loading states and password prompts - Optimized PVE connection logic to support password verification during key-based authentication
448 lines
12 KiB
Dart
448 lines
12 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,
|
|
),
|
|
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();
|
|
ref.read(serversProvider.notifier).delServer(spi!.id);
|
|
context.pop(true);
|
|
},
|
|
red: true,
|
|
).toList,
|
|
);
|
|
},
|
|
icon: const Icon(Icons.delete),
|
|
);
|
|
}
|
|
}
|