Files
worker/lib/features/auth/presentation/widgets/file_upload_card.dart
Phuoc Nguyen 49a41d24eb update theme
2025-12-02 15:20:54 +07:00

220 lines
6.2 KiB
Dart

/// File Upload Card Widget
///
/// Reusable widget for uploading image files with preview.
library;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// File Upload Card
///
/// A reusable widget for uploading files with preview functionality.
/// Features:
/// - Dashed border upload area
/// - Camera/file icon
/// - Title and subtitle
/// - Image preview after selection
/// - Remove button
///
/// Usage:
/// ```dart
/// FileUploadCard(
/// file: selectedFile,
/// onTap: () => pickImage(),
/// onRemove: () => removeImage(),
/// icon: Icons.camera_alt,
/// title: 'Chụp ảnh hoặc chọn file',
/// subtitle: 'JPG, PNG tối đa 5MB',
/// )
/// ```
class FileUploadCard extends StatelessWidget {
/// Creates a file upload card
const FileUploadCard({
super.key,
required this.file,
required this.onTap,
required this.onRemove,
required this.icon,
required this.title,
required this.subtitle,
});
/// Selected file (null if not selected)
final File? file;
/// Callback when upload area is tapped
final VoidCallback onTap;
/// Callback to remove selected file
final VoidCallback onRemove;
/// Icon to display in upload area
final IconData icon;
/// Title text
final String title;
/// Subtitle text
final String subtitle;
/// Format file size in bytes to human-readable string
String _formatFileSize(int bytes) {
if (bytes == 0) return '0 B';
const suffixes = ['B', 'KB', 'MB', 'GB'];
final i = (bytes.bitLength - 1) ~/ 10;
final size = bytes / (1 << (i * 10));
return '${size.toStringAsFixed(2)} ${suffixes[i]}';
}
/// Get file name from path
String _getFileName(String path) {
return path.split('/').last;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
if (file != null) {
// Show preview with remove option
return _buildPreview(context, colorScheme);
} else {
// Show upload area
return _buildUploadArea(context, colorScheme);
}
}
/// Build upload area
Widget _buildUploadArea(BuildContext context, ColorScheme colorScheme) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Container(
decoration: BoxDecoration(
color: colorScheme.surface,
border: Border.all(
color: colorScheme.outlineVariant,
width: 2,
strokeAlign: BorderSide.strokeAlignInside,
),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
children: [
// Icon
Icon(icon, size: 32, color: colorScheme.onSurfaceVariant),
const SizedBox(height: AppSpacing.sm),
// Title
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: AppSpacing.xs),
// Subtitle
Text(
subtitle,
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
),
],
),
),
);
}
/// Build preview with remove button
Widget _buildPreview(BuildContext context, ColorScheme colorScheme) {
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
border: Border.all(color: colorScheme.surfaceContainerHighest, width: 1),
borderRadius: BorderRadius.circular(AppRadius.md),
),
padding: const EdgeInsets.all(AppSpacing.sm),
child: Row(
children: [
// Thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.sm),
child: Image.file(
file!,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 50,
height: 50,
color: colorScheme.surfaceContainerHighest,
child: Icon(
FontAwesomeIcons.image,
color: colorScheme.onSurfaceVariant,
size: 24,
),
);
},
),
),
const SizedBox(width: AppSpacing.sm),
// File info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getFileName(file!.path),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
FutureBuilder<int>(
future: file!.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
_formatFileSize(snapshot.data!),
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant,
),
);
}
return const SizedBox.shrink();
},
),
],
),
),
const SizedBox(width: AppSpacing.xs),
// Remove button
IconButton(
icon: const FaIcon(FontAwesomeIcons.xmark, color: AppColors.danger, size: 18),
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
),
);
}
}