mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
fix: private key can't be selected in edit page (#879)
This commit is contained in:
@@ -7,7 +7,7 @@ part of 'private_key.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$privateKeyNotifierHash() =>
|
String _$privateKeyNotifierHash() =>
|
||||||
r'404836a4409f64d305c1e22f4a57b52985a57b68';
|
r'12edd05dca29d1cbc9e2a3e047c3d417d22f7bb7';
|
||||||
|
|
||||||
/// See also [PrivateKeyNotifier].
|
/// See also [PrivateKeyNotifier].
|
||||||
@ProviderFor(PrivateKeyNotifier)
|
@ProviderFor(PrivateKeyNotifier)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'all.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$serversNotifierHash() => r'2ae641188f772794a32e8700c008f51ba0cc1ec9';
|
String _$serversNotifierHash() => r'2b29ad3027a203c7a20bfd0142d384a503cbbcaa';
|
||||||
|
|
||||||
/// See also [ServersNotifier].
|
/// See also [ServersNotifier].
|
||||||
@ProviderFor(ServersNotifier)
|
@ProviderFor(ServersNotifier)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'snippet.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$snippetNotifierHash() => r'caf0361f9a0346fb99cb90f032f1ceb29446dd71';
|
String _$snippetNotifierHash() => r'8285c7edf905a4aaa41cd8b65b0a6755c8b97fc9';
|
||||||
|
|
||||||
/// See also [SnippetNotifier].
|
/// See also [SnippetNotifier].
|
||||||
@ProviderFor(SnippetNotifier)
|
@ProviderFor(SnippetNotifier)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import 'package:server_box/data/model/server/system.dart';
|
|||||||
import 'package:server_box/data/provider/server/single.dart';
|
import 'package:server_box/data/provider/server/single.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/pve.dart';
|
import 'package:server_box/view/page/pve.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|
||||||
part 'misc.dart';
|
part 'misc.dart';
|
||||||
|
|||||||
@@ -1,964 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:choice/choice.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:fl_lib/fl_lib.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:icons_plus/icons_plus.dart';
|
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
|
||||||
import 'package:server_box/core/route.dart';
|
|
||||||
import 'package:server_box/core/utils/server_dedup.dart';
|
|
||||||
import 'package:server_box/core/utils/ssh_config.dart';
|
|
||||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
|
||||||
import 'package:server_box/data/model/server/custom.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/all.dart';
|
|
||||||
import 'package:server_box/data/res/store.dart';
|
|
||||||
import 'package:server_box/data/store/server.dart';
|
|
||||||
import 'package:server_box/view/page/private_key/edit.dart';
|
|
||||||
|
|
||||||
class ServerEditPage extends ConsumerStatefulWidget {
|
|
||||||
final SpiRequiredArgs? args;
|
|
||||||
|
|
||||||
const ServerEditPage({super.key, this.args});
|
|
||||||
|
|
||||||
static const route = AppRoute<bool, SpiRequiredArgs>(page: ServerEditPage.new, path: '/servers/edit');
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayoutMixin {
|
|
||||||
late final spi = widget.args?.spi;
|
|
||||||
final _nameController = TextEditingController();
|
|
||||||
final _ipController = TextEditingController();
|
|
||||||
final _altUrlController = TextEditingController();
|
|
||||||
final _portController = TextEditingController();
|
|
||||||
final _usernameController = TextEditingController();
|
|
||||||
final _passwordController = TextEditingController();
|
|
||||||
final _pveAddrCtrl = TextEditingController();
|
|
||||||
final _preferTempDevCtrl = TextEditingController();
|
|
||||||
final _logoUrlCtrl = TextEditingController();
|
|
||||||
final _wolMacCtrl = TextEditingController();
|
|
||||||
final _wolIpCtrl = TextEditingController();
|
|
||||||
final _wolPwdCtrl = TextEditingController();
|
|
||||||
final _netDevCtrl = TextEditingController();
|
|
||||||
final _scriptDirCtrl = TextEditingController();
|
|
||||||
|
|
||||||
final _nameFocus = FocusNode();
|
|
||||||
final _ipFocus = FocusNode();
|
|
||||||
final _alterUrlFocus = FocusNode();
|
|
||||||
final _portFocus = FocusNode();
|
|
||||||
final _usernameFocus = FocusNode();
|
|
||||||
|
|
||||||
late FocusScopeNode _focusScope;
|
|
||||||
|
|
||||||
/// -1: non selected, null: password, others: index of private key
|
|
||||||
final _keyIdx = ValueNotifier<int?>(null);
|
|
||||||
final _autoConnect = ValueNotifier(true);
|
|
||||||
final _jumpServer = nvn<String?>();
|
|
||||||
final _pveIgnoreCert = ValueNotifier(false);
|
|
||||||
final _env = <String, String>{}.vn;
|
|
||||||
final _customCmds = <String, String>{}.vn;
|
|
||||||
final _tags = <String>{}.vn;
|
|
||||||
final _systemType = ValueNotifier<SystemType?>(null);
|
|
||||||
final _disabledCmdTypes = <String>{}.vn;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
_nameController.dispose();
|
|
||||||
_ipController.dispose();
|
|
||||||
_altUrlController.dispose();
|
|
||||||
_portController.dispose();
|
|
||||||
_usernameController.dispose();
|
|
||||||
_passwordController.dispose();
|
|
||||||
_preferTempDevCtrl.dispose();
|
|
||||||
_logoUrlCtrl.dispose();
|
|
||||||
_wolMacCtrl.dispose();
|
|
||||||
_wolIpCtrl.dispose();
|
|
||||||
_wolPwdCtrl.dispose();
|
|
||||||
_netDevCtrl.dispose();
|
|
||||||
_scriptDirCtrl.dispose();
|
|
||||||
|
|
||||||
_nameFocus.dispose();
|
|
||||||
_ipFocus.dispose();
|
|
||||||
_alterUrlFocus.dispose();
|
|
||||||
_portFocus.dispose();
|
|
||||||
_usernameFocus.dispose();
|
|
||||||
_pveAddrCtrl.dispose();
|
|
||||||
|
|
||||||
_keyIdx.dispose();
|
|
||||||
_autoConnect.dispose();
|
|
||||||
_jumpServer.dispose();
|
|
||||||
_pveIgnoreCert.dispose();
|
|
||||||
_env.dispose();
|
|
||||||
_customCmds.dispose();
|
|
||||||
_tags.dispose();
|
|
||||||
_systemType.dispose();
|
|
||||||
_disabledCmdTypes.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_focusScope = FocusScope.of(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final actions = <Widget>[];
|
|
||||||
if (spi != null) actions.add(_buildDelBtn());
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => _focusScope.unfocus(),
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),
|
|
||||||
body: _buildForm(),
|
|
||||||
floatingActionButton: _buildFAB(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildForm() {
|
|
||||||
final topItems = [_buildWriteScriptTip(), if (isMobile) _buildQrScan(), if (isDesktop) _buildSSHImport()];
|
|
||||||
final children = [
|
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: topItems.joinWith(UIs.width13).toList()),
|
|
||||||
Input(
|
|
||||||
autoFocus: true,
|
|
||||||
controller: _nameController,
|
|
||||||
type: TextInputType.text,
|
|
||||||
node: _nameFocus,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
|
|
||||||
hint: libL10n.example,
|
|
||||||
label: libL10n.name,
|
|
||||||
icon: BoxIcons.bx_rename,
|
|
||||||
obscureText: false,
|
|
||||||
autoCorrect: true,
|
|
||||||
suggestion: true,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _ipController,
|
|
||||||
type: TextInputType.url,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_portFocus),
|
|
||||||
node: _ipFocus,
|
|
||||||
label: l10n.host,
|
|
||||||
icon: BoxIcons.bx_server,
|
|
||||||
hint: 'example.com',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _portController,
|
|
||||||
type: TextInputType.number,
|
|
||||||
node: _portFocus,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
|
|
||||||
label: l10n.port,
|
|
||||||
icon: Bootstrap.number_123,
|
|
||||||
hint: '22',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _usernameController,
|
|
||||||
type: TextInputType.text,
|
|
||||||
node: _usernameFocus,
|
|
||||||
onSubmitted: (_) => _focusScope.requestFocus(_alterUrlFocus),
|
|
||||||
label: libL10n.user,
|
|
||||||
icon: Icons.account_box,
|
|
||||||
hint: 'root',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
TagTile(tags: _tags, allTags: ref.watch(serversNotifierProvider).tags).cardx,
|
|
||||||
ListTile(
|
|
||||||
title: Text(l10n.autoConnect),
|
|
||||||
trailing: _autoConnect.listenVal(
|
|
||||||
(val) => Switch(
|
|
||||||
value: val,
|
|
||||||
onChanged: (val) {
|
|
||||||
_autoConnect.value = val;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_buildAuth(),
|
|
||||||
_buildSystemType(),
|
|
||||||
_buildJumpServer(),
|
|
||||||
_buildMore(),
|
|
||||||
];
|
|
||||||
return AutoMultiList(children: children);
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
final privateKeyState = ref.watch(privateKeyNotifierProvider);
|
|
||||||
final pkis = privateKeyState.keys;
|
|
||||||
|
|
||||||
final tiles = List<Widget>.generate(pkis.length, (index) {
|
|
||||||
final e = pkis[index];
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.only(left: 10, right: 15),
|
|
||||||
leading: Radio<int>(value: index),
|
|
||||||
title: Text(e.id, textAlign: TextAlign.start),
|
|
||||||
subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey),
|
|
||||||
trailing: Btn.icon(
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: e)),
|
|
||||||
),
|
|
||||||
onTap: () => _keyIdx.value = index,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
tiles.add(
|
|
||||||
ListTile(
|
|
||||||
title: Text(libL10n.add),
|
|
||||||
contentPadding: const EdgeInsets.only(left: 23, right: 23),
|
|
||||||
trailing: const Icon(Icons.add),
|
|
||||||
onTap: () => PrivateKeyEditPage.route.go(context),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return RadioGroup<int>(
|
|
||||||
onChanged: (val) => _keyIdx.value = val,
|
|
||||||
child: _keyIdx.listenVal((_) => Column(children: tiles)).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 ${l10n.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: l10n.temperature,
|
|
||||||
icon: MingCute.low_temperature_line,
|
|
||||||
hint: 'nvme-pci-0400',
|
|
||||||
suggestion: false,
|
|
||||||
),
|
|
||||||
Input(
|
|
||||||
controller: _netDevCtrl,
|
|
||||||
type: TextInputType.text,
|
|
||||||
label: l10n.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 Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const CenterGreyTitle('PVE'),
|
|
||||||
Input(
|
|
||||||
controller: _pveAddrCtrl,
|
|
||||||
type: TextInputType.url,
|
|
||||||
icon: MingCute.web_line,
|
|
||||||
label: 'URL',
|
|
||||||
hint: addr,
|
|
||||||
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: l10n.customCmdDocUrl.launchUrl,
|
|
||||||
).cardx,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDisabledCmdTypes() {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
CenterGreyTitle('${libL10n.disabled} ${l10n.cmd}'),
|
|
||||||
_disabledCmdTypes.listenVal((disabled) {
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(Icons.disabled_by_default),
|
|
||||||
title: Text('${libL10n.disabled} ${l10n.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(serversNotifierProvider)
|
|
||||||
.servers
|
|
||||||
.values
|
|
||||||
.where((e) => e.jumpId == null)
|
|
||||||
.where((e) => e.id != spi?.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>(
|
|
||||||
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 _buildQrScan() {
|
|
||||||
return Btn.tile(
|
|
||||||
text: libL10n.import,
|
|
||||||
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
|
||||||
onTap: () async {
|
|
||||||
final ret = await BarcodeScannerPage.route.go(context, args: const BarcodeScannerPageArgs());
|
|
||||||
final code = ret?.text;
|
|
||||||
if (code == null) return;
|
|
||||||
try {
|
|
||||||
final spi = Spi.fromJson(json.decode(code));
|
|
||||||
_initWithSpi(spi);
|
|
||||||
} catch (e, s) {
|
|
||||||
context.showErrDialog(e, s);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textStyle: UIs.textGrey,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSSHImport() {
|
|
||||||
return Btn.tile(
|
|
||||||
text: l10n.sshConfigImport,
|
|
||||||
icon: const Icon(Icons.settings, color: Colors.grey),
|
|
||||||
onTap: _onTapSSHImport,
|
|
||||||
textStyle: UIs.textGrey,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDelBtn() {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.showRoundDialog(
|
|
||||||
title: libL10n.attention,
|
|
||||||
child: Text(libL10n.askContinue('${libL10n.delete} ${l10n.server}(${spi!.name})')),
|
|
||||||
actions: Btn.ok(
|
|
||||||
onTap: () async {
|
|
||||||
context.pop();
|
|
||||||
ref.read(serversNotifierProvider.notifier).delServer(spi!.id);
|
|
||||||
context.pop(true);
|
|
||||||
},
|
|
||||||
red: true,
|
|
||||||
).toList,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void afterFirstLayout(BuildContext context) {
|
|
||||||
if (spi != null) {
|
|
||||||
_initWithSpi(spi!);
|
|
||||||
} else {
|
|
||||||
// Only for new servers, check SSH config import on first time
|
|
||||||
_checkSSHConfigImport();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _Actions on _ServerEditPageState {
|
|
||||||
void _onTapSSHImport() async {
|
|
||||||
try {
|
|
||||||
final servers = await SSHConfig.parseConfig();
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
context.showSnackBar(l10n.sshConfigNoServers);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dprint('Parsed ${servers.length} servers from SSH config');
|
|
||||||
await _processSSHServers(servers);
|
|
||||||
dprint('Finished processing SSH config servers');
|
|
||||||
} catch (e, s) {
|
|
||||||
_handleImportSSHCfgPermissionIssue(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
|
|
||||||
dprint('Error importing SSH config: $e');
|
|
||||||
// Check if it's a permission error and offer file picker as fallback
|
|
||||||
if (e is PathAccessException || e.toString().contains('Operation not permitted')) {
|
|
||||||
final useFilePicker = await context.showRoundDialog<bool>(
|
|
||||||
title: l10n.sshConfigImport,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.sshConfigPermissionDenied),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(l10n.sshConfigManualSelect),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: Btnx.cancelOk,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (useFilePicker == true) {
|
|
||||||
await _onTapSSHImportWithFilePicker();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
context.showErrDialog(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _processSSHServers(List<Spi> servers) async {
|
|
||||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
|
||||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
|
||||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
|
||||||
|
|
||||||
if (!summary.hasItemsToImport) {
|
|
||||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final shouldImport = await context.showRoundDialog<bool>(
|
|
||||||
title: l10n.sshConfigImport,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.sshConfigFoundServers('${summary.total}')),
|
|
||||||
if (summary.hasDuplicates)
|
|
||||||
Text(l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'), style: UIs.textGrey),
|
|
||||||
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
...resolved.map((s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: Btnx.cancelOk,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldImport == true) {
|
|
||||||
for (final server in resolved) {
|
|
||||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
|
||||||
}
|
|
||||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onTapSSHImportWithFilePicker() async {
|
|
||||||
try {
|
|
||||||
final result = await FilePicker.platform.pickFiles(
|
|
||||||
type: FileType.any,
|
|
||||||
allowMultiple: false,
|
|
||||||
dialogTitle: 'SSH ${libL10n.select}',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result?.files.single.path case final path?) {
|
|
||||||
final servers = await SSHConfig.parseConfig(path);
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
context.showSnackBar(l10n.sshConfigNoServers);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _processSSHServers(servers);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
context.showErrDialog(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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} ${l10n.host}');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
|
||||||
final ok = await context.showRoundDialog<bool>(
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
final customCmds = _customCmds.value;
|
|
||||||
final custom = ServerCustom(
|
|
||||||
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
|
||||||
pveIgnoreCert: _pveIgnoreCert.value,
|
|
||||||
cmds: customCmds.isEmpty ? null : customCmds,
|
|
||||||
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
|
||||||
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(privateKeyNotifierProvider).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(serversNotifierProvider.notifier).addServer(spi);
|
|
||||||
} else {
|
|
||||||
ref.read(serversNotifierProvider.notifier).updateServer(this.spi!, spi);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension _Utils on _ServerEditPageState {
|
|
||||||
void _checkSSHConfigImport() async {
|
|
||||||
final prop = Stores.setting.firstTimeReadSSHCfg;
|
|
||||||
// Only check if it's first time and user hasn't disabled it
|
|
||||||
if (!prop.fetch()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if SSH config exists
|
|
||||||
final (_, configExists) = SSHConfig.configExists();
|
|
||||||
if (!configExists) return;
|
|
||||||
|
|
||||||
// Ask for permission
|
|
||||||
final hasPermission = await context.showRoundDialog<bool>(
|
|
||||||
title: l10n.sshConfigImport,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(l10n.sshConfigFound),
|
|
||||||
UIs.height7,
|
|
||||||
Text(l10n.sshConfigImportPermission),
|
|
||||||
UIs.height7,
|
|
||||||
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: Btnx.cancelOk,
|
|
||||||
);
|
|
||||||
|
|
||||||
prop.put(false);
|
|
||||||
|
|
||||||
if (hasPermission == true) {
|
|
||||||
// Parse and import SSH config
|
|
||||||
final servers = await SSHConfig.parseConfig();
|
|
||||||
if (servers.isEmpty) {
|
|
||||||
context.showSnackBar(l10n.sshConfigNoServers);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
|
||||||
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
|
||||||
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
|
||||||
|
|
||||||
if (!summary.hasItemsToImport) {
|
|
||||||
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import without asking again since user already gave permission
|
|
||||||
for (final server in resolved) {
|
|
||||||
ref.read(serversNotifierProvider.notifier).addServer(server);
|
|
||||||
}
|
|
||||||
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
_handleImportSSHCfgPermissionIssue(e, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
|
||||||
return context.showRoundDialog(
|
|
||||||
title: '${libL10n.disabled} ${l10n.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(privateKeyNotifierProvider).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;
|
|
||||||
_customCmds.value = custom.cmds ?? {};
|
|
||||||
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
|
||||||
_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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
382
lib/view/page/server/edit/actions.dart
Normal file
382
lib/view/page/server/edit/actions.dart
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
part of 'edit.dart';
|
||||||
|
|
||||||
|
extension _Actions on _ServerEditPageState {
|
||||||
|
void _onTapSSHImport() async {
|
||||||
|
try {
|
||||||
|
final servers = await SSHConfig.parseConfig();
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
context.showSnackBar(l10n.sshConfigNoServers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dprint('Parsed ${servers.length} servers from SSH config');
|
||||||
|
await _processSSHServers(servers);
|
||||||
|
dprint('Finished processing SSH config servers');
|
||||||
|
} catch (e, s) {
|
||||||
|
_handleImportSSHCfgPermissionIssue(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
|
||||||
|
dprint('Error importing SSH config: $e');
|
||||||
|
// Check if it's a permission error and offer file picker as fallback
|
||||||
|
if (e is PathAccessException ||
|
||||||
|
e.toString().contains('Operation not permitted')) {
|
||||||
|
final useFilePicker = await context.showRoundDialog<bool>(
|
||||||
|
title: l10n.sshConfigImport,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.sshConfigPermissionDenied),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(l10n.sshConfigManualSelect),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (useFilePicker == true) {
|
||||||
|
await _onTapSSHImportWithFilePicker();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.showErrDialog(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _processSSHServers(List<Spi> servers) async {
|
||||||
|
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||||
|
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||||
|
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||||
|
|
||||||
|
if (!summary.hasItemsToImport) {
|
||||||
|
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shouldImport = await context.showRoundDialog<bool>(
|
||||||
|
title: l10n.sshConfigImport,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.sshConfigFoundServers('${summary.total}')),
|
||||||
|
if (summary.hasDuplicates)
|
||||||
|
Text(
|
||||||
|
l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'),
|
||||||
|
style: UIs.textGrey,
|
||||||
|
),
|
||||||
|
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
...resolved.map(
|
||||||
|
(s) => Text('• ${s.name} (${s.user}@${s.ip}:${s.port})'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldImport == true) {
|
||||||
|
for (final server in resolved) {
|
||||||
|
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||||
|
}
|
||||||
|
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onTapSSHImportWithFilePicker() async {
|
||||||
|
try {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.any,
|
||||||
|
allowMultiple: false,
|
||||||
|
dialogTitle: 'SSH ${libL10n.select}',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result?.files.single.path case final path?) {
|
||||||
|
final servers = await SSHConfig.parseConfig(path);
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
context.showSnackBar(l10n.sshConfigNoServers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _processSSHServers(servers);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
context.showErrDialog(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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} ${l10n.host}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_keyIdx.value == null && _passwordController.text.isEmpty) {
|
||||||
|
final ok = await context.showRoundDialog<bool>(
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
final customCmds = _customCmds.value;
|
||||||
|
final custom = ServerCustom(
|
||||||
|
pveAddr: _pveAddrCtrl.text.selfNotEmptyOrNull,
|
||||||
|
pveIgnoreCert: _pveIgnoreCert.value,
|
||||||
|
cmds: customCmds.isEmpty ? null : customCmds,
|
||||||
|
preferTempDev: _preferTempDevCtrl.text.selfNotEmptyOrNull,
|
||||||
|
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(privateKeyNotifierProvider)
|
||||||
|
.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(serversNotifierProvider.notifier).addServer(spi);
|
||||||
|
} else {
|
||||||
|
ref.read(serversNotifierProvider.notifier).updateServer(this.spi!, spi);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _Utils on _ServerEditPageState {
|
||||||
|
void _checkSSHConfigImport() async {
|
||||||
|
final prop = Stores.setting.firstTimeReadSSHCfg;
|
||||||
|
// Only check if it's first time and user hasn't disabled it
|
||||||
|
if (!prop.fetch()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if SSH config exists
|
||||||
|
final (_, configExists) = SSHConfig.configExists();
|
||||||
|
if (!configExists) return;
|
||||||
|
|
||||||
|
// Ask for permission
|
||||||
|
final hasPermission = await context.showRoundDialog<bool>(
|
||||||
|
title: l10n.sshConfigImport,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.sshConfigFound),
|
||||||
|
UIs.height7,
|
||||||
|
Text(l10n.sshConfigImportPermission),
|
||||||
|
UIs.height7,
|
||||||
|
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: Btnx.cancelOk,
|
||||||
|
);
|
||||||
|
|
||||||
|
prop.put(false);
|
||||||
|
|
||||||
|
if (hasPermission == true) {
|
||||||
|
// Parse and import SSH config
|
||||||
|
final servers = await SSHConfig.parseConfig();
|
||||||
|
if (servers.isEmpty) {
|
||||||
|
context.showSnackBar(l10n.sshConfigNoServers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deduplicated = ServerDeduplication.deduplicateServers(servers);
|
||||||
|
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
|
||||||
|
final summary = ServerDeduplication.getImportSummary(servers, resolved);
|
||||||
|
|
||||||
|
if (!summary.hasItemsToImport) {
|
||||||
|
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import without asking again since user already gave permission
|
||||||
|
for (final server in resolved) {
|
||||||
|
ref.read(serversNotifierProvider.notifier).addServer(server);
|
||||||
|
}
|
||||||
|
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
_handleImportSSHCfgPermissionIssue(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
|
||||||
|
return context.showRoundDialog(
|
||||||
|
title: '${libL10n.disabled} ${l10n.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(privateKeyNotifierProvider)
|
||||||
|
.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;
|
||||||
|
_customCmds.value = custom.cmds ?? {};
|
||||||
|
_preferTempDevCtrl.text = custom.preferTempDev ?? '';
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
221
lib/view/page/server/edit/edit.dart
Normal file
221
lib/view/page/server/edit/edit.dart
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:choice/choice.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/core/route.dart';
|
||||||
|
import 'package:server_box/core/utils/server_dedup.dart';
|
||||||
|
import 'package:server_box/core/utils/ssh_config.dart';
|
||||||
|
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||||
|
import 'package:server_box/data/model/server/custom.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/all.dart';
|
||||||
|
import 'package:server_box/data/res/store.dart';
|
||||||
|
import 'package:server_box/data/store/server.dart';
|
||||||
|
import 'package:server_box/view/page/private_key/edit.dart';
|
||||||
|
|
||||||
|
part 'actions.dart';
|
||||||
|
part 'widget.dart';
|
||||||
|
|
||||||
|
class ServerEditPage extends ConsumerStatefulWidget {
|
||||||
|
final SpiRequiredArgs? args;
|
||||||
|
|
||||||
|
const ServerEditPage({super.key, this.args});
|
||||||
|
|
||||||
|
static const route = AppRoute<bool, SpiRequiredArgs>(
|
||||||
|
page: ServerEditPage.new,
|
||||||
|
path: '/servers/edit',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServerEditPageState extends ConsumerState<ServerEditPage>
|
||||||
|
with AfterLayoutMixin {
|
||||||
|
late final spi = widget.args?.spi;
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _ipController = TextEditingController();
|
||||||
|
final _altUrlController = TextEditingController();
|
||||||
|
final _portController = TextEditingController();
|
||||||
|
final _usernameController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _pveAddrCtrl = TextEditingController();
|
||||||
|
final _preferTempDevCtrl = TextEditingController();
|
||||||
|
final _logoUrlCtrl = TextEditingController();
|
||||||
|
final _wolMacCtrl = TextEditingController();
|
||||||
|
final _wolIpCtrl = TextEditingController();
|
||||||
|
final _wolPwdCtrl = TextEditingController();
|
||||||
|
final _netDevCtrl = TextEditingController();
|
||||||
|
final _scriptDirCtrl = TextEditingController();
|
||||||
|
|
||||||
|
final _nameFocus = FocusNode();
|
||||||
|
final _ipFocus = FocusNode();
|
||||||
|
final _alterUrlFocus = FocusNode();
|
||||||
|
final _portFocus = FocusNode();
|
||||||
|
final _usernameFocus = FocusNode();
|
||||||
|
|
||||||
|
late FocusScopeNode _focusScope;
|
||||||
|
|
||||||
|
/// -1: non selected, null: password, others: index of private key
|
||||||
|
final _keyIdx = ValueNotifier<int?>(null);
|
||||||
|
final _autoConnect = ValueNotifier(true);
|
||||||
|
final _jumpServer = nvn<String?>();
|
||||||
|
final _pveIgnoreCert = ValueNotifier(false);
|
||||||
|
final _env = <String, String>{}.vn;
|
||||||
|
final _customCmds = <String, String>{}.vn;
|
||||||
|
final _tags = <String>{}.vn;
|
||||||
|
final _systemType = ValueNotifier<SystemType?>(null);
|
||||||
|
final _disabledCmdTypes = <String>{}.vn;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_nameController.dispose();
|
||||||
|
_ipController.dispose();
|
||||||
|
_altUrlController.dispose();
|
||||||
|
_portController.dispose();
|
||||||
|
_usernameController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_preferTempDevCtrl.dispose();
|
||||||
|
_logoUrlCtrl.dispose();
|
||||||
|
_wolMacCtrl.dispose();
|
||||||
|
_wolIpCtrl.dispose();
|
||||||
|
_wolPwdCtrl.dispose();
|
||||||
|
_netDevCtrl.dispose();
|
||||||
|
_scriptDirCtrl.dispose();
|
||||||
|
|
||||||
|
_nameFocus.dispose();
|
||||||
|
_ipFocus.dispose();
|
||||||
|
_alterUrlFocus.dispose();
|
||||||
|
_portFocus.dispose();
|
||||||
|
_usernameFocus.dispose();
|
||||||
|
_pveAddrCtrl.dispose();
|
||||||
|
|
||||||
|
_keyIdx.dispose();
|
||||||
|
_autoConnect.dispose();
|
||||||
|
_jumpServer.dispose();
|
||||||
|
_pveIgnoreCert.dispose();
|
||||||
|
_env.dispose();
|
||||||
|
_customCmds.dispose();
|
||||||
|
_tags.dispose();
|
||||||
|
_systemType.dispose();
|
||||||
|
_disabledCmdTypes.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_focusScope = FocusScope.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final actions = <Widget>[];
|
||||||
|
if (spi != null) actions.add(_buildDelBtn());
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _focusScope.unfocus(),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: CustomAppBar(title: Text(libL10n.edit), actions: actions),
|
||||||
|
body: _buildForm(),
|
||||||
|
floatingActionButton: _buildFAB(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildForm() {
|
||||||
|
final topItems = [
|
||||||
|
_buildWriteScriptTip(),
|
||||||
|
if (isMobile) _buildQrScan(),
|
||||||
|
if (isDesktop) _buildSSHImport(),
|
||||||
|
];
|
||||||
|
final children = [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: topItems.joinWith(UIs.width13).toList(),
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
autoFocus: true,
|
||||||
|
controller: _nameController,
|
||||||
|
type: TextInputType.text,
|
||||||
|
node: _nameFocus,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_ipFocus),
|
||||||
|
hint: libL10n.example,
|
||||||
|
label: libL10n.name,
|
||||||
|
icon: BoxIcons.bx_rename,
|
||||||
|
obscureText: false,
|
||||||
|
autoCorrect: true,
|
||||||
|
suggestion: true,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _ipController,
|
||||||
|
type: TextInputType.url,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_portFocus),
|
||||||
|
node: _ipFocus,
|
||||||
|
label: l10n.host,
|
||||||
|
icon: BoxIcons.bx_server,
|
||||||
|
hint: 'example.com',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _portController,
|
||||||
|
type: TextInputType.number,
|
||||||
|
node: _portFocus,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
|
||||||
|
label: l10n.port,
|
||||||
|
icon: Bootstrap.number_123,
|
||||||
|
hint: '22',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _usernameController,
|
||||||
|
type: TextInputType.text,
|
||||||
|
node: _usernameFocus,
|
||||||
|
onSubmitted: (_) => _focusScope.requestFocus(_alterUrlFocus),
|
||||||
|
label: libL10n.user,
|
||||||
|
icon: Icons.account_box,
|
||||||
|
hint: 'root',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
TagTile(
|
||||||
|
tags: _tags,
|
||||||
|
allTags: ref.watch(serversNotifierProvider).tags,
|
||||||
|
).cardx,
|
||||||
|
ListTile(
|
||||||
|
title: Text(l10n.autoConnect),
|
||||||
|
trailing: _autoConnect.listenVal(
|
||||||
|
(val) => Switch(
|
||||||
|
value: val,
|
||||||
|
onChanged: (val) {
|
||||||
|
_autoConnect.value = val;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildAuth(),
|
||||||
|
_buildSystemType(),
|
||||||
|
_buildJumpServer(),
|
||||||
|
_buildMore(),
|
||||||
|
];
|
||||||
|
return AutoMultiList(children: children);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void afterFirstLayout(BuildContext context) {
|
||||||
|
if (spi != null) {
|
||||||
|
_initWithSpi(spi!);
|
||||||
|
} else {
|
||||||
|
// Only for new servers, check SSH config import on first time
|
||||||
|
_checkSSHConfigImport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
465
lib/view/page/server/edit/widget.dart
Normal file
465
lib/view/page/server/edit/widget.dart
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
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(privateKeyNotifierProvider);
|
||||||
|
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>(
|
||||||
|
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 ${l10n.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: l10n.temperature,
|
||||||
|
icon: MingCute.low_temperature_line,
|
||||||
|
hint: 'nvme-pci-0400',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
Input(
|
||||||
|
controller: _netDevCtrl,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: l10n.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 Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CenterGreyTitle('PVE'),
|
||||||
|
Input(
|
||||||
|
controller: _pveAddrCtrl,
|
||||||
|
type: TextInputType.url,
|
||||||
|
icon: MingCute.web_line,
|
||||||
|
label: 'URL',
|
||||||
|
hint: addr,
|
||||||
|
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: l10n.customCmdDocUrl.launchUrl,
|
||||||
|
).cardx,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDisabledCmdTypes() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CenterGreyTitle('${libL10n.disabled} ${l10n.cmd}'),
|
||||||
|
_disabledCmdTypes.listenVal((disabled) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.disabled_by_default),
|
||||||
|
title: Text('${libL10n.disabled} ${l10n.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(serversNotifierProvider)
|
||||||
|
.servers
|
||||||
|
.values
|
||||||
|
.where((e) => e.jumpId == null)
|
||||||
|
.where((e) => e.id != spi?.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>(
|
||||||
|
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 _buildQrScan() {
|
||||||
|
return Btn.tile(
|
||||||
|
text: libL10n.import,
|
||||||
|
icon: const Icon(Icons.qr_code, color: Colors.grey),
|
||||||
|
onTap: () async {
|
||||||
|
final ret = await BarcodeScannerPage.route.go(
|
||||||
|
context,
|
||||||
|
args: const BarcodeScannerPageArgs(),
|
||||||
|
);
|
||||||
|
final code = ret?.text;
|
||||||
|
if (code == null) return;
|
||||||
|
try {
|
||||||
|
final spi = Spi.fromJson(json.decode(code));
|
||||||
|
_initWithSpi(spi);
|
||||||
|
} catch (e, s) {
|
||||||
|
context.showErrDialog(e, s);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textStyle: UIs.textGrey,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSSHImport() {
|
||||||
|
return Btn.tile(
|
||||||
|
text: l10n.sshConfigImport,
|
||||||
|
icon: const Icon(Icons.settings, color: Colors.grey),
|
||||||
|
onTap: _onTapSSHImport,
|
||||||
|
textStyle: UIs.textGrey,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDelBtn() {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.showRoundDialog(
|
||||||
|
title: libL10n.attention,
|
||||||
|
child: Text(
|
||||||
|
libL10n.askContinue(
|
||||||
|
'${libL10n.delete} ${l10n.server}(${spi!.name})',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: Btn.ok(
|
||||||
|
onTap: () async {
|
||||||
|
context.pop();
|
||||||
|
ref.read(serversNotifierProvider.notifier).delServer(spi!.id);
|
||||||
|
context.pop(true);
|
||||||
|
},
|
||||||
|
red: true,
|
||||||
|
).toList,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ import 'package:server_box/data/provider/server/single.dart';
|
|||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
import 'package:server_box/view/page/server/detail/view.dart';
|
import 'package:server_box/view/page/server/detail/view.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||||
import 'package:server_box/view/page/setting/entry.dart';
|
import 'package:server_box/view/page/setting/entry.dart';
|
||||||
import 'package:server_box/view/widget/percent_circle.dart';
|
import 'package:server_box/view/widget/percent_circle.dart';
|
||||||
import 'package:server_box/view/widget/server_func_btns.dart';
|
import 'package:server_box/view/widget/server_func_btns.dart';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:icons_plus/icons_plus.dart';
|
|||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/provider/server/all.dart';
|
import 'package:server_box/data/provider/server/all.dart';
|
||||||
import 'package:server_box/view/page/server/edit.dart';
|
import 'package:server_box/view/page/server/edit/edit.dart';
|
||||||
import 'package:server_box/view/page/ssh/page/page.dart';
|
import 'package:server_box/view/page/ssh/page/page.dart';
|
||||||
|
|
||||||
class SSHTabPage extends ConsumerStatefulWidget {
|
class SSHTabPage extends ConsumerStatefulWidget {
|
||||||
|
|||||||
32
pubspec.lock
32
pubspec.lock
@@ -189,18 +189,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: camera_android_camerax
|
name: camera_android_camerax
|
||||||
sha256: "58b8fe843a3c83fd1273c00cb35f5a8ae507f6cc9b2029bcf7e2abba499e28d8"
|
sha256: "2d438248554f44766bf9ea34c117a5bb0074e241342ef7c22c768fb431335234"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.19+1"
|
version: "0.6.21"
|
||||||
camera_avfoundation:
|
camera_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: camera_avfoundation
|
name: camera_avfoundation
|
||||||
sha256: e4aca5bccaf897b70cac87e5fdd789393310985202442837922fd40325e2733b
|
sha256: "951ef122d01ebba68b7a54bfe294e8b25585635a90465c311b2f875ae72c412f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.21+1"
|
version: "0.9.21+2"
|
||||||
camera_platform_interface:
|
camera_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -644,10 +644,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: de82e6bf958cec7190fbc1c5298282c851228e35ae2b14e2b103e7f777818c64
|
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.13"
|
version: "2.2.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -911,10 +911,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_android
|
name: local_auth_android
|
||||||
sha256: "316503f6772dea9c0c038bb7aac4f68ab00112d707d258c770f7fc3c250a2d88"
|
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.51"
|
version: "1.0.52"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1184,10 +1184,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: provider
|
name: provider
|
||||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5"
|
version: "6.1.5+1"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1613,10 +1613,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
|
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.17"
|
version: "6.3.18"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1685,18 +1685,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics_codec
|
name: vector_graphics_codec
|
||||||
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
|
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.11+1"
|
version: "1.1.13"
|
||||||
vector_graphics_compiler:
|
vector_graphics_compiler:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_graphics_compiler
|
name: vector_graphics_compiler
|
||||||
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
|
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.11+1"
|
version: "1.1.19"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user