feat(ui): Material 3 redesign, cover-photo viewer, About screen
Apply a Material 3 seed-green palette and rounded components across the home, inventory, quick-add, quantity picker and variety-detail screens. The full-screen photo viewer can now set any photo as the cover (via attachment sort order) and delete after confirmation. Lot forms and packaging surface as choice chips. Extract About out of Settings into its own screen (app version via package_info_plus, website link). Add es/en strings for the new lot types, presentation, home tagline and About. Update Seedees v2 mockups.
This commit is contained in:
parent
f5c36f2369
commit
42c16c0e3f
24 changed files with 1960 additions and 416 deletions
|
|
@ -9,6 +9,7 @@ import 'data/variety_repository.dart';
|
||||||
import 'i18n/strings.g.dart';
|
import 'i18n/strings.g.dart';
|
||||||
import 'state/inventory_cubit.dart';
|
import 'state/inventory_cubit.dart';
|
||||||
import 'state/variety_detail_cubit.dart';
|
import 'state/variety_detail_cubit.dart';
|
||||||
|
import 'ui/about_screen.dart';
|
||||||
import 'ui/home_screen.dart';
|
import 'ui/home_screen.dart';
|
||||||
import 'ui/inventory_list_screen.dart';
|
import 'ui/inventory_list_screen.dart';
|
||||||
import 'ui/settings_screen.dart';
|
import 'ui/settings_screen.dart';
|
||||||
|
|
@ -33,6 +34,10 @@ class TaneApp extends StatelessWidget {
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
builder: (context, state) => const SettingsScreen(),
|
builder: (context, state) => const SettingsScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/about',
|
||||||
|
builder: (context, state) => const AboutScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/inventory',
|
path: '/inventory',
|
||||||
builder: (context, state) => BlocProvider(
|
builder: (context, state) => BlocProvider(
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,21 @@
|
||||||
"comingSoon": "Coming soon"
|
"comingSoon": "Coming soon"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"tagline": "Share and grow local seeds",
|
||||||
"openMarket": "Open market",
|
"openMarket": "Open market",
|
||||||
"yourInventory": "Your inventory"
|
"openMarketSubtitle": "Browse and exchange",
|
||||||
|
"yourInventory": "Your inventory",
|
||||||
|
"yourInventorySubtitle": "Manage your seeds"
|
||||||
},
|
},
|
||||||
"photo": {
|
"photo": {
|
||||||
"camera": "Take a photo",
|
"camera": "Take a photo",
|
||||||
"gallery": "Choose from gallery"
|
"gallery": "Choose from gallery",
|
||||||
|
"setAsCover": "Set as cover",
|
||||||
|
"isCover": "Cover photo",
|
||||||
|
"deleteConfirm": "Delete this photo?"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"tagline": "your seed bank",
|
||||||
"inventory": "Inventory",
|
"inventory": "Inventory",
|
||||||
"market": "Market",
|
"market": "Market",
|
||||||
"profile": "Your profile",
|
"profile": "Your profile",
|
||||||
|
|
@ -33,7 +40,21 @@
|
||||||
"langEs": "Español",
|
"langEs": "Español",
|
||||||
"langEn": "English",
|
"langEn": "English",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0."
|
"aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0.",
|
||||||
|
"aboutOpen": "About Tanemaki"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "About",
|
||||||
|
"kanji": "種まき",
|
||||||
|
"tagline": "A local-first, decentralized app for managing and sharing traditional seeds and seedlings.",
|
||||||
|
"intro": "Tanemaki (種まき, \"to sow / scatter seeds\" in Japanese; short name Tane, 種 \"seed\") helps people and collectives keep a friendly inventory of their seed bank, decide what they offer, and share it locally — without a central intermediary that could control, censor, or be fined for it. The goal is both practical and political: to support traditional seed varieties and push back against the seed monopoly.",
|
||||||
|
"heritage": "The name honors the old Japanese mutual-aid traditions around rice — yui (shared community labour) and tanomoshi (reciprocity funds) — that inspired the paper Plantare, the \"community currency for seed exchange\" (BAH-Semillero, 2009, CC-BY-SA). Tanemaki is the digital Plantare.",
|
||||||
|
"version": "Version",
|
||||||
|
"license": "License",
|
||||||
|
"licenseValue": "AGPL-3.0",
|
||||||
|
"website": "Website",
|
||||||
|
"openSourceLicenses": "Open-source licenses",
|
||||||
|
"openSourceLicensesSubtitle": "Third-party libraries and their licenses"
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"title": "Inventory",
|
"title": "Inventory",
|
||||||
|
|
@ -101,7 +122,20 @@
|
||||||
},
|
},
|
||||||
"lotType": {
|
"lotType": {
|
||||||
"seed": "Seeds",
|
"seed": "Seeds",
|
||||||
"plant": "Plants"
|
"plant": "Plant",
|
||||||
|
"seedling": "Seedling",
|
||||||
|
"tree": "Tree / shrub",
|
||||||
|
"bulb": "Bulb / tuber",
|
||||||
|
"cutting": "Cutting"
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"title": "Packaging",
|
||||||
|
"none": "Unspecified",
|
||||||
|
"pot": "Pot",
|
||||||
|
"tray": "Tray",
|
||||||
|
"plug": "Plug",
|
||||||
|
"bareRoot": "Bare-root",
|
||||||
|
"rootBall": "Root-ball"
|
||||||
},
|
},
|
||||||
"unit": {
|
"unit": {
|
||||||
"aFew": "a few",
|
"aFew": "a few",
|
||||||
|
|
@ -127,6 +161,9 @@
|
||||||
"plant": { "singular": "plant", "plural": "plants" },
|
"plant": { "singular": "plant", "plural": "plants" },
|
||||||
"pot": { "singular": "pot", "plural": "pots" },
|
"pot": { "singular": "pot", "plural": "pots" },
|
||||||
"tray": { "singular": "tray", "plural": "trays" },
|
"tray": { "singular": "tray", "plural": "trays" },
|
||||||
|
"seedling": { "singular": "seedling", "plural": "seedlings" },
|
||||||
|
"tree": { "singular": "tree", "plural": "trees" },
|
||||||
|
"cutting": { "singular": "cutting", "plural": "cuttings" },
|
||||||
"grams": { "singular": "gram", "plural": "grams" },
|
"grams": { "singular": "gram", "plural": "grams" },
|
||||||
"count": { "singular": "seed", "plural": "seeds" }
|
"count": { "singular": "seed", "plural": "seeds" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,21 @@
|
||||||
"comingSoon": "Pronto"
|
"comingSoon": "Pronto"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"tagline": "Comparte y cultiva semillas locales",
|
||||||
"openMarket": "Mercado abierto",
|
"openMarket": "Mercado abierto",
|
||||||
"yourInventory": "Tu inventario"
|
"openMarketSubtitle": "Explora e intercambia",
|
||||||
|
"yourInventory": "Tu inventario",
|
||||||
|
"yourInventorySubtitle": "Gestiona tus semillas"
|
||||||
},
|
},
|
||||||
"photo": {
|
"photo": {
|
||||||
"camera": "Hacer una foto",
|
"camera": "Hacer una foto",
|
||||||
"gallery": "Elegir de la galería"
|
"gallery": "Elegir de la galería",
|
||||||
|
"setAsCover": "Poner de portada",
|
||||||
|
"isCover": "Foto de portada",
|
||||||
|
"deleteConfirm": "¿Borrar esta foto?"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
|
"tagline": "tu banco de semillas",
|
||||||
"inventory": "Inventario",
|
"inventory": "Inventario",
|
||||||
"market": "Mercado",
|
"market": "Mercado",
|
||||||
"profile": "Tu perfil",
|
"profile": "Tu perfil",
|
||||||
|
|
@ -33,7 +40,21 @@
|
||||||
"langEs": "Español",
|
"langEs": "Español",
|
||||||
"langEn": "English",
|
"langEn": "English",
|
||||||
"about": "Acerca de",
|
"about": "Acerca de",
|
||||||
"aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0."
|
"aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0.",
|
||||||
|
"aboutOpen": "Acerca de Tanemaki"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "Acerca de",
|
||||||
|
"kanji": "種まき",
|
||||||
|
"tagline": "Una app local-first y descentralizada para gestionar y compartir semillas y plantones tradicionales.",
|
||||||
|
"intro": "Tanemaki (種まき, «sembrar / esparcir semillas» en japonés; nombre corto Tane, 種 «semilla») ayuda a personas y colectivos a llevar un inventario amable de su banco de semillas, decidir qué ofrecen y compartirlo localmente — sin un intermediario central que pueda controlarlo, censurarlo o ser multado por ello. El objetivo es a la vez práctico y político: apoyar las variedades tradicionales de semillas y plantar cara al monopolio semillero.",
|
||||||
|
"heritage": "El nombre honra las viejas tradiciones japonesas de ayuda mutua en torno al arroz — yui (trabajo comunitario compartido) y tanomoshi (fondos de reciprocidad) — que inspiraron el papel Plantare, la «moneda comunitaria para el intercambio de semillas» (BAH-Semillero, 2009, CC-BY-SA). Tanemaki es el Plantare digital.",
|
||||||
|
"version": "Versión",
|
||||||
|
"license": "Licencia",
|
||||||
|
"licenseValue": "AGPL-3.0",
|
||||||
|
"website": "Sitio web",
|
||||||
|
"openSourceLicenses": "Licencias de código abierto",
|
||||||
|
"openSourceLicensesSubtitle": "Bibliotecas de terceros y sus licencias"
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"title": "Inventario",
|
"title": "Inventario",
|
||||||
|
|
@ -101,7 +122,20 @@
|
||||||
},
|
},
|
||||||
"lotType": {
|
"lotType": {
|
||||||
"seed": "Semillas",
|
"seed": "Semillas",
|
||||||
"plant": "Plantel"
|
"plant": "Planta",
|
||||||
|
"seedling": "Plantón",
|
||||||
|
"tree": "Árbol / arbusto",
|
||||||
|
"bulb": "Bulbo / tubérculo",
|
||||||
|
"cutting": "Esqueje"
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"title": "Envase",
|
||||||
|
"none": "Sin especificar",
|
||||||
|
"pot": "Maceta",
|
||||||
|
"tray": "Bandeja",
|
||||||
|
"plug": "Alvéolo",
|
||||||
|
"bareRoot": "Raíz desnuda",
|
||||||
|
"rootBall": "Cepellón"
|
||||||
},
|
},
|
||||||
"unit": {
|
"unit": {
|
||||||
"aFew": "unas pocas",
|
"aFew": "unas pocas",
|
||||||
|
|
@ -127,6 +161,9 @@
|
||||||
"plant": { "singular": "planta", "plural": "plantas" },
|
"plant": { "singular": "planta", "plural": "plantas" },
|
||||||
"pot": { "singular": "maceta", "plural": "macetas" },
|
"pot": { "singular": "maceta", "plural": "macetas" },
|
||||||
"tray": { "singular": "bandeja", "plural": "bandejas" },
|
"tray": { "singular": "bandeja", "plural": "bandejas" },
|
||||||
|
"seedling": { "singular": "plántula", "plural": "plántulas" },
|
||||||
|
"tree": { "singular": "árbol", "plural": "árboles" },
|
||||||
|
"cutting": { "singular": "esqueje", "plural": "esquejes" },
|
||||||
"grams": { "singular": "gramo", "plural": "gramos" },
|
"grams": { "singular": "gramo", "plural": "gramos" },
|
||||||
"count": { "singular": "semilla", "plural": "semillas" }
|
"count": { "singular": "semilla", "plural": "semillas" }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 2
|
/// Locales: 2
|
||||||
/// Strings: 260 (130 per locale)
|
/// Strings: 332 (166 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-07-08 at 12:10 UTC
|
/// Built on 2026-07-09 at 09:25 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||||
late final Translations$photo$en photo = Translations$photo$en.internal(_root);
|
late final Translations$photo$en photo = Translations$photo$en.internal(_root);
|
||||||
late final Translations$menu$en menu = Translations$menu$en.internal(_root);
|
late final Translations$menu$en menu = Translations$menu$en.internal(_root);
|
||||||
late final Translations$settings$en settings = Translations$settings$en.internal(_root);
|
late final Translations$settings$en settings = Translations$settings$en.internal(_root);
|
||||||
|
late final Translations$about$en about = Translations$about$en.internal(_root);
|
||||||
late final Translations$inventory$en inventory = Translations$inventory$en.internal(_root);
|
late final Translations$inventory$en inventory = Translations$inventory$en.internal(_root);
|
||||||
late final Translations$quickAdd$en quickAdd = Translations$quickAdd$en.internal(_root);
|
late final Translations$quickAdd$en quickAdd = Translations$quickAdd$en.internal(_root);
|
||||||
late final Translations$detail$en detail = Translations$detail$en.internal(_root);
|
late final Translations$detail$en detail = Translations$detail$en.internal(_root);
|
||||||
|
|
@ -54,6 +55,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||||
late final Translations$addLot$en addLot = Translations$addLot$en.internal(_root);
|
late final Translations$addLot$en addLot = Translations$addLot$en.internal(_root);
|
||||||
late final Translations$harvest$en harvest = Translations$harvest$en.internal(_root);
|
late final Translations$harvest$en harvest = Translations$harvest$en.internal(_root);
|
||||||
late final Translations$lotType$en lotType = Translations$lotType$en.internal(_root);
|
late final Translations$lotType$en lotType = Translations$lotType$en.internal(_root);
|
||||||
|
late final Translations$presentation$en presentation = Translations$presentation$en.internal(_root);
|
||||||
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,11 +106,20 @@ class Translations$home$en {
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'Share and grow local seeds'
|
||||||
|
String get tagline => 'Share and grow local seeds';
|
||||||
|
|
||||||
/// en: 'Open market'
|
/// en: 'Open market'
|
||||||
String get openMarket => 'Open market';
|
String get openMarket => 'Open market';
|
||||||
|
|
||||||
|
/// en: 'Browse and exchange'
|
||||||
|
String get openMarketSubtitle => 'Browse and exchange';
|
||||||
|
|
||||||
/// en: 'Your inventory'
|
/// en: 'Your inventory'
|
||||||
String get yourInventory => 'Your inventory';
|
String get yourInventory => 'Your inventory';
|
||||||
|
|
||||||
|
/// en: 'Manage your seeds'
|
||||||
|
String get yourInventorySubtitle => 'Manage your seeds';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: photo
|
// Path: photo
|
||||||
|
|
@ -124,6 +135,15 @@ class Translations$photo$en {
|
||||||
|
|
||||||
/// en: 'Choose from gallery'
|
/// en: 'Choose from gallery'
|
||||||
String get gallery => 'Choose from gallery';
|
String get gallery => 'Choose from gallery';
|
||||||
|
|
||||||
|
/// en: 'Set as cover'
|
||||||
|
String get setAsCover => 'Set as cover';
|
||||||
|
|
||||||
|
/// en: 'Cover photo'
|
||||||
|
String get isCover => 'Cover photo';
|
||||||
|
|
||||||
|
/// en: 'Delete this photo?'
|
||||||
|
String get deleteConfirm => 'Delete this photo?';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: menu
|
// Path: menu
|
||||||
|
|
@ -134,6 +154,9 @@ class Translations$menu$en {
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'your seed bank'
|
||||||
|
String get tagline => 'your seed bank';
|
||||||
|
|
||||||
/// en: 'Inventory'
|
/// en: 'Inventory'
|
||||||
String get inventory => 'Inventory';
|
String get inventory => 'Inventory';
|
||||||
|
|
||||||
|
|
@ -181,6 +204,51 @@ class Translations$settings$en {
|
||||||
|
|
||||||
/// en: 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.'
|
/// en: 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.'
|
||||||
String get aboutText => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.';
|
String get aboutText => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.';
|
||||||
|
|
||||||
|
/// en: 'About Tanemaki'
|
||||||
|
String get aboutOpen => 'About Tanemaki';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: about
|
||||||
|
class Translations$about$en {
|
||||||
|
Translations$about$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'About'
|
||||||
|
String get title => 'About';
|
||||||
|
|
||||||
|
/// en: '種まき'
|
||||||
|
String get kanji => '種まき';
|
||||||
|
|
||||||
|
/// en: 'A local-first, decentralized app for managing and sharing traditional seeds and seedlings.'
|
||||||
|
String get tagline => 'A local-first, decentralized app for managing and sharing traditional seeds and seedlings.';
|
||||||
|
|
||||||
|
/// en: 'Tanemaki (種まき, "to sow / scatter seeds" in Japanese; short name Tane, 種 "seed") helps people and collectives keep a friendly inventory of their seed bank, decide what they offer, and share it locally — without a central intermediary that could control, censor, or be fined for it. The goal is both practical and political: to support traditional seed varieties and push back against the seed monopoly.'
|
||||||
|
String get intro => 'Tanemaki (種まき, "to sow / scatter seeds" in Japanese; short name Tane, 種 "seed") helps people and collectives keep a friendly inventory of their seed bank, decide what they offer, and share it locally — without a central intermediary that could control, censor, or be fined for it. The goal is both practical and political: to support traditional seed varieties and push back against the seed monopoly.';
|
||||||
|
|
||||||
|
/// en: 'The name honors the old Japanese mutual-aid traditions around rice — yui (shared community labour) and tanomoshi (reciprocity funds) — that inspired the paper Plantare, the "community currency for seed exchange" (BAH-Semillero, 2009, CC-BY-SA). Tanemaki is the digital Plantare.'
|
||||||
|
String get heritage => 'The name honors the old Japanese mutual-aid traditions around rice — yui (shared community labour) and tanomoshi (reciprocity funds) — that inspired the paper Plantare, the "community currency for seed exchange" (BAH-Semillero, 2009, CC-BY-SA). Tanemaki is the digital Plantare.';
|
||||||
|
|
||||||
|
/// en: 'Version'
|
||||||
|
String get version => 'Version';
|
||||||
|
|
||||||
|
/// en: 'License'
|
||||||
|
String get license => 'License';
|
||||||
|
|
||||||
|
/// en: 'AGPL-3.0'
|
||||||
|
String get licenseValue => 'AGPL-3.0';
|
||||||
|
|
||||||
|
/// en: 'Website'
|
||||||
|
String get website => 'Website';
|
||||||
|
|
||||||
|
/// en: 'Open-source licenses'
|
||||||
|
String get openSourceLicenses => 'Open-source licenses';
|
||||||
|
|
||||||
|
/// en: 'Third-party libraries and their licenses'
|
||||||
|
String get openSourceLicensesSubtitle => 'Third-party libraries and their licenses';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: inventory
|
// Path: inventory
|
||||||
|
|
@ -410,8 +478,50 @@ class Translations$lotType$en {
|
||||||
/// en: 'Seeds'
|
/// en: 'Seeds'
|
||||||
String get seed => 'Seeds';
|
String get seed => 'Seeds';
|
||||||
|
|
||||||
/// en: 'Plants'
|
/// en: 'Plant'
|
||||||
String get plant => 'Plants';
|
String get plant => 'Plant';
|
||||||
|
|
||||||
|
/// en: 'Seedling'
|
||||||
|
String get seedling => 'Seedling';
|
||||||
|
|
||||||
|
/// en: 'Tree / shrub'
|
||||||
|
String get tree => 'Tree / shrub';
|
||||||
|
|
||||||
|
/// en: 'Bulb / tuber'
|
||||||
|
String get bulb => 'Bulb / tuber';
|
||||||
|
|
||||||
|
/// en: 'Cutting'
|
||||||
|
String get cutting => 'Cutting';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: presentation
|
||||||
|
class Translations$presentation$en {
|
||||||
|
Translations$presentation$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'Packaging'
|
||||||
|
String get title => 'Packaging';
|
||||||
|
|
||||||
|
/// en: 'Unspecified'
|
||||||
|
String get none => 'Unspecified';
|
||||||
|
|
||||||
|
/// en: 'Pot'
|
||||||
|
String get pot => 'Pot';
|
||||||
|
|
||||||
|
/// en: 'Tray'
|
||||||
|
String get tray => 'Tray';
|
||||||
|
|
||||||
|
/// en: 'Plug'
|
||||||
|
String get plug => 'Plug';
|
||||||
|
|
||||||
|
/// en: 'Bare-root'
|
||||||
|
String get bareRoot => 'Bare-root';
|
||||||
|
|
||||||
|
/// en: 'Root-ball'
|
||||||
|
String get rootBall => 'Root-ball';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: unit
|
// Path: unit
|
||||||
|
|
@ -453,6 +563,9 @@ class Translations$unit$en {
|
||||||
late final Translations$unit$plant$en plant = Translations$unit$plant$en.internal(_root);
|
late final Translations$unit$plant$en plant = Translations$unit$plant$en.internal(_root);
|
||||||
late final Translations$unit$pot$en pot = Translations$unit$pot$en.internal(_root);
|
late final Translations$unit$pot$en pot = Translations$unit$pot$en.internal(_root);
|
||||||
late final Translations$unit$tray$en tray = Translations$unit$tray$en.internal(_root);
|
late final Translations$unit$tray$en tray = Translations$unit$tray$en.internal(_root);
|
||||||
|
late final Translations$unit$seedling$en seedling = Translations$unit$seedling$en.internal(_root);
|
||||||
|
late final Translations$unit$tree$en tree = Translations$unit$tree$en.internal(_root);
|
||||||
|
late final Translations$unit$cutting$en cutting = Translations$unit$cutting$en.internal(_root);
|
||||||
late final Translations$unit$grams$en grams = Translations$unit$grams$en.internal(_root);
|
late final Translations$unit$grams$en grams = Translations$unit$grams$en.internal(_root);
|
||||||
late final Translations$unit$count$en count = Translations$unit$count$en.internal(_root);
|
late final Translations$unit$count$en count = Translations$unit$count$en.internal(_root);
|
||||||
}
|
}
|
||||||
|
|
@ -742,6 +855,51 @@ class Translations$unit$tray$en {
|
||||||
String get plural => 'trays';
|
String get plural => 'trays';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: unit.seedling
|
||||||
|
class Translations$unit$seedling$en {
|
||||||
|
Translations$unit$seedling$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'seedling'
|
||||||
|
String get singular => 'seedling';
|
||||||
|
|
||||||
|
/// en: 'seedlings'
|
||||||
|
String get plural => 'seedlings';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: unit.tree
|
||||||
|
class Translations$unit$tree$en {
|
||||||
|
Translations$unit$tree$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'tree'
|
||||||
|
String get singular => 'tree';
|
||||||
|
|
||||||
|
/// en: 'trees'
|
||||||
|
String get plural => 'trees';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: unit.cutting
|
||||||
|
class Translations$unit$cutting$en {
|
||||||
|
Translations$unit$cutting$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'cutting'
|
||||||
|
String get singular => 'cutting';
|
||||||
|
|
||||||
|
/// en: 'cuttings'
|
||||||
|
String get plural => 'cuttings';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: unit.grams
|
// Path: unit.grams
|
||||||
class Translations$unit$grams$en {
|
class Translations$unit$grams$en {
|
||||||
Translations$unit$grams$en.internal(this._root);
|
Translations$unit$grams$en.internal(this._root);
|
||||||
|
|
@ -787,10 +945,17 @@ extension on Translations {
|
||||||
'common.edit' => 'Edit',
|
'common.edit' => 'Edit',
|
||||||
'common.type' => 'Type',
|
'common.type' => 'Type',
|
||||||
'common.comingSoon' => 'Coming soon',
|
'common.comingSoon' => 'Coming soon',
|
||||||
|
'home.tagline' => 'Share and grow local seeds',
|
||||||
'home.openMarket' => 'Open market',
|
'home.openMarket' => 'Open market',
|
||||||
|
'home.openMarketSubtitle' => 'Browse and exchange',
|
||||||
'home.yourInventory' => 'Your inventory',
|
'home.yourInventory' => 'Your inventory',
|
||||||
|
'home.yourInventorySubtitle' => 'Manage your seeds',
|
||||||
'photo.camera' => 'Take a photo',
|
'photo.camera' => 'Take a photo',
|
||||||
'photo.gallery' => 'Choose from gallery',
|
'photo.gallery' => 'Choose from gallery',
|
||||||
|
'photo.setAsCover' => 'Set as cover',
|
||||||
|
'photo.isCover' => 'Cover photo',
|
||||||
|
'photo.deleteConfirm' => 'Delete this photo?',
|
||||||
|
'menu.tagline' => 'your seed bank',
|
||||||
'menu.inventory' => 'Inventory',
|
'menu.inventory' => 'Inventory',
|
||||||
'menu.market' => 'Market',
|
'menu.market' => 'Market',
|
||||||
'menu.profile' => 'Your profile',
|
'menu.profile' => 'Your profile',
|
||||||
|
|
@ -804,6 +969,18 @@ extension on Translations {
|
||||||
'settings.langEn' => 'English',
|
'settings.langEn' => 'English',
|
||||||
'settings.about' => 'About',
|
'settings.about' => 'About',
|
||||||
'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.',
|
'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.',
|
||||||
|
'settings.aboutOpen' => 'About Tanemaki',
|
||||||
|
'about.title' => 'About',
|
||||||
|
'about.kanji' => '種まき',
|
||||||
|
'about.tagline' => 'A local-first, decentralized app for managing and sharing traditional seeds and seedlings.',
|
||||||
|
'about.intro' => 'Tanemaki (種まき, "to sow / scatter seeds" in Japanese; short name Tane, 種 "seed") helps people and collectives keep a friendly inventory of their seed bank, decide what they offer, and share it locally — without a central intermediary that could control, censor, or be fined for it. The goal is both practical and political: to support traditional seed varieties and push back against the seed monopoly.',
|
||||||
|
'about.heritage' => 'The name honors the old Japanese mutual-aid traditions around rice — yui (shared community labour) and tanomoshi (reciprocity funds) — that inspired the paper Plantare, the "community currency for seed exchange" (BAH-Semillero, 2009, CC-BY-SA). Tanemaki is the digital Plantare.',
|
||||||
|
'about.version' => 'Version',
|
||||||
|
'about.license' => 'License',
|
||||||
|
'about.licenseValue' => 'AGPL-3.0',
|
||||||
|
'about.website' => 'Website',
|
||||||
|
'about.openSourceLicenses' => 'Open-source licenses',
|
||||||
|
'about.openSourceLicensesSubtitle' => 'Third-party libraries and their licenses',
|
||||||
'inventory.title' => 'Inventory',
|
'inventory.title' => 'Inventory',
|
||||||
'inventory.searchHint' => 'Search seeds',
|
'inventory.searchHint' => 'Search seeds',
|
||||||
'inventory.empty' => 'No seeds yet. Tap + to add your first.',
|
'inventory.empty' => 'No seeds yet. Tap + to add your first.',
|
||||||
|
|
@ -863,7 +1040,18 @@ extension on Translations {
|
||||||
'harvest.monthNames.10' => 'November',
|
'harvest.monthNames.10' => 'November',
|
||||||
'harvest.monthNames.11' => 'December',
|
'harvest.monthNames.11' => 'December',
|
||||||
'lotType.seed' => 'Seeds',
|
'lotType.seed' => 'Seeds',
|
||||||
'lotType.plant' => 'Plants',
|
'lotType.plant' => 'Plant',
|
||||||
|
'lotType.seedling' => 'Seedling',
|
||||||
|
'lotType.tree' => 'Tree / shrub',
|
||||||
|
'lotType.bulb' => 'Bulb / tuber',
|
||||||
|
'lotType.cutting' => 'Cutting',
|
||||||
|
'presentation.title' => 'Packaging',
|
||||||
|
'presentation.none' => 'Unspecified',
|
||||||
|
'presentation.pot' => 'Pot',
|
||||||
|
'presentation.tray' => 'Tray',
|
||||||
|
'presentation.plug' => 'Plug',
|
||||||
|
'presentation.bareRoot' => 'Bare-root',
|
||||||
|
'presentation.rootBall' => 'Root-ball',
|
||||||
'unit.aFew' => 'a few',
|
'unit.aFew' => 'a few',
|
||||||
'unit.some' => 'some',
|
'unit.some' => 'some',
|
||||||
'unit.plenty' => 'plenty',
|
'unit.plenty' => 'plenty',
|
||||||
|
|
@ -906,6 +1094,12 @@ extension on Translations {
|
||||||
'unit.pot.plural' => 'pots',
|
'unit.pot.plural' => 'pots',
|
||||||
'unit.tray.singular' => 'tray',
|
'unit.tray.singular' => 'tray',
|
||||||
'unit.tray.plural' => 'trays',
|
'unit.tray.plural' => 'trays',
|
||||||
|
'unit.seedling.singular' => 'seedling',
|
||||||
|
'unit.seedling.plural' => 'seedlings',
|
||||||
|
'unit.tree.singular' => 'tree',
|
||||||
|
'unit.tree.plural' => 'trees',
|
||||||
|
'unit.cutting.singular' => 'cutting',
|
||||||
|
'unit.cutting.plural' => 'cuttings',
|
||||||
'unit.grams.singular' => 'gram',
|
'unit.grams.singular' => 'gram',
|
||||||
'unit.grams.plural' => 'grams',
|
'unit.grams.plural' => 'grams',
|
||||||
'unit.count.singular' => 'seed',
|
'unit.count.singular' => 'seed',
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$photo$es photo = _Translations$photo$es._(_root);
|
@override late final _Translations$photo$es photo = _Translations$photo$es._(_root);
|
||||||
@override late final _Translations$menu$es menu = _Translations$menu$es._(_root);
|
@override late final _Translations$menu$es menu = _Translations$menu$es._(_root);
|
||||||
@override late final _Translations$settings$es settings = _Translations$settings$es._(_root);
|
@override late final _Translations$settings$es settings = _Translations$settings$es._(_root);
|
||||||
|
@override late final _Translations$about$es about = _Translations$about$es._(_root);
|
||||||
@override late final _Translations$inventory$es inventory = _Translations$inventory$es._(_root);
|
@override late final _Translations$inventory$es inventory = _Translations$inventory$es._(_root);
|
||||||
@override late final _Translations$quickAdd$es quickAdd = _Translations$quickAdd$es._(_root);
|
@override late final _Translations$quickAdd$es quickAdd = _Translations$quickAdd$es._(_root);
|
||||||
@override late final _Translations$detail$es detail = _Translations$detail$es._(_root);
|
@override late final _Translations$detail$es detail = _Translations$detail$es._(_root);
|
||||||
|
|
@ -53,6 +54,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$addLot$es addLot = _Translations$addLot$es._(_root);
|
@override late final _Translations$addLot$es addLot = _Translations$addLot$es._(_root);
|
||||||
@override late final _Translations$harvest$es harvest = _Translations$harvest$es._(_root);
|
@override late final _Translations$harvest$es harvest = _Translations$harvest$es._(_root);
|
||||||
@override late final _Translations$lotType$es lotType = _Translations$lotType$es._(_root);
|
@override late final _Translations$lotType$es lotType = _Translations$lotType$es._(_root);
|
||||||
|
@override late final _Translations$presentation$es presentation = _Translations$presentation$es._(_root);
|
||||||
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,8 +90,11 @@ class _Translations$home$es extends Translations$home$en {
|
||||||
final TranslationsEs _root; // ignore: unused_field
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
|
@override String get tagline => 'Comparte y cultiva semillas locales';
|
||||||
@override String get openMarket => 'Mercado abierto';
|
@override String get openMarket => 'Mercado abierto';
|
||||||
|
@override String get openMarketSubtitle => 'Explora e intercambia';
|
||||||
@override String get yourInventory => 'Tu inventario';
|
@override String get yourInventory => 'Tu inventario';
|
||||||
|
@override String get yourInventorySubtitle => 'Gestiona tus semillas';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: photo
|
// Path: photo
|
||||||
|
|
@ -101,6 +106,9 @@ class _Translations$photo$es extends Translations$photo$en {
|
||||||
// Translations
|
// Translations
|
||||||
@override String get camera => 'Hacer una foto';
|
@override String get camera => 'Hacer una foto';
|
||||||
@override String get gallery => 'Elegir de la galería';
|
@override String get gallery => 'Elegir de la galería';
|
||||||
|
@override String get setAsCover => 'Poner de portada';
|
||||||
|
@override String get isCover => 'Foto de portada';
|
||||||
|
@override String get deleteConfirm => '¿Borrar esta foto?';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: menu
|
// Path: menu
|
||||||
|
|
@ -110,6 +118,7 @@ class _Translations$menu$es extends Translations$menu$en {
|
||||||
final TranslationsEs _root; // ignore: unused_field
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
|
@override String get tagline => 'tu banco de semillas';
|
||||||
@override String get inventory => 'Inventario';
|
@override String get inventory => 'Inventario';
|
||||||
@override String get market => 'Mercado';
|
@override String get market => 'Mercado';
|
||||||
@override String get profile => 'Tu perfil';
|
@override String get profile => 'Tu perfil';
|
||||||
|
|
@ -132,6 +141,27 @@ class _Translations$settings$es extends Translations$settings$en {
|
||||||
@override String get langEn => 'English';
|
@override String get langEn => 'English';
|
||||||
@override String get about => 'Acerca de';
|
@override String get about => 'Acerca de';
|
||||||
@override String get aboutText => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.';
|
@override String get aboutText => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.';
|
||||||
|
@override String get aboutOpen => 'Acerca de Tanemaki';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: about
|
||||||
|
class _Translations$about$es extends Translations$about$en {
|
||||||
|
_Translations$about$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Acerca de';
|
||||||
|
@override String get kanji => '種まき';
|
||||||
|
@override String get tagline => 'Una app local-first y descentralizada para gestionar y compartir semillas y plantones tradicionales.';
|
||||||
|
@override String get intro => 'Tanemaki (種まき, «sembrar / esparcir semillas» en japonés; nombre corto Tane, 種 «semilla») ayuda a personas y colectivos a llevar un inventario amable de su banco de semillas, decidir qué ofrecen y compartirlo localmente — sin un intermediario central que pueda controlarlo, censurarlo o ser multado por ello. El objetivo es a la vez práctico y político: apoyar las variedades tradicionales de semillas y plantar cara al monopolio semillero.';
|
||||||
|
@override String get heritage => 'El nombre honra las viejas tradiciones japonesas de ayuda mutua en torno al arroz — yui (trabajo comunitario compartido) y tanomoshi (fondos de reciprocidad) — que inspiraron el papel Plantare, la «moneda comunitaria para el intercambio de semillas» (BAH-Semillero, 2009, CC-BY-SA). Tanemaki es el Plantare digital.';
|
||||||
|
@override String get version => 'Versión';
|
||||||
|
@override String get license => 'Licencia';
|
||||||
|
@override String get licenseValue => 'AGPL-3.0';
|
||||||
|
@override String get website => 'Sitio web';
|
||||||
|
@override String get openSourceLicenses => 'Licencias de código abierto';
|
||||||
|
@override String get openSourceLicensesSubtitle => 'Bibliotecas de terceros y sus licencias';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: inventory
|
// Path: inventory
|
||||||
|
|
@ -265,7 +295,27 @@ class _Translations$lotType$es extends Translations$lotType$en {
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
@override String get seed => 'Semillas';
|
@override String get seed => 'Semillas';
|
||||||
@override String get plant => 'Plantel';
|
@override String get plant => 'Planta';
|
||||||
|
@override String get seedling => 'Plantón';
|
||||||
|
@override String get tree => 'Árbol / arbusto';
|
||||||
|
@override String get bulb => 'Bulbo / tubérculo';
|
||||||
|
@override String get cutting => 'Esqueje';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: presentation
|
||||||
|
class _Translations$presentation$es extends Translations$presentation$en {
|
||||||
|
_Translations$presentation$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Envase';
|
||||||
|
@override String get none => 'Sin especificar';
|
||||||
|
@override String get pot => 'Maceta';
|
||||||
|
@override String get tray => 'Bandeja';
|
||||||
|
@override String get plug => 'Alvéolo';
|
||||||
|
@override String get bareRoot => 'Raíz desnuda';
|
||||||
|
@override String get rootBall => 'Cepellón';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: unit
|
// Path: unit
|
||||||
|
|
@ -298,6 +348,9 @@ class _Translations$unit$es extends Translations$unit$en {
|
||||||
@override late final _Translations$unit$plant$es plant = _Translations$unit$plant$es._(_root);
|
@override late final _Translations$unit$plant$es plant = _Translations$unit$plant$es._(_root);
|
||||||
@override late final _Translations$unit$pot$es pot = _Translations$unit$pot$es._(_root);
|
@override late final _Translations$unit$pot$es pot = _Translations$unit$pot$es._(_root);
|
||||||
@override late final _Translations$unit$tray$es tray = _Translations$unit$tray$es._(_root);
|
@override late final _Translations$unit$tray$es tray = _Translations$unit$tray$es._(_root);
|
||||||
|
@override late final _Translations$unit$seedling$es seedling = _Translations$unit$seedling$es._(_root);
|
||||||
|
@override late final _Translations$unit$tree$es tree = _Translations$unit$tree$es._(_root);
|
||||||
|
@override late final _Translations$unit$cutting$es cutting = _Translations$unit$cutting$es._(_root);
|
||||||
@override late final _Translations$unit$grams$es grams = _Translations$unit$grams$es._(_root);
|
@override late final _Translations$unit$grams$es grams = _Translations$unit$grams$es._(_root);
|
||||||
@override late final _Translations$unit$count$es count = _Translations$unit$count$es._(_root);
|
@override late final _Translations$unit$count$es count = _Translations$unit$count$es._(_root);
|
||||||
}
|
}
|
||||||
|
|
@ -511,6 +564,39 @@ class _Translations$unit$tray$es extends Translations$unit$tray$en {
|
||||||
@override String get plural => 'bandejas';
|
@override String get plural => 'bandejas';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: unit.seedling
|
||||||
|
class _Translations$unit$seedling$es extends Translations$unit$seedling$en {
|
||||||
|
_Translations$unit$seedling$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get singular => 'plántula';
|
||||||
|
@override String get plural => 'plántulas';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: unit.tree
|
||||||
|
class _Translations$unit$tree$es extends Translations$unit$tree$en {
|
||||||
|
_Translations$unit$tree$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get singular => 'árbol';
|
||||||
|
@override String get plural => 'árboles';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: unit.cutting
|
||||||
|
class _Translations$unit$cutting$es extends Translations$unit$cutting$en {
|
||||||
|
_Translations$unit$cutting$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get singular => 'esqueje';
|
||||||
|
@override String get plural => 'esquejes';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: unit.grams
|
// Path: unit.grams
|
||||||
class _Translations$unit$grams$es extends Translations$unit$grams$en {
|
class _Translations$unit$grams$es extends Translations$unit$grams$en {
|
||||||
_Translations$unit$grams$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
_Translations$unit$grams$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
@ -548,10 +634,17 @@ extension on TranslationsEs {
|
||||||
'common.edit' => 'Editar',
|
'common.edit' => 'Editar',
|
||||||
'common.type' => 'Tipo',
|
'common.type' => 'Tipo',
|
||||||
'common.comingSoon' => 'Pronto',
|
'common.comingSoon' => 'Pronto',
|
||||||
|
'home.tagline' => 'Comparte y cultiva semillas locales',
|
||||||
'home.openMarket' => 'Mercado abierto',
|
'home.openMarket' => 'Mercado abierto',
|
||||||
|
'home.openMarketSubtitle' => 'Explora e intercambia',
|
||||||
'home.yourInventory' => 'Tu inventario',
|
'home.yourInventory' => 'Tu inventario',
|
||||||
|
'home.yourInventorySubtitle' => 'Gestiona tus semillas',
|
||||||
'photo.camera' => 'Hacer una foto',
|
'photo.camera' => 'Hacer una foto',
|
||||||
'photo.gallery' => 'Elegir de la galería',
|
'photo.gallery' => 'Elegir de la galería',
|
||||||
|
'photo.setAsCover' => 'Poner de portada',
|
||||||
|
'photo.isCover' => 'Foto de portada',
|
||||||
|
'photo.deleteConfirm' => '¿Borrar esta foto?',
|
||||||
|
'menu.tagline' => 'tu banco de semillas',
|
||||||
'menu.inventory' => 'Inventario',
|
'menu.inventory' => 'Inventario',
|
||||||
'menu.market' => 'Mercado',
|
'menu.market' => 'Mercado',
|
||||||
'menu.profile' => 'Tu perfil',
|
'menu.profile' => 'Tu perfil',
|
||||||
|
|
@ -565,6 +658,18 @@ extension on TranslationsEs {
|
||||||
'settings.langEn' => 'English',
|
'settings.langEn' => 'English',
|
||||||
'settings.about' => 'Acerca de',
|
'settings.about' => 'Acerca de',
|
||||||
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
|
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
|
||||||
|
'settings.aboutOpen' => 'Acerca de Tanemaki',
|
||||||
|
'about.title' => 'Acerca de',
|
||||||
|
'about.kanji' => '種まき',
|
||||||
|
'about.tagline' => 'Una app local-first y descentralizada para gestionar y compartir semillas y plantones tradicionales.',
|
||||||
|
'about.intro' => 'Tanemaki (種まき, «sembrar / esparcir semillas» en japonés; nombre corto Tane, 種 «semilla») ayuda a personas y colectivos a llevar un inventario amable de su banco de semillas, decidir qué ofrecen y compartirlo localmente — sin un intermediario central que pueda controlarlo, censurarlo o ser multado por ello. El objetivo es a la vez práctico y político: apoyar las variedades tradicionales de semillas y plantar cara al monopolio semillero.',
|
||||||
|
'about.heritage' => 'El nombre honra las viejas tradiciones japonesas de ayuda mutua en torno al arroz — yui (trabajo comunitario compartido) y tanomoshi (fondos de reciprocidad) — que inspiraron el papel Plantare, la «moneda comunitaria para el intercambio de semillas» (BAH-Semillero, 2009, CC-BY-SA). Tanemaki es el Plantare digital.',
|
||||||
|
'about.version' => 'Versión',
|
||||||
|
'about.license' => 'Licencia',
|
||||||
|
'about.licenseValue' => 'AGPL-3.0',
|
||||||
|
'about.website' => 'Sitio web',
|
||||||
|
'about.openSourceLicenses' => 'Licencias de código abierto',
|
||||||
|
'about.openSourceLicensesSubtitle' => 'Bibliotecas de terceros y sus licencias',
|
||||||
'inventory.title' => 'Inventario',
|
'inventory.title' => 'Inventario',
|
||||||
'inventory.searchHint' => 'Buscar semillas',
|
'inventory.searchHint' => 'Buscar semillas',
|
||||||
'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.',
|
'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.',
|
||||||
|
|
@ -624,7 +729,18 @@ extension on TranslationsEs {
|
||||||
'harvest.monthNames.10' => 'noviembre',
|
'harvest.monthNames.10' => 'noviembre',
|
||||||
'harvest.monthNames.11' => 'diciembre',
|
'harvest.monthNames.11' => 'diciembre',
|
||||||
'lotType.seed' => 'Semillas',
|
'lotType.seed' => 'Semillas',
|
||||||
'lotType.plant' => 'Plantel',
|
'lotType.plant' => 'Planta',
|
||||||
|
'lotType.seedling' => 'Plantón',
|
||||||
|
'lotType.tree' => 'Árbol / arbusto',
|
||||||
|
'lotType.bulb' => 'Bulbo / tubérculo',
|
||||||
|
'lotType.cutting' => 'Esqueje',
|
||||||
|
'presentation.title' => 'Envase',
|
||||||
|
'presentation.none' => 'Sin especificar',
|
||||||
|
'presentation.pot' => 'Maceta',
|
||||||
|
'presentation.tray' => 'Bandeja',
|
||||||
|
'presentation.plug' => 'Alvéolo',
|
||||||
|
'presentation.bareRoot' => 'Raíz desnuda',
|
||||||
|
'presentation.rootBall' => 'Cepellón',
|
||||||
'unit.aFew' => 'unas pocas',
|
'unit.aFew' => 'unas pocas',
|
||||||
'unit.some' => 'algunas',
|
'unit.some' => 'algunas',
|
||||||
'unit.plenty' => 'muchas',
|
'unit.plenty' => 'muchas',
|
||||||
|
|
@ -667,6 +783,12 @@ extension on TranslationsEs {
|
||||||
'unit.pot.plural' => 'macetas',
|
'unit.pot.plural' => 'macetas',
|
||||||
'unit.tray.singular' => 'bandeja',
|
'unit.tray.singular' => 'bandeja',
|
||||||
'unit.tray.plural' => 'bandejas',
|
'unit.tray.plural' => 'bandejas',
|
||||||
|
'unit.seedling.singular' => 'plántula',
|
||||||
|
'unit.seedling.plural' => 'plántulas',
|
||||||
|
'unit.tree.singular' => 'árbol',
|
||||||
|
'unit.tree.plural' => 'árboles',
|
||||||
|
'unit.cutting.singular' => 'esqueje',
|
||||||
|
'unit.cutting.plural' => 'esquejes',
|
||||||
'unit.grams.singular' => 'gramo',
|
'unit.grams.singular' => 'gramo',
|
||||||
'unit.grams.plural' => 'gramos',
|
'unit.grams.plural' => 'gramos',
|
||||||
'unit.count.singular' => 'semilla',
|
'unit.count.singular' => 'semilla',
|
||||||
|
|
|
||||||
179
apps/app_seeds/lib/ui/about_screen.dart
Normal file
179
apps/app_seeds/lib/ui/about_screen.dart
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../i18n/strings.g.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// The app's public website. Shown as a tappable row in [AboutScreen].
|
||||||
|
const String _websiteUrl = 'https://tanemaki.app';
|
||||||
|
|
||||||
|
/// An "about" screen à la Ğ1nkgo: brand header, the human explanation of what
|
||||||
|
/// Tanemaki is and where its name comes from, the app version, license, the
|
||||||
|
/// website, and the bundled open-source licenses page.
|
||||||
|
class AboutScreen extends StatelessWidget {
|
||||||
|
const AboutScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(t.about.title)),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 24),
|
||||||
|
children: [
|
||||||
|
const _Header(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
|
||||||
|
child: Text(
|
||||||
|
t.about.tagline,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: seedOnSurface,
|
||||||
|
height: 1.35,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _Paragraph(),
|
||||||
|
const Divider(height: 32),
|
||||||
|
_InfoTile(
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
label: t.about.version,
|
||||||
|
valueBuilder: (context) => const _VersionText(),
|
||||||
|
),
|
||||||
|
_InfoTile(
|
||||||
|
icon: Icons.balance_outlined,
|
||||||
|
label: t.about.license,
|
||||||
|
value: t.about.licenseValue,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.public, color: seedGreen),
|
||||||
|
title: Text(t.about.website),
|
||||||
|
subtitle: const Text(_websiteUrl),
|
||||||
|
onTap: () => launchUrl(
|
||||||
|
Uri.parse(_websiteUrl),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.description_outlined, color: seedGreen),
|
||||||
|
title: Text(t.about.openSourceLicenses),
|
||||||
|
subtitle: Text(t.about.openSourceLicensesSubtitle),
|
||||||
|
onTap: () => showLicensePage(
|
||||||
|
context: context,
|
||||||
|
applicationName: t.app.title,
|
||||||
|
applicationIcon: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Image.asset('assets/logo.png', width: 48),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Brand block: the logo, the app name and its 種まき kanji.
|
||||||
|
class _Header extends StatelessWidget {
|
||||||
|
const _Header();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 4),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Image.asset('assets/logo.png', width: 88),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
t.app.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: seedGreen,
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
t.about.kanji,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
color: seedGreen.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two explanatory paragraphs: what Tanemaki is, and the heritage of its
|
||||||
|
/// name (yui / tanomoshi / the paper Plantare).
|
||||||
|
class _Paragraph extends StatelessWidget {
|
||||||
|
const _Paragraph();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
final style = Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(color: seedOnSurface, height: 1.5);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(t.about.intro, style: style),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Text(t.about.heritage, style: style),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A leading-icon row showing a static [value] or a [valueBuilder] widget.
|
||||||
|
class _InfoTile extends StatelessWidget {
|
||||||
|
const _InfoTile({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
this.value,
|
||||||
|
this.valueBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String? value;
|
||||||
|
final WidgetBuilder? valueBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(icon, color: seedGreen),
|
||||||
|
title: Text(label),
|
||||||
|
trailing: value != null
|
||||||
|
? Text(value!, style: Theme.of(context).textTheme.bodyMedium)
|
||||||
|
: valueBuilder?.call(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the app version from the platform and renders it as `x.y.z (build)`.
|
||||||
|
class _VersionText extends StatelessWidget {
|
||||||
|
const _VersionText();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<PackageInfo>(
|
||||||
|
future: PackageInfo.fromPlatform(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final info = snapshot.data;
|
||||||
|
final text = info == null
|
||||||
|
? '—'
|
||||||
|
: '${info.version} (${info.buildNumber})';
|
||||||
|
return Text(text, style: Theme.of(context).textTheme.bodyMedium);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,10 @@ import '../i18n/strings.g.dart';
|
||||||
import 'seed_glyph.dart';
|
import 'seed_glyph.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
/// The app's navigation drawer (mockup 03). Inventory is live; the social items
|
/// The app's navigation drawer (redesign screen 05). A white sheet: Inventory is
|
||||||
/// (market, profile, chat…) belong to Block 2 and are shown disabled with a
|
/// the live destination (green seed glyph), the social items (market, profile,
|
||||||
/// "coming soon" tag so the roadmap is visible.
|
/// chat…) belong to Block 2 and are greyed with a "soon" tag, and Settings sits
|
||||||
|
/// pinned at the bottom.
|
||||||
class AppDrawer extends StatelessWidget {
|
class AppDrawer extends StatelessWidget {
|
||||||
const AppDrawer({super.key});
|
const AppDrawer({super.key});
|
||||||
|
|
||||||
|
|
@ -16,34 +17,42 @@ class AppDrawer extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = context.t;
|
final t = context.t;
|
||||||
return Drawer(
|
return Drawer(
|
||||||
backgroundColor: Colors.white,
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const _DrawerHeader(),
|
const _DrawerHeader(),
|
||||||
_ActiveItem(
|
_DrawerItem(
|
||||||
icon: const SeedGlyph(SeedGlyphs.jars, size: 22),
|
icon: const SeedGlyph(SeedGlyphs.jars, size: 23),
|
||||||
label: t.menu.inventory,
|
label: t.menu.inventory,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
context.go('/inventory');
|
context.go('/inventory');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_SoonItem(icon: Symbols.storefront, label: t.menu.market),
|
_DrawerItem(
|
||||||
const Divider(),
|
icon: const Icon(Symbols.storefront),
|
||||||
_SoonItem(icon: Icons.person_outline, label: t.menu.profile),
|
label: t.menu.market,
|
||||||
_SoonItem(icon: Icons.chat_bubble_outline, label: t.menu.chat),
|
divider: true,
|
||||||
_SoonItem(icon: Icons.favorite_border, label: t.menu.wishlist),
|
),
|
||||||
_SoonItem(icon: Icons.group_outlined, label: t.menu.following),
|
_DrawerItem(icon: const Icon(Icons.person), label: t.menu.profile),
|
||||||
|
_DrawerItem(
|
||||||
|
icon: const Icon(Icons.chat_bubble),
|
||||||
|
label: t.menu.chat,
|
||||||
|
),
|
||||||
|
_DrawerItem(
|
||||||
|
icon: const Icon(Icons.favorite),
|
||||||
|
label: t.menu.wishlist,
|
||||||
|
),
|
||||||
|
_DrawerItem(icon: const Icon(Icons.group), label: t.menu.following),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const Divider(),
|
const Divider(height: 1),
|
||||||
_ActiveItem(
|
_DrawerItem(
|
||||||
icon: const Icon(Icons.settings_outlined),
|
icon: const Icon(Icons.settings),
|
||||||
label: t.menu.settings,
|
label: t.menu.settings,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
context.go('/settings');
|
context.push('/settings');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -53,70 +62,110 @@ class AppDrawer extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The drawer's brand header. Tapping it closes the drawer and returns to the
|
||||||
|
/// home screen.
|
||||||
class _DrawerHeader extends StatelessWidget {
|
class _DrawerHeader extends StatelessWidget {
|
||||||
const _DrawerHeader();
|
const _DrawerHeader();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return InkWell(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
onTap: () {
|
||||||
child: Row(
|
Navigator.of(context).pop();
|
||||||
children: [
|
context.go('/');
|
||||||
const Icon(Symbols.eco, color: seedGreen, size: 32),
|
},
|
||||||
const SizedBox(width: 12),
|
child: Container(
|
||||||
Text(
|
padding: const EdgeInsets.fromLTRB(22, 18, 22, 16),
|
||||||
context.t.app.title,
|
decoration: const BoxDecoration(
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
border: Border(bottom: BorderSide(color: seedDivider)),
|
||||||
color: seedGreenDark,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Image.asset('assets/logo.png', height: 34),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
context.t.app.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: seedGreen,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActiveItem extends StatelessWidget {
|
/// A 52-tall drawer row. A live destination has an [onTap] (dark text, green
|
||||||
const _ActiveItem({
|
/// icon); a disabled Block-2 destination is greyed with a "soon" tag. [divider]
|
||||||
|
/// draws a hairline under the row (separating live from upcoming items).
|
||||||
|
class _DrawerItem extends StatelessWidget {
|
||||||
|
const _DrawerItem({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.onTap,
|
this.onTap,
|
||||||
|
this.divider = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onTap;
|
final VoidCallback? onTap;
|
||||||
|
final bool divider;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
final enabled = onTap != null;
|
||||||
leading: SizedBox(width: 24, child: Center(child: icon)),
|
final fg = enabled ? seedOnSurface : const Color(0xFF9AA88F);
|
||||||
title: Text(label),
|
final iconColor = enabled ? seedGreen : const Color(0xFF9AA88F);
|
||||||
|
final row = InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
);
|
child: Container(
|
||||||
}
|
height: 52,
|
||||||
}
|
padding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
|
decoration: divider
|
||||||
/// A disabled Block-2 destination with a subtle "coming soon" tag.
|
? const BoxDecoration(
|
||||||
class _SoonItem extends StatelessWidget {
|
border: Border(bottom: BorderSide(color: seedDivider)),
|
||||||
const _SoonItem({required this.icon, required this.label});
|
)
|
||||||
|
: null,
|
||||||
final IconData icon;
|
child: Row(
|
||||||
final String label;
|
children: [
|
||||||
|
SizedBox(
|
||||||
@override
|
width: 24,
|
||||||
Widget build(BuildContext context) {
|
child: Center(
|
||||||
return ListTile(
|
child: IconTheme.merge(
|
||||||
enabled: false,
|
data: IconThemeData(size: 23, color: iconColor),
|
||||||
leading: Icon(icon),
|
child: icon,
|
||||||
title: Text(label),
|
),
|
||||||
trailing: Text(
|
),
|
||||||
context.t.common.comingSoon,
|
),
|
||||||
style: Theme.of(
|
const SizedBox(width: 18),
|
||||||
context,
|
Expanded(
|
||||||
).textTheme.labelSmall?.copyWith(color: Colors.black38),
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: fg,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: enabled ? FontWeight.w500 : FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!enabled)
|
||||||
|
Text(
|
||||||
|
context.t.common.comingSoon.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFB3BDA8),
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ import 'app_drawer.dart';
|
||||||
import 'seed_glyph.dart';
|
import 'seed_glyph.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
/// The main menu (mockup 02): a sprout logo over a faint seed-glyph watermark,
|
/// The main menu (redesign screen 00): a sprout logo in a soft green disc over a
|
||||||
/// with the big "open market" (Block 2, coming soon) and "your inventory"
|
/// faint seed-glyph watermark, with "Your inventory" as the primary green call
|
||||||
/// destinations. The hamburger opens [AppDrawer].
|
/// to action and "Open market" (Block 2) as a disabled outlined card. The
|
||||||
|
/// hamburger opens [AppDrawer].
|
||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
|
|
@ -25,33 +26,61 @@ class HomeScreen extends StatelessWidget {
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 440),
|
constraints: const BoxConstraints(maxWidth: 440),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Center(
|
||||||
'assets/logo.png',
|
child: Container(
|
||||||
height: 140,
|
width: 128,
|
||||||
fit: BoxFit.contain,
|
height: 128,
|
||||||
),
|
decoration: const BoxDecoration(
|
||||||
const SizedBox(height: 40),
|
color: Color(0xFFDDF3CE),
|
||||||
_MenuButton(
|
shape: BoxShape.circle,
|
||||||
icon: const SeedGlyph(
|
),
|
||||||
SeedGlyphs.share,
|
alignment: Alignment.center,
|
||||||
size: 26,
|
child: Image.asset(
|
||||||
color: Colors.white,
|
'assets/logo.png',
|
||||||
|
height: 84,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
label: t.home.openMarket,
|
),
|
||||||
tag: t.common.comingSoon,
|
const SizedBox(height: 10),
|
||||||
onTap: null,
|
Text(
|
||||||
|
t.app.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF1A2A13),
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: -0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
t.home.tagline,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF55624A),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 34),
|
||||||
|
_PrimaryMenuCard(
|
||||||
|
key: const Key('home.inventory'),
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
label: t.home.yourInventory,
|
||||||
|
subtitle: t.home.yourInventorySubtitle,
|
||||||
|
onTap: () => context.go('/inventory'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_MenuButton(
|
_OutlinedMenuCard(
|
||||||
key: const Key('home.inventory'),
|
icon: Icons.storefront_outlined,
|
||||||
icon: const Icon(Icons.list, color: Colors.white),
|
label: t.home.openMarket,
|
||||||
label: t.home.yourInventory,
|
subtitle: t.home.openMarketSubtitle,
|
||||||
onTap: () => context.go('/inventory'),
|
tag: t.common.comingSoon,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -64,50 +93,49 @@ class HomeScreen extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MenuButton extends StatelessWidget {
|
/// The primary green call-to-action: white icon in a translucent disc, a title
|
||||||
const _MenuButton({
|
/// and a subtitle, with a soft green shadow.
|
||||||
|
class _PrimaryMenuCard extends StatelessWidget {
|
||||||
|
const _PrimaryMenuCard({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
this.onTap,
|
required this.subtitle,
|
||||||
this.tag,
|
required this.onTap,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback? onTap;
|
final String subtitle;
|
||||||
final String? tag;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final enabled = onTap != null;
|
|
||||||
return Material(
|
return Material(
|
||||||
color: enabled ? seedGreen : seedGreen.withValues(alpha: 0.45),
|
color: seedGreen,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: seedGreen.withValues(alpha: 0.4),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(18),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 22),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
icon,
|
_IconDisc(
|
||||||
|
background: Colors.white.withValues(alpha: 0.18),
|
||||||
|
child: Icon(icon, color: Colors.white, size: 26),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: _CardText(
|
||||||
label,
|
label: label,
|
||||||
style: const TextStyle(
|
subtitle: subtitle,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20,
|
subtitleColor: Colors.white.withValues(alpha: 0.85),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (tag != null)
|
|
||||||
Text(
|
|
||||||
tag!,
|
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -116,41 +144,168 @@ class _MenuButton extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A faint scatter of seed pictograms behind the menu (like mockup 02).
|
/// A disabled Block-2 destination: an outlined white card with a green-disc
|
||||||
|
/// icon and a "soon" tag.
|
||||||
|
class _OutlinedMenuCard extends StatelessWidget {
|
||||||
|
const _OutlinedMenuCard({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.tag,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String subtitle;
|
||||||
|
final String tag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
border: Border.all(color: const Color(0xFFB8C4AC)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 22),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_IconDisc(
|
||||||
|
background: const Color(0xFFDDF3CE),
|
||||||
|
child: Icon(icon, color: seedGreen, size: 26),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _CardText(
|
||||||
|
label: label,
|
||||||
|
subtitle: subtitle,
|
||||||
|
color: seedOnSurface,
|
||||||
|
subtitleColor: seedMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
tag.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFB3BDA8),
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IconDisc extends StatelessWidget {
|
||||||
|
const _IconDisc({required this.background, required this.child});
|
||||||
|
|
||||||
|
final Color background;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: background),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CardText extends StatelessWidget {
|
||||||
|
const _CardText({
|
||||||
|
required this.label,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.color,
|
||||||
|
required this.subtitleColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String subtitle;
|
||||||
|
final Color color;
|
||||||
|
final Color subtitleColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 19,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(subtitle, style: TextStyle(color: subtitleColor, fontSize: 13)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single seed pictogram scattered onto the watermark: [ax]/[ay] are
|
||||||
|
/// fractional positions (0..1) within the layer, [angle] a slight tilt so the
|
||||||
|
/// scatter feels hand-strewn rather than gridded (like screen 00).
|
||||||
|
class _ScatteredGlyph {
|
||||||
|
const _ScatteredGlyph(this.char, this.ax, this.ay, this.size, this.angle);
|
||||||
|
|
||||||
|
final String char;
|
||||||
|
final double ax;
|
||||||
|
final double ay;
|
||||||
|
final double size;
|
||||||
|
final double angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A faint, disordered scatter of seed pictograms behind the menu (like screen
|
||||||
|
/// 00): glyphs strewn around the edges at staggered positions and tilts,
|
||||||
|
/// positioned by fraction so it holds up at any screen size.
|
||||||
class _SeedWatermark extends StatelessWidget {
|
class _SeedWatermark extends StatelessWidget {
|
||||||
const _SeedWatermark();
|
const _SeedWatermark();
|
||||||
|
|
||||||
static const _glyphs = [
|
static const _glyphs = [
|
||||||
SeedGlyphs.jars,
|
_ScatteredGlyph(SeedGlyphs.sack, 0.06, 0.14, 62, -0.18),
|
||||||
SeedGlyphs.envelope,
|
_ScatteredGlyph(SeedGlyphs.envelope, 0.82, 0.09, 48, 0.22),
|
||||||
SeedGlyphs.bigSpoon,
|
_ScatteredGlyph(SeedGlyphs.scattered, 0.70, 0.30, 44, -0.12),
|
||||||
SeedGlyphs.jar,
|
_ScatteredGlyph(SeedGlyphs.jar, 0.16, 0.44, 40, 0.15),
|
||||||
SeedGlyphs.sack,
|
_ScatteredGlyph(SeedGlyphs.jars, 0.85, 0.66, 44, 0.18),
|
||||||
SeedGlyphs.scattered,
|
_ScatteredGlyph(SeedGlyphs.smallSpoon, 0.10, 0.74, 48, -0.22),
|
||||||
SeedGlyphs.mug,
|
_ScatteredGlyph(SeedGlyphs.pouring, 0.40, 0.90, 52, 0.10),
|
||||||
SeedGlyphs.smallSpoon,
|
_ScatteredGlyph(SeedGlyphs.mug, 0.72, 0.88, 60, -0.14),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: Opacity(
|
child: LayoutBuilder(
|
||||||
opacity: 0.06,
|
builder: (context, constraints) {
|
||||||
child: Align(
|
final w = constraints.maxWidth;
|
||||||
alignment: Alignment.bottomCenter,
|
final h = constraints.maxHeight;
|
||||||
child: Padding(
|
return Stack(
|
||||||
padding: const EdgeInsets.only(bottom: 24),
|
children: [
|
||||||
child: Wrap(
|
for (final g in _glyphs)
|
||||||
spacing: 28,
|
Positioned(
|
||||||
runSpacing: 18,
|
left: w * g.ax,
|
||||||
alignment: WrapAlignment.center,
|
top: h * g.ay,
|
||||||
children: [
|
child: Opacity(
|
||||||
for (final g in _glyphs)
|
opacity: 0.08,
|
||||||
SeedGlyph(g, size: 64, color: seedGreenDark),
|
child: Transform.rotate(
|
||||||
],
|
angle: g.angle,
|
||||||
),
|
child: SeedGlyph(
|
||||||
),
|
g.char,
|
||||||
),
|
size: g.size,
|
||||||
|
color: seedGreenDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,14 @@ class InventoryListScreen extends StatelessWidget {
|
||||||
key: const Key('inventory.search'),
|
key: const Key('inventory.search'),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: t.inventory.searchHint,
|
hintText: t.inventory.searchHint,
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search, color: seedMuted),
|
||||||
|
hintStyle: const TextStyle(color: seedMuted),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: Colors.white,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(28),
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -123,9 +125,10 @@ class _CategoryHeader extends StatelessWidget {
|
||||||
padding: const EdgeInsets.fromLTRB(16, 18, 16, 6),
|
padding: const EdgeInsets.fromLTRB(16, 18, 16, 6),
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontSize: 16,
|
||||||
color: seedGreenDark,
|
fontWeight: FontWeight.w500,
|
||||||
|
color: seedGreen,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -154,7 +157,7 @@ class _VarietyTile extends StatelessWidget {
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
color: Colors.black38,
|
color: seedMuted,
|
||||||
tooltip: context.t.common.edit,
|
tooltip: context.t.common.edit,
|
||||||
onPressed: open,
|
onPressed: open,
|
||||||
),
|
),
|
||||||
|
|
@ -183,8 +186,8 @@ class _Avatar extends StatelessWidget {
|
||||||
? '?'
|
? '?'
|
||||||
: trimmed.substring(0, 1).toUpperCase();
|
: trimmed.substring(0, 1).toUpperCase();
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: seedGreen.withValues(alpha: 0.15),
|
backgroundColor: seedAvatar,
|
||||||
foregroundColor: seedGreenDark,
|
foregroundColor: seedOnAvatar,
|
||||||
child: Text(initial),
|
child: Text(initial),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ import '../i18n/strings.g.dart';
|
||||||
QuantityKind.plant => (t.unit.plant.singular, t.unit.plant.plural),
|
QuantityKind.plant => (t.unit.plant.singular, t.unit.plant.plural),
|
||||||
QuantityKind.pot => (t.unit.pot.singular, t.unit.pot.plural),
|
QuantityKind.pot => (t.unit.pot.singular, t.unit.pot.plural),
|
||||||
QuantityKind.tray => (t.unit.tray.singular, t.unit.tray.plural),
|
QuantityKind.tray => (t.unit.tray.singular, t.unit.tray.plural),
|
||||||
|
QuantityKind.seedling => (t.unit.seedling.singular, t.unit.seedling.plural),
|
||||||
|
QuantityKind.tree => (t.unit.tree.singular, t.unit.tree.plural),
|
||||||
|
QuantityKind.cutting => (t.unit.cutting.singular, t.unit.cutting.plural),
|
||||||
QuantityKind.grams => (t.unit.grams.singular, t.unit.grams.plural),
|
QuantityKind.grams => (t.unit.grams.singular, t.unit.grams.plural),
|
||||||
QuantityKind.count => (t.unit.count.singular, t.unit.count.plural),
|
QuantityKind.count => (t.unit.count.singular, t.unit.count.plural),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:commons_core/commons_core.dart';
|
import 'package:commons_core/commons_core.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
|
|
||||||
import '../db/enums.dart';
|
import '../db/enums.dart';
|
||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
|
|
@ -28,17 +27,37 @@ const seedScaleKinds = <QuantityKind>[
|
||||||
QuantityKind.bunch,
|
QuantityKind.bunch,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Units for a plant/seedling lot.
|
/// The amount units offered for each lot form. Living forms are counted as
|
||||||
const plantScaleKinds = <QuantityKind>[
|
/// whole individuals (their packaging is a separate [Presentation]); [bulb] also
|
||||||
QuantityKind.plant,
|
/// allows grams. [pot]/[tray] are intentionally not offered anymore.
|
||||||
QuantityKind.pot,
|
List<QuantityKind> kindsForType(LotType type) => switch (type) {
|
||||||
QuantityKind.tray,
|
LotType.seed => seedScaleKinds,
|
||||||
];
|
LotType.seedling => const [QuantityKind.seedling],
|
||||||
|
LotType.plant => const [QuantityKind.plant],
|
||||||
|
LotType.tree => const [QuantityKind.tree],
|
||||||
|
LotType.bulb => const [
|
||||||
|
QuantityKind.bulb,
|
||||||
|
QuantityKind.tuber,
|
||||||
|
QuantityKind.grams,
|
||||||
|
],
|
||||||
|
LotType.cutting => const [QuantityKind.cutting],
|
||||||
|
};
|
||||||
|
|
||||||
List<QuantityKind> kindsForType(LotType type) =>
|
/// The human label for a lot form.
|
||||||
type == LotType.seed ? seedScaleKinds : plantScaleKinds;
|
String lotTypeLabel(Translations t, LotType type) => switch (type) {
|
||||||
|
LotType.seed => t.lotType.seed,
|
||||||
|
LotType.seedling => t.lotType.seedling,
|
||||||
|
LotType.plant => t.lotType.plant,
|
||||||
|
LotType.tree => t.lotType.tree,
|
||||||
|
LotType.bulb => t.lotType.bulb,
|
||||||
|
LotType.cutting => t.lotType.cutting,
|
||||||
|
};
|
||||||
|
|
||||||
/// Seed vs plant/seedling segmented selector.
|
/// Living forms whose packaging ([Presentation]) is worth asking about.
|
||||||
|
bool typeHasPresentation(LotType type) =>
|
||||||
|
type == LotType.seedling || type == LotType.plant || type == LotType.tree;
|
||||||
|
|
||||||
|
/// Form selector: a wrap of choice chips, one per [LotType].
|
||||||
class LotTypeSelector extends StatelessWidget {
|
class LotTypeSelector extends StatelessWidget {
|
||||||
const LotTypeSelector({
|
const LotTypeSelector({
|
||||||
required this.value,
|
required this.value,
|
||||||
|
|
@ -52,26 +71,65 @@ class LotTypeSelector extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = context.t;
|
final t = context.t;
|
||||||
return SegmentedButton<LotType>(
|
return Wrap(
|
||||||
segments: [
|
spacing: 8,
|
||||||
ButtonSegment(
|
runSpacing: 8,
|
||||||
value: LotType.seed,
|
children: [
|
||||||
label: Text(t.lotType.seed),
|
for (final type in LotType.values)
|
||||||
icon: const SeedGlyph(SeedGlyphs.jars, size: 18),
|
ChoiceChip(
|
||||||
),
|
key: Key('lotType.${type.name}'),
|
||||||
ButtonSegment(
|
selected: value == type,
|
||||||
value: LotType.plant,
|
onSelected: (_) => onChanged(type),
|
||||||
label: Text(t.lotType.plant),
|
avatar: type == LotType.seed
|
||||||
icon: const Icon(Symbols.potted_plant, size: 18),
|
? const SeedGlyph(SeedGlyphs.jars, size: 18)
|
||||||
),
|
: Icon(iconForLotType(type), size: 18),
|
||||||
|
label: Text(lotTypeLabel(t, type)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
selected: {value},
|
|
||||||
showSelectedIcon: false,
|
|
||||||
onSelectionChanged: (s) => onChanged(s.first),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional packaging selector for living lots. Tapping the selected chip again
|
||||||
|
/// clears it (packaging is optional).
|
||||||
|
class PresentationSelector extends StatelessWidget {
|
||||||
|
const PresentationSelector({
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Presentation? value;
|
||||||
|
final ValueChanged<Presentation?> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (final p in Presentation.values)
|
||||||
|
ChoiceChip(
|
||||||
|
key: Key('presentation.${p.name}'),
|
||||||
|
selected: value == p,
|
||||||
|
onSelected: (sel) => onChanged(sel ? p : null),
|
||||||
|
label: Text(presentationLabel(t, p)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The human label for a packaging [Presentation].
|
||||||
|
String presentationLabel(Translations t, Presentation p) => switch (p) {
|
||||||
|
Presentation.pot => t.presentation.pot,
|
||||||
|
Presentation.tray => t.presentation.tray,
|
||||||
|
Presentation.plug => t.presentation.plug,
|
||||||
|
Presentation.bareRoot => t.presentation.bareRoot,
|
||||||
|
Presentation.rootBall => t.presentation.rootBall,
|
||||||
|
};
|
||||||
|
|
||||||
/// A horizontal scale of large unit icons + an optional number stepper. Emits a
|
/// A horizontal scale of large unit icons + an optional number stepper. Emits a
|
||||||
/// [Quantity] (or null while nothing is picked) via [onChanged].
|
/// [Quantity] (or null while nothing is picked) via [onChanged].
|
||||||
class QuantityPicker extends StatefulWidget {
|
class QuantityPicker extends StatefulWidget {
|
||||||
|
|
|
||||||
|
|
@ -39,77 +39,81 @@ class QuickAddSheet extends StatelessWidget {
|
||||||
top: 16,
|
top: 16,
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
Text(
|
children: [
|
||||||
t.quickAdd.title,
|
Text(
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
t.quickAdd.title,
|
||||||
),
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
const SizedBox(height: 12),
|
|
||||||
TextField(
|
|
||||||
key: const Key('quickAdd.labelField'),
|
|
||||||
autofocus: true,
|
|
||||||
textInputAction: TextInputAction.done,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: t.quickAdd.labelField,
|
|
||||||
errorText: state.showLabelError
|
|
||||||
? t.quickAdd.labelRequired
|
|
||||||
: null,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
),
|
||||||
onChanged: cubit.labelChanged,
|
const SizedBox(height: 12),
|
||||||
onSubmitted: (_) => cubit.submit(),
|
TextField(
|
||||||
),
|
key: const Key('quickAdd.labelField'),
|
||||||
const SizedBox(height: 16),
|
autofocus: true,
|
||||||
LotTypeSelector(
|
textInputAction: TextInputAction.done,
|
||||||
value: state.lotType,
|
decoration: InputDecoration(
|
||||||
onChanged: (type) {
|
labelText: t.quickAdd.labelField,
|
||||||
cubit
|
errorText: state.showLabelError
|
||||||
..setLotType(type)
|
? t.quickAdd.labelRequired
|
||||||
..setQuantity(null);
|
: null,
|
||||||
},
|
border: const OutlineInputBorder(
|
||||||
),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
Text(
|
|
||||||
t.quickAdd.quantity,
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
QuantityPicker(
|
|
||||||
type: state.lotType,
|
|
||||||
value: state.quantity,
|
|
||||||
onChanged: cubit.setQuantity,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed: cubit.toggleExpanded,
|
|
||||||
icon: Icon(
|
|
||||||
state.expanded ? Icons.expand_less : Icons.expand_more,
|
|
||||||
),
|
),
|
||||||
label: Text(t.quickAdd.more),
|
onChanged: cubit.labelChanged,
|
||||||
|
onSubmitted: (_) => cubit.submit(),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
if (state.expanded) const _MoreSection(),
|
LotTypeSelector(
|
||||||
const SizedBox(height: 8),
|
value: state.lotType,
|
||||||
Row(
|
onChanged: (type) {
|
||||||
children: [
|
cubit
|
||||||
TextButton(
|
..setLotType(type)
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
..setQuantity(null);
|
||||||
child: Text(t.quickAdd.cancel),
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
t.quickAdd.quantity,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
QuantityPicker(
|
||||||
|
type: state.lotType,
|
||||||
|
value: state.quantity,
|
||||||
|
onChanged: cubit.setQuantity,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: cubit.toggleExpanded,
|
||||||
|
icon: Icon(
|
||||||
|
state.expanded ? Icons.expand_less : Icons.expand_more,
|
||||||
|
),
|
||||||
|
label: Text(t.quickAdd.more),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
),
|
||||||
FilledButton(
|
if (state.expanded) const _MoreSection(),
|
||||||
key: const Key('quickAdd.save'),
|
const SizedBox(height: 8),
|
||||||
onPressed: state.submitting ? null : cubit.submit,
|
Row(
|
||||||
child: Text(t.quickAdd.save),
|
children: [
|
||||||
),
|
TextButton(
|
||||||
],
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
child: Text(t.quickAdd.cancel),
|
||||||
],
|
),
|
||||||
|
const Spacer(),
|
||||||
|
FilledButton(
|
||||||
|
key: const Key('quickAdd.save'),
|
||||||
|
onPressed: state.submitting ? null : cubit.submit,
|
||||||
|
child: Text(t.quickAdd.save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import 'package:commons_core/commons_core.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
|
||||||
|
import '../db/enums.dart';
|
||||||
|
|
||||||
/// Renders a glyph from the bundled `seedks` icon font, whose characters `a`–`k`
|
/// Renders a glyph from the bundled `seedks` icon font, whose characters `a`–`k`
|
||||||
/// map to seed-themed pictograms (jar, packet, spoon…). See
|
/// map to seed-themed pictograms (jar, packet, spoon…). See
|
||||||
/// docs/mockups/seedks-glyphs.png.
|
/// docs/mockups/seedks-glyphs.png.
|
||||||
|
|
@ -61,18 +63,36 @@ String? seedGlyphForKind(QuantityKind kind) => switch (kind) {
|
||||||
IconData _materialIconForKind(QuantityKind kind) => switch (kind) {
|
IconData _materialIconForKind(QuantityKind kind) => switch (kind) {
|
||||||
QuantityKind.grams => Icons.scale_outlined,
|
QuantityKind.grams => Icons.scale_outlined,
|
||||||
QuantityKind.count => Icons.tag,
|
QuantityKind.count => Icons.tag,
|
||||||
QuantityKind.cob || QuantityKind.ear => Icons.grass,
|
// Seed/fruit forms — each a distinct icon so they're told apart at a glance.
|
||||||
QuantityKind.head || QuantityKind.seedHead => Icons.filter_vintage_outlined,
|
QuantityKind.cob => Symbols.grass, // mazorca
|
||||||
QuantityKind.fruit => Icons.local_florist_outlined,
|
QuantityKind.ear => Symbols.grain, // espiga
|
||||||
QuantityKind.plant => Symbols.potted_plant,
|
QuantityKind.head => Symbols.filter_vintage, // cabezuela
|
||||||
QuantityKind.pot => Icons.yard_outlined,
|
QuantityKind.seedHead => Symbols.spa,
|
||||||
QuantityKind.tray => Icons.grid_on_outlined,
|
QuantityKind.fruit => Symbols.nutrition, // fruto (apple)
|
||||||
QuantityKind.bulb ||
|
QuantityKind.bulb => Symbols.spa, // bulbo
|
||||||
QuantityKind.tuber ||
|
QuantityKind.tuber => Symbols.egg, // tubérculo
|
||||||
QuantityKind.bunch => Icons.spa_outlined,
|
QuantityKind.bunch => Symbols.local_florist, // manojo
|
||||||
|
// Living material counted as individuals.
|
||||||
|
QuantityKind.seedling => Symbols.potted_plant,
|
||||||
|
QuantityKind.plant => Symbols.yard,
|
||||||
|
QuantityKind.tree => Symbols.park,
|
||||||
|
QuantityKind.cutting => Symbols.content_cut,
|
||||||
|
QuantityKind.pot => Symbols.yard,
|
||||||
|
QuantityKind.tray => Symbols.grid_on,
|
||||||
_ => Icons.eco_outlined,
|
_ => Icons.eco_outlined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The icon for a lot's [LotType] form (used in the form selector and lot chip),
|
||||||
|
/// distinct across all forms so the selector reads clearly.
|
||||||
|
IconData iconForLotType(LotType type) => switch (type) {
|
||||||
|
LotType.seed => Symbols.eco, // overridden by a seed glyph where shown
|
||||||
|
LotType.seedling => Symbols.potted_plant,
|
||||||
|
LotType.plant => Symbols.local_florist,
|
||||||
|
LotType.tree => Symbols.park,
|
||||||
|
LotType.bulb => Symbols.spa,
|
||||||
|
LotType.cutting => Symbols.content_cut,
|
||||||
|
};
|
||||||
|
|
||||||
/// A consistent icon for a [QuantityKind]: the seed glyph where one exists,
|
/// A consistent icon for a [QuantityKind]: the seed glyph where one exists,
|
||||||
/// otherwise a Material fallback.
|
/// otherwise a Material fallback.
|
||||||
class QuantityKindIcon extends StatelessWidget {
|
class QuantityKindIcon extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
@ -35,8 +36,10 @@ class SettingsScreen extends StatelessWidget {
|
||||||
_SectionHeader(t.settings.about),
|
_SectionHeader(t.settings.about),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Image.asset('assets/logo.png', width: 32),
|
leading: Image.asset('assets/logo.png', width: 32),
|
||||||
title: Text(t.app.title),
|
title: Text(t.settings.aboutOpen),
|
||||||
subtitle: Text(t.settings.aboutText),
|
subtitle: Text(t.settings.aboutText),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => context.push('/about'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -56,8 +59,9 @@ class _SectionHeader extends StatelessWidget {
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
color: seedGreenDark,
|
color: seedGreen,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,104 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Seed-green palette drawn from the mockups (docs/mockups): a green app bar and
|
/// Material 3 palette from the Seedees v2 redesign
|
||||||
/// a pale-green canvas.
|
/// (docs/mockups/seedees_material3.html): green app bars over a pale-green
|
||||||
const seedGreen = Color(0xFF3C8B2E);
|
/// canvas, a saturated green for primary actions (FAB, buttons, links, category
|
||||||
const seedGreenDark = Color(0xFF1B5E20);
|
/// headers) and a soft green container for selected/tonal surfaces.
|
||||||
const seedCanvas = Color(0xFFEAF4E2);
|
const seedGreen = Color(0xFF2F7D34); // primary (actions, links, headers)
|
||||||
const seedLink = Color(0xFF1E88C0);
|
const seedGreenDark = Color(0xFF22521E); // status bar / darker green
|
||||||
|
const seedAppBar = Color(0xFF3E8C38); // app-bar green
|
||||||
|
const seedCanvas = Color(0xFFE7F1E0); // surface / scaffold
|
||||||
|
const seedPrimaryContainer = Color(0xFFD3E8C8); // tonal green container
|
||||||
|
const seedOnPrimaryContainer = Color(0xFF2F6D2A);
|
||||||
|
const seedField = Color(0xFFF3F7EE); // outlined field fill
|
||||||
|
const seedAvatar = Color(0xFFCFE7C3); // list avatar disc
|
||||||
|
const seedOnAvatar = Color(0xFF3B7A34);
|
||||||
|
const seedOutline = Color(0xFFC3CBB6);
|
||||||
|
const seedDivider = Color(0xFFE4E9DD);
|
||||||
|
const seedTitle = Color(0xFF1B2416); // heading text on canvas
|
||||||
|
const seedOnSurface = Color(0xFF1B2416);
|
||||||
|
const seedOnSurfaceVariant = Color(0xFF5C6B52); // scientific names, secondary
|
||||||
|
const seedMuted = Color(0xFF8A9880); // hints, list edit icon
|
||||||
|
const seedRating = Color(0xFFE8A200); // stars
|
||||||
|
|
||||||
ThemeData buildTaneTheme() {
|
ThemeData buildTaneTheme() {
|
||||||
final scheme = ColorScheme.fromSeed(
|
final scheme =
|
||||||
seedColor: seedGreen,
|
ColorScheme.fromSeed(
|
||||||
brightness: Brightness.light,
|
seedColor: seedGreen,
|
||||||
);
|
brightness: Brightness.light,
|
||||||
|
).copyWith(
|
||||||
|
primary: seedGreen,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
primaryContainer: seedPrimaryContainer,
|
||||||
|
onPrimaryContainer: seedOnPrimaryContainer,
|
||||||
|
secondaryContainer: seedPrimaryContainer,
|
||||||
|
onSecondaryContainer: seedOnPrimaryContainer,
|
||||||
|
tertiary: seedRating,
|
||||||
|
onTertiary: Colors.white,
|
||||||
|
surface: seedCanvas,
|
||||||
|
onSurface: seedOnSurface,
|
||||||
|
surfaceContainerHighest: seedField,
|
||||||
|
onSurfaceVariant: seedOnSurfaceVariant,
|
||||||
|
outline: seedMuted,
|
||||||
|
outlineVariant: seedOutline,
|
||||||
|
);
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: scheme,
|
colorScheme: scheme,
|
||||||
scaffoldBackgroundColor: seedCanvas,
|
scaffoldBackgroundColor: seedCanvas,
|
||||||
|
// Green app bars — aligned with the v2 mockups and the real app.
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: seedGreen,
|
backgroundColor: seedAppBar,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
|
iconTheme: IconThemeData(color: Colors.white),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
// Squircle FAB (20px radius) as in the mockups, in the saturated primary.
|
||||||
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
backgroundColor: seedGreen,
|
backgroundColor: seedGreen,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
),
|
||||||
|
drawerTheme: const DrawerThemeData(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
bottomSheetTheme: const BottomSheetThemeData(
|
bottomSheetTheme: const BottomSheetThemeData(
|
||||||
backgroundColor: seedCanvas,
|
backgroundColor: seedCanvas,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(26)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Pill "Save" actions across sheets and dialogs, as in the v3 mockups.
|
||||||
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: seedGreen,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(foregroundColor: seedGreen),
|
||||||
|
),
|
||||||
|
// Green floating labels on the edit-seed / add-batch text fields. (Borders
|
||||||
|
// are left to each field so the inventory search keeps its borderless pill.)
|
||||||
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
|
floatingLabelStyle: TextStyle(color: seedGreen),
|
||||||
|
),
|
||||||
|
dividerTheme: const DividerThemeData(
|
||||||
|
space: 1,
|
||||||
|
thickness: 1,
|
||||||
|
color: seedDivider,
|
||||||
),
|
),
|
||||||
dividerTheme: const DividerThemeData(space: 1, thickness: 1),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../data/species_repository.dart';
|
import '../data/species_repository.dart';
|
||||||
|
|
@ -198,6 +197,7 @@ String _lotSubtitle(Translations t, VarietyLot lot) {
|
||||||
else
|
else
|
||||||
t.detail.noYear,
|
t.detail.noYear,
|
||||||
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
if (lot.quantity != null) quantityDisplay(t, lot.quantity!),
|
||||||
|
if (lot.presentation != null) presentationLabel(t, lot.presentation!),
|
||||||
];
|
];
|
||||||
return parts.join(' · ');
|
return parts.join(' · ');
|
||||||
}
|
}
|
||||||
|
|
@ -214,8 +214,9 @@ class _LotTypeChip extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = context.t;
|
final t = context.t;
|
||||||
final isPlant = type == LotType.plant;
|
final isSeed = type == LotType.seed;
|
||||||
final color = isPlant ? const Color(0xFF00796B) : seedGreenDark;
|
// Seed lots read green; living/vegetative forms read teal.
|
||||||
|
final color = isSeed ? seedGreenDark : const Color(0xFF00796B);
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -225,13 +226,13 @@ class _LotTypeChip extends StatelessWidget {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (isPlant)
|
if (isSeed)
|
||||||
Icon(Symbols.potted_plant, size: 14, color: color)
|
SeedGlyph(SeedGlyphs.jars, size: 14, color: color)
|
||||||
else
|
else
|
||||||
SeedGlyph(SeedGlyphs.jars, size: 14, color: color),
|
Icon(iconForLotType(type), size: 14, color: color),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
isPlant ? t.lotType.plant : t.lotType.seed,
|
lotTypeLabel(t, type),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: color,
|
color: color,
|
||||||
|
|
@ -323,7 +324,9 @@ Future<void> _showGerminationSheet(
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: t.germination.germinated,
|
labelText: t.germination.germinated,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -335,7 +338,9 @@ Future<void> _showGerminationSheet(
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: t.germination.sampleSize,
|
labelText: t.germination.sampleSize,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -504,7 +509,9 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
||||||
controller: _name,
|
controller: _name,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: t.editVariety.name,
|
labelText: t.editVariety.name,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -515,7 +522,9 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
||||||
labelText: t.editVariety.species,
|
labelText: t.editVariety.species,
|
||||||
hintText: t.editVariety.speciesHint,
|
hintText: t.editVariety.speciesHint,
|
||||||
prefixIcon: const Icon(Icons.eco_outlined),
|
prefixIcon: const Icon(Icons.eco_outlined),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onChanged: _onSpeciesQuery,
|
onChanged: _onSpeciesQuery,
|
||||||
),
|
),
|
||||||
|
|
@ -532,7 +541,9 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
||||||
controller: _category,
|
controller: _category,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: t.editVariety.category,
|
labelText: t.editVariety.category,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
@ -542,7 +553,9 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: t.editVariety.notes,
|
labelText: t.editVariety.notes,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -577,6 +590,7 @@ Future<void> _showLotSheet(
|
||||||
var selectedMonth = existing?.harvestMonth;
|
var selectedMonth = existing?.harvestMonth;
|
||||||
var selectedType = existing?.type ?? LotType.seed;
|
var selectedType = existing?.type ?? LotType.seed;
|
||||||
var selectedQuantity = existing?.quantity;
|
var selectedQuantity = existing?.quantity;
|
||||||
|
var selectedPresentation = existing?.presentation;
|
||||||
final editing = existing != null;
|
final editing = existing != null;
|
||||||
return showModalBottomSheet<void>(
|
return showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -593,58 +607,84 @@ Future<void> _showLotSheet(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Flexible(
|
||||||
children: [
|
child: SingleChildScrollView(
|
||||||
Expanded(
|
child: Column(
|
||||||
child: Text(
|
mainAxisSize: MainAxisSize.min,
|
||||||
editing ? t.detail.editLot : t.addLot.title,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
style: Theme.of(sheetContext).textTheme.titleLarge,
|
children: [
|
||||||
),
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
editing ? t.detail.editLot : t.addLot.title,
|
||||||
|
style: Theme.of(sheetContext).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (editing)
|
||||||
|
IconButton(
|
||||||
|
key: const Key('lot.delete'),
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
tooltip: t.common.delete,
|
||||||
|
onPressed: () {
|
||||||
|
cubit.deleteLot(existing.id);
|
||||||
|
Navigator.of(sheetContext).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
LotTypeSelector(
|
||||||
|
value: selectedType,
|
||||||
|
onChanged: (type) => setState(() {
|
||||||
|
selectedType = type;
|
||||||
|
selectedQuantity = null;
|
||||||
|
if (!typeHasPresentation(type)) {
|
||||||
|
selectedPresentation = null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
if (typeHasPresentation(selectedType)) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
t.presentation.title,
|
||||||
|
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
PresentationSelector(
|
||||||
|
value: selectedPresentation,
|
||||||
|
onChanged: (p) =>
|
||||||
|
setState(() => selectedPresentation = p),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
t.addLot.year,
|
||||||
|
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
HarvestDateField(
|
||||||
|
year: selectedYear,
|
||||||
|
month: selectedMonth,
|
||||||
|
onChanged: (year, month) => setState(() {
|
||||||
|
selectedYear = year;
|
||||||
|
selectedMonth = month;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
t.addLot.quantity,
|
||||||
|
style: Theme.of(sheetContext).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
QuantityPicker(
|
||||||
|
type: selectedType,
|
||||||
|
value: selectedQuantity,
|
||||||
|
onChanged: (q) => selectedQuantity = q,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (editing)
|
),
|
||||||
IconButton(
|
|
||||||
key: const Key('lot.delete'),
|
|
||||||
icon: const Icon(Icons.delete_outline),
|
|
||||||
tooltip: t.common.delete,
|
|
||||||
onPressed: () {
|
|
||||||
cubit.deleteLot(existing.id);
|
|
||||||
Navigator.of(sheetContext).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
LotTypeSelector(
|
|
||||||
value: selectedType,
|
|
||||||
onChanged: (type) => setState(() {
|
|
||||||
selectedType = type;
|
|
||||||
selectedQuantity = null;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
t.addLot.year,
|
|
||||||
style: Theme.of(sheetContext).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
HarvestDateField(
|
|
||||||
year: selectedYear,
|
|
||||||
month: selectedMonth,
|
|
||||||
onChanged: (year, month) => setState(() {
|
|
||||||
selectedYear = year;
|
|
||||||
selectedMonth = month;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
t.addLot.quantity,
|
|
||||||
style: Theme.of(sheetContext).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
QuantityPicker(
|
|
||||||
type: selectedType,
|
|
||||||
value: selectedQuantity,
|
|
||||||
onChanged: (q) => selectedQuantity = q,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -664,6 +704,7 @@ Future<void> _showLotSheet(
|
||||||
year: selectedYear,
|
year: selectedYear,
|
||||||
month: selectedMonth,
|
month: selectedMonth,
|
||||||
quantity: selectedQuantity,
|
quantity: selectedQuantity,
|
||||||
|
presentation: selectedPresentation,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
cubit.addLot(
|
cubit.addLot(
|
||||||
|
|
@ -671,6 +712,7 @@ Future<void> _showLotSheet(
|
||||||
year: selectedYear,
|
year: selectedYear,
|
||||||
month: selectedMonth,
|
month: selectedMonth,
|
||||||
quantity: selectedQuantity,
|
quantity: selectedQuantity,
|
||||||
|
presentation: selectedPresentation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Navigator.of(sheetContext).pop();
|
Navigator.of(sheetContext).pop();
|
||||||
|
|
@ -807,12 +849,25 @@ class _DetailHeader extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (detail.category != null)
|
if (detail.category != null)
|
||||||
Text(
|
Row(
|
||||||
detail.category!,
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: const TextStyle(
|
children: [
|
||||||
color: seedLink,
|
const Icon(
|
||||||
fontWeight: FontWeight.w600,
|
Icons.category_outlined,
|
||||||
),
|
size: 17,
|
||||||
|
color: seedGreen,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
detail.category!,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: seedGreen,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (detail.scientificName != null) ...[
|
if (detail.scientificName != null) ...[
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
|
|
@ -890,37 +945,27 @@ class _PhotoGalleryState extends State<_PhotoGallery> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
// Tap a photo to open the full-screen viewer, where you can set the
|
||||||
|
// cover or delete it. The first photo is the cover.
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 140,
|
height: 140,
|
||||||
child: Stack(
|
child: ClipRRect(
|
||||||
children: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
ClipRRect(
|
child: PageView.builder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
controller: _controller,
|
||||||
child: PageView.builder(
|
itemCount: photos.length,
|
||||||
controller: _controller,
|
onPageChanged: (i) => setState(() => _page = i),
|
||||||
itemCount: photos.length,
|
itemBuilder: (_, i) => GestureDetector(
|
||||||
onPageChanged: (i) => setState(() => _page = i),
|
key: Key('photo.thumb.$i'),
|
||||||
itemBuilder: (_, i) => GestureDetector(
|
onTap: () => _openPhotoViewer(context, cubit, i),
|
||||||
onTap: () => _openPhotoViewer(context, photos, i),
|
child: Image.memory(
|
||||||
child: Image.memory(
|
photos[i].bytes,
|
||||||
photos[i].bytes,
|
width: 140,
|
||||||
width: 140,
|
height: 140,
|
||||||
height: 140,
|
fit: BoxFit.cover,
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
top: 2,
|
|
||||||
right: 2,
|
|
||||||
child: _CircleIconButton(
|
|
||||||
icon: Icons.close,
|
|
||||||
tooltip: context.t.common.delete,
|
|
||||||
onPressed: () => cubit.removePhoto(photos[page].id),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
|
@ -946,33 +991,6 @@ class _PhotoGalleryState extends State<_PhotoGallery> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CircleIconButton extends StatelessWidget {
|
|
||||||
const _CircleIconButton({
|
|
||||||
required this.icon,
|
|
||||||
required this.onPressed,
|
|
||||||
this.tooltip,
|
|
||||||
});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
final String? tooltip;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Colors.black45,
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
child: IconButton(
|
|
||||||
key: const Key('photo.delete'),
|
|
||||||
icon: Icon(icon, size: 18, color: Colors.white),
|
|
||||||
tooltip: tooltip,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
onPressed: onPressed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Dots extends StatelessWidget {
|
class _Dots extends StatelessWidget {
|
||||||
const _Dots({required this.count, required this.active, this.onTap});
|
const _Dots({required this.count, required this.active, this.onTap});
|
||||||
|
|
||||||
|
|
@ -1009,38 +1027,141 @@ class _Dots extends StatelessWidget {
|
||||||
|
|
||||||
void _openPhotoViewer(
|
void _openPhotoViewer(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<VarietyPhoto> photos,
|
VarietyDetailCubit cubit,
|
||||||
int initialIndex,
|
int initialIndex,
|
||||||
) {
|
) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute<void>(
|
MaterialPageRoute<void>(
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
builder: (_) => _PhotoViewer(photos: photos, initialIndex: initialIndex),
|
// Re-provide the cubit so the pushed route can set the cover / delete and
|
||||||
|
// rebuild live as the photo list reorders or shrinks.
|
||||||
|
builder: (_) => BlocProvider.value(
|
||||||
|
value: cubit,
|
||||||
|
child: _PhotoViewer(initialIndex: initialIndex),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Full-screen, swipeable, pinch-to-zoom photo viewer.
|
/// Full-screen, swipeable, pinch-to-zoom photo viewer. Its app bar hosts the
|
||||||
class _PhotoViewer extends StatelessWidget {
|
/// per-photo actions (set as cover, delete) — the thumbnail gallery only opens
|
||||||
const _PhotoViewer({required this.photos, required this.initialIndex});
|
/// it. Stays in sync with the cubit, so deleting the last photo closes it.
|
||||||
|
class _PhotoViewer extends StatefulWidget {
|
||||||
|
const _PhotoViewer({required this.initialIndex});
|
||||||
|
|
||||||
final List<VarietyPhoto> photos;
|
|
||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PhotoViewer> createState() => _PhotoViewerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhotoViewerState extends State<_PhotoViewer> {
|
||||||
|
late final PageController _controller = PageController(
|
||||||
|
initialPage: widget.initialIndex,
|
||||||
|
);
|
||||||
|
late int _page = widget.initialIndex;
|
||||||
|
bool _popped = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmDelete(
|
||||||
|
VarietyDetailCubit cubit,
|
||||||
|
VarietyPhoto photo,
|
||||||
|
) async {
|
||||||
|
final t = context.t;
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
content: Text(t.photo.deleteConfirm),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||||
|
child: Text(t.common.cancel),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
key: const Key('photo.deleteConfirm'),
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
|
child: Text(t.common.delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed ?? false) await cubit.removePhoto(photo.id);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
final cubit = context.read<VarietyDetailCubit>();
|
||||||
backgroundColor: Colors.black,
|
return BlocBuilder<VarietyDetailCubit, VarietyDetailState>(
|
||||||
appBar: AppBar(
|
builder: (context, state) {
|
||||||
backgroundColor: Colors.black,
|
final photos = state.detail?.photos ?? const <VarietyPhoto>[];
|
||||||
foregroundColor: Colors.white,
|
// Nothing left to show (last photo deleted / variety gone): close once.
|
||||||
elevation: 0,
|
// The guard stops the post-frame callback re-arming every rebuild.
|
||||||
),
|
if (photos.isEmpty) {
|
||||||
body: PageView.builder(
|
if (!_popped) {
|
||||||
controller: PageController(initialPage: initialIndex),
|
_popped = true;
|
||||||
itemCount: photos.length,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
itemBuilder: (_, i) => _ZoomablePhoto(bytes: photos[i].bytes),
|
if (mounted) Navigator.of(context).maybePop();
|
||||||
),
|
});
|
||||||
|
}
|
||||||
|
return const Scaffold(backgroundColor: Colors.black);
|
||||||
|
}
|
||||||
|
final page = _page.clamp(0, photos.length - 1);
|
||||||
|
// A delete can leave the pager past the end: snap it back. jumpToPage
|
||||||
|
// fires onPageChanged, which updates _page — so no setState here (that
|
||||||
|
// would re-arm the callback and spin the frame loop).
|
||||||
|
if (page != _page && _controller.hasClients) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted && _controller.hasClients) _controller.jumpToPage(page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
final isCover = page == 0;
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
actions: [
|
||||||
|
if (photos.length > 1)
|
||||||
|
IconButton(
|
||||||
|
key: const Key('photo.cover'),
|
||||||
|
icon: Icon(isCover ? Icons.star : Icons.star_border),
|
||||||
|
tooltip: isCover
|
||||||
|
? context.t.photo.isCover
|
||||||
|
: context.t.photo.setAsCover,
|
||||||
|
onPressed: isCover
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await cubit.setPreferredPhoto(photos[page].id);
|
||||||
|
// The chosen photo is now the cover (index 0): follow
|
||||||
|
// it there so the filled star reflects the change.
|
||||||
|
if (mounted && _controller.hasClients) {
|
||||||
|
_controller.jumpToPage(0);
|
||||||
|
setState(() => _page = 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
key: const Key('photo.delete'),
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
tooltip: context.t.common.delete,
|
||||||
|
onPressed: () => _confirmDelete(cubit, photos[page]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: PageView.builder(
|
||||||
|
controller: _controller,
|
||||||
|
itemCount: photos.length,
|
||||||
|
onPageChanged: (i) => setState(() => _page = i),
|
||||||
|
itemBuilder: (_, i) => _ZoomablePhoto(bytes: photos[i].bytes),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
83
apps/app_seeds/test/ui/about_screen_test.dart
Normal file
83
apps/app_seeds/test/ui/about_screen_test.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:tane/app.dart';
|
||||||
|
import 'package:tane/i18n/strings.g.dart';
|
||||||
|
|
||||||
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Widget app(db) => TranslationProvider(
|
||||||
|
child: TaneApp(
|
||||||
|
repository: newTestRepository(db),
|
||||||
|
species: newTestSpeciesRepository(db),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
PackageInfo.setMockInitialValues(
|
||||||
|
appName: 'Tanemaki',
|
||||||
|
packageName: 'org.comunes.tane',
|
||||||
|
version: '0.1.0',
|
||||||
|
buildNumber: '1',
|
||||||
|
buildSignature: '',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Settings About tile opens the About screen', (tester) async {
|
||||||
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||||
|
final db = newTestDatabase();
|
||||||
|
addTearDown(db.close);
|
||||||
|
|
||||||
|
await tester.pumpWidget(app(db));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.menu));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Settings'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('About Tanemaki'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Explanation text and kanji are shown above the fold.
|
||||||
|
expect(find.text('種まき'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.textContaining('helps people and collectives'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.textContaining('digital Plantare'), findsOneWidget);
|
||||||
|
|
||||||
|
// License and version live below the fold; scroll them into view.
|
||||||
|
await tester.scrollUntilVisible(find.text('AGPL-3.0'), 200);
|
||||||
|
expect(find.text('AGPL-3.0'), findsOneWidget);
|
||||||
|
expect(find.text('0.1.0 (1)'), findsOneWidget);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('About screen opens the open-source licenses page', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||||
|
final db = newTestDatabase();
|
||||||
|
addTearDown(db.close);
|
||||||
|
|
||||||
|
await tester.pumpWidget(app(db));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byIcon(Icons.menu));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Settings'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('About Tanemaki'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.scrollUntilVisible(find.text('Open-source licenses'), 200);
|
||||||
|
await tester.tap(find.text('Open-source licenses'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Flutter's built-in LicensePage renders the app name.
|
||||||
|
expect(find.byType(LicensePage), findsOneWidget);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,12 @@ void main() {
|
||||||
|
|
||||||
expect(find.text('Your inventory'), findsOneWidget);
|
expect(find.text('Your inventory'), findsOneWidget);
|
||||||
expect(find.text('Open market'), findsOneWidget);
|
expect(find.text('Open market'), findsOneWidget);
|
||||||
expect(find.text('Coming soon'), findsOneWidget); // market is Block 2
|
expect(find.text('COMING SOON'), findsOneWidget); // market is Block 2
|
||||||
|
|
||||||
|
// Redesign copy: tagline + the two destination subtitles.
|
||||||
|
expect(find.text('Share and grow local seeds'), findsOneWidget);
|
||||||
|
expect(find.text('Manage your seeds'), findsOneWidget);
|
||||||
|
expect(find.text('Browse and exchange'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home.inventory')));
|
await tester.tap(find.byKey(const Key('home.inventory')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -53,6 +58,31 @@ void main() {
|
||||||
await disposeTree(tester);
|
await disposeTree(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('drawer header returns to home', (tester) async {
|
||||||
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||||
|
final db = newTestDatabase();
|
||||||
|
addTearDown(db.close);
|
||||||
|
|
||||||
|
await tester.pumpWidget(app(db));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate away from home first.
|
||||||
|
await tester.tap(find.byKey(const Key('home.inventory')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('No seeds yet. Tap + to add your first.'), findsOneWidget);
|
||||||
|
|
||||||
|
// Open the drawer and tap the Tanemaki brand header.
|
||||||
|
await tester.tap(find.byIcon(Icons.menu));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Tanemaki'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Back on the home menu.
|
||||||
|
expect(find.text('Your inventory'), findsOneWidget);
|
||||||
|
expect(find.text('Open market'), findsOneWidget);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('drawer Settings opens the settings screen', (tester) async {
|
testWidgets('drawer Settings opens the settings screen', (tester) async {
|
||||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||||
final db = newTestDatabase();
|
final db = newTestDatabase();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:commons_core/commons_core.dart';
|
import 'package:commons_core/commons_core.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
@ -7,6 +9,14 @@ import 'package:tane/db/enums.dart';
|
||||||
|
|
||||||
import '../support/test_support.dart';
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
/// A valid 1x1 transparent PNG so photos decode cleanly in widget tests.
|
||||||
|
final _tinyPng = Uint8List.fromList(const [
|
||||||
|
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, //
|
||||||
|
0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, //
|
||||||
|
120, 156, 99, 250, 207, 0, 0, 0, 2, 0, 1, 226, 33, 188, 51, 0, 0, 0, 0, //
|
||||||
|
73, 69, 78, 68, 174, 66, 96, 130,
|
||||||
|
]);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late AppDatabase db;
|
late AppDatabase db;
|
||||||
|
|
||||||
|
|
@ -381,10 +391,117 @@ void main() {
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Plants'), findsOneWidget); // lot-type chip
|
expect(find.text('Plant'), findsOneWidget); // lot-type chip
|
||||||
expect(find.text('Year 2024 · 3 plants'), findsOneWidget);
|
expect(find.text('Year 2024 · 3 plants'), findsOneWidget);
|
||||||
// Germination is seed-only.
|
// Germination is seed-only.
|
||||||
expect(find.byIcon(Icons.grass_outlined), findsNothing);
|
expect(find.byIcon(Icons.grass_outlined), findsNothing);
|
||||||
await disposeTree(tester);
|
await disposeTree(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('adds a seedling lot with a packaging chip', (tester) async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final id = await repo.addQuickVariety(label: 'Basil');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrapDetail(
|
||||||
|
repository: repo,
|
||||||
|
varietyId: id,
|
||||||
|
species: newTestSpeciesRepository(db),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('detail.addLot')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Pick the seedling form, then its packaging.
|
||||||
|
await tester.tap(find.byKey(const Key('lotType.seedling')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('presentation.pot')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('addLot.save')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Seedling'), findsOneWidget); // form chip on the lot
|
||||||
|
expect(find.textContaining('Pot'), findsOneWidget); // packaging in subtitle
|
||||||
|
// Germination is seed-only, so a seedling lot hides it.
|
||||||
|
expect(find.byIcon(Icons.grass_outlined), findsNothing);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'the full-screen viewer deletes a photo only after confirmation',
|
||||||
|
(tester) async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final id = await repo.addQuickVariety(label: 'Maize');
|
||||||
|
await repo.addPhoto(id, _tinyPng);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrapDetail(
|
||||||
|
repository: repo,
|
||||||
|
varietyId: id,
|
||||||
|
species: newTestSpeciesRepository(db),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Open the viewer from the thumbnail.
|
||||||
|
await tester.tap(find.byKey(const Key('photo.thumb.0')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Cancelling the confirmation keeps the photo (still in the viewer).
|
||||||
|
await tester.tap(find.byKey(const Key('photo.delete')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Delete this photo?'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('Cancel'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Delete this photo?'), findsNothing); // dialog gone
|
||||||
|
expect(find.byKey(const Key('photo.delete')), findsOneWidget); // open
|
||||||
|
|
||||||
|
// Confirming removes it; with none left the viewer closes onto detail.
|
||||||
|
await tester.tap(find.byKey(const Key('photo.delete')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('photo.deleteConfirm')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('photo.thumb.0')), findsNothing); // no photos
|
||||||
|
expect(find.byKey(const Key('detail.edit')), findsOneWidget); // on detail
|
||||||
|
await disposeTree(tester);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('the full-screen viewer sets a non-cover photo as the cover', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repo = newTestRepository(db);
|
||||||
|
final id = await repo.addQuickVariety(label: 'Maize');
|
||||||
|
await repo.addPhoto(id, _tinyPng);
|
||||||
|
await repo.addPhoto(id, _tinyPng);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
wrapDetail(
|
||||||
|
repository: repo,
|
||||||
|
varietyId: id,
|
||||||
|
species: newTestSpeciesRepository(db),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('photo.thumb.0')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// On the cover (page 0) the star is filled and disabled.
|
||||||
|
expect(find.byIcon(Icons.star), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.star_border), findsNothing);
|
||||||
|
|
||||||
|
// Swipe to the second (non-cover) photo: the star turns to an outline.
|
||||||
|
await tester.fling(find.byType(PageView).last, const Offset(-400, 0), 1000);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byIcon(Icons.star_border), findsOneWidget);
|
||||||
|
|
||||||
|
// Setting it as cover snaps the view back to the (new) cover: filled again.
|
||||||
|
await tester.tap(find.byKey(const Key('photo.cover')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byIcon(Icons.star), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.star_border), findsNothing);
|
||||||
|
await disposeTree(tester);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
254
docs/mockups/seedees_material3.html
Normal file
254
docs/mockups/seedees_material3.html
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
<title>Tanemaki — Material mockups (v3)</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; -webkit-font-smoothing: antialiased; }
|
||||||
|
a { color: #2f7d34; text-decoration: none; }
|
||||||
|
a:hover { color: #216023; }
|
||||||
|
.ms { font-family: 'Material Symbols Outlined'; font-weight: normal; font-style: normal; line-height: 1; letter-spacing: normal; text-transform: none; display: inline-block; white-space: nowrap; direction: ltr; -webkit-font-feature-settings: 'liga'; }
|
||||||
|
.fill { font-variation-settings: 'FILL' 1; }
|
||||||
|
@font-face { font-family: 'seedks'; src: url('seedees_material3_assets/seedks.ttf'); }
|
||||||
|
.sk { font-family: 'seedks'; line-height: 1; display: inline-block; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="min-height:100vh;background:#CFE0C4;padding:48px 40px 72px;font-family:Roboto">
|
||||||
|
<header style="max-width:1200px;margin:0 auto 44px">
|
||||||
|
<div style="display:flex;align-items:center;gap:16px">
|
||||||
|
<img src="seedees_material3_assets/logo.png" alt="Tanemaki" style="height:52px;width:auto;display:block">
|
||||||
|
<div>
|
||||||
|
<div style="font:700 30px Roboto;color:#1a2a13;letter-spacing:-.01em">Tanemaki · Material mockups</div>
|
||||||
|
<div style="font:400 15px Roboto;color:#38492e;margin-top:2px">Aligned with the real app — green app bars, botanical content, search, batches and action sheets.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:44px;justify-content:center;align-items:flex-start;max-width:1720px;margin:0 auto">
|
||||||
|
|
||||||
|
<!-- ============ 00 HOME ============ -->
|
||||||
|
<figure data-screen-label="00 Home" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#3E8C38;display:flex;align-items:center;gap:6px;padding:0 8px;color:#fff"><span class="ms" style="font-size:24px;color:#fff;width:44px;height:44px;display:flex;align-items:center;justify-content:center">menu</span><span style="flex:1;font:500 20px Roboto;color:#fff;margin-left:6px">Tanemaki</span><span class="ms" style="font-size:23px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">search</span></header>
|
||||||
|
<div style="flex:1;overflow:hidden;position:relative;display:flex;flex-direction:column">
|
||||||
|
<div style="position:absolute;inset:0;overflow:hidden;pointer-events:none;opacity:.5">
|
||||||
|
<span class="sk" style="position:absolute;left:16px;bottom:74px;font-size:56px;color:#C3D9B2">a</span>
|
||||||
|
<span class="sk" style="position:absolute;left:122px;bottom:28px;font-size:48px;color:#C3D9B2">j</span>
|
||||||
|
<span class="sk" style="position:absolute;right:22px;bottom:66px;font-size:60px;color:#C3D9B2">e</span>
|
||||||
|
<span class="sk" style="position:absolute;right:124px;bottom:122px;font-size:44px;color:#C3D9B2">b</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 24px 28px;position:relative;z-index:1">
|
||||||
|
<div style="width:128px;height:128px;border-radius:50%;background:#DDF3CE;display:flex;align-items:center;justify-content:center;margin-bottom:10px"><img src="seedees_material3_assets/logo.png" alt="Tanemaki" style="height:84px;width:auto;display:block"></div>
|
||||||
|
<div style="font:500 26px Roboto;color:#1a2a13;letter-spacing:-.01em">Tanemaki</div>
|
||||||
|
<div style="font:400 14px Roboto;color:#55624a;margin-bottom:34px">Share and grow local seeds</div>
|
||||||
|
<button style="width:100%;border:none;cursor:pointer;background:#2f7d34;color:#fff;border-radius:18px;padding:19px 22px;display:flex;align-items:center;gap:16px;box-shadow:0 4px 12px rgba(47,125,52,.28);margin-bottom:16px;text-align:left"><span style="width:44px;height:44px;border-radius:50%;background:rgba(255,255,255,.18);display:flex;align-items:center;justify-content:center"><span class="ms" style="font-size:26px">inventory_2</span></span><span style="flex:1"><span style="display:block;font:500 19px Roboto">Your inventory</span><span style="display:block;font:400 13px Roboto;opacity:.85">Manage your seeds</span></span></button>
|
||||||
|
<button style="width:100%;border:1px solid #B8C4AC;cursor:pointer;background:#fff;color:#1b2416;border-radius:18px;padding:19px 22px;display:flex;align-items:center;gap:16px;text-align:left"><span style="width:44px;height:44px;border-radius:50%;background:#DDF3CE;display:flex;align-items:center;justify-content:center"><span class="ms" style="font-size:26px;color:#2f7d34">storefront</span></span><span style="flex:1"><span style="display:block;font:500 19px Roboto">Open market</span><span style="display:block;font:400 13px Roboto;color:#7b8a70">Browse and exchange</span></span><span style="font:500 11px Roboto;color:#b3bda8;letter-spacing:.05em">SOON</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">00 · Home</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- ============ 01 INVENTARIO ============ -->
|
||||||
|
<figure data-screen-label="01 Inventory" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#3E8C38;display:flex;align-items:center;gap:6px;padding:0 8px;color:#fff"><span class="ms" style="font-size:24px;color:#fff;width:44px;height:44px;display:flex;align-items:center;justify-content:center">menu</span><span style="flex:1;font:500 20px Roboto;color:#fff;margin-left:6px">Inventory</span></header>
|
||||||
|
<div style="flex:1;overflow:hidden;position:relative">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;background:#fff;border-radius:28px;box-shadow:0 1px 3px rgba(0,0,0,.14);padding:12px 18px;margin:16px 16px 8px"><span class="ms" style="font-size:22px;color:#7b8a70">search</span><span style="font:400 15px Roboto;color:#8a9880">Search seeds</span></div>
|
||||||
|
<div style="font:500 16px Roboto;color:#2f7d34;padding:12px 18px 4px">Poaceae</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:14px;padding:11px 18px"><span style="width:46px;height:46px;border-radius:50%;background:#CFE7C3;display:flex;align-items:center;justify-content:center;font:500 17px Roboto;color:#3b7a34">M</span><div style="flex:1"><div style="font:500 15px Roboto;color:#1b2416">Golden Bantam Corn</div><div style="font:italic 400 14px Roboto;color:#5c6b52">Zea mays</div></div><span class="ms" style="font-size:21px;color:#8a9880">edit</span></div>
|
||||||
|
<div style="font:500 16px Roboto;color:#2f7d34;padding:18px 18px 4px">Solanaceae</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:14px;padding:11px 18px"><span style="width:46px;height:46px;border-radius:50%;background:#CFE7C3;display:flex;align-items:center;justify-content:center;font:500 17px Roboto;color:#3b7a34">T</span><div style="flex:1"><div style="font:500 15px Roboto;color:#1b2416">Chestnut Tomato</div><div style="font:italic 400 14px Roboto;color:#5c6b52">Solanum lycopersicum</div></div><span class="ms" style="font-size:21px;color:#8a9880">edit</span></div>
|
||||||
|
<div style="position:absolute;right:18px;bottom:18px;width:60px;height:60px;border-radius:20px;background:#2f7d34;display:flex;align-items:center;justify-content:center;box-shadow:0 6px 16px rgba(47,125,52,.42)"><span class="ms" style="font-size:30px;color:#fff">add</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">01 · Inventory</figcaption>
|
||||||
|
</figure>
|
||||||
|
<!-- ============ 02 DETALLE SEMILLA ============ -->
|
||||||
|
<figure data-screen-label="02 Detail" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#3E8C38;display:flex;align-items:center;gap:2px;padding:0 6px;color:#fff"><span class="ms" style="font-size:24px;color:#fff;width:44px;height:44px;display:flex;align-items:center;justify-content:center">arrow_back</span><span style="flex:1;font:500 20px Roboto;color:#fff;margin-left:4px">Chestnut Tomato</span><span class="ms" style="font-size:22px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">edit</span><span class="ms" style="font-size:22px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">delete</span></header>
|
||||||
|
<div style="flex:1;overflow:hidden;padding:18px 18px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:26px">
|
||||||
|
<div><div style="font:500 16px Roboto;color:#2f7d34">Solanaceae</div><div style="font:italic 400 15px Roboto;color:#5c6b52;margin-top:2px">Solanum lycopersicum</div></div>
|
||||||
|
<div style="width:118px;height:118px;border-radius:50%;border:1.5px dashed #9aa88f;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#3b7a34;gap:4px"><span class="ms" style="font-size:26px">add_a_photo</span><span style="font:400 13px Roboto">Add photo</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="font:500 16px Roboto;color:#2f7d34;margin-bottom:10px">Also known as</div>
|
||||||
|
<button style="border:1px solid #C3CBB6;background:#F3F7EE;border-radius:12px;padding:11px 18px;display:inline-flex;align-items:center;gap:10px;font:400 15px Roboto;color:#1b2416;cursor:pointer;margin-bottom:24px"><span class="ms" style="font-size:20px;color:#2f7d34">add</span>Add name</button>
|
||||||
|
<div style="font:500 16px Roboto;color:#2f7d34;margin-bottom:10px">Links</div>
|
||||||
|
<button style="border:1px solid #C3CBB6;background:#F3F7EE;border-radius:12px;padding:11px 18px;display:inline-flex;align-items:center;gap:10px;font:400 15px Roboto;color:#1b2416;cursor:pointer;margin-bottom:24px"><span class="ms" style="font-size:20px;color:#2f7d34">add</span>Add link</button>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"><span style="font:500 16px Roboto;color:#2f7d34">Batches</span><span style="display:inline-flex;align-items:center;gap:6px;font:400 14px Roboto;color:#2f7d34"><span class="ms" style="font-size:19px">add</span>Add batch</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;padding:8px 0"><span class="sk" style="font-size:22px;color:#7b8a70">k</span><span style="display:inline-flex;align-items:center;gap:6px;background:#D3E8C8;border-radius:999px;padding:5px 12px;font:400 13px Roboto;color:#2f6d2a"><span class="sk" style="font-size:17px">b</span>Seeds</span><div style="flex:1;font:400 15px Roboto;color:#1b2416;line-height:1.35">Unknown year · a few</div><span class="ms" style="font-size:22px;color:#3b7a34">grass</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">02 · Seed detail</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- ============ 03 AÑADIR LOTE ============ -->
|
||||||
|
<figure data-screen-label="03 Add batch" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column;position:relative">
|
||||||
|
<div style="height:26px;background:#22521e;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#2f6a2b;display:flex;align-items:center;gap:2px;padding:0 6px;color:rgba(255,255,255,.55)"><span class="ms" style="font-size:24px;width:44px;height:44px;display:flex;align-items:center;justify-content:center">arrow_back</span><span style="flex:1;font:500 20px Roboto;margin-left:4px">Chestnut Tomato</span></header>
|
||||||
|
<div style="flex:1;position:relative;overflow:hidden">
|
||||||
|
<div style="position:absolute;inset:0;background:rgba(20,35,12,.42)"></div>
|
||||||
|
<div style="position:absolute;left:0;right:0;bottom:0;background:#EAF3E3;border-radius:26px 26px 0 0;padding:22px 20px 18px;box-shadow:0 -8px 24px rgba(0,0,0,.2)">
|
||||||
|
<div style="font:400 24px Roboto;color:#1b2416;margin-bottom:18px">Add batch</div>
|
||||||
|
<div style="display:flex;border:1px solid #B8C4AC;border-radius:999px;overflow:hidden;margin-bottom:20px">
|
||||||
|
<div style="flex:1;display:flex;align-items:center;justify-content:center;gap:8px;background:#D3E8C8;padding:12px;font:500 14px Roboto;color:#2f6d2a"><span class="sk" style="font-size:19px">b</span>Seeds</div>
|
||||||
|
<div style="flex:1;display:flex;align-items:center;justify-content:center;gap:8px;padding:12px;font:400 14px Roboto;color:#4b5a41"><span class="ms" style="font-size:19px">potted_plant</span>Seedling</div>
|
||||||
|
</div>
|
||||||
|
<div style="font:500 15px Roboto;color:#1b2416;margin-bottom:8px">Harvest date</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;border:1px solid #B8C4AC;border-radius:12px;padding:13px 16px;margin-bottom:20px"><span class="ms" style="font-size:20px;color:#2f7d34">calendar_month</span><span style="font:400 15px Roboto;color:#1b2416">Year 2026</span></div>
|
||||||
|
<div style="font:500 15px Roboto;color:#1b2416;margin-bottom:12px">How much?</div>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:14px 4px;margin-bottom:20px">
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="sk" style="font-size:24px;color:#3b7a34">k</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">a few</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="sk" style="font-size:24px;color:#3b7a34">f</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">teaspoon</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="sk" style="font-size:24px;color:#3b7a34">g</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">spoon</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="sk" style="font-size:24px;color:#3b7a34">i</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">packet</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="sk" style="font-size:24px;color:#3b7a34">h</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">cup</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="sk" style="font-size:24px;color:#3b7a34">a</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">jar</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="sk" style="font-size:24px;color:#3b7a34">e</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">sack</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms" style="font-size:24px;color:#3b7a34">grass</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">cob</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms" style="font-size:24px;color:#3b7a34">eco</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">pod</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms" style="font-size:24px;color:#3b7a34">grass</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">spike</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms" style="font-size:24px;color:#3b7a34">local_florist</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">flower head</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms fill" style="font-size:24px;color:#3b7a34">local_florist</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">fruit</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms" style="font-size:24px;color:#3b7a34">spa</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">bulb</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms" style="font-size:24px;color:#3b7a34">spa</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">tuber</span></div>
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:5px"><span class="ms" style="font-size:24px;color:#3b7a34">grass</span><span style="font:400 10px Roboto;color:#4b5a41;text-align:center">bunch</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between"><span style="font:500 15px Roboto;color:#2f7d34;padding:8px 4px">Cancel</span><button style="border:none;cursor:pointer;background:#2f7d34;color:#fff;border-radius:999px;padding:13px 30px;font:500 15px Roboto">Save</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">03 · Add batch</figcaption>
|
||||||
|
</figure>
|
||||||
|
<!-- ============ 04 EDITAR SEMILLA ============ -->
|
||||||
|
<figure data-screen-label="04 Edit seed" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column;position:relative">
|
||||||
|
<div style="height:26px;background:#22521e;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#2f6a2b;display:flex;align-items:center;gap:2px;padding:0 6px;color:rgba(255,255,255,.55)"><span class="ms" style="font-size:24px;width:44px;height:44px;display:flex;align-items:center;justify-content:center">arrow_back</span><span style="flex:1;font:500 20px Roboto;margin-left:4px">Chestnut Tomato</span><span class="ms" style="font-size:22px;width:42px;height:42px;display:flex;align-items:center;justify-content:center">edit</span></header>
|
||||||
|
<div style="flex:1;position:relative;overflow:hidden">
|
||||||
|
<div style="position:absolute;inset:0;background:rgba(20,35,12,.42)"></div>
|
||||||
|
<div style="position:absolute;left:0;right:0;bottom:0;background:#EAF3E3;border-radius:26px 26px 0 0;padding:24px 20px 20px;box-shadow:0 -8px 24px rgba(0,0,0,.2)">
|
||||||
|
<div style="font:400 24px Roboto;color:#1b2416;margin-bottom:22px">Edit seed</div>
|
||||||
|
<div style="position:relative;border:1px solid #7b8a70;border-radius:8px;padding:16px 14px 10px;margin-bottom:20px"><span style="position:absolute;top:-9px;left:12px;background:#EAF3E3;padding:0 5px;font:400 12px Roboto;color:#5c6b52">Name</span><span style="font:400 16px Roboto;color:#1b2416">Chestnut Tomato</span></div>
|
||||||
|
<div style="position:relative;border:1px solid #7b8a70;border-radius:8px;padding:16px 14px 10px;margin-bottom:20px;display:flex;align-items:center;gap:10px"><span style="position:absolute;top:-9px;left:12px;background:#EAF3E3;padding:0 5px;font:400 12px Roboto;color:#5c6b52">Species (from catalog)</span><span class="ms" style="font-size:20px;color:#5c6b52">eco</span><span style="font:400 16px Roboto;color:#1b2416">Solanum lycopersicum</span></div>
|
||||||
|
<div style="position:relative;border:1px solid #7b8a70;border-radius:8px;padding:16px 14px 10px;margin-bottom:20px"><span style="position:absolute;top:-9px;left:12px;background:#EAF3E3;padding:0 5px;font:400 12px Roboto;color:#5c6b52">Category</span><span style="font:400 16px Roboto;color:#1b2416">Solanaceae</span></div>
|
||||||
|
<div style="position:relative;border:1px solid #B8C4AC;border-radius:8px;padding:16px 14px;height:96px;margin-bottom:26px"><span style="font:400 16px Roboto;color:#8a9880">Notes</span></div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between"><span style="font:500 15px Roboto;color:#2f7d34;padding:8px 4px">Cancel</span><button style="border:none;cursor:pointer;background:#2f7d34;color:#fff;border-radius:999px;padding:13px 34px;font:500 15px Roboto">Save</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">04 · Edit seed</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- ============ 05 MENÚ LATERAL ============ -->
|
||||||
|
<figure data-screen-label="05 Drawer" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column;position:relative">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<div style="flex:1;position:relative;overflow:hidden">
|
||||||
|
<div style="position:absolute;inset:0;background:rgba(20,35,12,.4)"></div>
|
||||||
|
<div style="position:absolute;top:0;left:0;bottom:0;width:300px;background:#fff;box-shadow:2px 0 24px rgba(0,0,0,.2);display:flex;flex-direction:column">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;padding:18px 22px 16px;border-bottom:1px solid #E4E9DD"><img src="seedees_material3_assets/logo.png" alt="" style="height:34px;width:auto"><span style="font:700 22px Roboto;color:#2f7d34;letter-spacing:-.01em">Tanemaki</span></div>
|
||||||
|
<nav style="flex:1;padding:8px 0">
|
||||||
|
<div style="display:flex;align-items:center;gap:18px;height:52px;padding:0 22px;color:#1b2416"><span class="sk" style="font-size:23px;color:#2f7d34">b</span><span style="font:500 15px Roboto">Inventory</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:18px;height:52px;padding:0 22px;color:#9aa88f;border-bottom:1px solid #E4E9DD"><span class="ms" style="font-size:23px">storefront</span><span style="font:400 15px Roboto;flex:1">Market</span><span style="font:500 11px Roboto;color:#b3bda8;letter-spacing:.05em">SOON</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:18px;height:52px;padding:0 22px;color:#9aa88f"><span class="ms" style="font-size:23px">person</span><span style="font:400 15px Roboto;flex:1">Your profile</span><span style="font:500 11px Roboto;color:#b3bda8;letter-spacing:.05em">SOON</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:18px;height:52px;padding:0 22px;color:#9aa88f"><span class="ms" style="font-size:23px">chat_bubble</span><span style="font:400 15px Roboto;flex:1">Chat</span><span style="font:500 11px Roboto;color:#b3bda8;letter-spacing:.05em">SOON</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:18px;height:52px;padding:0 22px;color:#9aa88f"><span class="ms" style="font-size:23px">favorite</span><span style="font:400 15px Roboto;flex:1">Wishlist</span><span style="font:500 11px Roboto;color:#b3bda8;letter-spacing:.05em">SOON</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:18px;height:52px;padding:0 22px;color:#9aa88f"><span class="ms" style="font-size:23px">group</span><span style="font:400 15px Roboto;flex:1">Following</span><span style="font:500 11px Roboto;color:#b3bda8;letter-spacing:.05em">SOON</span></div>
|
||||||
|
</nav>
|
||||||
|
<div style="display:flex;align-items:center;gap:18px;height:56px;padding:0 22px;color:#1b2416;border-top:1px solid #E4E9DD"><span class="ms" style="font-size:23px;color:#4b5a41">settings</span><span style="font:400 15px Roboto">Settings</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">05 · Side menu</figcaption>
|
||||||
|
</figure>
|
||||||
|
<!-- ============ 06 MERCADO ============ -->
|
||||||
|
<figure data-screen-label="06 Market" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#3E8C38;display:flex;align-items:center;gap:4px;padding:0 8px;color:#fff"><span class="ms" style="font-size:24px;color:#fff;width:44px;height:44px;display:flex;align-items:center;justify-content:center">menu</span><span style="flex:1;font:500 20px Roboto;color:#fff;margin-left:6px">Market</span><span class="ms" style="font-size:23px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">near_me</span><span class="ms" style="font-size:23px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">search</span></header>
|
||||||
|
<div style="flex:1;overflow:hidden">
|
||||||
|
<div style="padding:12px 14px 6px;display:flex;gap:8px"><div style="display:flex;align-items:center;gap:6px;background:#2f7d34;color:#fff;border-radius:999px;padding:7px 14px;font:500 13px Roboto"><span class="ms" style="font-size:16px">near_me</span>Nearby</div><div style="background:#fff;color:#3d4c33;border:1px solid #C3CBB6;border-radius:999px;padding:7px 14px;font:400 13px Roboto">Exchange</div><div style="background:#fff;color:#3d4c33;border:1px solid #C3CBB6;border-radius:999px;padding:7px 14px;font:400 13px Roboto">Family</div></div>
|
||||||
|
<div style="padding:8px 14px 0;column-count:2;column-gap:12px">
|
||||||
|
<div style="break-inside:avoid;margin-bottom:12px;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.12)"><div style="height:112px;background:repeating-linear-gradient(45deg,#DCE6D0,#DCE6D0 8px,#D2DDC4 8px,#D2DDC4 16px);display:flex;align-items:center;justify-content:center;color:#6E7566;font:500 10px 'Roboto Mono'">tomate.jpg</div><div style="padding:10px 12px"><div style="font:500 15px Roboto;color:#1b2416">Chestnut Tomato</div><div style="font:italic 400 12px Roboto;color:#5c6b52;margin:1px 0 6px">Solanum lycopersicum</div><div style="display:flex;align-items:center;justify-content:space-between"><span style="display:inline-flex;align-items:center;gap:5px;background:#D3E8C8;color:#2f6d2a;border-radius:999px;padding:3px 9px;font:400 11px Roboto"><span class="ms" style="font-size:14px">swap_horiz</span>Barter</span><span class="sk" style="font-size:20px;color:#2f7d34">b</span></div><div style="display:flex;align-items:center;gap:5px;margin-top:8px;font:400 11px Roboto;color:#5c6b52"><span class="ms" style="font-size:15px;color:#2f7d34">location_on</span>2 km</div></div></div>
|
||||||
|
<div style="break-inside:avoid;margin-bottom:12px;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.12)"><div style="height:84px;background:repeating-linear-gradient(45deg,#DCE6D0,#DCE6D0 8px,#D2DDC4 8px,#D2DDC4 16px);display:flex;align-items:center;justify-content:center;color:#6E7566;font:500 10px 'Roboto Mono'">maíz.jpg</div><div style="padding:10px 12px"><div style="font:500 15px Roboto;color:#1b2416">Golden Bantam Corn</div><div style="font:italic 400 12px Roboto;color:#5c6b52;margin:1px 0 6px">Zea mays</div><div style="display:flex;align-items:center;justify-content:space-between"><span style="font:500 15px Roboto;color:#2f7d34">1.20 €</span><span class="ms" style="font-size:19px;color:#2f7d34">grass</span></div><div style="display:flex;align-items:center;gap:5px;margin-top:8px;font:400 11px Roboto;color:#5c6b52"><span class="ms" style="font-size:15px;color:#2f7d34">location_on</span>5 km</div></div></div>
|
||||||
|
<div style="break-inside:avoid;margin-bottom:12px;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.12)"><div style="height:100px;background:repeating-linear-gradient(45deg,#DCE6D0,#DCE6D0 8px,#D2DDC4 8px,#D2DDC4 16px);display:flex;align-items:center;justify-content:center;color:#6E7566;font:500 10px 'Roboto Mono'">girasol</div><div style="padding:10px 12px"><div style="font:500 15px Roboto;color:#1b2416">Sunflower</div><div style="font:italic 400 12px Roboto;color:#5c6b52;margin:1px 0 6px">Helianthus annuus</div><div style="display:flex;align-items:center;justify-content:space-between"><span style="font:500 15px Roboto;color:#2f7d34">0.40 €</span><span class="sk" style="font-size:20px;color:#2f7d34">a</span></div><div style="display:flex;align-items:center;gap:5px;margin-top:8px;font:400 11px Roboto;color:#5c6b52"><span class="ms" style="font-size:15px;color:#2f7d34">location_on</span>800 m</div></div></div>
|
||||||
|
<div style="break-inside:avoid;margin-bottom:12px;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.12)"><div style="height:120px;background:repeating-linear-gradient(45deg,#DCE6D0,#DCE6D0 8px,#D2DDC4 8px,#D2DDC4 16px);display:flex;align-items:center;justify-content:center;color:#6E7566;font:500 10px 'Roboto Mono'">calabaza</div><div style="padding:10px 12px"><div style="font:500 15px Roboto;color:#1b2416">Squash</div><div style="font:italic 400 12px Roboto;color:#5c6b52;margin:1px 0 6px">Cucurbita maxima</div><div style="display:flex;align-items:center;justify-content:space-between"><span style="display:inline-flex;align-items:center;gap:5px;background:#D3E8C8;color:#2f6d2a;border-radius:999px;padding:3px 9px;font:400 11px Roboto"><span class="ms" style="font-size:14px">swap_horiz</span>Barter</span><span class="sk" style="font-size:20px;color:#2f7d34">i</span></div><div style="display:flex;align-items:center;gap:5px;margin-top:8px;font:400 11px Roboto;color:#5c6b52"><span class="ms" style="font-size:15px;color:#2f7d34">location_on</span>3 km</div></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">06 · Market <span style="color:#7b8a70">(coming soon)</span></figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- ============ 07 DETALLE INTERCAMBIO ============ -->
|
||||||
|
<figure data-screen-label="07 Exchange" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#3E8C38;display:flex;align-items:center;gap:2px;padding:0 6px;color:#fff"><span class="ms" style="font-size:24px;color:#fff;width:44px;height:44px;display:flex;align-items:center;justify-content:center">arrow_back</span><span style="flex:1;font:500 20px Roboto;color:#fff;margin-left:4px">Chestnut Tomato</span><span class="ms" style="font-size:22px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">share</span><span class="ms fill" style="font-size:23px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">favorite</span></header>
|
||||||
|
<div style="flex:1;overflow:hidden">
|
||||||
|
<div style="height:210px;position:relative;background:repeating-linear-gradient(45deg,#D6E1C9,#D6E1C9 10px,#CCD8BE 10px,#CCD8BE 20px);display:flex;align-items:center;justify-content:center;color:#6E7566;font:500 12px 'Roboto Mono'">tomate_castañal.jpg<div style="position:absolute;bottom:12px;left:50%;transform:translateX(-50%);display:flex;gap:6px"><span style="width:7px;height:7px;border-radius:50%;background:#2f7d34"></span><span style="width:7px;height:7px;border-radius:50%;background:rgba(47,125,52,.35)"></span><span style="width:7px;height:7px;border-radius:50%;background:rgba(47,125,52,.35)"></span></div><div style="position:absolute;right:14px;bottom:-24px;width:52px;height:52px;border-radius:50%;background:#C9B18f;border:3px solid #E7F1E0;display:flex;align-items:center;justify-content:center;font:400 9px 'Roboto Mono';color:#5b4a30">avatar</div></div>
|
||||||
|
<div style="padding:18px 18px 0">
|
||||||
|
<div style="font:500 22px Roboto;color:#1b2416">Chestnut Tomato</div>
|
||||||
|
<div style="font:italic 400 14px Roboto;color:#5c6b52;margin:2px 0 12px">Solanum lycopersicum · Solanaceae</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px"><span style="display:inline-flex;align-items:center;gap:6px;background:#D3E8C8;color:#2f6d2a;border-radius:999px;padding:5px 12px;font:500 13px Roboto"><span class="ms" style="font-size:16px">swap_horiz</span>Exchange</span><span style="display:inline-flex;align-items:center;gap:5px;font:400 13px Roboto;color:#5c6b52"><span class="ms" style="font-size:17px;color:#2f7d34">location_on</span>2 km</span><span style="display:flex;align-items:center;gap:2px;color:#E8A200"><span class="ms fill" style="font-size:16px">star</span><span style="font:400 13px Roboto;color:#5c6b52">5.0</span></span></div>
|
||||||
|
<p style="font:400 14px/1.55 Roboto;color:#44483E;margin:0 0 16px">Local seasonal tomato variety. Open-pollinated seeds, 2025 harvest. Looking to trade for legumes or herbs.</p>
|
||||||
|
<button style="width:100%;border:none;cursor:pointer;background:#2f7d34;color:#fff;border-radius:14px;padding:14px;display:flex;align-items:center;justify-content:center;gap:10px;font:500 15px Roboto;margin-bottom:20px"><span class="ms" style="font-size:20px">chat_bubble</span>Chat with the grower</button>
|
||||||
|
<div style="font:500 16px Roboto;color:#1b2416;margin-bottom:10px">Similar</div>
|
||||||
|
<div style="display:flex;gap:10px"><div style="width:104px;height:74px;flex:none;border-radius:12px;background:repeating-linear-gradient(45deg,#DCE6D0,#DCE6D0 7px,#D2DDC4 7px,#D2DDC4 14px)"></div><div style="width:104px;height:74px;flex:none;border-radius:12px;background:repeating-linear-gradient(45deg,#DCE6D0,#DCE6D0 7px,#D2DDC4 7px,#D2DDC4 14px)"></div><div style="width:104px;height:74px;flex:none;border-radius:12px;background:repeating-linear-gradient(45deg,#DCE6D0,#DCE6D0 7px,#D2DDC4 7px,#D2DDC4 14px)"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">07 · Exchange detail <span style="color:#7b8a70">(coming soon)</span></figcaption>
|
||||||
|
</figure>
|
||||||
|
<!-- ============ 08 PERFIL ============ -->
|
||||||
|
<figure data-screen-label="08 Profile" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#3E8C38;display:flex;align-items:center;gap:2px;padding:0 6px;color:#fff"><span class="ms" style="font-size:24px;color:#fff;width:44px;height:44px;display:flex;align-items:center;justify-content:center">arrow_back</span><span style="flex:1;font:500 20px Roboto;color:#fff;margin-left:4px">Your profile</span><span class="ms" style="font-size:22px;color:#fff;width:42px;height:42px;display:flex;align-items:center;justify-content:center">edit</span></header>
|
||||||
|
<div style="flex:1;overflow:hidden;padding:20px 18px">
|
||||||
|
<div style="display:flex;align-items:center;gap:16px;margin-bottom:22px">
|
||||||
|
<div style="width:76px;height:76px;flex:none;border-radius:50%;background:#C9B18f;display:flex;align-items:center;justify-content:center;font:400 10px 'Roboto Mono';color:#5b4a30">avatar</div>
|
||||||
|
<div><div style="font:500 20px Roboto;color:#1b2416">Orto del Sole</div><div style="font:400 13px Roboto;color:#5c6b52;margin-top:2px">Perugia, Italy</div><div style="display:flex;align-items:center;gap:2px;margin-top:6px;color:#E8A200"><span class="ms fill" style="font-size:16px">star</span><span class="ms fill" style="font-size:16px">star</span><span class="ms fill" style="font-size:16px">star</span><span class="ms fill" style="font-size:16px">star</span><span class="ms fill" style="font-size:16px">star</span><span style="font:400 12px Roboto;color:#5c6b52;margin-left:5px">5.0 · 5 reviews</span></div></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;margin-bottom:22px"><div style="flex:1;background:#fff;border-radius:14px;padding:14px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1)"><div style="font:500 22px Roboto;color:#2f7d34">12</div><div style="font:400 12px Roboto;color:#5c6b52">seeds</div></div><div style="flex:1;background:#fff;border-radius:14px;padding:14px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1)"><div style="font:500 22px Roboto;color:#2f7d34">34</div><div style="font:400 12px Roboto;color:#5c6b52">exchanges</div></div><div style="flex:1;background:#fff;border-radius:14px;padding:14px;text-align:center;box-shadow:0 1px 3px rgba(0,0,0,.1)"><div style="font:500 22px Roboto;color:#2f7d34">8</div><div style="font:400 12px Roboto;color:#5c6b52">following</div></div></div>
|
||||||
|
<div style="height:160px;border-radius:14px;overflow:hidden;background:repeating-linear-gradient(45deg,#D6E1C9,#D6E1C9 12px,#CCD8BE 12px,#CCD8BE 24px);display:flex;align-items:center;justify-content:center;position:relative;margin-bottom:20px"><span style="font:500 11px 'Roboto Mono';color:#6E7566">map · approx. location</span><span class="ms fill" style="position:absolute;font-size:38px;color:#2f7d34">location_on</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid #DBE3D1"><span class="ms" style="font-size:20px;color:#5c6b52">mail</span><span style="font:400 15px Roboto;color:#1b2416">huerta@ejemplo.com</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;padding:12px 0"><span class="ms" style="font-size:20px;color:#5c6b52">verified_user</span><span style="font:400 15px Roboto;color:#1b2416">Verified member since 2024</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">08 · Profile <span style="color:#7b8a70">(coming soon)</span></figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- ============ 09 CHAT ============ -->
|
||||||
|
<figure data-screen-label="09 Chat" style="margin:0;width:360px">
|
||||||
|
<div style="width:360px;height:740px;border-radius:34px;overflow:hidden;background:#E7F1E0;box-shadow:0 22px 55px -14px rgba(20,45,15,.4);border:1px solid rgba(0,0,0,.06);display:flex;flex-direction:column">
|
||||||
|
<div style="height:26px;background:#2E6E28;display:flex;justify-content:space-between;align-items:center;padding:0 16px;color:#fff;font:500 12px Roboto"><span>9:41</span><span style="display:flex;gap:5px;align-items:center"><span class="ms" style="font-size:15px">network_wifi</span><span class="ms" style="font-size:15px">signal_cellular_alt</span><span class="ms fill" style="font-size:15px">battery_full</span></span></div>
|
||||||
|
<header style="height:60px;background:#3E8C38;display:flex;align-items:center;gap:8px;padding:0 8px;color:#fff"><span class="ms" style="font-size:24px;color:#fff;width:40px;height:40px;display:flex;align-items:center;justify-content:center">arrow_back</span><span style="width:38px;height:38px;flex:none;border-radius:50%;background:#C9B18f;display:flex;align-items:center;justify-content:center;font:400 8px 'Roboto Mono';color:#5b4a30">hort</span><div style="flex:1;min-width:0"><div style="font:500 16px Roboto;color:#fff">Orto del Sole</div><div style="display:flex;align-items:center;gap:5px;font:400 12px Roboto;color:rgba(255,255,255,.85)"><span style="width:7px;height:7px;border-radius:50%;background:#b6f09a"></span>online</div></div><span class="ms" style="font-size:22px;color:#fff;width:40px;height:40px;display:flex;align-items:center;justify-content:center">search</span></header>
|
||||||
|
<div style="flex:1;overflow:hidden;padding:16px 16px;display:flex;flex-direction:column;gap:12px">
|
||||||
|
<div style="text-align:center;font:400 11px Roboto;color:#8a9080">Apr 4</div>
|
||||||
|
<div style="display:flex;gap:8px;align-items:flex-end;max-width:82%"><span style="width:28px;height:28px;flex:none;border-radius:50%;background:#C9B18f"></span><div style="background:#fff;border-radius:16px 16px 16px 4px;padding:10px 14px;box-shadow:0 1px 2px rgba(0,0,0,.08)"><div style="font:400 14px/1.4 Roboto;color:#1b2416">Glad you're interested in these tomato seeds!</div><div style="font:400 10px Roboto;color:#9aa08f;text-align:right;margin-top:3px">17:22</div></div></div>
|
||||||
|
<div style="align-self:flex-end;max-width:82%"><div style="background:#D3E8C8;border-radius:16px 16px 4px 16px;padding:10px 14px"><div style="font:400 14px/1.4 Roboto;color:#173d0e">Can we meet this Saturday at your garden?</div><div style="display:flex;align-items:center;justify-content:flex-end;gap:4px;margin-top:3px"><span style="font:400 10px Roboto;color:#3d6b2a">17:24</span><span class="ms" style="font-size:13px;color:#2f7d34">done_all</span></div></div></div>
|
||||||
|
<div style="display:flex;gap:8px;align-items:flex-end;max-width:82%"><span style="width:28px;height:28px;flex:none;border-radius:50%;background:#C9B18f"></span><div style="background:#fff;border-radius:16px 16px 16px 4px;padding:10px 14px;box-shadow:0 1px 2px rgba(0,0,0,.08)"><div style="font:400 14px/1.4 Roboto;color:#1b2416">Sure! I'll bring a few packets.</div><div style="font:400 10px Roboto;color:#9aa08f;text-align:right;margin-top:3px">17:25</div></div></div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:center;gap:5px;margin-top:auto;font:400 11px Roboto;color:#8a9080"><span class="ms" style="font-size:13px">lock</span>End-to-end encrypted conversation</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:10px 12px;display:flex;align-items:center;gap:8px"><div style="flex:1;display:flex;align-items:center;gap:8px;background:#fff;border-radius:999px;padding:10px 16px;box-shadow:0 1px 3px rgba(0,0,0,.1)"><span class="ms" style="font-size:20px;color:#7b8a70">mood</span><span style="flex:1;font:400 14px Roboto;color:#9aa08f">Write a message</span><span class="ms" style="font-size:20px;color:#7b8a70">attach_file</span></div><div style="width:46px;height:46px;flex:none;border-radius:50%;background:#2f7d34;display:flex;align-items:center;justify-content:center"><span class="ms fill" style="font-size:22px;color:#fff">send</span></div></div>
|
||||||
|
</div>
|
||||||
|
<figcaption style="margin-top:14px;font:400 13px Roboto;color:#22301b;text-align:center">09 · Chat <span style="color:#7b8a70">(coming soon)</span></figcaption>
|
||||||
|
</figure>
|
||||||
|
<!--SCREENS_END-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
docs/mockups/seedees_material3_assets/logo.png
Normal file
BIN
docs/mockups/seedees_material3_assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/mockups/seedees_material3_assets/seedks.ttf
Normal file
BIN
docs/mockups/seedees_material3_assets/seedks.ttf
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue