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.
261 lines
7.5 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|