Reformulação de materias uploaded
This commit is contained in:
@@ -27,8 +27,38 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
);
|
);
|
||||||
final ImagePicker _imagePicker = ImagePicker();
|
final ImagePicker _imagePicker = ImagePicker();
|
||||||
bool _isUploading = false;
|
bool _isUploading = false;
|
||||||
|
List<Map<String, dynamic>> _classes = [];
|
||||||
|
List<Map<String, dynamic>> _filteredClasses = [];
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
Stream<QuerySnapshot> _getMaterialsStream() {
|
@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;
|
final currentUser = AuthService.currentUser;
|
||||||
if (currentUser == null) {
|
if (currentUser == null) {
|
||||||
return const Stream.empty();
|
return const Stream.empty();
|
||||||
@@ -37,64 +67,142 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
return _firestore
|
return _firestore
|
||||||
.collection('materials')
|
.collection('materials')
|
||||||
.where('teacherId', isEqualTo: currentUser.uid)
|
.where('teacherId', isEqualTo: currentUser.uid)
|
||||||
.orderBy('createdAt', descending: true)
|
.where('classId', isEqualTo: classId)
|
||||||
.snapshots();
|
.snapshots();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
if (_classes.isEmpty) {
|
||||||
appBar: AppBar(
|
return Scaffold(
|
||||||
title: const Text(
|
appBar: AppBar(
|
||||||
'Materiais da Disciplina',
|
title: const Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
'Materiais da Disciplina',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
),
|
),
|
||||||
elevation: 0,
|
body: Container(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
floatingActionButton: _isUploading
|
gradient: LinearGradient(
|
||||||
? FloatingActionButton.extended(
|
begin: Alignment.topCenter,
|
||||||
onPressed: null,
|
end: Alignment.bottomCenter,
|
||||||
backgroundColor: const Color(0xFFF68D2D).withOpacity(0.6),
|
colors: Theme.of(context).brightness == Brightness.dark
|
||||||
icon: const SizedBox(
|
? [
|
||||||
width: 20,
|
Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||||
height: 20,
|
Theme.of(context).colorScheme.background,
|
||||||
child: CircularProgressIndicator(
|
]
|
||||||
strokeWidth: 2,
|
: [const Color(0xFF82C9BD), const Color(0xFFF8F9FA)],
|
||||||
color: Colors.white,
|
stops: const [0.0, 0.4],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: const Text(
|
child: const Center(
|
||||||
'A enviar...',
|
child: Column(
|
||||||
style: TextStyle(color: Colors.white),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
)
|
Icon(Icons.folder_open, size: 64, color: Colors.grey),
|
||||||
: FloatingActionButton.extended(
|
SizedBox(height: 16),
|
||||||
onPressed: _showUploadOptions,
|
Text('Ainda não tens turmas criadas.'),
|
||||||
backgroundColor: const Color(0xFFF68D2D),
|
],
|
||||||
icon: const Icon(Icons.add, color: Colors.white),
|
|
||||||
label: const Text(
|
|
||||||
'Adicionar',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
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: SafeArea(
|
);
|
||||||
child: StreamBuilder<QuerySnapshot>(
|
}
|
||||||
stream: _getMaterialsStream(),
|
|
||||||
|
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: _filteredClasses.length > 3,
|
||||||
|
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(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
StreamBuilder<QuerySnapshot>(
|
||||||
|
stream: _getMaterialsStream(classId),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(
|
return const Center(
|
||||||
@@ -128,6 +236,18 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
|
|
||||||
final materials = snapshot.data?.docs ?? [];
|
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) {
|
if (materials.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -167,7 +287,6 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
materials[index].data() as Map<String, dynamic>;
|
materials[index].data() as Map<String, dynamic>;
|
||||||
final fileName = material['fileName'] ?? 'Ficheiro sem nome';
|
final fileName = material['fileName'] ?? 'Ficheiro sem nome';
|
||||||
final createdAt = material['createdAt'] as Timestamp?;
|
final createdAt = material['createdAt'] as Timestamp?;
|
||||||
// Inferir tipo pela extensão do filename
|
|
||||||
final extension = path.extension(fileName).toLowerCase();
|
final extension = path.extension(fileName).toLowerCase();
|
||||||
final fileType = extension == '.pdf'
|
final fileType = extension == '.pdf'
|
||||||
? 'pdf'
|
? 'pdf'
|
||||||
@@ -179,7 +298,6 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
|
|
||||||
final docId = materials[index].id;
|
final docId = materials[index].id;
|
||||||
final url = material['url'] as String?;
|
final url = material['url'] as String?;
|
||||||
final classId = material['classId'] as String?;
|
|
||||||
|
|
||||||
return _buildMaterialCard(
|
return _buildMaterialCard(
|
||||||
docId: docId,
|
docId: docId,
|
||||||
@@ -193,12 +311,42 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
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() {
|
void _showUploadOptions(String classId) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -242,7 +390,7 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
subtitle: 'Selecionar ficheiro PDF',
|
subtitle: 'Selecionar ficheiro PDF',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_selectPDF();
|
_selectPDF(classId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -253,7 +401,7 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
subtitle: 'Escolher foto existente',
|
subtitle: 'Escolher foto existente',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_selectImageFromGallery();
|
_selectImageFromGallery(classId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -264,7 +412,7 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
subtitle: 'Tirar foto nova',
|
subtitle: 'Tirar foto nova',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_takePhotoWithCamera();
|
_takePhotoWithCamera(classId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -332,7 +480,105 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectPDF() async {
|
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 {
|
try {
|
||||||
const XTypeGroup pdfTypeGroup = XTypeGroup(
|
const XTypeGroup pdfTypeGroup = XTypeGroup(
|
||||||
label: 'PDFs',
|
label: 'PDFs',
|
||||||
@@ -343,10 +589,15 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
final XFile? file = await openFile(acceptedTypeGroups: [pdfTypeGroup]);
|
final XFile? file = await openFile(acceptedTypeGroups: [pdfTypeGroup]);
|
||||||
if (file == null) return;
|
if (file == null) return;
|
||||||
|
|
||||||
await _pickClassAndUpload(
|
final originalName = path.basename(file.path);
|
||||||
|
final finalName = await _showRenameDialog(originalName);
|
||||||
|
if (finalName == null) return;
|
||||||
|
|
||||||
|
await _uploadFile(
|
||||||
filePath: file.path,
|
filePath: file.path,
|
||||||
fileName: path.basename(file.path),
|
fileName: finalName,
|
||||||
fileType: 'pdf',
|
fileType: 'pdf',
|
||||||
|
classId: classId,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -355,7 +606,7 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectImageFromGallery() async {
|
Future<void> _selectImageFromGallery(String classId) async {
|
||||||
try {
|
try {
|
||||||
final XFile? image = await _imagePicker.pickImage(
|
final XFile? image = await _imagePicker.pickImage(
|
||||||
source: ImageSource.gallery,
|
source: ImageSource.gallery,
|
||||||
@@ -365,10 +616,15 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
);
|
);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
await _pickClassAndUpload(
|
final originalName = path.basename(image.path);
|
||||||
|
final finalName = await _showRenameDialog(originalName);
|
||||||
|
if (finalName == null) return;
|
||||||
|
|
||||||
|
await _uploadFile(
|
||||||
filePath: image.path,
|
filePath: image.path,
|
||||||
fileName: path.basename(image.path),
|
fileName: finalName,
|
||||||
fileType: 'image',
|
fileType: 'image',
|
||||||
|
classId: classId,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -377,7 +633,7 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _takePhotoWithCamera() async {
|
Future<void> _takePhotoWithCamera(String classId) async {
|
||||||
try {
|
try {
|
||||||
final XFile? photo = await _imagePicker.pickImage(
|
final XFile? photo = await _imagePicker.pickImage(
|
||||||
source: ImageSource.camera,
|
source: ImageSource.camera,
|
||||||
@@ -387,10 +643,15 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
);
|
);
|
||||||
if (photo == null) return;
|
if (photo == null) return;
|
||||||
|
|
||||||
await _pickClassAndUpload(
|
final originalName = path.basename(photo.path);
|
||||||
|
final finalName = await _showRenameDialog(originalName);
|
||||||
|
if (finalName == null) return;
|
||||||
|
|
||||||
|
await _uploadFile(
|
||||||
filePath: photo.path,
|
filePath: photo.path,
|
||||||
fileName: path.basename(photo.path),
|
fileName: finalName,
|
||||||
fileType: 'image',
|
fileType: 'image',
|
||||||
|
classId: classId,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -399,142 +660,6 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickClassAndUpload({
|
|
||||||
required String filePath,
|
|
||||||
required String fileName,
|
|
||||||
required String fileType,
|
|
||||||
}) async {
|
|
||||||
final currentUser = AuthService.currentUser;
|
|
||||||
if (currentUser == null) {
|
|
||||||
_showErrorSnackBar('Utilizador não autenticado');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carregar as disciplinas do professor
|
|
||||||
List<Map<String, String>> teacherClasses = [];
|
|
||||||
try {
|
|
||||||
final snapshot = await _firestore
|
|
||||||
.collection('classes')
|
|
||||||
.where('teacherId', isEqualTo: currentUser.uid)
|
|
||||||
.orderBy('createdAt', descending: true)
|
|
||||||
.get();
|
|
||||||
teacherClasses = snapshot.docs.map((doc) {
|
|
||||||
final data = doc.data();
|
|
||||||
return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)};
|
|
||||||
}).toList();
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
// Se o professor não tem disciplinas, fazer upload sem associar disciplina
|
|
||||||
if (teacherClasses.isEmpty) {
|
|
||||||
await _uploadFile(
|
|
||||||
filePath: filePath,
|
|
||||||
fileName: fileName,
|
|
||||||
fileType: fileType,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar diálogo de seleção de disciplina
|
|
||||||
String? selectedClassId = await showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (dialogContext) {
|
|
||||||
String? picked;
|
|
||||||
return StatefulBuilder(
|
|
||||||
builder: (context, setDialogState) => AlertDialog(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
title: const Text(
|
|
||||||
'Escolher Disciplina',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Seleciona a disciplina que terá acesso a este material:',
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
DropdownButtonFormField<String>(
|
|
||||||
value: picked,
|
|
||||||
isExpanded: true,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
filled: true,
|
|
||||||
fillColor: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surfaceContainerHighest,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
hint: const Text('Seleciona a disciplina'),
|
|
||||||
items: teacherClasses
|
|
||||||
.map(
|
|
||||||
(c) => DropdownMenuItem<String>(
|
|
||||||
value: c['id'],
|
|
||||||
child: Text(c['name']!),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: (value) => setDialogState(() => picked = value),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(null),
|
|
||||||
child: const Text('Cancelar'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: picked == null
|
|
||||||
? null
|
|
||||||
: () => Navigator.of(dialogContext).pop(picked),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFFF68D2D),
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: const Text('Confirmar'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Se cancelou o diálogo, não fazer upload
|
|
||||||
if (selectedClassId == null) return;
|
|
||||||
|
|
||||||
await _uploadFile(
|
|
||||||
filePath: filePath,
|
|
||||||
fileName: fileName,
|
|
||||||
fileType: fileType,
|
|
||||||
classId: selectedClassId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _uploadFile({
|
Future<void> _uploadFile({
|
||||||
required String filePath,
|
required String filePath,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
@@ -700,18 +825,6 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _getClassName(String classId) async {
|
|
||||||
try {
|
|
||||||
final doc = await _firestore.collection('classes').doc(classId).get();
|
|
||||||
if (doc.exists) {
|
|
||||||
return doc.data()?['name'] as String?;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMaterialCard({
|
Widget _buildMaterialCard({
|
||||||
required String docId,
|
required String docId,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
@@ -773,58 +886,13 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
|||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
subtitle: classId != null
|
subtitle: Text(
|
||||||
? FutureBuilder<String?>(
|
formattedDate,
|
||||||
future: _getClassName(classId),
|
style: TextStyle(
|
||||||
builder: (context, snap) {
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
final className = snap.data;
|
fontSize: 13,
|
||||||
return Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
formattedDate,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (className != null) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.school_outlined,
|
|
||||||
size: 12,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
className,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: Text(
|
|
||||||
formattedDate,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||||
tooltip: 'Eliminar',
|
tooltip: 'Eliminar',
|
||||||
|
|||||||
Reference in New Issue
Block a user