Initial scaffold: FocusFlow ADHD Task Manager Flutter app
BLoC/Cubit state management, ADHD-friendly theme (calming teal, no red), GetIt DI, GoRouter navigation. Screens: task dashboard, focus mode, task create/detail, streaks, time perception, settings, onboarding, auth. Custom widgets: TaskCard, RewardPopup, StreakRing, GentleNudgeCard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
220
lib/features/tasks/presentation/screens/task_detail_screen.dart
Normal file
220
lib/features/tasks/presentation/screens/task_detail_screen.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:focusflow_shared/focusflow_shared.dart';
|
||||
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/widgets/time_visualizer.dart';
|
||||
import '../widgets/energy_selector.dart';
|
||||
|
||||
/// Task detail / edit screen.
|
||||
///
|
||||
/// Shows title, description, energy level selector, time estimate,
|
||||
/// tags, category. Provides save and delete actions.
|
||||
class TaskDetailScreen extends StatefulWidget {
|
||||
final String taskId;
|
||||
|
||||
const TaskDetailScreen({super.key, required this.taskId});
|
||||
|
||||
@override
|
||||
State<TaskDetailScreen> createState() => _TaskDetailScreenState();
|
||||
}
|
||||
|
||||
class _TaskDetailScreenState extends State<TaskDetailScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _titleController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _minutesController = TextEditingController();
|
||||
final _tagsController = TextEditingController();
|
||||
final _categoryController = TextEditingController();
|
||||
String _energyLevel = 'medium';
|
||||
|
||||
// Placeholder task — in production, load from BLoC.
|
||||
Task? _task;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadTask();
|
||||
}
|
||||
|
||||
Future<void> _loadTask() async {
|
||||
// Simulated placeholder — in production the BLoC fetches from the API.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
final task = Task(
|
||||
id: widget.taskId,
|
||||
userId: 'local',
|
||||
title: 'Sample Task',
|
||||
description: 'Tap to edit this task.',
|
||||
energyLevel: 'medium',
|
||||
estimatedMinutes: 25,
|
||||
actualMinutes: 20,
|
||||
tags: const ['work', 'focus'],
|
||||
category: 'work',
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_task = task;
|
||||
_loading = false;
|
||||
_titleController.text = task.title;
|
||||
_descriptionController.text = task.description ?? '';
|
||||
_minutesController.text = task.estimatedMinutes?.toString() ?? '';
|
||||
_tagsController.text = task.tags.join(', ');
|
||||
_categoryController.text = task.category ?? '';
|
||||
_energyLevel = task.energyLevel;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_minutesController.dispose();
|
||||
_tagsController.dispose();
|
||||
_categoryController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _save() {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
// TODO: dispatch update via BLoC.
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Task saved.')),
|
||||
);
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _delete() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete task?'),
|
||||
content: const Text('This cannot be undone.'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
// TODO: dispatch delete via BLoC.
|
||||
context.pop();
|
||||
},
|
||||
child: const Text('Delete', style: TextStyle(color: AppColors.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (_loading) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Task')),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Task Detail'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline_rounded, color: AppColors.error),
|
||||
onPressed: _delete,
|
||||
tooltip: 'Delete',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── Title ──────────────────────────────────────────
|
||||
TextFormField(
|
||||
controller: _titleController,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
style: theme.textTheme.titleLarge,
|
||||
validator: (v) =>
|
||||
(v == null || v.trim().isEmpty) ? 'Give it a name' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Description ────────────────────────────────────
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(labelText: 'Description (optional)'),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Energy level ───────────────────────────────────
|
||||
Text('Energy level', style: theme.textTheme.titleSmall),
|
||||
const SizedBox(height: 8),
|
||||
EnergySelector(
|
||||
value: _energyLevel,
|
||||
onChanged: (v) => setState(() => _energyLevel = v),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Time estimate ──────────────────────────────────
|
||||
TextFormField(
|
||||
controller: _minutesController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Estimated minutes',
|
||||
suffixText: 'min',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Time visualizer (if there is actual data) ──────
|
||||
if (_task != null &&
|
||||
_task!.estimatedMinutes != null &&
|
||||
_task!.actualMinutes != null)
|
||||
TimeVisualizer(
|
||||
estimatedMinutes: _task!.estimatedMinutes!,
|
||||
actualMinutes: _task!.actualMinutes,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Tags ───────────────────────────────────────────
|
||||
TextFormField(
|
||||
controller: _tagsController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tags (comma separated)',
|
||||
prefixIcon: Icon(Icons.label_outline),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Category ───────────────────────────────────────
|
||||
TextFormField(
|
||||
controller: _categoryController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Category',
|
||||
prefixIcon: Icon(Icons.category_outlined),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// ── Save button ────────────────────────────────────
|
||||
FilledButton(
|
||||
onPressed: _save,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user