From e15ce5b7b63f8319036bd6e14c58dc9d48166307 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:15:24 +0530 Subject: [PATCH 1/2] feat(ui): Add collapsible sections to chats drawer for better UX --- lib/core/persistence/persistence_keys.dart | 5 + .../navigation/widgets/chats_drawer.dart | 497 ++++++++++++------ lib/shared/theme/tweakcn_themes.dart | 2 +- 3 files changed, 338 insertions(+), 166 deletions(-) diff --git a/lib/core/persistence/persistence_keys.dart b/lib/core/persistence/persistence_keys.dart index faacb45..8e69006 100644 --- a/lib/core/persistence/persistence_keys.dart +++ b/lib/core/persistence/persistence_keys.dart @@ -30,6 +30,11 @@ final class PreferenceKeys { static const String ttsServerVoiceName = 'tts_server_voice_name'; static const String voiceSilenceDuration = 'voice_silence_duration'; static const String androidAssistantTrigger = 'android_assistant_trigger'; + + // Drawer section collapsed states + static const String drawerShowPinned = 'drawer_show_pinned'; + static const String drawerShowFolders = 'drawer_show_folders'; + static const String drawerShowRecent = 'drawer_show_recent'; } final class LegacyPreferenceKeys { diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 54c57b9..91ec7da 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -26,6 +26,12 @@ import '../../../shared/widgets/responsive_drawer_layout.dart'; import '../../../core/models/model.dart'; import '../../../core/models/conversation.dart'; import '../../../core/models/folder.dart'; +import '../../../core/persistence/persistence_keys.dart'; +import '../../../core/persistence/hive_boxes.dart'; +import 'package:hive_ce/hive.dart'; + +/// Defines the section types that can be collapsed in the chats drawer +enum _SectionType { pinned, recent } class ChatsDrawer extends ConsumerStatefulWidget { const ChatsDrawer({super.key}); @@ -49,6 +55,12 @@ class _ChatsDrawerState extends ConsumerState { // UI state providers for sections static final _showArchivedProvider = NotifierProvider<_ShowArchivedNotifier, bool>(_ShowArchivedNotifier.new); + static final _showPinnedProvider = + NotifierProvider<_ShowPinnedNotifier, bool>(_ShowPinnedNotifier.new); + static final _showFoldersProvider = + NotifierProvider<_ShowFoldersNotifier, bool>(_ShowFoldersNotifier.new); + static final _showRecentProvider = + NotifierProvider<_ShowRecentNotifier, bool>(_ShowRecentNotifier.new); static final _expandedFoldersProvider = NotifierProvider<_ExpandedFoldersNotifier, Map>( _ExpandedFoldersNotifier.new, @@ -318,6 +330,10 @@ class _ChatsDrawerState extends ConsumerState { final archived = list.where((c) => c.archived == true).toList(); + final showPinned = ref.watch(_showPinnedProvider); + final showFolders = ref.watch(_showFoldersProvider); + final showRecent = ref.watch(_showRecentProvider); + final slivers = [ if (pinned.isNotEmpty) ...[ SliverPadding( @@ -326,11 +342,14 @@ class _ChatsDrawerState extends ConsumerState { child: _buildSectionHeader( AppLocalizations.of(context)!.pinned, pinned.length, + sectionType: _SectionType.pinned, ), ), ), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), - _conversationsSliver(pinned, modelsById: modelsById), + if (showPinned) ...[ + const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), + _conversationsSliver(pinned, modelsById: modelsById), + ], const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), ], @@ -339,89 +358,94 @@ class _ChatsDrawerState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: Spacing.md), sliver: SliverToBoxAdapter(child: _buildFoldersSectionHeader()), ), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), - if (_isDragging && _draggingHasFolder) ...[ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: Spacing.md), - sliver: SliverToBoxAdapter(child: _buildUnfileDropTarget()), - ), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.sm)), - ], - ...ref - .watch(foldersProvider) - .when( - data: (folders) { - final grouped = >{}; - for (final c in foldered) { - final id = c.folderId!; - grouped.putIfAbsent(id, () => []).add(c); - } + if (showFolders) ...[ + const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), + if (_isDragging && _draggingHasFolder) ...[ + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), + sliver: SliverToBoxAdapter(child: _buildUnfileDropTarget()), + ), + const SliverToBoxAdapter(child: SizedBox(height: Spacing.sm)), + ], + ...ref + .watch(foldersProvider) + .when( + data: (folders) { + final grouped = >{}; + for (final c in foldered) { + final id = c.folderId!; + grouped.putIfAbsent(id, () => []).add(c); + } - final expandedMap = ref.watch(_expandedFoldersProvider); + final expandedMap = ref.watch(_expandedFoldersProvider); - final out = []; - for (final folder in folders) { - final existing = grouped[folder.id] ?? const []; - final convs = _resolveFolderConversations( - folder, - existing, - ); - final isExpanded = - expandedMap[folder.id] ?? folder.isExpanded; - final hasItems = convs.isNotEmpty; - out.add( - SliverPadding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - ), - sliver: SliverToBoxAdapter( - child: _buildFolderHeader( - folder.id, - folder.name, - convs.length, - defaultExpanded: folder.isExpanded, + final out = []; + for (final folder in folders) { + final existing = + grouped[folder.id] ?? const []; + final convs = _resolveFolderConversations( + folder, + existing, + ); + final isExpanded = + expandedMap[folder.id] ?? folder.isExpanded; + final hasItems = convs.isNotEmpty; + out.add( + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + ), + sliver: SliverToBoxAdapter( + child: _buildFolderHeader( + folder.id, + folder.name, + convs.length, + defaultExpanded: folder.isExpanded, + ), ), ), - ), - ); - if (isExpanded && hasItems) { - out.add( - const SliverToBoxAdapter( - child: SizedBox(height: Spacing.xs), - ), - ); - out.add( - _conversationsSliver( - convs, - inFolder: true, - modelsById: modelsById, - ), ); + if (isExpanded && hasItems) { + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.xs), + ), + ); + out.add( + _conversationsSliver( + convs, + inFolder: true, + modelsById: modelsById, + ), + ); + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.xs), + ), + ); + } out.add( const SliverToBoxAdapter( child: SizedBox(height: Spacing.xs), ), ); } - out.add( - const SliverToBoxAdapter( - child: SizedBox(height: Spacing.xs), - ), - ); - } - return out.isEmpty - ? [ - const SliverToBoxAdapter(child: SizedBox.shrink()), - ] - : out; - }, - loading: () => [ - const SliverToBoxAdapter(child: SizedBox.shrink()), - ], - error: (e, st) => [ - const SliverToBoxAdapter(child: SizedBox.shrink()), - ], - ), + return out.isEmpty + ? [ + const SliverToBoxAdapter( + child: SizedBox.shrink(), + ), + ] + : out; + }, + loading: () => [ + const SliverToBoxAdapter(child: SizedBox.shrink()), + ], + error: (e, st) => [ + const SliverToBoxAdapter(child: SizedBox.shrink()), + ], + ), + ], const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), if (regular.isNotEmpty) ...[ @@ -431,11 +455,14 @@ class _ChatsDrawerState extends ConsumerState { child: _buildSectionHeader( AppLocalizations.of(context)!.recent, regular.length, + sectionType: _SectionType.recent, ), ), ), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), - _conversationsSliver(regular, modelsById: modelsById), + if (showRecent) ...[ + const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), + _conversationsSliver(regular, modelsById: modelsById), + ], ], if (archived.isNotEmpty) ...[ @@ -525,6 +552,10 @@ class _ChatsDrawerState extends ConsumerState { final archived = list.where((c) => c.archived == true).toList(); + final showPinned = ref.watch(_showPinnedProvider); + final showFolders = ref.watch(_showFoldersProvider); + final showRecent = ref.watch(_showRecentProvider); + final slivers = [ SliverPadding( padding: const EdgeInsets.symmetric(horizontal: Spacing.md), @@ -543,118 +574,138 @@ class _ChatsDrawerState extends ConsumerState { child: _buildSectionHeader( AppLocalizations.of(context)!.pinned, pinned.length, + sectionType: _SectionType.pinned, ), ), ), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), - _conversationsSliver(pinned, modelsById: modelsById), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), ]); + if (showPinned) { + slivers.addAll([ + const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), + _conversationsSliver(pinned, modelsById: modelsById), + ]); + } + slivers.add( + const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), + ); } - slivers.addAll([ + slivers.add( SliverPadding( padding: const EdgeInsets.symmetric(horizontal: Spacing.md), sliver: SliverToBoxAdapter(child: _buildFoldersSectionHeader()), ), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), - ]); + ); - if (_isDragging && _draggingHasFolder) { + if (showFolders) { slivers.add( - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: Spacing.md), - sliver: SliverToBoxAdapter(child: _buildUnfileDropTarget()), - ), + const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), ); - slivers.add( - const SliverToBoxAdapter(child: SizedBox(height: Spacing.sm)), - ); - } - final folderSlivers = ref - .watch(foldersProvider) - .when( - data: (folders) { - final grouped = >{}; - for (final c in foldered) { - final id = c.folderId!; - grouped.putIfAbsent(id, () => []).add(c); - } - final expandedMap = ref.watch(_expandedFoldersProvider); - final out = []; - for (final folder in folders) { - final existing = grouped[folder.id] ?? const []; - final convs = _resolveFolderConversations(folder, existing); - final isExpanded = - expandedMap[folder.id] ?? folder.isExpanded; - final hasItems = convs.isNotEmpty; + if (_isDragging && _draggingHasFolder) { + slivers.add( + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), + sliver: SliverToBoxAdapter(child: _buildUnfileDropTarget()), + ), + ); + slivers.add( + const SliverToBoxAdapter(child: SizedBox(height: Spacing.sm)), + ); + } - out.add( - SliverPadding( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - ), - sliver: SliverToBoxAdapter( - child: _buildFolderHeader( - folder.id, - folder.name, - convs.length, - defaultExpanded: folder.isExpanded, + final folderSlivers = ref + .watch(foldersProvider) + .when( + data: (folders) { + final grouped = >{}; + for (final c in foldered) { + final id = c.folderId!; + grouped.putIfAbsent(id, () => []).add(c); + } + final expandedMap = ref.watch(_expandedFoldersProvider); + final out = []; + for (final folder in folders) { + final existing = grouped[folder.id] ?? const []; + final convs = _resolveFolderConversations(folder, existing); + final isExpanded = + expandedMap[folder.id] ?? folder.isExpanded; + final hasItems = convs.isNotEmpty; + + out.add( + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + ), + sliver: SliverToBoxAdapter( + child: _buildFolderHeader( + folder.id, + folder.name, + convs.length, + defaultExpanded: folder.isExpanded, + ), ), ), - ), - ); - if (isExpanded && hasItems) { - out.add( - const SliverToBoxAdapter( - child: SizedBox(height: Spacing.xs), - ), - ); - out.add( - _conversationsSliver( - convs, - inFolder: true, - modelsById: modelsById, - ), - ); - out.add( - const SliverToBoxAdapter( - child: SizedBox(height: Spacing.sm), - ), ); + if (isExpanded && hasItems) { + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.xs), + ), + ); + out.add( + _conversationsSliver( + convs, + inFolder: true, + modelsById: modelsById, + ), + ); + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.sm), + ), + ); + } } - } - return out.isEmpty - ? [ - const SliverToBoxAdapter(child: SizedBox.shrink()), - ] - : out; - }, - loading: () => [ - const SliverToBoxAdapter(child: SizedBox.shrink()), - ], - error: (e, st) => [ - const SliverToBoxAdapter(child: SizedBox.shrink()), - ], - ); - slivers.addAll(folderSlivers); + return out.isEmpty + ? [ + const SliverToBoxAdapter(child: SizedBox.shrink()), + ] + : out; + }, + loading: () => [ + const SliverToBoxAdapter(child: SizedBox.shrink()), + ], + error: (e, st) => [ + const SliverToBoxAdapter(child: SizedBox.shrink()), + ], + ); + slivers.addAll(folderSlivers); + } + + slivers.add( + const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), + ); if (regular.isNotEmpty) { - slivers.addAll([ - const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)), + slivers.add( SliverPadding( padding: const EdgeInsets.symmetric(horizontal: Spacing.md), sliver: SliverToBoxAdapter( child: _buildSectionHeader( AppLocalizations.of(context)!.recent, regular.length, + sectionType: _SectionType.recent, ), ), ), - const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), - _conversationsSliver(regular, modelsById: modelsById), - ]); + ); + if (showRecent) { + slivers.addAll([ + const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)), + _conversationsSliver(regular, modelsById: modelsById), + ]); + } } if (archived.isNotEmpty) { @@ -693,10 +744,41 @@ class _ChatsDrawerState extends ConsumerState { ); } - Widget _buildSectionHeader(String title, int count) { + Widget _buildSectionHeader( + String title, + int count, { + _SectionType? sectionType, + }) { final sidebarTheme = context.sidebarTheme; - return Row( + + // Get the collapsed state for the section type + bool isExpanded = true; + VoidCallback? onToggle; + + if (sectionType == _SectionType.pinned) { + isExpanded = ref.watch(_showPinnedProvider); + onToggle = () => ref.read(_showPinnedProvider.notifier).toggle(); + } else if (sectionType == _SectionType.recent) { + isExpanded = ref.watch(_showRecentProvider); + onToggle = () => ref.read(_showRecentProvider.notifier).toggle(); + } + + final headerContent = Row( children: [ + if (onToggle != null) ...[ + Icon( + isExpanded + ? (Platform.isIOS + ? CupertinoIcons.chevron_down + : Icons.expand_more) + : (Platform.isIOS + ? CupertinoIcons.chevron_right + : Icons.chevron_right), + color: sidebarTheme.foreground.withValues(alpha: 0.6), + size: IconSize.sm, + ), + const SizedBox(width: Spacing.xxs), + ], Text( title, style: AppTypography.labelStyle.copyWith( @@ -726,18 +808,58 @@ class _ChatsDrawerState extends ConsumerState { ), ], ); + + if (onToggle == null) { + return headerContent; + } + + return InkWell( + onTap: onToggle, + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: Spacing.xxs), + child: headerContent, + ), + ); } /// Header for the Folders section with a create button on the right Widget _buildFoldersSectionHeader() { final theme = context.conduitTheme; + final sidebarTheme = context.sidebarTheme; + final isExpanded = ref.watch(_showFoldersProvider); + return Row( children: [ - Text( - AppLocalizations.of(context)!.folders, - style: AppTypography.labelStyle.copyWith( - color: theme.textSecondary, - decoration: TextDecoration.none, + InkWell( + onTap: () => ref.read(_showFoldersProvider.notifier).toggle(), + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: Spacing.xxs), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isExpanded + ? (Platform.isIOS + ? CupertinoIcons.chevron_down + : Icons.expand_more) + : (Platform.isIOS + ? CupertinoIcons.chevron_right + : Icons.chevron_right), + color: sidebarTheme.foreground.withValues(alpha: 0.6), + size: IconSize.sm, + ), + const SizedBox(width: Spacing.xxs), + Text( + AppLocalizations.of(context)!.folders, + style: AppTypography.labelStyle.copyWith( + color: theme.textSecondary, + decoration: TextDecoration.none, + ), + ), + ], + ), ), ), const Spacer(), @@ -1580,6 +1702,51 @@ class _ShowArchivedNotifier extends Notifier { void set(bool value) => state = value; } +class _ShowPinnedNotifier extends Notifier { + Box get _box => Hive.box(HiveBoxNames.preferences); + + @override + bool build() { + return _box.get(PreferenceKeys.drawerShowPinned, defaultValue: true) + as bool; + } + + void toggle() { + state = !state; + _box.put(PreferenceKeys.drawerShowPinned, state); + } +} + +class _ShowFoldersNotifier extends Notifier { + Box get _box => Hive.box(HiveBoxNames.preferences); + + @override + bool build() { + return _box.get(PreferenceKeys.drawerShowFolders, defaultValue: true) + as bool; + } + + void toggle() { + state = !state; + _box.put(PreferenceKeys.drawerShowFolders, state); + } +} + +class _ShowRecentNotifier extends Notifier { + Box get _box => Hive.box(HiveBoxNames.preferences); + + @override + bool build() { + return _box.get(PreferenceKeys.drawerShowRecent, defaultValue: true) + as bool; + } + + void toggle() { + state = !state; + _box.put(PreferenceKeys.drawerShowRecent, state); + } +} + class _ExpandedFoldersNotifier extends Notifier> { @override Map build() => {}; diff --git a/lib/shared/theme/tweakcn_themes.dart b/lib/shared/theme/tweakcn_themes.dart index 89ecd4f..2a07e18 100644 --- a/lib/shared/theme/tweakcn_themes.dart +++ b/lib/shared/theme/tweakcn_themes.dart @@ -210,7 +210,7 @@ class TweakcnThemes { border: const Color(0xFF282828), input: const Color(0xFF343434), ring: const Color(0xFF737373), - sidebarBackground: const Color(0xFF171717), + sidebarBackground: const Color(0xFF0A0A0A), sidebarForeground: const Color(0xFFFAFAFA), sidebarPrimary: const Color(0xFF1447E6), sidebarPrimaryForeground: const Color(0xFFFAFAFA), From 98ae65d08f25475c4b6de2b04b435933730bc3c7 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:59:38 +0530 Subject: [PATCH 2/2] feat(ui): Improve text overflow and spacing in chat drawer --- lib/features/chat/views/chat_page.dart | 318 +++++++++--------- .../chat/widgets/streaming_title_text.dart | 75 +++-- .../navigation/widgets/chats_drawer.dart | 25 +- 3 files changed, 220 insertions(+), 198 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index a888cb8..7af8cc7 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -709,12 +709,13 @@ class _ChatPageState extends ConsumerState { ?.cast(); final name = meta?['name']?.toString() ?? parsed.host; - final collectionName = - result?['collection_name']?.toString(); + final collectionName = result?['collection_name'] + ?.toString(); // Add as appropriate type - final notifier = - ref.read(contextAttachmentsProvider.notifier); + final notifier = ref.read( + contextAttachmentsProvider.notifier, + ); if (isYoutube) { notifier.addYoutube( displayName: name, @@ -1680,28 +1681,29 @@ class _ChatPageState extends ConsumerState { constraints: BoxConstraints( maxWidth: constraints.maxWidth, ), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - AnimatedSwitcher( - duration: const Duration( - milliseconds: 250, - ), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: displayConversationTitle != null - ? Column( - key: ValueKey( - displayConversationTitle, - ), - mainAxisSize: MainAxisSize.min, - children: [ - StreamingTitleText( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AnimatedSwitcher( + duration: const Duration( + milliseconds: 250, + ), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + child: displayConversationTitle != null + ? Column( + key: ValueKey( + displayConversationTitle, + ), + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + constraints.maxWidth, + ), + child: StreamingTitleText( title: displayConversationTitle, style: AppTypography @@ -1720,96 +1722,45 @@ class _ChatPageState extends ConsumerState { .textPrimary .withValues(alpha: 0.8), ), - const SizedBox( - height: Spacing.xs, - ), - ], - ) - : const SizedBox.shrink( - key: ValueKey( - 'empty-title', ), + const SizedBox( + height: Spacing.xs, + ), + ], + ) + : const SizedBox.shrink( + key: ValueKey( + 'empty-title', ), - ), - Transform.translate( - offset: const Offset(0, 0), - child: () { - const double iconPaddingX = - Spacing.xs; - const double iconPaddingY = - Spacing.xxs; - const double iconWidth = - IconSize.small; - const double iconBoxWidth = - (iconPaddingX * 2) + - (BorderWidth.thin * 2) + - iconWidth; - final double maxLabelWidth = - (constraints.maxWidth - - (iconBoxWidth * 2) - - (Spacing.xs * 2)) - .clamp( - 48.0, - constraints.maxWidth, - ); + ), + ), + Transform.translate( + offset: const Offset(0, 0), + child: () { + const double iconPaddingX = Spacing.xs; + const double iconPaddingY = Spacing.xxs; + const double iconWidth = IconSize.small; + const double iconBoxWidth = + (iconPaddingX * 2) + + (BorderWidth.thin * 2) + + iconWidth; + final double maxLabelWidth = + (constraints.maxWidth - + (iconBoxWidth * 2) - + (Spacing.xs * 2)) + .clamp( + 48.0, + constraints.maxWidth, + ); - final row = Row( - mainAxisAlignment: - MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Opacity( - opacity: 0.0, - child: Container( - padding: - const EdgeInsets.symmetric( - horizontal: - iconPaddingX, - vertical: iconPaddingY, - ), - decoration: BoxDecoration( - color: context - .conduitTheme - .surfaceBackground - .withValues(alpha: 0.3), - borderRadius: - BorderRadius.circular( - AppBorderRadius.badge, - ), - border: Border.all( - color: context - .conduitTheme - .dividerColor, - width: BorderWidth.thin, - ), - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons - .chevron_down - : Icons - .keyboard_arrow_down, - color: context - .conduitTheme - .iconSecondary, - size: iconWidth, - ), - ), - ), - const SizedBox(width: Spacing.xs), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxLabelWidth, - ), - child: MiddleEllipsisText( - modelLabel, - style: modelTextStyle, - textAlign: TextAlign.center, - semanticsLabel: modelLabel, - ), - ), - const SizedBox(width: Spacing.xs), - Container( + final row = Row( + mainAxisAlignment: + MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Opacity( + opacity: 0.0, + child: Container( padding: const EdgeInsets.symmetric( horizontal: iconPaddingX, @@ -1843,64 +1794,107 @@ class _ChatPageState extends ConsumerState { size: iconWidth, ), ), - ], - ); - final constrainedRow = ConstrainedBox( - constraints: BoxConstraints( - maxWidth: constraints.maxWidth, ), - child: row, - ); - return hasConversationTitle - ? SizedBox( - height: 24, - child: constrainedRow, - ) - : constrainedRow; - }(), - ), - if (isReviewerMode) - Padding( - padding: const EdgeInsets.only( - top: 2.0, + const SizedBox(width: Spacing.xs), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: maxLabelWidth, + ), + child: MiddleEllipsisText( + modelLabel, + style: modelTextStyle, + textAlign: TextAlign.center, + semanticsLabel: modelLabel, + ), + ), + const SizedBox(width: Spacing.xs), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: iconPaddingX, + vertical: iconPaddingY, + ), + decoration: BoxDecoration( + color: context + .conduitTheme + .surfaceBackground + .withValues(alpha: 0.3), + borderRadius: + BorderRadius.circular( + AppBorderRadius.badge, + ), + border: Border.all( + color: context + .conduitTheme + .dividerColor, + width: BorderWidth.thin, + ), + ), + child: Icon( + Platform.isIOS + ? CupertinoIcons + .chevron_down + : Icons.keyboard_arrow_down, + color: context + .conduitTheme + .iconSecondary, + size: iconWidth, + ), + ), + ], + ); + final constrainedRow = ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth, ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: 1.0, + child: row, + ); + return hasConversationTitle + ? SizedBox( + height: 24, + child: constrainedRow, + ) + : constrainedRow; + }(), + ), + if (isReviewerMode) + Padding( + padding: const EdgeInsets.only( + top: 2.0, + ), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: 1.0, + ), + decoration: BoxDecoration( + color: context.conduitTheme.success + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular( + AppBorderRadius.badge, ), - decoration: BoxDecoration( + border: Border.all( color: context .conduitTheme .success - .withValues(alpha: 0.1), - borderRadius: - BorderRadius.circular( - AppBorderRadius.badge, - ), - border: Border.all( - color: context - .conduitTheme - .success - .withValues(alpha: 0.3), - width: BorderWidth.thin, - ), - ), - child: Text( - 'REVIEWER MODE', - style: AppTypography.captionStyle - .copyWith( - color: context - .conduitTheme - .success, - fontWeight: FontWeight.w600, - fontSize: 9, - ), + .withValues(alpha: 0.3), + width: BorderWidth.thin, ), ), + child: Text( + 'REVIEWER MODE', + style: AppTypography.captionStyle + .copyWith( + color: context + .conduitTheme + .success, + fontWeight: FontWeight.w600, + fontSize: 9, + ), + ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/features/chat/widgets/streaming_title_text.dart b/lib/features/chat/widgets/streaming_title_text.dart index c0d0f09..d3fa7e6 100644 --- a/lib/features/chat/widgets/streaming_title_text.dart +++ b/lib/features/chat/widgets/streaming_title_text.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../../../shared/widgets/middle_ellipsis_text.dart'; + /// Displays a chat title that reveals characters with a streaming animation /// whenever the title changes. class StreamingTitleText extends StatefulWidget { @@ -141,36 +143,53 @@ class _StreamingTitleTextState extends State ? widget.style.fontSize! * (widget.style.height ?? 1.1) : 18.0); - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - child: Text( - // When the animation completes we fall back to the full string. - revealedGlyphs >= totalGlyphs ? _activeTitle : visibleText, - maxLines: 1, - overflow: TextOverflow.fade, - softWrap: false, - textAlign: TextAlign.center, - style: widget.style, - ), - ), - if (isAnimating) - FadeTransition( - opacity: _cursorOpacity, - child: Container( - width: widget.cursorWidth, - height: cursorHeight, - margin: const EdgeInsets.only(left: 2), - decoration: BoxDecoration( - color: cursorColor, - borderRadius: BorderRadius.circular(widget.cursorWidth), - ), + // When animation is complete, use middle ellipsis for overflow. + // During animation, show partial text with standard Text widget. + final bool animationComplete = revealedGlyphs >= totalGlyphs; + + // Use middle ellipsis when animation is complete + if (animationComplete) { + return MiddleEllipsisText( + _activeTitle, + style: widget.style, + textAlign: TextAlign.center, + semanticsLabel: _activeTitle, + ); + } + + // During animation, use IntrinsicWidth to size the row to the text, + // then clip any overflow from the cursor + return ClipRect( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Text( + visibleText, + maxLines: 1, + overflow: TextOverflow.clip, + softWrap: false, + textAlign: TextAlign.center, + style: widget.style, ), ), - ], + if (isAnimating) + FadeTransition( + opacity: _cursorOpacity, + child: Container( + width: widget.cursorWidth, + height: cursorHeight, + margin: const EdgeInsets.only(left: 2), + decoration: BoxDecoration( + color: cursorColor, + borderRadius: BorderRadius.circular(widget.cursorWidth), + ), + ), + ), + ], + ), ); } diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 91ec7da..4117e99 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -29,6 +29,7 @@ import '../../../core/models/folder.dart'; import '../../../core/persistence/persistence_keys.dart'; import '../../../core/persistence/hive_boxes.dart'; import 'package:hive_ce/hive.dart'; +import '../../../shared/widgets/middle_ellipsis_text.dart'; /// Defines the section types that can be collapsed in the chats drawer enum _SectionType { pinned, recent } @@ -418,17 +419,19 @@ class _ChatsDrawerState extends ConsumerState { modelsById: modelsById, ), ); + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.sm), + ), + ); + } else { + // Only add spacing after collapsed folders out.add( const SliverToBoxAdapter( child: SizedBox(height: Spacing.xs), ), ); } - out.add( - const SliverToBoxAdapter( - child: SizedBox(height: Spacing.xs), - ), - ); } return out.isEmpty ? [ @@ -665,6 +668,13 @@ class _ChatsDrawerState extends ConsumerState { child: SizedBox(height: Spacing.sm), ), ); + } else { + // Only add spacing after collapsed folders + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.xs), + ), + ); } } return out.isEmpty @@ -1903,11 +1913,10 @@ class _ConversationTileContent extends StatelessWidget { ], Flexible( fit: textFit, - child: Text( + child: MiddleEllipsisText( title, - maxLines: 1, - overflow: TextOverflow.ellipsis, style: textStyle, + semanticsLabel: title, ), ), ...trailing,