todo-app/lib/ui/repo_picker_sheet.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

98 lines
3.1 KiB
Dart

/// Bottom sheet picking the repo selected notes are assigned to.
library;
import 'package:flutter/material.dart';
import 'package:todo/data/repo_assign.dart';
/// The repos already on notes, plus a field for a new `kuhyx/<name>`.
///
/// Pops with the chosen `owner/name`; dismissing it pops null. The field
/// starts at `kuhyx/` (content, not a label) and refuses anything that is
/// not `owner/name` with an error icon rather than words.
class RepoPickerSheet extends StatefulWidget {
/// Creates the picker listing [repos].
const RepoPickerSheet({required this.repos, super.key});
/// Distinct repos already present on notes, in display order.
final List<String> repos;
@override
State<RepoPickerSheet> createState() => _RepoPickerSheetState();
}
class _RepoPickerSheetState extends State<RepoPickerSheet> {
final _field = TextEditingController(text: 'kuhyx/');
bool _bad = false;
@override
void dispose() {
_field.dispose();
super.dispose();
}
void _submit() {
final value = _field.text.trim();
if (!isRepoName(value)) return setState(() => _bad = true);
Navigator.of(context).pop(value);
}
@override
Widget build(BuildContext context) {
return Padding(
key: const ValueKey('repo-picker'),
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: ListView(
shrinkWrap: true,
children: [
for (final repo in widget.repos)
ListTile(
key: ValueKey('repo-$repo'),
leading: const Icon(Icons.folder_outlined),
title: Text(repo),
onTap: () => Navigator.of(context).pop(repo),
),
],
),
),
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
key: const ValueKey('repo-new'),
controller: _field,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _submit(),
onChanged: (_) {
if (_bad) setState(() => _bad = false);
},
decoration: InputDecoration(
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.create_new_folder_outlined),
error: _bad
? const Icon(
Icons.error_outline,
size: 16,
semanticLabel: 'Not owner/name',
)
: null,
suffixIcon: IconButton(
key: const ValueKey('repo-new-submit'),
onPressed: _submit,
icon: const Icon(Icons.check, semanticLabel: 'Use repo'),
),
),
),
),
],
),
),
);
}
}