Export CSV/PDF
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// lib/features/igel/data/export_service.dart
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../igel/data/igel_repository.dart';
|
||||
import '../../messwerte/data/messwerte_repository.dart';
|
||||
import '../../messwerte/domain/messwert.dart';
|
||||
|
||||
class IgelExportService {
|
||||
final IgelRepository igelRepo;
|
||||
final MesswerteRepository messRepo;
|
||||
|
||||
IgelExportService({
|
||||
required this.igelRepo,
|
||||
required this.messRepo,
|
||||
});
|
||||
|
||||
String _fmtDate(DateTime? d) =>
|
||||
d == null ? '' : DateFormat('yyyy-MM-dd').format(d);
|
||||
|
||||
String _fmtDateTime(DateTime? d) =>
|
||||
d == null ? '' : DateFormat('yyyy-MM-dd HH:mm').format(d);
|
||||
|
||||
String _csvEscape(String? v, {String delimiter = ';'}) {
|
||||
final s = v ?? '';
|
||||
final needsQuote =
|
||||
s.contains(delimiter) || s.contains('\n') || s.contains('"');
|
||||
final escaped = s.replaceAll('"', '""');
|
||||
return needsQuote ? '"$escaped"' : escaped;
|
||||
}
|
||||
|
||||
/// Erzeugt eine *denormalisierte* CSV:
|
||||
/// Jede Zeile = 1 Messwert + zugehörige Igel-Stammdaten.
|
||||
/// Hat ein Igel *keine* Messwerte, gibt es *trotzdem eine Zeile* mit leeren Messwert-Spalten.
|
||||
Future<void> exportDenormalizedCsv() async {
|
||||
// Immer frisch laden
|
||||
final igelList = await igelRepo.list();
|
||||
|
||||
final sep = ';';
|
||||
final sb = StringBuffer();
|
||||
|
||||
// Kopfzeile – nur Felder, die deine Models wirklich haben
|
||||
sb.writeln([
|
||||
// Igel
|
||||
'igel_id',
|
||||
'igel_name',
|
||||
'igel_gender',
|
||||
'igel_feature',
|
||||
'igel_note',
|
||||
'igel_rescued_at',
|
||||
'igel_location',
|
||||
// Messwert
|
||||
'messwert_id',
|
||||
'messwert_datum',
|
||||
'messwert_gewicht',
|
||||
'messwert_behandlung',
|
||||
'messwert_bemerkung',
|
||||
].join(sep));
|
||||
|
||||
for (final ig in igelList) {
|
||||
List<Messwert> mw = [];
|
||||
try {
|
||||
mw = await messRepo.list(ig.id);
|
||||
} catch (_) {
|
||||
mw = [];
|
||||
}
|
||||
|
||||
if (mw.isEmpty) {
|
||||
// Igel ohne Messwerte → eine Zeile mit leeren Messwert-Spalten
|
||||
sb.writeln([
|
||||
ig.id,
|
||||
_csvEscape(ig.name, delimiter: sep),
|
||||
_csvEscape(ig.gender, delimiter: sep),
|
||||
_csvEscape(ig.feature, delimiter: sep),
|
||||
_csvEscape(ig.note, delimiter: sep),
|
||||
_fmtDate(ig.rescuedAt),
|
||||
_csvEscape(ig.location, delimiter: sep),
|
||||
|
||||
// Messwert-Spalten leer
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].join(sep));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final m in mw) {
|
||||
sb.writeln([
|
||||
ig.id,
|
||||
_csvEscape(ig.name, delimiter: sep),
|
||||
_csvEscape(ig.gender, delimiter: sep),
|
||||
_csvEscape(ig.feature, delimiter: sep),
|
||||
_csvEscape(ig.note, delimiter: sep),
|
||||
_fmtDate(ig.rescuedAt),
|
||||
_csvEscape(ig.location, delimiter: sep),
|
||||
m.id,
|
||||
_fmtDateTime(m.datum),
|
||||
m.gewicht,
|
||||
_csvEscape(m.behandlung, delimiter: sep),
|
||||
_csvEscape(m.bemerkung, delimiter: sep),
|
||||
].join(sep));
|
||||
}
|
||||
}
|
||||
|
||||
final bytes =
|
||||
Uint8List.fromList(const Utf8Encoder().convert(sb.toString()));
|
||||
final filename =
|
||||
'igel_export_denorm_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}';
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: filename,
|
||||
bytes: bytes,
|
||||
ext: 'csv',
|
||||
mimeType: MimeType.csv,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
// lib/features/igel/presentation/igel_detail_screen.dart
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:http_parser/http_parser.dart' show MediaType;
|
||||
import 'dart:ui' as ui show TextDirection;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../igel/data/igel_images_repository.dart';
|
||||
@@ -18,8 +25,8 @@ import '../domain/igel.dart';
|
||||
import '../domain/igel_image.dart';
|
||||
|
||||
// Messwerte
|
||||
import '../../messwerte/domain/messwert.dart';
|
||||
import '../../messwerte/data/messwerte_repository.dart';
|
||||
import '../../messwerte/domain/messwert.dart';
|
||||
|
||||
class IgelDetailScreen extends ConsumerStatefulWidget {
|
||||
const IgelDetailScreen({super.key, required this.igelId});
|
||||
@@ -44,7 +51,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
final featureC = TextEditingController();
|
||||
final noteC = TextEditingController();
|
||||
|
||||
// NEU: Ort/Fundstelle + Gerettet am
|
||||
// Ort/Fundstelle + Gerettet am
|
||||
final locationC = TextEditingController();
|
||||
DateTime? rescuedAt;
|
||||
DateTime? _initRescuedAt;
|
||||
@@ -68,6 +75,9 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
int _uploadDone = 0;
|
||||
int _uploadTotal = 0;
|
||||
|
||||
// Export: Chart als Bild rendern
|
||||
final GlobalKey _chartKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -77,7 +87,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
|
||||
featureC.addListener(_markDirtyBasics);
|
||||
noteC.addListener(_markDirtyBasics);
|
||||
locationC.addListener(_markDirtyBasics); // NEU
|
||||
locationC.addListener(_markDirtyBasics);
|
||||
|
||||
_loadAll();
|
||||
}
|
||||
@@ -87,7 +97,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
nameC.dispose();
|
||||
featureC.dispose();
|
||||
noteC.dispose();
|
||||
locationC.dispose(); // NEU
|
||||
locationC.dispose();
|
||||
mwGewichtC.dispose();
|
||||
mwBehandlungC.dispose();
|
||||
mwBemerkungC.dispose();
|
||||
@@ -111,7 +121,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
newNote != igel!.note ||
|
||||
gender != igel!.gender;
|
||||
|
||||
// NEU: rescuedAt & location berücksichtigen
|
||||
// rescuedAt & location berücksichtigen
|
||||
String? fmt(DateTime? d) =>
|
||||
d == null ? null : DateFormat('yyyy-MM-dd').format(d);
|
||||
if (fmt(rescuedAt) != fmt(_initRescuedAt)) changed = true;
|
||||
@@ -136,7 +146,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
noteC.text = data.note ?? '';
|
||||
gender = data.gender;
|
||||
|
||||
// NEU: rescuedAt + location aus Domain (falls Backend schon erweitert)
|
||||
// rescuedAt + location (sofern im Model vorhanden)
|
||||
rescuedAt = data.rescuedAt;
|
||||
locationC.text = data.location ?? '';
|
||||
_initRescuedAt = data.rescuedAt;
|
||||
@@ -184,7 +194,6 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// NEU: rescuedAt + location mitsenden (Repo wurde entsprechend erweitert)
|
||||
await igelRepo.update(
|
||||
widget.igelId,
|
||||
newName,
|
||||
@@ -204,7 +213,6 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
location: locationC.text.trim().isEmpty ? null : locationC.text.trim(),
|
||||
);
|
||||
|
||||
// NEU: Init-Stand für Change-Detection aktualisieren
|
||||
_initRescuedAt = rescuedAt;
|
||||
_initLocation =
|
||||
locationC.text.trim().isEmpty ? null : locationC.text.trim();
|
||||
@@ -427,12 +435,329 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export: CSV (nur dieser Igel, denormalisiert) -------------------------
|
||||
|
||||
String _fmtDate(DateTime? dt) =>
|
||||
dt == null ? '' : DateFormat('yyyy-MM-dd').format(dt);
|
||||
|
||||
String _fmtDateTime(DateTime dt) {
|
||||
final d = dt.toLocal();
|
||||
String two(int x) => x.toString().padLeft(2, '0');
|
||||
return '${two(d.day)}.${two(d.month)}.${d.year} ${two(d.hour)}:${two(d.minute)}';
|
||||
}
|
||||
|
||||
String _fmtDateTimeIso(DateTime? dt) =>
|
||||
dt == null ? '' : DateFormat('yyyy-MM-dd HH:mm').format(dt);
|
||||
|
||||
String _csvEscape(String? v, {String delimiter = ';'}) {
|
||||
final s = v ?? '';
|
||||
final needsQuote =
|
||||
s.contains(delimiter) || s.contains('\n') || s.contains('"');
|
||||
final escaped = s.replaceAll('"', '""');
|
||||
return needsQuote ? '"$escaped"' : escaped;
|
||||
}
|
||||
|
||||
Future<void> _exportCsvSingleIgel() async {
|
||||
if (igel == null) return;
|
||||
// Sicherheits-Reload der Messwerte, damit CSV aktuell ist
|
||||
final mw = await messRepo.list(widget.igelId);
|
||||
|
||||
final sep = ';';
|
||||
final sb = StringBuffer();
|
||||
|
||||
sb.writeln([
|
||||
'igel_id',
|
||||
'igel_name',
|
||||
'igel_gender',
|
||||
'igel_feature',
|
||||
'igel_note',
|
||||
'igel_rescued_at',
|
||||
'igel_location',
|
||||
'messwert_id',
|
||||
'messwert_datum',
|
||||
'messwert_gewicht',
|
||||
'messwert_behandlung',
|
||||
'messwert_bemerkung',
|
||||
].join(sep));
|
||||
|
||||
if (mw.isEmpty) {
|
||||
sb.writeln([
|
||||
igel!.id,
|
||||
_csvEscape(igel!.name, delimiter: sep),
|
||||
_csvEscape(igel!.gender, delimiter: sep),
|
||||
_csvEscape(igel!.feature, delimiter: sep),
|
||||
_csvEscape(igel!.note, delimiter: sep),
|
||||
_fmtDate(igel!.rescuedAt),
|
||||
_csvEscape(igel!.location, delimiter: sep),
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].join(sep));
|
||||
} else {
|
||||
for (final m in mw) {
|
||||
sb.writeln([
|
||||
igel!.id,
|
||||
_csvEscape(igel!.name, delimiter: sep),
|
||||
_csvEscape(igel!.gender, delimiter: sep),
|
||||
_csvEscape(igel!.feature, delimiter: sep),
|
||||
_csvEscape(igel!.note, delimiter: sep),
|
||||
_fmtDate(igel!.rescuedAt),
|
||||
_csvEscape(igel!.location, delimiter: sep),
|
||||
m.id,
|
||||
_fmtDateTimeIso(m.datum),
|
||||
m.gewicht,
|
||||
_csvEscape(m.behandlung, delimiter: sep),
|
||||
_csvEscape(m.bemerkung, delimiter: sep),
|
||||
].join(sep));
|
||||
}
|
||||
}
|
||||
|
||||
final bytes = Uint8List.fromList(utf8.encode(sb.toString()));
|
||||
final filename =
|
||||
'igel_${igel!.id}_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}';
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: filename,
|
||||
bytes: bytes,
|
||||
ext: 'csv',
|
||||
mimeType: MimeType.csv,
|
||||
);
|
||||
|
||||
_snack('CSV exportiert');
|
||||
}
|
||||
|
||||
// --- Export: PDF -----------------------------------------------------------
|
||||
|
||||
Future<Uint8List?> _captureChartPngBytes() async {
|
||||
try {
|
||||
final ctx = _chartKey.currentContext;
|
||||
if (ctx == null) return null;
|
||||
final boundary = ctx.findRenderObject() as RenderRepaintBoundary?;
|
||||
if (boundary == null) return null;
|
||||
final ui.Image img = await boundary.toImage(pixelRatio: 3);
|
||||
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData?.buffer.asUint8List();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Uint8List?>> _fetchImagesForPdf() async {
|
||||
final List<Uint8List?> result = [];
|
||||
for (final img in images) {
|
||||
final url = img.thumbUrl ?? img.url;
|
||||
try {
|
||||
final resp =
|
||||
await http.get(Uri.parse(url)).timeout(const Duration(seconds: 10));
|
||||
if (resp.statusCode == 200) {
|
||||
result.add(resp.bodyBytes);
|
||||
} else {
|
||||
result.add(null);
|
||||
}
|
||||
} catch (_) {
|
||||
result.add(null);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _exportPdfWithOptions() async {
|
||||
bool includeMesswerte = true;
|
||||
bool includeBilder = false;
|
||||
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (_) => StatefulBuilder(
|
||||
builder: (context, setStateDialog) {
|
||||
return AlertDialog(
|
||||
title: const Text('Export als PDF'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
value: includeMesswerte,
|
||||
onChanged: (v) =>
|
||||
setStateDialog(() => includeMesswerte = v ?? true),
|
||||
title: const Text('Messwerte aufnehmen'),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
CheckboxListTile(
|
||||
value: includeBilder,
|
||||
onChanged: (v) =>
|
||||
setStateDialog(() => includeBilder = v ?? false),
|
||||
title: const Text('Bilder aufnehmen'),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Exportieren'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (ok != true) return;
|
||||
await _exportPdf(
|
||||
includeMesswerte: includeMesswerte, includeBilder: includeBilder);
|
||||
}
|
||||
|
||||
Future<void> _exportPdf(
|
||||
{required bool includeMesswerte, required bool includeBilder}) async {
|
||||
if (igel == null) return;
|
||||
|
||||
// Aktuelle Messwerte/Bilderdaten holen
|
||||
final mw = await messRepo.list(widget.igelId);
|
||||
final chartBytes = await _captureChartPngBytes();
|
||||
final imageBytesList =
|
||||
includeBilder ? await _fetchImagesForPdf() : const <Uint8List?>[];
|
||||
|
||||
final doc = pw.Document();
|
||||
|
||||
final textStyle = pw.TextStyle(fontSize: 12);
|
||||
final headerStyle =
|
||||
pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold);
|
||||
final labelStyle = pw.TextStyle(fontSize: 12, color: PdfColors.grey600);
|
||||
|
||||
pw.Widget infoRow(String label, String value) => pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 2),
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.SizedBox(width: 110, child: pw.Text(label, style: labelStyle)),
|
||||
pw.Expanded(child: pw.Text(value, style: textStyle)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Seite 1: Stammdaten + Chart + optional Messwerte/Bilder (alles synchron)
|
||||
doc.addPage(
|
||||
pw.MultiPage(
|
||||
margin: const pw.EdgeInsets.all(24),
|
||||
build: (context) => [
|
||||
pw.Text('Igel-Bericht', style: headerStyle),
|
||||
pw.SizedBox(height: 10),
|
||||
infoRow('Name', igel!.name),
|
||||
infoRow('Geschlecht', igel!.gender ?? '—'),
|
||||
infoRow('Merkmal', igel!.feature ?? '—'),
|
||||
infoRow(
|
||||
'Gerettet am',
|
||||
rescuedAt == null
|
||||
? '—'
|
||||
: DateFormat('dd.MM.yyyy').format(rescuedAt!)),
|
||||
infoRow('Ort / Fundstelle', igel!.location ?? '—'),
|
||||
infoRow('Information', igel!.note ?? '—'),
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Text('Gewichtsverlauf',
|
||||
style:
|
||||
pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 8),
|
||||
if (chartBytes != null)
|
||||
pw.Image(pw.MemoryImage(chartBytes),
|
||||
height: 200, fit: pw.BoxFit.contain)
|
||||
else
|
||||
pw.Text('Kein Diagramm verfügbar', style: labelStyle),
|
||||
if (includeMesswerte) ...[
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Text('Messwerte',
|
||||
style:
|
||||
pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 6),
|
||||
if (mw.isEmpty)
|
||||
pw.Text('Keine Messwerte vorhanden.', style: labelStyle)
|
||||
else
|
||||
pw.TableHelper.fromTextArray(
|
||||
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||
headers: const [
|
||||
'Datum/Uhrzeit',
|
||||
'Gewicht (g)',
|
||||
'Behandlung',
|
||||
'Bemerkung'
|
||||
],
|
||||
data: mw
|
||||
.map((m) => [
|
||||
DateFormat('dd.MM.yyyy HH:mm')
|
||||
.format(m.datum.toLocal()),
|
||||
m.gewicht.toString(),
|
||||
m.behandlung ?? '',
|
||||
m.bemerkung ?? '',
|
||||
])
|
||||
.toList(),
|
||||
cellStyle: textStyle,
|
||||
headerDecoration:
|
||||
const pw.BoxDecoration(color: PdfColors.grey200),
|
||||
cellAlignment: pw.Alignment.centerLeft,
|
||||
),
|
||||
],
|
||||
if (includeBilder) ...[
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Text('Bilder',
|
||||
style:
|
||||
pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 6),
|
||||
if (imageBytesList.isEmpty)
|
||||
pw.Text('Keine Bilder vorhanden.', style: labelStyle)
|
||||
else
|
||||
pw.Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final bytes in imageBytesList)
|
||||
if (bytes != null)
|
||||
pw.Container(
|
||||
width: 160,
|
||||
height: 120,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.grey300),
|
||||
borderRadius: pw.BorderRadius.circular(6),
|
||||
),
|
||||
padding: const pw.EdgeInsets.all(2),
|
||||
child: pw.Image(pw.MemoryImage(bytes),
|
||||
fit: pw.BoxFit.cover),
|
||||
)
|
||||
else
|
||||
pw.Container(
|
||||
width: 160,
|
||||
height: 120,
|
||||
alignment: pw.Alignment.center,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.grey300),
|
||||
borderRadius: pw.BorderRadius.circular(6),
|
||||
),
|
||||
child:
|
||||
pw.Text('Bild nicht verfügbar', style: labelStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final pdfBytes = await doc.save();
|
||||
final filename =
|
||||
'igel_${igel!.id}_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}';
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: filename,
|
||||
bytes: pdfBytes,
|
||||
ext: 'pdf',
|
||||
mimeType: MimeType.pdf,
|
||||
);
|
||||
|
||||
_snack('PDF exportiert');
|
||||
}
|
||||
|
||||
String _fmtGramm(int g) => '$g g';
|
||||
String? _emptyToNull(String s) => s.trim().isEmpty ? null : s.trim();
|
||||
|
||||
@@ -554,6 +879,21 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
_basicsChanged ? 'Änderungen speichern' : 'Keine Änderungen',
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
tooltip: 'Export',
|
||||
onSelected: (v) async {
|
||||
if (v == 'csv') {
|
||||
await _exportCsvSingleIgel();
|
||||
} else if (v == 'pdf') {
|
||||
await _exportPdfWithOptions();
|
||||
}
|
||||
},
|
||||
itemBuilder: (ctx) => const [
|
||||
PopupMenuItem(value: 'csv', child: Text('Als CSV exportieren')),
|
||||
PopupMenuItem(value: 'pdf', child: Text('Als PDF exportieren…')),
|
||||
],
|
||||
icon: const Icon(Icons.ios_share),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _pickAndUpload,
|
||||
tooltip: 'Bilder hinzufügen',
|
||||
@@ -591,7 +931,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// NEU: Gerettet am (Datum)
|
||||
// Gerettet am (Datum)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading:
|
||||
@@ -622,7 +962,7 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// NEU: Ort / Fundstelle
|
||||
// Ort / Fundstelle
|
||||
TextField(
|
||||
controller: locationC,
|
||||
decoration: const InputDecoration(
|
||||
@@ -677,7 +1017,10 @@ class _IgelDetailState extends ConsumerState<IgelDetailScreen> {
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: WeightChart(data: chartData),
|
||||
child: RepaintBoundary(
|
||||
key: _chartKey,
|
||||
child: WeightChart(data: chartData),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data'; // ⬅️ für Uint8List (FileSaver)
|
||||
import 'package:file_saver/file_saver.dart'; // ⬅️ für Download/Speichern plattformübergreifend
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart'; // ⬅️ NEU: für Datumsausgabe
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
import '../../igel/data/igel_repository.dart';
|
||||
import '../domain/igel.dart';
|
||||
|
||||
// ⬇️ für denormalisierten Export benötigen wir die Messwerte
|
||||
import '../../messwerte/data/messwerte_repository.dart';
|
||||
import '../../messwerte/domain/messwert.dart';
|
||||
|
||||
class IgelListScreen extends ConsumerStatefulWidget {
|
||||
const IgelListScreen({super.key});
|
||||
@override
|
||||
@@ -16,6 +23,7 @@ class IgelListScreen extends ConsumerStatefulWidget {
|
||||
|
||||
class _IgelListState extends ConsumerState<IgelListScreen> {
|
||||
late final IgelRepository repo;
|
||||
late final MesswerteRepository messRepo; // ⬅️ NEU
|
||||
|
||||
List<Igel> items = [];
|
||||
bool busy = true;
|
||||
@@ -45,6 +53,7 @@ class _IgelListState extends ConsumerState<IgelListScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
repo = ref.read(igelRepoProvider);
|
||||
messRepo = ref.read(messwerteRepoProvider); // ⬅️ NEU
|
||||
_load();
|
||||
searchC.addListener(() {
|
||||
_debounce?.cancel();
|
||||
@@ -143,14 +152,144 @@ class _IgelListState extends ConsumerState<IgelListScreen> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// =========================
|
||||
// DENORMALISIERTER CSV-EXPORT
|
||||
// =========================
|
||||
|
||||
String _fmtDate(DateTime? d) =>
|
||||
d == null ? '' : DateFormat('yyyy-MM-dd').format(d);
|
||||
String _fmtDateTime(DateTime? d) =>
|
||||
d == null ? '' : DateFormat('yyyy-MM-dd HH:mm').format(d);
|
||||
|
||||
String _csvEscape(String? v, {String delimiter = ';'}) {
|
||||
final s = v ?? '';
|
||||
final needsQuote =
|
||||
s.contains(delimiter) || s.contains('\n') || s.contains('"');
|
||||
final escaped = s.replaceAll('"', '""');
|
||||
return needsQuote ? '"$escaped"' : escaped;
|
||||
}
|
||||
|
||||
Future<void> _exportCsvDenormalized() async {
|
||||
try {
|
||||
// frisch laden, um sicherzugehen
|
||||
final igelList = await repo.list();
|
||||
|
||||
// Header
|
||||
final sb = StringBuffer();
|
||||
final sep = ';';
|
||||
sb.writeln([
|
||||
// Igel-Stammdaten
|
||||
'igel_id',
|
||||
'igel_name',
|
||||
'igel_gender',
|
||||
'igel_feature',
|
||||
'igel_note',
|
||||
'igel_rescued_at',
|
||||
'igel_location',
|
||||
'igel_created_at',
|
||||
'igel_updated_at',
|
||||
// Messwert
|
||||
'messwert_id',
|
||||
'messwert_datum',
|
||||
'messwert_gewicht',
|
||||
'messwert_behandlung',
|
||||
'messwert_bemerkung',
|
||||
].join(sep));
|
||||
|
||||
for (final ig in igelList) {
|
||||
// Messwerte je Igel holen
|
||||
List<Messwert> mw = [];
|
||||
try {
|
||||
mw = await messRepo.list(ig.id);
|
||||
} catch (_) {
|
||||
mw = [];
|
||||
}
|
||||
|
||||
// wenn keine Messwerte → trotzdem 1 Zeile ausgeben
|
||||
if (mw.isEmpty) {
|
||||
sb.writeln([
|
||||
ig.id,
|
||||
_csvEscape(ig.name, delimiter: sep),
|
||||
_csvEscape(ig.gender, delimiter: sep),
|
||||
_csvEscape(ig.feature, delimiter: sep),
|
||||
_csvEscape(ig.note, delimiter: sep),
|
||||
_fmtDate(ig.rescuedAt),
|
||||
_csvEscape(ig.location, delimiter: sep),
|
||||
_fmtDateTime(ig.createdAt),
|
||||
_fmtDateTime(ig.updatedAt),
|
||||
|
||||
// leere Messwert-Spalten
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
].join(sep));
|
||||
continue;
|
||||
}
|
||||
|
||||
// pro Messwert eine Zeile
|
||||
for (final m in mw) {
|
||||
sb.writeln([
|
||||
ig.id,
|
||||
_csvEscape(ig.name, delimiter: sep),
|
||||
_csvEscape(ig.gender, delimiter: sep),
|
||||
_csvEscape(ig.feature, delimiter: sep),
|
||||
_csvEscape(ig.note, delimiter: sep),
|
||||
_fmtDate(ig.rescuedAt),
|
||||
_csvEscape(ig.location, delimiter: sep),
|
||||
_fmtDateTime(ig.createdAt),
|
||||
_fmtDateTime(ig.updatedAt),
|
||||
m.id,
|
||||
_fmtDateTime(m.datum),
|
||||
m.gewicht,
|
||||
_csvEscape(m.behandlung, delimiter: sep),
|
||||
_csvEscape(m.bemerkung, delimiter: sep),
|
||||
].join(sep));
|
||||
}
|
||||
}
|
||||
|
||||
final bytes =
|
||||
Uint8List.fromList(const Utf8Encoder().convert(sb.toString()));
|
||||
final name =
|
||||
'igel_export_denorm_${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}';
|
||||
|
||||
await FileSaver.instance.saveFile(
|
||||
name: name,
|
||||
bytes: bytes,
|
||||
ext: 'csv',
|
||||
mimeType: MimeType.csv,
|
||||
);
|
||||
|
||||
_snack('CSV exportiert: $name');
|
||||
} catch (e) {
|
||||
_snack('Export fehlgeschlagen: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) {
|
||||
final visible = _visibleItems;
|
||||
final df = DateFormat('dd.MM.yyyy'); // ⬅️ NEU
|
||||
final df = DateFormat('dd.MM.yyyy');
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Meine Igel'),
|
||||
actions: [
|
||||
// ⬇️ Export-Menü
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (v) async {
|
||||
if (v == 'export_csv_denorm') {
|
||||
await _exportCsvDenormalized();
|
||||
}
|
||||
},
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(
|
||||
value: 'export_csv_denorm',
|
||||
child: Text('Export CSV (denormalisiert)'),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Logout',
|
||||
icon: const Icon(Icons.logout),
|
||||
@@ -262,7 +401,7 @@ class _IgelListState extends ConsumerState<IgelListScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// ⬇️ NEU: „Gerettet am“
|
||||
// ⬇️ „Gerettet am“
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading:
|
||||
@@ -292,7 +431,7 @@ class _IgelListState extends ConsumerState<IgelListScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// ⬇️ NEU: „Ort / Fundstelle“
|
||||
// ⬇️ „Ort / Fundstelle“
|
||||
TextField(
|
||||
controller: locationC,
|
||||
decoration: const InputDecoration(
|
||||
@@ -323,11 +462,11 @@ class _IgelListState extends ConsumerState<IgelListScreen> {
|
||||
nameC.clear();
|
||||
noteC.clear();
|
||||
featureC.clear();
|
||||
locationC.clear(); // ⬅️ NEU
|
||||
locationC.clear(); // ⬅️
|
||||
setState(() {
|
||||
showForm = false;
|
||||
gender = null;
|
||||
rescuedAt = null; // ⬅️ NEU
|
||||
rescuedAt = null; // ⬅️
|
||||
});
|
||||
},
|
||||
child: const Text('Abbrechen'),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// lib/shared/csv.dart
|
||||
class CsvBuilder {
|
||||
final StringBuffer _buf = StringBuffer();
|
||||
final String delimiter;
|
||||
final String eol;
|
||||
|
||||
CsvBuilder({this.delimiter = ';', this.eol = '\n'});
|
||||
|
||||
static String _escape(String? v, String delimiter) {
|
||||
final s = v ?? '';
|
||||
final mustQuote =
|
||||
s.contains(delimiter) || s.contains('\n') || s.contains('"');
|
||||
var out = s.replaceAll('"', '""');
|
||||
return mustQuote ? '"$out"' : out;
|
||||
}
|
||||
|
||||
void addRow(Iterable<dynamic> cols) {
|
||||
_buf.writeln(
|
||||
cols.map((c) => _escape(c?.toString(), delimiter)).join(delimiter));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => _buf.toString();
|
||||
}
|
||||
@@ -6,14 +6,26 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) file_saver_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
||||
file_saver_plugin_register_with_registrar(file_saver_registrar);
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||
printing_plugin_register_with_registrar(printing_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_saver
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
printing
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
@@ -5,12 +5,18 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_saver
|
||||
import file_selector_macos
|
||||
import flutter_secure_storage_macos
|
||||
import path_provider_foundation
|
||||
import printing
|
||||
import share_plus
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
}
|
||||
|
||||
+177
-1
@@ -22,6 +22,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -38,6 +46,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: barcode
|
||||
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.9"
|
||||
bidi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bidi
|
||||
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -182,6 +206,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.7"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
exif:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -206,6 +246,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_saver
|
||||
sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.14"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -384,6 +432,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -560,8 +616,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_provider:
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@@ -608,6 +672,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
pdf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdf
|
||||
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.11.3"
|
||||
pdf_widget_wrapper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pdf_widget_wrapper
|
||||
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -632,6 +720,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
printing:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: printing
|
||||
sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.14.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -648,6 +752,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -656,6 +768,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.4"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -781,6 +909,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -837,6 +1005,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+6
-1
@@ -14,7 +14,12 @@ dependencies:
|
||||
intl: ^0.20.2
|
||||
http_parser: ^4.0.2
|
||||
exif: ^3.3.0
|
||||
|
||||
share_plus: ^10.0.0
|
||||
path_provider: ^2.1.4
|
||||
file_saver: ^0.2.12
|
||||
pdf: ^3.11.0
|
||||
printing: ^5.13.4
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.11
|
||||
json_serializable: ^6.9.0
|
||||
|
||||
@@ -6,12 +6,24 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSaverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSaverPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_saver
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
printing
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
||||
Reference in New Issue
Block a user