chore(release): beta packaging — signing, changelog, store metadata, scale test
- Android release signing reads a gitignored key.properties, falling back to debug keys so contributors can still build --release. - CHANGELOG.md (human, newest-first) + Fastlane/F-Droid metadata (en/es) with descriptions derived from docs/intro.md. - docs/release.md: the by-hand build + manual smoke checklist (no CI yet). - inventory_scale_test guards the list query against N+1 at 3000 varieties.
This commit is contained in:
parent
5d2b41a110
commit
4acf227e30
12 changed files with 260 additions and 4 deletions
32
CHANGELOG.md
Normal file
32
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Tanemaki are recorded here. Human-readable, newest
|
||||||
|
first. Dates are ISO (YYYY-MM-DD).
|
||||||
|
|
||||||
|
## [Unreleased] — Block 1 beta prep
|
||||||
|
|
||||||
|
Block 1 is the offline, encrypted seed inventory — useful with zero network.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Local sharing.** Mark a batch as to give away, to swap or for sale (gift
|
||||||
|
and swap first, sale never the default). A "what I share" filter and a
|
||||||
|
printable catalog (a paper bridge for fairs) surface it.
|
||||||
|
- **Sealed backups & recovery.** "Save a backup" writes a single encrypted
|
||||||
|
file; restoring on your own device is silent, and a copy from another
|
||||||
|
identity opens with the printed recovery code — which also brings your
|
||||||
|
identity back. A printable recovery sheet (QR + code) lives in Settings.
|
||||||
|
- **Full editing.** Lots and vernacular names can be edited and removed, not
|
||||||
|
just added.
|
||||||
|
- **Portuguese** interface, alongside Spanish and English; right-to-left
|
||||||
|
layouts verified.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- "How much I have" is answered by the coarse abundance scale (plenty to
|
||||||
|
share … running low), which now nudges toward sharing when you have plenty.
|
||||||
|
A precise gram/seed-count field is deferred until network offers need it.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Everything is local-first and encrypted at rest (SQLCipher). No account, no
|
||||||
|
server, no tracking.
|
||||||
|
- The social layer (network offers, messaging, web of trust) is Block 2 and
|
||||||
|
intentionally not part of this release.
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
|
|
@ -5,6 +8,17 @@ plugins {
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release signing is opt-in: drop a `key.properties` (gitignored) next to this
|
||||||
|
// module's android/ dir to sign release builds with your own keystore. Without
|
||||||
|
// it, release builds fall back to debug keys so contributors can still run
|
||||||
|
// `flutter run --release`. See docs/release.md.
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
val hasReleaseKeystore = keystorePropertiesFile.exists()
|
||||||
|
if (hasReleaseKeystore) {
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.comunes.tane"
|
namespace = "org.comunes.tane"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
|
@ -20,7 +34,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId = "org.comunes.tane"
|
applicationId = "org.comunes.tane"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
|
@ -30,11 +43,27 @@ android {
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
if (hasReleaseKeystore) {
|
||||||
|
create("release") {
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String
|
||||||
|
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// Sign with the real release keystore when key.properties is present,
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// otherwise fall back to debug keys so `flutter run --release` works
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
// for contributors without the signing material.
|
||||||
|
signingConfig = if (hasReleaseKeystore) {
|
||||||
|
signingConfigs.getByName("release")
|
||||||
|
} else {
|
||||||
|
signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
First public beta of Block 1: the offline, encrypted seed inventory.
|
||||||
|
|
||||||
|
- Local sharing (give away / swap / sell) with a printable catalog.
|
||||||
|
- Encrypted backups and a printable recovery sheet.
|
||||||
|
- Edit and delete lots and vernacular names.
|
||||||
|
- Portuguese added; right-to-left layouts verified.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
Tanemaki (種まき, "to sow seeds") is a local-first, decentralized app for
|
||||||
|
keeping and sharing traditional seeds.
|
||||||
|
|
||||||
|
Every traditional seed is a letter written by thousands of generations, passed
|
||||||
|
from hand to hand. Tanemaki helps people and seed-saver collectives keep a
|
||||||
|
friendly inventory of their bank, decide what they offer, and share it locally
|
||||||
|
— without a central intermediary that could control, censor, or be fined for
|
||||||
|
it.
|
||||||
|
|
||||||
|
MANAGE YOUR BANK
|
||||||
|
• Add a seed with just a photo and a name — no Latin required.
|
||||||
|
• Note the year, how much you have, where it came from, germination tests.
|
||||||
|
• Search and filter; snap a photo now and name it later.
|
||||||
|
|
||||||
|
YOURS, AND ONLY YOURS
|
||||||
|
• Works fully offline. No account, no server, no trackers.
|
||||||
|
• Everything is encrypted at rest on your device.
|
||||||
|
• Save an encrypted backup and keep a printed recovery sheet — restore your
|
||||||
|
whole bank on a new device.
|
||||||
|
|
||||||
|
SHARE, THE WAY IT'S ALWAYS BEEN DONE
|
||||||
|
• Mark what you have spare to give away, swap or sell.
|
||||||
|
• Print a catalog of what you share to take to a seed fair.
|
||||||
|
|
||||||
|
Tanemaki is free software (AGPL-3.0). No ads, no commissions, no business model
|
||||||
|
— it exists to support traditional varieties and push back against the seed
|
||||||
|
monopoly.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Your seed bank in your pocket — offline, encrypted, no account.
|
||||||
1
apps/app_seeds/fastlane/metadata/android/en-US/title.txt
Normal file
1
apps/app_seeds/fastlane/metadata/android/en-US/title.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Tanemaki
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
Primera beta pública del Bloque 1: el inventario de semillas sin conexión y cifrado.
|
||||||
|
|
||||||
|
- Compartir localmente (regalar / intercambiar / vender) con catálogo imprimible.
|
||||||
|
- Copias cifradas y hoja de recuperación imprimible.
|
||||||
|
- Editar y borrar lotes y nombres vernáculos.
|
||||||
|
- Añadido portugués; disposiciones de derecha a izquierda verificadas.
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
Tanemaki (種まき, "sembrar semillas") es una app local-first y descentralizada
|
||||||
|
para guardar y compartir semillas tradicionales.
|
||||||
|
|
||||||
|
Cada semilla tradicional es una carta escrita por miles de generaciones, que
|
||||||
|
pasa de mano en mano. Tanemaki ayuda a personas y colectivos de guardianas de
|
||||||
|
semillas a llevar un inventario amable de su banco, decidir qué ofrecen y
|
||||||
|
compartirlo localmente, sin un intermediario central que pueda controlarlo,
|
||||||
|
censurarlo o ser multado por ello.
|
||||||
|
|
||||||
|
GESTIONA TU BANCO
|
||||||
|
• Añade una semilla con solo una foto y un nombre; sin latín.
|
||||||
|
• Anota el año, cuánta tienes, de dónde viene, pruebas de germinación.
|
||||||
|
• Busca y filtra; haz una foto ahora y ponle nombre después.
|
||||||
|
|
||||||
|
TUYO, Y SOLO TUYO
|
||||||
|
• Funciona totalmente sin conexión. Sin cuenta, sin servidor, sin rastreadores.
|
||||||
|
• Todo va cifrado en tu dispositivo.
|
||||||
|
• Guarda una copia cifrada y ten una hoja de recuperación impresa: recupera
|
||||||
|
todo tu banco en un dispositivo nuevo.
|
||||||
|
|
||||||
|
COMPARTIR, COMO SE HA HECHO SIEMPRE
|
||||||
|
• Marca lo que te sobra para regalar, intercambiar o vender.
|
||||||
|
• Imprime un catálogo de lo que compartes para llevar a una feria de semillas.
|
||||||
|
|
||||||
|
Tanemaki es software libre (AGPL-3.0). Sin anuncios, sin comisiones, sin modelo
|
||||||
|
de negocio: existe para apoyar las variedades tradicionales y plantar cara al
|
||||||
|
monopolio de las semillas.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Tu banco de semillas en el bolsillo: sin conexión, cifrado, sin cuenta.
|
||||||
1
apps/app_seeds/fastlane/metadata/android/es-ES/title.txt
Normal file
1
apps/app_seeds/fastlane/metadata/android/es-ES/title.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Tanemaki
|
||||||
54
apps/app_seeds/test/data/inventory_scale_test.dart
Normal file
54
apps/app_seeds/test/data/inventory_scale_test.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tane/data/variety_repository.dart';
|
||||||
|
import 'package:tane/db/database.dart';
|
||||||
|
import 'package:tane/db/enums.dart';
|
||||||
|
|
||||||
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
/// A real seed bank can hold thousands of accessions. This guards that the
|
||||||
|
/// list query stays a handful of batched queries (not N+1) and returns in a
|
||||||
|
/// human blink even at that scale — the pre-beta performance check.
|
||||||
|
void main() {
|
||||||
|
late AppDatabase db;
|
||||||
|
late VarietyRepository repo;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
db = newTestDatabase();
|
||||||
|
repo = newTestRepository(db);
|
||||||
|
});
|
||||||
|
tearDown(() => db.close());
|
||||||
|
|
||||||
|
test('the inventory view loads 3000 varieties quickly', () async {
|
||||||
|
const count = 3000;
|
||||||
|
for (var i = 0; i < count; i++) {
|
||||||
|
final id = await repo.addQuickVariety(
|
||||||
|
label: 'Variety $i',
|
||||||
|
category: i.isEven ? 'Poaceae' : 'Fabaceae',
|
||||||
|
);
|
||||||
|
// Every few varieties, add a lot so the batched lot/type/share queries
|
||||||
|
// do real work.
|
||||||
|
if (i % 5 == 0) {
|
||||||
|
await repo.addLot(
|
||||||
|
varietyId: id,
|
||||||
|
harvestYear: 2020 + (i % 5),
|
||||||
|
quantity: const Quantity(kind: QuantityKind.handful),
|
||||||
|
offerStatus: i % 25 == 0 ? OfferStatus.shared : OfferStatus.private,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
final view = await repo.watchInventoryView().first;
|
||||||
|
sw.stop();
|
||||||
|
|
||||||
|
expect(view.items, hasLength(count));
|
||||||
|
// Generous ceiling for CI; typical local runs are well under 500ms. The
|
||||||
|
// point is to catch an accidental N+1 regression, not to micro-benchmark.
|
||||||
|
expect(
|
||||||
|
sw.elapsedMilliseconds,
|
||||||
|
lessThan(3000),
|
||||||
|
reason: 'inventory view took ${sw.elapsedMilliseconds}ms for $count rows',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
71
docs/release.md
Normal file
71
docs/release.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Releasing Tanemaki (manual, no CI yet)
|
||||||
|
|
||||||
|
Block 1 ships as a self-contained Android app (desktop builds also work). There
|
||||||
|
is no release pipeline yet — this page is the by-hand checklist.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
- Semantic version in [`apps/app_seeds/pubspec.yaml`](../apps/app_seeds/pubspec.yaml)
|
||||||
|
as `MAJOR.MINOR.PATCH+BUILD` (e.g. `0.1.0+1`). Flutter maps `+BUILD` to
|
||||||
|
Android `versionCode` and the rest to `versionName`.
|
||||||
|
- Record every user-visible change in [`../CHANGELOG.md`](../CHANGELOG.md) and
|
||||||
|
add a matching per-locale note under
|
||||||
|
`apps/app_seeds/fastlane/metadata/android/<locale>/changelogs/<versionCode>.txt`.
|
||||||
|
|
||||||
|
## Android signing (one-time)
|
||||||
|
|
||||||
|
Release builds sign with your own keystore when a **gitignored**
|
||||||
|
`apps/app_seeds/android/key.properties` exists; without it they fall back to
|
||||||
|
debug keys (so contributors can still `flutter run --release`).
|
||||||
|
|
||||||
|
1. Create a keystore (keep it and its passwords safe — losing it means you can
|
||||||
|
never update the app under the same identity):
|
||||||
|
```bash
|
||||||
|
keytool -genkey -v -keystore ~/tane-release.jks \
|
||||||
|
-keyalg RSA -keysize 4096 -validity 10000 -alias tane
|
||||||
|
```
|
||||||
|
2. Create `apps/app_seeds/android/key.properties` (never commit it):
|
||||||
|
```properties
|
||||||
|
storeFile=/home/you/tane-release.jks
|
||||||
|
storePassword=…
|
||||||
|
keyAlias=tane
|
||||||
|
keyPassword=…
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build the beta by hand
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/app_seeds
|
||||||
|
dart run slang # i18n (if strings changed)
|
||||||
|
dart run build_runner build --delete-conflicting-outputs # Drift (if schema changed)
|
||||||
|
flutter test # must be green
|
||||||
|
flutter build apk --release # or: flutter build appbundle --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The APK lands at
|
||||||
|
`apps/app_seeds/build/app/outputs/flutter-apk/app-release.apk`.
|
||||||
|
|
||||||
|
## Store metadata
|
||||||
|
|
||||||
|
F-Droid / Fastlane-style metadata lives under
|
||||||
|
`apps/app_seeds/fastlane/metadata/android/`. Descriptions are derived from
|
||||||
|
[`intro.md`](intro.md); icon and splash are generated by
|
||||||
|
`flutter_launcher_icons` / `flutter_native_splash` (see `pubspec.yaml`).
|
||||||
|
|
||||||
|
## Manual smoke test before shipping
|
||||||
|
|
||||||
|
Automated tests cover behaviour; a release build still gets a human pass on a
|
||||||
|
real device (the one place we allow manual testing — it validates the packaged
|
||||||
|
artifact, it does not replace tests):
|
||||||
|
|
||||||
|
1. Cold start → intro carousel → home.
|
||||||
|
2. Quick-add a seed with a photo; it appears in the list.
|
||||||
|
3. Mark a lot to share; the "I share" filter and printable catalog appear.
|
||||||
|
4. Save a backup, then restore it (and test the recovery sheet + code on a
|
||||||
|
second install).
|
||||||
|
5. Switch language (es / en / pt); check an RTL locale if the device offers one.
|
||||||
|
|
||||||
|
## Not in this release
|
||||||
|
|
||||||
|
CI, Play Store automation, web builds (SQLCipher is unavailable on web), and
|
||||||
|
the whole social layer (Block 2).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue