Files
unionflow-mobile-apps/lib/shared/widgets/file_upload_widget.dart
dahoud 0abcdcc478 feat(design-system): dark mode adaptatif sur widgets partagés
Pattern AppColors pair (isDark ? AppColors.surfaceDark : AppColors.surface)
appliqué sur :
- UnionStatWidget, UnionBalanceCard, UnionActionButton, UFSectionHeader
- DashboardEventRow, DashboardActivityRow, UnionExportButton
- MiniAvatar (border adaptatif)
- ConfirmationDialog (cancel colors adaptés)
- FileUploadWidget (textSecondary adaptatif)

Les couleurs surface/border/textPrimary/textSecondary hardcodées (light-only)
sont remplacées par les paires *Dark conditionnelles.
Les couleurs sémantiques (error, success, warning, primary) restent inchangées.
2026-04-15 20:13:22 +00:00

261 lines
7.5 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import '../design_system/tokens/app_colors.dart';
/// Widget réutilisable pour uploader un fichier (image ou PDF)
/// avec prévisualisation et validation
class FileUploadWidget extends StatefulWidget {
final Function(File?) onFileSelected;
final String? initialFileName;
final bool showImagePreview;
const FileUploadWidget({
super.key,
required this.onFileSelected,
this.initialFileName,
this.showImagePreview = true,
});
@override
State<FileUploadWidget> createState() => _FileUploadWidgetState();
}
class _FileUploadWidgetState extends State<FileUploadWidget> {
File? _selectedFile;
final ImagePicker _imagePicker = ImagePicker();
@override
void initState() {
super.initState();
if (widget.initialFileName != null) {
_selectedFile = File(widget.initialFileName!);
}
}
Future<void> _pickImage(ImageSource source) async {
try {
final XFile? image = await _imagePicker.pickImage(
source: source,
maxWidth: 1920,
maxHeight: 1920,
imageQuality: 85,
);
if (image != null) {
final file = File(image.path);
final fileSize = await file.length();
// Vérifier la taille (max 5 MB)
if (fileSize > 5 * 1024 * 1024) {
if (mounted) {
_showError('Fichier trop volumineux. Taille max: 5 MB');
}
return;
}
setState(() {
_selectedFile = file;
});
widget.onFileSelected(file);
}
} catch (e) {
_showError('Erreur lors de la sélection de l\'image: $e');
}
}
Future<void> _pickPdf() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final fileSize = await file.length();
// Vérifier la taille (max 5 MB)
if (fileSize > 5 * 1024 * 1024) {
if (mounted) {
_showError('Fichier trop volumineux. Taille max: 5 MB');
}
return;
}
setState(() {
_selectedFile = file;
});
widget.onFileSelected(file);
}
} catch (e) {
_showError('Erreur lors de la sélection du PDF: $e');
}
}
void _removeFile() {
setState(() {
_selectedFile = null;
});
widget.onFileSelected(null);
}
void _showError(String message) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.error,
),
);
}
}
void _showPickerOptions() {
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Wrap(
children: [
ListTile(
leading: const Icon(Icons.photo_camera),
title: const Text('Prendre une photo'),
onTap: () {
Navigator.pop(context);
_pickImage(ImageSource.camera);
},
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Choisir une image'),
onTap: () {
Navigator.pop(context);
_pickImage(ImageSource.gallery);
},
),
ListTile(
leading: const Icon(Icons.picture_as_pdf),
title: const Text('Choisir un PDF'),
onTap: () {
Navigator.pop(context);
_pickPdf();
},
),
],
),
),
);
}
bool _isImage() {
if (_selectedFile == null) return false;
final ext = _selectedFile!.path.split('.').last.toLowerCase();
return ['jpg', 'jpeg', 'png', 'gif'].contains(ext);
}
bool _isPdf() {
if (_selectedFile == null) return false;
return _selectedFile!.path.toLowerCase().endsWith('.pdf');
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Pièce justificative',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
if (_selectedFile != null)
IconButton(
icon: const Icon(Icons.delete, color: AppColors.error),
onPressed: _removeFile,
),
],
),
const SizedBox(height: 8),
if (_selectedFile == null)
OutlinedButton.icon(
onPressed: _showPickerOptions,
icon: const Icon(Icons.attach_file),
label: const Text('Joindre un fichier'),
style: OutlinedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
),
)
else ...[
// Prévisualisation
if (_isImage() && widget.showImagePreview)
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.borderStrong),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
_selectedFile!,
fit: BoxFit.cover,
),
),
)
else if (_isPdf())
Container(
height: 100,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.borderStrong),
),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.picture_as_pdf, size: 48, color: AppColors.error),
SizedBox(height: 8),
Text('Document PDF'),
],
),
),
),
const SizedBox(height: 8),
Text(
_selectedFile!.path.split('/').last,
style: TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? AppColors.textSecondaryDark
: AppColors.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 8),
Text(
'Formats acceptés: JPEG, PNG, PDF (max 5 MB)',
style: TextStyle(
fontSize: 12,
color: AppColors.textTertiary,
),
),
],
),
),
);
}
}