Erweiterungen

This commit is contained in:
Sven committed 2026-02-14 08:52:06 +01:00
1 parent ba1f7d5440
commit bb3dc9e856
28 files changed
+1672 -1291

No files matched your search

+1
View File
@@ -133,6 +133,7 @@ Future<void> showAcceptJobsPopup(SharedMapReference mapReference, Function() onF
// Ensure we always clean up subscriptions even if the dialog is dismissed via barrier tap or back button
dialogFuture.whenComplete(() {
isActive = false;
_scrollController.dispose();
if (builderCreated) {
try {
extendedStatefulBuilder.unsubscribe(queueId: Constants.messageTopicJobReceived);
+262
View File
@@ -0,0 +1,262 @@
import 'package:flutter/material.dart';
import 'package:stadtbote/content_views/pdf_viewer_page.dart';
import 'package:stadtbote/model/tour_file.dart';
import 'package:stadtbote/tools/constants.dart';
import 'package:stadtbote/tools/util.dart' show showAlertDialog;
import 'package:stadtbote/tools/util.dart' as util;
void showFilesPopup(BuildContext context, List<TourFile> files) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Dateien'),
content: SizedBox(
width: double.maxFinite,
height: 400,
child: ListView.builder(
shrinkWrap: true,
itemCount: files.length,
itemBuilder: (context, index) {
final file = files[index];
return _FileListItem(file: file);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Schließen'),
),
],
);
},
);
}
class _FileListItem extends StatefulWidget {
final TourFile file;
const _FileListItem({required this.file});
@override
State<_FileListItem> createState() => _FileListItemState();
}
class _FileListItemState extends State<_FileListItem> {
bool _isLoading = false;
@override
Widget build(BuildContext context) {
IconData iconData;
if (widget.file.isPdf) {
iconData = Icons.picture_as_pdf;
} else if (widget.file.isImage) {
iconData = Icons.image;
} else {
iconData = Icons.insert_drive_file;
}
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
leading: Icon(iconData, color: Constants.colorPrimary),
title: Text(widget.file.name),
subtitle: widget.file.remark != null && widget.file.remark!.isNotEmpty
? Text(widget.file.remark!)
: null,
trailing: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.open_in_new),
onTap: () => _openFile(context),
),
);
}
Future<void> _openFile(BuildContext context) async {
if (_isLoading) return;
setState(() {
_isLoading = true;
});
try {
if (widget.file.isPdf) {
Navigator.of(context).pop(); // Close the files dialog
// Navigate to PDF viewer
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PdfViewerPage(
pdfUrl: widget.file.url,
title: widget.file.name,
),
),
);
} else if (widget.file.isImage) {
Navigator.of(context).pop(); // Close the files dialog
// Show image in a dialog
await showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
title: Text(widget.file.name),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
automaticallyImplyLeading: false,
),
Flexible(
child: Image.network(
widget.file.url,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
'Fehler beim Laden des Bildes:\n$error',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
],
),
);
},
),
),
],
),
);
},
);
} else {
// For other file types, try to open with external app
showAlertDialog(
'Information',
'Dateityp wird nicht direkt unterstützt. Die Datei wird im Browser geöffnet.',
);
}
} catch (e) {
util.Util.logException('Error opening file: $e');
if (context.mounted) {
showAlertDialog(
'Fehler',
'Fehler beim Öffnen der Datei: $e',
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
/// Widget to display a file icon button if files are available
class FilesIconButton extends StatelessWidget {
final List<TourFile>? files;
final double iconSize;
const FilesIconButton({
super.key,
required this.files,
this.iconSize = 24,
});
@override
Widget build(BuildContext context) {
if (files == null || files!.isEmpty) {
return const SizedBox.shrink();
}
return IconButton(
icon: const Icon(Icons.attach_file),
iconSize: iconSize,
color: Constants.colorPrimary,
tooltip: '${files!.length} Datei(en)',
onPressed: () => showFilesPopup(context, files!),
);
}
}
/// Badge widget showing file count with icon
class FilesBadge extends StatelessWidget {
final List<TourFile>? files;
final VoidCallback? onTap;
const FilesBadge({
super.key,
required this.files,
this.onTap,
});
@override
Widget build(BuildContext context) {
if (files == null || files!.isEmpty) {
return const SizedBox.shrink();
}
return GestureDetector(
onTap: onTap ?? () => showFilesPopup(context, files!),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Constants.colorPrimary.withAlpha(30),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Constants.colorPrimary, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.attach_file,
size: 16,
color: Constants.colorPrimary,
),
const SizedBox(width: 4),
Text(
'${files!.length}',
style: TextStyle(
color: Constants.colorPrimary,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
);
}
}
+17 -8
View File
@@ -11,6 +11,9 @@ import '../model/job_tour.dart';
List<Color> _colors = [Colors.red, Colors.green];
showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList) async {
MapController? mapController;
Map<int, StreamController<bool>> streamControllers = {};
await showDialog(
context: context,
builder: (BuildContext context) {
@@ -19,24 +22,24 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
shadowColor: Colors.transparent,
backgroundColor: Colors.white,
content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
var mapController = MapController();
Map<int, StreamController<bool>> streamController = {};
mapController = MapController();
streamControllers = {};
List<Widget> tourWidgets = [];
for (JobTour tour in tourList) {
var currentStreamController = StreamController<bool>();
streamController[tour.sort] = currentStreamController;
streamControllers[tour.sort] = currentStreamController;
tourWidgets.add(
GestureDetector(
onTap: () async {
if (tour.pos != null) {
mapController.move(LatLng(tour.pos!.lat, tour.pos!.long), mapController.camera.zoom);
mapController!.move(LatLng(tour.pos!.lat, tour.pos!.long), mapController!.camera.zoom);
streamController[currentTour.sort]!.sink.add(false);
streamControllers[currentTour.sort]!.sink.add(false);
currentTour = tour;
streamController[tour.sort]!.sink.add(true);
streamControllers[tour.sort]!.sink.add(true);
} else {
showAlertDialog('Information', 'Es liegt keine Position für diese Station vor');
}
@@ -158,7 +161,7 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
height: 25,
onChanged: (b) {
valueNotifier.value = b;
mapController.move(mapController.camera.center, b ? 14.0 : 16.0);
mapController!.move(mapController!.camera.center, b ? 14.0 : 16.0);
},
styleBuilder: (i) => ToggleStyle(indicatorColor: i == true ? _colors[0] : _colors[1]),
textBuilder: (value) => value ? const Center(child: Text('Fern')) : const Center(child: Text('Nah')),
@@ -173,7 +176,7 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
child: AbsorbPointer(
absorbing: true,
child: FlutterMap(
mapController: mapController,
mapController: mapController!,
options: MapOptions(
initialCenter: LatLng(currentTour.pos!.lat, currentTour.pos!.long),
initialZoom: 14,
@@ -196,4 +199,10 @@ showMapPopup(BuildContext context, JobTour currentTour, List<JobTour> tourList)
));
}));
});
// Dispose all controllers after dialog is closed
mapController?.dispose();
for (var controller in streamControllers.values) {
controller.close();
}
}
@@ -50,7 +50,12 @@ showComissionNumberPopup(BuildContext context, TaskInfo taskInfo, Function(Strin
controller: textEditingController,
autofocus: true),
const SizedBox(height: 20),
getDefaultButton('Übernehmen', () => {Navigator.of(context).pop(), onFinished(textEditingController.text)})
getDefaultButton('Übernehmen', () {
final result = textEditingController.text;
textEditingController.dispose();
Navigator.of(context).pop();
onFinished(result);
})
],
),
),
@@ -24,6 +24,8 @@ showPickupDeliveryStatusPopup(BuildContext context, TaskInfo taskInfo, Function(
if (!didPop) {
final shouldPop = await taskDialogSecurityException(context);
if (shouldPop && context.mounted) {
controller.dispose();
remController.dispose();
Navigator.of(context).pop();
}
}
@@ -73,6 +75,9 @@ showPickupDeliveryStatusPopup(BuildContext context, TaskInfo taskInfo, Function(
if (remController.text.isNotEmpty) {
Navigator.of(context).pop(),
controller.dispose(),
remController.dispose(),
onFinished(selectedTourEntry, remController.text)
} else {
showAlertDialog("Fehler", "Bitte geben Sie eine Bemerkung ein")
@@ -80,6 +85,9 @@ showPickupDeliveryStatusPopup(BuildContext context, TaskInfo taskInfo, Function(
} else {
Navigator.of(context).pop(),
controller.dispose(),
remController.dispose(),
onFinished(selectedTourEntry, null)
}
})
@@ -77,7 +77,11 @@ showReceiptGiverPopup(BuildContext context, TaskInfo taskInfo, Function(String,
if (textEditingController.text.isNotEmpty && selectedTourEntry != null) {
Navigator.of(context).pop();
onFinished(textEditingController.text, selectedTourEntry!);
final result = textEditingController.text;
textEditingController.dispose();
controller.dispose();
onFinished(result, selectedTourEntry!);
} else {
showAlertDialog("Fehler", "Sie müssen einen Text eingeben und ein Geschlecht auswählen");
}
@@ -54,10 +54,12 @@ showStationNoticePopup(BuildContext context, TaskInfo taskInfo, Function(String)
autofocus: true),
),
const SizedBox(height: 20),
getDefaultButton('Übernehmen', buttonHeight: 50, () => {
Navigator.of(context).pop(),
onFinished(textEditingController.text)})
getDefaultButton('Übernehmen', buttonHeight: 50, () {
final result = textEditingController.text;
textEditingController.dispose();
Navigator.of(context).pop();
onFinished(result);
})
],
),
),
+10 -9
View File
@@ -67,15 +67,16 @@ showZipPopup(BuildContext context, String text, String buttonText,
const SizedBox(height: 20),
getDefaultButton(
buttonText,
() => {
if (_checkInput(context,
textEditingController.text))
{
Navigator.of(context).pop(),
onFinished(
textEditingController.text)
},
}),
() {
if (_checkInput(context,
textEditingController.text)) {
final result = textEditingController.text;
textEditingController.dispose();
focusNode.dispose();
Navigator.of(context).pop();
onFinished(result);
}
}),
],
),
),