200 lines
5.9 KiB
Dart
200 lines
5.9 KiB
Dart
/// Composes the FocusFlow Flutter application around Focus Flow App.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../controllers/today_screen_controller.dart';
|
|
import '../models/today_screen_models.dart';
|
|
import '../theme/focus_flow_theme.dart';
|
|
import '../theme/focus_flow_tokens.dart';
|
|
import '../widgets/app_shell.dart';
|
|
import '../widgets/required_banner.dart';
|
|
import '../widgets/sidebar.dart';
|
|
import '../widgets/task_selection_modal.dart';
|
|
import '../widgets/timeline/timeline_view.dart';
|
|
import '../widgets/top_bar.dart';
|
|
import 'demo_scheduler_composition.dart';
|
|
|
|
/// Root Material app for the FocusFlow desktop UI.
|
|
class FocusFlowApp extends StatelessWidget {
|
|
/// Creates a FocusFlow app from an application composition.
|
|
const FocusFlowApp({required this.composition, super.key});
|
|
|
|
/// Factory and controller bundle used by the app tree.
|
|
final DemoSchedulerComposition composition;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'FocusFlow',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: FocusFlowTheme.dark(),
|
|
home: FocusFlowHome(composition: composition),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Stateful home screen that owns the Today controller lifecycle.
|
|
class FocusFlowHome extends StatefulWidget {
|
|
/// Creates the FocusFlow home screen from an application composition.
|
|
const FocusFlowHome({required this.composition, super.key});
|
|
|
|
/// Factory and controller bundle used by the home screen.
|
|
final DemoSchedulerComposition composition;
|
|
|
|
@override
|
|
State<FocusFlowHome> createState() => _FocusFlowHomeState();
|
|
}
|
|
|
|
class _FocusFlowHomeState extends State<FocusFlowHome> {
|
|
late final TodayScreenController controller = widget.composition
|
|
.createTodayScreenController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
controller.load();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: FocusFlowTokens.appBackground,
|
|
body: AppShell(
|
|
sidebar: const Sidebar(),
|
|
child: AnimatedBuilder(
|
|
animation: controller,
|
|
builder: (context, _) {
|
|
return switch (controller.state) {
|
|
TodayScreenLoading() => const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
TodayScreenFailure(:final message) => _PlanIssue(
|
|
message: message,
|
|
),
|
|
TodayScreenEmpty() => _TodayScreenScaffold(
|
|
data: TodayScreenData.empty(
|
|
dateLabel: widget.composition.dateLabel,
|
|
),
|
|
selectedCard: controller.selectedCard,
|
|
onSelectCard: controller.selectCard,
|
|
onClearSelection: controller.clearSelection,
|
|
),
|
|
TodayScreenReady(:final data) => _TodayScreenScaffold(
|
|
data: data,
|
|
selectedCard: controller.selectedCard,
|
|
onSelectCard: controller.selectCard,
|
|
onClearSelection: controller.clearSelection,
|
|
),
|
|
};
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PlanIssue extends StatelessWidget {
|
|
const _PlanIssue({required this.message});
|
|
|
|
final String message;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Text(message, style: Theme.of(context).textTheme.titleMedium),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TodayScreenScaffold extends StatefulWidget {
|
|
const _TodayScreenScaffold({
|
|
required this.data,
|
|
required this.selectedCard,
|
|
required this.onSelectCard,
|
|
required this.onClearSelection,
|
|
});
|
|
|
|
final TodayScreenData data;
|
|
final TimelineCardModel? selectedCard;
|
|
final ValueChanged<TimelineCardModel> onSelectCard;
|
|
final VoidCallback onClearSelection;
|
|
|
|
@override
|
|
State<_TodayScreenScaffold> createState() => _TodayScreenScaffoldState();
|
|
}
|
|
|
|
class _TodayScreenScaffoldState extends State<_TodayScreenScaffold> {
|
|
String? _timelineScrollTargetCardId;
|
|
var _timelineScrollRequest = 0;
|
|
|
|
void _showUpcomingRequiredTask() {
|
|
final targetCardId = widget.data.requiredBanner?.timelineCardId;
|
|
if (targetCardId == null) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_timelineScrollTargetCardId = targetCardId;
|
|
_timelineScrollRequest += 1;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(
|
|
FocusFlowTokens.mainGutter,
|
|
32,
|
|
FocusFlowTokens.mainGutter,
|
|
28,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
TopBar(dateLabel: widget.data.dateLabel),
|
|
const SizedBox(height: 24),
|
|
RequiredBanner(
|
|
model: widget.data.requiredBanner,
|
|
onShowUpcoming: _showUpcomingRequiredTask,
|
|
),
|
|
const SizedBox(height: 22),
|
|
Expanded(
|
|
child: TimelineView(
|
|
cards: widget.data.cards,
|
|
range: widget.data.timelineRange,
|
|
scrollTargetCardId: _timelineScrollTargetCardId,
|
|
scrollRequest: _timelineScrollRequest,
|
|
onCardSelected: widget.onSelectCard,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (widget.selectedCard != null) ...[
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
key: const ValueKey('modal-click-away'),
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: widget.onClearSelection,
|
|
child: const ColoredBox(color: Colors.transparent),
|
|
),
|
|
),
|
|
Center(
|
|
child: TaskSelectionModal(
|
|
card: widget.selectedCard!,
|
|
onClose: widget.onClearSelection,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|