Device compatibility (regression fix):
- The CAMERA permission (from zxing_barcode_scanner / image_picker) implicitly
required android.hardware.camera, excluding camera-less devices — Play showed
Automotive -96%, Chromebook -86%, TV -25%. Declare camera & location
uses-feature required="false" to keep those devices supported.
- Detect a real camera at runtime (PackageManager.FEATURE_CAMERA_ANY via the
existing MethodChannel), cache it at bootstrap, and hide the QR scan button and
the camera photo-source option when absent, so no broken actions are offered.
Play "app optimization" recommendations:
- Enable R8 (isMinifyEnabled + isShrinkResources) with keep rules for the
OCR (tesseract4android), SQLCipher and notifications JNI/reflection code.
- Bitmap downscaling: cacheWidth/cacheHeight (and ResizeImage for avatars) on
list thumbnails and avatars so photos decode to on-screen size, not full res.
- Edge-to-edge: opt in with transparent system bars in main().
Release flow:
- Tagged builds now publish to the production track at 100% (was internal);
add a manual deploy_internal lane as a QA safety net.
- Document country/region availability as a Play Console setting (not in-repo).
Bound market memory and let large areas page instead of loading everything:
- DiscoveryQuery gains an until cursor; OfferTransport gains discoverPage()
(one-shot, EOSE-bounded, newest-first) alongside the existing live discover()
stream, which now accepts since so it only carries NEW offers going forward.
- NostrOfferTransport.discoverPage sorts by created_at desc and derives the
next cursor from the oldest event seen (not the oldest type-matched one, so
filtering never skips a page).
- OffersCubit: discover() fetches the first page + opens a since=now live sub;
loadNextPage() pages further back; the in-memory list is capped at 400
offers (each can carry a ~40KB inline photo thumbnail, so unbounded growth in
a busy area was a real OOM risk).
- market_screen: infinite scroll via a scroll-position trigger + footer
spinner, instead of a single unbounded ListView.
- Added discoverPage unit tests (commons_core) and cubit pagination tests
(first page/cursor, accumulation, cap, cross-page dedup).
Scale the local inventory to 10k+ varieties:
- Render the list with ListView.builder over a flattened header/item model
instead of building every tile upfront.
- Store a small regenerable JPEG thumbnail per photo (schema v14) and use it
for the 48px list avatar; full bytes stay for offer image hosting. Existing
photos are backfilled lazily at startup. Thumbnail is local-only (excluded
from CRDT sync and backups by the JSON codec).
- Add indexes on varieties(is_deleted,is_draft), attachments(parent_type,
parent_id,kind), lots(variety_id) via @TableIndex.
- Debounce watchInventoryView (~250ms) so a burst of table writes triggers one
reload, not seven.
- cacheWidth/cacheHeight on the list avatar decode.
- Scale test raised 3k -> 10k; migration test v13 -> v14.
One passive line in the batch story after a 'sown today' tap — 'this
species crosses, grow it ~400 m from others' — sourced from the bundled
seed-saving guidance (family default + species override). Selfers get
nothing: the trap is recording isolation religiously for beans where it
never mattered. No input fields, no schema change.
Closes the physical↔digital loop the labels opened: the QR on a printed
envelope (tane://seed, already encoded by seed_label_codec) can now be
scanned from the inventory bar. A known label opens its variety; an
unknown one asks before adding anything, then creates the variety (species
linked by exact scientific name when bundled) with a lot carrying the
label's year and origin. Scanner is pure ZXing (zxing_barcode_scanner,
MIT) — no ML Kit, no Play Services, F-Droid-safe, the stack Ğ1nkgo ships.
Mobile-only; other platforms don't show the button (OCR precedent).
The post-scan flow is a plain function (handleScannedPayload) so it's
widget-tested without a camera.
The note growers most wish they had two seasons later — did it do well,
was it worth keeping — never had a home. New append-only GardenOutcomes
table (lotId, season year, three-face rating, free note); the question
appears ONCE, inline, right after recording a harvest in the batch story,
and is skippable without a trace. The answer shows as one more story
line. Rides backups/sync like every mutable table (JSON codec, LWW
import, HLC clock absorb). Migration v12→v13 guarded + verified from
every historical version.
The Movement log (history + provenance DAG) was recorded but invisible.
A new history sheet on every lot tile narrates the batch: movements,
germination tests, storage checks and how it entered the collection
(origin attached), most-recent first. Two one-tap chips record 'sown
today' / 'harvested today' — the first UI door to the sown/harvested
movement types. No schema change; one-shot reads only (no live
subscriptions in a transient sheet).
Save the current market search (query + facets) and get notified when a
matching offer appears in your zone while the app is open, with a catch-up
scan on start. Adds a save affordance in the search bar, a saved-searches
list (apply/delete) with a per-search new-match badge, an AppBar entry
badged with the unseen total, and notification-tap routing to the list.
Alerts evaluate against the user's current area, dedup via seen keys, and
skip own/blocked/hidden/non-active offers. en+es strings.
The Cancel / Save-and-add-another / Save actions were a fixed Row that
could not shrink, so on narrow phones and long locales (es/pt/de/fr) the
buttons overflowed the right edge (the clipped 'Guardar'). Replace it with
an OverflowBar that wraps onto stacked, end-aligned lines when they don't
fit; move the added-count onto its own line above.
Guard it in small_screen_overflow_test.dart across the long locales.
Beta-tester feedback triage. The data model already had LotType.seedling as
first class; the gaps were copy and UX.
- Seedlings/plantones: broaden landing hero (EN/ES), in-app legal notice, and
all legal texts (site + docs masters) from seeds-only to "seeds and
seedlings", with a live-plant phytosanitary/transport caveat. Seeds stay the
hero — targeted, not a blanket rename.
- Backups: settings line now states the 7-day cadence explicitly; cadence lives
in AutoBackupService.backupInterval as the single source for text and schedule.
- Community servers: replace the manual wss:// text box with a checklist of the
known servers (defaults visible/toggleable) plus an "add server" affordance
with basic validation. No jargon in the UI.
i18n across en/es/pt/fr/de/ast (ja falls back). Tests updated + a new
server-picker widget test.
The propose sheet used a free-text seed name, unlinked from the
inventory. Now the seed is picked from your catalogued varieties so the
Plantaré is always tied to a real Variety (tap-through to its detail, and
it shows in that seed's commitments):
- Seed field is an Autocomplete over the inventory; on send the typed name
resolves to the matching Variety, or creates a quick one if it's new —
so varietyId is never null. Sheet takes the VarietyRepository; chat
passes it.
- New repo one-shot `varietyLabels()` (id + label, no joins) feeds the
picker. Deliberately a Future, not `watchInventory()` — a transient
sheet must not hold a live Drift subscription (that hung the widget test
~36 min). Tests use it too and now run in ~2s with bounded pumps + a
tall surface, no pumpAndSettle.
Tests: a new name creates & links a Variety; a matching name links the
existing one without duplicating. ui/services/data plantare suites green.
The viral loop, made usable end-to-end:
- Propose sheet (plantare_propose_sheet.dart): from a chat with a peer
(their key already in hand), pick your side, name the seed, choose what
comes back (similar · non-GMO organic / work hours / other) and an
optional return-by, then sign and send. Reached from the chat overflow
menu; confirms "waiting for them to sign".
- Plantares screen: incoming proposals show Accept & sign / Decline right
on the tile; every bilateral row wears a handshake badge — awaiting
signature, signed by both, or declined. Accept falls back to a gentle
"offline" nudge if the proposal isn't in hand yet.
- i18n: full en + es for the bilateral flow; other locales fall back to
base per slang config.
Wire-up only touches human copy (no jargon) and stays behind the
optional PlantareService, so inventory-only builds are unaffected.
Tests: propose sheet drives a real sign+send; the ledger renders the
signed/awaiting badges. Fake SocialSession in your_people test gains the
new transport getter. Widget tests targeted + timed. All green.
Bundle Noto Sans Arabic + Noto Sans JP (SIL OFL 1.1) as per-glyph fallbacks
for both the UI text theme and every generated PDF, so RTL (Arabic) and CJK
render on all platforms — including desktop, where the system font may lack
them — instead of tofu. A shared pdf_fonts.dart centralizes the PDF theme and
the three label/catalog/recovery services reuse it.
Add Japanese (ja) as the CJK reference locale — fittingly, the app's own name
is Japanese (種, tane, 'seed'). Core strings translated; the rest fall back to
English via slang. Wired into supportedLocales (automatic) and the settings
picker.
Fix three physical Alignment.centerLeft -> AlignmentDirectional.centerStart in
variety_detail_screen, and make month-abbreviation and avatar-initial
truncation grapheme-safe (.characters) so CJK text is never cut mid-character.
Tests: CJK/Arabic PDF glyph render, ja locale resolution + English fallback.
The chat-bubble mirroring flagged by the audit was verified correct (already
respects Directionality) and left unchanged.
The flat list of 6 languages plus 'system' dominated Settings. Replace it
with a single tile showing the current language, opening a bottom-sheet
picker (all languages + 'System language'). Resolve LocaleStore defensively
so the screen renders in DI-less widget tests.
Full fr/de translations of the app (625 keys each), wired into the
Settings language picker and the small-screen overflow test. Material
ships fr/de localizations, so chrome resolves natively.
The Plantaré screen stored madeOn/dueBy/varietyId but showed none of them,
so it felt half-built. Surface what's already there (no schema change):
- watchPlantareViews() joins each commitment with its variety label
- tile shows the made-on date, a "Return by {date}" line (amber + "overdue"
when an open promise is past due, via new seedWarning colour), the variety
name, and taps through to /variety/:id
- add sheet gains an optional return-by date picker (createPlantare already
took dueBy)
- variety detail shows a read-only commitments section (hidden when empty,
no add button — honours the single hand-over door)
- i18n en/es/pt/ast; tests for the join, dueBy round-trip, tile, and section
Also specs the deferred two-sided record in docs/design/plantare-bilateral.md
(Nostr pubkey counterparty, PlantareTransport propose->accept->counter-sign,
deferred key/signature/movement columns, provenance DAG, reputation anchor),
with a faithful transcription of the paper Plantaré v0.4 (BAH-Semillero 2009,
CC-BY-SA). Cross-linked from data-model §2.7 and sharing-model §6.
On short screens (e.g. landscape) the fixed-height trust and rating
strips plus the composer overflowed the body once the keyboard shrank
it. Wrap the two secondary strips in HideWhenKeyboardOpen so they yield
their vertical space while typing and reappear when the keyboard closes.
Adds a Favorites feature: a heart on the offer detail saves another
person's listing to an encrypted, per-identity SavedOffersStore
(keystore JSON snapshot, no plaintext at rest). A new /favorites screen
(wired from the drawer) lists saved offers offline-first and, when a
relay is reachable, flags ones that are gone as "no longer available".
i18n en/es/pt/ast; store + screen + detail-heart tests.
Drop the DiceBear generated avatar (and dicebear_core/dicebear_styles/
flutter_svg): the default is again a Material coloured-initial disc from
the pubkey. Picking a photo now goes through a square crop step
(crop_your_image — pure Flutter, all platforms incl. desktop, Apache-2.0,
only pulls the image package we already had) before the 24 KB thumbnail.
The picker now offers only a photo; with none, the generated DiceBear
avatar is used. Removes the seedks-ttf illustration row (and the now-dead
avatarGlyphNames/avatarGlyphToken). tane:seed:<glyph> values set before
this still render (avatarGlyphChar kept) for backward compatibility.
Replace the flutter_boring_avatars fallback with a friendly DiceBear
"thumbs" face, generated offline (dicebear_core) and deterministically
from the pubkey, rendered as SVG (flutter_svg). The thumbs style is
CC0 1.0 (public domain) — no attribution, AGPL-clean — unlike Multiavatar
whose custom licence restricts use. Style parsed once and per-pubkey SVG
memoized; the CC0 <metadata> block is stripped to keep flutter_svg quiet.
People without a photo or seed illustration now get a deterministic
pattern avatar drawn from their pubkey (flutter_boring_avatars, MIT),
in an earthy seed-tone palette, replacing the coloured-initial disc.
Same pubkey renders the same picture on every device with nothing shared.
Public name is now Tane (種, "seed"); Tanemaki (種まき, "to scatter seeds")
is kept only as etymology in the About screen and onboarding.
App not yet distributed, so format tokens rename cleanly with no migration:
backup extension .tanemaki→.tane, QR scheme tanemaki://→tane://, file
prefixes tane-*. Website link and species-catalog User-Agent →
tane.comunes.org. Android label capitalised to "Tane".
Identity/crypto constants already use org.comunes.tane (derivation, HKDF
backup, sync namespace, TANE1 recovery tag) — left untouched, so keys,
backups and sync are unaffected. Tests updated; analyze + full suite green.
Pinning it beside the horizontal scroll squeezed the chips into a narrow
strip and clipped them under the edge fade (e.g. 'Comparto' cut off by
the scroll arrow). Now the chips get the full width and 'clear filters'
sits on its own line below, shown only when a filter is active.
Six reported UX issues:
- Inventory 'clear filters' is now pinned outside the horizontal scroll,
always visible — you no longer have to scroll past every chip to find it.
- Empty result now reads 'No seeds match your filters' (not 'no seeds
yet') when a SEARCH or the 'this month' filter hid everything — the
'filtered' flag was missing query + sowThisMonthOnly. Guarded by a test.
- Profile avatar saves + publishes the instant you pick it, so it no
longer looks saved when it wasn't (the Save button sits below the fold).
- Market offer detail shows the author's real avatar (photo/illustration)
instead of a generic person icon — resolves + caches their kind:0 picture.
- 'Wishlist' renamed to Favourites/Favoritos (en/es/pt/ast).
- The drawer 'following' item now opens the web-of-trust 'your people'
screen when the social layer is on (was a dead item).
analyze clean; inventory/market/home + overflow suites green.
Follow-up to the avatar feature: peers' published photos/illustrations
now actually render where people appear.
- CachedAvatar: a PeerAvatar that resolves the person's picture from the
ProfileCache (FutureBuilder), falling back to the coloured-initial disc.
For list/row sites — one avatar each.
- Chat bubbles: the peer's and my own avatar are resolved once into state
(_peerPicture/_selfPicture — from the cache / ProfileStore, freshened
from the peer's published profile) and threaded to each bubble, so many
bubbles of two people don't each hit the cache.
- Chat list + your-people rows: swapped the static person-icon disc for
CachedAvatar, so photos/illustrations show there too.
- Tests: CachedAvatar render modes (cached illustration / initial / no
cache). analyze clean; chat + your-people suites green.
Market offers show no author avatar today (would be a new element, not a
swap) — left out of scope.
Profiles had only a coloured-initial disc. Now each person can set an
avatar — a real photo OR one of our own seed illustrations (so
pseudonymity stays the default; no photo required).
- ui/avatar.dart: the one-string value scheme carried in the kind:0
'picture' — a 'data:' photo thumbnail, a 'tane:seed:<glyph>' token, or
empty. The Nostr ProfileTransport already published 'picture'.
- Photos ride inline as a tiny thumbnail (reuses offerThumbnailDataUri,
24 KB cap) — no media server, like offer photos.
- avatar_edit.dart: pick/take a photo, choose a seed illustration, or
remove; ProfileStore stores it; ProfileScreen shows a big editable
avatar and publishes it.
- PeerAvatar renders photo / illustration / initial fallback.
- ProfileCache gains picture/setPicture; the inbox caches peers' avatars
alongside their names.
- i18n avatar block (en/es/pt/ast). Tests: value scheme + PeerAvatar
render modes, store + cache round-trips.
Follow-up: render peers' avatars in chat/market/your-people (thread the
cached picture into PeerAvatar at each call site).
From the owner's TODO.org review of the shipped features:
- Crop calendar now says the months are YOUR notes: a hint line in the
editor (cropCalendar.editorHint) and a caption under the 'this month'
screen (calendar.selfNote) — it read as if the app knew when to sow.
- Dropped the 'how much I have' (abundance) input + display: it competed
with the lot quantity as a second 'how much'. Removed from the lot sheet
and the lot summary; the DB column + backup/CSV/catalog plumbing stay
dormant (no destructive migration). offer_status carries 'do I share'.
- Seed-saving guidance moved into a collapsed expander (detail.seedSaving)
— reference facts about the species shouldn't sit open among the
grower's own records; opt-in depth, per progressive disclosure.
- Condition checks already live behind the collapsed 'advanced' expander
(seed lots only) — left as the discreet power-user surface.
Tests: dropped the obsolete abundance→share nudge test; the seed-saving
section test now expands before asserting. analyze clean; suites green.
Ego-centric trust replaces the global Duniter WoT, plus Wallapop-style
ratings v1 (kind 30778). Conflicts resolved: pubspec assets (kept
seed_saving, dropped trust referents), chat_screen (avatars/no-links
from main + rating strip and simplified TrustCubit from the branch),
strings.g.dart regenerated from merged sources.
- SocialSession grows a ratings transport (fifth interface on the one
shared channel).
- PeerRatingCubit: count, locale-aware average, and circleCount — how
many ratings come from your own circle (reuses the ego-centric trust
rule), the signal that makes strangers' review-stuffing irrelevant.
- Chat: slim rating strip under the trust banner; you can rate only
someone you've talked with (soft anchor until the signed bilateral
exchange form lands). Sheet with 5 stars + optional comment,
pre-filled for editing, with a take-back action.
- Offer detail: author's stars under their name, with 'N from people
you know' when your circle rated them; nothing shown when unrated.
- i18n ratings.* (en/es/pt/ast); tests for cubit and sheet.
The global membership rule (curated bootstrap referents + sigQty/stepMax
parameters) solved sybil-proof identity for a UBI — a problem this app
doesn't have — and its screen leaked graph jargon (npub roots, parameter
steppers). Trust is now computed from the user's own position only:
you vouch / vouched by people you know (distance <=2) / vouched by N.
- TrustCubit: drop networkMember tier, referents and params; keep the
circle rule and the 365-day vouch expiry (renewable, self-pruning).
- Delete TrustReferents, WotSettings, TrustNetworkScreen and the bundled
referents asset; unwire injector/bootstrap/app/chat.
- New 'Your people' screen (/your-people, from the profile): who you
vouch for (revocable) and who vouches for you, names via ProfileCache.
- i18n: wot.* removed, yourPeople.* added (en/es/pt/ast); trust.member
removed. Kind 30777 events on relays stay fully compatible — only the
interpretation changes.
The tick overlapped the leading icon on selected chips, reading as a
confusing blob. The solid fill already signals 'on', so hide the
checkmark. Also make the selected label reliably white via an explicit
per-chip style: the global ChipTheme's secondaryLabelStyle was not
applied to FilterChips, leaving dark text on green. Plain attribute/
market chips now share a PlainFilterChip widget; swatch and month chips
just drop their tick.
'Seeds changed hands' is now the single entry point on the variety
detail (and per lot row): direction (gave/received), one-tap 'all of it'
(default — the whole-plant/shrub case), optional partial quantity,
counterparty, and reveal-on-tap money and return-promise sections that
spawn the Sale/Plantare rows through recordHandover in one save.
Zero-lot varieties still work (ledger rows only); several lots get a
chip picker. The Sales/Plantares screens keep their own add buttons for
off-app records — the sheets stay, demoted to secondary.
i18n handover block in en/es/pt/ast; widget tests for the flow.
The data model always meant sale offers to carry an informational price
(data-model §2.8, sharing-model §4.4); with offer state collapsed onto
the Lot, the price lives there. Null amount on a sell lot = 'to be
agreed' — the NIP-99 event simply omits the price tag.
- schema v11: Lots.price_amount/price_currency, guarded migration,
schema dump + generated schema_v11 for the round-trip test
- ShareableLot/VarietyLot carry the price (sell-only in shareableLots);
addLot/updateLot persist it and clear it when the lot leaves sale
- OffersCubit.publishLots forwards it (outbox flush picks it up free)
- lot sheet: price + currency inputs revealed only on 'For sale', with
the €/Ğ1/hours quick-picks extracted to a shared CurrencyQuickPicks
widget (reused by the sale sheet)
- backup JSON round-trips the new columns
- i18n share.price/priceHint in en/es/pt/ast
Record where the guidance comes from (copyright/attribution) and make
clear it is general, not local gospel — kept light:
- seed_saving.json gains a top-level sources list (Seed to Seed / Seed
Savers Exchange / Organic Seed Alliance); parsed into
SeedSavingData.sources, exposed via SeedSavingCatalog.sources.
- The detail section shows a small muted footer: an italic 'advisory —
adapt to your climate' line plus a 'Source: …' credit.
- i18n seedSaving.advisory + sourcePrefix (en/es/pt/ast).
- Tests: asset carries sources; the section renders advisory + credit.
The selected state used a pale tonal container (seedPrimaryContainer)
that sat almost on top of the pale canvas, so 'on' barely read. Selected
chips now fill solid — seedGreen for the plain attribute/market chips and
the swatch's own ink for family/form chips — with white label, icon and
checkmark. Idle chips stay light with dark ink, so the on/off jump is
unmistakable. White-on-ink AA is locked by a new contrast test.
The fade-to-transparent was invisible: the row sits on the same pale
canvas, so masking the edge to transparent just revealed more of the
same colour (and often landed on an inter-chip gap). Replace it with a
surface-coloured scrim the chips slide under, topped by an outward
chevron, shown only on an edge with more content. RTL-mirrored.
A bundled, curated knowledge layer — the differentiator of a serious
seed app. For a variety's crop it shows how the plant reproduces and what
it takes to keep the variety true: life cycle, pollination (self/cross +
insect/wind), isolation distance, how many plants to save from, dry/wet
processing, a difficulty pill, and a short tip — all in human words.
- assets/catalog/seed_saving.json: curated public-domain data (Seed to
Seed / Seed Savers), keyed by botanical FAMILY (defaults) with
per-species/genus OVERRIDES that win — so e.g. Vicia faba is correctly
insect-cross-pollinated despite the selfing Fabaceae default, and maize
is wind-pollinated with a large population. Notes are locale-keyed
(es/en), extensible to any language — a starter set, not a ceiling.
- domain/seed_saving.dart (pure): enums + SeedSavingGuide (merge, note
fallback) + SeedSavingData.guideFor (species → genus → family).
- data/seed_saving_catalog.dart: loads the asset once into an in-memory
singleton (no DB table, no migration); injector loads it at startup.
- VarietyDetail gains (from the linked species — the reliable
key, vs the editable category); detail screen shows a _SeedSavingView
when a guide resolves.
- i18n seedSaving block (labels + enum values + params) in en/es/pt/ast.
- Tests: domain lookup/merge, the real asset's key corrections, and the
detail section shows/hides. 314 suite tests green; analyze clean.
Chips clipped at the viewport edge with no cue that more filters exist.
New reusable EdgeFade widget (ShaderMask + dstIn) fades an edge only
while content remains beyond it, driven by scroll metrics; RTL mirrors
via Directionality. Applied to the inventory filter bar, market filter
bar and calendar month strip.
Local-first chat between people whose web of trust is still forming is a
natural phishing/scam vector, so messages may not contain URLs. A
conservative `containsUrl` rule (explicit scheme, www., or a common-TLD
domain — but not "3.5kg" or "12.30") gates it: the composer warns and
keeps the text for editing, and MessagesCubit.send drops any URL as a
backstop. Incoming text was already non-tappable (plain selectable text).