import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../core/providers/app_providers.dart'; import '../../../core/services/api_service.dart'; import 'enhanced_image_attachment.dart'; import 'package:share_plus/share_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; import 'dart:convert'; class EnhancedAttachment extends ConsumerStatefulWidget { final String attachmentId; final bool isMarkdownFormat; final BoxConstraints? constraints; final bool isUserMessage; final bool disableAnimation; const EnhancedAttachment({ super.key, required this.attachmentId, this.isMarkdownFormat = false, this.constraints, this.isUserMessage = false, this.disableAnimation = false, }); @override ConsumerState createState() => _EnhancedAttachmentState(); } class _EnhancedAttachmentState extends ConsumerState { Map? _fileInfo; bool _isLoading = true; String? _error; String? _localFilePath; @override void initState() { super.initState(); _resolveType(); } Future _resolveType() async { try { // Data URL for images – short-circuit to image widget if (widget.attachmentId.startsWith('data:image/')) { setState(() { _isLoading = false; _fileInfo = {'mime': 'image/inline'}; }); return; } final api = ref.read(apiServiceProvider); if (api is! ApiService) { setState(() { _isLoading = false; _error = 'Service unavailable'; }); return; } final info = await api.getFileInfo(widget.attachmentId); setState(() { _fileInfo = info; _isLoading = false; }); } catch (e) { setState(() { _error = 'Failed to load attachment'; _isLoading = false; }); } } bool _isImageFile(Map? info) { if (info == null) return false; final mime = (info['content_type'] ?? info['mime'] ?? '') .toString() .toLowerCase(); if (mime.startsWith('image/')) return true; final name = (info['filename'] ?? info['name'] ?? '') .toString() .toLowerCase(); final ext = name.split('.').length > 1 ? name.split('.').last : ''; return ['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext); } Future _ensureLocalFile() async { if (_localFilePath != null && await File(_localFilePath!).exists()) { return _localFilePath; } try { final api = ref.read(apiServiceProvider); if (api is! ApiService) return null; final content = await api.getFileContent(widget.attachmentId); final filename = (_fileInfo?['filename'] ?? _fileInfo?['name'] ?? 'file') .toString(); final dir = await getTemporaryDirectory(); final filePath = '${dir.path}/$filename'; try { if (content.length > 128 && RegExp( r'^[A-Za-z0-9+/=\r\n]+$', ).hasMatch(content.replaceAll('\n', ''))) { final bytes = base64Decode(content.replaceAll('\n', '')); await File(filePath).writeAsBytes(bytes, flush: true); } else { await File(filePath).writeAsString(content, flush: true); } } catch (_) { await File(filePath).writeAsString(content, flush: true); } _localFilePath = filePath; return _localFilePath; } catch (e) { setState(() { _error = 'Failed to prepare file'; }); return null; } } Future _shareFile() async { final path = await _ensureLocalFile(); if (path == null) return; final filename = (_fileInfo?['filename'] ?? _fileInfo?['name'] ?? 'file') .toString(); await SharePlus.instance.share( ShareParams(files: [XFile(path, name: filename)]), ); } String _fileIconFor(String filename) { final lower = filename.toLowerCase(); String ext = ''; final parts = lower.split('.'); if (parts.length > 1) ext = parts.last; if (['pdf', 'doc', 'docx'].contains(ext)) return '📄'; if (['xls', 'xlsx'].contains(ext)) return '📊'; if (['ppt', 'pptx'].contains(ext)) return '📊'; if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) return '🖼️'; if (['js', 'ts', 'py', 'dart', 'java', 'cpp'].contains(ext)) return '💻'; if (['html', 'css', 'json', 'xml'].contains(ext)) return '🌐'; if (['zip', 'rar', '7z', 'tar', 'gz'].contains(ext)) return '📦'; if (['mp3', 'wav', 'flac', 'm4a'].contains(ext)) return '🎵'; if (['mp4', 'avi', 'mov', 'mkv'].contains(ext)) return '🎬'; return '📎'; } @override Widget build(BuildContext context) { if (_isLoading) { return Container( width: widget.constraints?.maxWidth ?? 160, height: 84, decoration: BoxDecoration( color: context.conduitTheme.cardBackground, borderRadius: BorderRadius.circular(AppBorderRadius.md), border: Border.all( color: context.conduitTheme.textPrimary.withValues(alpha: 0.1), width: BorderWidth.regular, ), ), ); } if (_error != null) { return Container( padding: const EdgeInsets.all(Spacing.sm), decoration: BoxDecoration( color: context.conduitTheme.surfaceContainer, borderRadius: BorderRadius.circular(AppBorderRadius.md), border: Border.all( color: context.conduitTheme.error.withValues(alpha: 0.3), width: BorderWidth.regular, ), ), child: Text( _error!, style: TextStyle( color: context.conduitTheme.error, fontSize: AppTypography.labelMedium, ), ), ); } // Image: delegate to existing image widget for consistency if (_isImageFile(_fileInfo)) { return EnhancedImageAttachment( attachmentId: widget.attachmentId, isMarkdownFormat: widget.isMarkdownFormat, constraints: widget.constraints, isUserMessage: widget.isUserMessage, disableAnimation: widget.disableAnimation, ); } final filename = (_fileInfo?['filename'] ?? _fileInfo?['name'] ?? 'File') .toString(); final size = _fileInfo?['size']; final sizeLabel = size is num ? _formatSize(size.toInt()) : null; final lowerName = filename.toLowerCase(); final fileExtension = lowerName.contains('.') ? lowerName.split('.').last : ''; final List metaParts = []; if (fileExtension.isNotEmpty) { metaParts.add('.${fileExtension.toUpperCase()}'); } if (sizeLabel != null) { metaParts.add(sizeLabel); } final metaLabel = metaParts.join(' • '); final card = Container( constraints: widget.constraints, padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( color: context.conduitTheme.cardBackground, borderRadius: BorderRadius.circular(AppBorderRadius.md), border: Border.all( color: context.conduitTheme.textPrimary.withValues(alpha: 0.12), width: BorderWidth.regular, ), ), child: Row( mainAxisSize: MainAxisSize.max, children: [ Text( _fileIconFor(filename), style: const TextStyle(fontSize: AppTypography.headlineLarge), ), const SizedBox(width: Spacing.sm), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( filename, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: context.conduitTheme.textPrimary, fontSize: AppTypography.labelLarge, fontWeight: FontWeight.w600, ), ), if (metaLabel.isNotEmpty) Text( metaLabel, style: TextStyle( color: context.conduitTheme.textSecondary.withValues( alpha: 0.7, ), fontSize: AppTypography.labelMedium, ), ), ], ), ), ], ), ); return InkWell( borderRadius: BorderRadius.circular(AppBorderRadius.md), onTap: () async { await HapticFeedback.mediumImpact(); await _shareFile(); }, child: card, ); } String _formatSize(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'; } }