436 lines
16 KiB
Dart
436 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:hive_flutter/hive_flutter.dart';
|
|
import '../models/task_item.dart';
|
|
import '../services/database_service.dart';
|
|
import '../widgets/elegant_header.dart';
|
|
import '../widgets/task_tile.dart';
|
|
import 'focus_timer_page.dart';
|
|
|
|
class DailyFocusPage extends StatefulWidget {
|
|
const DailyFocusPage({super.key});
|
|
|
|
@override
|
|
State<DailyFocusPage> createState() => _DailyFocusPageState();
|
|
}
|
|
|
|
class _DailyFocusPageState extends State<DailyFocusPage> {
|
|
late Box<TaskItem> _tasksBox;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tasksBox = DatabaseService.getTasksBox();
|
|
}
|
|
|
|
List<TaskItem> _getSortedTasks() {
|
|
final tasks = _tasksBox.values.toList();
|
|
tasks.sort((a, b) => a.sortIndex.compareTo(b.sortIndex));
|
|
return tasks;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: _tasksBox.listenable(),
|
|
builder: (context, Box<TaskItem> box, _) {
|
|
final allTasks = _getSortedTasks();
|
|
final essenceTasks = allTasks.take(3).toList();
|
|
final backlogTasks = allTasks.skip(3).toList();
|
|
final completedCount = allTasks.where((t) => t.isCompleted).length;
|
|
|
|
return Scaffold(
|
|
body: SafeArea(
|
|
bottom: false,
|
|
child: Column(
|
|
children: [
|
|
const ElegantHeader(),
|
|
Expanded(
|
|
child: CustomScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
slivers: [
|
|
// Header Section
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(28, 10, 28, 30),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Good Morning.",
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade400,
|
|
letterSpacing: 1,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
"Your Essence.",
|
|
style: TextStyle(
|
|
fontSize: 40,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.black,
|
|
height: 1.1,
|
|
letterSpacing: -1,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
// Progress Bar
|
|
Stack(
|
|
children: [
|
|
Container(
|
|
height: 4,
|
|
width: 100,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
AnimatedContainer(
|
|
duration: const Duration(milliseconds: 500),
|
|
height: 4,
|
|
width: allTasks.isEmpty
|
|
? 0
|
|
: (100 * (completedCount / allTasks.length)),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFF7F50),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
"$completedCount / ${allTasks.length} Completed",
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Essence (Top 3)
|
|
if (essenceTasks.isNotEmpty)
|
|
SliverReorderableList(
|
|
itemCount: essenceTasks.length,
|
|
onReorder: (old, newIdx) =>
|
|
_onReorder(allTasks, old, newIdx),
|
|
itemBuilder: (context, index) {
|
|
final task = essenceTasks[index];
|
|
return ReorderableDragStartListener(
|
|
key: ValueKey(task.id),
|
|
index: index,
|
|
child: TaskTile(
|
|
task: task,
|
|
index: index,
|
|
isPriority: true,
|
|
isTopOne: index == 0,
|
|
onTap: () => _showTaskDetails(context, task),
|
|
onFocusTap: index == 0
|
|
? () => _startFocusMode(context, task)
|
|
: null,
|
|
onDismissed: () => _tasksBox.delete(task.id),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
// Backlog Divider
|
|
if (backlogTasks.isNotEmpty)
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(28, 40, 28, 15),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
"UP NEXT",
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.grey.shade300,
|
|
letterSpacing: 1.2,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Divider(color: Colors.grey.shade100),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Backlog List
|
|
SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) {
|
|
final actualIndex = index + 3;
|
|
return TaskTile(
|
|
task: backlogTasks[index],
|
|
index: actualIndex,
|
|
isPriority: false,
|
|
onTap: () =>
|
|
_showTaskDetails(context, backlogTasks[index]),
|
|
onDismissed: () =>
|
|
_tasksBox.delete(backlogTasks[index].id),
|
|
);
|
|
},
|
|
childCount: backlogTasks.length,
|
|
),
|
|
),
|
|
|
|
const SliverToBoxAdapter(child: SizedBox(height: 120)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
onPressed: () => _showAddTaskModal(context),
|
|
backgroundColor: Colors.black,
|
|
elevation: 8,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(30),
|
|
),
|
|
label: const Row(
|
|
children: [
|
|
Icon(Icons.add, color: Colors.white, size: 20),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
"New Task",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _onReorder(List<TaskItem> allTasks, int oldIndex, int newIndex) async {
|
|
if (newIndex > oldIndex) newIndex -= 1;
|
|
final item = allTasks.removeAt(oldIndex);
|
|
allTasks.insert(newIndex, item);
|
|
for (int i = 0; i < allTasks.length; i++) {
|
|
final task = allTasks[i];
|
|
task.sortIndex = i;
|
|
await _tasksBox.put(task.id, task);
|
|
}
|
|
HapticFeedback.selectionClick();
|
|
}
|
|
|
|
void _showAddTaskModal(BuildContext context) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (context) => Padding(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(28),
|
|
height: 220,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"What is essential?",
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
autofocus: true,
|
|
decoration: InputDecoration(
|
|
hintText: "Enter task name...",
|
|
hintStyle: TextStyle(
|
|
color: Colors.grey.shade300,
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
border: InputBorder.none,
|
|
),
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
onSubmitted: (value) {
|
|
if (value.isNotEmpty) {
|
|
final newId = DateTime.now().millisecondsSinceEpoch.toString();
|
|
final newTask = TaskItem(
|
|
id: newId,
|
|
title: value,
|
|
createdAt: DateTime.now(),
|
|
sortIndex: _tasksBox.length,
|
|
);
|
|
_tasksBox.put(newId, newTask);
|
|
Navigator.pop(context);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showTaskDetails(BuildContext context, TaskItem task) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useRootNavigator: true,
|
|
builder: (_) => DraggableScrollableSheet(
|
|
initialChildSize: 0.55,
|
|
minChildSize: 0.4,
|
|
maxChildSize: 0.9,
|
|
expand: false,
|
|
builder: (_, controller) => Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
child: ListView(
|
|
controller: controller,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 30),
|
|
TextFormField(
|
|
initialValue: task.title,
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
decoration: const InputDecoration(border: InputBorder.none),
|
|
onChanged: (val) {
|
|
task.title = val;
|
|
_tasksBox.put(task.id, task);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildEditableDetail(Icons.sort, "Note", task.notes ?? "", (val) {
|
|
task.notes = val;
|
|
_tasksBox.put(task.id, task);
|
|
}),
|
|
const SizedBox(height: 30),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
task.isCompleted = !task.isCompleted;
|
|
_tasksBox.put(task.id, task);
|
|
Navigator.pop(context);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: task.isCompleted
|
|
? Colors.grey.shade300
|
|
: Colors.black,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
child: Text(
|
|
task.isCompleted ? "Mark as Incomplete" : "Complete Task",
|
|
style: TextStyle(
|
|
color: task.isCompleted ? Colors.black54 : Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
_tasksBox.delete(task.id);
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text(
|
|
"Delete",
|
|
style: TextStyle(color: Colors.redAccent),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEditableDetail(
|
|
IconData icon,
|
|
String label,
|
|
String value,
|
|
Function(String) onChange,
|
|
) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade50,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 20, color: Colors.grey.shade400),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
),
|
|
TextFormField(
|
|
initialValue: value,
|
|
maxLines: null,
|
|
decoration: const InputDecoration(
|
|
hintText: "Tap to add details...",
|
|
border: InputBorder.none,
|
|
isDense: true,
|
|
),
|
|
style: const TextStyle(fontSize: 15, height: 1.5),
|
|
onChanged: onChange,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _startFocusMode(BuildContext context, TaskItem task) {
|
|
Navigator.of(context).push(
|
|
PageRouteBuilder(
|
|
pageBuilder: (context, animation, secondaryAnimation) =>
|
|
FocusTimerPage(task: task),
|
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
return FadeTransition(opacity: animation, child: child);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|