Merge NDM-App-196: Onboarding flow + Add Task bottom sheet
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'onboarding_providers.g.dart';
|
||||
|
||||
/// Pet types available during onboarding.
|
||||
enum PetType { dog, cat, capybara }
|
||||
|
||||
/// Current onboarding step (0-based).
|
||||
@riverpod
|
||||
class OnboardingStep extends _$OnboardingStep {
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void next() {
|
||||
if (state < 2) state = state + 1;
|
||||
}
|
||||
|
||||
void previous() {
|
||||
if (state > 0) state = state - 1;
|
||||
}
|
||||
|
||||
void goTo(int step) {
|
||||
if (step >= 0 && step <= 2) state = step;
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected pet type — defaults to cat.
|
||||
@riverpod
|
||||
class SelectedPetType extends _$SelectedPetType {
|
||||
@override
|
||||
PetType build() => PetType.cat;
|
||||
|
||||
void select(PetType type) => state = type;
|
||||
}
|
||||
|
||||
/// Pet name — defaults to "Míša".
|
||||
@riverpod
|
||||
class PetName extends _$PetName {
|
||||
@override
|
||||
String build() => 'Míša';
|
||||
|
||||
void update(String name) => state = name;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'onboarding_providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Current onboarding step (0-based).
|
||||
|
||||
@ProviderFor(OnboardingStep)
|
||||
final onboardingStepProvider = OnboardingStepProvider._();
|
||||
|
||||
/// Current onboarding step (0-based).
|
||||
final class OnboardingStepProvider
|
||||
extends $NotifierProvider<OnboardingStep, int> {
|
||||
/// Current onboarding step (0-based).
|
||||
OnboardingStepProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'onboardingStepProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$onboardingStepHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
OnboardingStep create() => OnboardingStep();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$onboardingStepHash() => r'b8640b7a39647f58b2b7dc428f46a994ad14ce9a';
|
||||
|
||||
/// Current onboarding step (0-based).
|
||||
|
||||
abstract class _$OnboardingStep extends $Notifier<int> {
|
||||
int build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<int, int>;
|
||||
final element = ref.element
|
||||
as $ClassProviderElement<AnyNotifier<int, int>, int, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected pet type — defaults to cat.
|
||||
|
||||
@ProviderFor(SelectedPetType)
|
||||
final selectedPetTypeProvider = SelectedPetTypeProvider._();
|
||||
|
||||
/// Selected pet type — defaults to cat.
|
||||
final class SelectedPetTypeProvider
|
||||
extends $NotifierProvider<SelectedPetType, PetType> {
|
||||
/// Selected pet type — defaults to cat.
|
||||
SelectedPetTypeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedPetTypeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedPetTypeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedPetType create() => SelectedPetType();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PetType value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PetType>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedPetTypeHash() => r'f22a57222416e1b1dba8583b76055267af78cfdb';
|
||||
|
||||
/// Selected pet type — defaults to cat.
|
||||
|
||||
abstract class _$SelectedPetType extends $Notifier<PetType> {
|
||||
PetType build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<PetType, PetType>;
|
||||
final element = ref.element as $ClassProviderElement<
|
||||
AnyNotifier<PetType, PetType>, PetType, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pet name — defaults to "Míša".
|
||||
|
||||
@ProviderFor(PetName)
|
||||
final petNameProvider = PetNameProvider._();
|
||||
|
||||
/// Pet name — defaults to "Míša".
|
||||
final class PetNameProvider extends $NotifierProvider<PetName, String> {
|
||||
/// Pet name — defaults to "Míša".
|
||||
PetNameProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'petNameProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$petNameHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PetName create() => PetName();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$petNameHash() => r'e4f8b65d291597e897f1e0f44a8c086a4e41b4ae';
|
||||
|
||||
/// Pet name — defaults to "Míša".
|
||||
|
||||
abstract class _$PetName extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element = ref.element as $ClassProviderElement<
|
||||
AnyNotifier<String, String>, String, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../core/theme/kittie_colors.dart';
|
||||
import '../../../core/theme/kittie_typography.dart';
|
||||
import '../providers/onboarding_providers.dart';
|
||||
|
||||
class OnboardingScreen extends ConsumerStatefulWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<OnboardingScreen> createState() => _OnboardingScreenState();
|
||||
}
|
||||
|
||||
class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
late final PageController _pageController;
|
||||
late final TextEditingController _nameController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController();
|
||||
_nameController = TextEditingController(text: 'Míša');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _goToPage(int page) {
|
||||
_pageController.animateToPage(
|
||||
page,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
ref.read(onboardingStepProvider.notifier).goTo(page);
|
||||
}
|
||||
|
||||
Future<void> _completeOnboarding() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('onboarding_complete', true);
|
||||
await prefs.setString(
|
||||
'pet_type',
|
||||
ref.read(selectedPetTypeProvider).name,
|
||||
);
|
||||
await prefs.setString('pet_name', ref.read(petNameProvider));
|
||||
|
||||
if (mounted) {
|
||||
context.go('/tasks');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final step = ref.watch(onboardingStepProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: KittieColors.brownDark,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Progress dots
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(3, (i) {
|
||||
final isActive = i == step;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: isActive ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive
|
||||
? KittieColors.amber
|
||||
: KittieColors.brownLight,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
// Pages
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onPageChanged: (page) {
|
||||
ref.read(onboardingStepProvider.notifier).goTo(page);
|
||||
},
|
||||
children: [
|
||||
_WelcomeStep(onNext: () => _goToPage(1)),
|
||||
_PetSelectionStep(
|
||||
nameController: _nameController,
|
||||
onNext: () => _goToPage(2),
|
||||
onBack: () => _goToPage(0),
|
||||
),
|
||||
_ReadyStep(
|
||||
onComplete: _completeOnboarding,
|
||||
onBack: () => _goToPage(1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Step 1: Welcome ────────────────────────────────────────────────────────
|
||||
|
||||
class _WelcomeStep extends StatelessWidget {
|
||||
const _WelcomeStep({required this.onNext});
|
||||
final VoidCallback onNext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('🐱', style: TextStyle(fontSize: 80)),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Vítej v KittieMobile',
|
||||
style: KittieTypography.h1.copyWith(color: KittieColors.cream),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Tvůj mazlíček ti pomůže zvládnout úkoly.',
|
||||
style:
|
||||
KittieTypography.bodyLarge.copyWith(color: KittieColors.amber),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: onNext,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.amber,
|
||||
foregroundColor: KittieColors.brownDark,
|
||||
),
|
||||
child: const Text('Začít'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Step 2: Pet Selection ──────────────────────────────────────────────────
|
||||
|
||||
class _PetSelectionStep extends ConsumerWidget {
|
||||
const _PetSelectionStep({
|
||||
required this.nameController,
|
||||
required this.onNext,
|
||||
required this.onBack,
|
||||
});
|
||||
|
||||
final TextEditingController nameController;
|
||||
final VoidCallback onNext;
|
||||
final VoidCallback onBack;
|
||||
|
||||
static const _pets = [
|
||||
(type: PetType.dog, emoji: '🐶', label: 'Pes'),
|
||||
(type: PetType.cat, emoji: '🐱', label: 'Kočka'),
|
||||
(type: PetType.capybara, emoji: '🦫', label: 'Kapybara'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selected = ref.watch(selectedPetTypeProvider);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Vyber si mazlíčka',
|
||||
style: KittieTypography.h2.copyWith(color: KittieColors.cream),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Pet grid
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _pets.map((pet) {
|
||||
final isSelected = selected == pet.type;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
ref.read(selectedPetTypeProvider.notifier).select(pet.type);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: 80,
|
||||
height: 80,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: KittieColors.brownMid,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected ? KittieColors.amber : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(pet.emoji, style: const TextStyle(fontSize: 32)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
pet.label,
|
||||
style: KittieTypography.labelSmall
|
||||
.copyWith(color: KittieColors.cream),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Name input
|
||||
Text(
|
||||
'Jak se bude jmenovat?',
|
||||
style:
|
||||
KittieTypography.bodyLarge.copyWith(color: KittieColors.cream),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: nameController,
|
||||
style: KittieTypography.bodyLarge
|
||||
.copyWith(color: KittieColors.brownDark),
|
||||
textAlign: TextAlign.center,
|
||||
decoration: InputDecoration(
|
||||
fillColor: KittieColors.creamDark,
|
||||
hintText: 'Míša',
|
||||
hintStyle: KittieTypography.bodyLarge
|
||||
.copyWith(color: KittieColors.brownLight),
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(petNameProvider.notifier).update(value);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Navigation buttons
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: onBack,
|
||||
child: Text(
|
||||
'Zpět',
|
||||
style: KittieTypography.labelLarge
|
||||
.copyWith(color: KittieColors.brownLight),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: onNext,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.amber,
|
||||
foregroundColor: KittieColors.brownDark,
|
||||
),
|
||||
child: const Text('Pokračovat'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Step 3: Ready ──────────────────────────────────────────────────────────
|
||||
|
||||
class _ReadyStep extends ConsumerWidget {
|
||||
const _ReadyStep({required this.onComplete, required this.onBack});
|
||||
final VoidCallback onComplete;
|
||||
final VoidCallback onBack;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final petName = ref.watch(petNameProvider);
|
||||
final petType = ref.watch(selectedPetTypeProvider);
|
||||
|
||||
final emoji = switch (petType) {
|
||||
PetType.dog => '🐶',
|
||||
PetType.cat => '🐱',
|
||||
PetType.capybara => '🦫',
|
||||
};
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle_rounded,
|
||||
color: KittieColors.sageGreen,
|
||||
size: 80,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Vše je připraveno!',
|
||||
style: KittieTypography.h1.copyWith(color: KittieColors.cream),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'$emoji $petName je připraven/a ti pomáhat.',
|
||||
style:
|
||||
KittieTypography.bodyLarge.copyWith(color: KittieColors.amber),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: onBack,
|
||||
child: Text(
|
||||
'Zpět',
|
||||
style: KittieTypography.labelLarge
|
||||
.copyWith(color: KittieColors.brownLight),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: onComplete,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: KittieColors.sageGreen,
|
||||
foregroundColor: KittieColors.cream,
|
||||
),
|
||||
child: const Text('Začít používat'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'add_task_providers.g.dart';
|
||||
|
||||
/// Current add-task step (0-based, 4 steps total).
|
||||
@riverpod
|
||||
class AddTaskStep extends _$AddTaskStep {
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void next() {
|
||||
if (state < 3) state = state + 1;
|
||||
}
|
||||
|
||||
void previous() {
|
||||
if (state > 0) state = state - 1;
|
||||
}
|
||||
|
||||
void reset() => state = 0;
|
||||
}
|
||||
|
||||
/// Task title.
|
||||
@riverpod
|
||||
class TaskTitle extends _$TaskTitle {
|
||||
@override
|
||||
String build() => '';
|
||||
|
||||
void update(String value) => state = value;
|
||||
}
|
||||
|
||||
/// Task note (optional).
|
||||
@riverpod
|
||||
class TaskNote extends _$TaskNote {
|
||||
@override
|
||||
String build() => '';
|
||||
|
||||
void update(String value) => state = value;
|
||||
}
|
||||
|
||||
/// Selected day for the task.
|
||||
enum TaskDay { today, tomorrow, other, someday }
|
||||
|
||||
@riverpod
|
||||
class SelectedTaskDay extends _$SelectedTaskDay {
|
||||
@override
|
||||
TaskDay? build() => null;
|
||||
|
||||
void select(TaskDay day) => state = day;
|
||||
}
|
||||
|
||||
/// Selected time preset.
|
||||
@riverpod
|
||||
class SelectedTaskTime extends _$SelectedTaskTime {
|
||||
@override
|
||||
TimeOfDay? build() => null;
|
||||
|
||||
void select(TimeOfDay time) => state = time;
|
||||
}
|
||||
|
||||
/// Selected energy level (1-3).
|
||||
@riverpod
|
||||
class SelectedEnergy extends _$SelectedEnergy {
|
||||
@override
|
||||
int? build() => null;
|
||||
|
||||
void select(int level) => state = level;
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'add_task_providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Current add-task step (0-based, 4 steps total).
|
||||
|
||||
@ProviderFor(AddTaskStep)
|
||||
final addTaskStepProvider = AddTaskStepProvider._();
|
||||
|
||||
/// Current add-task step (0-based, 4 steps total).
|
||||
final class AddTaskStepProvider extends $NotifierProvider<AddTaskStep, int> {
|
||||
/// Current add-task step (0-based, 4 steps total).
|
||||
AddTaskStepProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'addTaskStepProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$addTaskStepHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
AddTaskStep create() => AddTaskStep();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$addTaskStepHash() => r'631dbac166b380b4703a8397bfb53929c3219414';
|
||||
|
||||
/// Current add-task step (0-based, 4 steps total).
|
||||
|
||||
abstract class _$AddTaskStep extends $Notifier<int> {
|
||||
int build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<int, int>;
|
||||
final element = ref.element
|
||||
as $ClassProviderElement<AnyNotifier<int, int>, int, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// Task title.
|
||||
|
||||
@ProviderFor(TaskTitle)
|
||||
final taskTitleProvider = TaskTitleProvider._();
|
||||
|
||||
/// Task title.
|
||||
final class TaskTitleProvider extends $NotifierProvider<TaskTitle, String> {
|
||||
/// Task title.
|
||||
TaskTitleProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'taskTitleProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$taskTitleHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
TaskTitle create() => TaskTitle();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$taskTitleHash() => r'b13b965832d8a85b395589a181b35b0e25d5fe28';
|
||||
|
||||
/// Task title.
|
||||
|
||||
abstract class _$TaskTitle extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element = ref.element as $ClassProviderElement<
|
||||
AnyNotifier<String, String>, String, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// Task note (optional).
|
||||
|
||||
@ProviderFor(TaskNote)
|
||||
final taskNoteProvider = TaskNoteProvider._();
|
||||
|
||||
/// Task note (optional).
|
||||
final class TaskNoteProvider extends $NotifierProvider<TaskNote, String> {
|
||||
/// Task note (optional).
|
||||
TaskNoteProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'taskNoteProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$taskNoteHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
TaskNote create() => TaskNote();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$taskNoteHash() => r'f2bc74473d53d259289317273b0ebebc501feab1';
|
||||
|
||||
/// Task note (optional).
|
||||
|
||||
abstract class _$TaskNote extends $Notifier<String> {
|
||||
String build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<String, String>;
|
||||
final element = ref.element as $ClassProviderElement<
|
||||
AnyNotifier<String, String>, String, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(SelectedTaskDay)
|
||||
final selectedTaskDayProvider = SelectedTaskDayProvider._();
|
||||
|
||||
final class SelectedTaskDayProvider
|
||||
extends $NotifierProvider<SelectedTaskDay, TaskDay?> {
|
||||
SelectedTaskDayProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedTaskDayProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedTaskDayHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedTaskDay create() => SelectedTaskDay();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(TaskDay? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<TaskDay?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedTaskDayHash() => r'dae3cf690e0b896deaf52a1dc59780962134fe6e';
|
||||
|
||||
abstract class _$SelectedTaskDay extends $Notifier<TaskDay?> {
|
||||
TaskDay? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<TaskDay?, TaskDay?>;
|
||||
final element = ref.element as $ClassProviderElement<
|
||||
AnyNotifier<TaskDay?, TaskDay?>, TaskDay?, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected time preset.
|
||||
|
||||
@ProviderFor(SelectedTaskTime)
|
||||
final selectedTaskTimeProvider = SelectedTaskTimeProvider._();
|
||||
|
||||
/// Selected time preset.
|
||||
final class SelectedTaskTimeProvider
|
||||
extends $NotifierProvider<SelectedTaskTime, TimeOfDay?> {
|
||||
/// Selected time preset.
|
||||
SelectedTaskTimeProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedTaskTimeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedTaskTimeHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedTaskTime create() => SelectedTaskTime();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(TimeOfDay? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<TimeOfDay?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedTaskTimeHash() => r'287e12b497319c2c9f0d394a73816953de87950d';
|
||||
|
||||
/// Selected time preset.
|
||||
|
||||
abstract class _$SelectedTaskTime extends $Notifier<TimeOfDay?> {
|
||||
TimeOfDay? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<TimeOfDay?, TimeOfDay?>;
|
||||
final element = ref.element as $ClassProviderElement<
|
||||
AnyNotifier<TimeOfDay?, TimeOfDay?>, TimeOfDay?, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected energy level (1-3).
|
||||
|
||||
@ProviderFor(SelectedEnergy)
|
||||
final selectedEnergyProvider = SelectedEnergyProvider._();
|
||||
|
||||
/// Selected energy level (1-3).
|
||||
final class SelectedEnergyProvider
|
||||
extends $NotifierProvider<SelectedEnergy, int?> {
|
||||
/// Selected energy level (1-3).
|
||||
SelectedEnergyProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedEnergyProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedEnergyHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedEnergy create() => SelectedEnergy();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedEnergyHash() => r'ef26bcd779e3f2073c3fea5c8abc4a32d17055cf';
|
||||
|
||||
/// Selected energy level (1-3).
|
||||
|
||||
abstract class _$SelectedEnergy extends $Notifier<int?> {
|
||||
int? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<int?, int?>;
|
||||
final element = ref.element as $ClassProviderElement<
|
||||
AnyNotifier<int?, int?>, int?, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
import '../../../core/theme/kittie_colors.dart';
|
||||
import '../../../core/theme/kittie_typography.dart';
|
||||
import '../providers/add_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() {
|
||||
// TODO: persist task via repository
|
||||
ref.read(addTaskStepProvider.notifier).next();
|
||||
}
|
||||
|
||||
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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user