focus-flow/apps/focus_flow_flutter/lib/app/focus_flow_app.dart

176 lines
5.1 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 StatelessWidget {
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
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: data.dateLabel),
const SizedBox(height: 24),
RequiredBanner(model: data.requiredBanner),
const SizedBox(height: 22),
Expanded(
child: TimelineView(
cards: data.cards,
range: data.timelineRange,
onCardSelected: onSelectCard,
),
),
],
),
),
if (selectedCard != null) ...[
Positioned.fill(
child: GestureDetector(
key: const ValueKey('modal-click-away'),
behavior: HitTestBehavior.opaque,
onTap: onClearSelection,
child: const ColoredBox(color: Colors.transparent),
),
),
Center(
child: TaskSelectionModal(
card: selectedCard!,
onClose: onClearSelection,
),
),
],
],
);
}
}