feat(import): implement recipe import functionality with file and URL support

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Nils-Johan Gynther
2026-04-22 21:31:25 +02:00
parent 8ebf119d39
commit 81117fbcb7
11 changed files with 617 additions and 13 deletions
@@ -7,12 +7,46 @@ import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
import '../domain/inventory_item.dart';
/// Returns a sensible step size for quick swipe-adjustments given [unit].
///
/// Examples: "g" → 50, "kg" → 0.1, "ml" → 50, "l" / "dl" → 0.1,
/// everything else (pieces, packages, etc.) → 1.
double _stepForUnit(String unit) {
switch (unit.trim().toLowerCase()) {
case 'g':
case 'gram':
return 50;
case 'kg':
case 'kilo':
case 'kilogram':
return 0.1;
case 'ml':
return 50;
case 'cl':
return 1;
case 'dl':
return 0.5;
case 'l':
case 'liter':
case 'litre':
return 0.1;
default:
return 1;
}
}
/// Formats a step value for display: whole numbers without decimal,
/// fractions with one decimal.
String _fmtStep(double step) =>
step == step.roundToDouble() ? step.toStringAsFixed(0) : step.toStringAsFixed(1);
/// A [ListTile] wrapped in a swipe-to-adjust widget.
///
/// • Swipe **right** (+) → adds 1 unit to [item.quantity] via PATCH.
/// • Swipe **left** () → consumes 1 unit via the consume endpoint,
/// preserving consumption history.
/// • Swipe **right** (+) → adds one unit-appropriate step via PATCH.
/// • Swipe **left** () → consumes one unit-appropriate step via the
/// consume endpoint, preserving consumption history.
///
/// The step size is determined by the item's unit (e.g. 50 g, 0.1 kg, 1 st).
/// A small swipe-hint icon is shown at the start of the subtitle so users
/// know the gesture is available without any extra instruction.
class SwipeableInventoryTile extends ConsumerStatefulWidget {
@@ -58,6 +92,7 @@ class _SwipeableInventoryTileState
void _snapBack() => setState(() => _drag = 0);
Future<void> _adjust(int direction) async {
final step = _stepForUnit(widget.item.unit);
setState(() => _acting = true);
try {
final token = await ref.read(authStateProvider.future);
@@ -67,16 +102,17 @@ class _SwipeableInventoryTileState
// Increase: direct PATCH with new quantity.
await repo.updateInventoryItem(
widget.item.id,
{'quantity': widget.item.quantity + 1},
{'quantity': widget.item.quantity + step},
token: token,
);
} else {
// Decrease: use consume endpoint so history is preserved.
// Guard against going below zero.
if (widget.item.quantity <= 0) return;
final consume = step > widget.item.quantity ? widget.item.quantity : step;
await repo.consumeInventoryItem(
widget.item.id,
amountUsed: 1,
amountUsed: consume,
token: token,
);
}
@@ -103,6 +139,10 @@ class _SwipeableInventoryTileState
final rightProgress = (_drag / _threshold).clamp(0.0, 1.0);
final leftProgress = (-_drag / _threshold).clamp(0.0, 1.0);
final step = _stepForUnit(widget.item.unit);
final stepLabel = _fmtStep(step);
final unit = widget.item.unit;
return GestureDetector(
onHorizontalDragUpdate: _onUpdate,
onHorizontalDragEnd: _onEnd,
@@ -131,7 +171,7 @@ class _SwipeableInventoryTileState
),
const SizedBox(height: 2),
Text(
'+1',
'+$stepLabel $unit',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
@@ -167,7 +207,7 @@ class _SwipeableInventoryTileState
),
const SizedBox(height: 2),
Text(
'1',
'$stepLabel $unit',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onTertiaryContainer,
fontWeight: FontWeight.bold,