fix: resolve widget API mismatches and build errors after merge

- EnergyBar: add energyLevel int? param, make fraction optional
- EnergyBadge: add energyLevel int? param, make level optional
- StreakDots: add streakDays int? param, make days/streakCount optional
- TaskCard: add task TaskEntity? and onComplete VoidCallback? params
- PetBubble: replace local PetType enum with domain PetType import
- TaskEntity: add isCompleted getter (status == TaskStatus.done)
- TasksNotifier: add uncompleteTask() method
- task_providers: add todayTasksProvider alias for tasksProvider
- PetStateNotifier: fix constructor (String ids, updatedAt), add onTaskCompleted()
- TasksScreen: watch userProvider + petMoodProvider, fix PetBubble/EmptyState calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
TrochtaOndrej
2026-03-18 11:11:35 +01:00
parent b75266f67c
commit 371e8260ac
9 changed files with 104 additions and 40 deletions
+2
View File
@@ -28,6 +28,8 @@ class TaskEntity {
required this.updatedAt,
});
bool get isCompleted => status == TaskStatus.done;
TaskEntity copyWith({
int? id,
int? userId,
@@ -40,18 +40,21 @@ final userProvider =
class PetStateNotifier extends Notifier<PetStateEntity> {
@override
PetStateEntity build() => const PetStateEntity(
id: 1,
userId: 1,
PetStateEntity build() => PetStateEntity(
id: '1',
userId: '1',
energyToday: 2,
streakDays: 3,
totalTasksDone: 12,
unlockedItems: 'masle,pelisek',
updatedAt: DateTime.now(),
);
void incrementTasksDone() {
state = state.copyWith(totalTasksDone: state.totalTasksDone + 1);
}
void onTaskCompleted() => incrementTasksDone();
}
final petStateProvider =
@@ -21,6 +21,19 @@ class TasksNotifier extends Notifier<List<TaskEntity>> {
];
}
void uncompleteTask(int id) {
state = [
for (final task in state)
if (task.id == id)
task.copyWith(
status: TaskStatus.pending,
updatedAt: DateTime.now(),
)
else
task,
];
}
void skipTask(int id) {
final skipped = state.firstWhere((t) => t.id == id);
state = [
@@ -34,6 +47,9 @@ class TasksNotifier extends Notifier<List<TaskEntity>> {
final tasksProvider =
NotifierProvider<TasksNotifier, List<TaskEntity>>(TasksNotifier.new);
/// Alias — today's tasks (same provider, filtered by status in screens).
final todayTasksProvider = tasksProvider;
/// Pending tasks only, ordered by sort order.
final pendingTasksProvider = Provider<List<TaskEntity>>((ref) {
final tasks = ref.watch(tasksProvider);
+7 -4
View File
@@ -23,6 +23,8 @@ class TasksScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final tasks = ref.watch(todayTasksProvider);
final petState = ref.watch(petStateProvider);
final userState = ref.watch(userProvider);
final petMood = ref.watch(petMoodProvider);
final userName = ref.watch(userNameProvider);
final pendingTasks = tasks.where((t) => !t.isCompleted).toList();
@@ -32,7 +34,7 @@ class TasksScreen extends ConsumerWidget {
return Scaffold(
body: SafeArea(
child: tasks.isEmpty
? _EmptyState(petType: petState.petType)
? _EmptyState(petType: userState.petType.name)
: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
child: Column(
@@ -67,9 +69,10 @@ class TasksScreen extends ConsumerWidget {
// 4. Pet area
PetBubble(
petType: petState.petType,
mood: petState.mood,
message: _petMessage(petState.mood, petState.petName),
petType: userState.petType,
petName: userState.petName,
mood: petMood.name,
message: _petMessage(petMood.name, userState.petName),
),
const SizedBox(height: 16),
+9 -3
View File
@@ -27,14 +27,20 @@ enum EnergyLevel {
/// - medium: orange bg, orange text, sun icon — "Stredni"
/// - high: red bg, red text, fire icon — "Narocne"
class EnergyBadge extends StatelessWidget {
const EnergyBadge({super.key, required this.level});
const EnergyBadge({super.key, this.level, this.energyLevel});
final EnergyLevel level;
final EnergyLevel? level;
/// Energy level as raw int (13); used when [level] is not provided.
final int? energyLevel;
/// Convenience constructor from raw int (1/2/3).
factory EnergyBadge.fromInt(int level) =>
EnergyBadge(level: EnergyLevel.fromInt(level));
EnergyLevel get _effectiveLevel =>
level ?? EnergyLevel.fromInt(energyLevel ?? 1);
@override
Widget build(BuildContext context) {
final (bgColor, textColor, icon, label) = _props;
@@ -61,7 +67,7 @@ class EnergyBadge extends StatelessWidget {
}
(Color bg, Color text, IconData icon, String label) get _props =>
switch (level) {
switch (_effectiveLevel) {
EnergyLevel.low => (
KittieColors.greenLight,
KittieColors.greenDark,
+14 -4
View File
@@ -8,19 +8,29 @@ import '../../core/theme/kittie_typography.dart';
///
/// Shows a track with creamDark background and amber fill
/// that animates to the current [fraction] (0.01.0).
/// Alternatively, pass [energyLevel] (13) to compute the fraction.
class EnergyBar extends StatelessWidget {
const EnergyBar({
super.key,
required this.fraction,
this.fraction,
this.energyLevel,
this.label = 'ENERGIE',
}) : assert(fraction >= 0.0 && fraction <= 1.0);
});
/// Fill amount from 0.0 (empty) to 1.0 (full).
final double fraction;
final double? fraction;
/// Energy level 13; used when [fraction] is not provided.
final int? energyLevel;
/// Label displayed above the bar.
final String label;
double get _effectiveFraction {
if (energyLevel != null) return (energyLevel! / 3.0).clamp(0.0, 1.0);
return (fraction ?? 0.0).clamp(0.0, 1.0);
}
@override
Widget build(BuildContext context) {
return Column(
@@ -52,7 +62,7 @@ class EnergyBar extends StatelessWidget {
AnimatedContainer(
duration: AppConstants.animationNormal,
curve: Curves.easeInOut,
width: constraints.maxWidth * fraction,
width: constraints.maxWidth * _effectiveFraction,
color: KittieColors.amber,
),
],
+1 -10
View File
@@ -3,16 +3,7 @@ import 'package:flutter/material.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/kittie_colors.dart';
import '../../core/theme/kittie_typography.dart';
/// Pet type determines the displayed emoji.
enum PetType {
dog('\u{1F436}'),
cat('\u{1F431}'),
capybara('\u{1F9AB}');
const PetType(this.emoji);
final String emoji;
}
import '../../domain/entities/pet_type.dart';
/// Pet area card with a speech bubble.
///
+28 -7
View File
@@ -17,18 +17,39 @@ enum DayStatus { done, today, future }
class StreakDots extends StatelessWidget {
const StreakDots({
super.key,
required this.days,
required this.streakCount,
}) : assert(days.length == 7, 'days must have exactly 7 entries');
this.days,
this.streakCount = 0,
this.streakDays,
});
/// Status for each day MonSun.
final List<DayStatus> days;
/// Status for each day MonSun. Computed from [streakDays] if omitted.
final List<DayStatus>? days;
/// Current consecutive-day streak count.
final int streakCount;
/// Streak as an integer; used to compute [days] and count when provided.
final int? streakDays;
static const _dayLabels = ['Po', 'Ut', 'St', 'Ct', 'Pa', 'So', 'Ne'];
static List<DayStatus> _generateDays(int streak) {
final todayIndex = DateTime.now().weekday - 1; // 0=Mon, 6=Sun
return List.generate(7, (i) {
if (i == todayIndex) return DayStatus.today;
if (i < todayIndex) {
final daysAgo = todayIndex - i;
return daysAgo <= streak ? DayStatus.done : DayStatus.future;
}
return DayStatus.future;
});
}
List<DayStatus> get _effectiveDays =>
streakDays != null ? _generateDays(streakDays!) : (days ?? List.filled(7, DayStatus.future));
int get _effectiveCount => streakDays ?? streakCount;
@override
Widget build(BuildContext context) {
return Column(
@@ -41,7 +62,7 @@ class StreakDots extends StatelessWidget {
const Text('\u{1F525}', style: TextStyle(fontSize: 16)),
const SizedBox(width: 4),
Text(
'$streakCount',
'$_effectiveCount',
style: KittieTypography.labelLarge.copyWith(
color: KittieColors.brown,
),
@@ -55,7 +76,7 @@ class StreakDots extends StatelessWidget {
children: List.generate(7, (i) {
return Padding(
padding: EdgeInsets.only(left: i > 0 ? 8 : 0),
child: _DayDot(label: _dayLabels[i], status: days[i]),
child: _DayDot(label: _dayLabels[i], status: _effectiveDays[i]),
);
}),
),
+21 -9
View File
@@ -4,6 +4,7 @@ import 'package:flutter/services.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 'energy_badge.dart';
/// A task list item with circular checkbox, title, energy badge, and time.
@@ -18,25 +19,36 @@ import 'energy_badge.dart';
class TaskCard extends StatelessWidget {
const TaskCard({
super.key,
required this.title,
required this.energyLevel,
this.task,
this.title,
this.energyLevel,
this.isDone = false,
this.isSelfCare = false,
this.isOverdue = false,
this.timeText,
this.onComplete,
this.onToggle,
this.onTap,
});
final String title;
final int energyLevel;
/// Optional task entity; its fields take precedence over individual params.
final TaskEntity? task;
final String? title;
final int? energyLevel;
final bool isDone;
final bool isSelfCare;
final bool isOverdue;
final String? timeText;
final VoidCallback? onComplete;
final VoidCallback? onToggle;
final VoidCallback? onTap;
String get _title => task?.title ?? title ?? '';
int get _energyLevel => task?.energyLevel ?? energyLevel ?? 2;
bool get _isDone => task?.isCompleted ?? isDone;
bool get _isSelfCare => task?.isSelfCare ?? isSelfCare;
VoidCallback? get _toggleCallback => onComplete ?? onToggle;
@override
Widget build(BuildContext context) {
return GestureDetector(
@@ -52,13 +64,13 @@ class TaskCard extends StatelessWidget {
),
child: Row(
children: [
_Checkbox(isDone: isDone, onToggle: onToggle),
_Checkbox(isDone: _isDone, onToggle: _toggleCallback),
const SizedBox(width: 12),
Expanded(child: _Content(
title: title,
energyLevel: energyLevel,
isDone: isDone,
isSelfCare: isSelfCare,
title: _title,
energyLevel: _energyLevel,
isDone: _isDone,
isSelfCare: _isSelfCare,
timeText: timeText,
)),
],