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:
parent
f5c36f2369
commit
42c16c0e3f
24 changed files with 1960 additions and 416 deletions
83
apps/app_seeds/test/ui/about_screen_test.dart
Normal file
83
apps/app_seeds/test/ui/about_screen_test.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:tane/app.dart';
|
||||
import 'package:tane/i18n/strings.g.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
void main() {
|
||||
Widget app(db) => TranslationProvider(
|
||||
child: TaneApp(
|
||||
repository: newTestRepository(db),
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
PackageInfo.setMockInitialValues(
|
||||
appName: 'Tanemaki',
|
||||
packageName: 'org.comunes.tane',
|
||||
version: '0.1.0',
|
||||
buildNumber: '1',
|
||||
buildSignature: '',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Settings About tile opens the About screen', (tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final db = newTestDatabase();
|
||||
addTearDown(db.close);
|
||||
|
||||
await tester.pumpWidget(app(db));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Settings'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('About Tanemaki'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Explanation text and kanji are shown above the fold.
|
||||
expect(find.text('種まき'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('helps people and collectives'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('digital Plantare'), findsOneWidget);
|
||||
|
||||
// License and version live below the fold; scroll them into view.
|
||||
await tester.scrollUntilVisible(find.text('AGPL-3.0'), 200);
|
||||
expect(find.text('AGPL-3.0'), findsOneWidget);
|
||||
expect(find.text('0.1.0 (1)'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('About screen opens the open-source licenses page', (
|
||||
tester,
|
||||
) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final db = newTestDatabase();
|
||||
addTearDown(db.close);
|
||||
|
||||
await tester.pumpWidget(app(db));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Settings'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('About Tanemaki'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.scrollUntilVisible(find.text('Open-source licenses'), 200);
|
||||
await tester.tap(find.text('Open-source licenses'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Flutter's built-in LicensePage renders the app name.
|
||||
expect(find.byType(LicensePage), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
}
|
||||
|
|
@ -23,7 +23,12 @@ void main() {
|
|||
|
||||
expect(find.text('Your inventory'), findsOneWidget);
|
||||
expect(find.text('Open market'), findsOneWidget);
|
||||
expect(find.text('Coming soon'), findsOneWidget); // market is Block 2
|
||||
expect(find.text('COMING SOON'), findsOneWidget); // market is Block 2
|
||||
|
||||
// Redesign copy: tagline + the two destination subtitles.
|
||||
expect(find.text('Share and grow local seeds'), findsOneWidget);
|
||||
expect(find.text('Manage your seeds'), findsOneWidget);
|
||||
expect(find.text('Browse and exchange'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('home.inventory')));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -53,6 +58,31 @@ void main() {
|
|||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('drawer header returns to home', (tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final db = newTestDatabase();
|
||||
addTearDown(db.close);
|
||||
|
||||
await tester.pumpWidget(app(db));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Navigate away from home first.
|
||||
await tester.tap(find.byKey(const Key('home.inventory')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No seeds yet. Tap + to add your first.'), findsOneWidget);
|
||||
|
||||
// Open the drawer and tap the Tanemaki brand header.
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Tanemaki'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Back on the home menu.
|
||||
expect(find.text('Your inventory'), findsOneWidget);
|
||||
expect(find.text('Open market'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('drawer Settings opens the settings screen', (tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final db = newTestDatabase();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
|
@ -7,6 +9,14 @@ import 'package:tane/db/enums.dart';
|
|||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// A valid 1x1 transparent PNG so photos decode cleanly in widget tests.
|
||||
final _tinyPng = Uint8List.fromList(const [
|
||||
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, //
|
||||
0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, //
|
||||
120, 156, 99, 250, 207, 0, 0, 0, 2, 0, 1, 226, 33, 188, 51, 0, 0, 0, 0, //
|
||||
73, 69, 78, 68, 174, 66, 96, 130,
|
||||
]);
|
||||
|
||||
void main() {
|
||||
late AppDatabase db;
|
||||
|
||||
|
|
@ -381,10 +391,117 @@ void main() {
|
|||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Plants'), findsOneWidget); // lot-type chip
|
||||
expect(find.text('Plant'), findsOneWidget); // lot-type chip
|
||||
expect(find.text('Year 2024 · 3 plants'), findsOneWidget);
|
||||
// Germination is seed-only.
|
||||
expect(find.byIcon(Icons.grass_outlined), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('adds a seedling lot with a packaging chip', (tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Basil');
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('detail.addLot')));
|
||||
await tester.pumpAndSettle();
|
||||
// Pick the seedling form, then its packaging.
|
||||
await tester.tap(find.byKey(const Key('lotType.seedling')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('presentation.pot')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('addLot.save')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Seedling'), findsOneWidget); // form chip on the lot
|
||||
expect(find.textContaining('Pot'), findsOneWidget); // packaging in subtitle
|
||||
// Germination is seed-only, so a seedling lot hides it.
|
||||
expect(find.byIcon(Icons.grass_outlined), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'the full-screen viewer deletes a photo only after confirmation',
|
||||
(tester) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
await repo.addPhoto(id, _tinyPng);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Open the viewer from the thumbnail.
|
||||
await tester.tap(find.byKey(const Key('photo.thumb.0')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Cancelling the confirmation keeps the photo (still in the viewer).
|
||||
await tester.tap(find.byKey(const Key('photo.delete')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Delete this photo?'), findsOneWidget);
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Delete this photo?'), findsNothing); // dialog gone
|
||||
expect(find.byKey(const Key('photo.delete')), findsOneWidget); // open
|
||||
|
||||
// Confirming removes it; with none left the viewer closes onto detail.
|
||||
await tester.tap(find.byKey(const Key('photo.delete')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('photo.deleteConfirm')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('photo.thumb.0')), findsNothing); // no photos
|
||||
expect(find.byKey(const Key('detail.edit')), findsOneWidget); // on detail
|
||||
await disposeTree(tester);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('the full-screen viewer sets a non-cover photo as the cover', (
|
||||
tester,
|
||||
) async {
|
||||
final repo = newTestRepository(db);
|
||||
final id = await repo.addQuickVariety(label: 'Maize');
|
||||
await repo.addPhoto(id, _tinyPng);
|
||||
await repo.addPhoto(id, _tinyPng);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapDetail(
|
||||
repository: repo,
|
||||
varietyId: id,
|
||||
species: newTestSpeciesRepository(db),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('photo.thumb.0')));
|
||||
await tester.pumpAndSettle();
|
||||
// On the cover (page 0) the star is filled and disabled.
|
||||
expect(find.byIcon(Icons.star), findsOneWidget);
|
||||
expect(find.byIcon(Icons.star_border), findsNothing);
|
||||
|
||||
// Swipe to the second (non-cover) photo: the star turns to an outline.
|
||||
await tester.fling(find.byType(PageView).last, const Offset(-400, 0), 1000);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.star_border), findsOneWidget);
|
||||
|
||||
// Setting it as cover snaps the view back to the (new) cover: filled again.
|
||||
await tester.tap(find.byKey(const Key('photo.cover')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byIcon(Icons.star), findsOneWidget);
|
||||
expect(find.byIcon(Icons.star_border), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue