Some checks failed
CI / test (macos-latest) (push) Has been cancelled
CI / test (ubuntu-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
CI / package (macos-latest, macos) (push) Has been cancelled
CI / package (ubuntu-latest, linux) (push) Has been cancelled
CI / package (windows-latest, windows) (push) Has been cancelled
CI / test (macos-latest) (pull_request) Has been cancelled
CI / test (ubuntu-latest) (pull_request) Has been cancelled
CI / test (windows-latest) (pull_request) Has been cancelled
CI / package (macos-latest, macos) (pull_request) Has been cancelled
CI / package (ubuntu-latest, linux) (pull_request) Has been cancelled
CI / package (windows-latest, windows) (pull_request) Has been cancelled
Backend was already V1-complete for these paths (moveFlexibleToBacklog,
breakUpTask); this closes the UI-wiring gap identified in the V1
feature-complete audit.
- Add createBacklogController() to SchedulerAppComposition and implement it
in PersistentSchedulerComposition (only the demo composition had one).
- Sidebar now switches the main content area between Today and Backlog via
a real SidebarScreen enum instead of hardcoded onTap: () {} stubs.
- Task modal's "Backlog" quick-action reuses the existing onPushToBacklog
wiring (same backend command as the Push menu's "Push to backlog").
- Add a break-up dialog (row-level title/priority/reward/duration per
MVP-AC-11) and SchedulerCommandController.breakUpTask(), wired to the
modal's "Break up" button.
- Fix a latent bug where the active sidebar nav item's key was hardcoded
to nav-today-active regardless of which item was active.
Closes: Circuit-Forge/focus-flow#9
Closes: Circuit-Forge/focus-flow#12
Closes: Circuit-Forge/focus-flow#13
194 lines
6.6 KiB
Dart
194 lines
6.6 KiB
Dart
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/// Renders the Task Selection Modal widget for the FocusFlow Flutter UI.
|
|
library;
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:scheduler_core/scheduler_core.dart';
|
|
|
|
import '../models/today_screen_models.dart';
|
|
import '../theme/focus_flow_tokens.dart';
|
|
import 'status_circle_button.dart';
|
|
import 'timeline/difficulty_bars.dart';
|
|
import 'timeline/reward_icon.dart';
|
|
|
|
part 'task_selection/action_button.dart';
|
|
part 'task_selection/break_up_dialog.dart';
|
|
part 'task_selection/header_metadata.dart';
|
|
part 'task_selection/info_tile.dart';
|
|
part 'task_selection/modal_accent.dart';
|
|
part 'task_selection/modal_status_button.dart';
|
|
|
|
/// Modal shown when a selectable timeline task card is opened.
|
|
class TaskSelectionModal extends StatelessWidget {
|
|
/// Creates a task selection modal for [card].
|
|
const TaskSelectionModal({
|
|
required this.card,
|
|
required this.onClose,
|
|
this.onStatusPressed,
|
|
this.onPushToNext,
|
|
this.onPushToTomorrow,
|
|
this.onPushToBacklog,
|
|
this.onRemove,
|
|
this.onBreakUp,
|
|
super.key,
|
|
});
|
|
|
|
/// Card model whose details and actions should be displayed.
|
|
final TimelineCardModel card;
|
|
|
|
/// Callback invoked when the modal should close.
|
|
final VoidCallback onClose;
|
|
|
|
/// Callback invoked when the status circle should toggle completion.
|
|
final VoidCallback? onStatusPressed;
|
|
|
|
/// Callback invoked when Push to next is selected.
|
|
final Future<void> Function(TimelineCardModel card)? onPushToNext;
|
|
|
|
/// Callback invoked when Push to tomorrow is selected.
|
|
final Future<void> Function(TimelineCardModel card)? onPushToTomorrow;
|
|
|
|
/// Callback invoked when Push to backlog is selected.
|
|
final Future<void> Function(TimelineCardModel card)? onPushToBacklog;
|
|
|
|
/// Callback invoked when Remove is selected.
|
|
final Future<void> Function(TimelineCardModel card)? onRemove;
|
|
|
|
/// Callback invoked with drafted child tasks after Break up is confirmed.
|
|
final Future<void> Function(
|
|
TimelineCardModel card,
|
|
List<ApplicationChildTaskDraft> children,
|
|
)?
|
|
onBreakUp;
|
|
|
|
/// Opens the break-up dialog and forwards drafted child tasks to
|
|
/// [onBreakUp] when the user confirms with at least one task.
|
|
Future<void> _handleBreakUp(BuildContext context) async {
|
|
final drafts = await showDialog<List<ApplicationChildTaskDraft>>(
|
|
context: context,
|
|
builder: (_) => const _BreakUpDialog(),
|
|
);
|
|
if (drafts == null || drafts.isEmpty) {
|
|
return;
|
|
}
|
|
await onBreakUp!(card, drafts);
|
|
}
|
|
|
|
/// Builds the widget subtree for this component.
|
|
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final accent = _accentFor(card.visualKind);
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: Container(
|
|
key: const ValueKey('task-selection-modal'),
|
|
width: 548,
|
|
padding: const EdgeInsets.fromLTRB(24, 22, 24, 20),
|
|
decoration: BoxDecoration(
|
|
color: FocusFlowTokens.elevatedPanel.withValues(alpha: 0.98),
|
|
borderRadius: BorderRadius.circular(FocusFlowTokens.modalRadius),
|
|
border: Border.all(color: accent.withValues(alpha: 0.86)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
_ModalStatusButton(
|
|
card: card,
|
|
color: accent,
|
|
onPressed: onStatusPressed,
|
|
),
|
|
const SizedBox(width: 28),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
card.title,
|
|
style: const TextStyle(
|
|
color: FocusFlowTokens.textPrimary,
|
|
fontSize: FocusFlowTokens.modalTitleSize,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
_HeaderMetadata(card: card),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
_HeaderMetricIcons(card: card),
|
|
const SizedBox(width: 14),
|
|
IconButton(
|
|
key: const ValueKey('close-task-modal'),
|
|
onPressed: onClose,
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: FocusFlowTokens.glassPanelStrong,
|
|
foregroundColor: FocusFlowTokens.textPrimary,
|
|
),
|
|
icon: const Icon(Icons.close),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
GridView.count(
|
|
crossAxisCount: 2,
|
|
crossAxisSpacing: 18,
|
|
mainAxisSpacing: 12,
|
|
childAspectRatio: 5.3,
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
children: [
|
|
_PushActionButton(
|
|
card: card,
|
|
onPushToNext: onPushToNext,
|
|
onPushToTomorrow: onPushToTomorrow,
|
|
onPushToBacklog: onPushToBacklog,
|
|
),
|
|
_ActionButton(
|
|
icon: Icons.inventory_2_outlined,
|
|
label: 'Backlog',
|
|
onTap: onPushToBacklog == null
|
|
? null
|
|
: () {
|
|
unawaited(onPushToBacklog!(card));
|
|
},
|
|
),
|
|
_ActionButton(
|
|
icon: Icons.apps,
|
|
label: 'Break up',
|
|
onTap: onBreakUp == null
|
|
? null
|
|
: () {
|
|
unawaited(_handleBreakUp(context));
|
|
},
|
|
),
|
|
_ActionButton(
|
|
icon: Icons.delete_outline,
|
|
label: 'Remove',
|
|
onTap: onRemove == null
|
|
? null
|
|
: () {
|
|
unawaited(onRemove!(card));
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
const Text(
|
|
'Options apply after confirmation.',
|
|
style: TextStyle(color: FocusFlowTokens.textMuted, fontSize: 14),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|