feat(inventory): local sharing — per-lot share terms, filter and printable catalog
The local half of the original Fase 2 (no network, no Offer entity yet): - Lot sheet gains a 'do you share it?' choice (gift/swap first, sale last, never the default); declaring plenty of abundance reveals it with a nudge. - Lot lines and list tiles badge shared batches; an 'I share' filter and a printable PDF catalog (paper bridge for fairs) round out the view. - Catalog PDF embeds DejaVu (Latin-ext/Cyrillic/Greek); fonts are a per-locale resource like the OCR packs.
This commit is contained in:
parent
e3ec855630
commit
ba87bf2719
20 changed files with 982 additions and 36 deletions
132
apps/app_seeds/lib/services/share_catalog_service.dart
Normal file
132
apps/app_seeds/lib/services/share_catalog_service.dart
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
|
||||
import 'file_service.dart';
|
||||
|
||||
/// One printable line of the "what I share" catalog. All strings arrive
|
||||
/// already localized — this service knows nothing about i18n or the domain.
|
||||
class ShareCatalogRow {
|
||||
const ShareCatalogRow({
|
||||
required this.name,
|
||||
required this.mode,
|
||||
this.scientificName,
|
||||
this.details,
|
||||
});
|
||||
|
||||
/// The variety's label, as its keeper writes it.
|
||||
final String name;
|
||||
|
||||
/// Scientific name of the linked species, if any (rendered in italics).
|
||||
final String? scientificName;
|
||||
|
||||
/// The batch facts line ("Seeds · 2024 · 2 cobs"), if any.
|
||||
final String? details;
|
||||
|
||||
/// On what terms it is offered ("To give away" / "To swap" / "For sale").
|
||||
final String mode;
|
||||
}
|
||||
|
||||
/// Renders the "what I share" catalog as a PDF and hands it to the user via
|
||||
/// the save dialog — a paper bridge for fairs and meet-ups, no network needed.
|
||||
class ShareCatalogService {
|
||||
ShareCatalogService({required FileService files}) : _files = files;
|
||||
|
||||
final FileService _files;
|
||||
|
||||
/// Builds the catalog PDF. Pure of platform channels apart from loading the
|
||||
/// bundled font, so it runs (and is tested) on the host.
|
||||
///
|
||||
/// The embedded DejaVu face covers Latin (extended), Cyrillic and Greek.
|
||||
/// Like the OCR traineddata, the PDF font is a per-locale resource: wider
|
||||
/// scripts (Arabic, CJK) ship alongside their locales, not hardcoded here.
|
||||
Future<Uint8List> buildPdf({
|
||||
required String title,
|
||||
required String date,
|
||||
required List<ShareCatalogRow> rows,
|
||||
}) async {
|
||||
final base = pw.Font.ttf(
|
||||
await rootBundle.load('assets/fonts/DejaVuSans.ttf'),
|
||||
);
|
||||
final bold = pw.Font.ttf(
|
||||
await rootBundle.load('assets/fonts/DejaVuSans-Bold.ttf'),
|
||||
);
|
||||
final doc = pw.Document(
|
||||
theme: pw.ThemeData.withFont(base: base, bold: bold, italic: base),
|
||||
);
|
||||
doc.addPage(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
build: (context) => [
|
||||
pw.Text(title, style: pw.TextStyle(font: bold, fontSize: 20)),
|
||||
pw.SizedBox(height: 2),
|
||||
pw.Text(
|
||||
date,
|
||||
style: const pw.TextStyle(fontSize: 10, color: PdfColors.grey700),
|
||||
),
|
||||
pw.SizedBox(height: 12),
|
||||
for (final row in rows)
|
||||
pw.Container(
|
||||
padding: const pw.EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(
|
||||
bottom: pw.BorderSide(color: PdfColors.grey300, width: 0.5),
|
||||
),
|
||||
),
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
row.name,
|
||||
style: pw.TextStyle(font: bold, fontSize: 12),
|
||||
),
|
||||
if (row.scientificName != null)
|
||||
pw.Text(
|
||||
row.scientificName!,
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10,
|
||||
fontStyle: pw.FontStyle.italic,
|
||||
color: PdfColors.grey700,
|
||||
),
|
||||
),
|
||||
if (row.details != null)
|
||||
pw.Text(
|
||||
row.details!,
|
||||
style: const pw.TextStyle(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 8),
|
||||
pw.Text(row.mode, style: const pw.TextStyle(fontSize: 10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
/// Builds the PDF and asks the user where to keep it. Returns true when
|
||||
/// saved, false when they cancelled the dialog.
|
||||
Future<bool> saveCatalog({
|
||||
required String title,
|
||||
required String date,
|
||||
required String suggestedName,
|
||||
required List<ShareCatalogRow> rows,
|
||||
}) async {
|
||||
final bytes = await buildPdf(title: title, date: date, rows: rows);
|
||||
final path = await _files.saveFile(
|
||||
suggestedName: suggestedName,
|
||||
bytes: bytes,
|
||||
);
|
||||
return path != null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue