Files
iiEsaywebUIapp/lib/features/notes/widgets/note_file_attachment.dart

313 lines
9.7 KiB
Dart

import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:conduit/l10n/app_localizations.dart';
import '../../../shared/theme/theme_extensions.dart';
/// A widget that displays a file attachment in a note.
///
/// Supports different file types with appropriate icons and actions.
class NoteFileAttachment extends StatelessWidget {
/// The file data from the note.
final Map<String, dynamic> file;
/// Called when the file is tapped.
final VoidCallback? onTap;
/// Called when the delete button is pressed.
final VoidCallback? onDelete;
/// Whether the file is currently loading.
final bool isLoading;
/// Whether to show the delete button.
final bool showDelete;
const NoteFileAttachment({
super.key,
required this.file,
this.onTap,
this.onDelete,
this.isLoading = false,
this.showDelete = true,
});
String get _fileName => file['name']?.toString() ?? 'Unknown file';
String get _fileType => file['type']?.toString() ?? 'file';
int? get _fileSize => file['size'] as int?;
bool get _isAudio =>
_fileType == 'audio' ||
_fileName.endsWith('.m4a') ||
_fileName.endsWith('.mp3') ||
_fileName.endsWith('.wav') ||
_fileName.endsWith('.aac');
bool get _isImage => _fileType == 'image';
IconData get _icon {
if (_isAudio) {
return Platform.isIOS
? CupertinoIcons.waveform
: Icons.audio_file_rounded;
}
if (_isImage) {
return Platform.isIOS ? CupertinoIcons.photo : Icons.image_rounded;
}
return Platform.isIOS
? CupertinoIcons.doc_fill
: Icons.insert_drive_file_rounded;
}
Color _iconColor(ConduitThemeExtension theme) {
if (_isAudio) return Colors.orange;
if (_isImage) return Colors.blue;
return theme.textSecondary;
}
String _formatFileSize(int? bytes) {
if (bytes == null) return '';
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final l10n = AppLocalizations.of(context)!;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.sm,
),
decoration: BoxDecoration(
color: theme.surfaceContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: theme.cardBorder.withValues(alpha: 0.3),
width: BorderWidth.thin,
),
),
child: Row(
children: [
// File icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _iconColor(theme).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
),
child: isLoading
? Center(
child: SizedBox(
width: IconSize.sm,
height: IconSize.sm,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(
_iconColor(theme),
),
),
),
)
: Icon(
_icon,
color: _iconColor(theme),
size: IconSize.md,
),
),
const SizedBox(width: Spacing.sm),
// File info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_fileName,
style: AppTypography.bodySmallStyle.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Row(
children: [
Text(
_isAudio
? l10n.audioFileType
: _isImage
? l10n.imageFileType
: l10n.file,
style: AppTypography.captionStyle.copyWith(
color: theme.textSecondary,
),
),
if (_fileSize != null) ...[
Text(
' · ',
style: AppTypography.captionStyle.copyWith(
color: theme.textSecondary,
),
),
Text(
_formatFileSize(_fileSize),
style: AppTypography.captionStyle.copyWith(
color: theme.textSecondary,
),
),
],
],
),
],
),
),
// Play button for audio
if (_isAudio && !isLoading)
IconButton(
icon: Icon(
Platform.isIOS
? CupertinoIcons.play_circle_fill
: Icons.play_circle_filled_rounded,
color: _iconColor(theme),
size: IconSize.lg,
),
onPressed: onTap,
tooltip: l10n.playAudio,
),
// Delete button
if (showDelete && !isLoading)
IconButton(
icon: Icon(
Platform.isIOS
? CupertinoIcons.xmark_circle_fill
: Icons.cancel_rounded,
color: theme.textSecondary.withValues(alpha: 0.5),
size: IconSize.md,
),
onPressed: () {
HapticFeedback.lightImpact();
onDelete?.call();
},
tooltip: l10n.removeFile,
),
],
),
),
),
);
}
}
/// A section that displays all file attachments for a note.
class NoteFilesSection extends StatelessWidget {
/// The list of files attached to the note.
final List<Map<String, dynamic>> files;
/// Called when a file should be played (for audio).
final void Function(Map<String, dynamic> file)? onPlayFile;
/// Called when a file should be deleted.
final void Function(Map<String, dynamic> file)? onDeleteFile;
/// Whether files can be deleted.
final bool canDelete;
const NoteFilesSection({
super.key,
required this.files,
this.onPlayFile,
this.onDeleteFile,
this.canDelete = true,
});
@override
Widget build(BuildContext context) {
if (files.isEmpty) return const SizedBox.shrink();
final theme = context.conduitTheme;
final l10n = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section header
Padding(
padding: const EdgeInsets.only(
left: Spacing.xs,
bottom: Spacing.xs,
),
child: Row(
children: [
Icon(
Platform.isIOS
? CupertinoIcons.paperclip
: Icons.attach_file_rounded,
size: IconSize.sm,
color: theme.textSecondary,
),
const SizedBox(width: Spacing.xs),
Text(
l10n.attachments,
style: AppTypography.labelStyle.copyWith(
color: theme.textSecondary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: Spacing.xs),
Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.xs,
vertical: 2,
),
decoration: BoxDecoration(
color: theme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
child: Text(
'${files.length}',
style: AppTypography.captionStyle.copyWith(
color: theme.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
// Files list
...files.map(
(file) => Padding(
padding: const EdgeInsets.only(bottom: Spacing.xs),
child: NoteFileAttachment(
file: file,
showDelete: canDelete,
onTap: () => onPlayFile?.call(file),
onDelete: () => onDeleteFile?.call(file),
),
),
),
],
);
}
}