220
lib/features/attachments/ui/attachment_viewer_screen.dart
Normal file
220
lib/features/attachments/ui/attachment_viewer_screen.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/utils/functions.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
import 'package:pdfx/pdfx.dart';
|
||||
import 'package:internet_file/internet_file.dart';
|
||||
|
||||
class AttachmentViewerScreen extends StatefulWidget {
|
||||
final AttachmentModel attachment;
|
||||
final Function(String newName)? onRename;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
const AttachmentViewerScreen({
|
||||
super.key,
|
||||
required this.attachment,
|
||||
this.onRename,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentViewerScreen> createState() => _AttachmentViewerScreenState();
|
||||
}
|
||||
|
||||
class _AttachmentViewerScreenState extends State<AttachmentViewerScreen> {
|
||||
PdfControllerPinch? _pdfController;
|
||||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
Uint8List? _fileBytes;
|
||||
late String _fileName;
|
||||
|
||||
bool get isPdf => widget.attachment.extension.toLowerCase() == 'pdf';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fileName = widget.attachment.name;
|
||||
_loadFile();
|
||||
}
|
||||
|
||||
Future<void> _loadFile() async {
|
||||
try {
|
||||
// 1. Capiamo da dove prendere i dati
|
||||
if (widget.attachment.localBytes != null) {
|
||||
_fileBytes = widget.attachment.localBytes;
|
||||
} else if (widget.attachment.storagePath != null &&
|
||||
widget.attachment.storagePath!.isNotEmpty) {
|
||||
final signedUrl = await getSignedUrl(widget.attachment.storagePath!);
|
||||
_fileBytes = await InternetFile.get(signedUrl);
|
||||
} else {
|
||||
throw Exception("Nessun documento trovato o byte mancanti.");
|
||||
}
|
||||
|
||||
// 2. Se è PDF, inizializziamo il controller
|
||||
if (isPdf && _fileBytes != null) {
|
||||
_pdfController = PdfControllerPinch(
|
||||
document: PdfDocument.openData(_fileBytes!),
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pdfController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showRenameDialog() {
|
||||
final ctrl = TextEditingController(text: _fileName);
|
||||
ctrl.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: ctrl.text.length,
|
||||
);
|
||||
final focusNode = FocusNode();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => focusNode.requestFocus(),
|
||||
);
|
||||
return AlertDialog(
|
||||
title: const Text('Rinomina File'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nuovo nome',
|
||||
suffixText: '.${widget.attachment.extension}',
|
||||
),
|
||||
onSubmitted: (val) {
|
||||
Navigator.pop(context);
|
||||
if (val.trim().isNotEmpty && widget.onRename != null) {
|
||||
setState(() {
|
||||
_fileName = val.trim();
|
||||
});
|
||||
widget.onRename!(val.trim());
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annulla'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
if (ctrl.text.trim().isNotEmpty && widget.onRename != null) {
|
||||
setState(() {
|
||||
_fileName = ctrl.text.trim();
|
||||
});
|
||||
widget.onRename!(ctrl.text.trim());
|
||||
}
|
||||
},
|
||||
child: const Text('Salva'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black87, // Sfondo scuro per i viewer è il top
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.black,
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(_fileName, style: const TextStyle(fontSize: 16)),
|
||||
actions: [
|
||||
if (widget.onRename != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: 'Rinomina',
|
||||
onPressed: _showRenameDialog,
|
||||
),
|
||||
if (widget.onDelete != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.redAccent),
|
||||
tooltip: 'Elimina',
|
||||
onPressed: () {
|
||||
// Chiediamo conferma
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Eliminare file?'),
|
||||
content: const Text(
|
||||
'Sei sicuro di voler eliminare questo allegato?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c),
|
||||
child: const Text('Annulla'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(c); // Chiude dialog
|
||||
widget.onDelete!(); // Lancia eliminazione
|
||||
Navigator.pop(context); // Chiude il viewer
|
||||
},
|
||||
child: const Text('Elimina'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
);
|
||||
}
|
||||
if (_errorMessage != null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Errore: $_errorMessage',
|
||||
style: const TextStyle(color: Colors.redAccent),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_fileBytes == null) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'File non disponibile',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isPdf && _pdfController != null) {
|
||||
return PdfViewPinch(controller: _pdfController!);
|
||||
} else {
|
||||
return InteractiveViewer(
|
||||
maxScale: 5.0,
|
||||
child: Center(child: Image.memory(_fileBytes!)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
lib/features/attachments/ui/quick_rename_dialog.dart
Normal file
85
lib/features/attachments/ui/quick_rename_dialog.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class QuickRenameDialog extends StatefulWidget {
|
||||
final String suggestedName;
|
||||
final Widget previewWidget; // Può essere Image.memory o un'icona PDF
|
||||
|
||||
const QuickRenameDialog({
|
||||
super.key,
|
||||
required this.suggestedName,
|
||||
required this.previewWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
State<QuickRenameDialog> createState() => _QuickRenameDialogState();
|
||||
}
|
||||
|
||||
class _QuickRenameDialogState extends State<QuickRenameDialog> {
|
||||
late TextEditingController _nameCtrl;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameCtrl = TextEditingController(text: widget.suggestedName);
|
||||
|
||||
// MAGIA UX: Selezioniamo tutto il testo di default appena si apre!
|
||||
_nameCtrl.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: widget.suggestedName.length,
|
||||
);
|
||||
|
||||
// Richiediamo il focus appena il widget è costruito
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Rinomina per Export'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Anteprima del documento (limitiamo l'altezza)
|
||||
Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
|
||||
child: widget.previewWidget,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _nameCtrl,
|
||||
focusNode: _focusNode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nome del file',
|
||||
suffixText: '.pdf', // Facciamo capire che sarà un PDF
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
// MAGIA UX 2: Se preme invio sulla tastiera, salva e chiude!
|
||||
onSubmitted: (value) => Navigator.of(context).pop(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(), // Ritorna null
|
||||
child: const Text('Salta'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(_nameCtrl.text),
|
||||
child: const Text('Esporta (Invio)'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user