tane/apps/app_seeds/lib/state/inventory_cubit.dart
vjrj 040f15a898 feat(block1): inventory walking skeleton + first quick-add slice
Stand up the Tanemaki monorepo and the first end-to-end vertical slice of
Block 1 (offline, encrypted inventory): add a seed → see it in a categorized,
searchable list → it persists → reopen and it's still there.

Architecture (state management like G1nkgo, adapted to Tane's reality):
- flutter_bloc (Cubit-first), but the encrypted Drift DB is the single source
  of truth; cubits stream from repositories (no hydrated_bloc/Hive, which would
  write plaintext at rest).
- get_it composition root; go_router; slang i18n (ES/EN, Weblate-friendly JSON).

Workspace & core:
- pub workspace: packages/commons_core (pure Dart) + apps/app_seeds (Flutter).
- commons_core primitives: UUIDv7 IdGen, Hybrid Logical Clock, Quantity value
  type (+ plant-aware QuantityKind), IdentityService root-seed stub.

Data & security:
- Drift schemaVersion=1 with all 10 Block-1 tables + common CRDT columns
  (HLC updated_at, last_author, tombstones); Movement append-only.
- SQLCipher via an injectable executor that refuses to open a plaintext DB;
  DB key + root seed in the OS keystore (separate secrets).
- Exported drift_schema_v1.json + migration scaffold.

Tests (near-TDD; nothing merges without tests):
- commons_core units (24), Drift migration test, "no plaintext at rest"
  security guard (runs where SQLCipher is present, skips otherwise),
  repository, widget, full quick-add flow, file-reopen persistence, and a
  no-hardcoded-strings i18n guard. GitLab CI: format + analyze + test + coverage.

Follow-on Block-1 stories (item detail/edit, species catalog, germination UI)
and all of Block 2 remain out of scope.
2026-07-07 15:16:14 +02:00

62 lines
1.7 KiB
Dart

import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../data/variety_repository.dart';
/// Inventory list state: all items from the DB plus the current search query.
/// [visibleItems] applies the query; grouping by category is done in the UI.
class InventoryState extends Equatable {
const InventoryState({
this.items = const [],
this.query = '',
this.loading = true,
});
final List<VarietyListItem> items;
final String query;
final bool loading;
List<VarietyListItem> get visibleItems {
if (query.trim().isEmpty) return items;
final q = query.toLowerCase();
return items.where((i) => i.label.toLowerCase().contains(q)).toList();
}
InventoryState copyWith({
List<VarietyListItem>? items,
String? query,
bool? loading,
}) {
return InventoryState(
items: items ?? this.items,
query: query ?? this.query,
loading: loading ?? this.loading,
);
}
@override
List<Object?> get props => [items, query, loading];
}
/// Subscribes to the repository's reactive inventory stream. The list updates
/// automatically after a quick-add — no manual refresh.
class InventoryCubit extends Cubit<InventoryState> {
InventoryCubit(this._repo) : super(const InventoryState()) {
_sub = _repo.watchInventory().listen(
(items) => emit(state.copyWith(items: items, loading: false)),
);
}
final VarietyRepository _repo;
late final StreamSubscription<List<VarietyListItem>> _sub;
void search(String query) => emit(state.copyWith(query: query));
@override
Future<void> close() async {
await _sub.cancel();
return super.close();
}
}