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.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 loadScreenshotFonts() async { if (_fontsLoaded) return; Future family(String name, List 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; for (final entry in manifest.cast>()) { 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 = >{ // 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 = ['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 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 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 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); }