import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../data/export_import/seed_label_codec.dart'; import '../data/variety_repository.dart'; import '../di/injector.dart'; import '../i18n/strings.g.dart'; import '../services/label_sheet_service.dart'; import 'quantity_kind_l10n.dart'; /// Opens the print-labels sheet for [entries] (already gathered from the /// current selection): choose how many copies of each label (prefilled from the /// stored container count) and a label size, then save a PDF sheet of labels — /// each with a QR that carries the seed's data. Future showLabelPrintSheet( BuildContext context, List entries, ) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _LabelPrintSheet(entries: entries), ); } /// Upper bound on copies per label — a printed sheet, not a print run. const _maxCopies = 99; class _LabelPrintSheet extends StatefulWidget { const _LabelPrintSheet({required this.entries}); final List entries; @override State<_LabelPrintSheet> createState() => _LabelPrintSheetState(); } class _LabelPrintSheetState extends State<_LabelPrintSheet> { LabelSheetFormat _format = LabelSheetFormat.stickers; bool _saving = false; /// One editable copy-count per entry, prefilled from its suggested count. late final List _copies = [ for (final e in widget.entries) e.suggestedCopies.clamp(0, _maxCopies), ]; late final List _controllers = [ for (final n in _copies) TextEditingController(text: '$n'), ]; int get _total => _copies.fold(0, (sum, n) => sum + n); @override void dispose() { for (final c in _controllers) { c.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { final t = context.t; return SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( t.printLabels.title, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 4), Text( t.printLabels.count(n: _total), key: const Key('printLabels.total'), style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 8), // The per-seed copy list can be long; keep the format chooser and // the save button always visible by scrolling only this section. Flexible( child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.4, ), child: ListView.builder( shrinkWrap: true, itemCount: widget.entries.length, itemBuilder: (context, i) => _CopyRow( entry: widget.entries[i], controller: _controllers[i], onMinus: _copies[i] > 0 ? () => _bump(i, -1) : null, onPlus: _copies[i] < _maxCopies ? () => _bump(i, 1) : null, onChanged: (value) => _setCopies(i, value), ), ), ), ), const Divider(height: 20), RadioGroup( groupValue: _format, onChanged: _pick, child: Column( mainAxisSize: MainAxisSize.min, children: [ RadioListTile( key: const Key('printLabels.format.stickers'), value: LabelSheetFormat.stickers, contentPadding: EdgeInsets.zero, title: Text(t.printLabels.formatStickers), subtitle: Text(t.printLabels.formatStickersHint), ), RadioListTile( key: const Key('printLabels.format.cards'), value: LabelSheetFormat.cards, contentPadding: EdgeInsets.zero, title: Text(t.printLabels.formatCards), subtitle: Text(t.printLabels.formatCardsHint), ), ], ), ), const SizedBox(height: 12), Align( alignment: AlignmentDirectional.centerEnd, child: FilledButton.icon( key: const Key('printLabels.save'), onPressed: (_saving || _total == 0) ? null : _save, icon: const Icon(Icons.save_alt_outlined), label: Text(t.printLabels.save), ), ), ], ), ), ); } void _pick(LabelSheetFormat? value) { if (value != null) setState(() => _format = value); } /// Steps the count for entry [i] by [delta], syncing the text field. void _bump(int i, int delta) { final next = (_copies[i] + delta).clamp(0, _maxCopies); _controllers[i].text = '$next'; setState(() => _copies[i] = next); } /// Applies a typed value (empty → 0), clamped to the allowed range. void _setCopies(int i, String value) { final parsed = int.tryParse(value) ?? 0; setState(() => _copies[i] = parsed.clamp(0, _maxCopies)); } Future _save() async { final t = context.t; final messenger = ScaffoldMessenger.of(context); final navigator = Navigator.of(context); final rtl = Directionality.of(context) == TextDirection.rtl; setState(() => _saving = true); // Expand each entry into its chosen number of identical labels. final labels = [ for (var i = 0; i < widget.entries.length; i++) for (var c = 0; c < _copies[i]; c++) _toLabel(t, widget.entries[i]), ]; final saved = await getIt().saveLabels( labels: labels, format: _format, suggestedName: 'tanemaki-labels-${_today()}.pdf', rtl: rtl, ); navigator.pop(); messenger.showSnackBar( SnackBar( content: Text(saved ? t.printLabels.saved : t.printLabels.cancelled), ), ); } /// Builds one printable label from a [SeedLabelEntry]: the visible fields plus /// the QR payload (structured, so a future scan can read it). LabelSheetLabel _toLabel(Translations t, SeedLabelEntry e) { final origin = _origin(e); final details = [ if (e.harvestYear != null) '${e.harvestYear}', ?origin, if (e.quantity != null) quantityDisplay(t, e.quantity!), ]; return LabelSheetLabel( varietyLabel: e.varietyLabel, commonName: e.commonName, scientificName: e.scientificName, details: details.isEmpty ? null : details.join(' · '), qrData: SeedLabelCodec.encode( SeedLabelData( varietyLabel: e.varietyLabel, scientificName: e.scientificName, commonName: e.commonName, year: e.harvestYear, origin: origin, ), ), ); } String? _origin(SeedLabelEntry e) { final parts = [ if (e.originName != null && e.originName!.trim().isNotEmpty) e.originName!.trim(), if (e.originPlace != null && e.originPlace!.trim().isNotEmpty) e.originPlace!.trim(), ]; return parts.isEmpty ? null : parts.join(' · '); } String _today() => DateTime.now().toIso8601String().substring(0, 10); } /// One row of the copy chooser: the seed's name plus a −/number/+ stepper. The /// number is directly editable so "12 jars" is a quick type, not twelve taps. class _CopyRow extends StatelessWidget { const _CopyRow({ required this.entry, required this.controller, required this.onMinus, required this.onPlus, required this.onChanged, }); final SeedLabelEntry entry; final TextEditingController controller; final VoidCallback? onMinus; final VoidCallback? onPlus; final ValueChanged onChanged; @override Widget build(BuildContext context) { final theme = Theme.of(context); final subtitle = [ if (entry.commonName != null && entry.commonName!.trim().isNotEmpty) entry.commonName!, if (entry.harvestYear != null) '${entry.harvestYear}', ].join(' · '); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( entry.varietyLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge, ), if (subtitle.isNotEmpty) Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall, ), ], ), ), const SizedBox(width: 8), IconButton( key: Key('printLabels.copies.minus.${entry.varietyLabel}'), onPressed: onMinus, icon: const Icon(Icons.remove_circle_outline), visualDensity: VisualDensity.compact, ), SizedBox( width: 44, child: TextField( key: Key('printLabels.copies.field.${entry.varietyLabel}'), controller: controller, onChanged: onChanged, textAlign: TextAlign.center, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration( isDense: true, contentPadding: EdgeInsets.symmetric(vertical: 8), ), ), ), IconButton( key: Key('printLabels.copies.plus.${entry.varietyLabel}'), onPressed: onPlus, icon: const Icon(Icons.add_circle_outline), visualDensity: VisualDensity.compact, ), ], ), ); } }