feat: pinch-to-zoom gestures for PDF reader & post editing for admins and authors
Build Android APK / Build Release APKs (push) Successful in 14m41s

This commit is contained in:
2026-09-18 12:05:18 +02:00
parent a228392dc6
commit f10d5ad5ed
8 changed files with 884 additions and 203 deletions
+140 -21
View File
@@ -1,28 +1,147 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../models/post_item.dart';
import '../services/auth_service.dart';
import '../services/firestore_service.dart';
import '../services/pdf_service.dart';
import '../services/storage_service.dart';
import 'add_issue_screen.dart';
import 'add_post_screen.dart';
import 'pdf_viewer_screen.dart';
class DetailScreen extends StatelessWidget {
class DetailScreen extends StatefulWidget {
final PostItem item;
final String? collectionName;
const DetailScreen({super.key, required this.item});
const DetailScreen({
super.key,
required this.item,
this.collectionName,
});
@override
State<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends State<DetailScreen> {
late PostItem _item;
@override
void initState() {
super.initState();
_item = widget.item;
}
Future<void> _editItem() async {
final collection = widget.collectionName ??
(_item.street != null ? 'Issues' : 'CitizensForum');
PostItem? updated;
if (collection == 'Issues' || _item.street != null) {
updated = await Navigator.push<PostItem>(
context,
MaterialPageRoute(
builder: (ctx) => AddIssueScreen(issueToEdit: _item),
),
);
} else {
updated = await Navigator.push<PostItem>(
context,
MaterialPageRoute(
builder: (ctx) => AddPostScreen(
collectionName: collection,
screenTitle: 'Beitrag bearbeiten',
postToEdit: _item,
),
),
);
}
if (updated != null && mounted) {
setState(() {
_item = updated!;
});
}
}
Future<void> _deleteItem() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogCtx) => AlertDialog(
title: const Text('Eintrag löschen?'),
content: const Text(
'Möchten Sie diesen Eintrag wirklich unwiderruflich löschen?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogCtx, false),
child: const Text('Abbrechen'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Navigator.pop(dialogCtx, true),
child: const Text('Löschen'),
),
],
),
);
if (confirmed != true || !mounted) return;
final collection = widget.collectionName ??
(_item.street != null ? 'Issues' : 'News');
try {
final storage = StorageService();
final firestore = FirestoreService();
for (final url in _item.imageUrls) {
try {
await storage.deleteFileByUrl(url);
} catch (_) {}
}
await firestore.deletePost(collection, _item.id);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Eintrag gelöscht.')),
);
Navigator.pop(context);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Löschen: $e')),
);
}
}
@override
Widget build(BuildContext context) {
final auth = Provider.of<AuthService>(context);
final currentUser = auth.user;
final isAuthor = currentUser != null && _item.authorUid == currentUser.uid;
final canEdit = !_item.isAussendung && (isAuthor || auth.isAdmin);
final canDelete = !_item.isAussendung && (isAuthor || auth.isAdmin);
final formattedDate =
DateFormat('dd.MM.yyyy HH:mm').format(item.createdDate);
DateFormat('dd.MM.yyyy HH:mm').format(_item.createdDate);
return Scaffold(
appBar: AppBar(
title: Text(item.title, overflow: TextOverflow.ellipsis),
title: Text(_item.title, overflow: TextOverflow.ellipsis),
actions: [
if (canEdit)
IconButton(
icon: const Icon(Icons.edit),
tooltip: 'Beitrag bearbeiten',
onPressed: _editItem,
),
if (canDelete)
IconButton(
icon: const Icon(Icons.delete_outline),
tooltip: 'Beitrag löschen',
onPressed: _deleteItem,
),
IconButton(
icon: const Icon(Icons.picture_as_pdf),
tooltip: 'Als PDF drucken / teilen',
onPressed: () => PdfService.printSinglePost(item),
onPressed: () => PdfService.printSinglePost(_item),
),
],
),
@@ -36,11 +155,11 @@ class DetailScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.imageUrls.isNotEmpty)
if (_item.imageUrls.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: item.imageUrls.first,
imageUrl: _item.imageUrls.first,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (context, url) => const Center(
@@ -58,7 +177,7 @@ class DetailScreen extends StatelessWidget {
),
const SizedBox(height: 16),
Text(
item.title,
_item.title,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
@@ -70,7 +189,7 @@ class DetailScreen extends StatelessWidget {
formattedDate,
style: TextStyle(fontSize: 13, color: Colors.grey.shade700),
),
if (item.status != null) ...[
if (_item.status != null) ...[
const Spacer(),
Container(
padding:
@@ -81,7 +200,7 @@ class DetailScreen extends StatelessWidget {
border: Border.all(color: Colors.blue.shade200),
),
child: Text(
item.status!,
_item.status!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
@@ -91,27 +210,27 @@ class DetailScreen extends StatelessWidget {
],
],
),
if (item.street != null && item.street!.isNotEmpty) ...[
if (_item.street != null && _item.street!.isNotEmpty) ...[
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.location_on, size: 16, color: Colors.redAccent),
const SizedBox(width: 6),
Text(
'Örtlichkeit: ${item.street}',
'Örtlichkeit: ${_item.street}',
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
],
if (item.category != null && item.category!.isNotEmpty) ...[
if (_item.category != null && _item.category!.isNotEmpty) ...[
const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.category, size: 16, color: Colors.orange),
const SizedBox(width: 6),
Text(
'Kategorie: ${item.category}',
'Kategorie: ${_item.category}',
style: TextStyle(color: Colors.grey.shade800),
),
],
@@ -126,10 +245,10 @@ class DetailScreen extends StatelessWidget {
),
const SizedBox(height: 8),
Text(
item.description,
_item.description,
style: const TextStyle(fontSize: 15, height: 1.5),
),
if (item.pdfUrl != null && item.pdfUrl!.isNotEmpty) ...[
if (_item.pdfUrl != null && _item.pdfUrl!.isNotEmpty) ...[
const SizedBox(height: 16),
Card(
color: Colors.red.shade50,
@@ -155,8 +274,8 @@ class DetailScreen extends StatelessWidget {
context,
MaterialPageRoute(
builder: (ctx) => PdfViewerScreen(
title: item.title,
pdfUrl: item.pdfUrl!,
title: _item.title,
pdfUrl: _item.pdfUrl!,
),
),
);
@@ -166,7 +285,7 @@ class DetailScreen extends StatelessWidget {
],
const SizedBox(height: 24),
// Additional images gallery if there are multiple images
if (item.imageUrls.length > 1) ...[
if (_item.imageUrls.length > 1) ...[
const Text(
'Weitere Fotos',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
@@ -176,10 +295,10 @@ class DetailScreen extends StatelessWidget {
height: 120,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: item.imageUrls.length - 1,
itemCount: _item.imageUrls.length - 1,
separatorBuilder: (ctx, i) => const SizedBox(width: 8),
itemBuilder: (ctx, index) {
final url = item.imageUrls[index + 1];
final url = _item.imageUrls[index + 1];
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(