refactor: ui/ux refinements
This commit is contained in:
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../features/chat/views/chat_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 '../../features/profile/views/profile_page.dart';
|
||||||
|
|
||||||
/// Service for handling deep links and navigation routing
|
/// 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
|
/// In single-screen mode, workspace/profile deep links route via navigator
|
||||||
static void navigateToFiles(BuildContext context) {
|
static void navigateToWorkspace(BuildContext context) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const FilesPage()),
|
MaterialPageRoute(builder: (context) => const WorkspacePage()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +37,12 @@ class DeepLinkService {
|
|||||||
case '/chat':
|
case '/chat':
|
||||||
case '/main/chat':
|
case '/main/chat':
|
||||||
return '/chat';
|
return '/chat';
|
||||||
case '/files':
|
// Support both new and legacy paths for workspace
|
||||||
case '/main/files':
|
case '/workspace':
|
||||||
return '/files';
|
case '/main/workspace':
|
||||||
|
case '/files': // legacy
|
||||||
|
case '/main/files': // legacy
|
||||||
|
return '/workspace';
|
||||||
case '/profile':
|
case '/profile':
|
||||||
case '/main/profile':
|
case '/main/profile':
|
||||||
return '/profile';
|
return '/profile';
|
||||||
@@ -52,8 +55,8 @@ class DeepLinkService {
|
|||||||
static Widget handleDeepLink(String route) {
|
static Widget handleDeepLink(String route) {
|
||||||
final path = parsePath(route);
|
final path = parsePath(route);
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case '/files':
|
case '/workspace':
|
||||||
return const FilesPage();
|
return const WorkspacePage();
|
||||||
case '/profile':
|
case '/profile':
|
||||||
return const ProfilePage();
|
return const ProfilePage();
|
||||||
case '/chat':
|
case '/chat':
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
// ThemedDialogs handles theming; no direct use of extensions here
|
// ThemedDialogs handles theming; no direct use of extensions here
|
||||||
import '../../features/auth/views/connect_signin_page.dart';
|
import '../../features/auth/views/connect_signin_page.dart';
|
||||||
import '../../features/chat/views/chat_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 '../../features/profile/views/profile_page.dart';
|
||||||
import '../../shared/widgets/themed_dialogs.dart';
|
import '../../shared/widgets/themed_dialogs.dart';
|
||||||
|
|
||||||
@@ -130,8 +130,8 @@ class NavigationService {
|
|||||||
page = const ConnectAndSignInPage();
|
page = const ConnectAndSignInPage();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Routes.files:
|
case Routes.workspace:
|
||||||
page = const FilesPage();
|
page = const WorkspacePage();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// chats list route removed (replaced by drawer)
|
// chats list route removed (replaced by drawer)
|
||||||
@@ -154,5 +154,5 @@ class Routes {
|
|||||||
static const String login = '/login';
|
static const String login = '/login';
|
||||||
static const String profile = '/profile';
|
static const String profile = '/profile';
|
||||||
static const String serverConnection = '/server-connection';
|
static const String serverConnection = '/server-connection';
|
||||||
static const String files = '/files';
|
static const String workspace = '/workspace';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import '../widgets/assistant_message_widget.dart' as assistant;
|
|||||||
import '../widgets/file_attachment_widget.dart';
|
import '../widgets/file_attachment_widget.dart';
|
||||||
import '../services/voice_input_service.dart';
|
import '../services/voice_input_service.dart';
|
||||||
import '../services/file_attachment_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 '../../tools/providers/tools_providers.dart';
|
||||||
import '../../navigation/widgets/chats_drawer.dart';
|
import '../../navigation/widgets/chats_drawer.dart';
|
||||||
import '../../../shared/widgets/offline_indicator.dart';
|
import '../../../shared/widgets/offline_indicator.dart';
|
||||||
@@ -30,6 +28,7 @@ import '../../../shared/widgets/loading_states.dart';
|
|||||||
import 'chat_page_helpers.dart';
|
import 'chat_page_helpers.dart';
|
||||||
import '../../../shared/widgets/themed_dialogs.dart';
|
import '../../../shared/widgets/themed_dialogs.dart';
|
||||||
import '../../onboarding/views/onboarding_sheet.dart';
|
import '../../onboarding/views/onboarding_sheet.dart';
|
||||||
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
|
|
||||||
class ChatPage extends ConsumerStatefulWidget {
|
class ChatPage extends ConsumerStatefulWidget {
|
||||||
const ChatPage({super.key});
|
const ChatPage({super.key});
|
||||||
@@ -464,133 +463,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
|
|
||||||
// Replaced bottom-sheet chat list with left drawer (see ChatsDrawer)
|
// 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() {
|
void _onScroll() {
|
||||||
if (!_scrollController.hasClients) return;
|
if (!_scrollController.hasClients) return;
|
||||||
|
|
||||||
@@ -1126,10 +998,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
// Open left drawer instead of bottom sheet
|
// Open left drawer instead of bottom sheet
|
||||||
Scaffold.of(ctx).openDrawer();
|
Scaffold.of(ctx).openDrawer();
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
|
||||||
HapticFeedback.mediumImpact();
|
|
||||||
_showQuickAccessMenu();
|
|
||||||
},
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(4.0),
|
padding: const EdgeInsets.all(4.0),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
@@ -1635,19 +1503,8 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
|||||||
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Search field
|
// Search field
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../../../shared/theme/app_theme.dart';
|
import '../../../shared/theme/app_theme.dart';
|
||||||
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -207,16 +208,8 @@ class CopyOptionsSheet extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
margin: const EdgeInsets.only(top: Spacing.sm),
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: Spacing.lg - Spacing.xs),
|
const SizedBox(height: Spacing.lg - Spacing.xs),
|
||||||
|
|
||||||
@@ -340,16 +333,8 @@ class ExportOptionsSheet extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
margin: const EdgeInsets.only(top: Spacing.sm),
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: Spacing.lg - Spacing.xs),
|
const SizedBox(height: Spacing.lg - Spacing.xs),
|
||||||
|
|
||||||
@@ -465,16 +450,8 @@ class MoreOptionsSheet extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: Spacing.lg - Spacing.xs),
|
const SizedBox(height: Spacing.lg - Spacing.xs),
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../../../shared/theme/theme_extensions.dart';
|
import '../../../shared/theme/theme_extensions.dart';
|
||||||
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
@@ -615,17 +616,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.conduitTheme.textPrimary.withValues(
|
|
||||||
alpha: Alpha.medium,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.lg),
|
const SizedBox(height: Spacing.lg),
|
||||||
|
|
||||||
// Options grid
|
// Options grid
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import '../../../core/services/navigation_service.dart';
|
|||||||
import '../../../shared/widgets/improved_loading_states.dart';
|
import '../../../shared/widgets/improved_loading_states.dart';
|
||||||
|
|
||||||
import '../../../shared/utils/ui_utils.dart';
|
import '../../../shared/utils/ui_utils.dart';
|
||||||
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
|
|
||||||
/// Files page for managing documents and uploads
|
/// Files page for managing documents and uploads
|
||||||
class FilesPage extends ConsumerStatefulWidget {
|
class WorkspacePage extends ConsumerStatefulWidget {
|
||||||
const FilesPage({super.key});
|
const WorkspacePage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<FilesPage> createState() => _FilesPageState();
|
ConsumerState<WorkspacePage> createState() => _WorkspacePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FilesPageState extends ConsumerState<FilesPage>
|
class _WorkspacePageState extends ConsumerState<WorkspacePage>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
int _selectedTab = 0;
|
int _selectedTab = 0;
|
||||||
late AnimationController _tabAnimationController;
|
late AnimationController _tabAnimationController;
|
||||||
@@ -109,17 +110,12 @@ class _FilesPageState extends ConsumerState<FilesPage>
|
|||||||
onPressed: () => NavigationService.goBack(),
|
onPressed: () => NavigationService.goBack(),
|
||||||
tooltip: 'Back',
|
tooltip: 'Back',
|
||||||
),
|
),
|
||||||
title: Row(
|
title: Text(
|
||||||
mainAxisSize: MainAxisSize.min,
|
'Workspace',
|
||||||
children: [
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
Text(
|
color: context.conduitTheme.textPrimary,
|
||||||
'Files',
|
fontWeight: FontWeight.w600,
|
||||||
style: context.conduitTheme.headingSmall?.copyWith(
|
),
|
||||||
color: context.conduitTheme.textPrimary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
actions: [
|
actions: [
|
||||||
@@ -290,16 +286,8 @@ class _FilesPageState extends ConsumerState<FilesPage>
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Enhanced handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
margin: const EdgeInsets.symmetric(vertical: Spacing.sm),
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Header with enhanced typography
|
// Header with enhanced typography
|
||||||
Padding(
|
Padding(
|
||||||
@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../core/auth/auth_state_manager.dart';
|
|
||||||
import '../../../shared/theme/theme_extensions.dart';
|
import '../../../shared/theme/theme_extensions.dart';
|
||||||
import '../../chat/providers/chat_providers.dart' as chat;
|
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 '../../profile/views/profile_page.dart';
|
||||||
import '../../../shared/utils/ui_utils.dart';
|
import '../../../shared/utils/ui_utils.dart';
|
||||||
|
import '../../../core/auth/auth_state_manager.dart';
|
||||||
|
|
||||||
class ChatsDrawer extends ConsumerStatefulWidget {
|
class ChatsDrawer extends ConsumerStatefulWidget {
|
||||||
const ChatsDrawer({super.key});
|
const ChatsDrawer({super.key});
|
||||||
@@ -27,6 +27,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
Timer? _debounce;
|
Timer? _debounce;
|
||||||
String _query = '';
|
String _query = '';
|
||||||
bool _isLoadingConversation = false;
|
bool _isLoadingConversation = false;
|
||||||
|
String? _dragHoverFolderId;
|
||||||
|
bool _isDragging = false;
|
||||||
|
bool _draggingHasFolder = false;
|
||||||
|
|
||||||
// UI state providers for sections
|
// UI state providers for sections
|
||||||
static final _showArchivedProvider = StateProvider<bool>((ref) => false);
|
static final _showArchivedProvider = StateProvider<bool>((ref) => false);
|
||||||
@@ -49,8 +52,15 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Bottom section now only shows navigation actions
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@@ -80,35 +90,33 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0),
|
padding: const EdgeInsets.fromLTRB(Spacing.md, Spacing.md, Spacing.md, 0),
|
||||||
child: Row(
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
// Centered title (no leading icon)
|
||||||
Platform.isIOS
|
Text(
|
||||||
? CupertinoIcons.chat_bubble_2
|
'Chats',
|
||||||
: Icons.chat_bubble_outline_rounded,
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
color: theme.iconPrimary,
|
color: theme.textPrimary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.sm),
|
// Right-aligned new chat action
|
||||||
Expanded(
|
Positioned(
|
||||||
child: Text(
|
right: 0,
|
||||||
'Chats',
|
child: IconButton(
|
||||||
style: AppTypography.headlineSmallStyle.copyWith(
|
icon: Icon(
|
||||||
color: theme.textPrimary,
|
Platform.isIOS ? CupertinoIcons.plus : Icons.add_rounded,
|
||||||
fontWeight: FontWeight.w600,
|
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<ChatsDrawer> {
|
|||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
],
|
],
|
||||||
|
|
||||||
if (foldered.isNotEmpty) ...[
|
// Folders section (shown even if empty)
|
||||||
...ref.watch(foldersProvider).when(
|
_buildFoldersSectionHeader(),
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
if (_isDragging && _draggingHasFolder) ...[
|
||||||
|
_buildUnfileDropTarget(),
|
||||||
|
const SizedBox(height: Spacing.sm),
|
||||||
|
],
|
||||||
|
...ref.watch(foldersProvider).when(
|
||||||
data: (folders) {
|
data: (folders) {
|
||||||
final grouped = <String, List<dynamic>>{};
|
final grouped = <String, List<dynamic>>{};
|
||||||
for (final c in foldered) {
|
for (final c in foldered) {
|
||||||
@@ -243,18 +257,16 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
grouped.putIfAbsent(id, () => []).add(c);
|
grouped.putIfAbsent(id, () => []).add(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show folders that have items
|
// Show all folders (including empty)
|
||||||
final sections = folders
|
final sections = folders.map((folder) {
|
||||||
.where((f) => grouped.containsKey(f.id))
|
|
||||||
.map((folder) {
|
|
||||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
final expandedMap = ref.watch(_expandedFoldersProvider);
|
||||||
final isExpanded = expandedMap[folder.id] ?? false;
|
final isExpanded = expandedMap[folder.id] ?? false;
|
||||||
final convs = grouped[folder.id]!;
|
final convs = grouped[folder.id] ?? const <dynamic>[];
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildFolderHeader(folder.id, folder.name, convs.length),
|
_buildFolderHeader(folder.id, folder.name, convs.length),
|
||||||
if (isExpanded) ...[
|
if (isExpanded && convs.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...convs.map((c) => _buildTileFor(c, inFolder: true)),
|
...convs.map((c) => _buildTileFor(c, inFolder: true)),
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
@@ -262,13 +274,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
return sections;
|
return sections.isEmpty ? [const SizedBox.shrink()] : sections;
|
||||||
},
|
},
|
||||||
loading: () => [const SizedBox.shrink()],
|
loading: () => [const SizedBox.shrink()],
|
||||||
error: (e, st) => [const SizedBox.shrink()],
|
error: (e, st) => [const SizedBox.shrink()],
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
],
|
|
||||||
|
|
||||||
if (regular.isNotEmpty) ...[
|
if (regular.isNotEmpty) ...[
|
||||||
_buildSectionHeader('Recent', regular.length),
|
_buildSectionHeader('Recent', regular.length),
|
||||||
@@ -348,26 +359,30 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
...pinned.map((conv) => _buildTileFor(conv)),
|
...pinned.map((conv) => _buildTileFor(conv)),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
],
|
],
|
||||||
if (foldered.isNotEmpty) ...[
|
// Folders section (shown even if empty)
|
||||||
...ref.watch(foldersProvider).when(
|
_buildFoldersSectionHeader(),
|
||||||
data: (folders) {
|
const SizedBox(height: Spacing.xs),
|
||||||
final grouped = <String, List<dynamic>>{};
|
if (_isDragging && _draggingHasFolder) ...[
|
||||||
for (final c in foldered) {
|
_buildUnfileDropTarget(),
|
||||||
final id = c.folderId!;
|
const SizedBox(height: Spacing.sm),
|
||||||
grouped.putIfAbsent(id, () => []).add(c);
|
],
|
||||||
}
|
...ref.watch(foldersProvider).when(
|
||||||
|
data: (folders) {
|
||||||
|
final grouped = <String, List<dynamic>>{};
|
||||||
|
for (final c in foldered) {
|
||||||
|
final id = c.folderId!;
|
||||||
|
grouped.putIfAbsent(id, () => []).add(c);
|
||||||
|
}
|
||||||
|
|
||||||
final sections = folders
|
final sections = folders.map((folder) {
|
||||||
.where((f) => grouped.containsKey(f.id))
|
|
||||||
.map((folder) {
|
|
||||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
final expandedMap = ref.watch(_expandedFoldersProvider);
|
||||||
final isExpanded = expandedMap[folder.id] ?? false;
|
final isExpanded = expandedMap[folder.id] ?? false;
|
||||||
final convs = grouped[folder.id]!;
|
final convs = grouped[folder.id] ?? const <dynamic>[];
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildFolderHeader(folder.id, folder.name, convs.length),
|
_buildFolderHeader(folder.id, folder.name, convs.length),
|
||||||
if (isExpanded) ...[
|
if (isExpanded && convs.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
...convs.map((c) => _buildTileFor(c, inFolder: true)),
|
...convs.map((c) => _buildTileFor(c, inFolder: true)),
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
@@ -375,13 +390,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
return sections;
|
return sections.isEmpty ? [const SizedBox.shrink()] : sections;
|
||||||
},
|
},
|
||||||
loading: () => [const SizedBox.shrink()],
|
loading: () => [const SizedBox.shrink()],
|
||||||
error: (e, st) => [const SizedBox.shrink()],
|
error: (e, st) => [const SizedBox.shrink()],
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
],
|
|
||||||
if (regular.isNotEmpty) ...[
|
if (regular.isNotEmpty) ...[
|
||||||
_buildSectionHeader('Recent', regular.length),
|
_buildSectionHeader('Recent', regular.length),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
@@ -440,24 +454,225 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<void> _promptCreateFolder() async {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
|
final controller = TextEditingController();
|
||||||
|
final name = await showDialog<String>(
|
||||||
|
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) {
|
Widget _buildFolderHeader(String folderId, String name, int count) {
|
||||||
final theme = context.conduitTheme;
|
final theme = context.conduitTheme;
|
||||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
final expandedMap = ref.watch(_expandedFoldersProvider);
|
||||||
final isExpanded = expandedMap[folderId] ?? false;
|
final isExpanded = expandedMap[folderId] ?? false;
|
||||||
return Material(
|
final isHover = _dragHoverFolderId == folderId;
|
||||||
color: theme.surfaceBackground.withValues(alpha: 0.05),
|
return DragTarget<_DragConversationData>(
|
||||||
shape: RoundedRectangleBorder(
|
onWillAcceptWithDetails: (details) {
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
setState(() => _dragHoverFolderId = folderId);
|
||||||
side: BorderSide(color: theme.dividerColor, width: BorderWidth.regular),
|
return true;
|
||||||
),
|
},
|
||||||
child: InkWell(
|
onLeave: (_) => setState(() => _dragHoverFolderId = null),
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
onAcceptWithDetails: (details) async {
|
||||||
onTap: () {
|
setState(() {
|
||||||
final current = {...ref.read(_expandedFoldersProvider)};
|
_dragHoverFolderId = null;
|
||||||
current[folderId] = !isExpanded;
|
_isDragging = false;
|
||||||
ref.read(_expandedFoldersProvider.notifier).state = current;
|
});
|
||||||
},
|
try {
|
||||||
child: Padding(
|
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(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: Spacing.md,
|
horizontal: Spacing.md,
|
||||||
vertical: Spacing.sm,
|
vertical: Spacing.sm,
|
||||||
@@ -465,58 +680,102 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isExpanded
|
Platform.isIOS
|
||||||
? (Platform.isIOS ? CupertinoIcons.folder_open : Icons.folder_open)
|
? CupertinoIcons.folder_badge_minus
|
||||||
: (Platform.isIOS ? CupertinoIcons.folder : Icons.folder),
|
: Icons.folder_off_outlined,
|
||||||
color: theme.iconPrimary,
|
color: theme.iconPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.sm),
|
const SizedBox(width: Spacing.sm),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
name,
|
'Drop here to remove from folder',
|
||||||
style: AppTypography.bodyLargeStyle.copyWith(
|
style: AppTypography.bodyMediumStyle.copyWith(
|
||||||
color: theme.textPrimary,
|
color: theme.textPrimary,
|
||||||
fontWeight: FontWeight.w600,
|
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}) {
|
Widget _buildTileFor(dynamic conv, {bool inFolder = false}) {
|
||||||
final isActive = ref.watch(activeConversationProvider)?.id == conv.id;
|
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(
|
return Padding(
|
||||||
padding: EdgeInsets.only(bottom: Spacing.xs, left: inFolder ? Spacing.md : 0),
|
padding: EdgeInsets.only(bottom: Spacing.xs, left: inFolder ? Spacing.md : 0),
|
||||||
child: _ConversationTile(
|
child: LongPressDraggable<_DragConversationData>(
|
||||||
title: conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat'),
|
data: _DragConversationData(id: conv.id, title: title),
|
||||||
pinned: conv.pinned == true,
|
dragAnchorStrategy: pointerDragAnchorStrategy,
|
||||||
selected: isActive,
|
feedback: Material(
|
||||||
onTap: _isLoadingConversation ? null : () => _selectConversation(context, conv.id),
|
color: Colors.transparent,
|
||||||
onLongPress: () {
|
elevation: 6,
|
||||||
HapticFeedback.selectionClick();
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
_showConversationContextMenu(context, conv);
|
child: Opacity(
|
||||||
},
|
opacity: 0.9,
|
||||||
onMorePressed: () {
|
child: Container(
|
||||||
HapticFeedback.selectionClick();
|
padding: const EdgeInsets.symmetric(
|
||||||
_showConversationContextMenu(context, conv);
|
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<ChatsDrawer> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (user != null) ...[
|
if (user != null) ...[
|
||||||
|
const SizedBox(height: Spacing.sm),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(Spacing.sm),
|
padding: const EdgeInsets.all(Spacing.sm),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -693,37 +953,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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<ChatsDrawer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _DragConversationData {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
const _DragConversationData({required this.id, required this.title});
|
||||||
|
}
|
||||||
|
|
||||||
class _ConversationTile extends StatelessWidget {
|
class _ConversationTile extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final bool pinned;
|
final bool pinned;
|
||||||
@@ -960,14 +1196,6 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
@@ -1001,46 +1229,4 @@ class _ConversationTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BottomAction extends StatelessWidget {
|
// Bottom quick actions widget removed as design now shows only profile card
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import '../../../shared/theme/theme_extensions.dart';
|
import '../../../shared/theme/theme_extensions.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
|
|
||||||
class OnboardingSheet extends StatefulWidget {
|
class OnboardingSheet extends StatefulWidget {
|
||||||
const OnboardingSheet({super.key});
|
const OnboardingSheet({super.key});
|
||||||
@@ -43,10 +44,10 @@ class _OnboardingSheetState extends State<OnboardingSheet> {
|
|||||||
_OnboardingPage(
|
_OnboardingPage(
|
||||||
title: 'Quick actions',
|
title: 'Quick actions',
|
||||||
subtitle:
|
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,
|
icon: CupertinoIcons.line_horizontal_3,
|
||||||
bullets: [
|
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',
|
'Jump instantly to New Chat, Files, or Profile',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -81,16 +82,8 @@ class _OnboardingSheetState extends State<OnboardingSheet> {
|
|||||||
padding: const EdgeInsets.all(Spacing.lg),
|
padding: const EdgeInsets.all(Spacing.lg),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
margin: const EdgeInsets.only(bottom: Spacing.md),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.conduitTheme.dividerColor,
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../../core/widgets/error_boundary.dart';
|
|||||||
import '../../../shared/widgets/improved_loading_states.dart';
|
import '../../../shared/widgets/improved_loading_states.dart';
|
||||||
|
|
||||||
import '../../../shared/utils/ui_utils.dart';
|
import '../../../shared/utils/ui_utils.dart';
|
||||||
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
import '../../../shared/widgets/conduit_components.dart';
|
import '../../../shared/widgets/conduit_components.dart';
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../auth/providers/unified_auth_providers.dart';
|
import '../../auth/providers/unified_auth_providers.dart';
|
||||||
@@ -49,17 +50,12 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
toolbarHeight: kToolbarHeight,
|
toolbarHeight: kToolbarHeight,
|
||||||
titleSpacing: 0.0,
|
titleSpacing: 0.0,
|
||||||
title: Row(
|
title: Text(
|
||||||
mainAxisSize: MainAxisSize.min,
|
'You',
|
||||||
children: [
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
Text(
|
color: context.conduitTheme.textPrimary,
|
||||||
'You',
|
fontWeight: FontWeight.w600,
|
||||||
style: context.conduitTheme.headingSmall?.copyWith(
|
),
|
||||||
color: context.conduitTheme.textPrimary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
@@ -114,7 +110,7 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'You',
|
'You',
|
||||||
style: context.conduitTheme.headingSmall?.copyWith(
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
color: context.conduitTheme.textPrimary,
|
color: context.conduitTheme.textPrimary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -144,7 +140,7 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'You',
|
'You',
|
||||||
style: context.conduitTheme.headingSmall?.copyWith(
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
color: context.conduitTheme.textPrimary,
|
color: context.conduitTheme.textPrimary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -195,56 +191,14 @@ class ProfilePage extends ConsumerWidget {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.sm),
|
||||||
Text(
|
Text(
|
||||||
user?.email ?? 'No email',
|
user?.email ?? 'No email',
|
||||||
style: context.conduitTheme.bodyMedium?.copyWith(
|
style: context.conduitTheme.bodyMedium?.copyWith(
|
||||||
color: context.conduitTheme.textSecondary,
|
color: context.conduitTheme.textSecondary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.sm),
|
// Status badge removed per design update
|
||||||
// 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -789,55 +743,11 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
|||||||
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
padding: const EdgeInsets.all(Spacing.bottomSheetPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Container(
|
const SheetHandle(),
|
||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Header
|
// Header removed (no icon/title or save button)
|
||||||
Padding(
|
const SizedBox(height: Spacing.md),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Search field
|
// Search field
|
||||||
Padding(
|
Padding(
|
||||||
@@ -963,10 +873,11 @@ class _DefaultModelBottomSheetState extends ConsumerState<_DefaultModelBottomShe
|
|||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
isAutoSelect: isAutoSelect,
|
isAutoSelect: isAutoSelect,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.lightImpact();
|
||||||
setState(() {
|
final selectedId =
|
||||||
_selectedModelId = isAutoSelect ? 'auto-select' : model.id;
|
isAutoSelect ? 'auto-select' : model.id;
|
||||||
});
|
// Return selection immediately; caller handles persisting
|
||||||
|
Navigator.pop(context, selectedId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../../../shared/theme/theme_extensions.dart';
|
|||||||
import '../../chat/providers/chat_providers.dart';
|
import '../../chat/providers/chat_providers.dart';
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../providers/tools_providers.dart';
|
import '../providers/tools_providers.dart';
|
||||||
|
import '../../../shared/widgets/sheet_handle.dart';
|
||||||
|
|
||||||
class UnifiedToolsModal extends ConsumerStatefulWidget {
|
class UnifiedToolsModal extends ConsumerStatefulWidget {
|
||||||
const UnifiedToolsModal({super.key});
|
const UnifiedToolsModal({super.key});
|
||||||
@@ -47,17 +48,8 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Handle bar
|
// Handle bar (standardized)
|
||||||
Center(
|
const SheetHandle(),
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.textPrimary.withValues(alpha: Alpha.medium),
|
|
||||||
borderRadius: BorderRadius.circular(2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.md),
|
||||||
|
|
||||||
// Removed header for minimal, focused layout
|
// Removed header for minimal, focused layout
|
||||||
|
|||||||
23
lib/shared/widgets/sheet_handle.dart
Normal file
23
lib/shared/widgets/sheet_handle.dart
Normal file
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user