focus-flow/apps/focus_flow_flutter/lib/models/today_screen_models.dart

237 lines
6.6 KiB
Dart

import 'package:scheduler_core/scheduler_core.dart';
class RequiredBannerModel {
const RequiredBannerModel({required this.title, required this.timeText});
final String title;
final String timeText;
}
class TimelineRangeModel {
const TimelineRangeModel({
required this.startMinutes,
required this.endMinutes,
});
final int startMinutes;
final int endMinutes;
}
enum TaskVisualKind {
flexible,
required,
appointment,
freeSlot,
completedSurprise,
}
class TodayScreenData {
const TodayScreenData({
required this.dateLabel,
required this.timelineRange,
required this.cards,
this.requiredBanner,
});
factory TodayScreenData.empty({required String dateLabel}) {
return TodayScreenData(
dateLabel: dateLabel,
timelineRange: defaultRange,
cards: const [],
);
}
factory TodayScreenData.fromTodayState(TodayState state) {
final cards = [
for (final item in state.timelineItems) TimelineCardModel.fromItem(item),
];
final nextRequired = state.nextRequiredItem;
return TodayScreenData(
dateLabel: _dateLabel(state.date),
timelineRange: defaultRange,
requiredBanner: nextRequired == null
? null
: RequiredBannerModel(
title: nextRequired.item.displayTitle,
timeText: _formatTime(nextRequired.start),
),
cards: List<TimelineCardModel>.unmodifiable(cards),
);
}
static const defaultRange = TimelineRangeModel(
startMinutes: 16 * 60,
endMinutes: 20 * 60 + 45,
);
final String dateLabel;
final RequiredBannerModel? requiredBanner;
final TimelineRangeModel timelineRange;
final List<TimelineCardModel> cards;
}
class TimelineCardModel {
const TimelineCardModel({
required this.id,
required this.title,
required this.typeLabel,
required this.subtitle,
required this.timeText,
required this.startMinutes,
required this.endMinutes,
required this.visualKind,
required this.rewardIconToken,
required this.difficultyIconToken,
required this.isCompleted,
required this.isSelectable,
required this.showsQuickActions,
this.durationMinutes,
});
factory TimelineCardModel.fromItem(TodayTimelineItem item) {
final start = item.start;
final end = item.end;
final visualKind = _visualKindFor(item);
final isCompleted = item.taskStatus == TaskStatus.completed;
final title = item.item.displayTitle;
final duration = item.item.durationMinutes;
final timeText = start == null || end == null
? duration == null
? ''
: '$duration min'
: '${_formatTime(start)} - ${_formatTime(end)}';
return TimelineCardModel(
id: item.id,
title: title,
typeLabel: _typeLabelFor(visualKind),
subtitle: _subtitleFor(item, visualKind, timeText),
timeText: timeText,
startMinutes: _minutesSinceMidnight(start),
endMinutes: _minutesSinceMidnight(end),
visualKind: visualKind,
rewardIconToken: item.item.rewardIconToken,
difficultyIconToken: item.item.difficultyIconToken,
durationMinutes: duration,
isCompleted: isCompleted,
isSelectable: true,
showsQuickActions: !isCompleted && visualKind != TaskVisualKind.freeSlot,
);
}
final String id;
final String title;
final String typeLabel;
final String subtitle;
final String timeText;
final int startMinutes;
final int endMinutes;
final int? durationMinutes;
final TaskVisualKind visualKind;
final TimelineRewardIconToken rewardIconToken;
final TimelineDifficultyIconToken difficultyIconToken;
final bool isCompleted;
final bool isSelectable;
final bool showsQuickActions;
String get rewardLabel {
return switch (rewardIconToken) {
TimelineRewardIconToken.notSet => 'Reward not set',
TimelineRewardIconToken.veryLow => 'Reward level 1',
TimelineRewardIconToken.low => 'Reward level 2',
TimelineRewardIconToken.medium => 'Reward level 3',
TimelineRewardIconToken.high => 'Reward level 4',
TimelineRewardIconToken.veryHigh => 'Reward level 5',
};
}
String get effortLabel {
return switch (difficultyIconToken) {
TimelineDifficultyIconToken.notSet => 'Effort not set',
TimelineDifficultyIconToken.veryEasy => 'Very easy effort',
TimelineDifficultyIconToken.easy => 'Easy effort',
TimelineDifficultyIconToken.medium => 'Medium effort',
TimelineDifficultyIconToken.hard => 'Hard effort',
TimelineDifficultyIconToken.veryHard => 'Very hard effort',
};
}
}
TaskVisualKind _visualKindFor(TodayTimelineItem item) {
if (item.taskStatus == TaskStatus.completed ||
(item.taskType == TaskType.surprise &&
item.taskStatus == TaskStatus.completed)) {
return TaskVisualKind.completedSurprise;
}
return switch (item.taskType) {
TaskType.flexible => TaskVisualKind.flexible,
TaskType.critical => TaskVisualKind.required,
TaskType.inflexible => TaskVisualKind.appointment,
TaskType.freeSlot => TaskVisualKind.freeSlot,
TaskType.surprise => TaskVisualKind.completedSurprise,
TaskType.locked => TaskVisualKind.completedSurprise,
};
}
String _typeLabelFor(TaskVisualKind visualKind) {
return switch (visualKind) {
TaskVisualKind.flexible => 'Flexible',
TaskVisualKind.required => 'Required',
TaskVisualKind.appointment => 'Required',
TaskVisualKind.freeSlot => 'Free Slot',
TaskVisualKind.completedSurprise => 'Surprise task',
};
}
String _subtitleFor(
TodayTimelineItem item,
TaskVisualKind visualKind,
String timeText,
) {
if (visualKind == TaskVisualKind.freeSlot) {
return 'Intentional rest';
}
if (visualKind == TaskVisualKind.completedSurprise) {
final completedAt = _formatTime(item.item.end ?? item.end);
return 'Surprise task - Completed at $completedAt';
}
if (item.taskType == TaskType.flexible && item.item.durationMinutes != null) {
return '${item.item.durationMinutes} min';
}
return timeText;
}
String _dateLabel(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}';
}
int _minutesSinceMidnight(DateTime? value) {
if (value == null) {
return 0;
}
return value.hour * 60 + value.minute;
}
String _formatTime(DateTime? value) {
if (value == null) {
return '';
}
final period = value.hour >= 12 ? 'PM' : 'AM';
final rawHour = value.hour % 12;
final hour = rawHour == 0 ? 12 : rawHour;
final minute = value.minute.toString().padLeft(2, '0');
return '$hour:$minute $period';
}