From 2cdbbbc1d3e18ff9e7187240c3f470032a791b0f Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 22 Aug 2025 01:24:04 +0530 Subject: [PATCH] refactor: ui/ux refinements --- lib/core/services/deep_link_service.dart | 21 +- lib/core/services/navigation_service.dart | 8 +- lib/features/chat/views/chat_page.dart | 149 +---- .../chat/widgets/message_batch_widget.dart | 37 +- .../chat/widgets/modern_chat_input.dart | 14 +- .../{files_page.dart => workspace_page.dart} | 38 +- .../navigation/widgets/chats_drawer.dart | 546 ++++++++++++------ .../onboarding/views/onboarding_sheet.dart | 17 +- lib/features/profile/views/profile_page.dart | 129 +---- .../tools/widgets/unified_tools_modal.dart | 14 +- lib/shared/widgets/sheet_handle.dart | 23 + 11 files changed, 459 insertions(+), 537 deletions(-) rename lib/features/files/views/{files_page.dart => workspace_page.dart} (93%) create mode 100644 lib/shared/widgets/sheet_handle.dart diff --git a/lib/core/services/deep_link_service.dart b/lib/core/services/deep_link_service.dart index 9ce110a..0e781e7 100644 --- a/lib/core/services/deep_link_service.dart +++ b/lib/core/services/deep_link_service.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../features/chat/views/chat_page.dart'; -import '../../features/files/views/files_page.dart'; +import '../../features/files/views/workspace_page.dart'; import '../../features/profile/views/profile_page.dart'; /// Service for handling deep links and navigation routing @@ -16,11 +16,11 @@ class DeepLinkService { ); } - /// In single-screen mode, files/profile deep links route via navigator - static void navigateToFiles(BuildContext context) { + /// In single-screen mode, workspace/profile deep links route via navigator + static void navigateToWorkspace(BuildContext context) { Navigator.push( context, - MaterialPageRoute(builder: (context) => const FilesPage()), + MaterialPageRoute(builder: (context) => const WorkspacePage()), ); } @@ -37,9 +37,12 @@ class DeepLinkService { case '/chat': case '/main/chat': return '/chat'; - case '/files': - case '/main/files': - return '/files'; + // Support both new and legacy paths for workspace + case '/workspace': + case '/main/workspace': + case '/files': // legacy + case '/main/files': // legacy + return '/workspace'; case '/profile': case '/main/profile': return '/profile'; @@ -52,8 +55,8 @@ class DeepLinkService { static Widget handleDeepLink(String route) { final path = parsePath(route); switch (path) { - case '/files': - return const FilesPage(); + case '/workspace': + return const WorkspacePage(); case '/profile': return const ProfilePage(); case '/chat': diff --git a/lib/core/services/navigation_service.dart b/lib/core/services/navigation_service.dart index 3fcb4e3..ff35c70 100644 --- a/lib/core/services/navigation_service.dart +++ b/lib/core/services/navigation_service.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; // ThemedDialogs handles theming; no direct use of extensions here import '../../features/auth/views/connect_signin_page.dart'; import '../../features/chat/views/chat_page.dart'; -import '../../features/files/views/files_page.dart'; +import '../../features/files/views/workspace_page.dart'; import '../../features/profile/views/profile_page.dart'; import '../../shared/widgets/themed_dialogs.dart'; @@ -130,8 +130,8 @@ class NavigationService { page = const ConnectAndSignInPage(); break; - case Routes.files: - page = const FilesPage(); + case Routes.workspace: + page = const WorkspacePage(); break; // chats list route removed (replaced by drawer) @@ -154,5 +154,5 @@ class Routes { static const String login = '/login'; static const String profile = '/profile'; static const String serverConnection = '/server-connection'; - static const String files = '/files'; + static const String workspace = '/workspace'; } diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 7fdd52a..9dce29c 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -18,8 +18,6 @@ import '../widgets/assistant_message_widget.dart' as assistant; import '../widgets/file_attachment_widget.dart'; import '../services/voice_input_service.dart'; import '../services/file_attachment_service.dart'; -import '../../files/views/files_page.dart'; -import '../../profile/views/profile_page.dart'; import '../../tools/providers/tools_providers.dart'; import '../../navigation/widgets/chats_drawer.dart'; import '../../../shared/widgets/offline_indicator.dart'; @@ -30,6 +28,7 @@ import '../../../shared/widgets/loading_states.dart'; import 'chat_page_helpers.dart'; import '../../../shared/widgets/themed_dialogs.dart'; import '../../onboarding/views/onboarding_sheet.dart'; +import '../../../shared/widgets/sheet_handle.dart'; class ChatPage extends ConsumerStatefulWidget { const ChatPage({super.key}); @@ -464,133 +463,6 @@ class _ChatPageState extends ConsumerState { // Replaced bottom-sheet chat list with left drawer (see ChatsDrawer) - void _showQuickAccessMenu() { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (context) => Container( - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.modal), - ), - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Handle bar - Container( - width: 40, - height: 4, - margin: const EdgeInsets.symmetric(vertical: Spacing.sm), - decoration: BoxDecoration( - color: context.conduitTheme.dividerColor, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), - // Hint text - Padding( - padding: const EdgeInsets.symmetric(horizontal: Spacing.md), - child: Text( - 'Quick Actions', - style: AppTypography.bodySmallStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: Spacing.xs), - // Menu items - ListTile( - leading: Icon( - Platform.isIOS ? CupertinoIcons.plus : Icons.add_rounded, - color: context.conduitTheme.iconPrimary, - ), - title: Text( - 'New Chat', - style: AppTypography.bodyLargeStyle.copyWith( - color: context.conduitTheme.textPrimary, - ), - ), - subtitle: Text( - 'Start a new conversation', - style: AppTypography.bodySmallStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - onTap: () { - Navigator.pop(context); - _handleNewChat(); - }, - ), - ListTile( - leading: Icon( - Platform.isIOS - ? CupertinoIcons.doc - : Icons.description_outlined, - color: context.conduitTheme.iconPrimary, - ), - title: Text( - 'Files', - style: AppTypography.bodyLargeStyle.copyWith( - color: context.conduitTheme.textPrimary, - ), - ), - subtitle: Text( - 'Manage your files and documents', - style: AppTypography.bodySmallStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - onTap: () { - Navigator.pop(context); - _navigateToFiles(); - }, - ), - - ListTile( - leading: Icon( - Platform.isIOS ? CupertinoIcons.person : Icons.person_outline, - color: context.conduitTheme.iconPrimary, - ), - title: Text( - 'Profile', - style: AppTypography.bodyLargeStyle.copyWith( - color: context.conduitTheme.textPrimary, - ), - ), - subtitle: Text( - 'View and manage your profile', - style: AppTypography.bodySmallStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - onTap: () { - Navigator.pop(context); - _navigateToProfile(); - }, - ), - const SizedBox(height: Spacing.sm), - ], - ), - ), - ), - ); - } - - void _navigateToFiles() { - Navigator.of( - context, - ).push(MaterialPageRoute(builder: (context) => const FilesPage())); - } - - void _navigateToProfile() { - Navigator.of( - context, - ).push(MaterialPageRoute(builder: (context) => const ProfilePage())); - } - void _onScroll() { if (!_scrollController.hasClients) return; @@ -1126,10 +998,6 @@ class _ChatPageState extends ConsumerState { // Open left drawer instead of bottom sheet Scaffold.of(ctx).openDrawer(); }, - onLongPress: () { - HapticFeedback.mediumImpact(); - _showQuickAccessMenu(); - }, child: Padding( padding: const EdgeInsets.all(4.0), child: Icon( @@ -1635,19 +1503,8 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> { padding: const EdgeInsets.all(Spacing.bottomSheetPadding), child: Column( children: [ - // Handle bar - Container( - margin: const EdgeInsets.only( - top: Spacing.sm, - bottom: Spacing.md, - ), - width: Spacing.xxl, - height: Spacing.xs, - decoration: BoxDecoration( - color: context.conduitTheme.dividerColor, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), + // Handle bar (standardized) + const SheetHandle(), // Search field Padding( diff --git a/lib/features/chat/widgets/message_batch_widget.dart b/lib/features/chat/widgets/message_batch_widget.dart index 99c4291..284088d 100644 --- a/lib/features/chat/widgets/message_batch_widget.dart +++ b/lib/features/chat/widgets/message_batch_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../../shared/theme/app_theme.dart'; +import '../../../shared/widgets/sheet_handle.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -207,16 +208,8 @@ class CopyOptionsSheet extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Handle bar - Container( - margin: const EdgeInsets.only(top: Spacing.sm), - width: 40, - height: 4, - decoration: BoxDecoration( - color: context.conduitTheme.dividerColor, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), + // Handle bar (standardized) + const SheetHandle(), const SizedBox(height: Spacing.lg - Spacing.xs), @@ -340,16 +333,8 @@ class ExportOptionsSheet extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Handle bar - Container( - margin: const EdgeInsets.only(top: Spacing.sm), - width: 40, - height: 4, - decoration: BoxDecoration( - color: context.conduitTheme.dividerColor, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), + // Handle bar (standardized) + const SheetHandle(), const SizedBox(height: Spacing.lg - Spacing.xs), @@ -465,16 +450,8 @@ class MoreOptionsSheet extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Handle bar - Container( - margin: const EdgeInsets.only(top: Spacing.sm), - width: 40, - height: 4, - decoration: BoxDecoration( - color: AppTheme.neutral50.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), + // Handle bar (standardized) + const SheetHandle(), const SizedBox(height: Spacing.lg - Spacing.xs), diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index f94a6f7..c8d85e2 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import '../../../shared/theme/theme_extensions.dart'; +import '../../../shared/widgets/sheet_handle.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -615,17 +616,8 @@ class _ModernChatInputState extends ConsumerState child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Handle bar - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: context.conduitTheme.textPrimary.withValues( - alpha: Alpha.medium, - ), - borderRadius: BorderRadius.circular(2), - ), - ), + // Handle bar (standardized) + const SheetHandle(), const SizedBox(height: Spacing.lg), // Options grid diff --git a/lib/features/files/views/files_page.dart b/lib/features/files/views/workspace_page.dart similarity index 93% rename from lib/features/files/views/files_page.dart rename to lib/features/files/views/workspace_page.dart index f03d4d0..7aeff59 100644 --- a/lib/features/files/views/files_page.dart +++ b/lib/features/files/views/workspace_page.dart @@ -8,16 +8,17 @@ import '../../../core/services/navigation_service.dart'; import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/utils/ui_utils.dart'; +import '../../../shared/widgets/sheet_handle.dart'; /// Files page for managing documents and uploads -class FilesPage extends ConsumerStatefulWidget { - const FilesPage({super.key}); +class WorkspacePage extends ConsumerStatefulWidget { + const WorkspacePage({super.key}); @override - ConsumerState createState() => _FilesPageState(); + ConsumerState createState() => _WorkspacePageState(); } -class _FilesPageState extends ConsumerState +class _WorkspacePageState extends ConsumerState with TickerProviderStateMixin { int _selectedTab = 0; late AnimationController _tabAnimationController; @@ -109,17 +110,12 @@ class _FilesPageState extends ConsumerState onPressed: () => NavigationService.goBack(), tooltip: 'Back', ), - title: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Files', - style: context.conduitTheme.headingSmall?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ], + title: Text( + 'Workspace', + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), ), centerTitle: true, actions: [ @@ -290,16 +286,8 @@ class _FilesPageState extends ConsumerState child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Enhanced handle bar - Container( - margin: const EdgeInsets.symmetric(vertical: Spacing.sm), - width: 40, - height: 4, - decoration: BoxDecoration( - color: context.conduitTheme.dividerColor, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), + // Handle bar (standardized) + const SheetHandle(), // Header with enhanced typography Padding( diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index a46d2c4..1618e7e 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -7,12 +7,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/providers/app_providers.dart'; -import '../../../core/auth/auth_state_manager.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../chat/providers/chat_providers.dart' as chat; -import '../../files/views/files_page.dart'; +// import '../../files/views/files_page.dart'; import '../../profile/views/profile_page.dart'; import '../../../shared/utils/ui_utils.dart'; +import '../../../core/auth/auth_state_manager.dart'; class ChatsDrawer extends ConsumerStatefulWidget { const ChatsDrawer({super.key}); @@ -27,6 +27,9 @@ class _ChatsDrawerState extends ConsumerState { Timer? _debounce; String _query = ''; bool _isLoadingConversation = false; + String? _dragHoverFolderId; + bool _isDragging = false; + bool _draggingHasFolder = false; // UI state providers for sections static final _showArchivedProvider = StateProvider((ref) => false); @@ -49,8 +52,15 @@ class _ChatsDrawerState extends ConsumerState { }); } + // Payload for drag-and-drop of conversations + // Kept local to this widget + // ignore: unused_element + static _DragConversationData _dragData(String id, String title) => + _DragConversationData(id: id, title: title); + @override Widget build(BuildContext context) { + // Bottom section now only shows navigation actions final theme = context.conduitTheme; return SafeArea( @@ -80,35 +90,33 @@ class _ChatsDrawerState extends ConsumerState { final theme = context.conduitTheme; return Padding( padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0), - child: Row( + child: Stack( + alignment: Alignment.center, children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.chat_bubble_2 - : Icons.chat_bubble_outline_rounded, - color: theme.iconPrimary, + // Centered title (no leading icon) + Text( + 'Chats', + style: AppTypography.headlineSmallStyle.copyWith( + color: theme.textPrimary, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, ), - const SizedBox(width: Spacing.sm), - Expanded( - child: Text( - 'Chats', - style: AppTypography.headlineSmallStyle.copyWith( - color: theme.textPrimary, - fontWeight: FontWeight.w600, + // Right-aligned new chat action + Positioned( + right: 0, + child: IconButton( + icon: Icon( + Platform.isIOS ? CupertinoIcons.plus : Icons.add_rounded, + color: theme.iconPrimary, ), + onPressed: () { + chat.startNewChat(ref); + if (mounted) Navigator.of(context).maybePop(); + }, + tooltip: 'New Chat', ), ), - IconButton( - icon: Icon( - Platform.isIOS ? CupertinoIcons.plus : Icons.add_rounded, - color: theme.iconPrimary, - ), - onPressed: () { - chat.startNewChat(ref); - if (mounted) Navigator.of(context).maybePop(); - }, - tooltip: 'New Chat', - ), ], ), ); @@ -234,8 +242,14 @@ class _ChatsDrawerState extends ConsumerState { const SizedBox(height: Spacing.md), ], - if (foldered.isNotEmpty) ...[ - ...ref.watch(foldersProvider).when( + // Folders section (shown even if empty) + _buildFoldersSectionHeader(), + const SizedBox(height: Spacing.xs), + if (_isDragging && _draggingHasFolder) ...[ + _buildUnfileDropTarget(), + const SizedBox(height: Spacing.sm), + ], + ...ref.watch(foldersProvider).when( data: (folders) { final grouped = >{}; for (final c in foldered) { @@ -243,18 +257,16 @@ class _ChatsDrawerState extends ConsumerState { grouped.putIfAbsent(id, () => []).add(c); } - // Only show folders that have items - final sections = folders - .where((f) => grouped.containsKey(f.id)) - .map((folder) { + // Show all folders (including empty) + final sections = folders.map((folder) { final expandedMap = ref.watch(_expandedFoldersProvider); final isExpanded = expandedMap[folder.id] ?? false; - final convs = grouped[folder.id]!; + final convs = grouped[folder.id] ?? const []; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildFolderHeader(folder.id, folder.name, convs.length), - if (isExpanded) ...[ + if (isExpanded && convs.isNotEmpty) ...[ const SizedBox(height: Spacing.xs), ...convs.map((c) => _buildTileFor(c, inFolder: true)), const SizedBox(height: Spacing.sm), @@ -262,13 +274,12 @@ class _ChatsDrawerState extends ConsumerState { ], ); }).toList(); - return sections; + return sections.isEmpty ? [const SizedBox.shrink()] : sections; }, loading: () => [const SizedBox.shrink()], error: (e, st) => [const SizedBox.shrink()], ), - const SizedBox(height: Spacing.md), - ], + const SizedBox(height: Spacing.md), if (regular.isNotEmpty) ...[ _buildSectionHeader('Recent', regular.length), @@ -348,26 +359,30 @@ class _ChatsDrawerState extends ConsumerState { ...pinned.map((conv) => _buildTileFor(conv)), const SizedBox(height: Spacing.md), ], - if (foldered.isNotEmpty) ...[ - ...ref.watch(foldersProvider).when( - data: (folders) { - final grouped = >{}; - for (final c in foldered) { - final id = c.folderId!; - grouped.putIfAbsent(id, () => []).add(c); - } + // Folders section (shown even if empty) + _buildFoldersSectionHeader(), + const SizedBox(height: Spacing.xs), + if (_isDragging && _draggingHasFolder) ...[ + _buildUnfileDropTarget(), + const 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 sections = folders - .where((f) => grouped.containsKey(f.id)) - .map((folder) { + final sections = folders.map((folder) { final expandedMap = ref.watch(_expandedFoldersProvider); final isExpanded = expandedMap[folder.id] ?? false; - final convs = grouped[folder.id]!; + final convs = grouped[folder.id] ?? const []; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildFolderHeader(folder.id, folder.name, convs.length), - if (isExpanded) ...[ + if (isExpanded && convs.isNotEmpty) ...[ const SizedBox(height: Spacing.xs), ...convs.map((c) => _buildTileFor(c, inFolder: true)), const SizedBox(height: Spacing.sm), @@ -375,13 +390,12 @@ class _ChatsDrawerState extends ConsumerState { ], ); }).toList(); - return sections; + return sections.isEmpty ? [const SizedBox.shrink()] : sections; }, loading: () => [const SizedBox.shrink()], error: (e, st) => [const SizedBox.shrink()], ), - const SizedBox(height: Spacing.md), - ], + const SizedBox(height: Spacing.md), if (regular.isNotEmpty) ...[ _buildSectionHeader('Recent', regular.length), const SizedBox(height: Spacing.xs), @@ -440,24 +454,225 @@ class _ChatsDrawerState extends ConsumerState { ); } + /// Header for the Folders section with a create button on the right + Widget _buildFoldersSectionHeader() { + final theme = context.conduitTheme; + return Row( + children: [ + Text( + 'Folders', + style: AppTypography.bodySmallStyle.copyWith( + fontWeight: FontWeight.w600, + color: theme.textSecondary, + letterSpacing: 0.2, + ), + ), + const Spacer(), + IconButton( + visualDensity: VisualDensity.compact, + tooltip: 'New Folder', + icon: Icon( + Platform.isIOS ? CupertinoIcons.folder_badge_plus : Icons.create_new_folder_outlined, + color: theme.iconPrimary, + ), + onPressed: _promptCreateFolder, + ), + ], + ); + } + + Future _promptCreateFolder() async { + final theme = context.conduitTheme; + final controller = TextEditingController(); + final name = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: theme.surfaceBackground, + title: Text('New Folder', style: TextStyle(color: theme.textPrimary)), + content: TextField( + controller: controller, + autofocus: true, + style: TextStyle(color: theme.inputText), + decoration: InputDecoration( + hintText: 'Folder name', + hintStyle: TextStyle(color: theme.inputPlaceholder), + enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: theme.inputBorder)), + focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: theme.buttonPrimary)), + ), + onSubmitted: (v) => Navigator.pop(ctx, controller.text.trim()), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, controller.text.trim()), + child: const Text('Create'), + ), + ], + ), + ); + + if (name == null) return; + if (name.isEmpty) return; + try { + final api = ref.read(apiServiceProvider); + if (api == null) throw Exception('No API service'); + await api.createFolder(name: name); + HapticFeedback.lightImpact(); + ref.invalidate(foldersProvider); + if (!mounted) return; + UiUtils.showMessage(context, 'Folder created'); + } catch (e) { + if (!mounted) return; + UiUtils.showMessage(context, 'Failed to create folder', isError: true); + } + } + Widget _buildFolderHeader(String folderId, String name, int count) { final theme = context.conduitTheme; final expandedMap = ref.watch(_expandedFoldersProvider); final isExpanded = expandedMap[folderId] ?? false; - return Material( - color: theme.surfaceBackground.withValues(alpha: 0.05), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - side: BorderSide(color: theme.dividerColor, width: BorderWidth.regular), - ), - child: InkWell( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - onTap: () { - final current = {...ref.read(_expandedFoldersProvider)}; - current[folderId] = !isExpanded; - ref.read(_expandedFoldersProvider.notifier).state = current; - }, - child: Padding( + final isHover = _dragHoverFolderId == folderId; + return DragTarget<_DragConversationData>( + onWillAcceptWithDetails: (details) { + setState(() => _dragHoverFolderId = folderId); + return true; + }, + onLeave: (_) => setState(() => _dragHoverFolderId = null), + onAcceptWithDetails: (details) async { + setState(() { + _dragHoverFolderId = null; + _isDragging = false; + }); + try { + final api = ref.read(apiServiceProvider); + if (api == null) throw Exception('No API service'); + await api.moveConversationToFolder(details.data.id, folderId); + HapticFeedback.selectionClick(); + ref.invalidate(conversationsProvider); + ref.invalidate(foldersProvider); + if (mounted) { + UiUtils.showMessage(context, 'Moved "${details.data.title}" to "$name"'); + } + } catch (_) { + if (mounted) { + UiUtils.showMessage(context, 'Failed to move chat', isError: true); + } + } + }, + builder: (context, candidateData, rejectedData) { + return Material( + color: isHover + ? theme.buttonPrimary.withValues(alpha: 0.08) + : theme.surfaceBackground.withValues(alpha: 0.05), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + side: BorderSide( + color: isHover + ? theme.buttonPrimary.withValues(alpha: 0.6) + : theme.dividerColor, + width: BorderWidth.regular, + ), + ), + child: InkWell( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + onTap: () { + final current = {...ref.read(_expandedFoldersProvider)}; + current[folderId] = !isExpanded; + ref.read(_expandedFoldersProvider.notifier).state = current; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + child: Row( + children: [ + Icon( + isExpanded + ? (Platform.isIOS ? CupertinoIcons.folder_open : Icons.folder_open) + : (Platform.isIOS ? CupertinoIcons.folder : Icons.folder), + color: theme.iconPrimary, + ), + const SizedBox(width: Spacing.sm), + Expanded( + child: Text( + name, + style: AppTypography.bodyLargeStyle.copyWith( + color: theme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + ), + Text( + '$count', + style: AppTypography.bodySmallStyle.copyWith( + color: theme.textSecondary, + ), + ), + const SizedBox(width: Spacing.xs), + Icon( + isExpanded + ? (Platform.isIOS ? CupertinoIcons.chevron_up : Icons.expand_less) + : (Platform.isIOS ? CupertinoIcons.chevron_down : Icons.expand_more), + color: theme.iconSecondary, + ) + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildUnfileDropTarget() { + final theme = context.conduitTheme; + final isHover = _dragHoverFolderId == '__UNFILE__'; + return DragTarget<_DragConversationData>( + onWillAcceptWithDetails: (details) { + setState(() => _dragHoverFolderId = '__UNFILE__'); + return true; + }, + onLeave: (_) => setState(() => _dragHoverFolderId = null), + onAcceptWithDetails: (details) async { + setState(() { + _dragHoverFolderId = null; + _isDragging = false; + }); + try { + final api = ref.read(apiServiceProvider); + if (api == null) throw Exception('No API service'); + await api.moveConversationToFolder(details.data.id, null); + HapticFeedback.selectionClick(); + ref.invalidate(conversationsProvider); + ref.invalidate(foldersProvider); + if (mounted) { + UiUtils.showMessage(context, 'Removed "${details.data.title}" from folder'); + } + } catch (_) { + if (mounted) { + UiUtils.showMessage(context, 'Failed to move chat', isError: true); + } + } + }, + builder: (context, candidate, rejected) { + return AnimatedContainer( + duration: const Duration(milliseconds: 120), + decoration: BoxDecoration( + color: isHover + ? theme.buttonPrimary.withValues(alpha: 0.08) + : theme.surfaceBackground.withValues(alpha: 0.03), + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: isHover + ? theme.buttonPrimary.withValues(alpha: 0.6) + : theme.dividerColor, + width: BorderWidth.regular, + ), + ), padding: const EdgeInsets.symmetric( horizontal: Spacing.md, vertical: Spacing.sm, @@ -465,58 +680,102 @@ class _ChatsDrawerState extends ConsumerState { child: Row( children: [ Icon( - isExpanded - ? (Platform.isIOS ? CupertinoIcons.folder_open : Icons.folder_open) - : (Platform.isIOS ? CupertinoIcons.folder : Icons.folder), + Platform.isIOS + ? CupertinoIcons.folder_badge_minus + : Icons.folder_off_outlined, color: theme.iconPrimary, ), const SizedBox(width: Spacing.sm), Expanded( child: Text( - name, - style: AppTypography.bodyLargeStyle.copyWith( + 'Drop here to remove from folder', + style: AppTypography.bodyMediumStyle.copyWith( color: theme.textPrimary, fontWeight: FontWeight.w600, ), ), ), - Text( - '$count', - style: AppTypography.bodySmallStyle.copyWith( - color: theme.textSecondary, - ), - ), - const SizedBox(width: Spacing.xs), - Icon( - isExpanded - ? (Platform.isIOS ? CupertinoIcons.chevron_up : Icons.expand_less) - : (Platform.isIOS ? CupertinoIcons.chevron_down : Icons.expand_more), - color: theme.iconSecondary, - ) ], ), - ), - ), + ); + }, ); } Widget _buildTileFor(dynamic conv, {bool inFolder = false}) { final isActive = ref.watch(activeConversationProvider)?.id == conv.id; + final title = conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat'); + final tile = _ConversationTile( + title: title, + pinned: conv.pinned == true, + selected: isActive, + onTap: _isLoadingConversation ? null : () => _selectConversation(context, conv.id), + // Remove long-press context menu to avoid conflict with drag gesture + onLongPress: null, + onMorePressed: () { + HapticFeedback.selectionClick(); + _showConversationContextMenu(context, conv); + }, + ); + return Padding( padding: EdgeInsets.only(bottom: Spacing.xs, left: inFolder ? Spacing.md : 0), - child: _ConversationTile( - title: conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat'), - pinned: conv.pinned == true, - selected: isActive, - onTap: _isLoadingConversation ? null : () => _selectConversation(context, conv.id), - onLongPress: () { - HapticFeedback.selectionClick(); - _showConversationContextMenu(context, conv); - }, - onMorePressed: () { - HapticFeedback.selectionClick(); - _showConversationContextMenu(context, conv); + child: LongPressDraggable<_DragConversationData>( + data: _DragConversationData(id: conv.id, title: title), + dragAnchorStrategy: pointerDragAnchorStrategy, + feedback: Material( + color: Colors.transparent, + elevation: 6, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + child: Opacity( + opacity: 0.9, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: Theme.of(context).dividerColor, + width: BorderWidth.regular, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Platform.isIOS + ? CupertinoIcons.chat_bubble_2 + : Icons.chat_bubble_outline, + size: IconSize.md, + ), + const SizedBox(width: Spacing.xs), + Text(title, maxLines: 1, overflow: TextOverflow.ellipsis), + ], + ), + ), + ), + ), + childWhenDragging: Opacity( + opacity: 0.5, + child: IgnorePointer(child: tile), + ), + onDragStarted: () { + HapticFeedback.lightImpact(); + final hasFolder = (conv.folderId != null && (conv.folderId as String).isNotEmpty); + setState(() { + _isDragging = true; + _draggingHasFolder = hasFolder; + }); }, + onDragEnd: (_) => setState(() { + _dragHoverFolderId = null; + _isDragging = false; + _draggingHasFolder = false; + }), + child: tile, ), ); } @@ -632,6 +891,7 @@ class _ChatsDrawerState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ if (user != null) ...[ + const SizedBox(height: Spacing.sm), Container( padding: const EdgeInsets.all(Spacing.sm), decoration: BoxDecoration( @@ -693,37 +953,7 @@ class _ChatsDrawerState extends ConsumerState { ], ), ), - const SizedBox(height: Spacing.sm), ], - Row( - children: [ - Expanded( - child: _BottomAction( - icon: Platform.isIOS ? CupertinoIcons.doc : Icons.description_outlined, - label: 'Files', - onTap: () { - Navigator.of(context).maybePop(); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const FilesPage()), - ); - }, - ), - ), - const SizedBox(width: Spacing.sm), - Expanded( - child: _BottomAction( - icon: Platform.isIOS ? CupertinoIcons.person : Icons.person_outline, - label: 'Profile', - onTap: () { - Navigator.of(context).maybePop(); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const ProfilePage()), - ); - }, - ), - ), - ], - ), ], ), ), @@ -918,6 +1148,12 @@ class _ChatsDrawerState extends ConsumerState { } } +class _DragConversationData { + final String id; + final String title; + const _DragConversationData({required this.id, required this.title}); +} + class _ConversationTile extends StatelessWidget { final String title; final bool pinned; @@ -960,14 +1196,6 @@ class _ConversationTile extends StatelessWidget { ), child: Row( children: [ - Icon( - pinned - ? (Platform.isIOS ? CupertinoIcons.pin_fill : Icons.push_pin) - : (Platform.isIOS ? CupertinoIcons.chat_bubble : Icons.chat_bubble_outline_rounded), - color: selected ? theme.buttonPrimary : theme.iconSecondary, - size: IconSize.md, - ), - const SizedBox(width: Spacing.sm), Expanded( child: Text( title, @@ -1001,46 +1229,4 @@ class _ConversationTile extends StatelessWidget { } } -class _BottomAction extends StatelessWidget { - final IconData icon; - final String label; - final VoidCallback onTap; - - const _BottomAction({ - required this.icon, - required this.label, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final theme = context.conduitTheme; - return Material( - color: theme.surfaceBackground.withValues(alpha: 0.04), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - side: BorderSide(color: theme.dividerColor, width: BorderWidth.regular), - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: Spacing.sm), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: theme.iconPrimary, size: IconSize.lg), - const SizedBox(height: 6), - Text( - label, - style: AppTypography.bodySmallStyle.copyWith( - color: theme.textPrimary, - ), - ), - ], - ), - ), - ), - ); - } -} +// Bottom quick actions widget removed as design now shows only profile card diff --git a/lib/features/onboarding/views/onboarding_sheet.dart b/lib/features/onboarding/views/onboarding_sheet.dart index 35f25b1..17ad34e 100644 --- a/lib/features/onboarding/views/onboarding_sheet.dart +++ b/lib/features/onboarding/views/onboarding_sheet.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import '../../../shared/theme/theme_extensions.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import '../../../shared/widgets/sheet_handle.dart'; class OnboardingSheet extends StatefulWidget { const OnboardingSheet({super.key}); @@ -43,10 +44,10 @@ class _OnboardingSheetState extends State { _OnboardingPage( title: 'Quick actions', subtitle: - 'Long‑press the top‑left menu to open shortcuts like New Chat, Files, and Profile.', + 'Use the top‑left menu to open the chats list and navigation.', icon: CupertinoIcons.line_horizontal_3, bullets: [ - 'Tap to open chats list; long‑press for Quick Actions', + 'Tap the menu to open the chats list and navigation', 'Jump instantly to New Chat, Files, or Profile', ], ), @@ -81,16 +82,8 @@ class _OnboardingSheetState extends State { padding: const EdgeInsets.all(Spacing.lg), child: Column( children: [ - // Handle bar - Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: Spacing.md), - decoration: BoxDecoration( - color: context.conduitTheme.dividerColor, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), + // Handle bar (standardized) + const SheetHandle(), Expanded( child: PageView.builder( diff --git a/lib/features/profile/views/profile_page.dart b/lib/features/profile/views/profile_page.dart index bd829ce..3e11867 100644 --- a/lib/features/profile/views/profile_page.dart +++ b/lib/features/profile/views/profile_page.dart @@ -10,6 +10,7 @@ import '../../../core/widgets/error_boundary.dart'; import '../../../shared/widgets/improved_loading_states.dart'; import '../../../shared/utils/ui_utils.dart'; +import '../../../shared/widgets/sheet_handle.dart'; import '../../../shared/widgets/conduit_components.dart'; import '../../../core/providers/app_providers.dart'; import '../../auth/providers/unified_auth_providers.dart'; @@ -49,17 +50,12 @@ class ProfilePage extends ConsumerWidget { ), toolbarHeight: kToolbarHeight, titleSpacing: 0.0, - title: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'You', - style: context.conduitTheme.headingSmall?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ], + title: Text( + 'You', + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), ), centerTitle: true, ), @@ -114,7 +110,7 @@ class ProfilePage extends ConsumerWidget { ), title: Text( 'You', - style: context.conduitTheme.headingSmall?.copyWith( + style: AppTypography.headlineSmallStyle.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, ), @@ -144,7 +140,7 @@ class ProfilePage extends ConsumerWidget { ), title: Text( 'You', - style: context.conduitTheme.headingSmall?.copyWith( + style: AppTypography.headlineSmallStyle.copyWith( color: context.conduitTheme.textPrimary, fontWeight: FontWeight.w600, ), @@ -195,56 +191,14 @@ class ProfilePage extends ConsumerWidget { fontWeight: FontWeight.w600, ), ), - const SizedBox(height: Spacing.xs), + const SizedBox(height: Spacing.sm), Text( user?.email ?? 'No email', style: context.conduitTheme.bodyMedium?.copyWith( color: context.conduitTheme.textSecondary, ), ), - const SizedBox(height: Spacing.sm), - // Enhanced status badge with better styling - Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), - decoration: BoxDecoration( - color: context.conduitTheme.success.withValues( - alpha: Alpha.badgeBackground, - ), - borderRadius: BorderRadius.circular( - AppBorderRadius.badge, - ), - border: Border.all( - color: context.conduitTheme.success.withValues( - alpha: Alpha.avatarBorder, - ), - width: BorderWidth.thin, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: context.conduitTheme.success, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: Spacing.xs), - Text( - 'Active', - style: context.conduitTheme.label?.copyWith( - color: context.conduitTheme.success, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), + // Status badge removed per design update ], ), ), @@ -789,55 +743,11 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe padding: const EdgeInsets.all(Spacing.bottomSheetPadding), child: Column( children: [ - // Handle bar - Container( - margin: const EdgeInsets.only( - top: Spacing.sm, - bottom: Spacing.md, - ), - width: Spacing.xxl, - height: Spacing.xs, - decoration: BoxDecoration( - color: context.conduitTheme.textPrimary.withValues(alpha: Alpha.medium), - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - ), - ), + // Handle bar (standardized) + const SheetHandle(), - // Header - Padding( - padding: const EdgeInsets.only(bottom: Spacing.md), - child: Row( - children: [ - Icon( - Platform.isIOS ? CupertinoIcons.cube : Icons.psychology, - color: context.conduitTheme.iconPrimary, - ), - const SizedBox(width: Spacing.sm), - Expanded( - child: Text( - 'Default Model', - style: context.conduitTheme.headingMedium?.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - TextButton( - onPressed: () { - HapticFeedback.lightImpact(); - Navigator.pop(context, _selectedModelId); - }, - child: Text( - 'Save', - style: context.conduitTheme.bodyMedium?.copyWith( - color: context.conduitTheme.buttonPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), + // Header removed (no icon/title or save button) + const SizedBox(height: Spacing.md), // Search field Padding( @@ -963,10 +873,11 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe isSelected: isSelected, isAutoSelect: isAutoSelect, onTap: () { - HapticFeedback.selectionClick(); - setState(() { - _selectedModelId = isAutoSelect ? 'auto-select' : model.id; - }); + HapticFeedback.lightImpact(); + final selectedId = + isAutoSelect ? 'auto-select' : model.id; + // Return selection immediately; caller handles persisting + Navigator.pop(context, selectedId); }, ); }, diff --git a/lib/features/tools/widgets/unified_tools_modal.dart b/lib/features/tools/widgets/unified_tools_modal.dart index ade575b..ac3dc85 100644 --- a/lib/features/tools/widgets/unified_tools_modal.dart +++ b/lib/features/tools/widgets/unified_tools_modal.dart @@ -8,6 +8,7 @@ import '../../../shared/theme/theme_extensions.dart'; import '../../chat/providers/chat_providers.dart'; import '../../../core/providers/app_providers.dart'; import '../providers/tools_providers.dart'; +import '../../../shared/widgets/sheet_handle.dart'; class UnifiedToolsModal extends ConsumerStatefulWidget { const UnifiedToolsModal({super.key}); @@ -47,17 +48,8 @@ class _UnifiedToolsModalState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Handle bar - Center( - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: theme.textPrimary.withValues(alpha: Alpha.medium), - borderRadius: BorderRadius.circular(2), - ), - ), - ), + // Handle bar (standardized) + const SheetHandle(), const SizedBox(height: Spacing.md), // Removed header for minimal, focused layout diff --git a/lib/shared/widgets/sheet_handle.dart b/lib/shared/widgets/sheet_handle.dart new file mode 100644 index 0000000..ecd973c --- /dev/null +++ b/lib/shared/widgets/sheet_handle.dart @@ -0,0 +1,23 @@ +import 'package:flutter/widgets.dart'; +import '../theme/theme_extensions.dart'; + +class SheetHandle extends StatelessWidget { + final EdgeInsetsGeometry? margin; + const SheetHandle({super.key, this.margin}); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + margin: margin ?? const EdgeInsets.only(top: Spacing.sm, bottom: Spacing.md), + width: 40, + height: 4, + decoration: BoxDecoration( + color: context.conduitTheme.textPrimary.withValues(alpha: Alpha.medium), + borderRadius: BorderRadius.circular(AppBorderRadius.xs), + ), + ), + ); + } +} +