refactor: cleanup unsued files

This commit is contained in:
cogwheel0
2025-08-23 11:54:41 +05:30
parent 7f30b728ab
commit b898adbe40
19 changed files with 9 additions and 6490 deletions

View File

@@ -1,956 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io' show Platform;
import 'dart:ui' as ui;
import '../../../core/models/conversation.dart';
import '../../../core/providers/app_providers.dart';
import '../../../shared/theme/app_theme.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/utils/ui_utils.dart';
import '../../../shared/widgets/conduit_components.dart';
import '../providers/chat_providers.dart';
// Optimized delete conversation provider with error handling
final deleteConversationProvider = FutureProvider.family<void, String>((
ref,
conversationId,
) async {
final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service available');
await api.deleteConversation(conversationId);
ref.invalidate(conversationsProvider);
});
/// Optimized conversation tile with Conduit design aesthetics
class ModernConversationTile extends ConsumerStatefulWidget {
final Conversation conversation;
final bool isActive;
final Future<void> Function() onTap;
final VoidCallback onDelete;
const ModernConversationTile({
super.key,
required this.conversation,
required this.isActive,
required this.onTap,
required this.onDelete,
});
@override
ConsumerState<ModernConversationTile> createState() =>
_ModernConversationTileState();
}
class _ModernConversationTileState extends ConsumerState<ModernConversationTile>
with SingleTickerProviderStateMixin {
bool _isLoading = false;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
),
child: Dismissible(
key: Key(widget.conversation.id),
direction: DismissDirection.horizontal,
background: _buildSwipeBackground(DismissDirection.startToEnd),
secondaryBackground: _buildSwipeBackground(
DismissDirection.endToStart,
),
confirmDismiss: _handleDismiss,
child: _buildTileContent(),
),
),
);
},
);
}
Widget _buildSwipeBackground(DismissDirection direction) {
final isArchive = direction == DismissDirection.startToEnd;
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isArchive
? [
AppTheme.brandPrimary.withValues(alpha: 0.1),
AppTheme.brandPrimary.withValues(alpha: 0.2),
]
: [
AppTheme.error.withValues(alpha: 0.1),
AppTheme.error.withValues(alpha: 0.2),
],
),
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
),
alignment: isArchive ? Alignment.centerLeft : Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: Spacing.lg),
child: Container(
width: Spacing.xxl,
height: Spacing.xxl,
decoration: BoxDecoration(
color: isArchive ? AppTheme.brandPrimary : AppTheme.error,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
boxShadow: ConduitShadows.low,
),
child: Icon(
isArchive
? (Platform.isIOS ? CupertinoIcons.archivebox : Icons.archive)
: (Platform.isIOS ? CupertinoIcons.delete : Icons.delete),
color: AppTheme.neutral50,
size: AppTypography.headlineMedium,
),
),
);
}
Future<bool?> _handleDismiss(DismissDirection direction) async {
if (direction == DismissDirection.startToEnd) {
await _handleArchive();
} else {
widget.onDelete();
}
return false;
}
Widget _buildTileContent() {
return GestureDetector(
onTapDown: (_) => _animationController.forward(),
onTapUp: (_) => _animationController.reverse(),
onTapCancel: () => _animationController.reverse(),
onTap: _isLoading ? null : _handleTap,
child: Container(
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
gradient: widget.isActive
? LinearGradient(
colors: [
AppTheme.brandPrimary.withValues(alpha: 0.15),
AppTheme.brandPrimary.withValues(alpha: 0.08),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: LinearGradient(
colors: [
AppTheme.neutral700.withValues(alpha: 0.6),
AppTheme.neutral700.withValues(alpha: 0.3),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
border: Border.all(
color: widget.isActive
? AppTheme.brandPrimary.withValues(alpha: 0.3)
: AppTheme.neutral600.withValues(alpha: 0.2),
width: widget.isActive ? BorderWidth.medium : BorderWidth.thin,
),
boxShadow: widget.isActive ? ConduitShadows.low : null,
),
child: Row(
children: [
_buildLeadingIcon(),
const SizedBox(width: Spacing.md),
Expanded(child: _buildContent()),
_buildTrailingActions(),
],
),
),
);
}
Widget _buildLeadingIcon() {
if (_isLoading) {
return SizedBox(
width: Spacing.xl,
height: Spacing.xl,
child: CircularProgressIndicator.adaptive(
strokeWidth: BorderWidth.thick,
valueColor: AlwaysStoppedAnimation<Color>(
widget.isActive ? AppTheme.brandPrimary : AppTheme.neutral300,
),
),
);
}
return Container(
width: Spacing.xl,
height: Spacing.xl,
decoration: BoxDecoration(
gradient: widget.isActive
? LinearGradient(
colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: LinearGradient(
colors: [
AppTheme.neutral600.withValues(alpha: 0.8),
AppTheme.neutral500.withValues(alpha: 0.6),
],
),
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Platform.isIOS
? CupertinoIcons.chat_bubble_2_fill
: Icons.chat_rounded,
color: AppTheme.neutral50,
size: Spacing.md,
),
if (widget.conversation.pinned)
Positioned(
top: Spacing.xxs,
right: Spacing.xxs,
child: Container(
width: Spacing.sm,
height: Spacing.sm,
decoration: const BoxDecoration(
color: AppTheme.warning,
shape: BoxShape.circle,
),
),
),
],
),
);
}
Widget _buildContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.conversation.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: widget.isActive ? AppTheme.neutral50 : AppTheme.neutral100,
fontWeight: FontWeight.w600,
fontSize: AppTypography.bodyLarge,
letterSpacing: -0.2,
),
),
const SizedBox(height: Spacing.xs),
Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.time : Icons.access_time_rounded,
size: AppTypography.labelMedium,
color: AppTheme.neutral400,
),
const SizedBox(width: Spacing.xs),
Text(
_formatDate(widget.conversation.updatedAt),
style: const TextStyle(
color: AppTheme.neutral400,
fontSize: AppTypography.labelMedium,
fontWeight: FontWeight.w500,
),
),
if (widget.conversation.messages.isNotEmpty) ...[
Container(
margin: const EdgeInsets.symmetric(horizontal: Spacing.sm),
width: Spacing.xxs,
height: Spacing.xxs,
decoration: const BoxDecoration(
color: AppTheme.neutral400,
shape: BoxShape.circle,
),
),
Text(
'${widget.conversation.messages.length} messages',
style: const TextStyle(
color: AppTheme.neutral400,
fontSize: AppTypography.labelMedium,
fontWeight: FontWeight.w500,
),
),
],
],
),
if (widget.conversation.tags.isNotEmpty) ...[
const SizedBox(height: Spacing.sm),
_buildTags(),
],
],
);
}
Widget _buildTags() {
return Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: widget.conversation.tags.take(3).map((tag) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.xs + Spacing.xxs,
vertical: Spacing.xxs,
),
decoration: BoxDecoration(
color: AppTheme.brandPrimary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
border: Border.all(
color: AppTheme.brandPrimary.withValues(alpha: 0.2),
width: BorderWidth.thin,
),
),
child: Text(
tag,
style: const TextStyle(
color: AppTheme.brandPrimary,
fontSize: AppTypography.labelSmall,
fontWeight: FontWeight.w600,
),
),
);
}).toList(),
);
}
Widget _buildTrailingActions() {
return PopupMenuButton<String>(
icon: Container(
width: Spacing.xl,
height: Spacing.xl,
decoration: BoxDecoration(
color: AppTheme.neutral700.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
),
child: Icon(
Platform.isIOS ? CupertinoIcons.ellipsis : Icons.more_vert_rounded,
color: AppTheme.neutral300,
size: Spacing.md,
),
),
color: AppTheme.neutral800,
elevation: Elevation.high + Spacing.xs,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
side: BorderSide(
color: AppTheme.neutral600.withValues(alpha: 0.3),
width: BorderWidth.thin,
),
),
onSelected: _handleMenuAction,
itemBuilder: (context) => _buildMenuItems(),
);
}
List<PopupMenuItem<String>> _buildMenuItems() {
return [
_buildMenuItem(
'pin',
widget.conversation.pinned
? (Platform.isIOS
? CupertinoIcons.pin_slash
: Icons.push_pin_outlined)
: (Platform.isIOS
? CupertinoIcons.pin_fill
: Icons.push_pin_rounded),
widget.conversation.pinned ? 'Unpin' : 'Pin',
),
_buildMenuItem(
'archive',
Platform.isIOS ? CupertinoIcons.archivebox : Icons.archive_rounded,
'Archive',
),
_buildMenuItem(
'share',
Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded,
'Share',
),
_buildMenuItem(
'clone',
Platform.isIOS ? CupertinoIcons.doc_on_doc : Icons.content_copy_rounded,
'Clone',
),
PopupMenuItem<String>(
enabled: false,
child: Divider(color: AppTheme.neutral600, height: BorderWidth.regular),
),
_buildMenuItem(
'delete',
Platform.isIOS ? CupertinoIcons.delete : Icons.delete_rounded,
'Delete',
isDestructive: true,
),
];
}
PopupMenuItem<String> _buildMenuItem(
String value,
IconData icon,
String label, {
bool isDestructive = false,
}) {
return PopupMenuItem(
value: value,
child: Row(
children: [
Container(
width: Spacing.lg + Spacing.xs,
height: Spacing.lg + Spacing.xs,
decoration: BoxDecoration(
color: isDestructive
? AppTheme.error.withValues(alpha: 0.1)
: AppTheme.neutral700.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
child: Icon(
icon,
size: Spacing.md,
color: isDestructive ? AppTheme.error : AppTheme.neutral200,
),
),
const SizedBox(width: Spacing.sm),
Text(
label,
style: TextStyle(
color: isDestructive ? AppTheme.error : AppTheme.neutral50,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Future<void> _handleTap() async {
setState(() => _isLoading = true);
try {
await widget.onTap();
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Future<void> _handleMenuAction(String action) async {
switch (action) {
case 'pin':
await _handlePin();
break;
case 'archive':
await _handleArchive();
break;
case 'share':
await _handleShare();
break;
case 'clone':
await _handleClone();
break;
case 'delete':
widget.onDelete();
break;
}
}
Future<void> _handlePin() async {
try {
await pinConversation(
ref,
widget.conversation.id,
!widget.conversation.pinned,
);
if (mounted) {
UiUtils.showMessage(
context,
widget.conversation.pinned
? 'Conversation unpinned'
: 'Conversation pinned',
);
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(
context,
'Failed to ${widget.conversation.pinned ? 'unpin' : 'pin'} conversation',
);
}
}
}
Future<void> _handleArchive() async {
try {
await archiveConversation(ref, widget.conversation.id, true);
if (mounted) {
UiUtils.showMessage(context, 'Conversation archived');
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(context, 'Failed to archive conversation');
}
}
}
Future<void> _handleShare() async {
try {
final shareId = await shareConversation(ref, widget.conversation.id);
if (mounted && shareId != null) {
_showShareDialog(shareId);
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(context, 'Failed to share conversation');
}
}
}
Future<void> _handleClone() async {
try {
await cloneConversation(ref, widget.conversation.id);
if (mounted) {
Navigator.pop(context);
UiUtils.showMessage(context, 'Conversation cloned');
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(context, 'Failed to clone conversation');
}
}
}
void _showShareDialog(String shareId) {
final shareUrl =
'${ref.read(apiServiceProvider)?.serverConfig.url}/s/$shareId';
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.neutral800,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
side: BorderSide(
color: AppTheme.neutral600.withValues(alpha: 0.3),
width: BorderWidth.thin,
),
),
title: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight],
),
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
),
child: const Icon(
Icons.share_rounded,
color: AppTheme.neutral50,
size: Spacing.md,
),
),
const SizedBox(width: Spacing.sm),
const Text(
'Share Conversation',
style: TextStyle(
color: AppTheme.neutral50,
fontWeight: FontWeight.w600,
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Anyone with this link can view the conversation:',
style: TextStyle(color: AppTheme.neutral300),
),
const SizedBox(height: Spacing.md),
Container(
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: AppTheme.neutral700.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: AppTheme.neutral600.withValues(alpha: 0.3),
width: BorderWidth.thin,
),
),
child: SelectableText(
shareUrl,
style: const TextStyle(
fontFamily: 'monospace',
color: AppTheme.neutral50,
fontSize: AppTypography.labelMedium,
),
),
),
],
),
actions: [
ConduitButton(
text: 'Close',
isSecondary: true,
onPressed: () => Navigator.pop(context),
),
ConduitButton(
text: 'Copy Link',
onPressed: () async {
await Clipboard.setData(ClipboardData(text: shareUrl));
if (context.mounted) {
UiUtils.showMessage(context, 'Link copied to clipboard');
Navigator.pop(context);
}
},
),
],
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
// Convert to local timezone if needed
final localDate = date.toLocal();
final localNow = now.toLocal();
final difference = localNow.difference(localDate);
// Handle negative differences (future dates)
if (difference.isNegative) {
return 'Just now';
}
if (difference.inDays == 0) {
if (difference.inHours == 0) {
if (difference.inMinutes <= 1) {
return 'Just now';
}
return '${difference.inMinutes}m';
}
return '${difference.inHours}h';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays}d';
} else if (difference.inDays < 365) {
return '${localDate.month}/${localDate.day}';
} else {
return '${localDate.month}/${localDate.day}/${localDate.year}';
}
}
}
/// Optimized archived chats view with improved performance
class ModernArchivedChatsView extends ConsumerWidget {
final ScrollController scrollController;
const ModernArchivedChatsView({super.key, required this.scrollController});
@override
Widget build(BuildContext context, WidgetRef ref) {
final archivedConversations = ref.watch(archivedConversationsProvider);
return Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppTheme.neutral800, AppTheme.neutral900],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.only(
topLeft: ui.Radius.circular(AppBorderRadius.lg),
topRight: ui.Radius.circular(AppBorderRadius.lg),
),
border: Border.all(
color: AppTheme.neutral600.withValues(alpha: 0.2),
width: BorderWidth.thin,
),
),
child: Column(
children: [
_buildHandle(),
_buildHeader(context),
const Divider(color: AppTheme.neutral600, height: 1, thickness: 0.5),
Expanded(child: _buildContent(context, archivedConversations, ref)),
],
),
);
}
Widget _buildHandle() {
return Container(
margin: const EdgeInsets.symmetric(vertical: Spacing.sm),
width: Spacing.xxl,
height: Spacing.xs,
decoration: BoxDecoration(
color: AppTheme.neutral500,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
);
}
Widget _buildHeader(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(Spacing.lg),
child: Row(
children: [
Container(
width: Spacing.xxl,
height: Spacing.xxl,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
),
child: const Icon(
Icons.archive_rounded,
color: AppTheme.neutral50,
size: AppTypography.headlineMedium,
),
),
const SizedBox(width: Spacing.md),
const Expanded(
child: Text(
'Archived Conversations',
style: TextStyle(
color: AppTheme.neutral50,
fontSize: AppTypography.headlineSmall,
fontWeight: FontWeight.w600,
letterSpacing: -0.3,
),
),
),
ConduitIconButton(
icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded,
onPressed: () => Navigator.pop(context),
),
],
),
);
}
Widget _buildContent(
BuildContext context,
List<Conversation> conversations,
WidgetRef ref,
) {
if (conversations.isEmpty) {
return _buildEmptyState();
}
return ListView.builder(
controller: scrollController,
padding: const EdgeInsets.all(Spacing.md),
itemCount: conversations.length,
itemBuilder: (context, index) {
final conversation = conversations[index];
return ModernArchivedConversationTile(
conversation: conversation,
onUnarchive: () => _handleUnarchive(ref, context, conversation.id),
);
},
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: Spacing.xxl + Spacing.xl,
height: Spacing.xxl + Spacing.xl,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.neutral600.withValues(alpha: 0.3),
AppTheme.neutral700.withValues(alpha: 0.1),
],
),
borderRadius: BorderRadius.circular(AppBorderRadius.round),
),
child: const Icon(
Icons.archive_rounded,
size: Spacing.xxl,
color: AppTheme.neutral400,
),
),
const SizedBox(height: Spacing.lg),
const Text(
'Nothing archived yet',
style: TextStyle(
color: AppTheme.neutral50,
fontSize: AppTypography.headlineSmall,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.sm),
const Text(
'Conversations you archive will appear here',
style: TextStyle(
color: AppTheme.neutral400,
fontSize: AppTypography.labelLarge,
),
textAlign: TextAlign.center,
),
],
),
);
}
Future<void> _handleUnarchive(
WidgetRef ref,
BuildContext context,
String conversationId,
) async {
try {
await archiveConversation(ref, conversationId, false);
if (context.mounted) {
UiUtils.showMessage(context, 'Conversation unarchived');
}
} catch (e) {
if (context.mounted) {
UiUtils.showMessage(context, 'Failed to unarchive conversation');
}
}
}
}
/// Optimized archived conversation tile
class ModernArchivedConversationTile extends StatelessWidget {
final Conversation conversation;
final VoidCallback onUnarchive;
const ModernArchivedConversationTile({
super.key,
required this.conversation,
required this.onUnarchive,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: Spacing.sm),
child: Container(
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.neutral700.withValues(alpha: 0.4),
AppTheme.neutral700.withValues(alpha: 0.2),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
border: Border.all(
color: AppTheme.neutral600.withValues(alpha: 0.2),
width: BorderWidth.thin,
),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: AppTheme.neutral600.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
),
child: const Icon(
Icons.archive_rounded,
color: AppTheme.neutral300,
size: 16,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
conversation.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppTheme.neutral50,
fontWeight: FontWeight.w600,
fontSize: AppTypography.bodyLarge,
),
),
const SizedBox(height: Spacing.xs),
Text(
_formatArchivedDate(conversation.updatedAt),
style: const TextStyle(
color: AppTheme.neutral400,
fontSize: AppTypography.labelMedium,
fontWeight: FontWeight.w500,
),
),
],
),
),
ConduitIconButton(
icon: Platform.isIOS
? CupertinoIcons.arrow_up_bin
: Icons.unarchive_rounded,
onPressed: onUnarchive,
tooltip: 'Unarchive',
),
],
),
),
);
}
String _formatArchivedDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return 'Today';
} else if (difference.inDays == 1) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
return '${date.month}/${date.day}/${date.year}';
}
}
}

View File

@@ -1,242 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../shared/theme/theme_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/models/file_info.dart';
import '../../../core/providers/app_providers.dart';
class FileViewerDialog extends ConsumerWidget {
final FileInfo fileInfo;
const FileViewerDialog({super.key, required this.fileInfo});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Use themed tokens via extension
final fileContent = ref.watch(fileContentProvider(fileInfo.id));
return Dialog.fullscreen(
child: Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
appBar: AppBar(
backgroundColor: context.conduitTheme.surfaceBackground,
elevation: 0,
title: Text(
fileInfo.originalFilename,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: context.conduitTheme.textPrimary),
),
iconTheme: IconThemeData(color: context.conduitTheme.iconPrimary),
leading: IconButton(
icon: Icon(Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: Icon(Platform.isIOS ? CupertinoIcons.info : Icons.info),
onPressed: () => _showFileInfo(context),
),
],
),
body: fileContent.when(
data: (content) => _buildContentView(context, content),
loading: () => Center(
child: CircularProgressIndicator(
color: context.conduitTheme.buttonPrimary,
),
),
error: (error, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: context.conduitTheme.error),
const SizedBox(height: Spacing.md),
Text(
'Failed to load file',
style: TextStyle(
color: context.conduitTheme.error,
fontSize: AppTypography.headlineSmall,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.sm),
Text(
error.toString(),
style: TextStyle(color: context.conduitTheme.textSecondary),
textAlign: TextAlign.center,
),
const SizedBox(height: Spacing.md),
ElevatedButton(
onPressed: () =>
ref.invalidate(fileContentProvider(fileInfo.id)),
child: const Text('Retry'),
),
],
),
),
),
),
);
}
Widget _buildContentView(BuildContext context, String content) {
final theme = context.conduitTheme;
final isTextFile = _isTextFile(fileInfo.mimeType);
if (!isTextFile) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getFileIcon(fileInfo.mimeType),
size: 64,
color: theme.buttonPrimary,
),
const SizedBox(height: Spacing.md),
Text(
fileInfo.originalFilename,
style: TextStyle(
color: theme.textPrimary,
fontSize: AppTypography.headlineSmall,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: Spacing.sm),
Text(
'File type: ${fileInfo.mimeType}',
style: TextStyle(color: theme.textSecondary),
),
Text(
'Size: ${_formatFileSize(fileInfo.size)}',
style: TextStyle(color: theme.textSecondary),
),
const SizedBox(height: Spacing.md),
Text(
'Preview not available for this file type',
style: TextStyle(color: theme.textTertiary),
),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(Spacing.md),
child: SelectableText(
content,
style: TextStyle(
color: theme.textPrimary,
fontFamily: 'monospace',
fontSize: AppTypography.labelLarge,
),
),
);
}
void _showFileInfo(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: context.conduitTheme.surfaceBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.dialog),
),
title: Text(
'File Information',
style: TextStyle(color: context.conduitTheme.textPrimary),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(context, 'Name', fileInfo.originalFilename),
_buildInfoRow(context, 'Size', _formatFileSize(fileInfo.size)),
_buildInfoRow(context, 'Type', fileInfo.mimeType),
_buildInfoRow(context, 'Created', _formatDate(fileInfo.createdAt)),
_buildInfoRow(context, 'Modified', _formatDate(fileInfo.updatedAt)),
if (fileInfo.hash != null)
_buildInfoRow(context, 'Hash', fileInfo.hash!),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Close',
style: TextStyle(color: context.conduitTheme.buttonPrimary),
),
),
],
),
);
}
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: Spacing.xxxl + Spacing.md,
child: Text(
'$label:',
style: TextStyle(
fontWeight: FontWeight.w600,
color: context.conduitTheme.textSecondary,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(color: context.conduitTheme.textPrimary),
),
),
],
),
);
}
bool _isTextFile(String mimeType) {
return mimeType.startsWith('text/') ||
mimeType == 'application/json' ||
mimeType == 'application/xml' ||
mimeType == 'application/javascript' ||
mimeType.contains('yaml') ||
mimeType.contains('markdown');
}
IconData _getFileIcon(String mimeType) {
if (mimeType.startsWith('image/')) {
return Platform.isIOS ? CupertinoIcons.photo : Icons.image;
} else if (mimeType.startsWith('video/')) {
return Platform.isIOS ? CupertinoIcons.video_camera : Icons.video_file;
} else if (mimeType.startsWith('audio/')) {
return Platform.isIOS ? CupertinoIcons.music_note : Icons.audio_file;
} else if (mimeType.contains('pdf')) {
return Platform.isIOS ? CupertinoIcons.doc : Icons.picture_as_pdf;
} else if (mimeType.startsWith('text/') || mimeType.contains('json')) {
return Platform.isIOS ? CupertinoIcons.doc_text : Icons.description;
} else {
return Platform.isIOS ? CupertinoIcons.doc : Icons.insert_drive_file;
}
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
String _formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
}

View File

@@ -1,938 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/widgets/conduit_components.dart';
import '../../../shared/utils/ui_utils.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/models/folder.dart';
import '../../../core/models/conversation.dart';
import '../../../core/providers/app_providers.dart';
class FolderManagementDialog extends ConsumerStatefulWidget {
final Conversation? conversation;
final BuildContext? parentContext;
const FolderManagementDialog({super.key, this.conversation, this.parentContext});
@override
ConsumerState<FolderManagementDialog> createState() =>
_FolderManagementDialogState();
}
class _FolderManagementDialogState
extends ConsumerState<FolderManagementDialog> {
final _nameController = TextEditingController();
bool _isCreating = false;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final folders = ref.watch(foldersProvider);
final isMovingConversation = widget.conversation != null;
return Directionality(
textDirection: TextDirection.ltr,
child: Dialog(
backgroundColor: Colors.transparent,
child: Container(
width: 480,
constraints: const BoxConstraints(maxHeight: 680),
decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.modal),
border: Border.all(
color: context.conduitTheme.cardBorder.withValues(alpha: 0.2),
width: BorderWidth.regular,
),
boxShadow: ConduitShadows.modal,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Modern Header
_buildModernHeader(context, isMovingConversation),
// Content Section
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Create folder section (only if managing folders)
if (!isMovingConversation) ...[
_buildCreateFolderSection(context),
ConduitDivider(color: context.conduitTheme.dividerColor.withValues(alpha: 0.2)),
],
// Folders list
Expanded(
child: folders.when(
data: (folderList) => _buildFoldersList(context, folderList, isMovingConversation),
loading: () => _buildLoadingState(context),
error: (error, _) => _buildErrorState(context, error),
),
),
],
),
),
// Bottom actions (only for conversation moving)
if (isMovingConversation) _buildBottomActions(context),
],
),
).animate().slideY(
begin: 0.1,
duration: AnimationDuration.modalPresentation,
curve: AnimationCurves.modalPresentation,
).fadeIn(
duration: AnimationDuration.modalPresentation,
curve: AnimationCurves.easeOut,
),
),
);
}
// Modern header with clean design
Widget _buildModernHeader(BuildContext context, bool isMovingConversation) {
return Container(
padding: const EdgeInsets.all(Spacing.lg),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal),
),
border: Border(
bottom: BorderSide(
color: context.conduitTheme.dividerColor.withValues(alpha: 0.1),
width: BorderWidth.regular,
),
),
),
child: Row(
children: [
// Modern icon container
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
),
child: Icon(
Platform.isIOS ? CupertinoIcons.folder_fill : Icons.folder_rounded,
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
const SizedBox(width: Spacing.md),
// Title and subtitle
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isMovingConversation ? 'Move to Folder' : 'Manage Folders',
style: AppTypography.headlineMediumStyle.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.xs),
Text(
isMovingConversation
? 'Select a folder for "${widget.conversation?.title ?? 'this conversation'}"'
: 'Create and organize your conversation folders',
style: AppTypography.bodyMediumStyle.copyWith(
color: context.conduitTheme.textSecondary,
),
),
],
),
),
// Close button
ConduitIconButton(
icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded,
onPressed: () => Navigator.pop(context),
tooltip: 'Close',
),
],
),
);
}
// Create folder section with improved UX
Widget _buildCreateFolderSection(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: Spacing.xl, vertical: Spacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create New Folder',
style: AppTypography.bodyMediumStyle.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.sm),
Row(
children: [
Expanded(
child: AccessibleFormField(
controller: _nameController,
hint: 'Enter folder name',
prefixIcon: Icon(
Platform.isIOS
? CupertinoIcons.folder_badge_plus
: Icons.create_new_folder_rounded,
color: context.conduitTheme.iconSecondary,
size: IconSize.medium,
),
onSubmitted: (_) => _createFolder(),
isCompact: true,
),
),
const SizedBox(width: Spacing.md),
ConduitButton(
text: 'Create',
onPressed: _isCreating ? null : _createFolder,
isLoading: _isCreating,
icon: Platform.isIOS ? CupertinoIcons.add : Icons.add_rounded,
isCompact: true,
),
],
),
],
),
);
}
// Enhanced folders list
Widget _buildFoldersList(BuildContext context, List<Folder> folderList, bool isMovingConversation) {
if (folderList.isEmpty) {
return _buildEmptyState(context, isMovingConversation);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.xl,
vertical: Spacing.md,
),
itemCount: folderList.length,
separatorBuilder: (context, index) => const SizedBox(height: Spacing.xs),
itemBuilder: (context, index) {
final folder = folderList[index];
return _buildFolderTile(folder, index).animate(delay: Duration(milliseconds: index * 50))
.slideX(begin: 0.2, duration: AnimationDuration.fast)
.fadeIn(duration: AnimationDuration.fast);
},
);
}
Widget _buildEmptyState(BuildContext context, bool isMovingConversation) {
return ConduitEmptyState(
icon: Platform.isIOS ? CupertinoIcons.folder : Icons.folder_outlined,
title: 'No folders yet',
message: isMovingConversation
? 'Create a folder first'
: 'Use the form above to create your first folder',
isCompact: true,
);
}
Widget _buildLoadingState(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(Spacing.xl),
child: ConduitLoadingIndicator(
message: 'Loading folders...',
size: IconSize.xl,
),
),
);
}
Widget _buildErrorState(BuildContext context, Object error) {
return ConduitEmptyState(
icon: Icons.error_outline_rounded,
title: 'Failed to load folders',
message: 'Please check your connection and try again',
isCompact: true,
action: ConduitButton(
text: 'Retry',
onPressed: () => ref.invalidate(foldersProvider),
icon: Icons.refresh_rounded,
isCompact: true,
),
);
}
// Bottom actions for conversation moving
Widget _buildBottomActions(BuildContext context) {
return Container(
padding: const EdgeInsets.all(Spacing.lg),
decoration: BoxDecoration(
color: context.conduitTheme.cardBackground,
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(AppBorderRadius.modal),
),
border: Border(
top: BorderSide(
color: context.conduitTheme.dividerColor.withValues(alpha: 0.1),
width: BorderWidth.regular,
),
),
),
child: Row(
children: [
Expanded(
child: ConduitButton(
text: 'Remove from Folder',
onPressed: () => _moveToFolder(null),
isSecondary: true,
icon: Platform.isIOS ? CupertinoIcons.folder_badge_minus : Icons.folder_off_rounded,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: ConduitButton(
text: 'Cancel',
onPressed: () => Navigator.pop(context),
isSecondary: true,
icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded,
),
),
],
),
);
}
Widget _buildFolderTile(Folder folder, int index) {
final isSelected = widget.conversation?.folderId == folder.id;
final isMovingConversation = widget.conversation != null;
return ConduitCard(
onTap: isMovingConversation ? () => _moveToFolder(folder.id) : null,
isSelected: isSelected,
child: ConduitListItem(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isSelected
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.15)
: context.conduitTheme.surfaceContainer,
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
border: isSelected ? Border.all(
color: context.conduitTheme.buttonPrimary.withValues(alpha: 0.3),
width: BorderWidth.regular,
) : null,
),
child: Icon(
Platform.isIOS ? CupertinoIcons.folder_fill : Icons.folder_rounded,
color: isSelected
? context.conduitTheme.buttonPrimary
: context.conduitTheme.iconSecondary,
size: IconSize.lg,
),
),
title: Text(
folder.name,
style: AppTypography.bodyLargeStyle.copyWith(
color: isSelected
? context.conduitTheme.buttonPrimary
: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.chat_bubble_2 : Icons.chat_bubble_outline_rounded,
size: IconSize.xs,
color: context.conduitTheme.textTertiary,
),
const SizedBox(width: Spacing.xs),
Text(
'${folder.conversationIds.length} conversation${folder.conversationIds.length != 1 ? 's' : ''}',
style: AppTypography.bodySmallStyle.copyWith(
color: context.conduitTheme.textSecondary,
),
),
if (folder.conversationIds.isNotEmpty) ...[
const SizedBox(width: Spacing.sm),
Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: context.conduitTheme.textTertiary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: Spacing.sm),
Text(
'Active',
style: AppTypography.captionStyle.copyWith(
color: context.conduitTheme.success,
fontWeight: FontWeight.w500,
),
),
],
],
),
trailing: _buildFolderActions(folder, isSelected, isMovingConversation),
isSelected: isSelected,
),
);
}
Widget _buildFolderActions(Folder folder, bool isSelected, bool isMovingConversation) {
if (isMovingConversation) {
return isSelected
? Container(
padding: const EdgeInsets.all(Spacing.xs),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary,
borderRadius: BorderRadius.circular(AppBorderRadius.round),
),
child: Icon(
Platform.isIOS ? CupertinoIcons.checkmark : Icons.check_rounded,
color: context.conduitTheme.buttonPrimaryText,
size: IconSize.small,
),
)
: Icon(
Platform.isIOS ? CupertinoIcons.chevron_right : Icons.arrow_forward_ios_rounded,
color: context.conduitTheme.iconSecondary.withValues(alpha: 0.6),
size: IconSize.small,
);
}
// Management mode - show actions menu
return PopupMenuButton<String>(
icon: Icon(
Platform.isIOS ? CupertinoIcons.ellipsis : Icons.more_vert_rounded,
color: context.conduitTheme.iconSecondary,
size: IconSize.medium,
),
color: context.conduitTheme.surfaceBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
side: BorderSide(
color: context.conduitTheme.cardBorder.withValues(alpha: 0.2),
width: BorderWidth.regular,
),
),
elevation: Elevation.medium,
onSelected: (value) {
switch (value) {
case 'rename':
_renameFolder(folder);
break;
case 'delete':
_deleteFolder(folder);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'rename',
child: Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_rounded,
size: IconSize.small,
color: context.conduitTheme.iconSecondary,
),
const SizedBox(width: Spacing.md),
Text(
'Rename',
style: AppTypography.bodyMediumStyle.copyWith(
color: context.conduitTheme.textPrimary,
),
),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.delete : Icons.delete_outline_rounded,
size: IconSize.small,
color: context.conduitTheme.error,
),
const SizedBox(width: Spacing.md),
Text(
'Delete',
style: AppTypography.bodyMediumStyle.copyWith(
color: context.conduitTheme.error,
),
),
],
),
),
],
);
}
Future<void> _createFolder() async {
final name = _nameController.text.trim();
if (name.isEmpty) return;
setState(() => _isCreating = true);
try {
final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service available');
await api.createFolder(name: name);
ref.invalidate(foldersProvider);
_nameController.clear();
if (mounted) {
UiUtils.showMessage(widget.parentContext ?? context, 'Folder "$name" created');
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(widget.parentContext ?? context, 'Error creating folder: $e');
}
} finally {
if (mounted) {
setState(() => _isCreating = false);
}
}
}
Future<void> _moveToFolder(String? folderId) async {
if (widget.conversation == null) return;
try {
final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service available');
await api.moveConversationToFolder(widget.conversation!.id, folderId);
ref.invalidate(conversationsProvider);
ref.invalidate(foldersProvider);
if (mounted) {
Navigator.pop(context);
UiUtils.showMessage(
widget.parentContext ?? context,
folderId != null
? 'Conversation moved to folder'
: 'Conversation removed from folder',
);
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(widget.parentContext ?? context, 'Error moving conversation: $e');
}
}
}
void _renameFolder(Folder folder) async {
final controller = TextEditingController(text: folder.name);
final result = await showDialog<String>(
context: context,
builder: (dialogContext) => Directionality(
textDirection: TextDirection.ltr,
child: Dialog(
backgroundColor: Colors.transparent,
child: Container(
width: 400,
decoration: BoxDecoration(
color: dialogContext.conduitTheme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.modal),
border: Border.all(
color: dialogContext.conduitTheme.cardBorder.withValues(alpha: 0.2),
width: BorderWidth.regular,
),
boxShadow: ConduitShadows.modal,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Container(
padding: const EdgeInsets.all(Spacing.xl),
decoration: BoxDecoration(
color: dialogContext.conduitTheme.cardBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal),
),
border: Border(
bottom: BorderSide(
color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1),
width: BorderWidth.regular,
),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: dialogContext.conduitTheme.buttonPrimary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
),
child: Icon(
Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_rounded,
color: dialogContext.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Rename Folder',
style: AppTypography.headlineSmallStyle.copyWith(
color: dialogContext.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.xs),
Text(
'Enter a new name for your folder',
style: AppTypography.bodyMediumStyle.copyWith(
color: dialogContext.conduitTheme.textSecondary,
),
),
],
),
),
],
),
),
// Content
Padding(
padding: const EdgeInsets.all(Spacing.xl),
child: AccessibleFormField(
controller: controller,
label: 'Folder Name',
hint: 'Enter folder name',
autofocus: true,
isRequired: true,
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
Navigator.pop(dialogContext, value.trim());
}
},
),
),
// Actions
Container(
padding: const EdgeInsets.all(Spacing.xl),
decoration: BoxDecoration(
color: dialogContext.conduitTheme.cardBackground,
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(AppBorderRadius.modal),
),
border: Border(
top: BorderSide(
color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1),
width: BorderWidth.regular,
),
),
),
child: Row(
children: [
Expanded(
child: ConduitButton(
text: 'Cancel',
onPressed: () => Navigator.pop(dialogContext),
isSecondary: true,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: ConduitButton(
text: 'Rename',
onPressed: () {
final newName = controller.text.trim();
if (newName.isNotEmpty) {
Navigator.pop(dialogContext, newName);
}
},
icon: Platform.isIOS ? CupertinoIcons.checkmark : Icons.check_rounded,
),
),
],
),
),
],
),
),
).animate().slideY(
begin: 0.1,
duration: AnimationDuration.modalPresentation,
curve: AnimationCurves.modalPresentation,
).fadeIn(
duration: AnimationDuration.modalPresentation,
curve: AnimationCurves.easeOut,
),
),
);
if (result != null && result.isNotEmpty && result != folder.name) {
try {
final api = ref.read(apiServiceProvider);
if (api != null) {
await api.updateFolder(folder.id, name: result);
ref.invalidate(foldersProvider);
if (mounted) {
UiUtils.showMessage(widget.parentContext ?? context, 'Folder renamed to "$result"');
}
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(widget.parentContext ?? context, 'Failed to rename folder: $e');
}
}
}
controller.dispose();
}
void _deleteFolder(Folder folder) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => Directionality(
textDirection: TextDirection.ltr,
child: Dialog(
backgroundColor: Colors.transparent,
child: Container(
width: 400,
decoration: BoxDecoration(
color: dialogContext.conduitTheme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.modal),
border: Border.all(
color: dialogContext.conduitTheme.cardBorder.withValues(alpha: 0.2),
width: BorderWidth.regular,
),
boxShadow: ConduitShadows.modal,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Container(
padding: const EdgeInsets.all(Spacing.xl),
decoration: BoxDecoration(
color: dialogContext.conduitTheme.cardBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.modal),
),
border: Border(
bottom: BorderSide(
color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1),
width: BorderWidth.regular,
),
),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: dialogContext.conduitTheme.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
),
child: Icon(
Platform.isIOS ? CupertinoIcons.delete : Icons.delete_outline_rounded,
color: dialogContext.conduitTheme.error,
size: IconSize.medium,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Delete Folder',
style: AppTypography.headlineSmallStyle.copyWith(
color: dialogContext.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: Spacing.xs),
Text(
'This action cannot be undone',
style: AppTypography.bodyMediumStyle.copyWith(
color: dialogContext.conduitTheme.error,
),
),
],
),
),
],
),
),
// Content
Padding(
padding: const EdgeInsets.all(Spacing.xl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: dialogContext.conduitTheme.surfaceContainer,
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
border: Border.all(
color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.2),
width: BorderWidth.regular,
),
),
child: Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.folder_fill : Icons.folder_rounded,
color: dialogContext.conduitTheme.iconSecondary,
size: IconSize.medium,
),
const SizedBox(width: Spacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
folder.name,
style: AppTypography.bodyLargeStyle.copyWith(
color: dialogContext.conduitTheme.textPrimary,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: Spacing.xs),
Text(
'${folder.conversationIds.length} conversation${folder.conversationIds.length != 1 ? 's' : ''}',
style: AppTypography.bodySmallStyle.copyWith(
color: dialogContext.conduitTheme.textSecondary,
),
),
],
),
),
],
),
),
const SizedBox(height: Spacing.lg),
Text(
'Are you sure you want to delete this folder?',
style: AppTypography.bodyLargeStyle.copyWith(
color: dialogContext.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: Spacing.sm),
Text(
folder.conversationIds.isNotEmpty
? 'All conversations in this folder will be moved to the main chat list.'
: 'This folder is empty and will be permanently deleted.',
style: AppTypography.bodyMediumStyle.copyWith(
color: dialogContext.conduitTheme.textSecondary,
height: 1.4,
),
),
],
),
),
// Actions
Container(
padding: const EdgeInsets.all(Spacing.xl),
decoration: BoxDecoration(
color: dialogContext.conduitTheme.cardBackground,
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(AppBorderRadius.modal),
),
border: Border(
top: BorderSide(
color: dialogContext.conduitTheme.dividerColor.withValues(alpha: 0.1),
width: BorderWidth.regular,
),
),
),
child: Row(
children: [
Expanded(
child: ConduitButton(
text: 'Cancel',
onPressed: () => Navigator.pop(dialogContext, false),
isSecondary: true,
),
),
const SizedBox(width: Spacing.md),
Expanded(
child: ConduitButton(
text: 'Delete Folder',
onPressed: () => Navigator.pop(dialogContext, true),
isDestructive: true,
icon: Platform.isIOS ? CupertinoIcons.delete : Icons.delete_outline_rounded,
),
),
],
),
),
],
),
),
).animate().slideY(
begin: 0.1,
duration: AnimationDuration.modalPresentation,
curve: AnimationCurves.modalPresentation,
).fadeIn(
duration: AnimationDuration.modalPresentation,
curve: AnimationCurves.easeOut,
),
),
);
if (confirmed == true) {
try {
final api = ref.read(apiServiceProvider);
if (api != null) {
await api.deleteFolder(folder.id);
ref.invalidate(foldersProvider);
ref.invalidate(conversationsProvider);
if (mounted) {
UiUtils.showMessage(widget.parentContext ?? context, 'Folder "${folder.name}" deleted');
}
}
} catch (e) {
if (mounted) {
UiUtils.showMessage(widget.parentContext ?? context, 'Failed to delete folder: $e');
}
}
}
}
}

View File

@@ -1,959 +0,0 @@
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';
import 'package:flutter_animate/flutter_animate.dart';
import 'dart:io' show Platform;
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/utils/platform_utils.dart';
import '../services/message_batch_service.dart';
import '../../../core/models/chat_message.dart';
import '../../../core/providers/app_providers.dart';
import '../providers/chat_providers.dart';
import '../../../shared/widgets/themed_dialogs.dart';
/// Batch operations toolbar that appears when messages are selected
class MessageBatchToolbar extends ConsumerWidget {
final List<ChatMessage> selectedMessages;
final VoidCallback? onCancel;
const MessageBatchToolbar({
super.key,
required this.selectedMessages,
this.onCancel,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final conduitTheme = context.conduitTheme;
final selectedCount = selectedMessages.length;
return Container(
height: 80,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm,
),
decoration: BoxDecoration(
color: conduitTheme.cardBackground,
border: Border(
top: BorderSide(color: conduitTheme.cardBorder, width: 1),
),
boxShadow: ConduitShadows.medium,
),
child: SafeArea(
child: Row(
children: [
// Selected count
Expanded(
child: Text(
'$selectedCount message${selectedCount == 1 ? '' : 's'} selected',
style: conduitTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
// Action buttons
_buildActionButton(
icon: Platform.isIOS
? CupertinoIcons.doc_on_clipboard
: Icons.copy,
label: 'Copy',
onPressed: () => _showCopyOptions(context, ref),
),
const SizedBox(width: Spacing.sm),
_buildActionButton(
icon: Platform.isIOS ? CupertinoIcons.share : Icons.share,
label: 'Export',
onPressed: () => _showExportOptions(context, ref),
),
const SizedBox(width: Spacing.sm),
_buildActionButton(
icon: Platform.isIOS
? CupertinoIcons.ellipsis_circle
: Icons.more_vert,
label: 'More',
onPressed: () => _showMoreOptions(context, ref),
),
const SizedBox(width: Spacing.sm),
// Cancel button
GestureDetector(
onTap: () {
PlatformUtils.lightHaptic();
onCancel?.call();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm,
),
decoration: BoxDecoration(
color: AppTheme.neutral50.withValues(alpha: Alpha.subtle),
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
),
child: Text(
'Cancel',
style: TextStyle(
color: AppTheme.neutral50.withValues(alpha: 0.8),
fontSize: AppTypography.labelLarge,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
).animate().slideY(
begin: 1,
end: 0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onPressed,
}) {
return GestureDetector(
onTap: () {
PlatformUtils.lightHaptic();
onPressed();
},
child: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: AppTheme.neutral50.withValues(alpha: Alpha.subtle),
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: AppTheme.neutral50.withValues(alpha: 0.8),
size: IconSize.md,
),
const SizedBox(height: Spacing.xxs),
Text(
label,
style: TextStyle(
color: AppTheme.neutral50.withValues(alpha: 0.8),
fontSize: AppTypography.labelSmall,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
void _showCopyOptions(BuildContext context, WidgetRef ref) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => CopyOptionsSheet(messages: selectedMessages),
);
}
void _showExportOptions(BuildContext context, WidgetRef ref) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ExportOptionsSheet(messages: selectedMessages),
);
}
void _showMoreOptions(BuildContext context, WidgetRef ref) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => MoreOptionsSheet(messages: selectedMessages),
);
}
}
/// Copy options bottom sheet
class CopyOptionsSheet extends ConsumerWidget {
final List<ChatMessage> messages;
const CopyOptionsSheet({super.key, required this.messages});
@override
Widget build(BuildContext context, WidgetRef ref) {
final conduitTheme = context.conduitTheme;
return Container(
decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.lg),
),
boxShadow: ConduitShadows.modal,
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar (standardized)
const SheetHandle(),
const SizedBox(height: Spacing.lg - Spacing.xs),
// Title
Text('Copy Messages', style: conduitTheme.headingMedium),
const SizedBox(height: Spacing.lg - Spacing.xs),
// Copy options
_buildCopyOption(
context,
ref,
icon: Icons.text_fields,
title: 'Plain Text',
subtitle: 'Copy as plain text',
format: CopyFormat.plain,
),
_buildCopyOption(
context,
ref,
icon: Icons.code,
title: 'Markdown',
subtitle: 'Copy with formatting',
format: CopyFormat.markdown,
),
_buildCopyOption(
context,
ref,
icon: Icons.data_object,
title: 'JSON',
subtitle: 'Copy as structured data',
format: CopyFormat.json,
),
const SizedBox(height: Spacing.lg - Spacing.xs),
],
),
),
);
}
Widget _buildCopyOption(
BuildContext context,
WidgetRef ref, {
required IconData icon,
required String title,
required String subtitle,
required CopyFormat format,
}) {
return ListTile(
leading: Icon(icon, color: context.conduitTheme.iconSecondary),
title: Text(
title,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
subtitle,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onTap: () async {
Navigator.pop(context);
await _copyMessages(context, ref, format);
},
);
}
Future<void> _copyMessages(
BuildContext context,
WidgetRef ref,
CopyFormat format,
) async {
try {
final batchService = ref.read(messageBatchServiceProvider);
final result = await batchService.copyMessages(
messages: messages,
format: format,
);
if (result.success) {
final content = result.data?['content'] as String?;
if (content != null) {
await Clipboard.setData(ClipboardData(text: content));
if (context.mounted) {}
}
} else {
if (context.mounted) {}
}
} catch (e) {
if (context.mounted) {}
}
}
}
/// Export options bottom sheet
class ExportOptionsSheet extends ConsumerWidget {
final List<ChatMessage> messages;
const ExportOptionsSheet({super.key, required this.messages});
@override
Widget build(BuildContext context, WidgetRef ref) {
final conduitTheme = context.conduitTheme;
return Container(
decoration: BoxDecoration(
color: context.conduitTheme.surfaceBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.lg),
),
boxShadow: ConduitShadows.modal,
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar (standardized)
const SheetHandle(),
const SizedBox(height: Spacing.lg - Spacing.xs),
// Title
Text('Export Messages', style: conduitTheme.headingMedium),
const SizedBox(height: Spacing.lg - Spacing.xs),
// Export options
_buildExportOption(
context,
ref,
icon: Icons.text_fields,
title: 'Text File',
subtitle: 'Export as plain text (.txt)',
format: ExportFormat.text,
),
_buildExportOption(
context,
ref,
icon: Icons.code,
title: 'Markdown',
subtitle: 'Export with formatting (.md)',
format: ExportFormat.markdown,
),
_buildExportOption(
context,
ref,
icon: Icons.data_object,
title: 'JSON',
subtitle: 'Export as structured data (.json)',
format: ExportFormat.json,
),
_buildExportOption(
context,
ref,
icon: Icons.table_chart,
title: 'CSV',
subtitle: 'Export as spreadsheet (.csv)',
format: ExportFormat.csv,
),
const SizedBox(height: Spacing.lg - Spacing.xs),
],
),
),
);
}
Widget _buildExportOption(
BuildContext context,
WidgetRef ref, {
required IconData icon,
required String title,
required String subtitle,
required ExportFormat format,
}) {
return ListTile(
leading: Icon(icon, color: AppTheme.neutral50.withValues(alpha: 0.8)),
title: Text(
title,
style: const TextStyle(
color: AppTheme.neutral50,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
subtitle,
style: TextStyle(
color: AppTheme.neutral50.withValues(alpha: Alpha.strong),
),
),
onTap: () {
Navigator.pop(context);
_showExportDialog(context, ref, format);
},
);
}
void _showExportDialog(
BuildContext context,
WidgetRef ref,
ExportFormat format,
) {
showDialog(
context: context,
builder: (context) => ExportDialog(messages: messages, format: format),
);
}
}
/// More options bottom sheet for additional batch operations
class MoreOptionsSheet extends ConsumerWidget {
final List<ChatMessage> messages;
const MoreOptionsSheet({super.key, required this.messages});
@override
Widget build(BuildContext context, WidgetRef ref) {
final conduitTheme = context.conduitTheme;
return Container(
decoration: BoxDecoration(
color: conduitTheme.cardBackground,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.lg),
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar (standardized)
const SheetHandle(),
const SizedBox(height: Spacing.lg - Spacing.xs),
// Title
Text('More Actions', style: conduitTheme.headingMedium),
const SizedBox(height: Spacing.lg - Spacing.xs),
// More options
ListTile(
leading: Icon(
Icons.label_outline,
color: context.conduitTheme.iconSecondary,
),
title: Text(
'Add Tags',
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
'Tag selected messages',
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onTap: () {
Navigator.pop(context);
_showTagDialog(context, ref);
},
),
ListTile(
leading: Icon(
Icons.archive_outlined,
color: context.conduitTheme.iconSecondary,
),
title: Text(
'Archive',
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
'Archive selected messages',
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onTap: () {
Navigator.pop(context);
_archiveMessages(context, ref);
},
),
ListTile(
leading: Icon(
Icons.delete_outline,
color: context.conduitTheme.error,
),
title: Text(
'Delete',
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.error,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
'Delete selected messages',
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
onTap: () {
Navigator.pop(context);
_showDeleteConfirmation(context, ref);
},
),
const SizedBox(height: Spacing.lg - Spacing.xs),
],
),
),
);
}
void _showTagDialog(BuildContext context, WidgetRef ref) async {
final activeConversation = ref.read(activeConversationProvider);
if (activeConversation == null) return;
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
backgroundColor: context.conduitTheme.surfaceBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.dialog),
),
title: Text(
'Manage Tags',
style: TextStyle(color: context.conduitTheme.textPrimary),
),
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add new tag input
TextField(
controller: controller,
style: TextStyle(color: context.conduitTheme.textPrimary),
decoration: InputDecoration(
hintText: 'Add a tag',
hintStyle: TextStyle(
color: context.conduitTheme.inputPlaceholder,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: context.conduitTheme.inputBorder,
),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.conduitTheme.inputBorder,
),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.conduitTheme.buttonPrimary,
),
),
suffixIcon: IconButton(
icon: Icon(
Icons.add,
color: context.conduitTheme.buttonPrimary,
),
onPressed: () async {
final tag = controller.text.trim();
if (tag.isNotEmpty) {
try {
final api = ref.read(apiServiceProvider);
if (api != null) {
await api.addTagToConversation(
activeConversation.id,
tag,
);
controller.clear();
setState(() {}); // Refresh the dialog
if (context.mounted) {}
}
} catch (e) {
if (context.mounted) {}
}
}
},
),
),
),
const SizedBox(height: Spacing.md),
// Current tags
FutureBuilder<List<String>>(
future: _loadConversationTags(ref, activeConversation.id),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(
color: context.conduitTheme.buttonPrimary,
),
);
}
final tags = snapshot.data ?? [];
if (tags.isEmpty) {
return Text(
'No tags yet',
style: TextStyle(
color: context.conduitTheme.textSecondary,
),
);
}
return Wrap(
spacing: 8,
runSpacing: 8,
children: tags
.map(
(tag) => Chip(
label: Text(
tag,
style: TextStyle(
color: context.conduitTheme.textPrimary,
),
),
backgroundColor: context
.conduitTheme
.buttonPrimary
.withValues(alpha: 0.2),
deleteIcon: Icon(
Icons.close,
color: context.conduitTheme.iconSecondary,
size: IconSize.sm,
),
onDeleted: () async {
try {
final api = ref.read(apiServiceProvider);
if (api != null) {
await api.removeTagFromConversation(
activeConversation.id,
tag,
);
setState(() {}); // Refresh the dialog
if (context.mounted) {}
}
} catch (e) {
if (context.mounted) {}
}
},
),
)
.toList(),
);
},
),
],
),
),
actions: [
TextButton(
onPressed: () {
controller.dispose();
Navigator.pop(context);
},
child: Text(
'Done',
style: TextStyle(
color: AppTheme.neutral50.withValues(alpha: Alpha.strong),
),
),
),
],
),
),
);
}
Future<List<String>> _loadConversationTags(
WidgetRef ref,
String conversationId,
) async {
try {
final api = ref.read(apiServiceProvider);
if (api != null) {
return await api.getConversationTags(conversationId);
}
} catch (e) {
// Return empty list on error
}
return [];
}
void _archiveMessages(BuildContext context, WidgetRef ref) async {
final activeConversation = ref.read(activeConversationProvider);
if (activeConversation == null) return;
final confirmed = await ThemedDialogs.confirm(
context,
title: 'Archive Conversation',
message:
'Archive this conversation? You can find it in the archived conversations section.',
confirmText: 'Archive',
);
if (confirmed == true) {
try {
final api = ref.read(apiServiceProvider);
if (api != null) {
await api.archiveConversation(activeConversation.id, true);
ref.invalidate(conversationsProvider);
ref.invalidate(archivedConversationsProvider);
if (context.mounted) {
// Navigate back or clear current conversation
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
} catch (e) {
if (context.mounted) {}
}
}
}
void _showDeleteConfirmation(BuildContext context, WidgetRef ref) async {
final confirmed = await ThemedDialogs.confirm(
context,
title: 'Delete Messages',
message:
'Are you sure you want to delete ${messages.length} message${messages.length == 1 ? '' : 's'}? This action cannot be undone.',
confirmText: 'Delete',
isDestructive: true,
);
if (confirmed == true && context.mounted) {
_deleteMessages(context, ref);
}
}
void _deleteMessages(BuildContext context, WidgetRef ref) async {
final activeConversation = ref.read(activeConversationProvider);
if (activeConversation == null) return;
final confirmed = await ThemedDialogs.confirm(
context,
title: 'Delete Conversation',
message:
'Are you sure you want to delete this conversation?\n\nThis action cannot be undone.',
confirmText: 'Delete',
isDestructive: true,
);
if (confirmed == true) {
try {
final api = ref.read(apiServiceProvider);
if (api != null) {
await api.deleteConversation(activeConversation.id);
ref.invalidate(conversationsProvider);
ref.invalidate(archivedConversationsProvider);
// Clear the current conversation
ref.read(activeConversationProvider.notifier).state = null;
ref.read(chatMessagesProvider.notifier).clearMessages();
if (context.mounted) {
// Navigate back to conversation list
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
} catch (e) {
if (context.mounted) {}
}
}
}
}
/// Export dialog with options
class ExportDialog extends ConsumerStatefulWidget {
final List<ChatMessage> messages;
final ExportFormat format;
const ExportDialog({super.key, required this.messages, required this.format});
@override
ConsumerState<ExportDialog> createState() => _ExportDialogState();
}
class _ExportDialogState extends ConsumerState<ExportDialog> {
bool _includeTimestamps = true;
bool _includeMetadata = false;
bool _includeAttachments = true;
bool _isExporting = false;
@override
Widget build(BuildContext context) {
final conduitTheme = context.conduitTheme;
return AlertDialog(
backgroundColor: AppTheme.neutral700,
title: Text('Export Options', style: conduitTheme.headingMedium),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Export ${widget.messages.length} messages as ${widget.format.name.toUpperCase()}',
style: conduitTheme.bodyMedium?.copyWith(
color: AppTheme.neutral50.withValues(alpha: 0.8),
),
),
const SizedBox(height: Spacing.lg - Spacing.xs),
// Export options
CheckboxListTile(
title: const Text(
'Include timestamps',
style: TextStyle(color: AppTheme.neutral50),
),
value: _includeTimestamps,
onChanged: (value) =>
setState(() => _includeTimestamps = value ?? true),
activeColor: AppTheme.brandPrimary,
),
CheckboxListTile(
title: const Text(
'Include metadata',
style: TextStyle(color: AppTheme.neutral50),
),
value: _includeMetadata,
onChanged: (value) =>
setState(() => _includeMetadata = value ?? false),
activeColor: AppTheme.brandPrimary,
),
CheckboxListTile(
title: const Text(
'Include attachments',
style: TextStyle(color: AppTheme.neutral50),
),
value: _includeAttachments,
onChanged: (value) =>
setState(() => _includeAttachments = value ?? true),
activeColor: AppTheme.brandPrimary,
),
],
),
actions: [
TextButton(
onPressed: _isExporting ? null : () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: _isExporting ? null : _performExport,
child: _isExporting
? const SizedBox(
width: Spacing.md,
height: Spacing.md,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Export'),
),
],
);
}
Future<void> _performExport() async {
setState(() => _isExporting = true);
try {
final batchService = ref.read(messageBatchServiceProvider);
final options = ExportOptions(
includeTimestamps: _includeTimestamps,
includeMetadata: _includeMetadata,
includeAttachments: _includeAttachments,
);
final result = await batchService.exportMessages(
messages: widget.messages,
format: widget.format,
options: options,
);
if (result.success && mounted) {
Navigator.pop(context);
// In a real app, you would save the file or share it
// For now, we'll copy to clipboard
final content = result.data?['content'] as String?;
if (content != null) {
await Clipboard.setData(ClipboardData(text: content));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Export copied to clipboard (${widget.format.name.toUpperCase()})',
),
backgroundColor: AppTheme.success,
),
);
}
}
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Export failed: ${result.error}'),
backgroundColor: AppTheme.error,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Export error: $e'),
backgroundColor: AppTheme.error,
),
);
}
} finally {
if (mounted) {
setState(() => _isExporting = false);
}
}
}
}

View File

@@ -1,237 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../shared/theme/theme_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/models/conversation.dart';
import '../../../core/providers/app_providers.dart';
class TagManagementDialog extends ConsumerStatefulWidget {
final Conversation conversation;
const TagManagementDialog({super.key, required this.conversation});
@override
ConsumerState<TagManagementDialog> createState() =>
_TagManagementDialogState();
}
class _TagManagementDialogState extends ConsumerState<TagManagementDialog> {
final _tagController = TextEditingController();
bool _isAdding = false;
@override
void dispose() {
_tagController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final conversationTags = widget.conversation.tags;
return Dialog(
child: Container(
width: 400,
constraints: const BoxConstraints(maxHeight: 500),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(Spacing.md),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppBorderRadius.lg),
),
),
child: Row(
children: [
Icon(
Platform.isIOS ? CupertinoIcons.tag : Icons.label,
color: theme.colorScheme.onPrimaryContainer,
),
const SizedBox(width: Spacing.sm),
Text(
'Manage Tags',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
IconButton(
icon: Icon(
Platform.isIOS ? CupertinoIcons.xmark : Icons.close,
color: theme.colorScheme.onPrimaryContainer,
),
onPressed: () => Navigator.pop(context),
),
],
),
),
// Add new tag section
Padding(
padding: const EdgeInsets.all(Spacing.md),
child: Row(
children: [
Expanded(
child: TextField(
controller: _tagController,
decoration: InputDecoration(
hintText: 'Add new tag',
border: const OutlineInputBorder(),
prefixIcon: Icon(
Platform.isIOS
? CupertinoIcons.tag_fill
: Icons.label,
),
),
onSubmitted: (_) => _addTag(),
),
),
const SizedBox(width: Spacing.sm),
ElevatedButton(
onPressed: _isAdding ? null : _addTag,
child: _isAdding
? const SizedBox(
width: Spacing.md,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Add'),
),
],
),
),
const Divider(height: 1),
// Current tags
Expanded(
child: conversationTags.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Platform.isIOS
? CupertinoIcons.tag
: Icons.label_outline,
size: 48,
color: theme.colorScheme.onSurface.withValues(
alpha: 0.3,
),
),
const SizedBox(height: Spacing.md),
Text(
'No tags yet',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.6,
),
),
),
const SizedBox(height: Spacing.sm),
Text(
'Add tags to organize and find conversations easily',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(
alpha: 0.5,
),
),
textAlign: TextAlign.center,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(Spacing.md),
itemCount: conversationTags.length,
itemBuilder: (context, index) {
final tag = conversationTags[index];
return _buildTagChip(context, tag);
},
),
),
// Bottom actions
Padding(
padding: const EdgeInsets.all(Spacing.md),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Done'),
),
),
),
],
),
),
);
}
Widget _buildTagChip(BuildContext context, String tag) {
final theme = Theme.of(context);
return Container(
margin: const EdgeInsets.only(bottom: 8),
child: Chip(
avatar: Icon(
Platform.isIOS ? CupertinoIcons.tag_fill : Icons.label,
size: 16,
color: theme.colorScheme.onPrimaryContainer,
),
label: Text(tag),
backgroundColor: theme.colorScheme.primaryContainer,
deleteIcon: Icon(
Platform.isIOS ? CupertinoIcons.xmark_circle_fill : Icons.cancel,
size: 18,
),
onDeleted: () => _removeTag(tag),
),
);
}
Future<void> _addTag() async {
final tag = _tagController.text.trim();
if (tag.isEmpty || widget.conversation.tags.contains(tag)) return;
setState(() => _isAdding = true);
try {
final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service available');
await api.addTagToConversation(widget.conversation.id, tag);
ref.invalidate(conversationsProvider);
_tagController.clear();
if (mounted) {}
} catch (e) {
if (mounted) {}
} finally {
setState(() => _isAdding = false);
}
}
Future<void> _removeTag(String tag) async {
try {
final api = ref.read(apiServiceProvider);
if (api == null) throw Exception('No API service available');
await api.removeTagFromConversation(widget.conversation.id, tag);
ref.invalidate(conversationsProvider);
if (mounted) {}
} catch (e) {
if (mounted) {}
}
}
}