feat(shell): real sprout logo on home + basic Settings screen

- Bundle the real sprout logo (docs/logo.png → assets/logo.png) and show it on
  the home menu instead of a Material icon.
- Settings screen: switch language (Español / English / system) via slang, plus
  an About tile (logo, app name, one-line description). Drawer "Settings" is now
  active and routes to /settings.

Tests: drawer Settings opens the screen and shows the language options. 54 green.
This commit is contained in:
vjrj 2026-07-08 14:05:12 +02:00
parent 46ef2c61cd
commit 4b209d6752
12 changed files with 196 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -10,6 +10,7 @@ import 'state/inventory_cubit.dart';
import 'state/variety_detail_cubit.dart';
import 'ui/home_screen.dart';
import 'ui/inventory_list_screen.dart';
import 'ui/settings_screen.dart';
import 'ui/theme.dart';
import 'ui/variety_detail_screen.dart';
@ -27,6 +28,10 @@ class TaneApp extends StatelessWidget {
return GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsScreen(),
),
GoRoute(
path: '/inventory',
builder: (context, state) => BlocProvider(

View file

@ -23,6 +23,14 @@
"following": "Following",
"settings": "Settings"
},
"settings": {
"language": "Language",
"systemLanguage": "System language",
"langEs": "Español",
"langEn": "English",
"about": "About",
"aboutText": "Local-first, encrypted inventory for traditional seeds. AGPL-3.0."
},
"inventory": {
"title": "Inventory",
"searchHint": "Search seeds",

View file

@ -23,6 +23,14 @@
"following": "Siguiendo",
"settings": "Ajustes"
},
"settings": {
"language": "Idioma",
"systemLanguage": "Idioma del sistema",
"langEs": "Español",
"langEn": "English",
"about": "Acerca de",
"aboutText": "Inventario local y cifrado para semillas tradicionales. AGPL-3.0."
},
"inventory": {
"title": "Inventario",
"searchHint": "Buscar semillas",

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 244 (122 per locale)
/// Strings: 256 (128 per locale)
///
/// Built on 2026-07-08 at 10:27 UTC
/// Built on 2026-07-08 at 12:01 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View file

@ -44,6 +44,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final Translations$common$en common = Translations$common$en.internal(_root);
late final Translations$home$en home = Translations$home$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$inventory$en inventory = Translations$inventory$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);
@ -139,6 +140,33 @@ class Translations$menu$en {
String get settings => 'Settings';
}
// Path: settings
class Translations$settings$en {
Translations$settings$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Language'
String get language => 'Language';
/// en: 'System language'
String get systemLanguage => 'System language';
/// en: 'Español'
String get langEs => 'Español';
/// en: 'English'
String get langEn => 'English';
/// en: 'About'
String get about => 'About';
/// en: 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.'
String get aboutText => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.';
}
// Path: inventory
class Translations$inventory$en {
Translations$inventory$en.internal(this._root);
@ -752,6 +780,12 @@ extension on Translations {
'menu.wishlist' => 'Wishlist',
'menu.following' => 'Following',
'menu.settings' => 'Settings',
'settings.language' => 'Language',
'settings.systemLanguage' => 'System language',
'settings.langEs' => 'Español',
'settings.langEn' => 'English',
'settings.about' => 'About',
'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.',
'inventory.title' => 'Inventory',
'inventory.searchHint' => 'Search seeds',
'inventory.empty' => 'No seeds yet. Tap + to add your first.',

View file

@ -43,6 +43,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$common$es common = _Translations$common$es._(_root);
@override late final _Translations$home$es home = _Translations$home$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$inventory$es inventory = _Translations$inventory$es._(_root);
@override late final _Translations$quickAdd$es quickAdd = _Translations$quickAdd$es._(_root);
@override late final _Translations$detail$es detail = _Translations$detail$es._(_root);
@ -106,6 +107,21 @@ class _Translations$menu$es extends Translations$menu$en {
@override String get settings => 'Ajustes';
}
// Path: settings
class _Translations$settings$es extends Translations$settings$en {
_Translations$settings$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get language => 'Idioma';
@override String get systemLanguage => 'Idioma del sistema';
@override String get langEs => 'Español';
@override String get langEn => 'English';
@override String get about => 'Acerca de';
@override String get aboutText => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.';
}
// Path: inventory
class _Translations$inventory$es extends Translations$inventory$en {
_Translations$inventory$es._(TranslationsEs root) : this._root = root, super.internal(root);
@ -529,6 +545,12 @@ extension on TranslationsEs {
'menu.wishlist' => 'Lista de deseos',
'menu.following' => 'Siguiendo',
'menu.settings' => 'Ajustes',
'settings.language' => 'Idioma',
'settings.systemLanguage' => 'Idioma del sistema',
'settings.langEs' => 'Español',
'settings.langEn' => 'English',
'settings.about' => 'Acerca de',
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
'inventory.title' => 'Inventario',
'inventory.searchHint' => 'Buscar semillas',
'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.',

View file

@ -38,7 +38,14 @@ class AppDrawer extends StatelessWidget {
_SoonItem(icon: Icons.group_outlined, label: t.menu.following),
const Spacer(),
const Divider(),
_SoonItem(icon: Icons.settings_outlined, label: t.menu.settings),
_ActiveItem(
icon: const Icon(Icons.settings_outlined),
label: t.menu.settings,
onTap: () {
Navigator.of(context).pop();
context.go('/settings');
},
),
],
),
),

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../i18n/strings.g.dart';
import 'app_drawer.dart';
@ -31,11 +30,10 @@ class HomeScreen extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(
Symbols.eco,
size: 120,
color: seedGreen,
fill: 1,
Image.asset(
'assets/logo.png',
height: 140,
fit: BoxFit.contain,
),
const SizedBox(height: 40),
_MenuButton(

View file

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import '../i18n/strings.g.dart';
import 'theme.dart';
/// Basic settings: app language and an "about" section.
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
final t = context.t;
final current = TranslationProvider.of(context).flutterLocale.languageCode;
return Scaffold(
appBar: AppBar(title: Text(t.menu.settings)),
body: ListView(
children: [
_SectionHeader(t.settings.language),
_LanguageTile(
label: t.settings.langEs,
selected: current == 'es',
onTap: () => LocaleSettings.setLocale(AppLocale.es),
),
_LanguageTile(
label: t.settings.langEn,
selected: current == 'en',
onTap: () => LocaleSettings.setLocale(AppLocale.en),
),
ListTile(
leading: const Icon(Icons.smartphone_outlined),
title: Text(t.settings.systemLanguage),
onTap: () => LocaleSettings.useDeviceLocale(),
),
const Divider(),
_SectionHeader(t.settings.about),
ListTile(
leading: Image.asset('assets/logo.png', width: 32),
title: Text(t.app.title),
subtitle: Text(t.settings.aboutText),
),
],
),
);
}
}
class _SectionHeader extends StatelessWidget {
const _SectionHeader(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Text(
text,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: seedGreenDark,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _LanguageTile extends StatelessWidget {
const _LanguageTile({
required this.label,
required this.selected,
required this.onTap,
});
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(label),
trailing: selected ? const Icon(Icons.check, color: seedGreen) : null,
onTap: onTap,
);
}
}

View file

@ -68,6 +68,7 @@ flutter:
# Seed strings for slang live under lib/i18n (not a Flutter asset; compiled).
assets:
- assets/catalog/species.json
- assets/logo.png
fonts:
# Custom seed-themed icon glyphs (chars ak). See docs/mockups/seedks-glyphs.png.
- family: seedks

View file

@ -52,4 +52,22 @@ void main() {
expect(find.text('No seeds yet. Tap + to add your first.'), findsOneWidget);
await disposeTree(tester);
});
testWidgets('drawer Settings opens the settings 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();
expect(find.text('Language'), findsOneWidget);
expect(find.text('Español'), findsOneWidget);
await disposeTree(tester);
});
}