// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// Coordinates Today Screen Controller state for the FocusFlow Flutter UI. library; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:scheduler_core/scheduler_core.dart'; import '../models/today_screen_models.dart'; import 'selected_date_controller.dart'; /// Reads Today state from the scheduler application layer. typedef TodayStateReader = Future> 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. sealed class TodayScreenState { /// Creates a Today screen state. const TodayScreenState(); } /// Indicates that the Today screen is loading. class TodayScreenLoading extends TodayScreenState { /// Creates a loading Today screen state. 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. class TodayScreenReady extends TodayScreenState { /// Creates a ready state with mapped screen [data]. const TodayScreenReady(this.data); /// Presentation model for the Today screen. final TodayScreenData data; } /// Indicates that the Today screen loaded successfully with no cards. class TodayScreenEmpty extends TodayScreenState { /// Creates an empty Today screen state. const TodayScreenEmpty(this.data); /// Empty presentation model for the selected date. final TodayScreenData data; } /// Indicates that the Today screen failed to load. class TodayScreenFailure extends TodayScreenState { /// Creates a failure state with a displayable [message]. const TodayScreenFailure({required this.message, required this.data}); /// User-facing failure message or code. 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. class TodayScreenController extends ChangeNotifier { /// Creates a Today screen controller from a read callback. TodayScreenController({ required this.read, required this.selectedDates, required this.dateLabelFor, DateTime Function()? now, }) : now = now ?? _utcNow, _state = TodayScreenLoading(dateLabelFor(selectedDates.date)) { selectedDates.addListener(_handleSelectedDateChanged); } /// Callback used to read Today state. 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; /// Clock used while mapping time-sensitive presentation state. final DateTime Function() now; /// 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. TodayScreenState _state; /// 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. 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. TodayScreenState get state => _state; /// Currently selected timeline card, if a modal should be visible. TimelineCardModel? get selectedCard => _selectedCard; /// Current selected owner-local planning date. CivilDate get selectedDate => selectedDates.date; /// Loads Today state and maps it into presentation data. Future load() async { final date = selectedDates.date; final sequence = _nextLoadSequence(); _state = TodayScreenLoading(dateLabelFor(date)); notifyListeners(); try { final result = await read(date); if (_isStale(sequence)) { return; } final failure = result.failure; if (failure != null) { _state = TodayScreenFailure( message: failure.detailCode ?? failure.code.name, data: _emptyDataFor(date), ); notifyListeners(); return; } final data = TodayScreenData.fromTodayState( result.requireValue, now: now(), ); _syncSelectedCard(data); _state = data.cards.isEmpty ? TodayScreenEmpty(data) : TodayScreenReady(data); notifyListeners(); } on Object catch (error) { if (_isStale(sequence)) { return; } _state = TodayScreenFailure( message: error.toString(), data: _emptyDataFor(date), ); notifyListeners(); } } /// Selects [card] when the card allows selection. void selectCard(TimelineCardModel card) { if (!card.isSelectable) { return; } _selectedCard = card; notifyListeners(); } /// Clears the selected timeline card. void clearSelection() { if (_selectedCard == null) { return; } _selectedCard = null; 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. /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read. void _syncSelectedCard(TodayScreenData data) { final selected = _selectedCard; if (selected == null) { return; } for (final card in data.cards) { if (card.id == selected.id) { _selectedCard = card; return; } } _selectedCard = null; } /// Runs the `_utcNow` helper used inside this library. /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read. static DateTime _utcNow() => DateTime.now().toUtc(); }