mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-11 15:54:15 +01:00
Glyph + spoken label replaces every app-written string the guard found: empty states, tile meta, sheet dates/fields, date presets, template/section headings, status footer, snackbars, GitHub and cloud-image results, upload/download badges, Material tooltips. Guard tests (text_free_*_test) walk the painted tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
2.6 KiB
Dart
87 lines
2.6 KiB
Dart
/// One row of the notes list.
|
|
///
|
|
/// Split out of `notes_list_screen.dart` for file size; public only because
|
|
/// Dart privacy is library-scoped.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:todo/data/note.dart';
|
|
import 'package:todo/data/note_template.dart';
|
|
import 'package:todo/ui/enum_glyphs.dart';
|
|
import 'package:todo/ui/note_grid_tile.dart';
|
|
|
|
/// One row in the notes list: title, metadata line, and an actions button.
|
|
class NoteTile extends StatelessWidget {
|
|
/// Creates a tile for [note].
|
|
const NoteTile({
|
|
required this.note,
|
|
required this.onTap,
|
|
required this.onActions,
|
|
this.selected = false,
|
|
super.key,
|
|
});
|
|
|
|
/// Whether the row is part of the multi-selection: highlighted + a check.
|
|
final bool selected;
|
|
|
|
/// The note this row displays.
|
|
final Note note;
|
|
|
|
/// Open the full note for reading/editing.
|
|
/// Called when the row body is tapped (opens the note).
|
|
final VoidCallback onTap;
|
|
|
|
/// Open the quick-actions sheet (priority/status/delete).
|
|
/// Called when the trailing actions button is tapped.
|
|
final VoidCallback onActions;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final firstLine = noteTitle(note.text);
|
|
return ListTile(
|
|
selected: selected,
|
|
leading: selected
|
|
? const Icon(Icons.check_circle, semanticLabel: 'Selected')
|
|
: null,
|
|
title: Text(
|
|
firstLine.isEmpty ? kEmptyTitle : firstLine,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: _meta(context),
|
|
trailing: IconButton(
|
|
icon: const Icon(Icons.more_vert, semanticLabel: 'Quick actions'),
|
|
onPressed: onActions,
|
|
),
|
|
onTap: onTap,
|
|
onLongPress: onActions,
|
|
);
|
|
}
|
|
|
|
/// Status and priority glyphs, the edit age, and the repo (content).
|
|
/// Every note has a status and a priority, so both are always shown.
|
|
Widget _meta(BuildContext context) => Row(
|
|
children: [
|
|
statusGlyph(note.status, size: 16),
|
|
const SizedBox(width: 6),
|
|
priorityGlyph(context, note.priority, size: 16),
|
|
const SizedBox(width: 6),
|
|
const Icon(Icons.history, size: 16, semanticLabel: 'Edited'),
|
|
Text(relativeAge(note.updatedAt)),
|
|
if (note.repoName case final repo?) ...[
|
|
const SizedBox(width: 6),
|
|
Flexible(child: Text(repo, overflow: TextOverflow.ellipsis)),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Compact age like "2m" / "3h" / "4d": a number plus a unit symbol.
|
|
String relativeAge(DateTime t) {
|
|
final d = DateTime.now().difference(t);
|
|
if (d.inHours < 1) return '${d.inMinutes}m';
|
|
if (d.inDays < 1) return '${d.inHours}h';
|
|
return '${d.inDays}d';
|
|
}
|