From 1ef8d6322d02be16efc9916f3aae8a4aa45e158a Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 25 Mar 2026 10:06:42 +0800 Subject: [PATCH] feat(ui): add settings quick actions and home shortcut --- lib/app/app_shell_desktop.dart | 1 + lib/app/app_shell_web.dart | 64 +++--- lib/web/web_focus_panel.dart | 27 ++- lib/widgets/assistant_focus_panel.dart | 27 ++- lib/widgets/chrome_quick_action_buttons.dart | 169 ++++++++++++++ lib/widgets/settings_focus_quick_actions.dart | 48 ++++ lib/widgets/sidebar_navigation.dart | 211 +++--------------- test/web/web_ui_browser_test.dart | 8 + test/widgets/assistant_focus_panel_suite.dart | 58 +++++ test/widgets/sidebar_navigation_suite.dart | 43 ++++ 10 files changed, 440 insertions(+), 216 deletions(-) create mode 100644 lib/widgets/chrome_quick_action_buttons.dart create mode 100644 lib/widgets/settings_focus_quick_actions.dart create mode 100644 test/widgets/assistant_focus_panel_suite.dart diff --git a/lib/app/app_shell_desktop.dart b/lib/app/app_shell_desktop.dart index 83576eab..6405d6fb 100644 --- a/lib/app/app_shell_desktop.dart +++ b/lib/app/app_shell_desktop.dart @@ -228,6 +228,7 @@ class _AppShellState extends State { onCycleSidebarState: controller.cycleSidebarState, onExpandFromCollapsed: () => controller .setSidebarState(AppSidebarState.expanded), + onOpenHome: controller.navigateHome, onOpenAccount: () => controller.navigateTo( WorkspaceDestination.account, ), diff --git a/lib/app/app_shell_web.dart b/lib/app/app_shell_web.dart index a399a24a..f721b450 100644 --- a/lib/app/app_shell_web.dart +++ b/lib/app/app_shell_web.dart @@ -60,17 +60,18 @@ class _AppShellState extends State { animation: widget.controller, builder: (context, _) { final controller = widget.controller; - final availableDestinations = [ - WorkspaceDestination.assistant, - WorkspaceDestination.tasks, - WorkspaceDestination.skills, - WorkspaceDestination.nodes, - WorkspaceDestination.secrets, - WorkspaceDestination.aiGateway, - WorkspaceDestination.settings, - ].where(controller.capabilities.supportsDestination).toList( - growable: false, - ); + final availableDestinations = + [ + 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 { ); if (isMobile) { - final mobileDestinations = [ - WorkspaceDestination.assistant, - WorkspaceDestination.tasks, - WorkspaceDestination.skills, - WorkspaceDestination.settings, - ].where(controller.capabilities.supportsDestination).toList( - growable: false, - ); - final selectedIndex = mobileDestinations.contains( - currentDestination, - ) + final mobileDestinations = + [ + 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 { _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 { .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 { 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], ), ), diff --git a/lib/web/web_focus_panel.dart b/lib/web/web_focus_panel.dart index 78120eb1..f9e3bbe2 100644 --- a/lib/web/web_focus_panel.dart +++ b/lib/web/web_focus_panel.dart @@ -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'), diff --git a/lib/widgets/assistant_focus_panel.dart b/lib/widgets/assistant_focus_panel.dart index 9ba94537..10167fe8 100644 --- a/lib/widgets/assistant_focus_panel.dart +++ b/lib/widgets/assistant_focus_panel.dart @@ -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'), diff --git a/lib/widgets/chrome_quick_action_buttons.dart b/lib/widgets/chrome_quick_action_buttons.dart new file mode 100644 index 00000000..3ab6aec5 --- /dev/null +++ b/lib/widgets/chrome_quick_action_buttons.dart @@ -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 createState() => _ChromeIconActionButtonState(); +} + +class _ChromeIconActionButtonState extends State { + 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 createState() => + _ChromeLanguageActionButtonState(); +} + +class _ChromeLanguageActionButtonState + extends State { + 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, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/settings_focus_quick_actions.dart b/lib/widgets/settings_focus_quick_actions.dart new file mode 100644 index 00000000..08fe5d0f --- /dev/null +++ b/lib/widgets/settings_focus_quick_actions.dart @@ -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, + ), + ], + ); + } +} diff --git a/lib/widgets/sidebar_navigation.dart b/lib/widgets/sidebar_navigation.dart index 63b362fd..2a3c9837 100644 --- a/lib/widgets/sidebar_navigation.dart +++ b/lib/widgets/sidebar_navigation.dart @@ -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 favoriteDestinations; final Future Function(WorkspaceDestination section)? onToggleFavorite; + final VoidCallback? onOpenHome; final ValueChanged 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 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, - ), - ), - ), - ), - ), - ); - } -} diff --git a/test/web/web_ui_browser_test.dart b/test/web/web_ui_browser_test.dart index 44a9d7be..9c1f997b 100644 --- a/test/web/web_ui_browser_test.dart +++ b/test/web/web_ui_browser_test.dart @@ -122,6 +122,14 @@ void main() { find.byKey(const ValueKey('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('assistant-focus-open-page-settings')), diff --git a/test/widgets/assistant_focus_panel_suite.dart b/test/widgets/assistant_focus_panel_suite.dart new file mode 100644 index 00000000..a7c9f363 --- /dev/null +++ b/test/widgets/assistant_focus_panel_suite.dart @@ -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); + }, + ); +} diff --git a/test/widgets/sidebar_navigation_suite.dart b/test/widgets/sidebar_navigation_suite.dart index 50fa51b1..096a4301 100644 --- a/test/widgets/sidebar_navigation_suite.dart +++ b/test/widgets/sidebar_navigation_suite.dart @@ -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); + }, + ); }