989 lines
31 KiB
Dart
989 lines
31 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:file_selector/file_selector.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:firebase_storage/firebase_storage.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import '../../../../core/services/auth_service.dart';
|
|
|
|
/// Página de Materiais do Professor
|
|
/// Tela dedicada para upload e gestão de materiais para a IA
|
|
class TeacherMaterialsPage extends StatefulWidget {
|
|
const TeacherMaterialsPage({super.key});
|
|
|
|
@override
|
|
State<TeacherMaterialsPage> createState() => _TeacherMaterialsPageState();
|
|
}
|
|
|
|
class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
|
final FirebaseStorage _storage = FirebaseStorage.instanceFor(
|
|
bucket: 'teachit-app.firebasestorage.app',
|
|
);
|
|
final ImagePicker _imagePicker = ImagePicker();
|
|
bool _isUploading = false;
|
|
List<Map<String, dynamic>> _classes = [];
|
|
List<Map<String, dynamic>> _filteredClasses = [];
|
|
String _searchQuery = '';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadClasses();
|
|
}
|
|
|
|
Future<void> _loadClasses() async {
|
|
final currentUser = AuthService.currentUser;
|
|
if (currentUser == null) return;
|
|
|
|
final snapshot = await _firestore
|
|
.collection('classes')
|
|
.where('teacherId', isEqualTo: currentUser.uid)
|
|
.orderBy('createdAt', descending: true)
|
|
.get();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_classes = snapshot.docs.map((doc) {
|
|
final data = doc.data();
|
|
return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)};
|
|
}).toList();
|
|
_filteredClasses = List.from(_classes);
|
|
});
|
|
}
|
|
}
|
|
|
|
Stream<QuerySnapshot> _getMaterialsStream(String classId) {
|
|
final currentUser = AuthService.currentUser;
|
|
if (currentUser == null) {
|
|
return const Stream.empty();
|
|
}
|
|
|
|
return _firestore
|
|
.collection('materials')
|
|
.where('teacherId', isEqualTo: currentUser.uid)
|
|
.where('classId', isEqualTo: classId)
|
|
.snapshots();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_classes.isEmpty) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Materiais da Disciplina',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
body: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: Theme.of(context).brightness == Brightness.dark
|
|
? [
|
|
Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
|
Theme.of(context).colorScheme.background,
|
|
]
|
|
: [const Color(0xFF82C9BD), const Color(0xFFF8F9FA)],
|
|
stops: const [0.0, 0.4],
|
|
),
|
|
),
|
|
child: const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.folder_open, size: 64, color: Colors.grey),
|
|
SizedBox(height: 16),
|
|
Text('Ainda não tens turmas criadas.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return DefaultTabController(
|
|
length: _filteredClasses.length,
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Materiais da Disciplina',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
elevation: 0,
|
|
bottom: PreferredSize(
|
|
preferredSize: const Size.fromHeight(kToolbarHeight + 60),
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
child: TextField(
|
|
decoration: InputDecoration(
|
|
hintText: 'Procurar turma...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
filled: true,
|
|
fillColor: Theme.of(context).colorScheme.surface,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_searchQuery = value.toLowerCase();
|
|
_filteredClasses = _classes.where((c) {
|
|
final name = (c['name'] as String).toLowerCase();
|
|
return name.contains(_searchQuery);
|
|
}).toList();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
if (_filteredClasses.isNotEmpty)
|
|
TabBar(
|
|
isScrollable: true,
|
|
indicatorColor: const Color(0xFFF68D2D),
|
|
labelColor: const Color(0xFFF68D2D),
|
|
unselectedLabelColor: Theme.of(
|
|
context,
|
|
).colorScheme.onSurface,
|
|
tabAlignment: TabAlignment.start,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
tabs: _filteredClasses
|
|
.map((c) => Tab(text: c['name'] as String))
|
|
.toList(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
body: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: Theme.of(context).brightness == Brightness.dark
|
|
? [
|
|
Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
|
Theme.of(context).colorScheme.background,
|
|
]
|
|
: [const Color(0xFF82C9BD), const Color(0xFFF8F9FA)],
|
|
stops: const [0.0, 0.4],
|
|
),
|
|
),
|
|
child: TabBarView(
|
|
children: _filteredClasses.map((classData) {
|
|
final classId = classData['id'] as String;
|
|
return _buildClassTab(classId: classId);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildClassTab({required String classId}) {
|
|
return SafeArea(
|
|
top: false,
|
|
child: Stack(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 52.0),
|
|
child: StreamBuilder<QuerySnapshot>(
|
|
stream: _getMaterialsStream(classId),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(color: Color(0xFF82C9BD)),
|
|
);
|
|
}
|
|
|
|
if (snapshot.hasError) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.error_outline,
|
|
color: Colors.red,
|
|
size: 48,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Erro ao carregar materiais:\n${snapshot.error}',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
final materials = snapshot.data?.docs ?? [];
|
|
|
|
// Sort by createdAt descending on client side
|
|
materials.sort((a, b) {
|
|
final aData = a.data() as Map<String, dynamic>?;
|
|
final bData = b.data() as Map<String, dynamic>?;
|
|
final aTime = aData?['createdAt'] as Timestamp?;
|
|
final bTime = bData?['createdAt'] as Timestamp?;
|
|
if (aTime == null && bTime == null) return 0;
|
|
if (aTime == null) return 1;
|
|
if (bTime == null) return -1;
|
|
return bTime.compareTo(aTime);
|
|
});
|
|
|
|
if (materials.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.folder_open,
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
size: 64,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Nenhum material enviado ainda.',
|
|
style: TextStyle(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.onSurfaceVariant,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Os materiais enviados aparecerão aqui.',
|
|
style: TextStyle(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.onSurfaceVariant,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: materials.length,
|
|
itemBuilder: (context, index) {
|
|
final material =
|
|
materials[index].data() as Map<String, dynamic>;
|
|
final fileName =
|
|
material['fileName'] ?? 'Ficheiro sem nome';
|
|
final createdAt = material['createdAt'] as Timestamp?;
|
|
final extension = path.extension(fileName).toLowerCase();
|
|
final fileType = extension == '.pdf'
|
|
? 'pdf'
|
|
: (extension == '.jpg' ||
|
|
extension == '.jpeg' ||
|
|
extension == '.png')
|
|
? 'image'
|
|
: 'other';
|
|
|
|
final docId = materials[index].id;
|
|
final url = material['url'] as String?;
|
|
|
|
return _buildMaterialCard(
|
|
docId: docId,
|
|
fileName: fileName,
|
|
fileType: fileType,
|
|
createdAt: createdAt,
|
|
url: url,
|
|
classId: classId,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
Positioned(
|
|
right: 16,
|
|
bottom: 16,
|
|
child: _isUploading
|
|
? FloatingActionButton.extended(
|
|
onPressed: null,
|
|
backgroundColor: const Color(0xFFF68D2D).withOpacity(0.6),
|
|
icon: const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
label: const Text(
|
|
'A enviar...',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
)
|
|
: FloatingActionButton.extended(
|
|
onPressed: () => _showUploadOptions(classId),
|
|
backgroundColor: const Color(0xFFF68D2D),
|
|
icon: const Icon(Icons.add, color: Colors.white),
|
|
label: const Text(
|
|
'Adicionar',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showUploadOptions(String classId) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (context) => Container(
|
|
decoration: BoxDecoration(
|
|
color: cs.surface,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: cs.onSurface.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
'Adicionar Material',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildUploadOption(
|
|
icon: Icons.picture_as_pdf,
|
|
color: Colors.red,
|
|
title: 'PDF',
|
|
subtitle: 'Selecionar ficheiro PDF',
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_selectPDF(classId);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildUploadOption(
|
|
icon: Icons.image,
|
|
color: Colors.blue,
|
|
title: 'Imagem da Galeria',
|
|
subtitle: 'Escolher foto existente',
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_selectImageFromGallery(classId);
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildUploadOption(
|
|
icon: Icons.camera_alt,
|
|
color: const Color(0xFF82C9BD),
|
|
title: 'Foto da Câmara',
|
|
subtitle: 'Tirar foto nova',
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
_takePhotoWithCamera(classId);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildUploadOption({
|
|
required IconData icon,
|
|
required Color color,
|
|
required String title,
|
|
required String subtitle,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: color.withOpacity(0.2), width: 1),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(icon, color: color, size: 24),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<String?> _showRenameDialog(String originalName) async {
|
|
if (!mounted) return null;
|
|
|
|
final wantRename = await showDialog<bool>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
title: const Text(
|
|
'Renomear Ficheiro',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
content: const Text('Deseja renomear o ficheiro?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
|
child: const Text('Não'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFF68D2D),
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
child: const Text('Sim'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (wantRename != true || !mounted) return originalName;
|
|
|
|
final newName = await showDialog<String>(
|
|
context: context,
|
|
builder: (dialogContext) {
|
|
return StatefulBuilder(
|
|
builder: (context, setDialogState) {
|
|
String textValue = originalName;
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
title: const Text(
|
|
'Novo Nome',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
content: TextField(
|
|
decoration: InputDecoration(
|
|
hintText: originalName,
|
|
filled: true,
|
|
fillColor: Theme.of(
|
|
context,
|
|
).colorScheme.surfaceContainerHighest,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
autofocus: true,
|
|
onChanged: (value) => textValue = value,
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(null),
|
|
child: const Text('Cancelar'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
final name = textValue.trim();
|
|
if (name.isNotEmpty) {
|
|
Navigator.of(dialogContext).pop(name);
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFFF68D2D),
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
child: const Text('Confirmar'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
return newName?.isNotEmpty == true ? newName : originalName;
|
|
}
|
|
|
|
Future<void> _selectPDF(String classId) async {
|
|
try {
|
|
const XTypeGroup pdfTypeGroup = XTypeGroup(
|
|
label: 'PDFs',
|
|
extensions: ['pdf'],
|
|
uniformTypeIdentifiers: ['com.adobe.pdf'],
|
|
);
|
|
|
|
final XFile? file = await openFile(acceptedTypeGroups: [pdfTypeGroup]);
|
|
if (file == null) return;
|
|
|
|
final originalName = path.basename(file.path);
|
|
final finalName = await _showRenameDialog(originalName);
|
|
if (finalName == null) return;
|
|
|
|
await _uploadFile(
|
|
filePath: file.path,
|
|
fileName: finalName,
|
|
fileType: 'pdf',
|
|
classId: classId,
|
|
);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
_showErrorSnackBar('Erro ao selecionar PDF: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _selectImageFromGallery(String classId) async {
|
|
try {
|
|
final XFile? image = await _imagePicker.pickImage(
|
|
source: ImageSource.gallery,
|
|
maxWidth: 1920,
|
|
maxHeight: 1080,
|
|
imageQuality: 85,
|
|
);
|
|
if (image == null) return;
|
|
|
|
final originalName = path.basename(image.path);
|
|
final finalName = await _showRenameDialog(originalName);
|
|
if (finalName == null) return;
|
|
|
|
await _uploadFile(
|
|
filePath: image.path,
|
|
fileName: finalName,
|
|
fileType: 'image',
|
|
classId: classId,
|
|
);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
_showErrorSnackBar('Erro ao selecionar imagem: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _takePhotoWithCamera(String classId) async {
|
|
try {
|
|
final XFile? photo = await _imagePicker.pickImage(
|
|
source: ImageSource.camera,
|
|
maxWidth: 1920,
|
|
maxHeight: 1080,
|
|
imageQuality: 85,
|
|
);
|
|
if (photo == null) return;
|
|
|
|
final originalName = path.basename(photo.path);
|
|
final finalName = await _showRenameDialog(originalName);
|
|
if (finalName == null) return;
|
|
|
|
await _uploadFile(
|
|
filePath: photo.path,
|
|
fileName: finalName,
|
|
fileType: 'image',
|
|
classId: classId,
|
|
);
|
|
} catch (e) {
|
|
if (mounted) {
|
|
_showErrorSnackBar('Erro ao tirar foto: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _uploadFile({
|
|
required String filePath,
|
|
required String fileName,
|
|
required String fileType,
|
|
String? classId,
|
|
}) async {
|
|
final currentUser = FirebaseAuth.instance.currentUser;
|
|
if (currentUser == null) {
|
|
_showErrorSnackBar('Utilizador não autenticado');
|
|
return;
|
|
}
|
|
|
|
final uid = currentUser.uid;
|
|
final cleanFileName = path.basename(fileName);
|
|
|
|
setState(() => _isUploading = true);
|
|
|
|
try {
|
|
// Upload para Firebase Storage: teachers/{uid}/materials/{fileName}
|
|
final storage = FirebaseStorage.instanceFor(
|
|
bucket: 'teachit-app.firebasestorage.app',
|
|
);
|
|
final ref = storage
|
|
.ref()
|
|
.child('teachers')
|
|
.child(uid)
|
|
.child('materials')
|
|
.child(cleanFileName);
|
|
|
|
await ref.putFile(File(filePath));
|
|
final downloadUrl = await ref.getDownloadURL();
|
|
|
|
// Criar documento no Firestore
|
|
final materialData = <String, dynamic>{
|
|
'teacherId': uid,
|
|
'fileName': cleanFileName,
|
|
'url': downloadUrl,
|
|
'createdAt': FieldValue.serverTimestamp(),
|
|
};
|
|
if (classId != null && classId.isNotEmpty) {
|
|
materialData['classId'] = classId;
|
|
}
|
|
await FirebaseFirestore.instance
|
|
.collection('materials')
|
|
.add(materialData);
|
|
|
|
if (mounted) {
|
|
_showSuccessSnackBar('Material enviado com sucesso!');
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
_showErrorSnackBar('Erro ao enviar material: $e');
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() => _isUploading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteMaterial({
|
|
required String docId,
|
|
required String fileName,
|
|
String? url,
|
|
}) async {
|
|
try {
|
|
// Apagar ficheiro do Firebase Storage
|
|
if (url != null && url.isNotEmpty) {
|
|
try {
|
|
final ref = _storage.refFromURL(url);
|
|
await ref.delete();
|
|
} catch (_) {
|
|
// Se o ficheiro já não existir no Storage, continuar na mesma
|
|
}
|
|
}
|
|
|
|
// Apagar documento no Firestore
|
|
await _firestore.collection('materials').doc(docId).delete();
|
|
|
|
if (mounted) {
|
|
_showSuccessSnackBar('Material eliminado com sucesso!');
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
_showErrorSnackBar('Erro ao eliminar material: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
void _showDeleteConfirmation({
|
|
required String docId,
|
|
required String fileName,
|
|
String? url,
|
|
}) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
title: const Text(
|
|
'Eliminar Material',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
content: Text(
|
|
'Tens a certeza que queres eliminar "$fileName"?\nEsta ação não pode ser desfeita.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
child: const Text('Cancelar'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(dialogContext).pop();
|
|
_deleteMaterial(docId: docId, fileName: fileName, url: url);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
child: const Text('Eliminar'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showSuccessSnackBar(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.check_circle, color: Colors.white),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: Text(message)),
|
|
],
|
|
),
|
|
backgroundColor: const Color(0xFF10B981),
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showErrorSnackBar(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
const Icon(Icons.error_outline, color: Colors.white),
|
|
const SizedBox(width: 12),
|
|
Expanded(child: Text(message)),
|
|
],
|
|
),
|
|
backgroundColor: Colors.red,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
duration: const Duration(seconds: 3),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _openFile(String? url, String fileName) async {
|
|
if (url == null || url.isEmpty) {
|
|
_showErrorSnackBar('URL do ficheiro não disponível');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
print('Opening file: $fileName');
|
|
print('URL: $url');
|
|
final uri = Uri.parse(url);
|
|
|
|
final launched = await launchUrl(
|
|
uri,
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
|
|
if (!launched) {
|
|
_showErrorSnackBar('Não foi possível abrir o ficheiro');
|
|
}
|
|
} catch (e) {
|
|
print('Error opening file: $e');
|
|
_showErrorSnackBar('Erro ao abrir ficheiro: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _downloadFile(String? url, String fileName) async {
|
|
if (url == null || url.isEmpty) {
|
|
_showErrorSnackBar('URL do ficheiro não disponível');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
print('Downloading file: $fileName');
|
|
print('URL: $url');
|
|
final uri = Uri.parse(url);
|
|
|
|
final launched = await launchUrl(
|
|
uri,
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
|
|
if (launched) {
|
|
_showSuccessSnackBar('A transferir $fileName...');
|
|
} else {
|
|
_showErrorSnackBar('Não foi possível iniciar a transferência');
|
|
}
|
|
} catch (e) {
|
|
print('Error downloading file: $e');
|
|
_showErrorSnackBar('Erro ao transferir ficheiro: $e');
|
|
}
|
|
}
|
|
|
|
Widget _buildMaterialCard({
|
|
required String docId,
|
|
required String fileName,
|
|
required String fileType,
|
|
required Timestamp? createdAt,
|
|
String? url,
|
|
String? classId,
|
|
}) {
|
|
IconData iconData;
|
|
Color iconColor;
|
|
|
|
switch (fileType.toLowerCase()) {
|
|
case 'pdf':
|
|
iconData = Icons.picture_as_pdf;
|
|
iconColor = Colors.red;
|
|
break;
|
|
case 'image':
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
case 'png':
|
|
iconData = Icons.image;
|
|
iconColor = Colors.blue;
|
|
break;
|
|
default:
|
|
iconData = Icons.insert_drive_file;
|
|
iconColor = const Color(0xFF82C9BD);
|
|
}
|
|
|
|
String formattedDate = 'Data desconhecida';
|
|
if (createdAt != null) {
|
|
formattedDate = DateFormat('dd/MM/yyyy HH:mm').format(createdAt.toDate());
|
|
}
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
child: ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
leading: Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(iconData, color: iconColor, size: 28),
|
|
),
|
|
title: Text(
|
|
fileName,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 16,
|
|
color: Theme.of(context).colorScheme.onSurface,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: Text(
|
|
formattedDate,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.visibility_outlined, color: Colors.blue),
|
|
tooltip: 'Ver',
|
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
|
onPressed: () => _openFile(url, fileName),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.download_outlined, color: Colors.green),
|
|
tooltip: 'Transferir',
|
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
|
onPressed: () => _downloadFile(url, fileName),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
|
tooltip: 'Eliminar',
|
|
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
|
onPressed: () => _showDeleteConfirmation(
|
|
docId: docId,
|
|
fileName: fileName,
|
|
url: url,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|