diff --git a/lib/AddPage.dart b/lib/AddPage.dart index cfe5410..b717bb4 100644 --- a/lib/AddPage.dart +++ b/lib/AddPage.dart @@ -1,8 +1,10 @@ import 'dart:io'; +import 'dart:math'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:image/image.dart' as img; class AddScreen extends StatefulWidget { @@ -35,6 +37,27 @@ class _AddScreenState extends State { return fileSize <= maxSizeInBytes; } + Future _cropImage(File imageFile) async { + final originalImage = img.decodeImage(await imageFile.readAsBytes()); + + if (originalImage != null) { + int originSize = min(originalImage.width, originalImage.height); + final croppedImage = img.copyCrop( + originalImage, + x: (originalImage.width - originSize)~/2, + y: (originalImage.height - originSize)~/2, + width: originSize, + height: originSize + ); + final newPath = imageFile.path.substring(0, imageFile.path.lastIndexOf('/')); + final newFile = File('$newPath/cropped.jpg'); + await newFile.writeAsBytes(img.encodePng(croppedImage)); + return newFile; + } else { + throw Exception('Unable to decode image file.'); + } + } + Future _getImage(ImageSource source) async { final pickedFile = await ImagePicker().pickImage(source: source); @@ -42,9 +65,28 @@ class _AddScreenState extends State { final tempFile = File(pickedFile.path); final bool isValid = await isFileSizeValid(tempFile, 10); if (isValid) { - setState(() { - _imageFile = tempFile; - }); + // Bildinformationen lesen + final originalImage = img.decodeImage(await tempFile.readAsBytes()); + + if (originalImage != null) { + if (originalImage.width == originalImage.height) { + // Wenn das Bild bereits im 1:1-Format ist, überspringen Sie das Zuschneiden + setState(() { + _imageFile = tempFile; + print("1:1"); + }); + + } else { + // Wenn das Bild nicht im 1:1-Format ist, schneiden Sie es zu + File croppedFile = await _cropImage(tempFile); + setState(() { + _imageFile = croppedFile; + }); + print("decode"); + } + } else { + throw Exception('Unable to decode image file.'); + } } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Die Dateigröße darf maximal 10 MB betragen.')), @@ -53,6 +95,8 @@ class _AddScreenState extends State { } } + + Future addNewElementToFirestore(String title, String description, File imageFile, {required Function(bool) onComplete}) async { String collectionName; if (_selectedIndex == 0) { diff --git a/lib/DetailPage.dart b/lib/DetailPage.dart index 74d97eb..7d0ae5a 100644 --- a/lib/DetailPage.dart +++ b/lib/DetailPage.dart @@ -1,5 +1,9 @@ -import 'package:cached_network_image/cached_network_image.dart'; +import 'dart:typed_data'; +import 'package:pdf/widgets.dart' as pdfWidgets; +import 'package:printing/printing.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:cached_network_image/cached_network_image.dart'; class DetailPage extends StatelessWidget { final String title; @@ -15,11 +19,62 @@ class DetailPage extends StatelessWidget { required this.imageUrl, }) : super(key: key); + + Future _printPdf() async { + final pdf = pdfWidgets.Document(); + final image = await _loadNetworkImage(imageUrl); + + pdf.addPage( + pdfWidgets.MultiPage( + build: (context) => [ + pdfWidgets.Center( + child: pdfWidgets.SizedBox( + width: double.infinity, // Die Breite auf die maximal mögliche Breite setzen + child: pdfWidgets.AspectRatio( + aspectRatio: 1, // Sie können das Seitenverhältnis entsprechend Ihrem Bild anpassen + child: pdfWidgets.Image(image), + ), + ), + ), + pdfWidgets.Text( + title, + style: pdfWidgets.TextStyle( + fontSize: 20, + fontWeight: pdfWidgets.FontWeight.bold // Titel fett + ) + ), + pdfWidgets.Text(datum, style: const pdfWidgets.TextStyle(fontSize: 14, )), + pdfWidgets.Paragraph(text: description, style: const pdfWidgets.TextStyle(fontSize: 12)), + ], + ), + ); + await Printing.sharePdf(bytes: await pdf.save(), filename: '$title.pdf'); + } + + Future _loadNetworkImage(String url) async { + final response = await http.get(Uri.parse(url)); + + if (response.statusCode != 200) { + throw Exception('Failed to load network image.'); + } + + final Uint8List bytes = response.bodyBytes; + final imageProvider = pdfWidgets.MemoryImage(bytes); + + return imageProvider; + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(title), + actions: [ + IconButton( + icon: const Icon(Icons.picture_as_pdf), + onPressed: _printPdf, + ), + ], ), body: SingleChildScrollView( child: Column( diff --git a/lib/UserDefinedList.dart b/lib/UserDefinedList.dart index 357cb00..336a2a0 100644 --- a/lib/UserDefinedList.dart +++ b/lib/UserDefinedList.dart @@ -3,14 +3,22 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:printing/printing.dart'; +import 'AddPage.dart'; import 'DetailPage.dart'; import 'package:intl/intl.dart'; +import 'package:pdf/widgets.dart' as pdfWidgets; +import 'package:http/http.dart' as http; +import 'package:image/image.dart' as img; class UserDefinedItem extends StatelessWidget { final String title; final String datum; final String description; final String imageUrl; + final Function onLongPress; + final bool isSelected; + final VoidCallback onTap; const UserDefinedItem({ Key? key, @@ -18,43 +26,50 @@ class UserDefinedItem extends StatelessWidget { required this.description, required this.datum, required this.imageUrl, + required this.onLongPress, + required this.isSelected, + required this.onTap, }) : super(key: key); + @override Widget build(BuildContext context) { - return Card( - child: InkWell( - onTap: () { - // Open a new screen when the user taps on the list item - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DetailPage( - title: title, - datum: datum, - description: description, - imageUrl: imageUrl, - ), - ), - ); - }, + return InkWell( + onTap:onTap, + onLongPress: onLongPress as void Function()?, + child: Card( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 120, - height: 120, - child: CachedNetworkImage( - imageUrl: imageUrl, - placeholder: (context, url) => const Center( - child: SizedBox( - width: 50, - height: 50, - child: CircularProgressIndicator(), + Stack( + children: [ + SizedBox( + width: 120, + height: 120, + child: CachedNetworkImage( + imageUrl: imageUrl, + placeholder: (context, url) => const Center( + child: SizedBox( + width: 50, + height: 50, + child: CircularProgressIndicator(), + ), + ), + errorWidget: (context, url, error) => const Icon(Icons.error), ), ), - errorWidget: (context, url, error) => const Icon(Icons.error), - ), + if (isSelected) // Wenn das Element ausgewählt ist, zeigen Sie die Checkbox an + Positioned( + top: 0, + left: 0, + child: Checkbox( + value: isSelected, + onChanged: (bool? value) { + onLongPress(); // Wenn die Checkbox angeklickt wird, rufen Sie die onLongPress-Funktion auf + }, + ), + ), + ], ), Flexible( flex: 1, @@ -104,9 +119,13 @@ class UserDefinedItem extends StatelessWidget { } } + + class GenericList extends StatefulWidget { final String collectionName; - const GenericList({Key? key, required this.collectionName}) : super(key: key); + final int selectedIndex; + + const GenericList({required this.collectionName, required this.selectedIndex}); @override _GenericListState createState() => _GenericListState(); @@ -114,6 +133,7 @@ class GenericList extends StatefulWidget { class _GenericListState extends State { final FirebaseFirestore firestore = FirebaseFirestore.instance; + Map> selectedItems = {}; @override void initState() { @@ -122,7 +142,19 @@ class _GenericListState extends State { firestore.enablePersistence(); // Enable network firestore.settings = - const Settings(cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED); + const Settings(cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED); + } + @override + void didUpdateWidget(GenericList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedIndex != oldWidget.selectedIndex) { + selectedItems.clear(); + } + } + void clearSelectedItems() { + setState(() { + selectedItems.clear(); + }); } Stream getDocumentStream() { @@ -139,7 +171,9 @@ class _GenericListState extends State { await FirebaseStorage.instance.refFromURL(imageUrl).delete(); } // Löschen Sie das Dokument von der Firestore-Kollektion - await firestore.collection(widget.collectionName).doc(documentId).delete(); + await firestore.collection(widget.collectionName) + .doc(documentId) + .delete(); } catch (e) { if (kDebugMode) { print('Error deleting document: $e'); @@ -149,53 +183,237 @@ class _GenericListState extends State { @override Widget build(BuildContext context) { - return StreamBuilder( - stream: getDocumentStream(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } - switch (snapshot.connectionState) { - case ConnectionState.waiting: - return const Text('Loading...'); - default: - final List documents = snapshot.data!.docs; - return ListView.builder( - itemCount: documents.length, - itemBuilder: (BuildContext context, int index) { - final Map data = - documents[index].data()! as Map; - final String documentId = documents[index].id; - final DateTime createdDate = data['createdDate'].toDate(); - final formattedDate = - DateFormat('dd.MM.yyyy').format(createdDate); - return Dismissible( - key: UniqueKey(), - direction: DismissDirection.endToStart, - onDismissed: (direction) { - _deleteDocument(documentId, data['imageUrl']); - }, - background: Container( - color: Colors.red, - child: const Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.only(right: 16), - child: Icon(Icons.delete, color: Colors.white), - ), + return Scaffold( + body: Stack( + children: [ + StreamBuilder( + stream: getDocumentStream(), + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const Text('Loading...'); + default: + final List documents = snapshot.data! + .docs; + return ListView.builder( + itemCount: documents.length, + itemBuilder: (BuildContext context, int index) { + final Map data = + documents[index].data()! as Map; + final String documentId = documents[index].id; + final DateTime createdDate = data['createdDate'].toDate(); + final formattedDate = + DateFormat('dd.MM.yyyy').format(createdDate); + return Dismissible( + key: UniqueKey(), + direction: DismissDirection.endToStart, + onDismissed: (direction) { + _deleteDocument(documentId, data['imageUrl']); + }, + background: Container( + color: Colors.red, + child: const Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only(right: 16), + child: Icon(Icons.delete, color: Colors.white), + ), + ), + ), + child: UserDefinedItem( + title: data['title'] ?? '', + datum: formattedDate, + description: data['description'] ?? '', + imageUrl: data['imageUrl'] ?? '', + isSelected: selectedItems.containsKey(documentId), + onTap: () { + clearSelectedItems(); + // Open a new screen when the user taps on the list item + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DetailPage( + title: data['title'], + datum: formattedDate, + description: data['description'], + imageUrl: data['imageUrl'], + ), + ), + ); + }, + onLongPress: () { + setState(() { + if (selectedItems.containsKey(documentId)) { + selectedItems.remove(documentId); + } else { + selectedItems[documentId] = data; + } + }); + }, + ), + ); + }, + ); + } + }, + ), + if (selectedItems.isNotEmpty) + Stack( + children: [ + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: FloatingActionButton( + backgroundColor: Colors.blue, + onPressed: () { + clearSelectedItems(); + }, + tooltip: "Auswahl zurücksetzen", + child: const Icon(Icons.refresh), ), ), - child: UserDefinedItem( - title: data['title'] ?? '', - datum: formattedDate, - description: data['description'] ?? '', - imageUrl: data['imageUrl'] ?? '', + ), + Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () => _printSelectedItemsAsPdf(widget.selectedIndex), + tooltip: "Ausgewählte Elemente als pdf speichern", + child: const Icon(Icons.picture_as_pdf), + ), ), - ); - }, - ); - } - }, + ), + ], + ), + ], + + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + clearSelectedItems(); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => AddScreen(initialIndex: widget.selectedIndex)), + ); + }, + tooltip: "Neues Element hinzufügen", + child: const Icon(Icons.add), + ), ); } + + Future _loadNetworkImage(String url) async { + final response = await http.get(Uri.parse(url)); + + if (response.statusCode != 200) { + throw Exception('Failed to load network image.'); + } + final Uint8List bytes = response.bodyBytes; + + final img.Image? image = img.decodeImage(bytes); + final img.Image resizedImage = img.copyResize(image!, width: 500); // Beispielwert für die Breite + final Uint8List resizedBytes = Uint8List.fromList(img.encodeJpg(resizedImage)); + + final imageProvider = pdfWidgets.MemoryImage(resizedBytes); + return imageProvider; + } + + Future _printSelectedItemsAsPdf(int selectedIndex) async { + final pdfWidgets.Document pdf = pdfWidgets.Document(); + + final List items = []; + for (final entry in selectedItems.entries) { + final String title = entry.value['title'] ?? ''; + final String description = (entry.value['description'] ?? '').length > 165 ? (entry.value['description'] ?? '').substring(0, 165) + '...' : (entry.value['description'] ?? ''); + final String imageUrl = entry.value['imageUrl'] ?? ''; + final DateTime date = entry.value['createdDate'].toDate(); + final String formattedDate = DateFormat('dd.MM.yyyy').format(date); + final pdfWidgets.ImageProvider imageProvider = await _loadNetworkImage(imageUrl); + + items.add( + pdfWidgets.Container( + height: 145, // Begrenzt die Höhe des Containers + padding: const pdfWidgets.EdgeInsets.all(5.0), // Reduziert den Abstand + child: pdfWidgets.Row( + children: [ + pdfWidgets.Container( + width: 145, // Reduziert die Breite des Bildes + height: 145, // Reduziert die Höhe des Bildes + child: pdfWidgets.Image(imageProvider), + ), + pdfWidgets.Flexible( + child: pdfWidgets.Padding( + padding: const pdfWidgets.EdgeInsets.all(5.0), // Reduziert den Abstand + child: pdfWidgets.Column( + crossAxisAlignment: pdfWidgets.CrossAxisAlignment.start, + children: [ + pdfWidgets.Row( + mainAxisAlignment: pdfWidgets.MainAxisAlignment.spaceBetween, + children: [ + pdfWidgets.Flexible( + child: pdfWidgets.Text( + title, + style: pdfWidgets.TextStyle( + fontSize: 20, fontWeight: pdfWidgets.FontWeight.bold), + ), + ), + pdfWidgets.Text( + formattedDate, + style: const pdfWidgets.TextStyle( + fontSize: 15, + ), + ), + + ], + ), + pdfWidgets.Padding( + padding: const pdfWidgets.EdgeInsets.all(5.0), // Reduziert den Abstand + child: pdfWidgets.Text( + description, + style: const pdfWidgets.TextStyle( + fontSize: 15, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + pdf.addPage( + pdfWidgets.MultiPage( + build: (pdfWidgets.Context context) => items, + ), + ); + + String filename; + switch(selectedIndex) { + case 0: + filename = 'Bürgerforum_items.pdf'; + break; + case 1: + filename = 'Neuigkeiten_items.pdf'; + break; + case 2: + filename = 'Mängel_items.pdf'; + break; + default: + filename = 'selected_items.pdf'; + } + + await Printing.sharePdf(bytes: await pdf.save(), filename: filename); + clearSelectedItems(); + } } diff --git a/lib/home.dart b/lib/home.dart index ae78e26..c8b4c47 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -34,21 +34,6 @@ class _MyHomePageState extends State { body: Center( child: widgetOptions.elementAt(_selectedIndex), ), - floatingActionButton: _selectedIndex == 3 - ? null - : FloatingActionButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AddScreen(initialIndex: _selectedIndex), - ), - ); - }, - tooltip: 'Add', - child: const Icon(Icons.add), - ), bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem( diff --git a/pubspec.lock b/pubspec.lock index a63eef7..c82e205 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.10.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "52570564684bbb0240a9f1fdb6bad12adc5e0540103c1c96d6dd550bd928b1c9" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + bidi: + dependency: transitive + description: + name: bidi + sha256: dc00274c7edabae2ab30c676e736ea1eb0b1b7a1b436cb5fe372e431ccb39ab0 + url: "https://pub.dev" + source: hosted + version: "2.0.6" boolean_selector: dependency: transitive description: @@ -312,6 +328,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.16" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + sha256: "710ab4b7953e9ce1d27d833f741e5f8f3afb0b0ba3556dc0b844741b5f55c2b3" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: "09e93a8ec0435adcaa23622ac090442872f18145d70b9ff605ffedcf97d56255" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: "62349e3aab63873ea9b9ab9f69d036ab8a0d74b3004beec4303981386cb9273f" + url: "https://pub.dev" + source: hosted + version: "3.0.3" image_picker: dependency: "direct main" description: @@ -376,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" matcher: dependency: transitive description: @@ -416,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.2" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider: dependency: transitive description: @@ -464,6 +520,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.6" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "586d3debf5432e5377044754032cfa53ab45e9abf371d4865e9ad5019570e246" + url: "https://pub.dev" + source: hosted + version: "3.10.1" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: "754867bf32c4683da29ccc6bf31250905431fb0818a7258c153e21cd627b3212" + url: "https://pub.dev" + source: hosted + version: "1.0.0" pedantic: dependency: transitive description: @@ -504,6 +576,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.3" + printing: + dependency: "direct main" + description: + name: printing + sha256: "6aa86779d51f1c60608defee7b231e1133ab9b00f63b3b71abfa85cb39898571" + url: "https://pub.dev" + source: hosted + version: "5.10.4" process: dependency: transitive description: @@ -512,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" rxdart: dependency: transitive description: @@ -647,4 +735,4 @@ packages: version: "6.2.2" sdks: dart: ">=2.19.0 <3.0.0" - flutter: ">=3.3.0" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 292470e..31a2529 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,10 @@ dependencies: change_app_package_name: ^1.1.0 cached_network_image: ^3.2.3 image: ^4.0.16 + pdf: ^3.10.1 + printing: ^5.10.4 + image_cropper: ^3.0.3 + logging: ^1.1.1 dev_dependencies: