todo-app/lib/ui/notes_grid.dart
Krzysztof kuhy Rudnicki 757c54beda Multi-select notes with an icon action bar
An app-bar toggle enters selection mode: a tap toggles a note (tint +
check on grid tiles, ListTile.selected + check in the list), Ctrl/Cmd-
click toggles and Shift-click range-selects on desktop, long-press
outside the mode still opens the per-note sheet. While anything is
selected an icon-only bottom bar sets status or priority for the whole
selection in one bulkUpdate, assigns plain notes to a repo picked from
the repos on notes or typed as owner/name, or clears the selection. A
refused assign (note already in a repo) flashes a block icon. Labels
are semantic only; menus pass an empty tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWXdsB7CQPNXVN829G1
2026-11-10 15:39:13 +01:00

82 lines
2.2 KiB
Dart

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,
required this.isSelected,
super.key,
});
/// Whether the note with this id is in the multi-selection.
final bool Function(String id) isSelected;
/// 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,
selected: isSelected(note.id),
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]),
),
),
),
);
}
}