feat(web): tane.comunes.org landing + localized app screenshots
- Hugo site (site/): EN/ES intro + legal (generated from docs/legal), an
ES/EN About page from docs/{que-es-tane,what-is-tane}.md, seed-green theme,
responsive header, store-badge placeholders, multi-stage Hugo->nginx image.
- Golden screenshot harness (test/screenshots/): en,es,fr,de,pt,ja + RTL demo,
real fonts via DejaVu Sans + FontManifest; skip-by-default tag so CI stays
green. collect_screenshots.sh feeds site/ and fastlane phoneScreenshots.
- Drop editorial notes from the public explainer.
This commit is contained in:
parent
bbf2e97027
commit
8dce6f7027
117 changed files with 1958 additions and 4 deletions
188
apps/app_seeds/test/screenshots/screenshot_support.dart
Normal file
188
apps/app_seeds/test/screenshots/screenshot_support.dart
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
) async {
|
||||
// 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]);
|
||||
|
||||
// Varieties render with the app's default coloured-initial disc avatar —
|
||||
// the on-brand default when no photo is set — so no image decoding is needed.
|
||||
Future<String> add(String label, String family) async {
|
||||
final id = await repo.addQuickVariety(label: label, category: family);
|
||||
await (db.update(db.varieties)..where((v) => v.id.equals(id))).write(
|
||||
VarietiesCompanion(
|
||||
sowMonths: Value(sow),
|
||||
seedHarvestMonths: Value(harvest),
|
||||
),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
final tomato = await add('Cherry tomato', 'Solanaceae');
|
||||
await repo.addLot(
|
||||
varietyId: tomato,
|
||||
harvestYear: 2024,
|
||||
quantity: const Quantity(kind: QuantityKind.handful),
|
||||
originName: 'Community seed swap',
|
||||
abundance: Abundance.plentyToShare,
|
||||
offerStatus: OfferStatus.shared,
|
||||
);
|
||||
await repo.addLot(
|
||||
varietyId: tomato,
|
||||
harvestYear: 2023,
|
||||
quantity: const Quantity(kind: QuantityKind.packet, count: 1),
|
||||
offerStatus: OfferStatus.exchange,
|
||||
);
|
||||
|
||||
await add('Climbing bean', 'Fabaceae');
|
||||
await add('Sunflower', 'Asteraceae');
|
||||
await add('Sweet basil', 'Lamiaceae');
|
||||
await add('Rainbow chard', 'Amaranthaceae');
|
||||
await add('Purple carrot', 'Apiaceae');
|
||||
|
||||
return SeededInventory(tomato);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue