part of 'edit.dart'; /// Only permit ipv4 / ipv6 / domain chars (including IPv6 zone identifier like %en0) final _hostReg = RegExp(r'^[a-zA-Z0-9\.\-_:%;]+$'); extension _Actions on _ServerEditPageState { bool _isInvalidJumpSelection(String? candidateJumpId) { final currentServer = spi; return wouldCreateJumpCycle( currentServerId: currentServer?.id, candidateJumpId: candidateJumpId, serversById: ref.read(serversProvider).servers, ); } void _onTapCustomItem() async { final res = await KvEditor.route.go( context, KvEditorArgs(data: _customCmds.value), ); if (res == null) return; _customCmds.value = res; } void _onTapDisabledCmdTypes() async { final allCmdTypes = ShellCmdType.all; // [TimeSeq] depends on the `time` cmd type, so it should be removed from the list allCmdTypes.remove(StatusCmdType.time); await _showCmdTypesDialog(allCmdTypes); } void _onSave() async { if (_ipController.text.isEmpty) { context.showSnackBar('${libL10n.empty} ${libL10n.host}'); return; } if (!_hostReg.hasMatch(_ipController.text)) { context.showSnackBar(l10n.invalidHostFormat); return; } if (_keyIdx.value == null && _passwordController.text.isEmpty) { final ok = await context.showRoundDialog( title: libL10n.attention, child: Text(libL10n.askContinue(l10n.useNoPwd)), actions: Btnx.cancelRedOk, ); if (ok != true) return; } // If [_pubKeyIndex] is -1, it means that the user has not selected if (_keyIdx.value == -1) { context.showSnackBar(libL10n.empty); return; } if (_usernameController.text.isEmpty) { _usernameController.text = 'root'; } if (_portController.text.isEmpty) { _portController.text = '22'; } if (_isInvalidJumpSelection(_jumpServer.value)) { context.showSnackBar('${l10n.invalid}: ${l10n.jumpServer}'); return; } final customCmds = _customCmds.value; final custom = ServerCustom( pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull, pveIgnoreCert: _pveIgnoreCert.value, pvePwd: _pvePwdCtrl.text.selfNotEmptyOrNull, cmds: customCmds.isEmpty ? null : customCmds, preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull, tempIsCelsius: _tempIsCelsius.value, logoUrl: _logoUrlCtrl.text.selfNotEmptyOrNull, netDev: _netDevCtrl.text.selfNotEmptyOrNull, scriptDir: _scriptDirCtrl.text.selfNotEmptyOrNull, ); final wolEmpty = _wolMacCtrl.text.isEmpty && _wolIpCtrl.text.isEmpty && _wolPwdCtrl.text.isEmpty; final wol = wolEmpty ? null : WakeOnLanCfg( mac: _wolMacCtrl.text, ip: _wolIpCtrl.text, pwd: _wolPwdCtrl.text.selfNotEmptyOrNull, ); if (wol != null) { final wolValidation = wol.validate(); if (!wolValidation.$2) { context.showSnackBar('${libL10n.fail}: ${wolValidation.$1}'); return; } } final spi = Spi( name: _nameController.text.isEmpty ? _ipController.text : _nameController.text, ip: _ipController.text, port: int.parse(_portController.text), user: _usernameController.text, pwd: _passwordController.text.selfNotEmptyOrNull, keyId: _keyIdx.value != null ? ref.read(privateKeyProvider).keys.elementAt(_keyIdx.value!).id : null, tags: _tags.value.isEmpty ? null : _tags.value.toList(), alterUrl: _altUrlController.text.selfNotEmptyOrNull, autoConnect: _autoConnect.value, jumpId: _jumpServer.value, custom: custom, wolCfg: wol, envs: _env.value.isEmpty ? null : _env.value, id: widget.args?.spi.id ?? ShortId.generate(), customSystemType: _systemType.value, disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(), ); if (this.spi == null) { final existsIds = ServerStore.instance.box.keys; if (existsIds.contains(spi.id)) { context.showSnackBar('${l10n.sameIdServerExist}: ${spi.id}'); return; } ref.read(serversProvider.notifier).addServer(spi); } else { ref.read(serversProvider.notifier).updateServer(this.spi!, spi); } context.pop(); } } extension _Utils on _ServerEditPageState { Future _checkSSHConfigImport() async { final hasExistingServers = ref.read(serversProvider).servers.isNotEmpty; if (hasExistingServers) { Stores.setting.firstTimeReadSSHCfg.put(false); return; } try { final servers = await SSHConfig.parseConfig(); if (!mounted) return; if (servers.isEmpty) { Stores.setting.firstTimeReadSSHCfg.put(false); return; } final shouldImport = await context.showRoundDialog( title: l10n.sshConfigImport, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(l10n.sshConfigFound), const SizedBox(height: 8), Text(l10n.sshConfigImportPermission), ], ), actions: Btnx.cancelOk, ); if (!mounted) return; Stores.setting.firstTimeReadSSHCfg.put(false); if (shouldImport == true) { await ServerDeduplication.importServersWithNotification( servers: servers, ref: ref, context: context, allExistMessage: l10n.sshConfigAllExist, importedMessage: l10n.sshConfigImported, ); } } catch (e) { if (!mounted) return; if (e is PathAccessException || e.toString().contains('Operation not permitted')) { Stores.setting.firstTimeReadSSHCfg.put(false); context.showSnackBar( '${l10n.sshConfigPermissionDenied} ${l10n.sshConfigManualSelect}', ); } else { dprint('Error checking SSH config: $e'); Stores.setting.firstTimeReadSSHCfg.put(false); } } } Future _showCmdTypesDialog(Set allCmdTypes) { return context.showRoundDialog( title: '${libL10n.disabled} ${libL10n.cmd}', child: SizedBox( width: 270, child: _disabledCmdTypes.listenVal((disabled) { return ListView.builder( itemCount: allCmdTypes.length, itemExtent: 50, itemBuilder: (context, index) { final cmdType = allCmdTypes.elementAtOrNull(index); if (cmdType == null) return UIs.placeholder; final display = cmdType.displayName; return ListTile( leading: Icon(cmdType.sysType.icon, size: 20), title: Text(cmdType.name, style: const TextStyle(fontSize: 16)), trailing: Checkbox( value: disabled.contains(display), onChanged: (value) { if (value == null) return; if (value) { _disabledCmdTypes.value.add(display); } else { _disabledCmdTypes.value.remove(display); } _disabledCmdTypes.notify(); }, ), onTap: () { final isDisabled = disabled.contains(display); if (isDisabled) { _disabledCmdTypes.value.remove(display); } else { _disabledCmdTypes.value.add(display); } _disabledCmdTypes.notify(); }, ); }, ); }), ), actions: Btnx.oks, ); } void _initWithSpi(Spi spi) { _nameController.text = spi.name; _ipController.text = spi.ip; _portController.text = spi.port.toString(); _usernameController.text = spi.user; if (spi.keyId == null) { _passwordController.text = spi.pwd ?? ''; } else { _keyIdx.value = ref .read(privateKeyProvider) .keys .indexWhere((e) => e.id == spi.keyId); } /// List in dart is passed by pointer, so you need to copy it here _tags.value = spi.tags?.toSet() ?? {}; _altUrlController.text = spi.alterUrl ?? ''; _autoConnect.value = spi.autoConnect; _jumpServer.value = spi.jumpId; final custom = spi.custom; if (custom != null) { _pveAddrCtrl.text = custom.pveAddr ?? ''; _pveIgnoreCert.value = custom.pveIgnoreCert; _pvePwdCtrl.text = custom.pvePwd ?? ''; _customCmds.value = custom.cmds ?? {}; _preferTempDevCtrl.text = custom.preferTempDev ?? ''; _tempIsCelsius.value = custom.tempIsCelsius; _logoUrlCtrl.text = custom.logoUrl ?? ''; } final wol = spi.wolCfg; if (wol != null) { _wolMacCtrl.text = wol.mac; _wolIpCtrl.text = wol.ip; _wolPwdCtrl.text = wol.pwd ?? ''; } _env.value = spi.envs ?? {}; _netDevCtrl.text = spi.custom?.netDev ?? ''; _scriptDirCtrl.text = spi.custom?.scriptDir ?? ''; _systemType.value = spi.customSystemType; final disabledCmdTypes = spi.disabledCmdTypes?.toSet() ?? {}; final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName); disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e)); _disabledCmdTypes.value = disabledCmdTypes; } }