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:
vjrj 2026-07-09 11:51:59 +02:00
parent f5c36f2369
commit 42c16c0e3f
24 changed files with 1960 additions and 416 deletions

View 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);
},
);
}
}