220 lines
6.2 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|