feat: add selected date read controller
This commit is contained in:
parent
7a0bb72f43
commit
16c460db0a
13 changed files with 540 additions and 36 deletions
|
|
@ -8,6 +8,7 @@ import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
import '../controllers/scheduler_command_controller.dart';
|
import '../controllers/scheduler_command_controller.dart';
|
||||||
import '../controllers/scheduler_read_controller.dart';
|
import '../controllers/scheduler_read_controller.dart';
|
||||||
|
import '../controllers/selected_date_controller.dart';
|
||||||
import '../controllers/today_screen_controller.dart';
|
import '../controllers/today_screen_controller.dart';
|
||||||
import 'scheduler_app_composition.dart';
|
import 'scheduler_app_composition.dart';
|
||||||
|
|
||||||
|
|
@ -56,12 +57,28 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
|
||||||
|
|
||||||
/// Human-readable label for [date].
|
/// Human-readable label for [date].
|
||||||
@override
|
@override
|
||||||
String get dateLabel => formatSchedulerDateLabel(date);
|
String get dateLabel => dateLabelFor(date);
|
||||||
|
|
||||||
|
/// Initial selected owner-local planning date.
|
||||||
|
@override
|
||||||
|
CivilDate get initialDate => date;
|
||||||
|
|
||||||
/// Default project id used by quick capture.
|
/// Default project id used by quick capture.
|
||||||
@override
|
@override
|
||||||
String get defaultProjectId => projectId;
|
String get defaultProjectId => projectId;
|
||||||
|
|
||||||
|
/// Formats [date] for compact UI date labels.
|
||||||
|
@override
|
||||||
|
String dateLabelFor(CivilDate date) {
|
||||||
|
return formatSchedulerDateLabel(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the controller that owns the visible planning date.
|
||||||
|
@override
|
||||||
|
SelectedDateController createSelectedDateController() {
|
||||||
|
return SelectedDateController(initialDate: initialDate);
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a deterministic seeded composition for the demo app and tests.
|
/// Creates a deterministic seeded composition for the demo app and tests.
|
||||||
factory DemoSchedulerComposition.seeded({
|
factory DemoSchedulerComposition.seeded({
|
||||||
Clock? clock,
|
Clock? clock,
|
||||||
|
|
@ -122,9 +139,13 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
|
||||||
|
|
||||||
/// Creates the controller used by the compact Today screen mockup.
|
/// Creates the controller used by the compact Today screen mockup.
|
||||||
@override
|
@override
|
||||||
TodayScreenController createTodayScreenController() {
|
TodayScreenController createTodayScreenController({
|
||||||
|
required SelectedDateController selectedDates,
|
||||||
|
}) {
|
||||||
return TodayScreenController(
|
return TodayScreenController(
|
||||||
read: () {
|
selectedDates: selectedDates,
|
||||||
|
dateLabelFor: dateLabelFor,
|
||||||
|
read: (date) {
|
||||||
return todayQuery.execute(
|
return todayQuery.execute(
|
||||||
GetTodayStateRequest(
|
GetTodayStateRequest(
|
||||||
context: context('ui-plan-1-read-today'),
|
context: context('ui-plan-1-read-today'),
|
||||||
|
|
@ -173,11 +194,12 @@ class DemoSchedulerComposition implements SchedulerAppComposition {
|
||||||
@override
|
@override
|
||||||
SchedulerCommandController createCommandController({
|
SchedulerCommandController createCommandController({
|
||||||
required ReadRefresh refreshReads,
|
required ReadRefresh refreshReads,
|
||||||
|
required SelectedDateReader selectedDate,
|
||||||
}) {
|
}) {
|
||||||
return SchedulerCommandController(
|
return SchedulerCommandController(
|
||||||
commands: commandUseCases,
|
commands: commandUseCases,
|
||||||
contextFor: context,
|
contextFor: context,
|
||||||
localDate: date,
|
selectedDate: selectedDate,
|
||||||
refreshReads: refreshReads,
|
refreshReads: refreshReads,
|
||||||
quickCaptureProjectId: defaultProjectId,
|
quickCaptureProjectId: defaultProjectId,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../controllers/scheduler_command_controller.dart';
|
import '../controllers/scheduler_command_controller.dart';
|
||||||
|
import '../controllers/selected_date_controller.dart';
|
||||||
import '../controllers/today_screen_controller.dart';
|
import '../controllers/today_screen_controller.dart';
|
||||||
import '../models/today_screen_models.dart';
|
import '../models/today_screen_models.dart';
|
||||||
import '../theme/focus_flow_theme.dart';
|
import '../theme/focus_flow_theme.dart';
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ class _FocusFlowHomeState extends State<FocusFlowHome> {
|
||||||
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
late final TodayScreenController controller;
|
late final TodayScreenController controller;
|
||||||
|
|
||||||
|
/// Stores the `selectedDateController` value for this object.
|
||||||
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
|
late final SelectedDateController selectedDateController;
|
||||||
|
|
||||||
/// Stores the `commandController` value for this object.
|
/// Stores the `commandController` value for this object.
|
||||||
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
||||||
late final SchedulerCommandController commandController;
|
late final SchedulerCommandController commandController;
|
||||||
|
|
@ -19,9 +23,13 @@ class _FocusFlowHomeState extends State<FocusFlowHome> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
controller = widget.composition.createTodayScreenController();
|
selectedDateController = widget.composition.createSelectedDateController();
|
||||||
|
controller = widget.composition.createTodayScreenController(
|
||||||
|
selectedDates: selectedDateController,
|
||||||
|
);
|
||||||
commandController = widget.composition.createCommandController(
|
commandController = widget.composition.createCommandController(
|
||||||
refreshReads: controller.load,
|
refreshReads: controller.load,
|
||||||
|
selectedDate: () => selectedDateController.date,
|
||||||
);
|
);
|
||||||
controller.load();
|
controller.load();
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +40,7 @@ class _FocusFlowHomeState extends State<FocusFlowHome> {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
commandController.dispose();
|
commandController.dispose();
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
|
selectedDateController.dispose();
|
||||||
unawaited(widget.composition.dispose());
|
unawaited(widget.composition.dispose());
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -75,10 +84,8 @@ class _FocusFlowHomeState extends State<FocusFlowHome> {
|
||||||
TodayScreenFailure(:final message) => _PlanIssue(
|
TodayScreenFailure(:final message) => _PlanIssue(
|
||||||
message: message,
|
message: message,
|
||||||
),
|
),
|
||||||
TodayScreenEmpty() => _TodayScreenScaffold(
|
TodayScreenEmpty(:final data) => _TodayScreenScaffold(
|
||||||
data: TodayScreenData.empty(
|
data: data,
|
||||||
dateLabel: widget.composition.dateLabel,
|
|
||||||
),
|
|
||||||
selectedCard: controller.selectedCard,
|
selectedCard: controller.selectedCard,
|
||||||
onSelectCard: controller.selectCard,
|
onSelectCard: controller.selectCard,
|
||||||
onCompleteCard: _toggleCardCompletion,
|
onCompleteCard: _toggleCardCompletion,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,18 @@ class _TodayScreenScaffoldState extends State<_TodayScreenScaffold> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs the `didUpdateWidget` behavior for this scheduler component.
|
||||||
|
/// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _TodayScreenScaffold oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.data.dateLabel == widget.data.dateLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_timelineScrollTargetCardId = null;
|
||||||
|
_timelineScrollRequest = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds the widget subtree for this component.
|
/// Builds the widget subtree for this component.
|
||||||
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
/// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:scheduler_core/scheduler_core.dart';
|
||||||
import 'package:scheduler_persistence_sqlite/sqlite.dart';
|
import 'package:scheduler_persistence_sqlite/sqlite.dart';
|
||||||
|
|
||||||
import '../controllers/scheduler_command_controller.dart';
|
import '../controllers/scheduler_command_controller.dart';
|
||||||
|
import '../controllers/selected_date_controller.dart';
|
||||||
import '../controllers/today_screen_controller.dart';
|
import '../controllers/today_screen_controller.dart';
|
||||||
import 'demo_scheduler_composition.dart' show formatSchedulerDateLabel;
|
import 'demo_scheduler_composition.dart' show formatSchedulerDateLabel;
|
||||||
import 'scheduler_app_composition.dart';
|
import 'scheduler_app_composition.dart';
|
||||||
|
|
@ -106,13 +107,33 @@ final class PersistentSchedulerComposition implements SchedulerAppComposition {
|
||||||
|
|
||||||
/// Human-readable label for [date].
|
/// Human-readable label for [date].
|
||||||
@override
|
@override
|
||||||
String get dateLabel => formatSchedulerDateLabel(date);
|
String get dateLabel => dateLabelFor(date);
|
||||||
|
|
||||||
|
/// Initial selected owner-local planning date.
|
||||||
|
@override
|
||||||
|
CivilDate get initialDate => date;
|
||||||
|
|
||||||
|
/// Formats [date] for compact UI date labels.
|
||||||
|
@override
|
||||||
|
String dateLabelFor(CivilDate date) {
|
||||||
|
return formatSchedulerDateLabel(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the controller that owns the visible planning date.
|
||||||
|
@override
|
||||||
|
SelectedDateController createSelectedDateController() {
|
||||||
|
return SelectedDateController(initialDate: initialDate);
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates the controller used by the compact Today screen mockup.
|
/// Creates the controller used by the compact Today screen mockup.
|
||||||
@override
|
@override
|
||||||
TodayScreenController createTodayScreenController() {
|
TodayScreenController createTodayScreenController({
|
||||||
|
required SelectedDateController selectedDates,
|
||||||
|
}) {
|
||||||
return TodayScreenController(
|
return TodayScreenController(
|
||||||
read: () {
|
selectedDates: selectedDates,
|
||||||
|
dateLabelFor: dateLabelFor,
|
||||||
|
read: (date) {
|
||||||
return todayQuery.execute(
|
return todayQuery.execute(
|
||||||
GetTodayStateRequest(
|
GetTodayStateRequest(
|
||||||
context: context('ui-plan-1-read-today'),
|
context: context('ui-plan-1-read-today'),
|
||||||
|
|
@ -128,11 +149,12 @@ final class PersistentSchedulerComposition implements SchedulerAppComposition {
|
||||||
@override
|
@override
|
||||||
SchedulerCommandController createCommandController({
|
SchedulerCommandController createCommandController({
|
||||||
required ReadRefresh refreshReads,
|
required ReadRefresh refreshReads,
|
||||||
|
required SelectedDateReader selectedDate,
|
||||||
}) {
|
}) {
|
||||||
return SchedulerCommandController(
|
return SchedulerCommandController(
|
||||||
commands: commandUseCases,
|
commands: commandUseCases,
|
||||||
contextFor: context,
|
contextFor: context,
|
||||||
localDate: date,
|
selectedDate: selectedDate,
|
||||||
refreshReads: refreshReads,
|
refreshReads: refreshReads,
|
||||||
quickCaptureProjectId: defaultProjectId,
|
quickCaptureProjectId: defaultProjectId,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@
|
||||||
/// Defines the app composition protocol used by FocusFlow widgets.
|
/// Defines the app composition protocol used by FocusFlow widgets.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
import '../controllers/scheduler_command_controller.dart';
|
import '../controllers/scheduler_command_controller.dart';
|
||||||
|
import '../controllers/selected_date_controller.dart';
|
||||||
import '../controllers/today_screen_controller.dart';
|
import '../controllers/today_screen_controller.dart';
|
||||||
|
|
||||||
/// Runtime-independent composition surface consumed by the app shell.
|
/// Runtime-independent composition surface consumed by the app shell.
|
||||||
|
|
@ -12,15 +15,27 @@ abstract interface class SchedulerAppComposition {
|
||||||
/// Human-readable label for the selected local date.
|
/// Human-readable label for the selected local date.
|
||||||
String get dateLabel;
|
String get dateLabel;
|
||||||
|
|
||||||
|
/// Initial selected owner-local planning date.
|
||||||
|
CivilDate get initialDate;
|
||||||
|
|
||||||
/// Default project id used by quick-capture commands.
|
/// Default project id used by quick-capture commands.
|
||||||
String get defaultProjectId;
|
String get defaultProjectId;
|
||||||
|
|
||||||
|
/// Formats [date] for compact UI date labels.
|
||||||
|
String dateLabelFor(CivilDate date);
|
||||||
|
|
||||||
|
/// Creates the controller that owns the visible planning date.
|
||||||
|
SelectedDateController createSelectedDateController();
|
||||||
|
|
||||||
/// Creates the controller used by the compact Today screen.
|
/// Creates the controller used by the compact Today screen.
|
||||||
TodayScreenController createTodayScreenController();
|
TodayScreenController createTodayScreenController({
|
||||||
|
required SelectedDateController selectedDates,
|
||||||
|
});
|
||||||
|
|
||||||
/// Creates the command controller used by interactive scheduler controls.
|
/// Creates the command controller used by interactive scheduler controls.
|
||||||
SchedulerCommandController createCommandController({
|
SchedulerCommandController createCommandController({
|
||||||
required ReadRefresh refreshReads,
|
required ReadRefresh refreshReads,
|
||||||
|
required SelectedDateReader selectedDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Releases resources owned by the composition.
|
/// Releases resources owned by the composition.
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,6 @@ typedef OperationContextFactory =
|
||||||
|
|
||||||
/// Refreshes any reads that should reflect a completed command.
|
/// Refreshes any reads that should reflect a completed command.
|
||||||
typedef ReadRefresh = Future<void> Function();
|
typedef ReadRefresh = Future<void> Function();
|
||||||
|
|
||||||
|
/// Reads the owner-local date that commands should target at execution time.
|
||||||
|
typedef SchedulerSelectedDateReader = CivilDate Function();
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
SchedulerCommandController({
|
SchedulerCommandController({
|
||||||
required this.commands,
|
required this.commands,
|
||||||
required this.contextFor,
|
required this.contextFor,
|
||||||
required this.localDate,
|
required this.selectedDate,
|
||||||
required this.refreshReads,
|
required this.refreshReads,
|
||||||
required this.quickCaptureProjectId,
|
required this.quickCaptureProjectId,
|
||||||
DateTime Function()? now,
|
DateTime Function()? now,
|
||||||
|
|
@ -21,8 +21,8 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
/// Factory for command-scoped application operation contexts.
|
/// Factory for command-scoped application operation contexts.
|
||||||
final OperationContextFactory contextFor;
|
final OperationContextFactory contextFor;
|
||||||
|
|
||||||
/// Local date commands should operate against.
|
/// Reads the selected local date commands should operate against.
|
||||||
final CivilDate localDate;
|
final SchedulerSelectedDateReader selectedDate;
|
||||||
|
|
||||||
/// Refresh callback invoked after commands that mutate read state.
|
/// Refresh callback invoked after commands that mutate read state.
|
||||||
final ReadRefresh refreshReads;
|
final ReadRefresh refreshReads;
|
||||||
|
|
@ -57,8 +57,9 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
successMessage: 'Task captured',
|
successMessage: 'Task captured',
|
||||||
refreshAfterSuccess: true,
|
refreshAfterSuccess: true,
|
||||||
action: (operationId) {
|
action: (operationId) {
|
||||||
|
final commandAt = now();
|
||||||
return commands.quickCaptureToBacklog(
|
return commands.quickCaptureToBacklog(
|
||||||
context: contextFor(operationId),
|
context: contextFor(operationId, now: commandAt),
|
||||||
title: title,
|
title: title,
|
||||||
projectId: quickCaptureProjectId,
|
projectId: quickCaptureProjectId,
|
||||||
);
|
);
|
||||||
|
|
@ -73,9 +74,10 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
successMessage: 'Task added',
|
successMessage: 'Task added',
|
||||||
refreshAfterSuccess: true,
|
refreshAfterSuccess: true,
|
||||||
action: (operationId) {
|
action: (operationId) {
|
||||||
|
final commandAt = now();
|
||||||
return commands.quickCaptureToNextAvailableSlot(
|
return commands.quickCaptureToNextAvailableSlot(
|
||||||
context: contextFor(operationId),
|
context: contextFor(operationId, now: commandAt),
|
||||||
localDate: localDate,
|
localDate: selectedDate(),
|
||||||
title: _genericFlexibleTaskTitle,
|
title: _genericFlexibleTaskTitle,
|
||||||
durationMinutes: _genericFlexibleTaskDurationMinutes,
|
durationMinutes: _genericFlexibleTaskDurationMinutes,
|
||||||
projectId: quickCaptureProjectId,
|
projectId: quickCaptureProjectId,
|
||||||
|
|
@ -95,9 +97,10 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
successMessage: 'Task scheduled',
|
successMessage: 'Task scheduled',
|
||||||
refreshAfterSuccess: true,
|
refreshAfterSuccess: true,
|
||||||
action: (operationId) {
|
action: (operationId) {
|
||||||
|
final commandAt = now();
|
||||||
return commands.scheduleBacklogItemToNextAvailableSlot(
|
return commands.scheduleBacklogItemToNextAvailableSlot(
|
||||||
context: contextFor(operationId),
|
context: contextFor(operationId, now: commandAt),
|
||||||
localDate: localDate,
|
localDate: selectedDate(),
|
||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
durationMinutes: durationMinutes,
|
durationMinutes: durationMinutes,
|
||||||
expectedUpdatedAt: expectedUpdatedAt,
|
expectedUpdatedAt: expectedUpdatedAt,
|
||||||
|
|
@ -134,14 +137,14 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
return switch (taskType) {
|
return switch (taskType) {
|
||||||
TaskType.flexible => commands.completeFlexibleTask(
|
TaskType.flexible => commands.completeFlexibleTask(
|
||||||
context: context,
|
context: context,
|
||||||
localDate: localDate,
|
localDate: selectedDate(),
|
||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
expectedUpdatedAt: expectedUpdatedAt,
|
expectedUpdatedAt: expectedUpdatedAt,
|
||||||
),
|
),
|
||||||
TaskType.critical ||
|
TaskType.critical ||
|
||||||
TaskType.inflexible => commands.applyRequiredTaskAction(
|
TaskType.inflexible => commands.applyRequiredTaskAction(
|
||||||
context: context,
|
context: context,
|
||||||
localDate: localDate,
|
localDate: selectedDate(),
|
||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
action: RequiredTaskAction.done,
|
action: RequiredTaskAction.done,
|
||||||
expectedUpdatedAt: expectedUpdatedAt,
|
expectedUpdatedAt: expectedUpdatedAt,
|
||||||
|
|
@ -175,7 +178,7 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
action: (operationId) {
|
action: (operationId) {
|
||||||
return commands.uncompleteTask(
|
return commands.uncompleteTask(
|
||||||
context: contextFor(operationId, now: occurredAt),
|
context: contextFor(operationId, now: occurredAt),
|
||||||
localDate: localDate,
|
localDate: selectedDate(),
|
||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
expectedUpdatedAt: expectedUpdatedAt,
|
expectedUpdatedAt: expectedUpdatedAt,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/// Coordinates selected planning date state for FocusFlow.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
/// Reads the currently selected owner-local planning date.
|
||||||
|
typedef SelectedDateReader = CivilDate Function();
|
||||||
|
|
||||||
|
/// Owns the visible owner-local planning day.
|
||||||
|
class SelectedDateController extends ChangeNotifier {
|
||||||
|
/// Creates a selected-date controller initialized to [initialDate].
|
||||||
|
SelectedDateController({required CivilDate initialDate})
|
||||||
|
: _date = initialDate;
|
||||||
|
|
||||||
|
/// Private state storing the currently selected date.
|
||||||
|
CivilDate _date;
|
||||||
|
|
||||||
|
/// Current visible owner-local planning date.
|
||||||
|
CivilDate get date => _date;
|
||||||
|
|
||||||
|
/// Selects [date] as the visible planning day.
|
||||||
|
bool selectDate(CivilDate date) {
|
||||||
|
if (date == _date) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_date = date;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the selected planning day backward by one civil day.
|
||||||
|
bool selectPreviousDay() {
|
||||||
|
return selectDate(_date.addDays(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the selected planning day forward by one civil day.
|
||||||
|
bool selectNextDay() {
|
||||||
|
return selectDate(_date.addDays(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,13 +4,20 @@
|
||||||
/// Coordinates Today Screen Controller state for the FocusFlow Flutter UI.
|
/// Coordinates Today Screen Controller state for the FocusFlow Flutter UI.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:scheduler_core/scheduler_core.dart';
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
import '../models/today_screen_models.dart';
|
import '../models/today_screen_models.dart';
|
||||||
|
import 'selected_date_controller.dart';
|
||||||
|
|
||||||
/// Reads Today state from the scheduler application layer.
|
/// Reads Today state from the scheduler application layer.
|
||||||
typedef TodayStateReader = Future<ApplicationResult<TodayState>> Function();
|
typedef TodayStateReader =
|
||||||
|
Future<ApplicationResult<TodayState>> Function(CivilDate date);
|
||||||
|
|
||||||
|
/// Formats a selected date for Today screen loading and empty states.
|
||||||
|
typedef TodayDateLabelFormatter = String Function(CivilDate date);
|
||||||
|
|
||||||
/// Base state for the compact Today screen.
|
/// Base state for the compact Today screen.
|
||||||
sealed class TodayScreenState {
|
sealed class TodayScreenState {
|
||||||
|
|
@ -21,7 +28,10 @@ sealed class TodayScreenState {
|
||||||
/// Indicates that the Today screen is loading.
|
/// Indicates that the Today screen is loading.
|
||||||
class TodayScreenLoading extends TodayScreenState {
|
class TodayScreenLoading extends TodayScreenState {
|
||||||
/// Creates a loading Today screen state.
|
/// Creates a loading Today screen state.
|
||||||
const TodayScreenLoading();
|
const TodayScreenLoading(this.dateLabel);
|
||||||
|
|
||||||
|
/// Display-ready date label for the read in progress.
|
||||||
|
final String dateLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicates that the Today screen has renderable timeline data.
|
/// Indicates that the Today screen has renderable timeline data.
|
||||||
|
|
@ -36,49 +46,86 @@ class TodayScreenReady extends TodayScreenState {
|
||||||
/// Indicates that the Today screen loaded successfully with no cards.
|
/// Indicates that the Today screen loaded successfully with no cards.
|
||||||
class TodayScreenEmpty extends TodayScreenState {
|
class TodayScreenEmpty extends TodayScreenState {
|
||||||
/// Creates an empty Today screen state.
|
/// Creates an empty Today screen state.
|
||||||
const TodayScreenEmpty();
|
const TodayScreenEmpty(this.data);
|
||||||
|
|
||||||
|
/// Empty presentation model for the selected date.
|
||||||
|
final TodayScreenData data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicates that the Today screen failed to load.
|
/// Indicates that the Today screen failed to load.
|
||||||
class TodayScreenFailure extends TodayScreenState {
|
class TodayScreenFailure extends TodayScreenState {
|
||||||
/// Creates a failure state with a displayable [message].
|
/// Creates a failure state with a displayable [message].
|
||||||
const TodayScreenFailure(this.message);
|
const TodayScreenFailure({required this.message, required this.data});
|
||||||
|
|
||||||
/// User-facing failure message or code.
|
/// User-facing failure message or code.
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
/// Empty presentation model for the selected date that failed to load.
|
||||||
|
final TodayScreenData data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads Today screen data and tracks selected timeline cards.
|
/// Loads Today screen data and tracks selected timeline cards.
|
||||||
class TodayScreenController extends ChangeNotifier {
|
class TodayScreenController extends ChangeNotifier {
|
||||||
/// Creates a Today screen controller from a read callback.
|
/// Creates a Today screen controller from a read callback.
|
||||||
TodayScreenController({required this.read});
|
TodayScreenController({
|
||||||
|
required this.read,
|
||||||
|
required this.selectedDates,
|
||||||
|
required this.dateLabelFor,
|
||||||
|
}) : _state = TodayScreenLoading(dateLabelFor(selectedDates.date)) {
|
||||||
|
selectedDates.addListener(_handleSelectedDateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
/// Callback used to read Today state.
|
/// Callback used to read Today state.
|
||||||
final TodayStateReader read;
|
final TodayStateReader read;
|
||||||
|
|
||||||
|
/// Selected planning date that drives Today reads.
|
||||||
|
final SelectedDateController selectedDates;
|
||||||
|
|
||||||
|
/// Formats dates before a backend read has returned screen data.
|
||||||
|
final TodayDateLabelFormatter dateLabelFor;
|
||||||
|
|
||||||
/// Private state stored as `_state` for this implementation.
|
/// Private state stored as `_state` for this implementation.
|
||||||
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
||||||
TodayScreenState _state = const TodayScreenLoading();
|
TodayScreenState _state;
|
||||||
|
|
||||||
/// Private state stored as `_selectedCard` for this implementation.
|
/// Private state stored as `_selectedCard` for this implementation.
|
||||||
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
||||||
TimelineCardModel? _selectedCard;
|
TimelineCardModel? _selectedCard;
|
||||||
|
|
||||||
|
/// Private state stored as `_loadSequence` for this implementation.
|
||||||
|
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
||||||
|
var _loadSequence = 0;
|
||||||
|
|
||||||
|
/// Private state stored as `_isDisposed` for this implementation.
|
||||||
|
/// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
|
||||||
|
var _isDisposed = false;
|
||||||
|
|
||||||
/// Current Today screen loading/data/failure state.
|
/// Current Today screen loading/data/failure state.
|
||||||
TodayScreenState get state => _state;
|
TodayScreenState get state => _state;
|
||||||
|
|
||||||
/// Currently selected timeline card, if a modal should be visible.
|
/// Currently selected timeline card, if a modal should be visible.
|
||||||
TimelineCardModel? get selectedCard => _selectedCard;
|
TimelineCardModel? get selectedCard => _selectedCard;
|
||||||
|
|
||||||
|
/// Current selected owner-local planning date.
|
||||||
|
CivilDate get selectedDate => selectedDates.date;
|
||||||
|
|
||||||
/// Loads Today state and maps it into presentation data.
|
/// Loads Today state and maps it into presentation data.
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
_state = const TodayScreenLoading();
|
final date = selectedDates.date;
|
||||||
|
final sequence = _nextLoadSequence();
|
||||||
|
_state = TodayScreenLoading(dateLabelFor(date));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final result = await read();
|
final result = await read(date);
|
||||||
|
if (_isStale(sequence)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final failure = result.failure;
|
final failure = result.failure;
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
_state = TodayScreenFailure(failure.detailCode ?? failure.code.name);
|
_state = TodayScreenFailure(
|
||||||
|
message: failure.detailCode ?? failure.code.name,
|
||||||
|
data: _emptyDataFor(date),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -86,11 +133,17 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
final data = TodayScreenData.fromTodayState(result.requireValue);
|
final data = TodayScreenData.fromTodayState(result.requireValue);
|
||||||
_syncSelectedCard(data);
|
_syncSelectedCard(data);
|
||||||
_state = data.cards.isEmpty
|
_state = data.cards.isEmpty
|
||||||
? const TodayScreenEmpty()
|
? TodayScreenEmpty(data)
|
||||||
: TodayScreenReady(data);
|
: TodayScreenReady(data);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} on Object catch (error) {
|
} on Object catch (error) {
|
||||||
_state = TodayScreenFailure(error.toString());
|
if (_isStale(sequence)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_state = TodayScreenFailure(
|
||||||
|
message: error.toString(),
|
||||||
|
data: _emptyDataFor(date),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +166,40 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Releases selected-date listeners owned by this controller.
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
|
selectedDates.removeListener(_handleSelectedDateChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the `_handleSelectedDateChanged` helper used inside this library.
|
||||||
|
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||||
|
void _handleSelectedDateChanged() {
|
||||||
|
_selectedCard = null;
|
||||||
|
unawaited(load());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the `_nextLoadSequence` helper used inside this library.
|
||||||
|
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||||
|
int _nextLoadSequence() {
|
||||||
|
_loadSequence += 1;
|
||||||
|
return _loadSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the `_isStale` helper used inside this library.
|
||||||
|
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||||
|
bool _isStale(int sequence) {
|
||||||
|
return _isDisposed || sequence != _loadSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the `_emptyDataFor` helper used inside this library.
|
||||||
|
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||||
|
TodayScreenData _emptyDataFor(CivilDate date) {
|
||||||
|
return TodayScreenData.empty(dateLabel: dateLabelFor(date));
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs the `_syncSelectedCard` helper used inside this library.
|
/// Runs the `_syncSelectedCard` helper used inside this library.
|
||||||
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
/// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
|
||||||
void _syncSelectedCard(TodayScreenData data) {
|
void _syncSelectedCard(TodayScreenData data) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/// Tests selected date controller behavior.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
import 'package:focus_flow_flutter/controllers/selected_date_controller.dart';
|
||||||
|
|
||||||
|
/// Runs selected date controller tests.
|
||||||
|
void main() {
|
||||||
|
test('selectDate is a no-op when the date is unchanged', () {
|
||||||
|
final controller = SelectedDateController(
|
||||||
|
initialDate: CivilDate(2026, 7, 2),
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
var notifications = 0;
|
||||||
|
controller.addListener(() {
|
||||||
|
notifications += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
final changed = controller.selectDate(CivilDate(2026, 7, 2));
|
||||||
|
|
||||||
|
expect(changed, isFalse);
|
||||||
|
expect(controller.date, CivilDate(2026, 7, 2));
|
||||||
|
expect(notifications, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('previous and next move exactly one civil day', () {
|
||||||
|
final controller = SelectedDateController(
|
||||||
|
initialDate: CivilDate(2026, 7, 2),
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
expect(controller.selectPreviousDay(), isTrue);
|
||||||
|
expect(controller.date, CivilDate(2026, 7, 1));
|
||||||
|
|
||||||
|
expect(controller.selectNextDay(), isTrue);
|
||||||
|
expect(controller.date, CivilDate(2026, 7, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('direct selection notifies listeners once for a changed date', () {
|
||||||
|
final controller = SelectedDateController(
|
||||||
|
initialDate: CivilDate(2026, 7, 2),
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
var notifications = 0;
|
||||||
|
controller.addListener(() {
|
||||||
|
notifications += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
final changed = controller.selectDate(CivilDate(2026, 8, 14));
|
||||||
|
|
||||||
|
expect(changed, isTrue);
|
||||||
|
expect(controller.date, CivilDate(2026, 8, 14));
|
||||||
|
expect(notifications, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,225 @@
|
||||||
|
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/// Tests Today screen controller date-aware read behavior.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
import 'package:focus_flow_flutter/controllers/selected_date_controller.dart';
|
||||||
|
import 'package:focus_flow_flutter/controllers/today_screen_controller.dart';
|
||||||
|
|
||||||
|
/// Runs Today screen controller tests.
|
||||||
|
void main() {
|
||||||
|
test(
|
||||||
|
'load reads the selected date and shows empty data for that date',
|
||||||
|
() async {
|
||||||
|
final selectedDates = SelectedDateController(initialDate: _july2);
|
||||||
|
addTearDown(selectedDates.dispose);
|
||||||
|
final reads = <CivilDate>[];
|
||||||
|
final controller = TodayScreenController(
|
||||||
|
selectedDates: selectedDates,
|
||||||
|
dateLabelFor: _label,
|
||||||
|
read: (date) async {
|
||||||
|
reads.add(date);
|
||||||
|
return ApplicationResult.success(_todayState(date));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(reads, [_july2]);
|
||||||
|
final state = controller.state;
|
||||||
|
expect(state, isA<TodayScreenEmpty>());
|
||||||
|
expect((state as TodayScreenEmpty).data.dateLabel, 'July 2, 2026');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'date changes trigger distinct reads and clear selected cards',
|
||||||
|
() async {
|
||||||
|
final selectedDates = SelectedDateController(initialDate: _july2);
|
||||||
|
addTearDown(selectedDates.dispose);
|
||||||
|
final reads = <CivilDate>[];
|
||||||
|
final controller = TodayScreenController(
|
||||||
|
selectedDates: selectedDates,
|
||||||
|
dateLabelFor: _label,
|
||||||
|
read: (date) async {
|
||||||
|
reads.add(date);
|
||||||
|
return ApplicationResult.success(
|
||||||
|
_todayState(
|
||||||
|
date,
|
||||||
|
items: date == _july2 ? [_timelineItem('first-task', date)] : [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
final ready = controller.state as TodayScreenReady;
|
||||||
|
controller.selectCard(ready.data.cards.single);
|
||||||
|
expect(controller.selectedCard?.id, 'first-task');
|
||||||
|
|
||||||
|
selectedDates.selectNextDay();
|
||||||
|
await _flushAsyncWork();
|
||||||
|
|
||||||
|
expect(reads, [_july2, _july3]);
|
||||||
|
expect(controller.selectedCard, isNull);
|
||||||
|
final state = controller.state;
|
||||||
|
expect(state, isA<TodayScreenEmpty>());
|
||||||
|
expect((state as TodayScreenEmpty).data.dateLabel, 'July 3, 2026');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('read failures keep selected date without showing old data', () async {
|
||||||
|
final selectedDates = SelectedDateController(initialDate: _july2);
|
||||||
|
addTearDown(selectedDates.dispose);
|
||||||
|
final controller = TodayScreenController(
|
||||||
|
selectedDates: selectedDates,
|
||||||
|
dateLabelFor: _label,
|
||||||
|
read: (date) async {
|
||||||
|
return ApplicationResult.failure(
|
||||||
|
const ApplicationFailure(
|
||||||
|
code: ApplicationFailureCode.persistenceFailure,
|
||||||
|
detailCode: 'readFailed',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(controller.selectedDate, _july2);
|
||||||
|
final state = controller.state;
|
||||||
|
expect(state, isA<TodayScreenFailure>());
|
||||||
|
expect((state as TodayScreenFailure).message, 'readFailed');
|
||||||
|
expect(state.data.dateLabel, 'July 2, 2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'older in-flight reads cannot overwrite the latest selected date',
|
||||||
|
() async {
|
||||||
|
final selectedDates = SelectedDateController(initialDate: _july2);
|
||||||
|
addTearDown(selectedDates.dispose);
|
||||||
|
final firstRead = Completer<ApplicationResult<TodayState>>();
|
||||||
|
final secondRead = Completer<ApplicationResult<TodayState>>();
|
||||||
|
final controller = TodayScreenController(
|
||||||
|
selectedDates: selectedDates,
|
||||||
|
dateLabelFor: _label,
|
||||||
|
read: (date) {
|
||||||
|
if (date == _july2) {
|
||||||
|
return firstRead.future;
|
||||||
|
}
|
||||||
|
if (date == _july3) {
|
||||||
|
return secondRead.future;
|
||||||
|
}
|
||||||
|
throw StateError('Unexpected read date: ${date.toIsoString()}');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
final firstLoad = controller.load();
|
||||||
|
selectedDates.selectNextDay();
|
||||||
|
await _flushAsyncWork();
|
||||||
|
secondRead.complete(ApplicationResult.success(_todayState(_july3)));
|
||||||
|
await _flushAsyncWork();
|
||||||
|
|
||||||
|
final latest = controller.state;
|
||||||
|
expect(latest, isA<TodayScreenEmpty>());
|
||||||
|
expect((latest as TodayScreenEmpty).data.dateLabel, 'July 3, 2026');
|
||||||
|
|
||||||
|
firstRead.complete(ApplicationResult.success(_todayState(_july2)));
|
||||||
|
await firstLoad;
|
||||||
|
await _flushAsyncWork();
|
||||||
|
|
||||||
|
final unchanged = controller.state;
|
||||||
|
expect(unchanged, isA<TodayScreenEmpty>());
|
||||||
|
expect((unchanged as TodayScreenEmpty).data.dateLabel, 'July 3, 2026');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First controller test date.
|
||||||
|
final _july2 = CivilDate(2026, 7, 2);
|
||||||
|
|
||||||
|
/// Next controller test date.
|
||||||
|
final _july3 = CivilDate(2026, 7, 3);
|
||||||
|
|
||||||
|
/// Formats [date] as a long month/day/year label.
|
||||||
|
String _label(CivilDate date) {
|
||||||
|
const months = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
];
|
||||||
|
return '${months[date.month - 1]} ${date.day}, ${date.year}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for microtask-scheduled controller reloads.
|
||||||
|
Future<void> _flushAsyncWork() async {
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a lightweight Today state for [date].
|
||||||
|
TodayState _todayState(
|
||||||
|
CivilDate date, {
|
||||||
|
List<TodayTimelineItem> items = const [],
|
||||||
|
}) {
|
||||||
|
final dayStart = DateTime.utc(date.year, date.month, date.day);
|
||||||
|
return TodayState(
|
||||||
|
date: date,
|
||||||
|
ownerId: 'owner-1',
|
||||||
|
timeZoneId: 'UTC',
|
||||||
|
readAt: dayStart.add(const Duration(hours: 12)),
|
||||||
|
dayStart: dayStart,
|
||||||
|
dayEnd: dayStart.add(const Duration(days: 1)),
|
||||||
|
ownerSettings: OwnerSettings(ownerId: 'owner-1', timeZoneId: 'UTC'),
|
||||||
|
timelineItems: items,
|
||||||
|
compactState: const TodayCompactState(
|
||||||
|
manualCompactModeEnabled: true,
|
||||||
|
fullTimelineExpanded: false,
|
||||||
|
),
|
||||||
|
pendingNotices: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds one planned timeline item on [date].
|
||||||
|
TodayTimelineItem _timelineItem(String id, CivilDate date) {
|
||||||
|
final start = DateTime.utc(date.year, date.month, date.day, 9);
|
||||||
|
return TodayTimelineItem(
|
||||||
|
item: TimelineItem(
|
||||||
|
id: id,
|
||||||
|
displayTitle: 'Task $id',
|
||||||
|
taskType: TaskType.flexible,
|
||||||
|
projectColorToken: 'project-home',
|
||||||
|
backgroundToken: TimelineBackgroundToken.flexible,
|
||||||
|
rewardIconToken: TimelineRewardIconToken.medium,
|
||||||
|
difficultyIconToken: TimelineDifficultyIconToken.medium,
|
||||||
|
showsExplicitTime: true,
|
||||||
|
quickActions: const [],
|
||||||
|
category: TimelineItemCategory.taskCard,
|
||||||
|
start: start,
|
||||||
|
end: start.add(const Duration(minutes: 30)),
|
||||||
|
durationMinutes: 30,
|
||||||
|
),
|
||||||
|
source: TodayTimelineItemSource.task,
|
||||||
|
taskStatus: TaskStatus.planned,
|
||||||
|
taskId: id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -40,6 +40,7 @@ void main() {
|
||||||
|
|
||||||
final commandController = composition.createCommandController(
|
final commandController = composition.createCommandController(
|
||||||
refreshReads: () async {},
|
refreshReads: () async {},
|
||||||
|
selectedDate: () => composition.initialDate,
|
||||||
);
|
);
|
||||||
addTearDown(commandController.dispose);
|
addTearDown(commandController.dispose);
|
||||||
await tester.runAsync(
|
await tester.runAsync(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue