From 48e9d157721b9a6a5622f0f23971a05f2967702f Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 8 Jul 2026 00:14:43 +0200 Subject: [PATCH] feat(ui): align inventory + detail with mockups (seed icons, green theme) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual pass toward docs/mockups (06 inventory, 07 item): - Bundle the seedks icon font (chars a–k → seed pictograms: jar, packet, spoon…) and a SeedGlyph/QuantityKindIcon helper. - Green app bar + pale-green canvas theme (buildTaneTheme). - Inventory list: rounded-square photo thumbnail or green initial avatar, bold title, scientific-name subtitle, trailing edit pencil, styled category headers, rounded search field, seed-glyph empty state. - Quantity selectors (quick-add + add-lot) show the seed pictogram + word; detail lot lines use the kind's glyph. - Detail header restyled to mockup 07: category link + scientific name on the left, photo on the right. watchInventory now also returns the linked species' scientific name (subtitle). 37 app tests green; seedks font verified in the Linux asset bundle. --- apps/app_seeds/fonts/seedks.ttf | Bin 0 -> 11016 bytes apps/app_seeds/lib/app.dart | 6 +- .../lib/data/variety_repository.dart | 23 ++++- .../lib/ui/inventory_list_screen.dart | 87 ++++++++++++++--- apps/app_seeds/lib/ui/quick_add_sheet.dart | 2 + apps/app_seeds/lib/ui/seed_glyph.dart | 87 +++++++++++++++++ apps/app_seeds/lib/ui/theme.dart | 35 +++++++ .../lib/ui/variety_detail_screen.dart | 90 ++++++++++++------ apps/app_seeds/pubspec.yaml | 5 + .../test/data/variety_detail_test.dart | 4 + docs/design/block1-status.md | 39 +++++++- docs/mockups/seedks-glyphs.png | Bin 0 -> 30106 bytes 12 files changed, 329 insertions(+), 49 deletions(-) create mode 100644 apps/app_seeds/fonts/seedks.ttf create mode 100644 apps/app_seeds/lib/ui/seed_glyph.dart create mode 100644 apps/app_seeds/lib/ui/theme.dart create mode 100644 docs/mockups/seedks-glyphs.png diff --git a/apps/app_seeds/fonts/seedks.ttf b/apps/app_seeds/fonts/seedks.ttf new file mode 100644 index 0000000000000000000000000000000000000000..47a1bb3c0271203ea8805b5839ab01e8616f5bed GIT binary patch literal 11016 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 literal 0 HcmV?d00001 diff --git a/apps/app_seeds/lib/app.dart b/apps/app_seeds/lib/app.dart index 2d415ee..1579674 100644 --- a/apps/app_seeds/lib/app.dart +++ b/apps/app_seeds/lib/app.dart @@ -9,6 +9,7 @@ import 'i18n/strings.g.dart'; import 'state/inventory_cubit.dart'; import 'state/variety_detail_cubit.dart'; import 'ui/inventory_list_screen.dart'; +import 'ui/theme.dart'; import 'ui/variety_detail_screen.dart'; /// Root widget. Provides the repository + inventory cubit to the tree and wires @@ -54,10 +55,7 @@ class TaneApp extends StatelessWidget { child: MaterialApp.router( onGenerateTitle: (context) => context.t.app.title, debugShowCheckedModeBanner: false, - theme: ThemeData( - colorSchemeSeed: const Color(0xFF4C7A34), // seed-green - useMaterial3: true, - ), + theme: buildTaneTheme(), locale: TranslationProvider.of(context).flutterLocale, supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ diff --git a/apps/app_seeds/lib/data/variety_repository.dart b/apps/app_seeds/lib/data/variety_repository.dart index 307fca3..34e1d81 100644 --- a/apps/app_seeds/lib/data/variety_repository.dart +++ b/apps/app_seeds/lib/data/variety_repository.dart @@ -12,6 +12,7 @@ class VarietyListItem extends Equatable { required this.id, required this.label, this.category, + this.scientificName, this.photo, }); @@ -19,11 +20,14 @@ class VarietyListItem extends Equatable { final String label; final String? category; + /// Scientific name of the linked species, shown as the tile subtitle. + final String? scientificName; + /// First photo (encrypted BLOB) for the avatar, or null → show an initial. final Uint8List? photo; @override - List get props => [id, label, category, photo]; + List get props => [id, label, category, scientificName, photo]; } /// One germination test on a lot; [rate] is derived (0..1), null when it can't @@ -154,12 +158,18 @@ class VarietyRepository { ]); return query.watch().asyncMap((rows) async { final photos = await _firstPhotosFor(rows.map((v) => v.id).toList()); + final sciNames = await _scientificNamesFor( + rows.map((v) => v.speciesId).whereType().toSet(), + ); return rows .map( (v) => VarietyListItem( id: v.id, label: v.label, category: v.category, + scientificName: v.speciesId == null + ? null + : sciNames[v.speciesId], photo: photos[v.id], ), ) @@ -189,6 +199,17 @@ class VarietyRepository { return byVariety; } + /// Maps each of [speciesIds] to its scientific name (one query). + Future> _scientificNamesFor( + Set speciesIds, + ) async { + if (speciesIds.isEmpty) return const {}; + final rows = await (_db.select( + _db.species, + )..where((s) => s.id.isIn(speciesIds))).get(); + return {for (final s in rows) s.id: s.scientificName}; + } + /// The 20-second quick-add: a [label] (required) plus an optional [category], /// an optional [quantity] (creates a Lot) and an optional [photoBytes] /// (stored as an encrypted BLOB Attachment). Returns the new Variety id. diff --git a/apps/app_seeds/lib/ui/inventory_list_screen.dart b/apps/app_seeds/lib/ui/inventory_list_screen.dart index f4a9f4c..9c2e52e 100644 --- a/apps/app_seeds/lib/ui/inventory_list_screen.dart +++ b/apps/app_seeds/lib/ui/inventory_list_screen.dart @@ -6,6 +6,8 @@ import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../state/inventory_cubit.dart'; import 'quick_add_sheet.dart'; +import 'seed_glyph.dart'; +import 'theme.dart'; /// The inventory home: a searchable list of seeds grouped by category, with a /// quick-add FAB. Driven by [InventoryCubit] over the encrypted DB stream. @@ -34,13 +36,19 @@ class InventoryListScreen extends StatelessWidget { return Column( children: [ Padding( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), child: TextField( key: const Key('inventory.search'), decoration: InputDecoration( hintText: t.inventory.searchHint, prefixIcon: const Icon(Icons.search), - border: const OutlineInputBorder(), + filled: true, + fillColor: Colors.white, + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), ), onChanged: context.read().search, ), @@ -66,10 +74,21 @@ class _InventoryBody extends StatelessWidget { return Center( child: Padding( padding: const EdgeInsets.all(24), - child: Text( - t.inventory.empty, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SeedGlyph( + SeedGlyphs.jars, + size: 72, + color: seedGreen.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + t.inventory.empty, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], ), ), ); @@ -99,11 +118,12 @@ class _CategoryHeader extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + padding: const EdgeInsets.fromLTRB(16, 18, 16, 6), child: Text( title, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.primary, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: seedGreenDark, ), ), ); @@ -117,16 +137,53 @@ class _VarietyTile extends StatelessWidget { @override Widget build(BuildContext context) { + void open() => context.push('/variety/${item.id}'); + return ListTile( + leading: _Avatar(item: item), + title: Text( + item.label, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: item.scientificName == null + ? null + : Text( + item.scientificName!, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + trailing: IconButton( + icon: const Icon(Icons.edit_outlined), + color: Colors.black38, + tooltip: context.t.common.edit, + onPressed: open, + ), + onTap: open, + ); + } +} + +/// Rounded-square photo thumbnail, or a green initial circle when there is none. +class _Avatar extends StatelessWidget { + const _Avatar({required this.item}); + + final VarietyListItem item; + + @override + Widget build(BuildContext context) { + final photo = item.photo; + if (photo != null) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory(photo, width: 48, height: 48, fit: BoxFit.cover), + ); + } final trimmed = item.label.trim(); final initial = trimmed.isEmpty ? '?' : trimmed.substring(0, 1).toUpperCase(); - return ListTile( - leading: item.photo != null - ? CircleAvatar(backgroundImage: MemoryImage(item.photo!)) - : CircleAvatar(child: Text(initial)), - title: Text(item.label), - onTap: () => context.push('/variety/${item.id}'), + return CircleAvatar( + backgroundColor: seedGreen.withValues(alpha: 0.15), + foregroundColor: seedGreenDark, + child: Text(initial), ); } } diff --git a/apps/app_seeds/lib/ui/quick_add_sheet.dart b/apps/app_seeds/lib/ui/quick_add_sheet.dart index 405d2ef..0921011 100644 --- a/apps/app_seeds/lib/ui/quick_add_sheet.dart +++ b/apps/app_seeds/lib/ui/quick_add_sheet.dart @@ -9,6 +9,7 @@ import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../state/quick_add_cubit.dart'; import 'quantity_kind_l10n.dart'; +import 'seed_glyph.dart'; /// Picks a photo and returns its bytes (or null if cancelled). Injected so /// widget tests can supply a fake instead of the camera plugin. @@ -103,6 +104,7 @@ class QuickAddSheet extends StatelessWidget { children: [ for (final kind in _quickKinds) ChoiceChip( + avatar: QuantityKindIcon(kind, size: 18), label: Text(quantityKindLabel(t, kind)), selected: state.quantityKind == kind, onSelected: (_) => cubit.selectQuantity(kind), diff --git a/apps/app_seeds/lib/ui/seed_glyph.dart b/apps/app_seeds/lib/ui/seed_glyph.dart new file mode 100644 index 0000000..086b62a --- /dev/null +++ b/apps/app_seeds/lib/ui/seed_glyph.dart @@ -0,0 +1,87 @@ +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; + +/// Renders a glyph from the bundled `seedks` icon font, whose characters `a`–`k` +/// map to seed-themed pictograms (jar, packet, spoon…). See +/// docs/mockups/seedks-glyphs.png. +class SeedGlyph extends StatelessWidget { + const SeedGlyph(this.char, {this.size = 24, this.color, super.key}); + + final String char; + final double size; + final Color? color; + + @override + Widget build(BuildContext context) { + return Text( + char, + style: TextStyle( + fontFamily: 'seedks', + fontSize: size, + height: 1, + color: color ?? Theme.of(context).colorScheme.primary, + ), + ); + } +} + +/// Named seedks glyphs (the character each pictogram is mapped to). +abstract final class SeedGlyphs { + static const jar = 'a'; + static const jars = 'b'; + static const search = 'c'; + static const share = 'd'; + static const sack = 'e'; + static const smallSpoon = 'f'; + static const bigSpoon = 'g'; + static const mug = 'h'; + static const envelope = 'i'; + static const pouring = 'j'; + static const scattered = 'k'; +} + +/// The seedks character for a quantity [kind], or null for plant-form/precise +/// kinds that have no seed pictogram (they fall back to a Material icon). +String? seedGlyphForKind(QuantityKind kind) => switch (kind) { + QuantityKind.aFew => SeedGlyphs.scattered, + QuantityKind.some => SeedGlyphs.smallSpoon, + QuantityKind.plenty => SeedGlyphs.jars, + QuantityKind.handful => SeedGlyphs.bigSpoon, + QuantityKind.pinch => SeedGlyphs.scattered, + QuantityKind.jar => SeedGlyphs.jar, + QuantityKind.packet => SeedGlyphs.envelope, + _ => null, +}; + +IconData _materialIconForKind(QuantityKind kind) => switch (kind) { + QuantityKind.grams => Icons.scale_outlined, + QuantityKind.count => Icons.tag, + QuantityKind.cob || QuantityKind.ear => Icons.grass, + QuantityKind.head || QuantityKind.seedHead => Icons.filter_vintage_outlined, + QuantityKind.fruit => Icons.local_florist_outlined, + QuantityKind.bulb || + QuantityKind.tuber || + QuantityKind.bunch => Icons.spa_outlined, + _ => Icons.eco_outlined, +}; + +/// A consistent icon for a [QuantityKind]: the seed glyph where one exists, +/// otherwise a Material fallback. +class QuantityKindIcon extends StatelessWidget { + const QuantityKindIcon(this.kind, {this.size = 24, this.color, super.key}); + + final QuantityKind kind; + final double size; + final Color? color; + + @override + Widget build(BuildContext context) { + final glyph = seedGlyphForKind(kind); + if (glyph != null) return SeedGlyph(glyph, size: size, color: color); + return Icon( + _materialIconForKind(kind), + size: size, + color: color ?? Theme.of(context).colorScheme.primary, + ); + } +} diff --git a/apps/app_seeds/lib/ui/theme.dart b/apps/app_seeds/lib/ui/theme.dart new file mode 100644 index 0000000..1126683 --- /dev/null +++ b/apps/app_seeds/lib/ui/theme.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +/// Seed-green palette drawn from the mockups (docs/mockups): a green app bar and +/// a pale-green canvas. +const seedGreen = Color(0xFF3C8B2E); +const seedGreenDark = Color(0xFF1B5E20); +const seedCanvas = Color(0xFFEAF4E2); +const seedLink = Color(0xFF1E88C0); + +ThemeData buildTaneTheme() { + final scheme = ColorScheme.fromSeed( + seedColor: seedGreen, + brightness: Brightness.light, + ); + return ThemeData( + useMaterial3: true, + colorScheme: scheme, + scaffoldBackgroundColor: seedCanvas, + appBarTheme: const AppBarTheme( + backgroundColor: seedGreen, + foregroundColor: Colors.white, + elevation: 0, + centerTitle: false, + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: seedGreen, + foregroundColor: Colors.white, + ), + bottomSheetTheme: const BottomSheetThemeData( + backgroundColor: seedCanvas, + surfaceTintColor: Colors.transparent, + ), + dividerTheme: const DividerThemeData(space: 1, thickness: 1), + ); +} diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index e0e2abc..1ef55a9 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -6,6 +6,8 @@ import '../data/species_repository.dart'; import '../data/variety_repository.dart'; import '../i18n/strings.g.dart'; import '../state/variety_detail_cubit.dart'; +import 'seed_glyph.dart'; +import 'theme.dart'; import 'quantity_kind_l10n.dart'; /// Read + edit view of a single variety: its photo, category, notes, other @@ -72,33 +74,7 @@ class _DetailView extends StatelessWidget { body: ListView( padding: const EdgeInsets.all(16), children: [ - if (detail.scientificName != null) ...[ - Text( - detail.scientificName!, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontStyle: FontStyle.italic, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 12), - ], - if (detail.photo != null) - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.memory( - detail.photo!, - height: 200, - width: double.infinity, - fit: BoxFit.cover, - ), - ), - if (detail.category != null) ...[ - const SizedBox(height: 12), - Align( - alignment: Alignment.centerLeft, - child: Chip(label: Text(detail.category!)), - ), - ], + _DetailHeader(detail: detail), if (detail.vernacularNames.isNotEmpty) ...[ const SizedBox(height: 16), _SectionTitle(t.detail.names), @@ -134,7 +110,9 @@ class _DetailView extends StatelessWidget { for (final lot in detail.lots) ListTile( contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.inventory_2_outlined), + leading: lot.quantity == null + ? const Icon(Icons.inventory_2_outlined) + : QuantityKindIcon(lot.quantity!.kind, size: 28), title: Text(_lotSubtitle(t, lot)), subtitle: lot.storageLocation == null ? null @@ -548,6 +526,7 @@ Future _showAddLotSheet(BuildContext context, VarietyDetailCubit cubit) { children: [ for (final kind in _addLotKinds) ChoiceChip( + avatar: QuantityKindIcon(kind, size: 18), label: Text(quantityKindLabel(t, kind)), selected: selectedKind == kind, onSelected: (_) => setState(() => selectedKind = kind), @@ -598,6 +577,61 @@ String? _nullIfBlank(String value) { return trimmed.isEmpty ? null : trimmed; } +/// Mockup-07 header: category link + scientific name on the left, the photo on +/// the right. +class _DetailHeader extends StatelessWidget { + const _DetailHeader({required this.detail}); + + final VarietyDetail detail; + + @override + Widget build(BuildContext context) { + final hasText = detail.category != null || detail.scientificName != null; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (detail.category != null) + Text( + detail.category!, + style: const TextStyle( + color: seedLink, + fontWeight: FontWeight.w600, + ), + ), + if (detail.scientificName != null) ...[ + const SizedBox(height: 2), + Text( + detail.scientificName!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontStyle: FontStyle.italic, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + if (detail.photo != null) ...[ + if (hasText) const SizedBox(width: 12), + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory( + detail.photo!, + width: 140, + height: 140, + fit: BoxFit.cover, + ), + ), + ], + ], + ); + } +} + class _SectionTitle extends StatelessWidget { const _SectionTitle(this.text); diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index 8d4e6c3..f2cf588 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -66,3 +66,8 @@ flutter: # Seed strings for slang live under lib/i18n (not a Flutter asset; compiled). assets: - assets/catalog/species.json + fonts: + # Custom seed-themed icon glyphs (chars a–k). See docs/mockups/seedks-glyphs.png. + - family: seedks + fonts: + - asset: fonts/seedks.ttf diff --git a/apps/app_seeds/test/data/variety_detail_test.dart b/apps/app_seeds/test/data/variety_detail_test.dart index f3cea69..435f222 100644 --- a/apps/app_seeds/test/data/variety_detail_test.dart +++ b/apps/app_seeds/test/data/variety_detail_test.dart @@ -83,6 +83,10 @@ void main() { final detail = await repo.watchVariety(id).first; expect(detail!.scientificName, 'Solanum lycopersicum'); expect(detail.category, 'Solanaceae'); // prefilled from family + + // The inventory list row also surfaces the scientific name (subtitle). + final row = (await repo.watchInventory().first).single; + expect(row.scientificName, 'Solanum lycopersicum'); }, ); diff --git a/docs/design/block1-status.md b/docs/design/block1-status.md index 2ac311c..54683d6 100644 --- a/docs/design/block1-status.md +++ b/docs/design/block1-status.md @@ -37,11 +37,48 @@ JSON por locale, compatible Weblate). Ver [`../../apps/app_seeds`](../../apps/ap - **Editar nombres vernáculos** ("también conocida como": hoy solo lectura; falta añadir/quitar — colección OR-Set). -- **Editar/borrar lotes** (se pueden añadir pero no modificar/eliminar). +- **Editar/borrar lotes** (se pueden añadir pero no modificar/eliminar). Falta + `updateLot`/`softDeleteLot` en el repo (hoy solo `addLot` en + [`variety_repository.dart`](../../apps/app_seeds/lib/data/variety_repository.dart)) + y la afordancia en las tiles de lote de + [`variety_detail_screen.dart`](../../apps/app_seeds/lib/ui/variety_detail_screen.dart). + El soft-delete ya tiene infraestructura (`isDeleted`/tombstone vía `SyncColumns`). +- **Editar cantidad del lote**: la UI de alta solo ofrece 6 `QuantityKind` como + chips; falta campo para cantidad numérica (`precise`) y etiqueta libre (`label`), + que el modelo (`Quantity`) ya soporta. - (Opcional) QR de recuperación de la semilla raíz; almacenamiento cifrado de fotos fuera de la BD (hoy van como BLOB cifrado dentro de la BD, que cumple la regla). +### Feedback de prueba (vjrj, 2026-07-07) + +Tras probar la app: no se pueden editar unidades/cantidad ni editar/borrar lotes +(solo añadir) — **esperado**, cubierto por los pendientes de arriba. Echa en falta +la iconografía de los mockups (ver nota siguiente). + +## Iconografía de los mockups (diseño futuro) 🎨 + +La app usa hoy **Material Icons genéricos**. Los iconos de los mockups **existen** ya +como font custom: **[`seedks.ttf`](../../seedks.ttf)** (raíz del repo, trackeado en +git), familia interna **"Seedks"**, **11 glifos** en `a`–`k` (U+0061–U+006B), de la +misma época que [`../icons.svg`](../icons.svg) (2018). Preview renderizado: +[`../mockups/seedks-glyphs.png`](../mockups/seedks-glyphs.png). + +Mapeo de glifos: + +| Glifo | Icono | Glifo | Icono | Glifo | Icono | +|---|---|---|---|---|---| +| `a` | tarro | `e` | saco/bolsa | `i` | sobre | +| `b` | tarros (varios) | `f` | cucharilla | `j` | sobre vertiendo | +| `c` | búsqueda (lupa) | `g` | cuchara | `k` | semillas sueltas | +| `d` | mano compartiendo | `h` | taza | | | + +**No** está cableado: falta declarar `fonts:` en +[`../../apps/app_seeds/pubspec.yaml`](../../apps/app_seeds/pubspec.yaml) y crear un +mapa codepoint→`IconData` (`fontFamily: 'Seedks'`). Recuperar la iconografía **no** +requiere SVGs nuevos. Iconos por familia/categoría y por unidad de cantidad = +trabajo de diseño para después del pulido del Bloque 1. + ## Fuera de alcance (Bloque 2, no empezar) 🚫 Ofertas, mensajería, relays, Nostr, red de confianza, Ğ1, sincronización entre diff --git a/docs/mockups/seedks-glyphs.png b/docs/mockups/seedks-glyphs.png new file mode 100644 index 0000000000000000000000000000000000000000..e290861a8e36943564f1b91ef3782c27bfde67be GIT binary patch literal 30106 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�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%0Av8v!da3gxQAb*pzY(=7)8kE zOT74ZedqW0?{`W-%TEK?3tC&AO;b_Py62NDkS6fz7fUC!;BNr7;N>9^f+7V{#GPLv l2;zH67=PI0=}&i#Nh>-x-{Vwq=U`+Y%6HV{^W{wa{|6iOPDB6z literal 0 HcmV?d00001