228 lines
9.6 KiB
Dart
228 lines
9.6 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:commons_core/commons_core.dart';
|
|
import 'package:drift/drift.dart' show Value;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart' show FontLoader, rootBundle;
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:tane/app.dart' show materialLocaleFor;
|
|
import 'package:tane/data/species_repository.dart';
|
|
import 'package:tane/data/variety_repository.dart';
|
|
import 'package:tane/db/database.dart';
|
|
import 'package:tane/db/enums.dart';
|
|
import 'package:tane/domain/crop_calendar.dart' show monthsToMask;
|
|
import 'package:tane/i18n/strings.g.dart';
|
|
import 'package:tane/state/inventory_cubit.dart';
|
|
import 'package:tane/ui/theme.dart';
|
|
|
|
/// Marketing screenshot canvas: a tall phone at 3x. Logical 360x760.
|
|
const screenshotSize = Size(1080, 2280);
|
|
const screenshotPixelRatio = 3.0;
|
|
|
|
/// Locales we render real, translated screenshots for. Asturian (`ast`) is
|
|
/// omitted from store assets; the RTL demo is handled separately (the app has
|
|
/// no Arabic strings yet — see [ScreenshotLocale.rtlDemo]).
|
|
const screenshotLocales = <AppLocale>[
|
|
AppLocale.en,
|
|
AppLocale.es,
|
|
AppLocale.fr,
|
|
AppLocale.de,
|
|
AppLocale.pt,
|
|
AppLocale.ja,
|
|
];
|
|
|
|
bool _fontsLoaded = false;
|
|
|
|
/// Registers real glyph-bearing fonts so golden screenshots show text and icons
|
|
/// rather than the Ahem/tofu boxes flutter_test uses by default. Loads every
|
|
/// family declared in the test bundle's FontManifest.json (MaterialIcons, the
|
|
/// bundled Noto Sans JP/Arabic and seedks glyphs), then adds DejaVu Sans — a
|
|
/// bundled asset, not a font *family* — as the Latin text face so en/es/fr/de/pt
|
|
/// render legibly without needing Roboto.
|
|
Future<void> loadScreenshotFonts() async {
|
|
if (_fontsLoaded) return;
|
|
|
|
Future<void> family(String name, List<String> assets) async {
|
|
final loader = FontLoader(name);
|
|
for (final asset in assets) {
|
|
loader.addFont(rootBundle.load(asset));
|
|
}
|
|
await loader.load();
|
|
}
|
|
|
|
final manifest =
|
|
json.decode(await rootBundle.loadString('FontManifest.json'))
|
|
as List<dynamic>;
|
|
for (final entry in manifest.cast<Map<String, dynamic>>()) {
|
|
await family(
|
|
entry['family'] as String,
|
|
[for (final f in entry['fonts'] as List) f['asset'] as String],
|
|
);
|
|
}
|
|
|
|
await family('DejaVu Sans', [
|
|
'assets/fonts/DejaVuSans.ttf',
|
|
'assets/fonts/DejaVuSans-Bold.ttf',
|
|
]);
|
|
_fontsLoaded = true;
|
|
}
|
|
|
|
/// Localized sample labels so each screenshot reads in its own language. Family
|
|
/// names stay botanical Latin; only the human variety/offer names are localized.
|
|
/// English is the fallback (e.g. for `ja`, whose UI is itself partly English).
|
|
const sampleLabels = <String, Map<AppLocale, String>>{
|
|
// Inventory varieties
|
|
'maize': {AppLocale.en: 'Painted flint corn', AppLocale.es: 'Maíz rojo', AppLocale.fr: 'Maïs corné rouge', AppLocale.de: 'Roter Hartmais', AppLocale.pt: 'Milho pintado'},
|
|
'buckwheat': {AppLocale.en: 'Common buckwheat', AppLocale.es: 'Trigo sarraceno', AppLocale.fr: 'Sarrasin', AppLocale.de: 'Buchweizen', AppLocale.pt: 'Trigo-mourisco'},
|
|
'raspberry': {AppLocale.en: 'Wild raspberry', AppLocale.es: 'Frambuesa', AppLocale.fr: 'Framboise sauvage', AppLocale.de: 'Wilde Himbeere', AppLocale.pt: 'Framboesa'},
|
|
'tomato': {AppLocale.en: 'Slicing tomato', AppLocale.es: 'Tomate de ensalada', AppLocale.fr: 'Tomate à trancher', AppLocale.de: 'Fleischtomate', AppLocale.pt: 'Tomate para salada'},
|
|
'chilli': {AppLocale.en: 'Guajillo chilli', AppLocale.es: 'Chile guajillo', AppLocale.fr: 'Piment guajillo', AppLocale.de: 'Guajillo-Chili', AppLocale.pt: 'Pimenta guajillo'},
|
|
'origin': {AppLocale.en: 'Community seed swap', AppLocale.es: 'Intercambio de semillas', AppLocale.fr: 'Troc de graines', AppLocale.de: 'Saatgut-Tausch', AppLocale.pt: 'Troca de sementes'},
|
|
// Market offers
|
|
'cherryTomato': {AppLocale.en: 'Cherry tomato', AppLocale.es: 'Tomate cherry', AppLocale.fr: 'Tomate cerise', AppLocale.de: 'Kirschtomate', AppLocale.pt: 'Tomate-cereja'},
|
|
'bean': {AppLocale.en: 'Climbing bean', AppLocale.es: 'Judía de enrame', AppLocale.fr: 'Haricot à rames', AppLocale.de: 'Stangenbohne', AppLocale.pt: 'Feijão-trepador'},
|
|
'sunflower': {AppLocale.en: 'Sunflower', AppLocale.es: 'Girasol', AppLocale.fr: 'Tournesol', AppLocale.de: 'Sonnenblume', AppLocale.pt: 'Girassol'},
|
|
'basil': {AppLocale.en: 'Sweet basil', AppLocale.es: 'Albahaca', AppLocale.fr: 'Basilic', AppLocale.de: 'Basilikum', AppLocale.pt: 'Manjericão'},
|
|
'carrot': {AppLocale.en: 'Purple carrot', AppLocale.es: 'Zanahoria morada', AppLocale.fr: 'Carotte violette', AppLocale.de: 'Violette Karotte', AppLocale.pt: 'Cenoura roxa'},
|
|
};
|
|
|
|
/// The sample label for [key] in [locale], falling back to English.
|
|
String labelFor(String key, AppLocale locale) {
|
|
final m = sampleLabels[key]!;
|
|
return m[locale] ?? m[AppLocale.en]!;
|
|
}
|
|
|
|
const _fontFallbacks = <String>['Noto Sans JP', 'Noto Sans Arabic'];
|
|
|
|
/// The real app theme, but with every text style pinned to the bundled
|
|
/// [DejaVu Sans] so screenshots render legible glyphs (see [loadScreenshotFonts]).
|
|
ThemeData screenshotTheme() {
|
|
final base = buildTaneTheme();
|
|
return base.copyWith(
|
|
textTheme: base.textTheme.apply(
|
|
fontFamily: 'DejaVu Sans',
|
|
fontFamilyFallback: _fontFallbacks,
|
|
),
|
|
primaryTextTheme: base.primaryTextTheme.apply(
|
|
fontFamily: 'DejaVu Sans',
|
|
fontFamilyFallback: _fontFallbacks,
|
|
),
|
|
appBarTheme: base.appBarTheme.copyWith(
|
|
titleTextStyle: base.appBarTheme.titleTextStyle?.copyWith(
|
|
fontFamily: 'DejaVu Sans',
|
|
fontFamilyFallback: _fontFallbacks,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Wraps [child] as a full screen for capture: the real green theme with
|
|
/// screenshot fonts, the given [locale], repositories and an [InventoryCubit].
|
|
/// Pass [textDirection] to force RTL for the layout demo.
|
|
Widget screenshotApp({
|
|
required VarietyRepository repository,
|
|
required SpeciesRepository species,
|
|
required Widget child,
|
|
AppLocale locale = AppLocale.en,
|
|
TextDirection? textDirection,
|
|
}) {
|
|
LocaleSettings.setLocaleSync(locale);
|
|
return TranslationProvider(
|
|
child: MultiRepositoryProvider(
|
|
providers: [
|
|
RepositoryProvider.value(value: repository),
|
|
RepositoryProvider.value(value: species),
|
|
],
|
|
child: BlocProvider(
|
|
create: (_) => InventoryCubit(repository),
|
|
child: MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
theme: screenshotTheme(),
|
|
locale: materialLocaleFor(locale.flutterLocale),
|
|
supportedLocales: AppLocaleUtils.supportedLocales,
|
|
localizationsDelegates: const [
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
home: textDirection == null
|
|
? child
|
|
: Directionality(textDirection: textDirection, child: child),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// One seeded variety plus the id of a "hero" variety used for the detail shot.
|
|
class SeededInventory {
|
|
const SeededInventory(this.heroVarietyId);
|
|
final String heroVarietyId;
|
|
}
|
|
|
|
/// Seeds an attractive, internationally-neutral inventory: a spread of
|
|
/// colour-coded botanical families, lots with years/quantities/origins, photo
|
|
/// avatars, and crop-calendar month masks so the calendar screen is populated.
|
|
/// Returns the hero variety (a cherry tomato with several lots) for detail.
|
|
Future<SeededInventory> seedShowcase(
|
|
AppDatabase db,
|
|
VarietyRepository repo,
|
|
AppLocale locale,
|
|
) async {
|
|
String l(String key) => labelFor(key, locale);
|
|
// Sow in spring, harvest seed in late summer — visible in the calendar.
|
|
final sow = monthsToMask(const [3, 4, 5]);
|
|
final harvest = monthsToMask(const [8, 9]);
|
|
|
|
// Most varieties use the app's default coloured-initial disc avatar; the hero
|
|
// (maize) carries a real photo (below).
|
|
Future<String> add(String label, String family, {Uint8List? photo}) async {
|
|
final id = await repo.addQuickVariety(
|
|
label: label,
|
|
category: family,
|
|
photoBytes: photo,
|
|
);
|
|
await (db.update(db.varieties)..where((v) => v.id.equals(id))).write(
|
|
VarietiesCompanion(
|
|
sowMonths: Value(sow),
|
|
seedHarvestMonths: Value(harvest),
|
|
),
|
|
);
|
|
return id;
|
|
}
|
|
|
|
// Example photos are CC0 / public domain (see fixtures/CREDITS.md), except the
|
|
// maize which is vjrj's own; loaded from the committed fixtures (CWD is the
|
|
// package root under `flutter test`). The maize is the hero for the detail shot.
|
|
Future<Uint8List> photo(String name) =>
|
|
File('test/screenshots/fixtures/$name.jpg').readAsBytes();
|
|
|
|
final maize = await add(l('maize'), 'Poaceae', photo: await photo('maize'));
|
|
await repo.addLot(
|
|
varietyId: maize,
|
|
harvestYear: 2024,
|
|
quantity: const Quantity(kind: QuantityKind.cob, count: 6),
|
|
originName: l('origin'),
|
|
abundance: Abundance.plentyToShare,
|
|
offerStatus: OfferStatus.shared,
|
|
);
|
|
await repo.addLot(
|
|
varietyId: maize,
|
|
harvestYear: 2023,
|
|
quantity: const Quantity(kind: QuantityKind.packet, count: 1),
|
|
offerStatus: OfferStatus.exchange,
|
|
);
|
|
|
|
// The rest sit in families that sort at/after Poaceae, so the maize stays the
|
|
// first, top-of-list group in the inventory shot. Each carries a CC0/PD photo.
|
|
await add(l('buckwheat'), 'Polygonaceae', photo: await photo('buckwheat'));
|
|
await add(l('raspberry'), 'Rosaceae', photo: await photo('raspberry'));
|
|
await add(l('tomato'), 'Solanaceae', photo: await photo('tomato'));
|
|
await add(l('chilli'), 'Solanaceae', photo: await photo('pepper'));
|
|
|
|
return SeededInventory(maize);
|
|
}
|