refactor: cleanup unsued files
This commit is contained in:
@@ -1,139 +0,0 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/utils/debug_logger.dart';
|
||||
|
||||
// Global attachment cache state
|
||||
class AttachmentCacheState {
|
||||
final Map<String, String> imageDataCache;
|
||||
final Map<String, bool> loadingStates;
|
||||
final Map<String, String> errorStates;
|
||||
|
||||
AttachmentCacheState({
|
||||
required this.imageDataCache,
|
||||
required this.loadingStates,
|
||||
required this.errorStates,
|
||||
});
|
||||
|
||||
AttachmentCacheState copyWith({
|
||||
Map<String, String>? imageDataCache,
|
||||
Map<String, bool>? loadingStates,
|
||||
Map<String, String>? errorStates,
|
||||
}) {
|
||||
return AttachmentCacheState(
|
||||
imageDataCache: imageDataCache ?? this.imageDataCache,
|
||||
loadingStates: loadingStates ?? this.loadingStates,
|
||||
errorStates: errorStates ?? this.errorStates,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentCacheNotifier extends StateNotifier<AttachmentCacheState> {
|
||||
AttachmentCacheNotifier()
|
||||
: super(AttachmentCacheState(
|
||||
imageDataCache: {},
|
||||
loadingStates: {},
|
||||
errorStates: {},
|
||||
));
|
||||
|
||||
void cacheImageData(String attachmentId, String imageData) {
|
||||
DebugLogger.log('Caching image data for: $attachmentId');
|
||||
state = state.copyWith(
|
||||
imageDataCache: {
|
||||
...state.imageDataCache,
|
||||
attachmentId: imageData,
|
||||
},
|
||||
);
|
||||
|
||||
// Limit cache size to prevent memory issues
|
||||
if (state.imageDataCache.length > 100) {
|
||||
final newCache = Map<String, String>.from(state.imageDataCache);
|
||||
final keysToRemove = newCache.keys.take(20).toList();
|
||||
for (final key in keysToRemove) {
|
||||
newCache.remove(key);
|
||||
state.loadingStates.remove(key);
|
||||
state.errorStates.remove(key);
|
||||
}
|
||||
state = state.copyWith(imageDataCache: newCache);
|
||||
}
|
||||
}
|
||||
|
||||
String? getCachedImageData(String attachmentId) {
|
||||
return state.imageDataCache[attachmentId];
|
||||
}
|
||||
|
||||
void setLoadingState(String attachmentId, bool isLoading) {
|
||||
state = state.copyWith(
|
||||
loadingStates: {
|
||||
...state.loadingStates,
|
||||
attachmentId: isLoading,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool isLoading(String attachmentId) {
|
||||
return state.loadingStates[attachmentId] ?? false;
|
||||
}
|
||||
|
||||
void setErrorState(String attachmentId, String? error) {
|
||||
if (error == null) {
|
||||
final newErrorStates = Map<String, String>.from(state.errorStates);
|
||||
newErrorStates.remove(attachmentId);
|
||||
state = state.copyWith(errorStates: newErrorStates);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
errorStates: {
|
||||
...state.errorStates,
|
||||
attachmentId: error,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? getErrorState(String attachmentId) {
|
||||
return state.errorStates[attachmentId];
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
state = AttachmentCacheState(
|
||||
imageDataCache: {},
|
||||
loadingStates: {},
|
||||
errorStates: {},
|
||||
);
|
||||
}
|
||||
|
||||
void clearAttachmentCache(String attachmentId) {
|
||||
final newImageCache = Map<String, String>.from(state.imageDataCache);
|
||||
final newLoadingStates = Map<String, bool>.from(state.loadingStates);
|
||||
final newErrorStates = Map<String, String>.from(state.errorStates);
|
||||
|
||||
newImageCache.remove(attachmentId);
|
||||
newLoadingStates.remove(attachmentId);
|
||||
newErrorStates.remove(attachmentId);
|
||||
|
||||
state = AttachmentCacheState(
|
||||
imageDataCache: newImageCache,
|
||||
loadingStates: newLoadingStates,
|
||||
errorStates: newErrorStates,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final attachmentCacheProvider =
|
||||
StateNotifierProvider<AttachmentCacheNotifier, AttachmentCacheState>((ref) {
|
||||
return AttachmentCacheNotifier();
|
||||
});
|
||||
|
||||
// Helper providers for easier access
|
||||
final cachedImageDataProvider = Provider.family<String?, String>((ref, attachmentId) {
|
||||
final cache = ref.watch(attachmentCacheProvider);
|
||||
return cache.imageDataCache[attachmentId];
|
||||
});
|
||||
|
||||
final attachmentLoadingStateProvider = Provider.family<bool, String>((ref, attachmentId) {
|
||||
final cache = ref.watch(attachmentCacheProvider);
|
||||
return cache.loadingStates[attachmentId] ?? false;
|
||||
});
|
||||
|
||||
final attachmentErrorStateProvider = Provider.family<String?, String>((ref, attachmentId) {
|
||||
final cache = ref.watch(attachmentCacheProvider);
|
||||
return cache.errorStates[attachmentId];
|
||||
});
|
||||
@@ -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}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')}';
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user