feat: discover local ssh server (#921)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-09-19 23:29:01 +08:00
committed by GitHub
parent 17db393c12
commit f68c4a851b
48 changed files with 3728 additions and 1928 deletions

View File

@@ -1,6 +1,100 @@
part of 'edit.dart';
extension _Actions on _ServerEditPageState {
Future<void> _onTapSSHDiscovery() async {
try {
final result = await SshDiscoveryPage.route.go(context);
if (result != null && result.isNotEmpty) {
await _processDiscoveredServers(result);
}
} catch (e, s) {
context.showErrDialog(e, s);
}
}
Future<void> _processDiscoveredServers(List<SshDiscoveryResult> discoveredServers) async {
if (discoveredServers.length == 1) {
// Single server - populate the current form
final server = discoveredServers.first;
_ipController.text = server.ip;
_portController.text = server.port.toString();
if (_nameController.text.isEmpty) {
_nameController.text = server.ip;
}
context.showSnackBar('${libL10n.found} 1 ${l10n.server}');
} else {
// Multiple servers - show import dialog
final shouldImport = await context.showRoundDialog<bool>(
title: libL10n.import,
child: Text(libL10n.askContinue('${libL10n.found} ${discoveredServers.length} ${l10n.servers}')),
actions: Btnx.cancelOk,
);
if (shouldImport == true) {
// Prompt user to configure default values before importing
final defaultUsername = 'root';
final defaultKeyId = _keyIdx.value?.toString() ?? '';
final usernameController = TextEditingController(text: defaultUsername);
final keyIdController = TextEditingController(text: defaultKeyId);
final shouldProceed = await context.showRoundDialog<bool>(
title: libL10n.import,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('${libL10n.found} ${discoveredServers.length} ${l10n.servers}.'),
const SizedBox(height: 8),
Text(libL10n.setting),
const SizedBox(height: 8),
TextField(
controller: usernameController,
decoration: InputDecoration(labelText: libL10n.user),
),
TextField(
controller: keyIdController,
decoration: InputDecoration(labelText: l10n.privateKey),
),
],
),
actions: Btnx.cancelOk,
);
if (shouldProceed == true) {
final username = usernameController.text.isNotEmpty ? usernameController.text : defaultUsername;
final keyId = keyIdController.text.isNotEmpty ? keyIdController.text : null;
final servers = discoveredServers.map((result) => Spi(
name: result.ip,
ip: result.ip,
port: result.port,
user: username,
keyId: keyId,
pwd: _passwordController.text.isEmpty ? null : _passwordController.text,
)).toList();
await _batchImportServers(servers);
}
usernameController.dispose();
keyIdController.dispose();
}
}
}
Future<void> _batchImportServers(List<Spi> servers) async {
final store = Stores.server;
int imported = 0;
for (final server in servers) {
try {
store.put(server);
imported++;
} catch (e) {
dprint('Failed to import server ${server.name}: $e');
}
}
context.showSnackBar('${libL10n.success}: $imported ${l10n.servers}');
if (mounted) Navigator.of(context).pop(true);
}
void _onTapSSHImport() async {
try {
final servers = await SSHConfig.parseConfig();

View File

@@ -13,6 +13,7 @@ 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/discovery_result.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';
@@ -21,6 +22,7 @@ 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';
import 'package:server_box/view/page/server/discovery/discovery.dart';
part 'actions.dart';
part 'widget.dart';
@@ -30,17 +32,13 @@ class ServerEditPage extends ConsumerStatefulWidget {
const ServerEditPage({super.key, this.args});
static const route = AppRoute<bool, SpiRequiredArgs>(
page: ServerEditPage.new,
path: '/servers/edit',
);
static const route = AppRoute<bool, SpiRequiredArgs>(page: ServerEditPage.new, path: '/servers/edit');
@override
ConsumerState<ServerEditPage> createState() => _ServerEditPageState();
}
class _ServerEditPageState extends ConsumerState<ServerEditPage>
with AfterLayoutMixin {
class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayoutMixin {
late final spi = widget.args?.spi;
final _nameController = TextEditingController();
final _ipController = TextEditingController();
@@ -137,11 +135,12 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage>
_buildWriteScriptTip(),
if (isMobile) _buildQrScan(),
if (isDesktop) _buildSSHImport(),
_buildSSHDiscovery(),
];
final children = [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: topItems.joinWith(UIs.width13).toList(),
SizedBox(
height: 50,
child: ListView(scrollDirection: Axis.horizontal, children: topItems.joinWith(UIs.width13).toList()),
),
Input(
autoFocus: true,
@@ -186,10 +185,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage>
hint: 'root',
suggestion: false,
),
TagTile(
tags: _tags,
allTags: ref.watch(serversNotifierProvider).tags,
).cardx,
TagTile(tags: _tags, allTags: ref.watch(serversNotifierProvider).tags).cardx,
ListTile(
title: Text(l10n.autoConnect),
trailing: _autoConnect.listenVal(

View File

@@ -439,6 +439,16 @@ extension _Widgets on _ServerEditPageState {
);
}
Widget _buildSSHDiscovery() {
return Btn.tile(
text: l10n.discoverSshServers,
icon: const Icon(BoxIcons.bx_search, color: Colors.grey),
onTap: _onTapSSHDiscovery,
textStyle: UIs.textGrey,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildDelBtn() {
return IconButton(
onPressed: () {