forked from eva/focus-flow
docs: document focus flow flutter app
This commit is contained in:
parent
fc1d100b1f
commit
f985b3ee84
28 changed files with 410 additions and 0 deletions
|
|
@ -21,6 +21,7 @@ linter:
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
# producing the lint.
|
# producing the lint.
|
||||||
rules:
|
rules:
|
||||||
|
public_member_api_docs: true
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../controllers/scheduler_command_controller.dart';
|
||||||
import '../controllers/scheduler_read_controller.dart';
|
import '../controllers/scheduler_read_controller.dart';
|
||||||
import '../controllers/today_screen_controller.dart';
|
import '../controllers/today_screen_controller.dart';
|
||||||
|
|
||||||
|
/// Wires the Flutter demo UI to in-memory scheduler application use cases.
|
||||||
class DemoSchedulerComposition {
|
class DemoSchedulerComposition {
|
||||||
DemoSchedulerComposition._({
|
DemoSchedulerComposition._({
|
||||||
required this.store,
|
required this.store,
|
||||||
|
|
@ -17,19 +18,37 @@ class DemoSchedulerComposition {
|
||||||
required this.readAt,
|
required this.readAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Fixed owner used by the seeded demo data.
|
||||||
static const ownerId = 'owner-1';
|
static const ownerId = 'owner-1';
|
||||||
|
|
||||||
|
/// Fixed owner time zone used by the seeded demo data.
|
||||||
static const timeZoneId = 'UTC';
|
static const timeZoneId = 'UTC';
|
||||||
|
|
||||||
|
/// Fixed project id used by the seeded demo data.
|
||||||
static const projectId = 'home';
|
static const projectId = 'home';
|
||||||
|
|
||||||
|
/// In-memory application store backing the demo.
|
||||||
final InMemoryApplicationUnitOfWork store;
|
final InMemoryApplicationUnitOfWork store;
|
||||||
|
|
||||||
|
/// Query object used to read the Today state.
|
||||||
final GetTodayStateQuery todayQuery;
|
final GetTodayStateQuery todayQuery;
|
||||||
|
|
||||||
|
/// Management use cases used by backlog-facing UI surfaces.
|
||||||
final V1ApplicationManagementUseCases managementUseCases;
|
final V1ApplicationManagementUseCases managementUseCases;
|
||||||
|
|
||||||
|
/// Command use cases used by interactive UI controls.
|
||||||
final V1ApplicationCommandUseCases commandUseCases;
|
final V1ApplicationCommandUseCases commandUseCases;
|
||||||
|
|
||||||
|
/// Local date represented by the seeded demo.
|
||||||
final CivilDate date;
|
final CivilDate date;
|
||||||
|
|
||||||
|
/// Stable read clock used for deterministic demo operations.
|
||||||
final DateTime readAt;
|
final DateTime readAt;
|
||||||
|
|
||||||
|
/// Human-readable label for [date].
|
||||||
String get dateLabel => formatDateLabel(date);
|
String get dateLabel => formatDateLabel(date);
|
||||||
|
|
||||||
|
/// Creates a deterministic seeded composition for the demo app and tests.
|
||||||
factory DemoSchedulerComposition.seeded({
|
factory DemoSchedulerComposition.seeded({
|
||||||
Clock? clock,
|
Clock? clock,
|
||||||
CivilDate? selectedDate,
|
CivilDate? selectedDate,
|
||||||
|
|
@ -87,6 +106,7 @@ class DemoSchedulerComposition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates the controller used by the compact Today screen mockup.
|
||||||
TodayScreenController createTodayScreenController() {
|
TodayScreenController createTodayScreenController() {
|
||||||
return TodayScreenController(
|
return TodayScreenController(
|
||||||
read: () {
|
read: () {
|
||||||
|
|
@ -101,6 +121,7 @@ class DemoSchedulerComposition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a generic read controller for Today-state panes.
|
||||||
UiReadController<TodayState> createTodayController() {
|
UiReadController<TodayState> createTodayController() {
|
||||||
return ApplicationReadController<TodayState>(
|
return ApplicationReadController<TodayState>(
|
||||||
read: () {
|
read: () {
|
||||||
|
|
@ -118,6 +139,7 @@ class DemoSchedulerComposition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a generic read controller for backlog panes.
|
||||||
UiReadController<BacklogQueryResult> createBacklogController() {
|
UiReadController<BacklogQueryResult> createBacklogController() {
|
||||||
return ApplicationReadController<BacklogQueryResult>(
|
return ApplicationReadController<BacklogQueryResult>(
|
||||||
read: () {
|
read: () {
|
||||||
|
|
@ -132,6 +154,7 @@ class DemoSchedulerComposition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates the command controller used by interactive scheduler controls.
|
||||||
SchedulerCommandController createCommandController({
|
SchedulerCommandController createCommandController({
|
||||||
required ReadRefresh refreshReads,
|
required ReadRefresh refreshReads,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -143,6 +166,7 @@ class DemoSchedulerComposition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates an application operation context for the supplied [operationId].
|
||||||
ApplicationOperationContext context(String operationId) {
|
ApplicationOperationContext context(String operationId) {
|
||||||
return ApplicationOperationContext.start(
|
return ApplicationOperationContext.start(
|
||||||
operationId: operationId,
|
operationId: operationId,
|
||||||
|
|
@ -155,6 +179,7 @@ class DemoSchedulerComposition {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formats a civil date as a long month/day/year label.
|
||||||
static String formatDateLabel(CivilDate date) {
|
static String formatDateLabel(CivilDate date) {
|
||||||
const months = [
|
const months = [
|
||||||
'January',
|
'January',
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,12 @@ import '../widgets/timeline/timeline_view.dart';
|
||||||
import '../widgets/top_bar.dart';
|
import '../widgets/top_bar.dart';
|
||||||
import 'demo_scheduler_composition.dart';
|
import 'demo_scheduler_composition.dart';
|
||||||
|
|
||||||
|
/// Root Material app for the FocusFlow desktop UI.
|
||||||
class FocusFlowApp extends StatelessWidget {
|
class FocusFlowApp extends StatelessWidget {
|
||||||
|
/// Creates a FocusFlow app from an application composition.
|
||||||
const FocusFlowApp({required this.composition, super.key});
|
const FocusFlowApp({required this.composition, super.key});
|
||||||
|
|
||||||
|
/// Factory and controller bundle used by the app tree.
|
||||||
final DemoSchedulerComposition composition;
|
final DemoSchedulerComposition composition;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -31,9 +34,12 @@ class FocusFlowApp extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stateful home screen that owns the Today controller lifecycle.
|
||||||
class FocusFlowHome extends StatefulWidget {
|
class FocusFlowHome extends StatefulWidget {
|
||||||
|
/// Creates the FocusFlow home screen from an application composition.
|
||||||
const FocusFlowHome({required this.composition, super.key});
|
const FocusFlowHome({required this.composition, super.key});
|
||||||
|
|
||||||
|
/// Factory and controller bundle used by the home screen.
|
||||||
final DemoSchedulerComposition composition;
|
final DemoSchedulerComposition composition;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,55 @@ library;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:scheduler_core/scheduler_core.dart';
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
/// Builds an application operation context for a UI command operation.
|
||||||
typedef OperationContextFactory =
|
typedef OperationContextFactory =
|
||||||
ApplicationOperationContext Function(String operationId);
|
ApplicationOperationContext Function(String operationId);
|
||||||
|
|
||||||
|
/// Refreshes any reads that should reflect a completed command.
|
||||||
typedef ReadRefresh = Future<void> Function();
|
typedef ReadRefresh = Future<void> Function();
|
||||||
|
|
||||||
|
/// Base state for scheduler command execution in the UI.
|
||||||
sealed class SchedulerCommandState {
|
sealed class SchedulerCommandState {
|
||||||
|
/// Creates a scheduler command state.
|
||||||
const SchedulerCommandState();
|
const SchedulerCommandState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that no scheduler command is currently running.
|
||||||
class SchedulerCommandIdle extends SchedulerCommandState {
|
class SchedulerCommandIdle extends SchedulerCommandState {
|
||||||
|
/// Creates an idle command state.
|
||||||
const SchedulerCommandIdle();
|
const SchedulerCommandIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that a scheduler command is currently running.
|
||||||
class SchedulerCommandRunning extends SchedulerCommandState {
|
class SchedulerCommandRunning extends SchedulerCommandState {
|
||||||
|
/// Creates a running command state with a user-facing [label].
|
||||||
const SchedulerCommandRunning(this.label);
|
const SchedulerCommandRunning(this.label);
|
||||||
|
|
||||||
|
/// User-facing description of the command in progress.
|
||||||
final String label;
|
final String label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that a scheduler command completed successfully.
|
||||||
class SchedulerCommandSuccess extends SchedulerCommandState {
|
class SchedulerCommandSuccess extends SchedulerCommandState {
|
||||||
|
/// Creates a success command state with a user-facing [message].
|
||||||
const SchedulerCommandSuccess(this.message);
|
const SchedulerCommandSuccess(this.message);
|
||||||
|
|
||||||
|
/// User-facing success message.
|
||||||
final String message;
|
final String message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that a scheduler command failed.
|
||||||
class SchedulerCommandFailure extends SchedulerCommandState {
|
class SchedulerCommandFailure extends SchedulerCommandState {
|
||||||
|
/// Creates a failure state from an application [failure] or unexpected [error].
|
||||||
const SchedulerCommandFailure({this.failure, this.error});
|
const SchedulerCommandFailure({this.failure, this.error});
|
||||||
|
|
||||||
|
/// Typed application failure returned by the scheduler core.
|
||||||
final ApplicationFailure? failure;
|
final ApplicationFailure? failure;
|
||||||
|
|
||||||
|
/// Unexpected error thrown while running the command.
|
||||||
final Object? error;
|
final Object? error;
|
||||||
|
|
||||||
|
/// Stable label for display and diagnostics.
|
||||||
String get codeLabel {
|
String get codeLabel {
|
||||||
final typed = failure;
|
final typed = failure;
|
||||||
if (typed != null) {
|
if (typed != null) {
|
||||||
|
|
@ -44,7 +62,9 @@ class SchedulerCommandFailure extends SchedulerCommandState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs scheduler write commands and exposes command state to widgets.
|
||||||
class SchedulerCommandController extends ChangeNotifier {
|
class SchedulerCommandController extends ChangeNotifier {
|
||||||
|
/// Creates a command controller from scheduler use cases and UI callbacks.
|
||||||
SchedulerCommandController({
|
SchedulerCommandController({
|
||||||
required this.commands,
|
required this.commands,
|
||||||
required this.contextFor,
|
required this.contextFor,
|
||||||
|
|
@ -52,15 +72,24 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
required this.refreshReads,
|
required this.refreshReads,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Scheduler write use cases invoked by this controller.
|
||||||
final V1ApplicationCommandUseCases commands;
|
final V1ApplicationCommandUseCases commands;
|
||||||
|
|
||||||
|
/// Factory for command-scoped application operation contexts.
|
||||||
final OperationContextFactory contextFor;
|
final OperationContextFactory contextFor;
|
||||||
|
|
||||||
|
/// Local date commands should operate against.
|
||||||
final CivilDate localDate;
|
final CivilDate localDate;
|
||||||
|
|
||||||
|
/// Refresh callback invoked after commands that mutate read state.
|
||||||
final ReadRefresh refreshReads;
|
final ReadRefresh refreshReads;
|
||||||
var _sequence = 0;
|
var _sequence = 0;
|
||||||
SchedulerCommandState _state = const SchedulerCommandIdle();
|
SchedulerCommandState _state = const SchedulerCommandIdle();
|
||||||
|
|
||||||
|
/// Current command execution state.
|
||||||
SchedulerCommandState get state => _state;
|
SchedulerCommandState get state => _state;
|
||||||
|
|
||||||
|
/// Captures [title] as a backlog task.
|
||||||
Future<void> quickCaptureToBacklog(String title) async {
|
Future<void> quickCaptureToBacklog(String title) async {
|
||||||
await _run(
|
await _run(
|
||||||
label: 'Capturing task',
|
label: 'Capturing task',
|
||||||
|
|
@ -76,6 +105,7 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Schedules a backlog task into the next available slot.
|
||||||
Future<void> scheduleBacklogItem({
|
Future<void> scheduleBacklogItem({
|
||||||
required String taskId,
|
required String taskId,
|
||||||
required int durationMinutes,
|
required int durationMinutes,
|
||||||
|
|
@ -97,6 +127,7 @@ class SchedulerCommandController extends ChangeNotifier {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Completes a planned flexible task from the Today view.
|
||||||
Future<void> completeFlexibleTask({
|
Future<void> completeFlexibleTask({
|
||||||
required String taskId,
|
required String taskId,
|
||||||
DateTime? expectedUpdatedAt,
|
DateTime? expectedUpdatedAt,
|
||||||
|
|
|
||||||
|
|
@ -4,43 +4,66 @@ library;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:scheduler_core/scheduler_core.dart';
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
/// Reads an application value for a UI surface.
|
||||||
typedef ApplicationReader<T> = Future<ApplicationResult<T>> Function();
|
typedef ApplicationReader<T> = Future<ApplicationResult<T>> Function();
|
||||||
|
|
||||||
|
/// Determines whether a successful read should be rendered as empty state.
|
||||||
typedef EmptyPredicate<T> = bool Function(T value);
|
typedef EmptyPredicate<T> = bool Function(T value);
|
||||||
|
|
||||||
|
/// Shared interface for read controllers consumed by UI widgets.
|
||||||
abstract class UiReadController<T> extends ChangeNotifier {
|
abstract class UiReadController<T> extends ChangeNotifier {
|
||||||
|
/// Current read state.
|
||||||
SchedulerReadState<T> get state;
|
SchedulerReadState<T> get state;
|
||||||
|
|
||||||
|
/// Loads the backing value.
|
||||||
Future<void> load();
|
Future<void> load();
|
||||||
|
|
||||||
|
/// Retries the latest read.
|
||||||
Future<void> retry();
|
Future<void> retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Base state for scheduler read operations.
|
||||||
sealed class SchedulerReadState<T> {
|
sealed class SchedulerReadState<T> {
|
||||||
|
/// Creates a scheduler read state.
|
||||||
const SchedulerReadState();
|
const SchedulerReadState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that a scheduler read is in progress.
|
||||||
class SchedulerReadLoading<T> extends SchedulerReadState<T> {
|
class SchedulerReadLoading<T> extends SchedulerReadState<T> {
|
||||||
|
/// Creates a loading read state.
|
||||||
const SchedulerReadLoading();
|
const SchedulerReadLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that a scheduler read returned renderable data.
|
||||||
class SchedulerReadData<T> extends SchedulerReadState<T> {
|
class SchedulerReadData<T> extends SchedulerReadState<T> {
|
||||||
|
/// Creates a data state from [value].
|
||||||
const SchedulerReadData(this.value);
|
const SchedulerReadData(this.value);
|
||||||
|
|
||||||
|
/// Value returned by the read operation.
|
||||||
final T value;
|
final T value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that a scheduler read succeeded but has no primary content.
|
||||||
class SchedulerReadEmpty<T> extends SchedulerReadState<T> {
|
class SchedulerReadEmpty<T> extends SchedulerReadState<T> {
|
||||||
|
/// Creates an empty state that still carries the read [value].
|
||||||
const SchedulerReadEmpty(this.value);
|
const SchedulerReadEmpty(this.value);
|
||||||
|
|
||||||
|
/// Value returned by the read operation.
|
||||||
final T value;
|
final T value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that a scheduler read failed.
|
||||||
class SchedulerReadFailure<T> extends SchedulerReadState<T> {
|
class SchedulerReadFailure<T> extends SchedulerReadState<T> {
|
||||||
|
/// Creates a failure state from an application [failure] or unexpected [error].
|
||||||
const SchedulerReadFailure({this.failure, this.error});
|
const SchedulerReadFailure({this.failure, this.error});
|
||||||
|
|
||||||
|
/// Typed application failure returned by the scheduler core.
|
||||||
final ApplicationFailure? failure;
|
final ApplicationFailure? failure;
|
||||||
|
|
||||||
|
/// Unexpected error thrown while running the read.
|
||||||
final Object? error;
|
final Object? error;
|
||||||
|
|
||||||
|
/// Stable label for display and diagnostics.
|
||||||
String get codeLabel {
|
String get codeLabel {
|
||||||
final typed = failure;
|
final typed = failure;
|
||||||
if (typed != null) {
|
if (typed != null) {
|
||||||
|
|
@ -50,17 +73,24 @@ class SchedulerReadFailure<T> extends SchedulerReadState<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default read controller backed by an [ApplicationReader].
|
||||||
class ApplicationReadController<T> extends UiReadController<T> {
|
class ApplicationReadController<T> extends UiReadController<T> {
|
||||||
|
/// Creates a controller from a read callback and empty-state predicate.
|
||||||
ApplicationReadController({required this.read, required this.isEmpty});
|
ApplicationReadController({required this.read, required this.isEmpty});
|
||||||
|
|
||||||
|
/// Callback used to load the application value.
|
||||||
final ApplicationReader<T> read;
|
final ApplicationReader<T> read;
|
||||||
|
|
||||||
|
/// Predicate used to classify a successful value as empty or populated.
|
||||||
final EmptyPredicate<T> isEmpty;
|
final EmptyPredicate<T> isEmpty;
|
||||||
|
|
||||||
SchedulerReadState<T> _state = SchedulerReadLoading<T>();
|
SchedulerReadState<T> _state = SchedulerReadLoading<T>();
|
||||||
|
|
||||||
|
/// Current read state.
|
||||||
@override
|
@override
|
||||||
SchedulerReadState<T> get state => _state;
|
SchedulerReadState<T> get state => _state;
|
||||||
|
|
||||||
|
/// Loads the current value and updates [state].
|
||||||
@override
|
@override
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
_setState(SchedulerReadLoading<T>());
|
_setState(SchedulerReadLoading<T>());
|
||||||
|
|
@ -82,6 +112,7 @@ class ApplicationReadController<T> extends UiReadController<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retries by running [load] again.
|
||||||
@override
|
@override
|
||||||
Future<void> retry() => load();
|
Future<void> retry() => load();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,44 +6,63 @@ import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
import '../models/today_screen_models.dart';
|
import '../models/today_screen_models.dart';
|
||||||
|
|
||||||
|
/// Reads Today state from the scheduler application layer.
|
||||||
typedef TodayStateReader = Future<ApplicationResult<TodayState>> Function();
|
typedef TodayStateReader = Future<ApplicationResult<TodayState>> Function();
|
||||||
|
|
||||||
|
/// Base state for the compact Today screen.
|
||||||
sealed class TodayScreenState {
|
sealed class TodayScreenState {
|
||||||
|
/// Creates a Today screen state.
|
||||||
const TodayScreenState();
|
const TodayScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that the Today screen is loading.
|
||||||
class TodayScreenLoading extends TodayScreenState {
|
class TodayScreenLoading extends TodayScreenState {
|
||||||
|
/// Creates a loading Today screen state.
|
||||||
const TodayScreenLoading();
|
const TodayScreenLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that the Today screen has renderable timeline data.
|
||||||
class TodayScreenReady extends TodayScreenState {
|
class TodayScreenReady extends TodayScreenState {
|
||||||
|
/// Creates a ready state with mapped screen [data].
|
||||||
const TodayScreenReady(this.data);
|
const TodayScreenReady(this.data);
|
||||||
|
|
||||||
|
/// Presentation model for the Today screen.
|
||||||
final TodayScreenData data;
|
final TodayScreenData data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that the Today screen loaded successfully with no cards.
|
||||||
class TodayScreenEmpty extends TodayScreenState {
|
class TodayScreenEmpty extends TodayScreenState {
|
||||||
|
/// Creates an empty Today screen state.
|
||||||
const TodayScreenEmpty();
|
const TodayScreenEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates that the Today screen failed to load.
|
||||||
class TodayScreenFailure extends TodayScreenState {
|
class TodayScreenFailure extends TodayScreenState {
|
||||||
|
/// Creates a failure state with a displayable [message].
|
||||||
const TodayScreenFailure(this.message);
|
const TodayScreenFailure(this.message);
|
||||||
|
|
||||||
|
/// User-facing failure message or code.
|
||||||
final String message;
|
final String message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
TodayScreenController({required this.read});
|
TodayScreenController({required this.read});
|
||||||
|
|
||||||
|
/// Callback used to read Today state.
|
||||||
final TodayStateReader read;
|
final TodayStateReader read;
|
||||||
|
|
||||||
TodayScreenState _state = const TodayScreenLoading();
|
TodayScreenState _state = const TodayScreenLoading();
|
||||||
TimelineCardModel? _selectedCard;
|
TimelineCardModel? _selectedCard;
|
||||||
|
|
||||||
|
/// 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.
|
||||||
TimelineCardModel? get selectedCard => _selectedCard;
|
TimelineCardModel? get selectedCard => _selectedCard;
|
||||||
|
|
||||||
|
/// Loads Today state and maps it into presentation data.
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
_state = const TodayScreenLoading();
|
_state = const TodayScreenLoading();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
@ -67,6 +86,7 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Selects [card] when the card allows selection.
|
||||||
void selectCard(TimelineCardModel card) {
|
void selectCard(TimelineCardModel card) {
|
||||||
if (!card.isSelectable) {
|
if (!card.isSelectable) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -75,6 +95,7 @@ class TodayScreenController extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clears the selected timeline card.
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
if (_selectedCard == null) {
|
if (_selectedCard == null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import 'app/focus_flow_app.dart';
|
||||||
export 'app/demo_scheduler_composition.dart';
|
export 'app/demo_scheduler_composition.dart';
|
||||||
export 'app/focus_flow_app.dart';
|
export 'app/focus_flow_app.dart';
|
||||||
|
|
||||||
|
/// Starts the FocusFlow app with the seeded demo scheduler composition.
|
||||||
void main() {
|
void main() {
|
||||||
runApp(FocusFlowApp(composition: DemoSchedulerComposition.seeded()));
|
runApp(FocusFlowApp(composition: DemoSchedulerComposition.seeded()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,54 @@ library;
|
||||||
|
|
||||||
import 'package:scheduler_core/scheduler_core.dart';
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
/// Presentation model for the next-required-task banner.
|
||||||
class RequiredBannerModel {
|
class RequiredBannerModel {
|
||||||
|
/// Creates a required banner model.
|
||||||
const RequiredBannerModel({required this.title, required this.timeText});
|
const RequiredBannerModel({required this.title, required this.timeText});
|
||||||
|
|
||||||
|
/// Title of the next required task.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
/// Display-ready time for the next required task.
|
||||||
final String timeText;
|
final String timeText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visible timeline range expressed as minutes since midnight.
|
||||||
class TimelineRangeModel {
|
class TimelineRangeModel {
|
||||||
|
/// Creates a timeline range model.
|
||||||
const TimelineRangeModel({
|
const TimelineRangeModel({
|
||||||
required this.startMinutes,
|
required this.startMinutes,
|
||||||
required this.endMinutes,
|
required this.endMinutes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// First visible minute from midnight.
|
||||||
final int startMinutes;
|
final int startMinutes;
|
||||||
|
|
||||||
|
/// Last visible minute from midnight.
|
||||||
final int endMinutes;
|
final int endMinutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visual category used to style timeline cards.
|
||||||
enum TaskVisualKind {
|
enum TaskVisualKind {
|
||||||
|
/// Flexible planned work.
|
||||||
flexible,
|
flexible,
|
||||||
|
|
||||||
|
/// Critical required task.
|
||||||
required,
|
required,
|
||||||
|
|
||||||
|
/// Inflexible appointment-style task.
|
||||||
appointment,
|
appointment,
|
||||||
|
|
||||||
|
/// Intentional free time.
|
||||||
freeSlot,
|
freeSlot,
|
||||||
|
|
||||||
|
/// Completed surprise task.
|
||||||
completedSurprise,
|
completedSurprise,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Presentation model for the compact Today screen.
|
||||||
class TodayScreenData {
|
class TodayScreenData {
|
||||||
|
/// Creates Today screen data.
|
||||||
const TodayScreenData({
|
const TodayScreenData({
|
||||||
required this.dateLabel,
|
required this.dateLabel,
|
||||||
required this.timelineRange,
|
required this.timelineRange,
|
||||||
|
|
@ -36,6 +58,7 @@ class TodayScreenData {
|
||||||
this.requiredBanner,
|
this.requiredBanner,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Creates an empty Today screen model for [dateLabel].
|
||||||
factory TodayScreenData.empty({required String dateLabel}) {
|
factory TodayScreenData.empty({required String dateLabel}) {
|
||||||
return TodayScreenData(
|
return TodayScreenData(
|
||||||
dateLabel: dateLabel,
|
dateLabel: dateLabel,
|
||||||
|
|
@ -44,6 +67,7 @@ class TodayScreenData {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maps scheduler core [TodayState] into UI presentation data.
|
||||||
factory TodayScreenData.fromTodayState(TodayState state) {
|
factory TodayScreenData.fromTodayState(TodayState state) {
|
||||||
final cards = [
|
final cards = [
|
||||||
for (final item in state.timelineItems) TimelineCardModel.fromItem(item),
|
for (final item in state.timelineItems) TimelineCardModel.fromItem(item),
|
||||||
|
|
@ -62,18 +86,28 @@ class TodayScreenData {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default visible range for the compact timeline.
|
||||||
static const defaultRange = TimelineRangeModel(
|
static const defaultRange = TimelineRangeModel(
|
||||||
startMinutes: 16 * 60,
|
startMinutes: 16 * 60,
|
||||||
endMinutes: 20 * 60 + 45,
|
endMinutes: 20 * 60 + 45,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Display-ready date label.
|
||||||
final String dateLabel;
|
final String dateLabel;
|
||||||
|
|
||||||
|
/// Optional next-required-task banner model.
|
||||||
final RequiredBannerModel? requiredBanner;
|
final RequiredBannerModel? requiredBanner;
|
||||||
|
|
||||||
|
/// Visible timeline range.
|
||||||
final TimelineRangeModel timelineRange;
|
final TimelineRangeModel timelineRange;
|
||||||
|
|
||||||
|
/// Timeline cards to render.
|
||||||
final List<TimelineCardModel> cards;
|
final List<TimelineCardModel> cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Presentation model for a single compact timeline card.
|
||||||
class TimelineCardModel {
|
class TimelineCardModel {
|
||||||
|
/// Creates a timeline card model.
|
||||||
const TimelineCardModel({
|
const TimelineCardModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
|
|
@ -91,6 +125,7 @@ class TimelineCardModel {
|
||||||
this.durationMinutes,
|
this.durationMinutes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Maps a scheduler timeline item into card presentation data.
|
||||||
factory TimelineCardModel.fromItem(TodayTimelineItem item) {
|
factory TimelineCardModel.fromItem(TodayTimelineItem item) {
|
||||||
final start = item.start;
|
final start = item.start;
|
||||||
final end = item.end;
|
final end = item.end;
|
||||||
|
|
@ -121,21 +156,49 @@ class TimelineCardModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stable card identifier.
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
|
/// Primary task title shown on the card.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
/// Short task type label.
|
||||||
final String typeLabel;
|
final String typeLabel;
|
||||||
|
|
||||||
|
/// Secondary line shown under the title.
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
|
|
||||||
|
/// Display-ready time or duration text.
|
||||||
final String timeText;
|
final String timeText;
|
||||||
|
|
||||||
|
/// Card start position in minutes since midnight.
|
||||||
final int startMinutes;
|
final int startMinutes;
|
||||||
|
|
||||||
|
/// Card end position in minutes since midnight.
|
||||||
final int endMinutes;
|
final int endMinutes;
|
||||||
|
|
||||||
|
/// Optional task duration in minutes.
|
||||||
final int? durationMinutes;
|
final int? durationMinutes;
|
||||||
|
|
||||||
|
/// Visual category used for styling.
|
||||||
final TaskVisualKind visualKind;
|
final TaskVisualKind visualKind;
|
||||||
|
|
||||||
|
/// Reward icon token from the scheduler core.
|
||||||
final TimelineRewardIconToken rewardIconToken;
|
final TimelineRewardIconToken rewardIconToken;
|
||||||
|
|
||||||
|
/// Difficulty icon token from the scheduler core.
|
||||||
final TimelineDifficultyIconToken difficultyIconToken;
|
final TimelineDifficultyIconToken difficultyIconToken;
|
||||||
|
|
||||||
|
/// Whether the card represents completed work.
|
||||||
final bool isCompleted;
|
final bool isCompleted;
|
||||||
|
|
||||||
|
/// Whether tapping the card should open its selection modal.
|
||||||
final bool isSelectable;
|
final bool isSelectable;
|
||||||
|
|
||||||
|
/// Whether the card should show compact quick-action controls.
|
||||||
final bool showsQuickActions;
|
final bool showsQuickActions;
|
||||||
|
|
||||||
|
/// Accessibility and modal label for the reward icon.
|
||||||
String get rewardLabel {
|
String get rewardLabel {
|
||||||
return switch (rewardIconToken) {
|
return switch (rewardIconToken) {
|
||||||
TimelineRewardIconToken.notSet => 'Reward not set',
|
TimelineRewardIconToken.notSet => 'Reward not set',
|
||||||
|
|
@ -147,6 +210,7 @@ class TimelineCardModel {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Accessibility and modal label for the effort icon.
|
||||||
String get effortLabel {
|
String get effortLabel {
|
||||||
return switch (difficultyIconToken) {
|
return switch (difficultyIconToken) {
|
||||||
TimelineDifficultyIconToken.notSet => 'Effort not set',
|
TimelineDifficultyIconToken.notSet => 'Effort not set',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'focus_flow_tokens.dart';
|
import 'focus_flow_tokens.dart';
|
||||||
|
|
||||||
|
/// Theme factory for the FocusFlow Flutter app.
|
||||||
abstract final class FocusFlowTheme {
|
abstract final class FocusFlowTheme {
|
||||||
|
/// Builds the dark Material theme used by the desktop app.
|
||||||
static ThemeData dark() {
|
static ThemeData dark() {
|
||||||
final scheme = ColorScheme.fromSeed(
|
final scheme = ColorScheme.fromSeed(
|
||||||
seedColor: FocusFlowTokens.accentMagenta,
|
seedColor: FocusFlowTokens.accentMagenta,
|
||||||
|
|
|
||||||
|
|
@ -3,48 +3,119 @@ library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Shared visual constants for the FocusFlow Flutter app.
|
||||||
abstract final class FocusFlowTokens {
|
abstract final class FocusFlowTokens {
|
||||||
|
/// Root application background color.
|
||||||
static const appBackground = Color(0xFF070811);
|
static const appBackground = Color(0xFF070811);
|
||||||
|
|
||||||
|
/// Border color for the app frame.
|
||||||
static const appFrameBorder = Color(0xFF3B3D4E);
|
static const appFrameBorder = Color(0xFF3B3D4E);
|
||||||
|
|
||||||
|
/// Sidebar background color.
|
||||||
static const sidebarBackground = Color(0xFF090A14);
|
static const sidebarBackground = Color(0xFF090A14);
|
||||||
|
|
||||||
|
/// Standard panel background color.
|
||||||
static const panelBackground = Color(0xFF11121D);
|
static const panelBackground = Color(0xFF11121D);
|
||||||
|
|
||||||
|
/// Elevated panel background color.
|
||||||
static const elevatedPanel = Color(0xFF181724);
|
static const elevatedPanel = Color(0xFF181724);
|
||||||
|
|
||||||
|
/// Translucent panel color for soft surfaces.
|
||||||
static const glassPanel = Color(0x662A1025);
|
static const glassPanel = Color(0x662A1025);
|
||||||
|
|
||||||
|
/// Stronger translucent panel color for selected surfaces.
|
||||||
static const glassPanelStrong = Color(0x992A1025);
|
static const glassPanelStrong = Color(0x992A1025);
|
||||||
|
|
||||||
|
/// Timeline grid line color.
|
||||||
static const gridLine = Color(0x243B3D4E);
|
static const gridLine = Color(0x243B3D4E);
|
||||||
|
|
||||||
|
/// Timeline rail color.
|
||||||
static const rail = Color(0xFF7D7F91);
|
static const rail = Color(0xFF7D7F91);
|
||||||
|
|
||||||
|
/// Subtle border color for controls and panels.
|
||||||
static const subtleBorder = Color(0xFF3A3A4A);
|
static const subtleBorder = Color(0xFF3A3A4A);
|
||||||
|
|
||||||
|
/// Active navigation border color.
|
||||||
static const navBorder = Color(0xFFB72D82);
|
static const navBorder = Color(0xFFB72D82);
|
||||||
|
|
||||||
|
/// Primary accent color.
|
||||||
static const accentMagenta = Color(0xFFFF5BA8);
|
static const accentMagenta = Color(0xFFFF5BA8);
|
||||||
|
|
||||||
|
/// Primary text color.
|
||||||
static const textPrimary = Color(0xFFF7F2FA);
|
static const textPrimary = Color(0xFFF7F2FA);
|
||||||
|
|
||||||
|
/// Muted text color.
|
||||||
static const textMuted = Color(0xFFBBB5C8);
|
static const textMuted = Color(0xFFBBB5C8);
|
||||||
|
|
||||||
|
/// Faint text color.
|
||||||
static const textFaint = Color(0xFF8E889D);
|
static const textFaint = Color(0xFF8E889D);
|
||||||
|
|
||||||
|
/// Text color for completed task labels.
|
||||||
static const completedText = Color(0xFF8F8A99);
|
static const completedText = Color(0xFF8F8A99);
|
||||||
|
|
||||||
|
/// Accent color for flexible tasks.
|
||||||
static const flexibleGreen = Color(0xFF4BE064);
|
static const flexibleGreen = Color(0xFF4BE064);
|
||||||
|
|
||||||
|
/// Accent color for required tasks.
|
||||||
static const requiredPink = Color(0xFFFF4E7D);
|
static const requiredPink = Color(0xFFFF4E7D);
|
||||||
|
|
||||||
|
/// Accent color for appointment tasks.
|
||||||
static const appointmentBlue = Color(0xFF38A8FF);
|
static const appointmentBlue = Color(0xFF38A8FF);
|
||||||
|
|
||||||
|
/// Accent color for rest and free-slot surfaces.
|
||||||
static const restPurple = Color(0xFFB45CFF);
|
static const restPurple = Color(0xFFB45CFF);
|
||||||
|
|
||||||
|
/// Accent color for completed tasks.
|
||||||
static const completedGray = Color(0xFF777482);
|
static const completedGray = Color(0xFF777482);
|
||||||
|
|
||||||
|
/// Warning accent color.
|
||||||
static const warningYellow = Color(0xFFFFCC66);
|
static const warningYellow = Color(0xFFFFCC66);
|
||||||
|
|
||||||
|
/// Fixed sidebar width.
|
||||||
static const sidebarWidth = 250.0;
|
static const sidebarWidth = 250.0;
|
||||||
|
|
||||||
|
/// Horizontal gutter for main app content.
|
||||||
static const mainGutter = 32.0;
|
static const mainGutter = 32.0;
|
||||||
|
|
||||||
|
/// Width of the timeline time rail.
|
||||||
static const timelineRailWidth = 120.0;
|
static const timelineRailWidth = 120.0;
|
||||||
|
|
||||||
|
/// Horizontal padding inside timeline cards.
|
||||||
static const cardHorizontalPadding = 18.0;
|
static const cardHorizontalPadding = 18.0;
|
||||||
|
|
||||||
|
/// Border radius for the outer app frame.
|
||||||
static const appFrameRadius = 8.0;
|
static const appFrameRadius = 8.0;
|
||||||
|
|
||||||
|
/// Border radius for navigation items.
|
||||||
static const navRadius = 8.0;
|
static const navRadius = 8.0;
|
||||||
|
|
||||||
|
/// Border radius for required-task banners.
|
||||||
static const bannerRadius = 8.0;
|
static const bannerRadius = 8.0;
|
||||||
|
|
||||||
|
/// Border radius for timeline cards.
|
||||||
static const cardRadius = 8.0;
|
static const cardRadius = 8.0;
|
||||||
|
|
||||||
|
/// Border radius for task selection modals.
|
||||||
static const modalRadius = 8.0;
|
static const modalRadius = 8.0;
|
||||||
|
|
||||||
|
/// Border radius for compact controls.
|
||||||
static const smallButtonRadius = 8.0;
|
static const smallButtonRadius = 8.0;
|
||||||
|
|
||||||
|
/// Font size for page titles.
|
||||||
static const pageTitleSize = 40.0;
|
static const pageTitleSize = 40.0;
|
||||||
|
|
||||||
|
/// Font size for timeline card titles.
|
||||||
static const cardTitleSize = 24.0;
|
static const cardTitleSize = 24.0;
|
||||||
|
|
||||||
|
/// Font size for timeline card metadata.
|
||||||
static const cardMetaSize = 16.0;
|
static const cardMetaSize = 16.0;
|
||||||
|
|
||||||
|
/// Font size for sidebar navigation labels.
|
||||||
static const sidebarNavSize = 18.0;
|
static const sidebarNavSize = 18.0;
|
||||||
|
|
||||||
|
/// Font size for modal titles.
|
||||||
static const modalTitleSize = 26.0;
|
static const modalTitleSize = 26.0;
|
||||||
|
|
||||||
|
/// Font size for modal action buttons.
|
||||||
static const modalButtonSize = 18.0;
|
static const modalButtonSize = 18.0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ library;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:scheduler_core/scheduler_core.dart';
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
|
/// Maps scheduler domain tokens to Flutter colors and icons.
|
||||||
abstract final class SchedulerVisualTokens {
|
abstract final class SchedulerVisualTokens {
|
||||||
|
/// Returns the color associated with a project color [token].
|
||||||
static Color projectColor(String token) {
|
static Color projectColor(String token) {
|
||||||
return switch (token) {
|
return switch (token) {
|
||||||
'home' => const Color(0xFF68B0AB),
|
'home' => const Color(0xFF68B0AB),
|
||||||
|
|
@ -13,6 +15,7 @@ abstract final class SchedulerVisualTokens {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the background color associated with a timeline background token.
|
||||||
static Color backgroundColor(
|
static Color backgroundColor(
|
||||||
TimelineBackgroundToken token,
|
TimelineBackgroundToken token,
|
||||||
ColorScheme scheme,
|
ColorScheme scheme,
|
||||||
|
|
@ -27,6 +30,7 @@ abstract final class SchedulerVisualTokens {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the icon associated with a task [type].
|
||||||
static IconData taskIcon(TaskType type) {
|
static IconData taskIcon(TaskType type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
TaskType.flexible => Icons.task_alt,
|
TaskType.flexible => Icons.task_alt,
|
||||||
|
|
@ -38,6 +42,7 @@ abstract final class SchedulerVisualTokens {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the icon associated with a reward token.
|
||||||
static IconData rewardIcon(TimelineRewardIconToken token) {
|
static IconData rewardIcon(TimelineRewardIconToken token) {
|
||||||
return switch (token) {
|
return switch (token) {
|
||||||
TimelineRewardIconToken.notSet => Icons.remove_circle_outline,
|
TimelineRewardIconToken.notSet => Icons.remove_circle_outline,
|
||||||
|
|
@ -49,6 +54,7 @@ abstract final class SchedulerVisualTokens {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the icon associated with a difficulty token.
|
||||||
static IconData difficultyIcon(TimelineDifficultyIconToken token) {
|
static IconData difficultyIcon(TimelineDifficultyIconToken token) {
|
||||||
return switch (token) {
|
return switch (token) {
|
||||||
TimelineDifficultyIconToken.notSet => Icons.remove_circle_outline,
|
TimelineDifficultyIconToken.notSet => Icons.remove_circle_outline,
|
||||||
|
|
@ -60,6 +66,7 @@ abstract final class SchedulerVisualTokens {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the icon associated with a backlog staleness marker.
|
||||||
static IconData stalenessIcon(BacklogStalenessMarker marker) {
|
static IconData stalenessIcon(BacklogStalenessMarker marker) {
|
||||||
return switch (marker) {
|
return switch (marker) {
|
||||||
BacklogStalenessMarker.green => Icons.circle,
|
BacklogStalenessMarker.green => Icons.circle,
|
||||||
|
|
@ -68,6 +75,7 @@ abstract final class SchedulerVisualTokens {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the color associated with a backlog staleness marker.
|
||||||
static Color stalenessColor(
|
static Color stalenessColor(
|
||||||
BacklogStalenessMarker marker,
|
BacklogStalenessMarker marker,
|
||||||
ColorScheme scheme,
|
ColorScheme scheme,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,15 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../theme/focus_flow_tokens.dart';
|
import '../theme/focus_flow_tokens.dart';
|
||||||
|
|
||||||
|
/// Top-level shell that frames the sidebar and main content area.
|
||||||
class AppShell extends StatelessWidget {
|
class AppShell extends StatelessWidget {
|
||||||
|
/// Creates an app shell with a [sidebar] and main [child].
|
||||||
const AppShell({required this.sidebar, required this.child, super.key});
|
const AppShell({required this.sidebar, required this.child, super.key});
|
||||||
|
|
||||||
|
/// Sidebar widget displayed at the left edge of the app.
|
||||||
final Widget sidebar;
|
final Widget sidebar;
|
||||||
|
|
||||||
|
/// Primary content widget displayed beside the sidebar.
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,19 @@ import '../controllers/scheduler_read_controller.dart';
|
||||||
import 'read_state_view.dart';
|
import 'read_state_view.dart';
|
||||||
import 'schedule_components.dart';
|
import 'schedule_components.dart';
|
||||||
|
|
||||||
|
/// Read-driven pane that renders backlog content and command controls.
|
||||||
class BacklogPane extends StatelessWidget {
|
class BacklogPane extends StatelessWidget {
|
||||||
|
/// Creates a backlog pane from a read [controller].
|
||||||
const BacklogPane({
|
const BacklogPane({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.commandController,
|
this.commandController,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Controller that loads backlog data.
|
||||||
final UiReadController<BacklogQueryResult> controller;
|
final UiReadController<BacklogQueryResult> controller;
|
||||||
|
|
||||||
|
/// Optional command controller for mutating backlog items.
|
||||||
final SchedulerCommandController? commandController;
|
final SchedulerCommandController? commandController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -32,14 +37,19 @@ class BacklogPane extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Populated backlog view for a loaded backlog query result.
|
||||||
class BacklogContent extends StatelessWidget {
|
class BacklogContent extends StatelessWidget {
|
||||||
|
/// Creates backlog content for [result].
|
||||||
const BacklogContent({
|
const BacklogContent({
|
||||||
required this.result,
|
required this.result,
|
||||||
this.commandController,
|
this.commandController,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Loaded backlog result to display.
|
||||||
final BacklogQueryResult result;
|
final BacklogQueryResult result;
|
||||||
|
|
||||||
|
/// Optional command controller for capture and scheduling actions.
|
||||||
final SchedulerCommandController? commandController;
|
final SchedulerCommandController? commandController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -73,9 +83,12 @@ class BacklogContent extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Small form for quickly capturing a task into the backlog.
|
||||||
class QuickCaptureForm extends StatefulWidget {
|
class QuickCaptureForm extends StatefulWidget {
|
||||||
|
/// Creates a quick capture form.
|
||||||
const QuickCaptureForm({required this.commandController, super.key});
|
const QuickCaptureForm({required this.commandController, super.key});
|
||||||
|
|
||||||
|
/// Command controller used to submit captured task titles.
|
||||||
final SchedulerCommandController commandController;
|
final SchedulerCommandController commandController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -121,9 +134,12 @@ class _QuickCaptureFormState extends State<QuickCaptureForm> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Displays the latest command state for backlog interactions.
|
||||||
class CommandStatusBanner extends StatelessWidget {
|
class CommandStatusBanner extends StatelessWidget {
|
||||||
|
/// Creates a command status banner.
|
||||||
const CommandStatusBanner({required this.controller, super.key});
|
const CommandStatusBanner({required this.controller, super.key});
|
||||||
|
|
||||||
|
/// Command controller whose state should be rendered.
|
||||||
final SchedulerCommandController controller;
|
final SchedulerCommandController controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../controllers/scheduler_read_controller.dart';
|
import '../controllers/scheduler_read_controller.dart';
|
||||||
|
|
||||||
|
/// Renders loading, empty, failure, and data states from a read controller.
|
||||||
class ReadStateView<T> extends StatelessWidget {
|
class ReadStateView<T> extends StatelessWidget {
|
||||||
|
/// Creates a read-state view.
|
||||||
const ReadStateView({
|
const ReadStateView({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.title,
|
required this.title,
|
||||||
|
|
@ -15,10 +17,19 @@ class ReadStateView<T> extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Controller that owns the current read state.
|
||||||
final UiReadController<T> controller;
|
final UiReadController<T> controller;
|
||||||
|
|
||||||
|
/// Name of the surface being loaded, used in failure copy.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
/// Title shown when the read succeeds with empty content.
|
||||||
final String emptyTitle;
|
final String emptyTitle;
|
||||||
|
|
||||||
|
/// Message shown when the read succeeds with empty content.
|
||||||
final String emptyMessage;
|
final String emptyMessage;
|
||||||
|
|
||||||
|
/// Builds the populated content for a read value.
|
||||||
final Widget Function(BuildContext context, T value) dataBuilder;
|
final Widget Function(BuildContext context, T value) dataBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -52,7 +63,9 @@ class ReadStateView<T> extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Standard empty-state panel with optional surrounding content.
|
||||||
class EmptyStatePanel extends StatelessWidget {
|
class EmptyStatePanel extends StatelessWidget {
|
||||||
|
/// Creates an empty-state panel.
|
||||||
const EmptyStatePanel({
|
const EmptyStatePanel({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.message,
|
required this.message,
|
||||||
|
|
@ -60,8 +73,13 @@ class EmptyStatePanel extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Primary empty-state title.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
/// Supporting empty-state message.
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
/// Optional content shown alongside the empty-state panel.
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -97,7 +115,9 @@ class EmptyStatePanel extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Standard failure panel with a retry action.
|
||||||
class FailureStatePanel extends StatelessWidget {
|
class FailureStatePanel extends StatelessWidget {
|
||||||
|
/// Creates a failure-state panel.
|
||||||
const FailureStatePanel({
|
const FailureStatePanel({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.code,
|
required this.code,
|
||||||
|
|
@ -106,9 +126,16 @@ class FailureStatePanel extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Primary failure title.
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
|
/// Displayable failure code.
|
||||||
final String code;
|
final String code;
|
||||||
|
|
||||||
|
/// Optional technical detail for unexpected errors.
|
||||||
final String? technicalDetail;
|
final String? technicalDetail;
|
||||||
|
|
||||||
|
/// Callback invoked when the user retries the read.
|
||||||
final Future<void> Function() onRetry;
|
final Future<void> Function() onRetry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,12 @@ import 'package:flutter/material.dart';
|
||||||
import '../models/today_screen_models.dart';
|
import '../models/today_screen_models.dart';
|
||||||
import '../theme/focus_flow_tokens.dart';
|
import '../theme/focus_flow_tokens.dart';
|
||||||
|
|
||||||
|
/// Banner that highlights the next required task, if one exists.
|
||||||
class RequiredBanner extends StatelessWidget {
|
class RequiredBanner extends StatelessWidget {
|
||||||
|
/// Creates a required-task banner.
|
||||||
const RequiredBanner({this.model, super.key});
|
const RequiredBanner({this.model, super.key});
|
||||||
|
|
||||||
|
/// Optional required task model to render.
|
||||||
final RequiredBannerModel? model;
|
final RequiredBannerModel? model;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,12 @@ import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
import '../theme/scheduler_visual_tokens.dart';
|
import '../theme/scheduler_visual_tokens.dart';
|
||||||
|
|
||||||
|
/// Compact summary panel for timeline items.
|
||||||
class CompactPanel extends StatelessWidget {
|
class CompactPanel extends StatelessWidget {
|
||||||
|
/// Creates a compact panel for [items].
|
||||||
const CompactPanel({required this.items, super.key});
|
const CompactPanel({required this.items, super.key});
|
||||||
|
|
||||||
|
/// Timeline items to display in compact form.
|
||||||
final List<TodayTimelineItem> items;
|
final List<TodayTimelineItem> items;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -28,10 +31,15 @@ class CompactPanel extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Card row for a single Today timeline item.
|
||||||
class TimelineRow extends StatelessWidget {
|
class TimelineRow extends StatelessWidget {
|
||||||
|
/// Creates a timeline row.
|
||||||
const TimelineRow({required this.item, this.onDone, super.key});
|
const TimelineRow({required this.item, this.onDone, super.key});
|
||||||
|
|
||||||
|
/// Timeline item to render.
|
||||||
final TodayTimelineItem item;
|
final TodayTimelineItem item;
|
||||||
|
|
||||||
|
/// Optional completion callback for eligible tasks.
|
||||||
final Future<void> Function()? onDone;
|
final Future<void> Function()? onDone;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -85,10 +93,15 @@ class TimelineRow extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Backlog row with optional scheduling controls.
|
||||||
class BacklogRow extends StatefulWidget {
|
class BacklogRow extends StatefulWidget {
|
||||||
|
/// Creates a backlog row.
|
||||||
const BacklogRow({required this.item, this.onSchedule, super.key});
|
const BacklogRow({required this.item, this.onSchedule, super.key});
|
||||||
|
|
||||||
|
/// Backlog item to render.
|
||||||
final BacklogItemReadModel item;
|
final BacklogItemReadModel item;
|
||||||
|
|
||||||
|
/// Optional callback that schedules the item for a duration in minutes.
|
||||||
final Future<void> Function(int durationMinutes)? onSchedule;
|
final Future<void> Function(int durationMinutes)? onSchedule;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -154,9 +167,12 @@ class _BacklogRowState extends State<BacklogRow> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Icon marker for backlog item staleness.
|
||||||
class StalenessMarker extends StatelessWidget {
|
class StalenessMarker extends StatelessWidget {
|
||||||
|
/// Creates a staleness marker.
|
||||||
const StalenessMarker({required this.marker, super.key});
|
const StalenessMarker({required this.marker, super.key});
|
||||||
|
|
||||||
|
/// Staleness marker value to render.
|
||||||
final BacklogStalenessMarker marker;
|
final BacklogStalenessMarker marker;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -168,9 +184,12 @@ class StalenessMarker extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notice banner for pending Today-state messages.
|
||||||
class NoticeBanner extends StatelessWidget {
|
class NoticeBanner extends StatelessWidget {
|
||||||
|
/// Creates a notice banner.
|
||||||
const NoticeBanner({required this.message, super.key});
|
const NoticeBanner({required this.message, super.key});
|
||||||
|
|
||||||
|
/// Notice message to display.
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -187,6 +206,7 @@ class NoticeBanner extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Formats a scheduler timeline item as display-ready time text.
|
||||||
String timeText(TimelineItem item) {
|
String timeText(TimelineItem item) {
|
||||||
final start = item.start;
|
final start = item.start;
|
||||||
final end = item.end;
|
final end = item.end;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../theme/focus_flow_tokens.dart';
|
import '../theme/focus_flow_tokens.dart';
|
||||||
|
|
||||||
|
/// Static sidebar for primary FocusFlow navigation.
|
||||||
class Sidebar extends StatelessWidget {
|
class Sidebar extends StatelessWidget {
|
||||||
|
/// Creates the FocusFlow sidebar.
|
||||||
const Sidebar({super.key});
|
const Sidebar({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,19 @@ import '../theme/focus_flow_tokens.dart';
|
||||||
import 'timeline/difficulty_bars.dart';
|
import 'timeline/difficulty_bars.dart';
|
||||||
import 'timeline/reward_icon.dart';
|
import 'timeline/reward_icon.dart';
|
||||||
|
|
||||||
|
/// Modal shown when a selectable timeline task card is opened.
|
||||||
class TaskSelectionModal extends StatelessWidget {
|
class TaskSelectionModal extends StatelessWidget {
|
||||||
|
/// Creates a task selection modal for [card].
|
||||||
const TaskSelectionModal({
|
const TaskSelectionModal({
|
||||||
required this.card,
|
required this.card,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Card model whose details and actions should be displayed.
|
||||||
final TimelineCardModel card;
|
final TimelineCardModel card;
|
||||||
|
|
||||||
|
/// Callback invoked when the modal should close.
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
||||||
import '../../theme/focus_flow_tokens.dart';
|
import '../../theme/focus_flow_tokens.dart';
|
||||||
|
|
||||||
|
/// Five-bar effort indicator for a timeline task.
|
||||||
class DifficultyBars extends StatelessWidget {
|
class DifficultyBars extends StatelessWidget {
|
||||||
|
/// Creates difficulty bars for a scheduler difficulty token.
|
||||||
const DifficultyBars({
|
const DifficultyBars({
|
||||||
required this.difficulty,
|
required this.difficulty,
|
||||||
this.color = FocusFlowTokens.restPurple,
|
this.color = FocusFlowTokens.restPurple,
|
||||||
|
|
@ -14,10 +16,16 @@ class DifficultyBars extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Difficulty token that determines how many bars are filled.
|
||||||
final TimelineDifficultyIconToken difficulty;
|
final TimelineDifficultyIconToken difficulty;
|
||||||
|
|
||||||
|
/// Color used for filled bars.
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
|
/// Fixed paint size for the indicator.
|
||||||
final Size size;
|
final Size size;
|
||||||
|
|
||||||
|
/// Converts a timeline difficulty token to a filled bar count.
|
||||||
static int fillCount(TimelineDifficultyIconToken difficulty) {
|
static int fillCount(TimelineDifficultyIconToken difficulty) {
|
||||||
return switch (difficulty) {
|
return switch (difficulty) {
|
||||||
TimelineDifficultyIconToken.notSet => 0,
|
TimelineDifficultyIconToken.notSet => 0,
|
||||||
|
|
@ -29,6 +37,7 @@ class DifficultyBars extends StatelessWidget {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts a domain difficulty level to a filled bar count.
|
||||||
static int fillCountForLevel(DifficultyLevel difficulty) {
|
static int fillCountForLevel(DifficultyLevel difficulty) {
|
||||||
return switch (difficulty) {
|
return switch (difficulty) {
|
||||||
DifficultyLevel.notSet => 0,
|
DifficultyLevel.notSet => 0,
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,19 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../theme/focus_flow_tokens.dart';
|
import '../../theme/focus_flow_tokens.dart';
|
||||||
|
|
||||||
|
/// Sparkle-style reward indicator for a timeline task.
|
||||||
class RewardIcon extends StatelessWidget {
|
class RewardIcon extends StatelessWidget {
|
||||||
|
/// Creates a reward icon.
|
||||||
const RewardIcon({
|
const RewardIcon({
|
||||||
this.color = FocusFlowTokens.accentMagenta,
|
this.color = FocusFlowTokens.accentMagenta,
|
||||||
this.size = 24,
|
this.size = 24,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Fill color used by the icon painter.
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
|
/// Square size of the painted icon.
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@ import '../../theme/focus_flow_tokens.dart';
|
||||||
import 'difficulty_bars.dart';
|
import 'difficulty_bars.dart';
|
||||||
import 'reward_icon.dart';
|
import 'reward_icon.dart';
|
||||||
|
|
||||||
|
/// Interactive card that renders a scheduled task on the compact timeline.
|
||||||
class TaskTimelineCard extends StatelessWidget {
|
class TaskTimelineCard extends StatelessWidget {
|
||||||
|
/// Creates a task timeline card.
|
||||||
const TaskTimelineCard({required this.card, required this.onTap, super.key});
|
const TaskTimelineCard({required this.card, required this.onTap, super.key});
|
||||||
|
|
||||||
|
/// Card model to render.
|
||||||
final TimelineCardModel card;
|
final TimelineCardModel card;
|
||||||
|
|
||||||
|
/// Callback invoked when the card is tapped.
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,12 @@ import 'package:flutter/material.dart';
|
||||||
import '../../theme/focus_flow_tokens.dart';
|
import '../../theme/focus_flow_tokens.dart';
|
||||||
import 'timeline_geometry.dart';
|
import 'timeline_geometry.dart';
|
||||||
|
|
||||||
|
/// Time labels and rail shown beside the compact timeline.
|
||||||
class TimelineAxis extends StatelessWidget {
|
class TimelineAxis extends StatelessWidget {
|
||||||
|
/// Creates a timeline axis for [geometry].
|
||||||
const TimelineAxis({required this.geometry, super.key});
|
const TimelineAxis({required this.geometry, super.key});
|
||||||
|
|
||||||
|
/// Geometry used to position labels and tick marks.
|
||||||
final TimelineGeometry geometry;
|
final TimelineGeometry geometry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -60,9 +63,12 @@ class TimelineAxis extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Background grid aligned to timeline tick marks.
|
||||||
class TimelineGrid extends StatelessWidget {
|
class TimelineGrid extends StatelessWidget {
|
||||||
|
/// Creates a timeline grid for [geometry].
|
||||||
const TimelineGrid({required this.geometry, super.key});
|
const TimelineGrid({required this.geometry, super.key});
|
||||||
|
|
||||||
|
/// Geometry used to position grid lines.
|
||||||
final TimelineGeometry geometry;
|
final TimelineGeometry geometry;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Converts timeline clock values into vertical pixel positions.
|
||||||
class TimelineGeometry {
|
class TimelineGeometry {
|
||||||
|
/// Creates geometry for a visible time range.
|
||||||
const TimelineGeometry({
|
const TimelineGeometry({
|
||||||
required this.visibleStart,
|
required this.visibleStart,
|
||||||
required this.visibleEnd,
|
required this.visibleEnd,
|
||||||
|
|
@ -11,26 +13,39 @@ class TimelineGeometry {
|
||||||
this.minCardHeight = 72,
|
this.minCardHeight = 72,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// First visible time on the timeline.
|
||||||
final TimeOfDay visibleStart;
|
final TimeOfDay visibleStart;
|
||||||
|
|
||||||
|
/// Last visible time on the timeline.
|
||||||
final TimeOfDay visibleEnd;
|
final TimeOfDay visibleEnd;
|
||||||
|
|
||||||
|
/// Vertical scale used to convert minutes into pixels.
|
||||||
final double pixelsPerMinute;
|
final double pixelsPerMinute;
|
||||||
|
|
||||||
|
/// Minimum rendered height for a timeline card.
|
||||||
final double minCardHeight;
|
final double minCardHeight;
|
||||||
|
|
||||||
|
/// First visible minute from midnight.
|
||||||
int get startMinutes => visibleStart.hour * 60 + visibleStart.minute;
|
int get startMinutes => visibleStart.hour * 60 + visibleStart.minute;
|
||||||
|
|
||||||
|
/// Last visible minute from midnight.
|
||||||
int get endMinutes => visibleEnd.hour * 60 + visibleEnd.minute;
|
int get endMinutes => visibleEnd.hour * 60 + visibleEnd.minute;
|
||||||
|
|
||||||
|
/// Total vertical height of the visible timeline.
|
||||||
double get totalHeight => (endMinutes - startMinutes) * pixelsPerMinute;
|
double get totalHeight => (endMinutes - startMinutes) * pixelsPerMinute;
|
||||||
|
|
||||||
|
/// Returns the vertical offset for [minutes] since midnight.
|
||||||
double yForMinutesSinceMidnight(int minutes) {
|
double yForMinutesSinceMidnight(int minutes) {
|
||||||
return (minutes - startMinutes) * pixelsPerMinute;
|
return (minutes - startMinutes) * pixelsPerMinute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the rendered height for a duration in minutes.
|
||||||
double heightForDuration(int minutes) {
|
double heightForDuration(int minutes) {
|
||||||
final height = minutes * pixelsPerMinute;
|
final height = minutes * pixelsPerMinute;
|
||||||
return height < minCardHeight ? minCardHeight : height;
|
return height < minCardHeight ? minCardHeight : height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates timeline ticks at [stepMinutes] intervals.
|
||||||
List<TimelineTick> ticks({int stepMinutes = 15}) {
|
List<TimelineTick> ticks({int stepMinutes = 15}) {
|
||||||
return [
|
return [
|
||||||
for (
|
for (
|
||||||
|
|
@ -60,7 +75,9 @@ class TimelineGeometry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A labeled tick on the compact timeline axis.
|
||||||
class TimelineTick {
|
class TimelineTick {
|
||||||
|
/// Creates a timeline tick.
|
||||||
const TimelineTick({
|
const TimelineTick({
|
||||||
required this.minutesSinceMidnight,
|
required this.minutesSinceMidnight,
|
||||||
required this.y,
|
required this.y,
|
||||||
|
|
@ -68,8 +85,15 @@ class TimelineTick {
|
||||||
required this.major,
|
required this.major,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Minute from midnight represented by this tick.
|
||||||
final int minutesSinceMidnight;
|
final int minutesSinceMidnight;
|
||||||
|
|
||||||
|
/// Vertical pixel offset for this tick.
|
||||||
final double y;
|
final double y;
|
||||||
|
|
||||||
|
/// Display label for this tick.
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
/// Whether this tick represents a major interval.
|
||||||
final bool major;
|
final bool major;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import 'task_timeline_card.dart';
|
||||||
import 'timeline_axis.dart';
|
import 'timeline_axis.dart';
|
||||||
import 'timeline_geometry.dart';
|
import 'timeline_geometry.dart';
|
||||||
|
|
||||||
|
/// Scrollable compact timeline view for Today cards.
|
||||||
class TimelineView extends StatelessWidget {
|
class TimelineView extends StatelessWidget {
|
||||||
|
/// Creates a timeline view.
|
||||||
const TimelineView({
|
const TimelineView({
|
||||||
required this.cards,
|
required this.cards,
|
||||||
required this.range,
|
required this.range,
|
||||||
|
|
@ -17,8 +19,13 @@ class TimelineView extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Cards to position on the timeline.
|
||||||
final List<TimelineCardModel> cards;
|
final List<TimelineCardModel> cards;
|
||||||
|
|
||||||
|
/// Visible range used to build the timeline geometry.
|
||||||
final TimelineRangeModel range;
|
final TimelineRangeModel range;
|
||||||
|
|
||||||
|
/// Callback invoked when a card is selected.
|
||||||
final ValueChanged<TimelineCardModel> onCardSelected;
|
final ValueChanged<TimelineCardModel> onCardSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,19 @@ import '../controllers/scheduler_read_controller.dart';
|
||||||
import 'read_state_view.dart';
|
import 'read_state_view.dart';
|
||||||
import 'schedule_components.dart';
|
import 'schedule_components.dart';
|
||||||
|
|
||||||
|
/// Read-driven pane that renders the Today view and optional commands.
|
||||||
class TodayPane extends StatelessWidget {
|
class TodayPane extends StatelessWidget {
|
||||||
|
/// Creates a Today pane from a read [controller].
|
||||||
const TodayPane({
|
const TodayPane({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
this.commandController,
|
this.commandController,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Controller that loads Today state.
|
||||||
final UiReadController<TodayState> controller;
|
final UiReadController<TodayState> controller;
|
||||||
|
|
||||||
|
/// Optional command controller for Today actions.
|
||||||
final SchedulerCommandController? commandController;
|
final SchedulerCommandController? commandController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -32,10 +37,15 @@ class TodayPane extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Populated Today view for a loaded Today state.
|
||||||
class TodayContent extends StatelessWidget {
|
class TodayContent extends StatelessWidget {
|
||||||
|
/// Creates Today content for [state].
|
||||||
const TodayContent({required this.state, this.commandController, super.key});
|
const TodayContent({required this.state, this.commandController, super.key});
|
||||||
|
|
||||||
|
/// Loaded Today state to display.
|
||||||
final TodayState state;
|
final TodayState state;
|
||||||
|
|
||||||
|
/// Optional command controller for completing tasks.
|
||||||
final SchedulerCommandController? commandController;
|
final SchedulerCommandController? commandController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../theme/focus_flow_tokens.dart';
|
import '../theme/focus_flow_tokens.dart';
|
||||||
|
|
||||||
|
/// Header bar for the compact Today view.
|
||||||
class TopBar extends StatelessWidget {
|
class TopBar extends StatelessWidget {
|
||||||
|
/// Creates a top bar with a display-ready [dateLabel].
|
||||||
const TopBar({required this.dateLabel, super.key});
|
const TopBar({required this.dateLabel, super.key});
|
||||||
|
|
||||||
|
/// Date label shown beside the date navigation controls.
|
||||||
final String dateLabel;
|
final String dateLabel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Runs app package-boundary tests.
|
||||||
void main() {
|
void main() {
|
||||||
test('Flutter UI imports stay inside the UI Plan 1 package boundary', () {
|
test('Flutter UI imports stay inside the UI Plan 1 package boundary', () {
|
||||||
final appRoot = Directory.current;
|
final appRoot = Directory.current;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:focus_flow_flutter/app/focus_flow_app.dart';
|
||||||
import 'package:focus_flow_flutter/models/today_screen_models.dart';
|
import 'package:focus_flow_flutter/models/today_screen_models.dart';
|
||||||
import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart';
|
import 'package:focus_flow_flutter/widgets/timeline/difficulty_bars.dart';
|
||||||
|
|
||||||
|
/// Runs FocusFlow widget and visual adapter tests.
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('app shell renders compact Today frame', (tester) async {
|
testWidgets('app shell renders compact Today frame', (tester) async {
|
||||||
await _pumpPlanApp(tester);
|
await _pumpPlanApp(tester);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue