Files
flutter_opencode_client/lib/view/page/server/edit/edit.dart
GT610 1bea565c21 refactor(server): Move the SSH import and discovery features from the server edit page to the settings page (#1079)
* refactor(server): Move the SSH import and discovery features from the server edit page to the settings page

* feat (SSH Configuration): Added a feature to automatically import SSH configurations upon first launch

Checks for and prompts the user to import SSH configurations upon the first launch on the desktop

Optimized the SSH server import logic, adding duplicate detection and name conflict handling

Fixed an issue with mount status checks that could occur during the import process

* refactor (UI): Adjust the placement of the QR code scanning and SSH configuration import features

Move the QR code scanning feature from the server editing page to the settings page, and display different access points based on the platform

Optimize the SSH configuration import logic to ensure the status is updated correctly after the configuration is read for the first time

* refactor(ssh): Refactor server import logic and extract common methods

Extract server import logic into the `ServerDeduplication` class

Use the `importServersWithNotification` method consistently to handle imports

Remove duplicate `_importServers` and `_resolveServers` methods

Add checks for existing server IDs

* refactor(SSH): Optimized server import logic and fixed permission issues

- Moved the SSH configuration import logic from `edit.dart` to `actions.dart`
- Removed redundant checks for the `mounted` parameter
- Added handling for file permission exceptions
- Improved logic for resolving server name conflicts

* fix(ssh): Fixed an issue with message display during SSH configuration import

- Modified the format of the import success message to display the number of servers successfully imported
- Added a prompt for manual selection when permissions are denied
- Optimized the server deduplication logic to display an “already exists” message based on the original count

* fix(ssh): Fixed an issue with the count display when importing SSH configurations

Adjusted the server's deduplication logic to ensure the correct original count is used when displaying the number of imports

Removed unnecessary flag settings for the first read of SSH configurations

* fix: Fixed an issue where the “first read” flag was not updated when SSH configuration access was denied

When SSH configuration access is denied, set the “first read” flag to false to prevent repeated prompts

* fix(server): Optimized the logic for checking existing servers when importing SSH configurations

Moved the logic for checking existing servers to an earlier stage to avoid unnecessary parsing of SSH configurations
2026-03-20 20:41:09 +08:00

218 lines
6.5 KiB
Dart

import 'dart:io';
import 'package:choice/choice.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/jump_chain.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(),
];
final children = [
SizedBox(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
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: libL10n.host,
icon: BoxIcons.bx_server,
hint: 'example.com',
suggestion: false,
),
Input(
controller: _portController,
type: TextInputType.number,
node: _portFocus,
onSubmitted: (_) => _focusScope.requestFocus(_usernameFocus),
label: libL10n.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(serversProvider).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 if (isDesktop && Stores.setting.firstTimeReadSSHCfg.fetch()) {
_checkSSHConfigImport();
}
}
}