Skip to content

Commit 10a735c

Browse files
refactor media capture buttons WIP
1 parent 4fc302a commit 10a735c

9 files changed

Lines changed: 674 additions & 466 deletions

File tree

school_data_hub_flutter/lib/common/widgets/growth_dropdown.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ class GrowthDropdown extends StatelessWidget {
4141
}
4242
}
4343

44+
const _itemPadding = EdgeInsets.all(4.0);
45+
4446
List<DropdownMenuItem<int>> competenceCheckDropdownItems = [
4547
const DropdownMenuItem(
4648
value: 0,
4749
alignment: AlignmentDirectional.center,
48-
child: Center(
49-
child: Icon(Icons.question_mark_rounded, color: Colors.black, size: 50),
50-
),
50+
child: Icon(Icons.question_mark_rounded, color: Colors.black, size: 50),
5151
),
5252
DropdownMenuItem(
5353
value: 1,
5454
child: Padding(
55-
padding: const EdgeInsets.all(4),
55+
padding: _itemPadding,
5656
child: Container(
5757
decoration: BoxDecoration(
5858
color: AppColors.growthIconColor1,
@@ -69,7 +69,7 @@ List<DropdownMenuItem<int>> competenceCheckDropdownItems = [
6969
DropdownMenuItem(
7070
value: 2,
7171
child: Padding(
72-
padding: const EdgeInsets.all(4.0),
72+
padding: _itemPadding,
7373
child: Container(
7474
decoration: BoxDecoration(
7575
color: AppColors.growthIconColor2,
@@ -86,7 +86,7 @@ List<DropdownMenuItem<int>> competenceCheckDropdownItems = [
8686
DropdownMenuItem(
8787
value: 3,
8888
child: Padding(
89-
padding: const EdgeInsets.all(4.0),
89+
padding: _itemPadding,
9090
child: Container(
9191
decoration: BoxDecoration(
9292
color: AppColors.growthIconColor3,
@@ -103,7 +103,7 @@ List<DropdownMenuItem<int>> competenceCheckDropdownItems = [
103103
DropdownMenuItem(
104104
value: 4,
105105
child: Padding(
106-
padding: const EdgeInsets.all(2.0),
106+
padding: _itemPadding,
107107
child: Container(
108108
decoration: BoxDecoration(
109109
color: AppColors.growthIconColor4,
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import 'dart:io';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_it/flutter_it.dart';
5+
import 'package:gap/gap.dart';
6+
import 'package:school_data_hub_client/school_data_hub_client.dart';
7+
import 'package:school_data_hub_flutter/common/audio/audio.dart';
8+
import 'package:school_data_hub_flutter/common/services/notification_service.dart';
9+
import 'package:school_data_hub_flutter/common/theme/app_colors.dart';
10+
import 'package:school_data_hub_flutter/common/widgets/dialogs/confirmation_dialog.dart';
11+
import 'package:school_data_hub_flutter/common/widgets/encrypted_document_image.dart';
12+
import 'package:school_data_hub_flutter/common/widgets/media_capture_buttons.dart';
13+
import 'package:school_data_hub_flutter/core/models/datetime_extensions.dart';
14+
import 'package:school_data_hub_flutter/core/session/hub_session_manager.dart';
15+
16+
/// A reusable widget for displaying and managing document collections.
17+
///
18+
/// Supports both image and audio documents, with optional metadata display
19+
/// (creation date and creator) and admin-controlled deletion.
20+
class HubDocumentsSection extends StatelessWidget {
21+
/// The list of documents to display.
22+
final List<HubDocument>? documents;
23+
24+
/// Callback when a file is captured (photo/video).
25+
final Future<void> Function(File? file) onFileCaptured;
26+
27+
/// Callback when a file is recorded (audio).
28+
final Future<void> Function(File? file, Map<String, dynamic>? fileInfo)?
29+
onFileRecorded;
30+
31+
/// Callback when a document should be removed.
32+
final Future<void> Function(String documentId) onDeleteDocument;
33+
34+
/// Maximum number of documents allowed (default: 4).
35+
final int maxDocuments;
36+
37+
/// Wether ro have a spacer between domuments and buttons
38+
final bool withSpacer;
39+
40+
/// Whether to show metadata (date, creator) under each document.
41+
final bool showMetadata;
42+
43+
/// Optional title to display above the documents section.
44+
final String? title;
45+
46+
/// Optional padding for the MediaCaptureButtons.
47+
final EdgeInsets? captureButtonPadding;
48+
49+
/// Optional background color for MediaCaptureButtons.
50+
final Color? captureButtonBackgroundColor;
51+
52+
/// Optional icon color for MediaCaptureButtons.
53+
final Color? captureButtonIconColor;
54+
55+
const HubDocumentsSection({
56+
required this.documents,
57+
required this.withSpacer,
58+
required this.onFileCaptured,
59+
required this.onDeleteDocument,
60+
this.onFileRecorded,
61+
this.maxDocuments = 4,
62+
this.showMetadata = true,
63+
this.title,
64+
this.captureButtonPadding,
65+
this.captureButtonBackgroundColor,
66+
this.captureButtonIconColor,
67+
super.key,
68+
});
69+
70+
@override
71+
Widget build(BuildContext context) {
72+
final files = documents;
73+
final isAdmin = di<HubSessionManager>().isAdmin;
74+
final imageFiles = files?.where((f) => !_isAudioDocument(f)).toList() ?? [];
75+
final audioFiles = files?.where((f) => _isAudioDocument(f)).toList() ?? [];
76+
final totalCount = (files?.length ?? 0);
77+
78+
return Column(
79+
crossAxisAlignment: CrossAxisAlignment.start,
80+
children: [
81+
if (title != null) ...[
82+
Text(title!, style: const TextStyle(fontWeight: FontWeight.bold)),
83+
const Gap(4),
84+
],
85+
Row(
86+
mainAxisSize: MainAxisSize.min,
87+
children: [
88+
for (final file in imageFiles) ...[
89+
_DocumentItem(
90+
file: file,
91+
isAdmin: isAdmin,
92+
showMetadata: showMetadata,
93+
onDelete: () => onDeleteDocument(file.documentId),
94+
),
95+
const Gap(10),
96+
],
97+
for (final file in audioFiles) ...[
98+
_AudioDocumentItem(
99+
file: file,
100+
showMetadata: showMetadata,
101+
onDelete: () => onDeleteDocument(file.documentId),
102+
),
103+
const Gap(10),
104+
],
105+
if (totalCount < maxDocuments) ...[
106+
if (withSpacer) const Spacer(),
107+
MediaCaptureButtons(
108+
onFileCaptured: onFileCaptured,
109+
onFileRecorded: (file, fileInfo) {
110+
onFileRecorded?.call(
111+
file,
112+
fileInfo != null ? {'info': fileInfo} : null,
113+
);
114+
},
115+
iconSize: 20,
116+
padding: captureButtonPadding ?? const EdgeInsets.all(11),
117+
backgroundColor:
118+
captureButtonBackgroundColor ?? AppColors.backgroundColor,
119+
iconColor: captureButtonIconColor ?? Colors.white,
120+
),
121+
],
122+
],
123+
),
124+
],
125+
);
126+
}
127+
128+
/// Whether [doc] represents an audio file based on its extension.
129+
static bool _isAudioDocument(HubDocument doc) {
130+
return isAudioDocument(doc.documentId);
131+
}
132+
}
133+
134+
/// Displays a single image document with optional metadata.
135+
class _DocumentItem extends StatelessWidget {
136+
final HubDocument file;
137+
final bool isAdmin;
138+
final bool showMetadata;
139+
final VoidCallback onDelete;
140+
141+
const _DocumentItem({
142+
required this.file,
143+
required this.isAdmin,
144+
required this.showMetadata,
145+
required this.onDelete,
146+
});
147+
148+
@override
149+
Widget build(BuildContext context) {
150+
return Column(
151+
mainAxisSize: MainAxisSize.min,
152+
children: [
153+
if (showMetadata)
154+
Text(
155+
file.createdAt.formatDateForUser(),
156+
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
157+
),
158+
InkWell(
159+
onTap: () {
160+
showDialog(
161+
context: context,
162+
builder: (context) => Dialog(
163+
child: Container(
164+
constraints: const BoxConstraints(
165+
maxWidth: 600,
166+
maxHeight: 800,
167+
),
168+
child: EncryptedDocumentImage(
169+
documentId: file.documentId,
170+
size: 400,
171+
),
172+
),
173+
),
174+
);
175+
},
176+
onLongPress: () async {
177+
if (!isAdmin) {
178+
di<NotificationService>().showSnackBar(
179+
NotificationType.error,
180+
'Nur Admins können Dokumente löschen',
181+
);
182+
return;
183+
}
184+
final confirm = await confirmationDialog(
185+
context: context,
186+
title: 'Dokument löschen',
187+
message: 'Dokument wirklich löschen?',
188+
);
189+
if (confirm != true) return;
190+
191+
onDelete();
192+
},
193+
child: EncryptedDocumentImage(documentId: file.documentId, size: 70),
194+
),
195+
if (showMetadata)
196+
Text(
197+
file.createdBy,
198+
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
199+
),
200+
],
201+
);
202+
}
203+
}
204+
205+
/// Displays a single audio document with optional metadata.
206+
class _AudioDocumentItem extends StatelessWidget {
207+
final HubDocument file;
208+
final bool showMetadata;
209+
final VoidCallback onDelete;
210+
211+
const _AudioDocumentItem({
212+
required this.file,
213+
required this.showMetadata,
214+
required this.onDelete,
215+
});
216+
217+
@override
218+
Widget build(BuildContext context) {
219+
return Column(
220+
mainAxisSize: MainAxisSize.min,
221+
children: [
222+
if (showMetadata)
223+
Text(
224+
file.createdAt.formatDateForUser(),
225+
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
226+
),
227+
AudioButton(file: file, onDelete: (file) async => onDelete()),
228+
if (showMetadata)
229+
Text(
230+
file.createdBy,
231+
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
232+
),
233+
],
234+
);
235+
}
236+
}

0 commit comments

Comments
 (0)