xworkmate-app/lib/features/settings/settings_page_sections.dart

577 lines
21 KiB
Dart

// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/app_metadata.dart';
import '../../app/app_store_policy.dart';
import '../../app/ui_feature_manifest.dart';
import '../../app/workspace_navigation.dart';
import '../../i18n/app_language.dart';
import '../../models/app_models.dart';
import '../../runtime/gateway_runtime.dart';
import '../../runtime/runtime_controllers.dart';
import '../../runtime/runtime_models.dart';
import 'codex_integration_card.dart';
import 'skill_directory_authorization_card.dart';
import '../../widgets/section_tabs.dart';
import '../../widgets/surface_card.dart';
import '../../widgets/top_bar.dart';
import 'settings_page_core.dart';
import 'settings_page_gateway.dart';
import 'settings_page_gateway_connection.dart';
import 'settings_page_gateway_llm.dart';
import 'settings_page_presentation.dart';
import 'settings_page_multi_agent.dart';
import 'settings_page_support.dart';
import 'settings_page_device.dart';
import 'settings_page_widgets.dart';
extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
List<Widget> buildContentForCurrentStateInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
UiFeatureAccess uiFeatures,
) {
if (detailInternal != null) {
return buildDetailContentInternal(
context,
controller,
settings,
uiFeatures,
detailInternal!,
);
}
return switch (tabInternal) {
SettingsTab.general => buildGeneralInternal(
context,
controller,
settings,
uiFeatures,
),
SettingsTab.workspace => buildWorkspaceInternal(
context,
controller,
settings,
),
SettingsTab.gateway => buildGatewayInternal(
context,
controller,
settings,
uiFeatures,
),
SettingsTab.agents => buildAgentsInternal(context, controller, settings),
SettingsTab.appearance => buildAppearanceInternal(context, controller),
SettingsTab.diagnostics => buildDiagnosticsInternal(context, controller),
SettingsTab.experimental => buildExperimentalInternal(
context,
controller,
settings,
uiFeatures,
),
SettingsTab.about => buildAboutInternal(context, controller),
};
}
List<Widget> buildDetailContentInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
UiFeatureAccess uiFeatures,
SettingsDetailPage detail,
) {
return switch (detail) {
SettingsDetailPage.gatewayConnection => <Widget>[
buildDetailIntroInternal(
context,
title: detail.label,
description: appText(
'集中编辑 Gateway 连接、设备配对和会话级连接入口。',
'Edit gateway connection, device pairing, and session-level connection entry points in one place.',
),
),
const SizedBox(height: 16),
buildOpenClawGatewayCardInternal(context, controller, settings),
if (uiFeatures.supportsVaultServer) ...[
const SizedBox(height: 16),
buildVaultProviderCardInternal(context, controller, settings),
],
const SizedBox(height: 16),
buildLlmEndpointManagerInternal(context, controller, settings),
],
SettingsDetailPage.aiGatewayIntegration => <Widget>[
buildDetailIntroInternal(
context,
title: detail.label,
description: appText(
'把主 LLM API 与可选兼容端点统一收口成接入点列表。默认先显示主接入点,需要时可通过 + 扩展更多端点。',
'Manage the primary LLM API and optional compatible endpoints from one endpoint list. Start with the primary entry and expand more endpoints with + when needed.',
),
),
const SizedBox(height: 16),
buildLlmEndpointManagerInternal(context, controller, settings),
],
SettingsDetailPage.vaultProvider => <Widget>[
buildDetailIntroInternal(
context,
title: detail.label,
description: appText(
'只在这里维护 Vault 地址、命名空间和安全 token 引用。',
'Maintain Vault endpoint, namespace, and secure token references here.',
),
),
const SizedBox(height: 16),
if (uiFeatures.supportsVaultServer)
buildVaultProviderCardInternal(context, controller, settings)
else
SurfaceCard(
child: Text(
appText(
'当前发布配置未开放 Vault Server 参数。',
'Vault Server settings are disabled in this release configuration.',
),
),
),
],
SettingsDetailPage.externalAgents => <Widget>[
buildDetailIntroInternal(
context,
title: detail.label,
description: appText(
'多 Agent 协作、角色编排和外部 Agent / ACP 连接的详细参数集中在这里。',
'Detailed multi-agent collaboration, role orchestration, and external Agent / ACP connection settings are edited here.',
),
),
const SizedBox(height: 16),
...buildAgentsInternal(context, controller, settings),
const SizedBox(height: 16),
CodexIntegrationCard(controller: controller),
],
SettingsDetailPage.diagnosticsAdvanced => <Widget>[
buildDetailIntroInternal(
context,
title: detail.label,
description: appText(
'高级诊断集中展示网关诊断、运行日志和设备信息。',
'Advanced diagnostics centralize gateway diagnostics, runtime logs, and device information.',
),
),
const SizedBox(height: 16),
...buildDiagnosticsInternal(context, controller),
],
};
}
Widget buildDetailIntroInternal(
BuildContext context, {
required String title,
required String description,
}) {
return SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 10),
Text(description, style: Theme.of(context).textTheme.bodyMedium),
],
),
);
}
Widget buildGlobalApplyBarInternal(
BuildContext context,
AppController controller,
) {
final theme = Theme.of(context);
final hasDraft = controller.hasSettingsDraftChanges;
final hasPendingApply = controller.hasPendingSettingsApply;
final message = controller.settingsDraftStatusMessage;
return SurfaceCard(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('设置提交流程', 'Settings Submission'),
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 6),
Text(
message.isNotEmpty
? message
: hasDraft
? appText(
'当前存在未保存草稿。保存:仅保存配置,不立即生效。',
'There are unsaved drafts. Save persists configuration only and does not apply it immediately.',
)
: hasPendingApply
? appText(
'当前存在已保存但未应用的更改。应用:立即按当前配置生效。',
'There are saved changes waiting to be applied. Apply makes the current configuration take effect immediately.',
)
: (message.isEmpty
? appText(
'当前没有待提交更改。',
'There are no pending settings changes.',
)
: message),
style: theme.textTheme.bodyMedium,
),
],
),
),
const SizedBox(width: 16),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
OutlinedButton(
key: const ValueKey('settings-global-save-button'),
onPressed: hasDraft
? () => handleTopLevelSaveInternal(controller)
: null,
child: Text(appText('保存', 'Save')),
),
FilledButton.tonal(
key: const ValueKey('settings-global-apply-button'),
onPressed: (!hasDraft && !hasPendingApply)
? null
: () => handleTopLevelApplyInternal(controller),
child: Text(appText('应用', 'Apply')),
),
],
),
],
),
);
}
List<Widget> buildGeneralInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
UiFeatureAccess uiFeatures,
) {
return [
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Application', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 16),
SwitchRowInternal(
label: appText('启用工作台外壳', 'Active workspace shell'),
value: settings.appActive,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(appActive: value),
),
),
SwitchRowInternal(
label: appText('开机启动', 'Launch at login'),
value: settings.launchAtLogin,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(launchAtLogin: value),
),
),
SwitchRowInternal(
label: controller.supportsDesktopIntegration
? appText('显示托盘图标', 'Show tray icon')
: appText('显示 Dock 图标', 'Show dock icon'),
value: settings.showDockIcon,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(showDockIcon: value),
),
),
if (uiFeatures.supportsAccountAccess)
SwitchRowInternal(
label: appText('账号本地模式', 'Account local mode'),
value: settings.accountLocalMode,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(accountLocalMode: value),
),
),
],
),
),
if (controller.supportsDesktopIntegration)
buildLinuxDesktopIntegrationInternal(context, controller, settings),
if (uiFeatures.supportsAccountAccess)
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('账号访问', 'Account Access'),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
EditableFieldInternal(
label: appText('账号服务地址', 'Account Base URL'),
value: settings.accountBaseUrl,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(accountBaseUrl: value),
),
),
EditableFieldInternal(
label: appText('账号用户名', 'Account Username'),
value: settings.accountUsername,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(accountUsername: value),
),
),
EditableFieldInternal(
label: appText('工作区名称', 'Workspace Label'),
value: settings.accountWorkspace,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(accountWorkspace: value),
),
),
],
),
),
];
}
Widget buildLinuxDesktopIntegrationInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
) {
final desktop = controller.desktopIntegration;
final config = settings.linuxDesktop;
final theme = Theme.of(context);
return SurfaceCard(
key: const ValueKey('linux-desktop-integration-card'),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('Linux 桌面集成', 'Linux Desktop Integration'),
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
appText(
'统一管理 GNOME / KDE 的代理模式、隧道连接、托盘菜单与开机自启。',
'Manage GNOME / KDE proxy mode, tunnel session, tray menu, and autostart from one surface.',
),
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
InfoRowInternal(
label: appText('桌面环境', 'Desktop'),
value: desktop.environment.label,
),
InfoRowInternal(
label: 'NetworkManager',
value: desktop.networkManagerAvailable
? appText('可用', 'Available')
: appText('不可用', 'Unavailable'),
),
InfoRowInternal(
label: appText('当前模式', 'Current Mode'),
value: desktop.mode.label,
),
InfoRowInternal(
label: appText('隧道状态', 'Tunnel'),
value: desktop.tunnel.connected
? appText('已连接', 'Connected')
: desktop.tunnel.available
? appText('可连接', 'Ready')
: appText('未检测到配置', 'No profile detected'),
),
InfoRowInternal(
label: appText('系统代理', 'System Proxy'),
value: desktop.systemProxy.enabled
? '${desktop.systemProxy.host}:${desktop.systemProxy.port}'
: appText('未启用', 'Disabled'),
),
SwitchRowInternal(
label: appText('开机启动', 'Launch at login'),
value: settings.launchAtLogin,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(launchAtLogin: value),
),
),
SwitchRowInternal(
label: appText('托盘菜单', 'Tray menu'),
value: config.trayEnabled,
onChanged: (value) => saveSettingsInternal(
controller,
settings.copyWith(
linuxDesktop: config.copyWith(trayEnabled: value),
),
),
),
EditableFieldInternal(
label: appText('隧道连接名称', 'Tunnel Connection Name'),
value: config.vpnConnectionName,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(
linuxDesktop: config.copyWith(vpnConnectionName: value.trim()),
),
),
),
Row(
children: [
Expanded(
child: EditableFieldInternal(
label: appText('代理主机', 'Proxy Host'),
value: config.proxyHost,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(
linuxDesktop: config.copyWith(proxyHost: value.trim()),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: EditableFieldInternal(
label: appText('代理端口', 'Proxy Port'),
value: config.proxyPort.toString(),
onSubmitted: (value) {
final parsed = int.tryParse(value.trim());
if (parsed == null || parsed <= 0) {
return;
}
saveSettingsInternal(
controller,
settings.copyWith(
linuxDesktop: config.copyWith(proxyPort: parsed),
),
);
},
),
),
],
),
const SizedBox(height: 6),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
FilledButton.tonal(
onPressed: controller.desktopPlatformBusy
? null
: () => controller.setDesktopVpnMode(VpnMode.proxy),
child: Text(appText('切换到代理', 'Use Proxy')),
),
FilledButton.tonal(
onPressed: controller.desktopPlatformBusy
? null
: () => controller.setDesktopVpnMode(VpnMode.tunnel),
child: Text(appText('切换到隧道', 'Use Tunnel')),
),
OutlinedButton(
onPressed: controller.desktopPlatformBusy
? null
: controller.connectDesktopTunnel,
child: Text(appText('连接隧道', 'Connect Tunnel')),
),
OutlinedButton(
onPressed: controller.desktopPlatformBusy
? null
: controller.disconnectDesktopTunnel,
child: Text(appText('断开隧道', 'Disconnect Tunnel')),
),
OutlinedButton(
onPressed: controller.desktopPlatformBusy
? null
: controller.refreshDesktopIntegration,
child: Text(appText('刷新状态', 'Refresh Status')),
),
],
),
if (desktop.statusMessage.trim().isNotEmpty) ...[
const SizedBox(height: 16),
buildNoticeInternal(
context,
tone: theme.colorScheme.surfaceContainerHighest,
title: appText('桌面状态', 'Desktop Status'),
message: desktop.statusMessage,
),
],
],
),
);
}
List<Widget> buildWorkspaceInternal(
BuildContext context,
AppController controller,
SettingsSnapshot settings,
) {
return [
SurfaceCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appText('工作区', 'Workspace'),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
EditableFieldInternal(
label: appText('工作区路径', 'Workspace Path'),
value: settings.workspacePath,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(workspacePath: value),
),
),
EditableFieldInternal(
label: appText('远程项目根目录', 'Remote Project Root'),
value: settings.remoteProjectRoot,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(remoteProjectRoot: value),
),
),
EditableFieldInternal(
label: appText('CLI 路径', 'CLI Path'),
value: settings.cliPath,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(cliPath: value),
),
),
EditableFieldInternal(
label: appText('默认模型', 'Default Model'),
value: settings.defaultModel,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(defaultModel: value),
),
),
EditableFieldInternal(
label: appText('默认提供方', 'Default Provider'),
value: settings.defaultProvider,
onSubmitted: (value) => saveSettingsInternal(
controller,
settings.copyWith(defaultProvider: value),
),
),
],
),
),
];
}
}