refactor(web): align shell and focused entry chrome
This commit is contained in:
parent
c67db9472b
commit
37ee2729e0
@ -96,6 +96,12 @@ class AppController extends ChangeNotifier {
|
||||
bool get hasPendingSettingsApply => _pendingSettingsApply;
|
||||
String get settingsDraftStatusMessage => _settingsDraftStatusMessage;
|
||||
AppLanguage get appLanguage => _settings.appLanguage;
|
||||
AssistantPermissionLevel get assistantPermissionLevel =>
|
||||
_settings.assistantPermissionLevel;
|
||||
List<WorkspaceDestination> get assistantNavigationDestinations =>
|
||||
_settings.assistantNavigationDestinations
|
||||
.where(capabilities.supportsDestination)
|
||||
.toList(growable: false);
|
||||
GatewayConnectionSnapshot get connection => _relayClient.snapshot;
|
||||
bool get relayBusy => _relayBusy;
|
||||
bool get aiGatewayBusy => _aiGatewayBusy;
|
||||
@ -643,6 +649,27 @@ class AppController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> toggleAssistantNavigationDestination(
|
||||
WorkspaceDestination destination,
|
||||
) async {
|
||||
if (!kAssistantNavigationDestinationCandidates.contains(destination) ||
|
||||
!capabilities.supportsDestination(destination)) {
|
||||
return;
|
||||
}
|
||||
final current = assistantNavigationDestinations;
|
||||
final next = current.contains(destination)
|
||||
? current.where((item) => item != destination).toList(growable: false)
|
||||
: <WorkspaceDestination>[...current, destination];
|
||||
_settings = _settings.copyWith(assistantNavigationDestinations: next);
|
||||
if (_settingsDraftInitialized) {
|
||||
_settingsDraft = settingsDraft.copyWith(
|
||||
assistantNavigationDestinations: next,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
await _persistSettings();
|
||||
}
|
||||
|
||||
Future<void> setThemeMode(ThemeMode mode) async {
|
||||
if (_themeMode == mode) {
|
||||
return;
|
||||
@ -1829,9 +1856,15 @@ class AppController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
SettingsSnapshot _sanitizeSettings(SettingsSnapshot snapshot) {
|
||||
final allowedDestinations = featuresFor(UiFeaturePlatform.web)
|
||||
.allowedDestinations;
|
||||
final target = featuresFor(UiFeaturePlatform.web).sanitizeExecutionTarget(
|
||||
_sanitizeTarget(snapshot.assistantExecutionTarget),
|
||||
);
|
||||
final assistantNavigationDestinations =
|
||||
normalizeAssistantNavigationDestinations(
|
||||
snapshot.assistantNavigationDestinations,
|
||||
).where(allowedDestinations.contains).toList(growable: false);
|
||||
final normalizedSessionBaseUrl =
|
||||
RemoteWebSessionRepository.normalizeBaseUrl(
|
||||
snapshot.webSessionPersistence.remoteBaseUrl,
|
||||
@ -1862,7 +1895,7 @@ class AppController extends ChangeNotifier {
|
||||
webSessionPersistence: snapshot.webSessionPersistence.copyWith(
|
||||
remoteBaseUrl: normalizedSessionBaseUrl,
|
||||
),
|
||||
assistantNavigationDestinations: const <WorkspaceDestination>[],
|
||||
assistantNavigationDestinations: assistantNavigationDestinations,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
import '../web/web_assistant_page.dart';
|
||||
import '../web/web_settings_page.dart';
|
||||
import '../widgets/app_brand_logo.dart';
|
||||
import 'app_controller_web.dart';
|
||||
|
||||
class AppShell extends StatelessWidget {
|
||||
@ -75,87 +72,9 @@ class AppShell extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final palette = context.palette;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 76,
|
||||
margin: const EdgeInsets.fromLTRB(4, 4, 0, 4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
palette.chromeHighlight.withValues(alpha: 0.94),
|
||||
palette.chromeSurface.withValues(alpha: 0.92),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: palette.chromeStroke),
|
||||
boxShadow: [palette.chromeShadowAmbient],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 12, 10, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: const Center(
|
||||
child: AppBrandLogo(size: 28, borderRadius: 8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
...availableDestinations.map(
|
||||
(destination) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _WebNavRailButton(
|
||||
key: Key('web-shell-nav-${destination.name}'),
|
||||
destination: destination,
|
||||
selected: currentDestination == destination,
|
||||
onTap: () =>
|
||||
controller.navigateTo(destination),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_WebUtilityButton(
|
||||
key: const Key('web-shell-language-toggle'),
|
||||
tooltip: controller.appLanguage == AppLanguage.zh
|
||||
? '中文'
|
||||
: 'English',
|
||||
icon: Icons.translate_rounded,
|
||||
onTap: controller.toggleAppLanguage,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_WebUtilityButton(
|
||||
key: const Key('web-shell-theme-toggle'),
|
||||
tooltip: _themeLabel(controller.themeMode),
|
||||
icon: controller.themeMode == ThemeMode.dark
|
||||
? Icons.dark_mode_rounded
|
||||
: Icons.light_mode_rounded,
|
||||
onTap: () => controller.setThemeMode(
|
||||
controller.themeMode == ThemeMode.dark
|
||||
? ThemeMode.light
|
||||
: ThemeMode.dark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPage(
|
||||
controller,
|
||||
destination: currentDestination,
|
||||
),
|
||||
),
|
||||
],
|
||||
return _buildPage(
|
||||
controller,
|
||||
destination: currentDestination,
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -175,92 +94,3 @@ class AppShell extends StatelessWidget {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _WebNavRailButton extends StatelessWidget {
|
||||
const _WebNavRailButton({
|
||||
super.key,
|
||||
required this.destination,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final WorkspaceDestination destination;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
return Tooltip(
|
||||
message: destination.label,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? palette.accentMuted : palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? palette.accent.withValues(alpha: 0.28)
|
||||
: palette.strokeSoft,
|
||||
),
|
||||
boxShadow: selected ? [palette.chromeShadowLift] : null,
|
||||
),
|
||||
child: Icon(
|
||||
destination.icon,
|
||||
size: 22,
|
||||
color: selected ? palette.accent : palette.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WebUtilityButton extends StatelessWidget {
|
||||
const _WebUtilityButton({
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 52,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: Icon(icon, size: 20, color: palette.textSecondary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _themeLabel(ThemeMode mode) {
|
||||
return switch (mode) {
|
||||
ThemeMode.dark => appText('深色', 'Dark'),
|
||||
ThemeMode.system => appText('跟随系统', 'System'),
|
||||
ThemeMode.light => appText('浅色', 'Light'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -807,109 +807,359 @@ class _AssistantQuickPane extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final favorites = controller.assistantNavigationDestinations
|
||||
.where((item) => item == WorkspaceDestination.settings)
|
||||
.toList(growable: false);
|
||||
final canAddSettings =
|
||||
controller.capabilities.supportsDestination(
|
||||
WorkspaceDestination.settings,
|
||||
) &&
|
||||
!favorites.contains(WorkspaceDestination.settings);
|
||||
final palette = context.palette;
|
||||
|
||||
return SurfaceCard(
|
||||
borderRadius: 10,
|
||||
tone: SurfaceCardTone.chrome,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 10, 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('关注入口', 'Focused navigation'),
|
||||
key: const Key('assistant-focus-panel-title'),
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
appText(
|
||||
'添加后的入口会直接出现在最左侧侧板。这里负责管理关注项和查看摘要,需要完整页面时再单独打开。',
|
||||
'Added entries appear directly in the far-left rail. Manage focused destinations and review summaries here, then open the full page only when needed.',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: palette.textSecondary,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (canAddSettings)
|
||||
Tooltip(
|
||||
message: appText('添加关注入口', 'Add focused destination'),
|
||||
child: InkWell(
|
||||
key: const Key('assistant-focus-add-menu'),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
controller.toggleAssistantNavigationDestination(
|
||||
WorkspaceDestination.settings,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 38,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
palette.chromeHighlight.withValues(alpha: 0.94),
|
||||
palette.chromeSurfacePressed,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: palette.chromeStroke),
|
||||
boxShadow: [palette.chromeShadowLift],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add_rounded,
|
||||
size: 18,
|
||||
color: palette.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: palette.strokeSoft),
|
||||
Expanded(
|
||||
child: favorites.isEmpty
|
||||
? _FocusedNavigationEmptyState(
|
||||
canAddSettings: canAddSettings,
|
||||
onAddSettings: () {
|
||||
controller.toggleAssistantNavigationDestination(
|
||||
WorkspaceDestination.settings,
|
||||
);
|
||||
},
|
||||
)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
|
||||
children: [
|
||||
_FocusedSettingsCard(
|
||||
controller: controller,
|
||||
onOpenPage: () =>
|
||||
controller.openSettings(tab: SettingsTab.general),
|
||||
onRemoveFavorite: () {
|
||||
controller.toggleAssistantNavigationDestination(
|
||||
WorkspaceDestination.settings,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FocusedNavigationEmptyState extends StatelessWidget {
|
||||
const _FocusedNavigationEmptyState({
|
||||
required this.canAddSettings,
|
||||
required this.onAddSettings,
|
||||
});
|
||||
|
||||
final bool canAddSettings;
|
||||
final VoidCallback onAddSettings;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: Text(
|
||||
appText(
|
||||
'还没有关注入口。给功能菜单点星标,或从右上角添加一个入口,加入最左侧侧板。',
|
||||
'No focused entries yet. Star a destination or add one from the top-right menu to place it in the far-left rail.',
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: palette.textSecondary,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (canAddSettings) ...[
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ActionChip(
|
||||
key: const ValueKey<String>('assistant-focus-add-settings'),
|
||||
avatar: const Icon(Icons.tune_rounded, size: 16),
|
||||
label: Text(WorkspaceDestination.settings.label),
|
||||
onPressed: onAddSettings,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FocusedSettingsCard extends StatelessWidget {
|
||||
const _FocusedSettingsCard({
|
||||
required this.controller,
|
||||
required this.onOpenPage,
|
||||
required this.onRemoveFavorite,
|
||||
});
|
||||
|
||||
final AppController controller;
|
||||
final VoidCallback onOpenPage;
|
||||
final VoidCallback onRemoveFavorite;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final theme = Theme.of(context);
|
||||
final languageLabel = controller.appLanguage == AppLanguage.zh
|
||||
? appText('中文', 'Chinese')
|
||||
: 'English';
|
||||
final themeLabel = switch (controller.themeMode) {
|
||||
ThemeMode.dark => appText('深色', 'Dark'),
|
||||
ThemeMode.light => appText('浅色', 'Light'),
|
||||
ThemeMode.system => appText('跟随系统', 'System'),
|
||||
};
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 10, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
WorkspaceDestination.settings.icon,
|
||||
size: 18,
|
||||
color: palette.accent,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
WorkspaceDestination.settings.label,
|
||||
key: const ValueKey<String>(
|
||||
'assistant-focus-active-title-settings',
|
||||
),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
WorkspaceDestination.settings.description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: palette.textSecondary,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey<String>('assistant-focus-open-page-settings'),
|
||||
tooltip: appText('打开全页', 'Open full page'),
|
||||
onPressed: onOpenPage,
|
||||
icon: const Icon(Icons.open_in_new_rounded, size: 18),
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey<String>('assistant-focus-remove-settings'),
|
||||
tooltip: appText('取消关注', 'Remove from focused panel'),
|
||||
onPressed: onRemoveFavorite,
|
||||
icon: Icon(Icons.star_rounded, color: palette.accent),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: palette.strokeSoft),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||
child: Column(
|
||||
children: [
|
||||
_FocusedPreviewTile(
|
||||
title: appText('语言', 'Language'),
|
||||
subtitle: appText('当前界面语言', 'Current interface language'),
|
||||
trailing: languageLabel,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_FocusedPreviewTile(
|
||||
title: appText('主题', 'Theme'),
|
||||
subtitle: appText('当前显示模式', 'Current display mode'),
|
||||
trailing: themeLabel,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_FocusedPreviewTile(
|
||||
title: appText('执行目标', 'Execution target'),
|
||||
subtitle: appText(
|
||||
'Assistant 默认运行位置',
|
||||
'Default assistant execution target',
|
||||
),
|
||||
trailing: controller.assistantExecutionTarget.label,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_FocusedPreviewTile(
|
||||
title: appText('权限', 'Permissions'),
|
||||
subtitle: appText(
|
||||
'Assistant 默认权限级别',
|
||||
'Default assistant permission level',
|
||||
),
|
||||
trailing: controller.assistantPermissionLevel.label,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FocusedPreviewTile extends StatelessWidget {
|
||||
const _FocusedPreviewTile({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.trailing,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('关注入口', 'Focused navigation'),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
appText(
|
||||
'这里放首页常用设置与运行态摘要;需要完整配置时再进入 Settings。',
|
||||
'Use this rail for homepage settings and runtime summaries, then open full Settings when needed.',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: context.palette.textSecondary,
|
||||
subtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: palette.textSecondary,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_QuickInfoCard(
|
||||
icon: WorkspaceDestination.settings.icon,
|
||||
title: WorkspaceDestination.settings.label,
|
||||
description: WorkspaceDestination.settings.description,
|
||||
trailing: IconButton(
|
||||
tooltip: appText('打开全页', 'Open full page'),
|
||||
onPressed: () =>
|
||||
controller.openSettings(tab: SettingsTab.general),
|
||||
icon: const Icon(Icons.open_in_new_rounded),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_QuickValueCard(
|
||||
title: appText('语言', 'Language'),
|
||||
subtitle: appText('当前界面语言', 'Current interface language'),
|
||||
value: controller.appLanguage == AppLanguage.zh ? '中文' : 'English',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_QuickValueCard(
|
||||
title: appText('主题', 'Theme'),
|
||||
subtitle: appText('当前显示模式', 'Current display mode'),
|
||||
value: switch (controller.themeMode) {
|
||||
ThemeMode.dark => appText('深色', 'Dark'),
|
||||
ThemeMode.system => appText('跟随系统', 'System'),
|
||||
ThemeMode.light => appText('浅色', 'Light'),
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_QuickValueCard(
|
||||
title: appText('执行目标', 'Execution target'),
|
||||
subtitle: appText('Assistant 默认运行位置', 'Assistant default target'),
|
||||
value: _targetLabel(controller.assistantExecutionTarget),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: context.palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: context.palette.strokeSoft),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('权限', 'Permissions'),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
appText(
|
||||
'Assistant 默认权限级别',
|
||||
'Assistant default permission level',
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: context.palette.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: context.palette.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: context.palette.strokeSoft),
|
||||
),
|
||||
child: _CompactDropdown<AssistantPermissionLevel>(
|
||||
value: permissionLevel,
|
||||
items: AssistantPermissionLevel.values,
|
||||
labelBuilder: (item) => item.label,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
onPermissionChanged(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
trailing,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: palette.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -1709,118 +1959,6 @@ class _MetaChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickInfoCard extends StatelessWidget {
|
||||
const _QuickInfoCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.trailing = const SizedBox.shrink(),
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String description;
|
||||
final Widget trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfaceSecondary,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(icon, size: 18, color: palette.accent),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: palette.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickValueCard extends StatelessWidget {
|
||||
const _QuickValueCard({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: palette.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CompactDropdown<T> extends StatelessWidget {
|
||||
const _CompactDropdown({
|
||||
|
||||
@ -21,14 +21,60 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('助手'), findsWidgets);
|
||||
expect(find.byKey(const Key('web-shell-nav-assistant')), findsOneWidget);
|
||||
expect(find.byKey(const Key('web-shell-nav-settings')), findsOneWidget);
|
||||
expect(find.byKey(const Key('web-shell-nav-assistant')), findsNothing);
|
||||
expect(find.byKey(const Key('web-shell-nav-settings')), findsNothing);
|
||||
expect(find.byKey(const Key('web-shell-language-toggle')), findsNothing);
|
||||
expect(find.byKey(const Key('web-shell-theme-toggle')), findsNothing);
|
||||
expect(find.text('Tasks'), findsNothing);
|
||||
expect(find.byKey(const Key('assistant-task-rail')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-attachment-menu-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(const Key('assistant-focus-panel-title')), findsNothing);
|
||||
|
||||
await tester.tap(find.byKey(const Key('assistant-side-pane-tab-quick')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('assistant-focus-panel-title')), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-add-settings')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-add-tasks')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-add-skills')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-add-nodes')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-add-secrets')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-add-aiGateway')),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-add-settings')),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-open-page-settings')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-focus-remove-settings')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('连接设置'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user