feat(app): the path editor in two cards, as before
On the tinted ground, one card holds the path's details (title, secondary title, a line, the description row, a line, its tags behind an Add tags chip) and one below holds its steps under their header, lines between them, filling the window at least. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
0d46c04a44
commit
252110832a
7 changed files with 255 additions and 120 deletions
|
|
@ -99,6 +99,7 @@
|
||||||
"pathDescriptionEditTitle": "Pfadbeschreibung",
|
"pathDescriptionEditTitle": "Pfadbeschreibung",
|
||||||
"pathAddTags": "Tags hinzufügen",
|
"pathAddTags": "Tags hinzufügen",
|
||||||
"pathTags": "Tags",
|
"pathTags": "Tags",
|
||||||
|
"pathEditTags": "Tags bearbeiten",
|
||||||
"pathSteps": "Schritte",
|
"pathSteps": "Schritte",
|
||||||
"pathStepsEmpty": "Noch keine Schritte",
|
"pathStepsEmpty": "Noch keine Schritte",
|
||||||
"pathStepsEmptyHint": "Wähle einen Schritt-Typ, um mit dem Schreiben der Lektion zu beginnen.",
|
"pathStepsEmptyHint": "Wähle einen Schritt-Typ, um mit dem Schreiben der Lektion zu beginnen.",
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@
|
||||||
"pathDescriptionEditTitle": "Path description",
|
"pathDescriptionEditTitle": "Path description",
|
||||||
"pathAddTags": "Add tags",
|
"pathAddTags": "Add tags",
|
||||||
"pathTags": "Tags",
|
"pathTags": "Tags",
|
||||||
|
"pathEditTags": "Edit tags",
|
||||||
"pathSteps": "Steps",
|
"pathSteps": "Steps",
|
||||||
"pathStepsEmpty": "No steps yet",
|
"pathStepsEmpty": "No steps yet",
|
||||||
"pathStepsEmptyHint": "Pick a step type to start writing the lesson.",
|
"pathStepsEmptyHint": "Pick a step type to start writing the lesson.",
|
||||||
|
|
|
||||||
|
|
@ -652,6 +652,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Tags'**
|
/// **'Tags'**
|
||||||
String get pathTags;
|
String get pathTags;
|
||||||
|
|
||||||
|
/// No description provided for @pathEditTags.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Edit tags'**
|
||||||
|
String get pathEditTags;
|
||||||
|
|
||||||
/// No description provided for @pathSteps.
|
/// No description provided for @pathSteps.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get pathTags => 'Tags';
|
String get pathTags => 'Tags';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathEditTags => 'Tags bearbeiten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pathSteps => 'Schritte';
|
String get pathSteps => 'Schritte';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get pathTags => 'Tags';
|
String get pathTags => 'Tags';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pathEditTags => 'Edit tags';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get pathSteps => 'Steps';
|
String get pathSteps => 'Steps';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/person_scope.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_text_screen.dart';
|
import 'package:halkyon_learn_flutter/src/core/editor/rich_text_screen.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/layout/readable_width.dart';
|
import 'package:halkyon_learn_flutter/src/core/layout/readable_width.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.dart';
|
import 'package:halkyon_learn_flutter/src/core/widget/undo_snack_bar.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_editor_bloc.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_editor_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
||||||
|
|
@ -17,9 +19,9 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// Writes a learning path of the studyset [studysetId] in the whole window,
|
/// Writes a learning path of the studyset [studysetId] in the whole window,
|
||||||
/// over the tabs: its title, secondary title, description and tags, and its
|
/// over the tabs: a card of its title, secondary title, description and
|
||||||
/// steps, added by kind, reordered and deleted with an undo
|
/// tags, and a card of its steps, added by kind, reordered and deleted with
|
||||||
/// (`PathEditorBloc`). A step added opens at once.
|
/// an undo (`PathEditorBloc`). A step added opens at once.
|
||||||
class const PathEditorScreen({
|
class const PathEditorScreen({
|
||||||
required final String studysetId,
|
required final String studysetId,
|
||||||
required final String pathId,
|
required final String pathId,
|
||||||
|
|
@ -61,6 +63,7 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) => Scaffold(
|
builder: (context, state) => Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
appBar: AppBar(title: Text(l10n.pathEditTitle)),
|
appBar: AppBar(title: Text(l10n.pathEditTitle)),
|
||||||
body: switch (state) {
|
body: switch (state) {
|
||||||
PathEditorState(loaded: false) => const Center(
|
PathEditorState(loaded: false) => const Center(
|
||||||
|
|
@ -86,63 +89,63 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The path in two cards on the tinted ground: its details (title,
|
||||||
|
/// secondary title, description, tags), and below them its steps, filling
|
||||||
|
/// the window at least.
|
||||||
Widget _form(BuildContext context, PathEditorState state) {
|
Widget _form(BuildContext context, PathEditorState state) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final bloc = context.read<PathEditorBloc>();
|
final bloc = context.read<PathEditorBloc>();
|
||||||
final description = state.content.description;
|
|
||||||
final chosenTags = [
|
final chosenTags = [
|
||||||
for (final tag in state.tagging.all)
|
for (final tag in state.tagging.all)
|
||||||
if (state.tagIds.contains(tag.id)) tag,
|
if (state.tagIds.contains(tag.id)) tag,
|
||||||
];
|
];
|
||||||
return Align(
|
final label = theme.textTheme.labelMedium?.copyWith(
|
||||||
alignment: Alignment.topCenter,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
child: ConstrainedBox(
|
);
|
||||||
constraints: const BoxConstraints(maxWidth: readableContentMaxWidth),
|
final details = SectionCard(
|
||||||
child: ListView(
|
padding: EdgeInsets.zero,
|
||||||
padding: const EdgeInsets.all(16),
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||||
|
child: TextField(
|
||||||
controller: _title,
|
controller: _title,
|
||||||
style: theme.textTheme.titleLarge,
|
style: theme.textTheme.headlineSmall,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.pathTitleLabel,
|
hintText: l10n.pathTitleLabel,
|
||||||
|
border: InputBorder.none,
|
||||||
errorText: state.content.title.trim().isEmpty
|
errorText: state.content.title.trim().isEmpty
|
||||||
? l10n.pathTitleRequired
|
? l10n.pathTitleRequired
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
onChanged: (text) => bloc.add(PathEditorEventTitleChanged(text)),
|
onChanged: (text) => bloc.add(PathEditorEventTitleChanged(text)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
TextField(
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||||
|
child: TextField(
|
||||||
controller: _secondary,
|
controller: _secondary,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.pathSecondaryTitleLabel,
|
hintText: l10n.pathSecondaryTitleLabel,
|
||||||
hintText: l10n.pathSecondaryTitleHint,
|
isDense: true,
|
||||||
|
border: InputBorder.none,
|
||||||
),
|
),
|
||||||
onChanged: (text) =>
|
onChanged: (text) =>
|
||||||
bloc.add(PathEditorEventSecondaryTitleChanged(text)),
|
bloc.add(PathEditorEventSecondaryTitleChanged(text)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
ListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
leading: const Icon(LucideIcons.text),
|
|
||||||
title: Text(
|
|
||||||
description.isBlank
|
|
||||||
? l10n.pathAddDescription
|
|
||||||
: l10n.pathDescription,
|
|
||||||
),
|
|
||||||
subtitle: description.isBlank
|
|
||||||
? null
|
|
||||||
: Text(
|
|
||||||
description.plainText.trim(),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
_DescriptionRow(
|
||||||
|
description: state.content.description,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final edited = await editRichText(
|
final edited = await editRichText(
|
||||||
context,
|
context,
|
||||||
description,
|
state.content.description,
|
||||||
title: l10n.pathDescriptionEditTitle,
|
title: l10n.pathDescriptionEditTitle,
|
||||||
);
|
);
|
||||||
if (edited != null) {
|
if (edited != null) {
|
||||||
|
|
@ -150,16 +153,26 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.pathTags, style: label),
|
||||||
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 4,
|
runSpacing: 8,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
for (final tag in chosenTags) TagChip(tag: tag),
|
for (final tag in chosenTags) TagChip(tag: tag),
|
||||||
TextButton.icon(
|
ActionChip(
|
||||||
icon: const Icon(LucideIcons.tag, size: 16),
|
avatar: const Icon(LucideIcons.pencil, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
chosenTags.isEmpty ? l10n.pathAddTags : l10n.pathTags,
|
chosenTags.isEmpty
|
||||||
|
? l10n.pathAddTags
|
||||||
|
: l10n.pathEditTags,
|
||||||
),
|
),
|
||||||
onPressed: () => showTagPicker(
|
onPressed: () => showTagPicker(
|
||||||
context,
|
context,
|
||||||
|
|
@ -170,8 +183,20 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
Row(
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final steps = SectionCard(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -204,16 +229,14 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
if (state.steps.isEmpty)
|
if (state.steps.isEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(l10n.pathStepsEmpty, style: theme.textTheme.titleSmall),
|
||||||
l10n.pathStepsEmpty,
|
|
||||||
style: theme.textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
l10n.pathStepsEmptyHint,
|
l10n.pathStepsEmptyHint,
|
||||||
|
|
@ -246,9 +269,9 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
for (final step in state.steps)
|
for (final (index, step) in state.steps.indexed) ...[
|
||||||
|
if (index > 0) const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
leading: Icon(stepKindIcon(step.kind)),
|
leading: Icon(stepKindIcon(step.kind)),
|
||||||
title: _StepTitle(step: step),
|
title: _StepTitle(step: step),
|
||||||
subtitle: Text(stepKindLabel(l10n, step.kind)),
|
subtitle: Text(stepKindLabel(l10n, step.kind)),
|
||||||
|
|
@ -262,13 +285,35 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
LucideIcons.trash2,
|
LucideIcons.trash2,
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
tooltip: l10n.pathStepDelete,
|
tooltip: l10n.pathStepDelete,
|
||||||
onPressed: () => _deleteStep(context, step),
|
onPressed: () => _deleteStep(context, step),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: readableContentMaxWidth),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||||
|
sliver: SliverToBoxAdapter(child: details),
|
||||||
|
),
|
||||||
|
// Padded inside: the sliver fills to the window's edge.
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||||
|
child: steps,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -286,6 +331,54 @@ class _PathEditorScreenState() extends State<PathEditorScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The path's description, a few lines of it or an invitation to write
|
||||||
|
/// one, opening its editor.
|
||||||
|
class const _DescriptionRow({
|
||||||
|
required final RichContent description,
|
||||||
|
required final VoidCallback onTap,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final muted = theme.colorScheme.onSurfaceVariant;
|
||||||
|
final text = description.plainText.trim();
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 12, 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.pathDescription,
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(color: muted),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
text.isEmpty ? l10n.pathAddDescription : text,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: text.isEmpty ? muted : null,
|
||||||
|
fontStyle: text.isEmpty ? FontStyle.italic : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(LucideIcons.chevronRight, size: 18, color: muted),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A step's title, or "Untitled step".
|
/// A step's title, or "Untitled step".
|
||||||
class const _StepTitle({required final LearningPathStep step})
|
class const _StepTitle({required final LearningPathStep step})
|
||||||
extends StatelessWidget {
|
extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/core/citation/model/source.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
import 'package:halkyon_learn_flutter/src/core/device_settings/model/device_settings.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/widget/section_card.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
||||||
|
|
@ -186,6 +187,33 @@ void main() {
|
||||||
expect(find.text('Which one?'), findsOneWidget);
|
expect(find.text('Which one?'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("the path editor groups the path's details in one card and its "
|
||||||
|
'steps in another, filling the window below', (tester) async {
|
||||||
|
final paths = repository();
|
||||||
|
final id = await paths.createPath(
|
||||||
|
'japanese',
|
||||||
|
LearningPathContent.titled('Particles'),
|
||||||
|
);
|
||||||
|
await paths.createStep(id, StepKind.info, step(StepKind.info, 'Read'));
|
||||||
|
await showApp(tester, RoutePaths.editPath('japanese', id));
|
||||||
|
|
||||||
|
final cards = find.byType(SectionCard);
|
||||||
|
expect(cards, findsNWidgets(2));
|
||||||
|
Finder inCard(int index, Finder part) =>
|
||||||
|
find.descendant(of: cards.at(index), matching: part);
|
||||||
|
for (final part in ['Particles', 'Description', 'Add a description']) {
|
||||||
|
expect(inCard(0, find.text(part)), findsOneWidget, reason: part);
|
||||||
|
}
|
||||||
|
expect(inCard(0, find.widgetWithText(ActionChip, 'Add tags')), findsOne);
|
||||||
|
for (final part in ['Steps', 'Add step', 'Read']) {
|
||||||
|
expect(inCard(1, find.text(part)), findsOneWidget, reason: part);
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
tester.getRect(cards.at(1)).bottom,
|
||||||
|
moreOrLessEquals(1000 - 12, epsilon: 1),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('imports a path from a file, saying what came in and what was '
|
testWidgets('imports a path from a file, saying what came in and what was '
|
||||||
'left out', (tester) async {
|
'left out', (tester) async {
|
||||||
files.next = (
|
files.next = (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue