763 lines
27 KiB
Dart
763 lines
27 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:flutter/foundation.dart';
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
|
|
import 'package:file_picker/file_picker.dart';
|
||
|
|
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
||
|
|
import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart';
|
||
|
|
import 'package:flux/features/attachments/ui/quick_rename_dialog.dart';
|
||
|
|
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
|
||
|
|
import 'package:get_it/get_it.dart';
|
||
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||
|
|
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||
|
|
import 'package:pdf/widgets.dart' as pw;
|
||
|
|
import 'package:pdf/pdf.dart' as p; // Se ti serve formattazione core
|
||
|
|
import 'package:pdfx/pdfx.dart' as px; // Isoliamo pdfx
|
||
|
|
|
||
|
|
class _ExportItem {
|
||
|
|
final Uint8List bytes;
|
||
|
|
final String sourceName;
|
||
|
|
final bool isMultiPage;
|
||
|
|
final int pageIndex;
|
||
|
|
|
||
|
|
_ExportItem({
|
||
|
|
required this.bytes,
|
||
|
|
required this.sourceName,
|
||
|
|
required this.isMultiPage,
|
||
|
|
required this.pageIndex,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
class OperationFilesSection extends StatefulWidget {
|
||
|
|
final OperationModel currentOp;
|
||
|
|
|
||
|
|
const OperationFilesSection({super.key, required this.currentOp});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<OperationFilesSection> createState() => _OperationFilesSectionState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _OperationFilesSectionState extends State<OperationFilesSection> {
|
||
|
|
String? _exportDirectory;
|
||
|
|
double _maxMbLimit = 1.0;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_loadExportDirectory();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- GESTIONE CARTELLA CITRIX (SOLO DESKTOP) ---
|
||
|
|
Future<void> _loadExportDirectory() async {
|
||
|
|
if (kIsWeb) return;
|
||
|
|
final prefs = await SharedPreferences.getInstance();
|
||
|
|
setState(() {
|
||
|
|
_exportDirectory = prefs.getString('citrix_export_path');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _selectExportDirectory() async {
|
||
|
|
final String? selectedDirectory = await FilePicker.getDirectoryPath(
|
||
|
|
dialogTitle: 'Seleziona la cartella di esportazione per TIM/Citrix',
|
||
|
|
);
|
||
|
|
|
||
|
|
if (selectedDirectory != null) {
|
||
|
|
final prefs = await SharedPreferences.getInstance();
|
||
|
|
await prefs.setString('citrix_export_path', selectedDirectory);
|
||
|
|
setState(() {
|
||
|
|
_exportDirectory = selectedDirectory;
|
||
|
|
});
|
||
|
|
if (mounted) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
SnackBar(
|
||
|
|
content: Text('Cartella Export impostata: $selectedDirectory'),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- SELEZIONE FILE DAL PC/TELEFONO ---
|
||
|
|
Future<void> _pickFiles() async {
|
||
|
|
final result = await FilePicker.pickFiles(
|
||
|
|
allowMultiple: true,
|
||
|
|
type: FileType.custom,
|
||
|
|
allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'],
|
||
|
|
withData: true,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (result != null && mounted) {
|
||
|
|
// MAGIA: Passiamo direttamente la lista di PlatformFile al tuo BLoC!
|
||
|
|
context.read<OperationFilesBloc>().add(
|
||
|
|
AddOperationFilesEvent(result.files),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- APERTURA VIEWER ---
|
||
|
|
void _openFile(AttachmentModel file) {
|
||
|
|
// 1. Catturiamo il BLoC dalla pagina corrente prima di navigare
|
||
|
|
final operationFilesBloc = context.read<OperationFilesBloc>();
|
||
|
|
Navigator.push(
|
||
|
|
context,
|
||
|
|
MaterialPageRoute(
|
||
|
|
builder: (viewerContext) => BlocProvider.value(
|
||
|
|
value: operationFilesBloc,
|
||
|
|
child: AttachmentViewerScreen(
|
||
|
|
attachment: file,
|
||
|
|
onRename: (newName) {
|
||
|
|
// Spara l'evento al BLoC e lui farà il resto!
|
||
|
|
operationFilesBloc.add(RenameOperationFileEvent(file, newName));
|
||
|
|
},
|
||
|
|
onDelete: () {
|
||
|
|
operationFilesBloc.add(DeleteSpecificOperationFileEvent(file));
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _exportMergedPdf(List<AttachmentModel> selectedFiles) async {
|
||
|
|
if (!kIsWeb && (_exportDirectory == null || _exportDirectory!.isEmpty)) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
const SnackBar(content: Text('Imposta prima la cartella Citrix!')),
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
showDialog(
|
||
|
|
context: context,
|
||
|
|
barrierDismissible: false,
|
||
|
|
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||
|
|
);
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 1. "FLATTEN" DI TUTTO (Stessa magia di prima)
|
||
|
|
List<Uint8List> allPagesAsImages = [];
|
||
|
|
final repository = GetIt.I.get<AttachmentsRepository>();
|
||
|
|
|
||
|
|
for (var file in selectedFiles) {
|
||
|
|
Uint8List? fileBytes;
|
||
|
|
|
||
|
|
if (file.localBytes != null) {
|
||
|
|
fileBytes = file.localBytes;
|
||
|
|
} else if (file.storagePath != null && file.storagePath!.isNotEmpty) {
|
||
|
|
fileBytes = await repository.downloadAttachmentBytes(
|
||
|
|
file.storagePath!,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fileBytes == null) continue;
|
||
|
|
|
||
|
|
if (file.extension == 'pdf') {
|
||
|
|
final document = await px.PdfDocument.openData(fileBytes);
|
||
|
|
for (int i = 1; i <= document.pagesCount; i++) {
|
||
|
|
final page = await document.getPage(i);
|
||
|
|
final pageImage = await page.render(
|
||
|
|
width: page.width * 2,
|
||
|
|
height: page.height * 2,
|
||
|
|
format: px.PdfPageImageFormat.jpeg,
|
||
|
|
);
|
||
|
|
if (pageImage != null) {
|
||
|
|
allPagesAsImages.add(pageImage.bytes);
|
||
|
|
}
|
||
|
|
await page.close();
|
||
|
|
}
|
||
|
|
await document.close();
|
||
|
|
} else {
|
||
|
|
// È un'immagine
|
||
|
|
allPagesAsImages.add(fileBytes);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mounted) Navigator.pop(context); // Togliamo il loading
|
||
|
|
|
||
|
|
// Se per qualche motivo la lista è vuota, usciamo
|
||
|
|
if (allPagesAsImages.isEmpty) return;
|
||
|
|
|
||
|
|
// 2. LOGICA DEL NOME SUGGERITO
|
||
|
|
String suggestedName;
|
||
|
|
if (selectedFiles.length == 1) {
|
||
|
|
// Se c'è un solo file (es. ho selezionato 3 foto ma poi ho deselezionato le altre)
|
||
|
|
suggestedName = selectedFiles.first.name;
|
||
|
|
} else {
|
||
|
|
// Se sono più file uniti
|
||
|
|
suggestedName = '${widget.currentOp.customerDisplayName}_Unito';
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!mounted) return;
|
||
|
|
|
||
|
|
// 3. DIALOG DI CONFERMA (Mostriamo la PRIMA pagina come anteprima per fargli capire cos'è)
|
||
|
|
final finalName = await showDialog<String>(
|
||
|
|
context: context,
|
||
|
|
builder: (_) => QuickRenameDialog(
|
||
|
|
suggestedName: suggestedName,
|
||
|
|
previewWidget: Image.memory(
|
||
|
|
allPagesAsImages.first,
|
||
|
|
fit: BoxFit.contain,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (finalName == null || finalName.isEmpty) return; // Ha annullato
|
||
|
|
|
||
|
|
// 4. CREAZIONE DEL PDF UNICO (IL MERGE VERO E PROPRIO)
|
||
|
|
final pdf = pw.Document();
|
||
|
|
|
||
|
|
// Cicliamo su tutte le immagini estratte e creiamo una pagina per ognuna
|
||
|
|
for (var imageBytes in allPagesAsImages) {
|
||
|
|
final pdfImage = pw.MemoryImage(imageBytes);
|
||
|
|
|
||
|
|
pdf.addPage(
|
||
|
|
pw.Page(
|
||
|
|
margin: pw.EdgeInsets.zero,
|
||
|
|
build: (pw.Context context) {
|
||
|
|
return pw.Center(child: pw.Image(pdfImage));
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
final mergedPdfBytes = await pdf.save();
|
||
|
|
|
||
|
|
// 5. SALVATAGGIO SUL DISCO
|
||
|
|
if (kIsWeb) {
|
||
|
|
// Trigger download web
|
||
|
|
} else {
|
||
|
|
final fileToSave = File('$_exportDirectory/$finalName.pdf');
|
||
|
|
await fileToSave.writeAsBytes(mergedPdfBytes);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mounted) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
const SnackBar(
|
||
|
|
content: Text('PDF Multi-pagina creato e salvato con successo!'),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
if (mounted) {
|
||
|
|
// Se il loading è ancora aperto, lo chiudiamo
|
||
|
|
if (Navigator.canPop(context)) Navigator.pop(context);
|
||
|
|
ScaffoldMessenger.of(
|
||
|
|
context,
|
||
|
|
).showSnackBar(SnackBar(content: Text('Errore durante l\'unione: $e')));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _exportSplitPdfs(List<AttachmentModel> selectedFiles) async {
|
||
|
|
if (!kIsWeb && (_exportDirectory == null || _exportDirectory!.isEmpty)) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
const SnackBar(content: Text('Imposta prima la cartella Citrix!')),
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
showDialog(
|
||
|
|
context: context,
|
||
|
|
barrierDismissible: false,
|
||
|
|
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||
|
|
);
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 1. "FLATTEN" INTELLIGENTE (Ora usiamo la nostra classe _ExportItem)
|
||
|
|
List<_ExportItem> itemsToExport = [];
|
||
|
|
final repository = GetIt.I.get<AttachmentsRepository>();
|
||
|
|
|
||
|
|
for (var file in selectedFiles) {
|
||
|
|
Uint8List? fileBytes;
|
||
|
|
|
||
|
|
if (file.localBytes != null) {
|
||
|
|
fileBytes = file.localBytes;
|
||
|
|
} else if (file.storagePath != null && file.storagePath!.isNotEmpty) {
|
||
|
|
fileBytes = await repository.downloadAttachmentBytes(
|
||
|
|
file.storagePath!,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fileBytes == null) continue;
|
||
|
|
|
||
|
|
// Recuperiamo il nome che l'utente ha (magari) già impostato
|
||
|
|
final baseName = file.name ?? 'Documento';
|
||
|
|
|
||
|
|
if (file.extension == 'pdf') {
|
||
|
|
final document = await px.PdfDocument.openData(fileBytes);
|
||
|
|
final isMulti =
|
||
|
|
document.pagesCount > 1; // Controlliamo se è multipagina!
|
||
|
|
|
||
|
|
for (int i = 1; i <= document.pagesCount; i++) {
|
||
|
|
final page = await document.getPage(i);
|
||
|
|
|
||
|
|
final pageImage = await page.render(
|
||
|
|
width: page.width * 2,
|
||
|
|
height: page.height * 2,
|
||
|
|
format: px.PdfPageImageFormat.jpeg,
|
||
|
|
);
|
||
|
|
|
||
|
|
if (pageImage != null) {
|
||
|
|
// Salviamo l'immagine CON il suo contesto storico
|
||
|
|
itemsToExport.add(
|
||
|
|
_ExportItem(
|
||
|
|
bytes: pageImage.bytes,
|
||
|
|
sourceName: baseName,
|
||
|
|
isMultiPage: isMulti,
|
||
|
|
pageIndex: i,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
await page.close();
|
||
|
|
}
|
||
|
|
await document.close();
|
||
|
|
} else {
|
||
|
|
// SE È UN'IMMAGINE, la salviamo come singola pagina
|
||
|
|
itemsToExport.add(
|
||
|
|
_ExportItem(
|
||
|
|
bytes: fileBytes,
|
||
|
|
sourceName: baseName,
|
||
|
|
isMultiPage: false,
|
||
|
|
pageIndex: 1,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mounted) Navigator.pop(context);
|
||
|
|
|
||
|
|
// 2. IL CICLO UX
|
||
|
|
for (var item in itemsToExport) {
|
||
|
|
if (!mounted) return;
|
||
|
|
|
||
|
|
// LA TUA MAGIA UX SUI NOMI:
|
||
|
|
// Se è singolo (foto o PDF da 1 pag) -> Usa il nome originale nudo e crudo!
|
||
|
|
// Se è multipagina -> Usa il nome originale + il numero di pagina
|
||
|
|
String suggestedName = item.sourceName;
|
||
|
|
if (item.isMultiPage) {
|
||
|
|
suggestedName = '${item.sourceName}_Pag_${item.pageIndex}';
|
||
|
|
}
|
||
|
|
|
||
|
|
final finalName = await showDialog<String>(
|
||
|
|
context: context,
|
||
|
|
builder: (_) => QuickRenameDialog(
|
||
|
|
suggestedName: suggestedName,
|
||
|
|
previewWidget: Image.memory(item.bytes, fit: BoxFit.contain),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (finalName == null || finalName.isEmpty) continue;
|
||
|
|
|
||
|
|
// CREAZIONE DEL PDF SINGOLO
|
||
|
|
final pdf = pw.Document();
|
||
|
|
final pdfImage = pw.MemoryImage(item.bytes); // Usiamo item.bytes!
|
||
|
|
|
||
|
|
pdf.addPage(
|
||
|
|
pw.Page(
|
||
|
|
margin: pw.EdgeInsets.zero,
|
||
|
|
build: (pw.Context context) {
|
||
|
|
return pw.Center(child: pw.Image(pdfImage));
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
final singlePdfBytes = await pdf.save();
|
||
|
|
|
||
|
|
if (kIsWeb) {
|
||
|
|
// Trigger download web
|
||
|
|
} else {
|
||
|
|
final fileToSave = File('$_exportDirectory/$finalName.pdf');
|
||
|
|
await fileToSave.writeAsBytes(singlePdfBytes);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mounted) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
const SnackBar(
|
||
|
|
content: Text('Esportazione completata con successo!'),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
if (mounted) {
|
||
|
|
Navigator.pop(context);
|
||
|
|
ScaffoldMessenger.of(
|
||
|
|
context,
|
||
|
|
).showSnackBar(SnackBar(content: Text('Errore: $e')));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
|
||
|
|
// USIAMO IL TUO BLOC!
|
||
|
|
return BlocBuilder<OperationFilesBloc, OperationFilesState>(
|
||
|
|
builder: (context, state) {
|
||
|
|
final allFiles = state.allFiles;
|
||
|
|
final selectedFiles = state.selectedFiles;
|
||
|
|
final hasSelection = selectedFiles.isNotEmpty;
|
||
|
|
|
||
|
|
return Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
// 1. SETTINGS CARTELLA (Solo visibile su Desktop)
|
||
|
|
if (!kIsWeb)
|
||
|
|
Card(
|
||
|
|
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||
|
|
alpha: 0.5,
|
||
|
|
),
|
||
|
|
elevation: 0,
|
||
|
|
margin: const EdgeInsets.only(bottom: 16),
|
||
|
|
child: ListTile(
|
||
|
|
leading: Icon(
|
||
|
|
Icons.folder_special,
|
||
|
|
color: theme.colorScheme.primary,
|
||
|
|
),
|
||
|
|
title: const Text(
|
||
|
|
'Cartella Export (Es. Citrix TIM)',
|
||
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||
|
|
),
|
||
|
|
subtitle: Text(
|
||
|
|
_exportDirectory ??
|
||
|
|
'Nessuna cartella selezionata. Clicca per impostare.',
|
||
|
|
style: TextStyle(
|
||
|
|
color: _exportDirectory == null
|
||
|
|
? theme.colorScheme.error
|
||
|
|
: null,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
trailing: const Icon(Icons.settings),
|
||
|
|
onTap: _selectExportDirectory,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
// 2. ACTION BAR DINAMICA
|
||
|
|
Wrap(
|
||
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||
|
|
spacing: 8,
|
||
|
|
runSpacing: 8,
|
||
|
|
children: [
|
||
|
|
// Bottone di Aggiunta
|
||
|
|
ElevatedButton.icon(
|
||
|
|
icon: const Icon(Icons.add_photo_alternate),
|
||
|
|
label: const Text('Aggiungi File'),
|
||
|
|
onPressed: state.status == OperationFilesStatus.uploading
|
||
|
|
? null
|
||
|
|
: _pickFiles,
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
|
||
|
|
// NUOVO: SELEZIONA / DESELEZIONA TUTTO
|
||
|
|
if (allFiles.isNotEmpty) ...[
|
||
|
|
TextButton.icon(
|
||
|
|
icon: Icon(
|
||
|
|
selectedFiles.length == allFiles.length
|
||
|
|
? Icons.deselect
|
||
|
|
: Icons.select_all,
|
||
|
|
),
|
||
|
|
label: Text(
|
||
|
|
selectedFiles.length == allFiles.length
|
||
|
|
? 'Deseleziona Tutto'
|
||
|
|
: 'Seleziona Tutto',
|
||
|
|
),
|
||
|
|
onPressed: () {
|
||
|
|
if (selectedFiles.length == allFiles.length) {
|
||
|
|
context.read<OperationFilesBloc>().add(
|
||
|
|
ClearOperationFileSelectionEvent(),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
context.read<OperationFilesBloc>().add(
|
||
|
|
SelectAllOperationFilesEvent(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
|
||
|
|
// Loader di upload
|
||
|
|
if (state.status == OperationFilesStatus.uploading)
|
||
|
|
const SizedBox(
|
||
|
|
width: 24,
|
||
|
|
height: 24,
|
||
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||
|
|
),
|
||
|
|
|
||
|
|
const Spacer(),
|
||
|
|
|
||
|
|
// Azioni visibili SOLO se c'è una selezione!
|
||
|
|
if (hasSelection) ...[
|
||
|
|
// Bottone Elimina
|
||
|
|
IconButton(
|
||
|
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||
|
|
tooltip: 'Elimina selezionati',
|
||
|
|
onPressed: () {
|
||
|
|
context.read<OperationFilesBloc>().add(
|
||
|
|
DeleteOperationFilesEvent(),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
// Bottone Associa a Cliente
|
||
|
|
if (widget.currentOp.customerId != null &&
|
||
|
|
widget.currentOp.customerId!.isNotEmpty)
|
||
|
|
IconButton(
|
||
|
|
icon: const Icon(Icons.person_add, color: Colors.blue),
|
||
|
|
tooltip: 'Copia nei documenti del Cliente',
|
||
|
|
onPressed: () {
|
||
|
|
context.read<OperationFilesBloc>().add(
|
||
|
|
LinkFilesToCustomerEvent(
|
||
|
|
customerId: widget.currentOp.customerId!,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
const SnackBar(
|
||
|
|
content: Text('File copiati nella scheda cliente!'),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
// IL NUOVO BOTTONE ESPORTA CON MENU A TENDINA
|
||
|
|
PopupMenuButton<String>(
|
||
|
|
tooltip: 'Opzioni di esportazione',
|
||
|
|
position: PopupMenuPosition
|
||
|
|
.under, // Opzionale: fa aprire il menu sotto al bottone
|
||
|
|
onSelected: (value) {
|
||
|
|
if (value == 'merge') {
|
||
|
|
_exportMergedPdf(selectedFiles);
|
||
|
|
} else if (value == 'split') {
|
||
|
|
_exportSplitPdfs(selectedFiles);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
itemBuilder: (BuildContext context) =>
|
||
|
|
<PopupMenuEntry<String>>[
|
||
|
|
const PopupMenuItem<String>(
|
||
|
|
value: 'merge',
|
||
|
|
child: ListTile(
|
||
|
|
leading: Icon(
|
||
|
|
Icons.merge_type,
|
||
|
|
color: Colors.blue,
|
||
|
|
),
|
||
|
|
title: Text('Unisci in un singolo PDF'),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const PopupMenuItem<String>(
|
||
|
|
value: 'split',
|
||
|
|
child: ListTile(
|
||
|
|
leading: Icon(
|
||
|
|
Icons.splitscreen,
|
||
|
|
color: Colors.orange,
|
||
|
|
),
|
||
|
|
title: Text(
|
||
|
|
'Dividi: un PDF per ogni pagina/foto',
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
// IL FIX È QUI SOTTO: AbsorbPointer + onPressed vuoto
|
||
|
|
child: AbsorbPointer(
|
||
|
|
child: FilledButton.icon(
|
||
|
|
style: FilledButton.styleFrom(
|
||
|
|
backgroundColor: Colors.red,
|
||
|
|
),
|
||
|
|
icon: const Icon(Icons.picture_as_pdf),
|
||
|
|
label: Text('Esporta (${selectedFiles.length})'),
|
||
|
|
onPressed: () {}, // Manteniamo vivo il colore!
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
|
||
|
|
// 3. GRIGLIA DEI FILE
|
||
|
|
if (allFiles.isEmpty)
|
||
|
|
Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.all(32),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
border: Border.all(
|
||
|
|
color: theme.dividerColor,
|
||
|
|
style: BorderStyle.solid,
|
||
|
|
),
|
||
|
|
borderRadius: BorderRadius.circular(8),
|
||
|
|
),
|
||
|
|
child: const Column(
|
||
|
|
children: [
|
||
|
|
Icon(Icons.upload_file, size: 48, color: Colors.grey),
|
||
|
|
SizedBox(height: 8),
|
||
|
|
Text(
|
||
|
|
'Nessun file allegato. Usa il pulsante per aggiungere documenti o foto.',
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
style: TextStyle(color: Colors.grey),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
)
|
||
|
|
else
|
||
|
|
GridView.builder(
|
||
|
|
shrinkWrap: true,
|
||
|
|
physics: const NeverScrollableScrollPhysics(),
|
||
|
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||
|
|
maxCrossAxisExtent: 150,
|
||
|
|
crossAxisSpacing: 12,
|
||
|
|
mainAxisSpacing: 12,
|
||
|
|
childAspectRatio: 0.8,
|
||
|
|
),
|
||
|
|
itemCount: allFiles.length,
|
||
|
|
itemBuilder: (context, index) {
|
||
|
|
final file = allFiles[index];
|
||
|
|
final isPdf = file.extension == 'pdf';
|
||
|
|
final isSelected = selectedFiles.contains(file);
|
||
|
|
final isLocal =
|
||
|
|
file.localBytes !=
|
||
|
|
null; // Per capire se è un file in bozza
|
||
|
|
|
||
|
|
return Stack(
|
||
|
|
children: [
|
||
|
|
// CARD DEL FILE
|
||
|
|
InkWell(
|
||
|
|
onTap: () => _openFile(file),
|
||
|
|
onLongPress: () {
|
||
|
|
// Selezione rapida con long press!
|
||
|
|
context.read<OperationFilesBloc>().add(
|
||
|
|
ToggleOperationFileSelectionEvent(file),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
borderRadius: BorderRadius.circular(8),
|
||
|
|
child: Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
border: Border.all(
|
||
|
|
color: isSelected
|
||
|
|
? theme.colorScheme.primary
|
||
|
|
: theme.dividerColor,
|
||
|
|
width: isSelected ? 3 : 1,
|
||
|
|
),
|
||
|
|
borderRadius: BorderRadius.circular(8),
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
|
|
children: [
|
||
|
|
// Anteprima
|
||
|
|
Expanded(
|
||
|
|
child: Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: theme
|
||
|
|
.colorScheme
|
||
|
|
.surfaceContainerHighest,
|
||
|
|
borderRadius: const BorderRadius.vertical(
|
||
|
|
top: Radius.circular(8),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: isPdf
|
||
|
|
? const Icon(
|
||
|
|
Icons.picture_as_pdf,
|
||
|
|
size: 48,
|
||
|
|
color: Colors.red,
|
||
|
|
)
|
||
|
|
: isLocal
|
||
|
|
? ClipRRect(
|
||
|
|
borderRadius:
|
||
|
|
const BorderRadius.vertical(
|
||
|
|
top: Radius.circular(8),
|
||
|
|
),
|
||
|
|
child: Image.memory(
|
||
|
|
file.localBytes!,
|
||
|
|
fit: BoxFit.cover,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
: const Icon(
|
||
|
|
Icons.image,
|
||
|
|
size: 48,
|
||
|
|
color: Colors.blue,
|
||
|
|
), // Da remoto metterai il tuo NetworkImage se vuoi
|
||
|
|
),
|
||
|
|
),
|
||
|
|
// Nome File
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.all(8.0),
|
||
|
|
child: Text(
|
||
|
|
file.name,
|
||
|
|
maxLines: 2,
|
||
|
|
overflow: TextOverflow.ellipsis,
|
||
|
|
style: const TextStyle(fontSize: 12),
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
// CHECKBOX DI SELEZIONE
|
||
|
|
Positioned(
|
||
|
|
top: 4,
|
||
|
|
right: 4,
|
||
|
|
child: InkWell(
|
||
|
|
onTap: () {
|
||
|
|
context.read<OperationFilesBloc>().add(
|
||
|
|
ToggleOperationFileSelectionEvent(file),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
child: Container(
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: isSelected
|
||
|
|
? theme.colorScheme.primary
|
||
|
|
: Colors.white.withValues(alpha: 0.8),
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
border: Border.all(
|
||
|
|
color: theme.colorScheme.primary,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.all(4.0),
|
||
|
|
child: Icon(
|
||
|
|
isSelected ? Icons.check : Icons.circle,
|
||
|
|
size: 16,
|
||
|
|
color: isSelected
|
||
|
|
? Colors.white
|
||
|
|
: Colors.transparent,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
// BADGE "IN ATTESA" (Se è locale ma la pratica è salvata)
|
||
|
|
if (isLocal)
|
||
|
|
Positioned(
|
||
|
|
top: 4,
|
||
|
|
left: 4,
|
||
|
|
child: Container(
|
||
|
|
padding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: 6,
|
||
|
|
vertical: 2,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.orange,
|
||
|
|
borderRadius: BorderRadius.circular(4),
|
||
|
|
),
|
||
|
|
child: const Text(
|
||
|
|
'Bozza',
|
||
|
|
style: TextStyle(
|
||
|
|
color: Colors.white,
|
||
|
|
fontSize: 10,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|