131 lines
4.1 KiB
Dart
131 lines
4.1 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 'package:flutter/foundation.dart';
|
|
import 'package:scheduler_core/scheduler_core.dart';
|
|
|
|
import '../models/today_screen_models.dart';
|
|
|
|
/// Reads Today state from the scheduler application layer.
|
|
typedef TodayStateReader = Future<ApplicationResult<TodayState>> Function();
|
|
|
|
/// 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();
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
|
|
/// Indicates that the Today screen failed to load.
|
|
class TodayScreenFailure extends TodayScreenState {
|
|
/// Creates a failure state with a displayable [message].
|
|
const TodayScreenFailure(this.message);
|
|
|
|
/// User-facing failure message or code.
|
|
final String message;
|
|
}
|
|
|
|
/// 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});
|
|
|
|
/// Callback used to read Today state.
|
|
final TodayStateReader read;
|
|
|
|
/// 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 = const TodayScreenLoading();
|
|
|
|
/// 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;
|
|
|
|
/// 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;
|
|
|
|
/// Loads Today state and maps it into presentation data.
|
|
Future<void> load() async {
|
|
_state = const TodayScreenLoading();
|
|
notifyListeners();
|
|
try {
|
|
final result = await read();
|
|
final failure = result.failure;
|
|
if (failure != null) {
|
|
_state = TodayScreenFailure(failure.detailCode ?? failure.code.name);
|
|
notifyListeners();
|
|
return;
|
|
}
|
|
|
|
final data = TodayScreenData.fromTodayState(result.requireValue);
|
|
_syncSelectedCard(data);
|
|
_state = data.cards.isEmpty
|
|
? const TodayScreenEmpty()
|
|
: TodayScreenReady(data);
|
|
notifyListeners();
|
|
} on Object catch (error) {
|
|
_state = TodayScreenFailure(error.toString());
|
|
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();
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|