feat(ui): add settings quick actions and home shortcut

This commit is contained in:
Haitao Pan 2026-03-25 10:06:42 +08:00
parent 9000d78ed5
commit 1ef8d6322d
10 changed files with 440 additions and 216 deletions

View File

@ -228,6 +228,7 @@ class _AppShellState extends State<AppShell> {
onCycleSidebarState: controller.cycleSidebarState,
onExpandFromCollapsed: () => controller
.setSidebarState(AppSidebarState.expanded),
onOpenHome: controller.navigateHome,
onOpenAccount: () => controller.navigateTo(
WorkspaceDestination.account,
),

View File

@ -60,17 +60,18 @@ class _AppShellState extends State<AppShell> {
animation: widget.controller,
builder: (context, _) {
final controller = widget.controller;
final availableDestinations = <WorkspaceDestination>[
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.skills,
WorkspaceDestination.nodes,
WorkspaceDestination.secrets,
WorkspaceDestination.aiGateway,
WorkspaceDestination.settings,
].where(controller.capabilities.supportsDestination).toList(
growable: false,
);
final availableDestinations =
<WorkspaceDestination>[
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.skills,
WorkspaceDestination.nodes,
WorkspaceDestination.secrets,
WorkspaceDestination.aiGateway,
WorkspaceDestination.settings,
]
.where(controller.capabilities.supportsDestination)
.toList(growable: false);
final currentDestination =
availableDestinations.contains(controller.destination)
? controller.destination
@ -95,17 +96,17 @@ class _AppShellState extends State<AppShell> {
);
if (isMobile) {
final mobileDestinations = <WorkspaceDestination>[
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.skills,
WorkspaceDestination.settings,
].where(controller.capabilities.supportsDestination).toList(
growable: false,
);
final selectedIndex = mobileDestinations.contains(
currentDestination,
)
final mobileDestinations =
<WorkspaceDestination>[
WorkspaceDestination.assistant,
WorkspaceDestination.tasks,
WorkspaceDestination.skills,
WorkspaceDestination.settings,
]
.where(controller.capabilities.supportsDestination)
.toList(growable: false);
final selectedIndex =
mobileDestinations.contains(currentDestination)
? mobileDestinations.indexOf(currentDestination)
: 0;
return Column(
@ -159,18 +160,21 @@ class _AppShellState extends State<AppShell> {
_sidebarState = AppSidebarState.expanded;
});
},
onOpenHome: controller.navigateHome,
onOpenAccount: () {},
onOpenThemeToggle: () => controller.setThemeMode(
controller.themeMode == ThemeMode.dark
? ThemeMode.light
: ThemeMode.dark,
),
accountName: controller.settings.accountUsername
accountName:
controller.settings.accountUsername
.trim()
.isNotEmpty
? controller.settings.accountUsername
: appText('Web 操作员', 'Web operator'),
accountSubtitle: controller.settings.accountWorkspace
accountSubtitle:
controller.settings.accountWorkspace
.trim()
.isNotEmpty
? controller.settings.accountWorkspace
@ -184,7 +188,8 @@ class _AppShellState extends State<AppShell> {
.toSet(),
onToggleFavorite:
controller.toggleAssistantNavigationDestination,
availableDestinations: controller.capabilities.allowedDestinations,
availableDestinations:
controller.capabilities.allowedDestinations,
),
if (showWorkspaceSidebar &&
_sidebarState == AppSidebarState.expanded)
@ -226,7 +231,9 @@ class _AppShellState extends State<AppShell> {
WorkspaceDestination.skills => WebSkillsPage(controller: controller),
WorkspaceDestination.nodes => WebNodesPage(controller: controller),
WorkspaceDestination.secrets => WebSecretsPage(controller: controller),
WorkspaceDestination.aiGateway => WebAiGatewayPage(controller: controller),
WorkspaceDestination.aiGateway => WebAiGatewayPage(
controller: controller,
),
WorkspaceDestination.settings => WebSettingsPage(controller: controller),
_ => WebAssistantPage(controller: controller),
};
@ -248,10 +255,7 @@ class _WebShellBody extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeBackground,
palette.canvas,
],
colors: [palette.chromeBackground, palette.canvas],
stops: const [0.0, 0.68],
),
),

View File

@ -5,6 +5,7 @@ import '../i18n/app_language.dart';
import '../models/app_models.dart';
import '../runtime/runtime_models.dart';
import '../theme/app_palette.dart';
import '../widgets/settings_focus_quick_actions.dart';
import '../widgets/surface_card.dart';
class WebAssistantFocusPanel extends StatefulWidget {
@ -423,8 +424,7 @@ class _SkillsFocusPreview extends StatelessWidget {
: controller.skills.take(4).toList(growable: false);
if (items.isEmpty) {
return _PreviewEmptyState(
message:
controller.isSingleAgentMode
message: controller.isSingleAgentMode
? (controller.currentSingleAgentNeedsAiGatewayConfiguration
? appText(
'当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。',
@ -583,10 +583,7 @@ class _ClawHubFocusPreview extends StatelessWidget {
runSpacing: 8,
children: [
_FocusPill(
label: appText(
'已加载技能 $skillCount',
'Loaded skills $skillCount',
),
label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'),
),
_FocusPill(
label: appText(
@ -706,7 +703,25 @@ class _SettingsFocusPreview extends StatelessWidget {
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsFocusQuickActions(
appLanguage: controller.appLanguage,
themeMode: controller.themeMode,
onToggleLanguage: controller.toggleAppLanguage,
onToggleTheme: () {
controller.setThemeMode(
controller.themeMode == ThemeMode.dark
? ThemeMode.light
: ThemeMode.dark,
);
},
languageButtonKey: const Key(
'assistant-focus-settings-language-toggle',
),
themeButtonKey: const Key('assistant-focus-settings-theme-toggle'),
),
const SizedBox(height: 12),
_FocusListTile(
title: appText('语言', 'Language'),
subtitle: appText('当前界面语言', 'Current interface language'),

View File

@ -5,6 +5,7 @@ import '../i18n/app_language.dart';
import '../models/app_models.dart';
import '../runtime/runtime_models.dart';
import '../theme/app_palette.dart';
import 'settings_focus_quick_actions.dart';
import 'surface_card.dart';
class AssistantFocusPanel extends StatefulWidget {
@ -423,8 +424,7 @@ class _SkillsFocusPreview extends StatelessWidget {
: controller.skills.take(4).toList(growable: false);
if (items.isEmpty) {
return _PreviewEmptyState(
message:
controller.isSingleAgentMode
message: controller.isSingleAgentMode
? (controller.currentSingleAgentNeedsAiGatewayConfiguration
? appText(
'当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。',
@ -583,10 +583,7 @@ class _ClawHubFocusPreview extends StatelessWidget {
runSpacing: 8,
children: [
_FocusPill(
label: appText(
'已加载技能 $skillCount',
'Loaded skills $skillCount',
),
label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'),
),
_FocusPill(
label: appText(
@ -706,7 +703,25 @@ class _SettingsFocusPreview extends StatelessWidget {
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsFocusQuickActions(
appLanguage: controller.appLanguage,
themeMode: controller.themeMode,
onToggleLanguage: controller.toggleAppLanguage,
onToggleTheme: () {
controller.setThemeMode(
controller.themeMode == ThemeMode.dark
? ThemeMode.light
: ThemeMode.dark,
);
},
languageButtonKey: const Key(
'assistant-focus-settings-language-toggle',
),
themeButtonKey: const Key('assistant-focus-settings-theme-toggle'),
),
const SizedBox(height: 12),
_FocusListTile(
title: appText('语言', 'Language'),
subtitle: appText('当前界面语言', 'Current interface language'),

View File

@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import '../i18n/app_language.dart';
import '../theme/app_palette.dart';
import '../theme/app_theme.dart';
IconData chromeThemeToggleIcon(ThemeMode themeMode) {
return switch (themeMode) {
ThemeMode.dark => Icons.dark_mode_rounded,
ThemeMode.light => Icons.light_mode_rounded,
ThemeMode.system => Icons.brightness_auto_rounded,
};
}
String chromeThemeToggleTooltip(ThemeMode themeMode) {
return themeMode == ThemeMode.dark
? appText('切换浅色', 'Switch to light')
: appText('切换深色', 'Switch to dark');
}
class ChromeIconActionButton extends StatefulWidget {
const ChromeIconActionButton({
super.key,
required this.icon,
this.tooltip,
required this.onPressed,
});
final IconData icon;
final String? tooltip;
final VoidCallback onPressed;
@override
State<ChromeIconActionButton> createState() => _ChromeIconActionButtonState();
}
class _ChromeIconActionButtonState extends State<ChromeIconActionButton> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final resolvedBackground = _hovered
? palette.chromeSurfacePressed
: palette.chromeSurface;
return Tooltip(
message: widget.tooltip ?? '',
child: MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: _hovered ? 0.94 : 0.88,
),
resolvedBackground,
],
),
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered ? palette.chromeShadowLift : palette.chromeShadowAmbient,
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.button),
onTap: widget.onPressed,
child: Container(
height: AppSizes.sidebarItemHeight,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
child: Center(
child: Icon(
widget.icon,
size: AppSizes.sidebarIconSize,
color: palette.textSecondary,
),
),
),
),
),
),
),
);
}
}
class ChromeLanguageActionButton extends StatefulWidget {
const ChromeLanguageActionButton({
super.key,
required this.appLanguage,
required this.compact,
required this.tooltip,
required this.onPressed,
});
final AppLanguage appLanguage;
final bool compact;
final String tooltip;
final VoidCallback onPressed;
@override
State<ChromeLanguageActionButton> createState() =>
_ChromeLanguageActionButtonState();
}
class _ChromeLanguageActionButtonState
extends State<ChromeLanguageActionButton> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final size = widget.compact ? AppSizes.sidebarItemHeight : 44.0;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Tooltip(
message: widget.tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.button),
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: _hovered ? 0.94 : 0.88,
),
_hovered
? palette.chromeSurfacePressed
: palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered
? palette.chromeShadowLift
: palette.chromeShadowAmbient,
],
),
child: Text(
widget.appLanguage.compactLabel,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: palette.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import '../i18n/app_language.dart';
import '../theme/app_theme.dart';
import 'chrome_quick_action_buttons.dart';
class SettingsFocusQuickActions extends StatelessWidget {
const SettingsFocusQuickActions({
super.key,
required this.appLanguage,
required this.themeMode,
required this.onToggleLanguage,
required this.onToggleTheme,
this.languageButtonKey,
this.themeButtonKey,
});
final AppLanguage appLanguage;
final ThemeMode themeMode;
final VoidCallback onToggleLanguage;
final VoidCallback onToggleTheme;
final Key? languageButtonKey;
final Key? themeButtonKey;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: ChromeLanguageActionButton(
key: languageButtonKey,
appLanguage: appLanguage,
compact: false,
tooltip: appText('切换语言', 'Toggle language'),
onPressed: onToggleLanguage,
),
),
const SizedBox(width: AppSpacing.xs),
ChromeIconActionButton(
key: themeButtonKey,
icon: chromeThemeToggleIcon(themeMode),
tooltip: chromeThemeToggleTooltip(themeMode),
onPressed: onToggleTheme,
),
],
);
}
}

View File

@ -5,6 +5,7 @@ import '../i18n/app_language.dart';
import '../models/app_models.dart';
import '../theme/app_palette.dart';
import '../theme/app_theme.dart';
import 'chrome_quick_action_buttons.dart';
class SidebarNavigation extends StatelessWidget {
const SidebarNavigation({
@ -19,6 +20,7 @@ class SidebarNavigation extends StatelessWidget {
required this.onExpandFromCollapsed,
required this.onOpenAccount,
required this.onOpenThemeToggle,
this.onOpenHome,
required this.accountName,
required this.accountSubtitle,
this.onOpenOnlineWorkspace,
@ -40,6 +42,7 @@ class SidebarNavigation extends StatelessWidget {
final VoidCallback onExpandFromCollapsed;
final VoidCallback onOpenAccount;
final VoidCallback onOpenThemeToggle;
final VoidCallback? onOpenHome;
final String accountName;
final String accountSubtitle;
final VoidCallback? onOpenOnlineWorkspace;
@ -129,6 +132,7 @@ class SidebarNavigation extends StatelessWidget {
emphasis: _SidebarItemEmphasis.primary,
favoriteDestinations: favoriteDestinations,
onToggleFavorite: onToggleFavorite,
onOpenHome: onOpenHome,
onSectionChanged: onSectionChanged,
),
if (primarySections.isNotEmpty &&
@ -143,6 +147,7 @@ class SidebarNavigation extends StatelessWidget {
emphasis: _SidebarItemEmphasis.secondary,
favoriteDestinations: favoriteDestinations,
onToggleFavorite: onToggleFavorite,
onOpenHome: onOpenHome,
onSectionChanged: onSectionChanged,
),
],
@ -158,6 +163,7 @@ class SidebarNavigation extends StatelessWidget {
emphasis: _SidebarItemEmphasis.secondary,
favoriteDestinations: favoriteDestinations,
onToggleFavorite: onToggleFavorite,
onOpenHome: onOpenHome,
onSectionChanged: onSectionChanged,
),
if (toolSections.isNotEmpty) const SizedBox(height: 6),
@ -247,6 +253,7 @@ class _SidebarSectionGroup extends StatelessWidget {
required this.emphasis,
required this.favoriteDestinations,
this.onToggleFavorite,
this.onOpenHome,
required this.onSectionChanged,
});
@ -257,6 +264,7 @@ class _SidebarSectionGroup extends StatelessWidget {
final _SidebarItemEmphasis emphasis;
final Set<WorkspaceDestination> favoriteDestinations;
final Future<void> Function(WorkspaceDestination section)? onToggleFavorite;
final VoidCallback? onOpenHome;
final ValueChanged<WorkspaceDestination> onSectionChanged;
@override
@ -279,8 +287,11 @@ class _SidebarSectionGroup extends StatelessWidget {
),
),
],
...sections.map(
(section) => Padding(
...sections.map((section) {
final useHomeShortcut =
currentSection == WorkspaceDestination.settings &&
section == WorkspaceDestination.assistant;
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xxs),
child: _SidebarNavItem(
section: section,
@ -292,15 +303,20 @@ class _SidebarSectionGroup extends StatelessWidget {
!collapsed &&
onToggleFavorite != null &&
kAssistantNavigationDestinationCandidates.contains(section),
labelOverride: useHomeShortcut
? appText('回到 APP首页', 'Back to app home')
: null,
onToggleFavorite: onToggleFavorite == null
? null
: () async {
await onToggleFavorite!(section);
},
onTap: () => onSectionChanged(section),
onTap: useHomeShortcut && onOpenHome != null
? onOpenHome!
: () => onSectionChanged(section),
),
),
),
);
}),
],
);
}
@ -314,6 +330,7 @@ class _SidebarNavItem extends StatefulWidget {
required this.emphasis,
required this.favorite,
required this.showFavoriteToggle,
this.labelOverride,
this.onToggleFavorite,
required this.onTap,
});
@ -324,6 +341,7 @@ class _SidebarNavItem extends StatefulWidget {
final _SidebarItemEmphasis emphasis;
final bool favorite;
final bool showFavoriteToggle;
final String? labelOverride;
final Future<void> Function()? onToggleFavorite;
final VoidCallback onTap;
@ -338,6 +356,7 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
Widget build(BuildContext context) {
final palette = context.palette;
final theme = Theme.of(context);
final label = widget.labelOverride ?? _sectionLabel(widget.section);
final isPrimary = widget.emphasis == _SidebarItemEmphasis.primary;
final background = widget.selected
? palette.surfacePrimary
@ -351,7 +370,7 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
final radius = AppRadius.button;
return Tooltip(
message: widget.collapsed ? _sectionLabel(widget.section) : '',
message: widget.collapsed ? label : '',
child: MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
@ -413,7 +432,7 @@ class _SidebarNavItemState extends State<_SidebarNavItem> {
const SizedBox(width: 6),
Expanded(
child: Text(
_sectionLabel(widget.section),
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
@ -551,9 +570,7 @@ class SidebarFooter extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = context.palette;
final themeToggleTooltip = themeMode == ThemeMode.dark
? appText('切换浅色', 'Switch to light')
: appText('切换深色', 'Switch to dark');
final themeToggleTooltip = chromeThemeToggleTooltip(themeMode);
if (isCollapsed) {
return Column(
@ -564,25 +581,21 @@ class SidebarFooter extends StatelessWidget {
color: palette.chromeStroke.withValues(alpha: 0.9),
),
const SizedBox(height: 6),
_SidebarLanguageButton(
ChromeLanguageActionButton(
appLanguage: appLanguage,
compact: true,
tooltip: appText('切换语言', 'Toggle language'),
onPressed: onToggleLanguage,
),
const SizedBox(height: 6),
_SidebarActionButton(
icon: themeMode == ThemeMode.dark
? Icons.dark_mode_rounded
: themeMode == ThemeMode.light
? Icons.light_mode_rounded
: Icons.brightness_auto_rounded,
ChromeIconActionButton(
icon: chromeThemeToggleIcon(themeMode),
tooltip: themeToggleTooltip,
onPressed: onOpenThemeToggle,
),
const SizedBox(height: AppSpacing.xs),
if (showCollapseControl) ...[
_SidebarActionButton(
ChromeIconActionButton(
icon: _sidebarStateIcon(sidebarState),
tooltip: _sidebarStateLabel(sidebarState),
onPressed: onCycleSidebarState,
@ -590,7 +603,7 @@ class SidebarFooter extends StatelessWidget {
const SizedBox(height: 6),
],
if (showSettingsButton) ...[
_SidebarActionButton(
ChromeIconActionButton(
icon: Icons.tune_rounded,
tooltip: appText('设置', 'Settings'),
onPressed: onOpenSettings,
@ -598,7 +611,7 @@ class SidebarFooter extends StatelessWidget {
const SizedBox(height: 6),
],
if (onOpenOnlineWorkspace != null) ...[
_SidebarActionButton(
ChromeIconActionButton(
icon: Icons.open_in_new_rounded,
tooltip: appText('打开在线版', 'Open online workspace'),
onPressed: onOpenOnlineWorkspace!,
@ -642,7 +655,7 @@ class SidebarFooter extends StatelessWidget {
Row(
children: [
Expanded(
child: _SidebarLanguageButton(
child: ChromeLanguageActionButton(
appLanguage: appLanguage,
compact: false,
tooltip: appText('切换语言', 'Toggle language'),
@ -650,18 +663,14 @@ class SidebarFooter extends StatelessWidget {
),
),
const SizedBox(width: AppSpacing.xs),
_SidebarActionButton(
icon: themeMode == ThemeMode.dark
? Icons.dark_mode_rounded
: themeMode == ThemeMode.light
? Icons.light_mode_rounded
: Icons.brightness_auto_rounded,
ChromeIconActionButton(
icon: chromeThemeToggleIcon(themeMode),
tooltip: themeToggleTooltip,
onPressed: onOpenThemeToggle,
),
const SizedBox(width: AppSpacing.xs),
if (showCollapseControl)
_SidebarActionButton(
ChromeIconActionButton(
icon: _sidebarStateIcon(sidebarState),
tooltip: _sidebarStateLabel(sidebarState),
onPressed: onCycleSidebarState,
@ -701,79 +710,6 @@ class SidebarFooter extends StatelessWidget {
enum _SidebarItemEmphasis { primary, secondary }
class _SidebarActionButton extends StatefulWidget {
const _SidebarActionButton({
required this.icon,
this.tooltip,
required this.onPressed,
});
final IconData icon;
final String? tooltip;
final VoidCallback onPressed;
@override
State<_SidebarActionButton> createState() => _SidebarActionButtonState();
}
class _SidebarActionButtonState extends State<_SidebarActionButton> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final resolvedBackground = _hovered
? palette.chromeSurfacePressed
: palette.chromeSurface;
return Tooltip(
message: widget.tooltip ?? '',
child: MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: _hovered ? 0.94 : 0.88,
),
resolvedBackground,
],
),
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered ? palette.chromeShadowLift : palette.chromeShadowAmbient,
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.button),
onTap: widget.onPressed,
child: Container(
height: AppSizes.sidebarItemHeight,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
child: Center(
child: Icon(
widget.icon,
size: AppSizes.sidebarIconSize,
color: palette.textSecondary,
),
),
),
),
),
),
),
);
}
}
class _SidebarAccountTile extends StatefulWidget {
const _SidebarAccountTile({
required this.selected,
@ -903,76 +839,3 @@ class _SidebarAccountTileState extends State<_SidebarAccountTile> {
);
}
}
class _SidebarLanguageButton extends StatefulWidget {
const _SidebarLanguageButton({
required this.appLanguage,
required this.compact,
required this.tooltip,
required this.onPressed,
});
final AppLanguage appLanguage;
final bool compact;
final String tooltip;
final VoidCallback onPressed;
@override
State<_SidebarLanguageButton> createState() => _SidebarLanguageButtonState();
}
class _SidebarLanguageButtonState extends State<_SidebarLanguageButton> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final palette = context.palette;
final size = widget.compact ? AppSizes.sidebarItemHeight : 44.0;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Tooltip(
message: widget.tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.button),
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: _hovered ? 0.94 : 0.88,
),
_hovered
? palette.chromeSurfacePressed
: palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(AppRadius.button),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered
? palette.chromeShadowLift
: palette.chromeShadowAmbient,
],
),
child: Text(
widget.appLanguage.compactLabel,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: palette.textPrimary,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
}
}

View File

@ -122,6 +122,14 @@ void main() {
find.byKey(const ValueKey<String>('assistant-focus-open-page-settings')),
findsOneWidget,
);
expect(
find.byKey(const Key('assistant-focus-settings-language-toggle')),
findsOneWidget,
);
expect(
find.byKey(const Key('assistant-focus-settings-theme-toggle')),
findsOneWidget,
);
await tester.tap(
find.byKey(const ValueKey<String>('assistant-focus-open-page-settings')),

View File

@ -0,0 +1,58 @@
@TestOn('vm')
library;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/i18n/app_language.dart';
import 'package:xworkmate/models/app_models.dart';
import 'package:xworkmate/theme/app_theme.dart';
import 'package:xworkmate/widgets/assistant_focus_panel.dart';
import '../test_support.dart';
void main() {
testWidgets(
'Settings focused preview reuses language and theme quick actions',
(WidgetTester tester) async {
final controller = await createTestController(tester);
await tester.pumpWidget(
MaterialApp(
locale: const Locale('zh'),
theme: AppTheme.light(platform: TargetPlatform.macOS),
darkTheme: AppTheme.dark(platform: TargetPlatform.macOS),
home: Scaffold(
body: AssistantFocusDestinationCard(
controller: controller,
destination: WorkspaceDestination.settings,
onOpenPage: () {},
onRemoveFavorite: () async {},
),
),
),
);
await tester.pumpAndSettle();
expect(
find.byKey(const Key('assistant-focus-settings-language-toggle')),
findsOneWidget,
);
expect(
find.byKey(const Key('assistant-focus-settings-theme-toggle')),
findsOneWidget,
);
await tester.tap(
find.byKey(const Key('assistant-focus-settings-language-toggle')),
);
await tester.pumpAndSettle();
expect(controller.appLanguage, AppLanguage.en);
await tester.tap(
find.byKey(const Key('assistant-focus-settings-theme-toggle')),
);
await tester.pumpAndSettle();
expect(controller.themeMode, ThemeMode.dark);
},
);
}

View File

@ -25,6 +25,7 @@ void main() {
onToggleLanguage: () {},
onCycleSidebarState: () {},
onExpandFromCollapsed: () {},
onOpenHome: () {},
onOpenAccount: () {},
onOpenThemeToggle: () {},
accountName: 'Tester',
@ -64,6 +65,7 @@ void main() {
onToggleLanguage: () => languageToggled++,
onCycleSidebarState: () => sidebarCycled++,
onExpandFromCollapsed: () {},
onOpenHome: () {},
onOpenAccount: () => accountOpened++,
onOpenThemeToggle: () => themeToggled++,
accountName: 'Tester',
@ -111,4 +113,45 @@ void main() {
await tester.pumpAndSettle();
expect(accountOpened, 1);
});
testWidgets(
'SidebarNavigation shows app home shortcut copy on settings page',
(WidgetTester tester) async {
var selected = WorkspaceDestination.settings;
var homeOpened = 0;
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: Scaffold(
body: SidebarNavigation(
currentSection: selected,
sidebarState: AppSidebarState.expanded,
appLanguage: AppLanguage.zh,
themeMode: ThemeMode.light,
onSectionChanged: (value) => selected = value,
onToggleLanguage: () {},
onCycleSidebarState: () {},
onExpandFromCollapsed: () {},
onOpenHome: () => homeOpened++,
onOpenAccount: () {},
onOpenThemeToggle: () {},
accountName: 'Tester',
accountSubtitle: 'Workspace',
),
),
),
);
await tester.pumpAndSettle();
expect(find.text('回到 APP首页'), findsOneWidget);
expect(find.text('新对话'), findsNothing);
await tester.tap(find.text('回到 APP首页'));
await tester.pumpAndSettle();
expect(homeOpened, 1);
expect(selected, WorkspaceDestination.settings);
},
);
}