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.
|
|
@ -11,3 +11,13 @@
|
|||
#
|
||||
# A genuinely slower (but finite) test can opt out with @Timeout(Duration(...)).
|
||||
timeout: 90s
|
||||
|
||||
tags:
|
||||
# Localized store/landing screenshots (test/screenshots/). These are asset
|
||||
# GENERATION, not a correctness gate, and golden pixel-matching is sensitive
|
||||
# to OS/font/Flutter version — so they must NOT run in the normal suite (incl.
|
||||
# CI's `flutter test --coverage`). Run them explicitly:
|
||||
# flutter test --run-skipped --tags screenshots test/screenshots/
|
||||
# add --update-goldens to (re)write the PNGs.
|
||||
screenshots:
|
||||
skip: "asset generation; run with --run-skipped --tags screenshots"
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 184 KiB |
43
apps/app_seeds/test/screenshots/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Localized app screenshots
|
||||
|
||||
Golden-based harness that renders key screens as PNGs in several languages, for
|
||||
the landing site (`tane.comunes.org`) and the app-store listings. No emulator —
|
||||
it runs under `flutter test`, so it's deterministic and CI-friendly.
|
||||
|
||||
## Regenerate
|
||||
|
||||
```sh
|
||||
cd apps/app_seeds
|
||||
flutter test --update-goldens --run-skipped --tags screenshots test/screenshots/screenshots_test.dart
|
||||
tool/collect_screenshots.sh # copy PNGs into site/ and fastlane/
|
||||
```
|
||||
|
||||
Drop `--update-goldens` to *verify* the committed PNGs still match. The
|
||||
`screenshots` tag is **skipped by default** (`dart_test.yaml`) — so it never
|
||||
runs in the normal suite or CI's `flutter test --coverage`, where golden pixel
|
||||
differences across OS/font versions would cause false failures. `--run-skipped`
|
||||
opts it back in.
|
||||
|
||||
## What it produces
|
||||
|
||||
`goldens/<locale>/<screen>.png` for **en, es, fr, de, pt, ja**, screens
|
||||
**home · inventory · market · calendar · detail**, plus `goldens/rtl/inventory.png`.
|
||||
The market is rendered populated via a seeded `OffersCubit` with a no-op transport
|
||||
(no network); home/calendar/detail/inventory render from the repository alone.
|
||||
|
||||
- Fonts: real glyphs come from `loadScreenshotFonts()` — every family in
|
||||
`FontManifest.json` (MaterialIcons + bundled Noto/seedks) plus the bundled
|
||||
**DejaVu Sans** as the Latin text face. No Roboto needed.
|
||||
- `rtl/` is a **layout demo**, not a locale: English strings forced
|
||||
right-to-left to prove the design is RTL-safe. The app ships no Arabic
|
||||
translation yet, so there is deliberately no `ar/` with Arabic copy.
|
||||
- Partial locales (e.g. `ja`) fall back to English for untranslated strings —
|
||||
that's the app's real runtime behavior, shown honestly.
|
||||
- These are raw device-canvas frames (no marketing captions or device bezels).
|
||||
Add those downstream only if a store rejects plain screenshots.
|
||||
|
||||
## Adding a screen or locale
|
||||
|
||||
Edit `screenshotLocales` / the `screens` map in `screenshots_test.dart`. Screens
|
||||
must render from the repository + cubits alone (no live Nostr/network stack), so
|
||||
inventory/detail/calendar are the safe, self-contained set.
|
||||
BIN
apps/app_seeds/test/screenshots/goldens/de/calendar.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/de/detail.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/de/home.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/de/inventory.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/de/market.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/en/calendar.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/en/detail.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/en/home.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/en/inventory.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/en/market.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/es/calendar.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/es/detail.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/es/home.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/es/inventory.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/es/market.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/fr/calendar.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/fr/detail.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/fr/home.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/fr/inventory.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/fr/market.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/ja/calendar.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/ja/detail.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/ja/home.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/ja/inventory.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/ja/market.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/pt/calendar.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/pt/detail.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/pt/home.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/pt/inventory.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/pt/market.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
apps/app_seeds/test/screenshots/goldens/rtl/inventory.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
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);
|
||||
}
|
||||
161
apps/app_seeds/test/screenshots/screenshots_test.dart
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
@Tags(['screenshots'])
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/species_repository.dart';
|
||||
import 'package:tane/data/variety_repository.dart';
|
||||
import 'package:tane/db/database.dart';
|
||||
import 'package:tane/i18n/strings.g.dart';
|
||||
import 'package:tane/state/offers_cubit.dart';
|
||||
import 'package:tane/state/variety_detail_cubit.dart';
|
||||
import 'package:tane/ui/calendar_screen.dart';
|
||||
import 'package:tane/ui/home_screen.dart';
|
||||
import 'package:tane/ui/inventory_list_screen.dart';
|
||||
import 'package:tane/ui/market_screen.dart';
|
||||
import 'package:tane/ui/variety_detail_screen.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
import 'screenshot_support.dart';
|
||||
|
||||
/// Generates localized marketing/store screenshots as golden PNGs.
|
||||
///
|
||||
/// Run (writes the PNGs) — the `screenshots` tag is skipped by default
|
||||
/// (dart_test.yaml), so `--run-skipped` is required:
|
||||
/// flutter test --update-goldens --run-skipped --tags screenshots test/screenshots/screenshots_test.dart
|
||||
///
|
||||
/// Output: test/screenshots/goldens/`<locale>`/`<screen>`.png for en/es/fr/de/pt/ja,
|
||||
/// plus goldens/rtl/ — a right-to-left *layout* demo (English strings mirrored;
|
||||
/// the app ships no Arabic translation yet, so this proves the design is RTL-safe
|
||||
/// rather than claiming an Arabic locale). These are raw device-canvas frames
|
||||
/// with no marketing chrome; add captions/device frames downstream if a store
|
||||
/// requires them. `tool/collect_screenshots.sh` copies them into site/ and fastlane/.
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
await loadScreenshotFonts();
|
||||
// Touch mode never draws focus rings, so no highlight frames the FAB/fields.
|
||||
FocusManager.instance.highlightStrategy =
|
||||
FocusHighlightStrategy.alwaysTouch;
|
||||
});
|
||||
|
||||
late AppDatabase db;
|
||||
late VarietyRepository repo;
|
||||
late SpeciesRepository species;
|
||||
late SeededInventory seeded;
|
||||
|
||||
setUp(() async {
|
||||
db = newTestDatabase();
|
||||
repo = newTestRepository(db);
|
||||
species = newTestSpeciesRepository(db);
|
||||
seeded = await seedShowcase(db, repo);
|
||||
});
|
||||
tearDown(() => db.close());
|
||||
|
||||
Future<void> capture(WidgetTester tester, String dir, String name) async {
|
||||
tester.view.physicalSize = screenshotSize;
|
||||
tester.view.devicePixelRatio = screenshotPixelRatio;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byType(MaterialApp),
|
||||
matchesGoldenFile('goldens/$dir/$name.png'),
|
||||
);
|
||||
await disposeTree(tester);
|
||||
}
|
||||
|
||||
// A POPULATED market: a seeded OffersCubit (no transport) feeding MarketBody,
|
||||
// so the offer list renders with no live discovery stream to hang
|
||||
// pumpAndSettle. (Live discovery is exercised by offers_cubit_test.dart; see
|
||||
// the note in market_screen_test.dart on why we don't drive it via widgets.)
|
||||
Future<Widget> market() async => Builder(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.t.market.title),
|
||||
actions: [IconButton(icon: const Icon(Icons.tune), onPressed: () {})],
|
||||
),
|
||||
body: BlocProvider<OffersCubit>(
|
||||
create: (_) => _SeededOffersCubit(_sampleOffers()),
|
||||
child: MarketBody(onConfigure: () {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Each entry renders one screen; the runner re-seeds and re-locales per shot.
|
||||
final screens = <String, Future<Widget> Function()>{
|
||||
'home': () async => const HomeScreen(marketEnabled: true),
|
||||
'inventory': () async => const InventoryListScreen(),
|
||||
'market': market,
|
||||
'calendar': () async => const CalendarScreen(initialMonth: 4),
|
||||
'detail': () async => BlocProvider(
|
||||
create: (_) => VarietyDetailCubit(repo, seeded.heroVarietyId),
|
||||
child: const VarietyDetailScreen(),
|
||||
),
|
||||
};
|
||||
|
||||
for (final locale in screenshotLocales) {
|
||||
for (final entry in screens.entries) {
|
||||
testWidgets('${locale.languageCode}/${entry.key}', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
screenshotApp(
|
||||
repository: repo,
|
||||
species: species,
|
||||
locale: locale,
|
||||
child: await entry.value(),
|
||||
),
|
||||
);
|
||||
await capture(tester, locale.languageCode, entry.key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// RTL layout demo: base (English) strings, forced right-to-left, so the
|
||||
// mirrored inventory proves the design is RTL-safe. Not a translated locale.
|
||||
testWidgets('rtl/inventory', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
screenshotApp(
|
||||
repository: repo,
|
||||
species: species,
|
||||
locale: AppLocale.en,
|
||||
textDirection: TextDirection.rtl,
|
||||
child: const InventoryListScreen(),
|
||||
),
|
||||
);
|
||||
await capture(tester, 'rtl', 'inventory');
|
||||
});
|
||||
}
|
||||
|
||||
/// An [OffersCubit] pre-loaded with a fixed offer list. It carries a no-op
|
||||
/// transport (never called — offers are pre-seeded) purely so `isOnline` is
|
||||
/// true and MarketBody renders the list instead of the "can't reach" state.
|
||||
class _SeededOffersCubit extends OffersCubit {
|
||||
_SeededOffersCubit(List<Offer> offers) : super(_NoopOfferTransport()) {
|
||||
emit(state.copyWith(offers: offers, hasSearched: true, areaGeohash: 'sp3e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// A transport that is present (so the market reads as online) but never used.
|
||||
class _NoopOfferTransport implements OfferTransport {
|
||||
@override
|
||||
Stream<Offer> discover(DiscoveryQuery query) => const Stream.empty();
|
||||
@override
|
||||
Future<PublishResult> publish(Offer offer) => throw UnimplementedError();
|
||||
@override
|
||||
Future<void> retract(String offerId) async {}
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
/// A small, internationally-neutral set of nearby offers across gift / swap /
|
||||
/// sale and colour-coded botanical families.
|
||||
List<Offer> _sampleOffers() => [
|
||||
Offer(id: 'o1', authorPubkeyHex: 'a1' * 32, summary: 'Cherry tomato', type: OfferType.gift, category: 'Solanaceae', approxGeohash: 'sp3e9', isOrganic: true),
|
||||
Offer(id: 'o2', authorPubkeyHex: 'a2' * 32, summary: 'Climbing bean', type: OfferType.exchange, category: 'Fabaceae', approxGeohash: 'sp3e8'),
|
||||
Offer(id: 'o3', authorPubkeyHex: 'a3' * 32, summary: 'Sunflower', type: OfferType.sale, category: 'Asteraceae', approxGeohash: 'sp3ec', priceAmount: 2, priceCurrency: '€'),
|
||||
Offer(id: 'o4', authorPubkeyHex: 'a4' * 32, summary: 'Sweet basil', type: OfferType.gift, category: 'Lamiaceae', approxGeohash: 'sp3e2', isOrganic: true),
|
||||
Offer(id: 'o5', authorPubkeyHex: 'a5' * 32, summary: 'Purple carrot', type: OfferType.exchange, category: 'Apiaceae', approxGeohash: 'sp3ef'),
|
||||
];
|
||||
54
apps/app_seeds/tool/collect_screenshots.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
# Copies the localized golden screenshots into the two places that consume them:
|
||||
# 1. the Hugo landing site (site/assets/screenshots/<locale>/)
|
||||
# 2. the Play Store metadata (fastlane/metadata/android/<play-locale>/images/phoneScreenshots/)
|
||||
#
|
||||
# Regenerate the PNGs first:
|
||||
# flutter test --update-goldens --tags screenshots test/screenshots/screenshots_test.dart
|
||||
# then run this from apps/app_seeds/:
|
||||
# tool/collect_screenshots.sh
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # apps/app_seeds
|
||||
repo_root="$(cd "$here/../.." && pwd)"
|
||||
goldens="$here/test/screenshots/goldens"
|
||||
site_dir="$repo_root/site/assets/screenshots"
|
||||
fastlane_dir="$here/fastlane/metadata/android"
|
||||
|
||||
if [[ ! -d "$goldens" ]]; then
|
||||
echo "No goldens at $goldens — run the screenshots test with --update-goldens first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# App locale (golden dir) -> Play Store locale (fastlane dir). 'rtl' is a layout
|
||||
# demo only (not a real locale) and goes to the site, not the stores.
|
||||
declare -A play_locale=(
|
||||
[en]=en-US [es]=es-ES [fr]=fr-FR [de]=de-DE [pt]=pt-PT [ja]=ja-JP
|
||||
)
|
||||
|
||||
for dir in "$goldens"/*/; do
|
||||
locale="$(basename "$dir")"
|
||||
|
||||
# 1) Landing site: every locale, including the RTL demo.
|
||||
mkdir -p "$site_dir/$locale"
|
||||
cp "$dir"*.png "$site_dir/$locale/"
|
||||
|
||||
# 2) Play Store: only locales that ALREADY have text metadata (a title.txt).
|
||||
# A locale dir with images but no store text is invalid for `fastlane supply`,
|
||||
# so we don't create image-only locales — add the text first, then it's picked up.
|
||||
play="${play_locale[$locale]:-}"
|
||||
if [[ -n "$play" && -f "$fastlane_dir/$play/title.txt" ]]; then
|
||||
shots="$fastlane_dir/$play/images/phoneScreenshots"
|
||||
mkdir -p "$shots"
|
||||
# Number them so the store shows them in a deliberate order.
|
||||
i=1
|
||||
for name in home inventory market calendar detail; do
|
||||
[[ -f "$dir$name.png" ]] && cp "$dir$name.png" "$shots/$i.png" && i=$((i + 1))
|
||||
done
|
||||
echo " store: $play ($((i - 1)) shots)"
|
||||
fi
|
||||
echo "site: $locale"
|
||||
done
|
||||
|
||||
echo "Done. To add a Play locale beyond en-US/es-ES, create its title/short/full"
|
||||
echo "description text under fastlane/metadata/android/<locale>/ first, then re-run."
|
||||