218 lines
7.2 KiB
Dart
218 lines
7.2 KiB
Dart
// 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<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.
|
|
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,
|
|
}) : _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;
|
|
|
|
/// 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<void> 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);
|
|
_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;
|
|
}
|
|
}
|