mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
Notes list: 2-column tile grid on phones, whole rows fill the screen
Below 600dp the notes list is a compact tile grid (title + priority/status cues). Tile height is derived so whole rows exactly fill the viewport between the status and navigation bars; past a screenful it scrolls vertically (no paging). Body and dates moved into the long-press sheet. Wider windows keep the list. Not using design_system FitGrid: it is square-celled and pages horizontally. Pins: design_system v0.3.0 (newest tag); also bumped crdt_sync_dart 0.11.2, sync_settings_ui 0.4.2, file_picker 13.3.1, idb_shim 2.11.1, sqflite_common_ffi 2.4.5, web 1.1.2 because the freshness gate was already red on them at HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2ffae26f93
commit
f7ea0ffb3d
10
AGENTS.md
10
AGENTS.md
@ -85,7 +85,11 @@ Three layers under `lib/`, each single-purpose:
|
|||||||
with the structured template; lazy note creation on first keystroke.
|
with the structured template; lazy note creation on first keystroke.
|
||||||
- `notes_list_screen.dart`: list + search + sort + filter sheet + per-note
|
- `notes_list_screen.dart`: list + search + sort + filter sheet + per-note
|
||||||
sheet. **Default view hides Done/Abandoned and shows no filter badge**
|
sheet. **Default view hides Done/Abandoned and shows no filter badge**
|
||||||
(looks unfiltered).
|
(looks unfiltered). Under 600dp wide (`kNotesGridMaxWidth`) the notes are
|
||||||
|
a 2-column `NotesGrid` of compact `NoteGridTile`s (title + priority/status
|
||||||
|
cues; body and dates are in the long-press sheet). The tile height is
|
||||||
|
derived so whole rows exactly fill the viewport; past one screenful it
|
||||||
|
scrolls vertically (no paging). Wider windows (desktop) keep the list.
|
||||||
- `settings_screen.dart`: GitHub connect/test, device-flow auth, and the
|
- `settings_screen.dart`: GitHub connect/test, device-flow auth, and the
|
||||||
Export/Import backup actions.
|
Export/Import backup actions.
|
||||||
- `main.dart`: bootstrap only (`// coverage:ignore-file`) — wires platform DB
|
- `main.dart`: bootstrap only (`// coverage:ignore-file`) — wires platform DB
|
||||||
@ -140,8 +144,8 @@ rest of the wrapper one request at a time. BACKLOG.md rewrites links to
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **The suite must stay quick and fully green.** It is currently **587 tests at
|
- **The suite must stay quick and fully green.** It is currently **604 tests at
|
||||||
100% line coverage** (3213 lines), ~40s on this machine with
|
100% line coverage** (3305 lines), ~40s on this machine with
|
||||||
`--concurrency=3` (unbounded concurrency under `capped.sh`'s 4 GiB gets its
|
`--concurrency=3` (unbounded concurrency under `capped.sh`'s 4 GiB gets its
|
||||||
test processes SIGTERMed while the wrapper still exits 0 — check the last
|
test processes SIGTERMed while the wrapper still exits 0 — check the last
|
||||||
line says "All tests passed"). Don't regress either.
|
line says "All tests passed"). Don't regress either.
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:todo/data/note.dart';
|
import 'package:todo/data/note.dart';
|
||||||
import 'package:todo/data/note_template.dart';
|
import 'package:todo/data/note_template.dart';
|
||||||
|
import 'package:todo/ui/note_sheet_details.dart';
|
||||||
import 'package:todo/ui/note_task_details.dart';
|
import 'package:todo/ui/note_task_details.dart';
|
||||||
|
|
||||||
/// The per-note bottom sheet: status/priority chips, and delete.
|
/// The per-note bottom sheet: status/priority chips, and delete.
|
||||||
@ -66,6 +67,7 @@ class _NoteActionsSheetState extends State<NoteActionsSheet> {
|
|||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
NoteSheetDetails(note: widget.note),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_EnumChips<Status>(
|
_EnumChips<Status>(
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
|
|||||||
115
lib/ui/note_grid_tile.dart
Normal file
115
lib/ui/note_grid_tile.dart
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/// One compact tile of the notes grid: title plus priority and status cues.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:todo/data/note.dart';
|
||||||
|
import 'package:todo/data/note_template.dart';
|
||||||
|
|
||||||
|
/// A note as a grid cell. It fills whatever cell it is given.
|
||||||
|
class NoteGridTile extends StatelessWidget {
|
||||||
|
/// Creates a tile for [note].
|
||||||
|
const NoteGridTile({
|
||||||
|
required this.note,
|
||||||
|
required this.onTap,
|
||||||
|
required this.onActions,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The note this tile displays.
|
||||||
|
final Note note;
|
||||||
|
|
||||||
|
/// Open the full note.
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
/// Open the per-note sheet (body, dates, priority/status, delete).
|
||||||
|
final VoidCallback onActions;
|
||||||
|
|
||||||
|
/// Danger for high, the accent for medium, muted for low.
|
||||||
|
Color accentFor(ColorScheme scheme) => switch (note.priority) {
|
||||||
|
Priority.high => scheme.error,
|
||||||
|
Priority.medium => scheme.primary,
|
||||||
|
Priority.low => scheme.outline,
|
||||||
|
};
|
||||||
|
|
||||||
|
static IconData _statusIcon(Status s) => switch (s) {
|
||||||
|
Status.todo => Icons.radio_button_unchecked,
|
||||||
|
Status.inProgress => Icons.timelapse,
|
||||||
|
Status.done => Icons.check_circle_outline,
|
||||||
|
Status.abandoned => Icons.cancel_outlined,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scheme = theme.colorScheme;
|
||||||
|
final title = noteTitle(note.text);
|
||||||
|
final cue = theme.textTheme.labelSmall;
|
||||||
|
final closed =
|
||||||
|
note.status == Status.done || note.status == Status.abandoned;
|
||||||
|
return Material(
|
||||||
|
color: scheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onActions,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
left: BorderSide(color: accentFor(scheme), width: 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
AppSpacing.sm,
|
||||||
|
AppSpacing.sm,
|
||||||
|
AppSpacing.sm,
|
||||||
|
AppSpacing.xs,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Expanded: a big text scale shortens the title, never overflows.
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title.isEmpty ? '(empty)' : title,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: closed ? scheme.onSurfaceVariant : null,
|
||||||
|
decoration: closed ? TextDecoration.lineThrough : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_statusIcon(note.status),
|
||||||
|
size: 14,
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppSpacing.xs),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
note.status.label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: cue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
note.priority.label,
|
||||||
|
maxLines: 1,
|
||||||
|
style: cue?.copyWith(color: accentFor(scheme)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
lib/ui/note_sheet_details.dart
Normal file
50
lib/ui/note_sheet_details.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:todo/data/note.dart';
|
||||||
|
import 'package:todo/images/attachment_links.dart';
|
||||||
|
|
||||||
|
/// [text] without the line that serves as the title.
|
||||||
|
String noteBodyOf(String text) {
|
||||||
|
final lines = text.split('\n');
|
||||||
|
final title = lines.indexWhere(
|
||||||
|
(l) => l.trim().isNotEmpty && attachmentOfLine(l.trim()) == null,
|
||||||
|
);
|
||||||
|
if (title < 0) return text.trim();
|
||||||
|
return [...lines.take(title), ...lines.skip(title + 1)].join('\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _stamp(DateTime t) {
|
||||||
|
String two(int n) => n.toString().padLeft(2, '0');
|
||||||
|
final date = '${t.year}-${two(t.month)}-${two(t.day)}';
|
||||||
|
return '$date ${two(t.hour)}:${two(t.minute)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a grid tile leaves out: the note body and its two dates.
|
||||||
|
class NoteSheetDetails extends StatelessWidget {
|
||||||
|
/// Shows the body and the dates of [note].
|
||||||
|
const NoteSheetDetails({required this.note, super.key});
|
||||||
|
|
||||||
|
/// The note being shown.
|
||||||
|
final Note note;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context).textTheme;
|
||||||
|
final body = noteBodyOf(note.text);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (body.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(body, style: theme.bodyMedium),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Edited ${_stamp(note.updatedAt)}'
|
||||||
|
' · Created ${_stamp(note.createdAt)}',
|
||||||
|
style: theme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
lib/ui/notes_grid.dart
Normal file
76
lib/ui/notes_grid.dart
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:todo/data/note.dart';
|
||||||
|
import 'package:todo/ui/note_grid_tile.dart';
|
||||||
|
|
||||||
|
/// Below this width the notes list is a grid; at or above it, the list.
|
||||||
|
const double kNotesGridMaxWidth = 600;
|
||||||
|
|
||||||
|
const _columns = 2;
|
||||||
|
const double _gap = AppSpacing.sm;
|
||||||
|
const double _sidePadding = AppSpacing.md - AppSpacing.xs;
|
||||||
|
const _minTileHeight = 96.0;
|
||||||
|
|
||||||
|
double _tileHeightFor(double height) {
|
||||||
|
final rows = math.max(1, ((height + _gap) / (_minTileHeight + _gap)).floor());
|
||||||
|
return (height + _gap) / rows - _gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phone notes list: two columns, scrolling only past one screenful.
|
||||||
|
class NotesGrid extends StatelessWidget {
|
||||||
|
/// Creates the grid; [onOpen] and [onActions] get the tapped note.
|
||||||
|
const NotesGrid({
|
||||||
|
required this.notes,
|
||||||
|
required this.onOpen,
|
||||||
|
required this.onActions,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Notes in display order.
|
||||||
|
final List<Note> notes;
|
||||||
|
|
||||||
|
/// Tap on a tile.
|
||||||
|
final ValueChanged<Note> onOpen;
|
||||||
|
|
||||||
|
/// Long-press on a tile.
|
||||||
|
final ValueChanged<Note> onActions;
|
||||||
|
|
||||||
|
Widget _tile(Note note) {
|
||||||
|
return NoteGridTile(
|
||||||
|
key: ValueKey(note.id),
|
||||||
|
note: note,
|
||||||
|
onTap: () => onOpen(note),
|
||||||
|
onActions: () => onActions(note),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SliverGridDelegate _delegate(double viewportHeight) {
|
||||||
|
return SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: _columns,
|
||||||
|
mainAxisSpacing: _gap,
|
||||||
|
crossAxisSpacing: _gap,
|
||||||
|
mainAxisExtent: _tileHeightFor(viewportHeight),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: _gap),
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, box) => GridView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: _sidePadding),
|
||||||
|
gridDelegate: _delegate(box.maxHeight),
|
||||||
|
itemCount: notes.length,
|
||||||
|
itemBuilder: (context, i) => _tile(notes[i]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import 'package:todo/data/note.dart';
|
|||||||
import 'package:todo/data/note_repository.dart';
|
import 'package:todo/data/note_repository.dart';
|
||||||
import 'package:todo/ui/note_tile.dart';
|
import 'package:todo/ui/note_tile.dart';
|
||||||
import 'package:todo/ui/notes_filter_sheet.dart';
|
import 'package:todo/ui/notes_filter_sheet.dart';
|
||||||
|
import 'package:todo/ui/notes_grid.dart';
|
||||||
import 'package:todo/ui/notes_list_defaults.dart';
|
import 'package:todo/ui/notes_list_defaults.dart';
|
||||||
import 'package:todo/ui/notes_list_navigation.dart';
|
import 'package:todo/ui/notes_list_navigation.dart';
|
||||||
|
|
||||||
@ -197,21 +198,27 @@ class _NotesListScreenState extends State<NotesListScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.separated(
|
void open(Note n) =>
|
||||||
itemCount: notes.length,
|
openNote(context, n, widget.repository, widget.appSettings);
|
||||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
void actions(Note n) =>
|
||||||
itemBuilder: (context, i) => NoteTile(
|
openNoteActions(context, n, widget.repository);
|
||||||
key: ValueKey(notes[i].id),
|
return LayoutBuilder(
|
||||||
note: notes[i],
|
builder: (context, box) => box.maxWidth < kNotesGridMaxWidth
|
||||||
onTap: () => openNote(
|
? NotesGrid(
|
||||||
context,
|
notes: notes,
|
||||||
notes[i],
|
onOpen: open,
|
||||||
widget.repository,
|
onActions: actions,
|
||||||
widget.appSettings,
|
)
|
||||||
),
|
: ListView.separated(
|
||||||
onActions: () =>
|
itemCount: notes.length,
|
||||||
openNoteActions(context, notes[i], widget.repository),
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
),
|
itemBuilder: (context, i) => NoteTile(
|
||||||
|
key: ValueKey(notes[i].id),
|
||||||
|
note: notes[i],
|
||||||
|
onTap: () => open(notes[i]),
|
||||||
|
onActions: () => actions(notes[i]),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
58
pubspec.lock
58
pubspec.lock
@ -5,10 +5,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: android_file_picker
|
name: android_file_picker
|
||||||
sha256: "14ab27769b54c48d5a8a71aa9858372b7a3ae77572ea0a8aee744643c5d8c53d"
|
sha256: "03d3dd3cb578ab26b1e528aeea27bf6b976942f5a24e3da82d270369b8e161a8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.1.2"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -109,11 +109,11 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: crdt_sync_dart
|
path: crdt_sync_dart
|
||||||
ref: "crdt_sync_dart-v0.11.1"
|
ref: "crdt_sync_dart-v0.11.2"
|
||||||
resolved-ref: "978d32af3af66f0d6500c4c6c0c6df5370ebe9a2"
|
resolved-ref: "1b5515fd8b1192207896c409212483aeb09704ea"
|
||||||
url: "https://github.com/kuhyx/utils"
|
url: "https://github.com/kuhyx/utils"
|
||||||
source: git
|
source: git
|
||||||
version: "0.11.1"
|
version: "0.11.2"
|
||||||
cross_file:
|
cross_file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -150,11 +150,11 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: design_system
|
path: design_system
|
||||||
ref: "design_system-v0.2.1"
|
ref: "design_system-v0.3.0"
|
||||||
resolved-ref: "978d32af3af66f0d6500c4c6c0c6df5370ebe9a2"
|
resolved-ref: b3c9611af212880301ca79ed21f909bc4640f81f
|
||||||
url: "https://github.com/kuhyx/utils"
|
url: "https://github.com/kuhyx/utils"
|
||||||
source: git
|
source: git
|
||||||
version: "0.2.1"
|
version: "0.3.0"
|
||||||
dev_build:
|
dev_build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -199,42 +199,42 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "98c0b156b6380ba55bc767a14e2e6b3feb246e713ad40092424596fa79005bea"
|
sha256: "02f2945a7f29535c735f6b283c3c5553810566d3052d730429aec78fe9d06592"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.1.0"
|
version: "13.3.1"
|
||||||
file_picker_darwin:
|
file_picker_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_darwin
|
name: file_picker_darwin
|
||||||
sha256: "51aef9f4c80449c736e7cc02198675fda73689f0bd45a84da642cfeab0250d46"
|
sha256: b845af092ca7a3cff0e768029f835157d371a8341750f4158a7ad358143b0e54
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.4.1"
|
||||||
file_picker_linux:
|
file_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_linux
|
name: file_picker_linux
|
||||||
sha256: "9c870771500c758eb46506c98532c3ee60e6e51d0a62dcde24a849ef213ea5ce"
|
sha256: c6562fc7d426db2fb85ea66fe2627953d2709932df5584ceaacf0aa5e39cf4da
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.2"
|
||||||
file_picker_platform_interface:
|
file_picker_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_platform_interface
|
name: file_picker_platform_interface
|
||||||
sha256: bbdc085a6f168e63f147e9ce8988f79fa0800a58e7f25a48f762c60837107ba5
|
sha256: "336e7168306834a2b68e6168ed768e00d3fabeac23154d5793e277ca9f97cf20"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.2.0"
|
||||||
file_picker_web:
|
file_picker_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_picker_web
|
name: file_picker_web
|
||||||
sha256: b3004268da0c1b52baa18df430e7ec4438194de855a807d26b447cbf07ef3496
|
sha256: a2288008280aabe371aff029466b3405e60838e0a1d714767488c59b926c8b8c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.2.0"
|
||||||
file_selector:
|
file_selector:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -479,10 +479,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: idb_shim
|
name: idb_shim
|
||||||
sha256: "4f32cd03ce97e2996b57cea03b2d098f5153ac55435a27cf2c89716db571728d"
|
sha256: "923c92ef88569de4ec1b257b6a6d10360dc3407179f61d0c73329701a7d7b12e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.1+1"
|
version: "2.11.1"
|
||||||
image:
|
image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -956,10 +956,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sqflite_common_ffi
|
name: sqflite_common_ffi
|
||||||
sha256: "4cb6179390549743fc1a5a2fb8f134e5ad87ce459f6f4e563531487dadd81710"
|
sha256: "119d730460264a1f11f414d9eaa43fb686789357e81ce35f9e8ccb66ddd040e4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.4"
|
version: "2.4.5"
|
||||||
sqflite_common_ffi_web:
|
sqflite_common_ffi_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1028,11 +1028,11 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: sync_settings_ui
|
path: sync_settings_ui
|
||||||
ref: "sync_settings_ui-v0.4.1"
|
ref: "sync_settings_ui-v0.4.2"
|
||||||
resolved-ref: "51700aa89eaf1789ae98491aaa854c54d35b4170"
|
resolved-ref: a4c7d29595bb9cf75f06877545249ed77d5560a9
|
||||||
url: "https://github.com/kuhyx/utils"
|
url: "https://github.com/kuhyx/utils"
|
||||||
source: git
|
source: git
|
||||||
version: "0.4.1"
|
version: "0.4.2"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1165,10 +1165,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
sha256: e119e82ac4795c7925103b7cf3768191f33941b99e7ccffbd6c645201d9f606b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.2"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1181,10 +1181,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: windows_file_picker
|
name: windows_file_picker
|
||||||
sha256: "9f3aa833068b09e380fdc59560ad260a4a14e9397173abb533699f967443602e"
|
sha256: "18452aa293de3b6961c6d05b0e71b4be7bb90c67635abdd3aa5c4e75dd875072"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.1"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
14
pubspec.yaml
14
pubspec.yaml
@ -33,7 +33,7 @@ dependencies:
|
|||||||
crdt_sync:
|
crdt_sync:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/kuhyx/utils
|
url: https://github.com/kuhyx/utils
|
||||||
ref: crdt_sync_dart-v0.11.1
|
ref: crdt_sync_dart-v0.11.2
|
||||||
path: crdt_sync_dart
|
path: crdt_sync_dart
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
@ -45,9 +45,9 @@ dependencies:
|
|||||||
design_system:
|
design_system:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/kuhyx/utils
|
url: https://github.com/kuhyx/utils
|
||||||
ref: design_system-v0.2.1
|
ref: design_system-v0.3.0
|
||||||
path: design_system
|
path: design_system
|
||||||
file_picker: 13.1.0
|
file_picker: 13.3.1
|
||||||
file_selector: 1.1.0
|
file_selector: 1.1.0
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@ -66,7 +66,7 @@ dependencies:
|
|||||||
# this plugin does not exist. See lib/sync/google_sign_in_backend.dart.
|
# this plugin does not exist. See lib/sync/google_sign_in_backend.dart.
|
||||||
google_sign_in: 7.2.0
|
google_sign_in: 7.2.0
|
||||||
http: 1.6.0
|
http: 1.6.0
|
||||||
idb_shim: 2.10.1+1
|
idb_shim: 2.11.1
|
||||||
# Re-encodes attached images (resize, EXIF strip) in pure Dart, so the
|
# Re-encodes attached images (resize, EXIF strip) in pure Dart, so the
|
||||||
# phone and the desktop wrapper share one code path.
|
# phone and the desktop wrapper share one code path.
|
||||||
image: 4.12.0
|
image: 4.12.0
|
||||||
@ -79,7 +79,7 @@ dependencies:
|
|||||||
path_provider: 2.1.6
|
path_provider: 2.1.6
|
||||||
share_plus: 13.3.1
|
share_plus: 13.3.1
|
||||||
shared_preferences: 2.5.6
|
shared_preferences: 2.5.6
|
||||||
sqflite_common_ffi: 2.4.4
|
sqflite_common_ffi: 2.4.5
|
||||||
# Retained only to read the legacy on-device DB during the one-time
|
# Retained only to read the legacy on-device DB during the one-time
|
||||||
# migration in NoteRepository.open; remove once all devices are migrated.
|
# migration in NoteRepository.open; remove once all devices are migrated.
|
||||||
sqlite_crdt: 3.0.4
|
sqlite_crdt: 3.0.4
|
||||||
@ -91,12 +91,12 @@ dependencies:
|
|||||||
sync_settings_ui:
|
sync_settings_ui:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/kuhyx/utils
|
url: https://github.com/kuhyx/utils
|
||||||
ref: sync_settings_ui-v0.4.1
|
ref: sync_settings_ui-v0.4.2
|
||||||
path: sync_settings_ui
|
path: sync_settings_ui
|
||||||
url_launcher: 6.3.3
|
url_launcher: 6.3.3
|
||||||
uuid: 4.6.0
|
uuid: 4.6.0
|
||||||
# Paste and drag-and-drop listeners on the desktop (web) page.
|
# Paste and drag-and-drop listeners on the desktop (web) page.
|
||||||
web: 1.1.1
|
web: 1.1.2
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
# Virtual clock for unit-testing debounced timers (e.g. LocalBackup).
|
# Virtual clock for unit-testing debounced timers (e.g. LocalBackup).
|
||||||
fake_async: 1.3.3
|
fake_async: 1.3.3
|
||||||
|
|||||||
166
test/notes_grid_geometry_test.dart
Normal file
166
test/notes_grid_geometry_test.dart
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
/// Pixel 6a geometry of the notes grid: whole tiles only, nothing under the
|
||||||
|
/// status bar or the navigation bar, no overflow, scrolls only past a screen.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:todo/data/note.dart';
|
||||||
|
import 'package:todo/ui/note_grid_tile.dart';
|
||||||
|
|
||||||
|
import 'notes_list_harness.dart';
|
||||||
|
|
||||||
|
const _dpr = 2.625;
|
||||||
|
const _navBar = 63 / _dpr;
|
||||||
|
const _statusBar = 132 / _dpr;
|
||||||
|
const _screenHeight = 2400 / _dpr;
|
||||||
|
|
||||||
|
/// 1080x2400 px at 2.625, with the real system-bar insets.
|
||||||
|
void _pixel6a(WidgetTester t, {double keyboardDp = 0}) {
|
||||||
|
const bars = FakeViewPadding(top: 132, bottom: 63);
|
||||||
|
t.view
|
||||||
|
..physicalSize = const Size(1080, 2400)
|
||||||
|
..devicePixelRatio = _dpr
|
||||||
|
..padding = bars
|
||||||
|
..viewPadding = bars
|
||||||
|
..viewInsets = FakeViewPadding(bottom: keyboardDp * _dpr);
|
||||||
|
addTearDown(t.view.reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Note> _notes(int n) => [
|
||||||
|
for (var i = 0; i < n; i++)
|
||||||
|
note(
|
||||||
|
'n$i',
|
||||||
|
'Note $i with a rather long title that wraps onto several lines',
|
||||||
|
priority: Priority.values[i % 3],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Rect _viewport(WidgetTester t) => t.getRect(find.byType(GridView));
|
||||||
|
|
||||||
|
/// Tiles whose rect touches the viewport.
|
||||||
|
List<Rect> _shown(WidgetTester t) {
|
||||||
|
final view = _viewport(t);
|
||||||
|
return [
|
||||||
|
for (final e in find.byType(NoteGridTile).evaluate())
|
||||||
|
t.getRect(find.byWidget(e.widget)),
|
||||||
|
].where((r) => r.overlaps(view)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
double _maxScroll(WidgetTester t) => t
|
||||||
|
.state<ScrollableState>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(GridView),
|
||||||
|
matching: find.byType(Scrollable),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.position
|
||||||
|
.maxScrollExtent;
|
||||||
|
|
||||||
|
void _expectWholeTilesOnly(WidgetTester t) {
|
||||||
|
final view = _viewport(t);
|
||||||
|
final shown = _shown(t);
|
||||||
|
expect(shown, isNotEmpty);
|
||||||
|
for (final r in shown) {
|
||||||
|
expect(r.top, greaterThanOrEqualTo(view.top - 0.01), reason: '$r');
|
||||||
|
expect(r.bottom, lessThanOrEqualTo(view.bottom + 0.01), reason: '$r');
|
||||||
|
}
|
||||||
|
// The rows use up the whole viewport: no strip of dead space under them.
|
||||||
|
final lastBottom = shown.map((r) => r.bottom).reduce((a, b) => a > b ? a : b);
|
||||||
|
expect(lastBottom, closeTo(view.bottom, 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Pixel 6a: whole tiles between the system bars', (t) async {
|
||||||
|
_pixel6a(t);
|
||||||
|
await pumpList(t, seed: _notes(300));
|
||||||
|
|
||||||
|
expect(t.takeException(), isNull);
|
||||||
|
final view = _viewport(t);
|
||||||
|
expect(view.top, greaterThanOrEqualTo(_statusBar + kToolbarHeight));
|
||||||
|
expect(view.bottom, closeTo(_screenHeight - _navBar, 0.01));
|
||||||
|
_expectWholeTilesOnly(t);
|
||||||
|
// Two columns, a real screenful, and tiles big enough to read.
|
||||||
|
final shown = _shown(t);
|
||||||
|
expect(shown.length.isEven, isTrue);
|
||||||
|
expect(shown.length, greaterThanOrEqualTo(10));
|
||||||
|
expect(shown.first.height, greaterThanOrEqualTo(88));
|
||||||
|
expect(shown.first.width, closeTo(shown[1].width, 0.01));
|
||||||
|
expect(shown[1].left, greaterThan(shown.first.right));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Pixel 6a: 300 notes scroll vertically to the last one', (
|
||||||
|
t,
|
||||||
|
) async {
|
||||||
|
_pixel6a(t);
|
||||||
|
await pumpList(t, seed: _notes(300));
|
||||||
|
|
||||||
|
expect(_maxScroll(t), greaterThan(0));
|
||||||
|
await t.drag(find.byType(GridView), const Offset(0, -500000));
|
||||||
|
await t.pump();
|
||||||
|
await t.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
expect(find.textContaining('Note 299 '), findsOneWidget);
|
||||||
|
expect(find.textContaining('Note 299 ').hitTestable(), findsOneWidget);
|
||||||
|
expect(_viewport(t).bottom, closeTo(_screenHeight - _navBar, 0.01));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Pixel 6a: exactly one screenful does not scroll', (t) async {
|
||||||
|
_pixel6a(t);
|
||||||
|
await pumpList(t, seed: _notes(300));
|
||||||
|
final perScreen = _shown(t).length;
|
||||||
|
|
||||||
|
await t.pumpWidget(const SizedBox());
|
||||||
|
await pumpList(t, seed: _notes(perScreen));
|
||||||
|
expect(_maxScroll(t), 0);
|
||||||
|
_expectWholeTilesOnly(t);
|
||||||
|
expect(_shown(t), hasLength(perScreen));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Pixel 6a: a few notes keep full-size tiles at the top', (
|
||||||
|
t,
|
||||||
|
) async {
|
||||||
|
_pixel6a(t);
|
||||||
|
await pumpList(t, seed: _notes(300));
|
||||||
|
final full = _shown(t).first;
|
||||||
|
|
||||||
|
await t.pumpWidget(const SizedBox());
|
||||||
|
await pumpList(t, seed: _notes(3));
|
||||||
|
expect(_maxScroll(t), 0);
|
||||||
|
expect(t.takeException(), isNull);
|
||||||
|
final first = t.getRect(find.byType(NoteGridTile).first);
|
||||||
|
expect(first.size, full.size);
|
||||||
|
expect(first.top, closeTo(_viewport(t).top, 0.01));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Pixel 6a: toolbar, search and a tile are all tappable', (
|
||||||
|
t,
|
||||||
|
) async {
|
||||||
|
_pixel6a(t);
|
||||||
|
await pumpList(t, seed: _notes(20));
|
||||||
|
|
||||||
|
expect(find.byIcon(Icons.sort).hitTestable(), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.filter_list).hitTestable(), findsOneWidget);
|
||||||
|
expect(find.byType(TextField).hitTestable(), findsOneWidget);
|
||||||
|
expect(find.byType(NoteGridTile).hitTestable(), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Pixel 6a with the keyboard open: still whole tiles', (t) async {
|
||||||
|
_pixel6a(t, keyboardDp: 300);
|
||||||
|
await pumpList(t, seed: _notes(300));
|
||||||
|
|
||||||
|
expect(t.takeException(), isNull);
|
||||||
|
_expectWholeTilesOnly(t);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Pixel 6a at 2x text scale: no overflow, tiles stay whole', (
|
||||||
|
t,
|
||||||
|
) async {
|
||||||
|
_pixel6a(t);
|
||||||
|
t.platformDispatcher.textScaleFactorTestValue = 2;
|
||||||
|
addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue);
|
||||||
|
await pumpList(t, seed: _notes(300));
|
||||||
|
|
||||||
|
expect(t.takeException(), isNull);
|
||||||
|
_expectWholeTilesOnly(t);
|
||||||
|
});
|
||||||
|
}
|
||||||
161
test/notes_grid_test.dart
Normal file
161
test/notes_grid_test.dart
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/// What the narrow notes grid shows and does, apart from its geometry.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:todo/data/note.dart';
|
||||||
|
import 'package:todo/ui/note_grid_tile.dart';
|
||||||
|
|
||||||
|
import 'notes_list_harness.dart';
|
||||||
|
|
||||||
|
void _phone(WidgetTester t) {
|
||||||
|
t.view
|
||||||
|
..physicalSize = const Size(1080, 2400)
|
||||||
|
..devicePixelRatio = 2.625;
|
||||||
|
addTearDown(t.view.reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openSheet(WidgetTester t, String title) async {
|
||||||
|
await t.longPress(find.text(title));
|
||||||
|
await t.pump();
|
||||||
|
await t.pump(const Duration(milliseconds: 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('phone width shows tiles, desktop width keeps the list', (
|
||||||
|
t,
|
||||||
|
) async {
|
||||||
|
_phone(t);
|
||||||
|
await pumpList(t, seed: [note('a', 'Buy milk')]);
|
||||||
|
expect(find.byType(NoteGridTile), findsOneWidget);
|
||||||
|
expect(find.byType(ListTile), findsNothing);
|
||||||
|
|
||||||
|
t.view
|
||||||
|
..physicalSize = const Size(3840, 2160)
|
||||||
|
..devicePixelRatio = 1;
|
||||||
|
await t.pump();
|
||||||
|
expect(find.byType(NoteGridTile), findsNothing);
|
||||||
|
expect(find.byType(ListTile), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a tile is title plus priority and status cues only', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
await pumpList(
|
||||||
|
t,
|
||||||
|
seed: [
|
||||||
|
note(
|
||||||
|
'a',
|
||||||
|
'Buy milk\n\n## what\nTwo litres',
|
||||||
|
priority: Priority.high,
|
||||||
|
status: Status.inProgress,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Buy milk'), findsOneWidget);
|
||||||
|
expect(find.text('High'), findsOneWidget);
|
||||||
|
expect(find.text('In progress'), findsOneWidget);
|
||||||
|
expect(find.textContaining('Two litres'), findsNothing);
|
||||||
|
expect(find.textContaining('edited'), findsNothing);
|
||||||
|
expect(find.byTooltip('Quick actions'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('priority and status each get their own cue', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
await pumpList(
|
||||||
|
t,
|
||||||
|
seed: [
|
||||||
|
note('h', 'H', priority: Priority.high),
|
||||||
|
note('m', 'M'),
|
||||||
|
note('l', 'L', priority: Priority.low, status: Status.inProgress),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final scheme = Theme.of(t.element(find.byType(Scaffold))).colorScheme;
|
||||||
|
final accents = {
|
||||||
|
for (final id in ['h', 'm', 'l'])
|
||||||
|
t.widget<NoteGridTile>(find.byKey(ValueKey(id))).accentFor(scheme),
|
||||||
|
};
|
||||||
|
expect(accents, hasLength(3));
|
||||||
|
expect(find.byIcon(Icons.radio_button_unchecked), findsNWidgets(2));
|
||||||
|
expect(find.byIcon(Icons.timelapse), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('every status has an icon', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
await pumpList(
|
||||||
|
t,
|
||||||
|
seed: [for (final s in Status.values) note(s.name, s.name, status: s)],
|
||||||
|
);
|
||||||
|
expect(find.byIcon(Icons.check_circle_outline), findsOneWidget);
|
||||||
|
expect(find.byIcon(Icons.cancel_outlined), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('an empty note reads (empty)', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
await pumpList(t, seed: [note('a', '')]);
|
||||||
|
expect(find.text('(empty)'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('tap opens the note', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
await pumpList(t, seed: [note('a', 'Open me')]);
|
||||||
|
|
||||||
|
await t.tap(find.text('Open me'));
|
||||||
|
await t.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Priority'), findsOneWidget);
|
||||||
|
expect(find.text('View'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('long-press opens the sheet with body, dates and actions', (
|
||||||
|
t,
|
||||||
|
) async {
|
||||||
|
_phone(t);
|
||||||
|
final repo = await pumpList(
|
||||||
|
t,
|
||||||
|
seed: [note('a', 'Buy milk\n\n## what\nTwo litres of oat')],
|
||||||
|
);
|
||||||
|
|
||||||
|
await _openSheet(t, 'Buy milk');
|
||||||
|
|
||||||
|
expect(find.textContaining('Two litres of oat'), findsOneWidget);
|
||||||
|
expect(find.textContaining('Edited'), findsOneWidget);
|
||||||
|
expect(find.textContaining('Created'), findsOneWidget);
|
||||||
|
await t.tap(find.text('In progress'));
|
||||||
|
await t.pump();
|
||||||
|
expect((await repo.listNotes()).single.status, Status.inProgress);
|
||||||
|
expect(find.text('Delete note'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a title-only note adds no body to the sheet', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
await pumpList(t, seed: [note('a', 'Just a title')]);
|
||||||
|
|
||||||
|
await _openSheet(t, 'Just a title');
|
||||||
|
|
||||||
|
expect(find.text('Just a title'), findsNWidgets(2));
|
||||||
|
expect(find.textContaining('Edited'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the sheet can delete a grid note', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
final repo = await pumpList(t, seed: [note('a', 'Delete me')]);
|
||||||
|
|
||||||
|
await _openSheet(t, 'Delete me');
|
||||||
|
await t.tap(find.text('Delete note'));
|
||||||
|
await t.pumpAndSettle();
|
||||||
|
await t.tap(find.text('Delete'));
|
||||||
|
await t.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(await repo.listNotes(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('empty state and the default filter survive the grid', (t) async {
|
||||||
|
_phone(t);
|
||||||
|
final repo = await pumpList(t);
|
||||||
|
|
||||||
|
expect(find.textContaining('No notes'), findsOneWidget);
|
||||||
|
expect(repo.lastFilter!.statuses, {Status.todo, Status.inProgress});
|
||||||
|
expect(t.widget<Badge>(find.byType(Badge)).isLabelVisible, isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user