532 lines
17 KiB
Dart
532 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import '../../../core/constants/app_constants.dart';
|
|
import '../../../core/theme/kittie_colors.dart';
|
|
import '../../../core/theme/kittie_typography.dart';
|
|
import '../../../domain/entities/task_entity.dart';
|
|
import '../providers/add_task_providers.dart';
|
|
import '../providers/task_providers.dart';
|
|
|
|
/// Shows the Add Task bottom sheet. Call from any screen:
|
|
/// ```dart
|
|
/// showAddTaskSheet(context);
|
|
/// ```
|
|
void showAddTaskSheet(BuildContext context) {
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (_) => const _AddTaskSheet(),
|
|
);
|
|
}
|
|
|
|
class _AddTaskSheet extends ConsumerStatefulWidget {
|
|
const _AddTaskSheet();
|
|
|
|
@override
|
|
ConsumerState<_AddTaskSheet> createState() => _AddTaskSheetState();
|
|
}
|
|
|
|
class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
|
|
late final TextEditingController _titleController;
|
|
late final TextEditingController _noteController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_titleController = TextEditingController();
|
|
_noteController = TextEditingController();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleController.dispose();
|
|
_noteController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _saveTask() {
|
|
final title = ref.read(taskTitleProvider);
|
|
final note = ref.read(taskNoteProvider);
|
|
final energy = ref.read(selectedEnergyProvider) ?? 2;
|
|
final taskDay = ref.read(selectedTaskDayProvider);
|
|
final taskTime = ref.read(selectedTaskTimeProvider);
|
|
final deadline = _resolveDeadline(taskDay, taskTime);
|
|
final now = DateTime.now();
|
|
final tasks = ref.read(tasksProvider).value ?? [];
|
|
|
|
final entity = TaskEntity(
|
|
id: const Uuid().v4(),
|
|
userId: 'user-1',
|
|
title: title,
|
|
description: note.isEmpty ? null : note,
|
|
energyLevel: energy,
|
|
sortOrder: tasks.length,
|
|
deadline: deadline,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
|
|
ref.read(taskRepositoryProvider).insertTask(entity);
|
|
ref.read(addTaskStepProvider.notifier).next();
|
|
}
|
|
|
|
DateTime? _resolveDeadline(TaskDay? day, TimeOfDay? time) {
|
|
final now = DateTime.now();
|
|
final int hour = time?.hour ?? 9;
|
|
final int minute = time?.minute ?? 0;
|
|
return switch (day) {
|
|
TaskDay.today => DateTime(now.year, now.month, now.day, hour, minute),
|
|
TaskDay.tomorrow => DateTime(
|
|
now.year, now.month, now.day + 1, hour, minute),
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
void _resetAndClose() {
|
|
ref.invalidate(addTaskStepProvider);
|
|
ref.invalidate(taskTitleProvider);
|
|
ref.invalidate(taskNoteProvider);
|
|
ref.invalidate(selectedTaskDayProvider);
|
|
ref.invalidate(selectedTaskTimeProvider);
|
|
ref.invalidate(selectedEnergyProvider);
|
|
_titleController.clear();
|
|
_noteController.clear();
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
void _addAnother() {
|
|
ref.invalidate(addTaskStepProvider);
|
|
ref.invalidate(taskTitleProvider);
|
|
ref.invalidate(taskNoteProvider);
|
|
ref.invalidate(selectedTaskDayProvider);
|
|
ref.invalidate(selectedTaskTimeProvider);
|
|
ref.invalidate(selectedEnergyProvider);
|
|
_titleController.clear();
|
|
_noteController.clear();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final step = ref.watch(addTaskStepProvider);
|
|
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
|
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
color: KittieColors.cream,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(AppConstants.bottomSheetBorderRadius),
|
|
),
|
|
),
|
|
padding: EdgeInsets.only(
|
|
left: 24,
|
|
right: 24,
|
|
top: 16,
|
|
bottom: 24 + bottomInset,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Handle
|
|
Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: KittieColors.creamBorder,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Step dots
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: List.generate(4, (i) {
|
|
final isActive = i == step;
|
|
final isPast = i < step;
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
margin: const EdgeInsets.symmetric(horizontal: 3),
|
|
width: isActive ? 20 : 6,
|
|
height: 6,
|
|
decoration: BoxDecoration(
|
|
color: isActive
|
|
? KittieColors.brown
|
|
: isPast
|
|
? KittieColors.sageGreen
|
|
: KittieColors.creamBorder,
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Step content
|
|
AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 250),
|
|
child: switch (step) {
|
|
0 => _Step1Title(
|
|
key: const ValueKey(0),
|
|
titleController: _titleController,
|
|
noteController: _noteController,
|
|
onNext: () {
|
|
if (_titleController.text.trim().isNotEmpty) {
|
|
ref
|
|
.read(taskTitleProvider.notifier)
|
|
.update(_titleController.text.trim());
|
|
ref
|
|
.read(taskNoteProvider.notifier)
|
|
.update(_noteController.text.trim());
|
|
ref.read(addTaskStepProvider.notifier).next();
|
|
}
|
|
},
|
|
),
|
|
1 => _Step2When(
|
|
key: const ValueKey(1),
|
|
onNext: () =>
|
|
ref.read(addTaskStepProvider.notifier).next(),
|
|
onBack: () =>
|
|
ref.read(addTaskStepProvider.notifier).previous(),
|
|
),
|
|
2 => _Step3Energy(
|
|
key: const ValueKey(2),
|
|
onSave: _saveTask,
|
|
onBack: () =>
|
|
ref.read(addTaskStepProvider.notifier).previous(),
|
|
),
|
|
3 => _Step4Confirmation(
|
|
key: const ValueKey(3),
|
|
onClose: _resetAndClose,
|
|
onAddAnother: _addAnother,
|
|
),
|
|
_ => const SizedBox.shrink(),
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── Step 1: Title & Note ───────────────────────────────────────────────────
|
|
|
|
class _Step1Title extends StatelessWidget {
|
|
const _Step1Title({
|
|
super.key,
|
|
required this.titleController,
|
|
required this.noteController,
|
|
required this.onNext,
|
|
});
|
|
|
|
final TextEditingController titleController;
|
|
final TextEditingController noteController;
|
|
final VoidCallback onNext;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Nový úkol', style: KittieTypography.h2),
|
|
const SizedBox(height: 20),
|
|
Text('NÁZEV', style: KittieTypography.labelMedium),
|
|
const SizedBox(height: 8),
|
|
TextField(
|
|
controller: titleController,
|
|
autofocus: true,
|
|
style: KittieTypography.bodyLarge
|
|
.copyWith(color: KittieColors.brownDark),
|
|
decoration: const InputDecoration(hintText: 'Co potřebuješ udělat?'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text('POZNÁMKA', style: KittieTypography.labelMedium),
|
|
const SizedBox(height: 8),
|
|
TextField(
|
|
controller: noteController,
|
|
style: KittieTypography.bodyMedium
|
|
.copyWith(color: KittieColors.brownDark),
|
|
maxLines: 3,
|
|
decoration: const InputDecoration(hintText: 'Volitelné...'),
|
|
),
|
|
const SizedBox(height: 24),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: onNext,
|
|
child: const Text('Pokračovat'),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── Step 2: When ───────────────────────────────────────────────────────────
|
|
|
|
class _Step2When extends ConsumerWidget {
|
|
const _Step2When({
|
|
super.key,
|
|
required this.onNext,
|
|
required this.onBack,
|
|
});
|
|
|
|
final VoidCallback onNext;
|
|
final VoidCallback onBack;
|
|
|
|
static const _days = [
|
|
(day: TaskDay.today, label: 'Dnes'),
|
|
(day: TaskDay.tomorrow, label: 'Zítra'),
|
|
(day: TaskDay.other, label: 'Jindy'),
|
|
(day: TaskDay.someday, label: 'Někdy'),
|
|
];
|
|
|
|
static const _timePresets = [8, 10, 14, 18];
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final selectedDay = ref.watch(selectedTaskDayProvider);
|
|
final selectedTime = ref.watch(selectedTaskTimeProvider);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Kdy to chceš udělat?', style: KittieTypography.h2),
|
|
const SizedBox(height: 20),
|
|
|
|
// Day chips
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: _days.map((d) {
|
|
final isSelected = selectedDay == d.day;
|
|
return ChoiceChip(
|
|
label: Text(d.label),
|
|
selected: isSelected,
|
|
onSelected: (_) {
|
|
ref.read(selectedTaskDayProvider.notifier).select(d.day);
|
|
},
|
|
selectedColor: KittieColors.amber,
|
|
backgroundColor: KittieColors.creamDark,
|
|
labelStyle: KittieTypography.labelLarge.copyWith(
|
|
color:
|
|
isSelected ? KittieColors.brownDark : KittieColors.brown,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
side: BorderSide.none,
|
|
showCheckmark: false,
|
|
);
|
|
}).toList(),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
// Time presets
|
|
Text('Čas', style: KittieTypography.labelMedium),
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: _timePresets.map((hour) {
|
|
final time = TimeOfDay(hour: hour, minute: 0);
|
|
final isSelected = selectedTime?.hour == hour;
|
|
return ChoiceChip(
|
|
label: Text('$hour:00'),
|
|
selected: isSelected,
|
|
onSelected: (_) {
|
|
ref.read(selectedTaskTimeProvider.notifier).select(time);
|
|
},
|
|
selectedColor: KittieColors.amber,
|
|
backgroundColor: KittieColors.creamDark,
|
|
labelStyle: KittieTypography.labelLarge.copyWith(
|
|
color:
|
|
isSelected ? KittieColors.brownDark : KittieColors.brown,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
side: BorderSide.none,
|
|
showCheckmark: false,
|
|
);
|
|
}).toList(),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: onBack,
|
|
child: const Text('Zpět'),
|
|
),
|
|
const Spacer(),
|
|
ElevatedButton(
|
|
onPressed: onNext,
|
|
child: const Text('Pokračovat'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── Step 3: Energy ─────────────────────────────────────────────────────────
|
|
|
|
class _Step3Energy extends ConsumerWidget {
|
|
const _Step3Energy({
|
|
super.key,
|
|
required this.onSave,
|
|
required this.onBack,
|
|
});
|
|
|
|
final VoidCallback onSave;
|
|
final VoidCallback onBack;
|
|
|
|
static const _energyOptions = [
|
|
(level: 1, label: 'Lehké', emoji: '🍃', color: KittieColors.sageGreen),
|
|
(level: 2, label: 'Střední', emoji: '⚡', color: KittieColors.orange),
|
|
(level: 3, label: 'Náročné', emoji: '🔥', color: KittieColors.red),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final selected = ref.watch(selectedEnergyProvider);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Pet bubble
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: KittieColors.creamDark,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Text('🐱', style: TextStyle(fontSize: 28)),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Kolik energie to bude stát?',
|
|
style: KittieTypography.bodyLarge,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Energy picker
|
|
Row(
|
|
children: _energyOptions.map((opt) {
|
|
final isSelected = selected == opt.level;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
ref.read(selectedEnergyProvider.notifier).select(opt.level);
|
|
},
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? opt.color.withValues(alpha: 0.15)
|
|
: KittieColors.creamDark,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: isSelected ? opt.color : Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(opt.emoji, style: const TextStyle(fontSize: 28)),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
opt.label,
|
|
style: KittieTypography.labelLarge.copyWith(
|
|
color: isSelected ? opt.color : KittieColors.brown,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: onBack,
|
|
child: const Text('Zpět'),
|
|
),
|
|
const Spacer(),
|
|
ElevatedButton(
|
|
onPressed: selected != null ? onSave : null,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: KittieColors.sageGreen,
|
|
foregroundColor: KittieColors.cream,
|
|
disabledBackgroundColor: KittieColors.creamDark,
|
|
),
|
|
child: const Text('Uložit úkol'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── Step 4: Confirmation ───────────────────────────────────────────────────
|
|
|
|
class _Step4Confirmation extends StatelessWidget {
|
|
const _Step4Confirmation({
|
|
super.key,
|
|
required this.onClose,
|
|
required this.onAddAnother,
|
|
});
|
|
|
|
final VoidCallback onClose;
|
|
final VoidCallback onAddAnother;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
const Icon(
|
|
Icons.check_circle_rounded,
|
|
color: KittieColors.sageGreen,
|
|
size: 64,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text('Úkol uložen!', style: KittieTypography.h2),
|
|
const SizedBox(height: 32),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: onClose,
|
|
child: const Text('Zpět na seznam'),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: TextButton(
|
|
onPressed: onAddAnother,
|
|
child: const Text('Přidat další úkol'),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|