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>
121 lines
3.5 KiB
Dart
121 lines
3.5 KiB
Dart
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:focusflow_shared/focusflow_shared.dart';
|
|
|
|
import '../../../../core/network/api_client.dart';
|
|
import '../../../../main.dart';
|
|
|
|
// ── Events ─────────────────────────────────────────────────────────
|
|
|
|
sealed class StreakEvent extends Equatable {
|
|
const StreakEvent();
|
|
@override
|
|
List<Object?> get props => [];
|
|
}
|
|
|
|
class StreaksLoaded extends StreakEvent {
|
|
const StreaksLoaded();
|
|
}
|
|
|
|
class StreakCompleted extends StreakEvent {
|
|
final String id;
|
|
const StreakCompleted(this.id);
|
|
@override
|
|
List<Object?> get props => [id];
|
|
}
|
|
|
|
class StreakForgiven extends StreakEvent {
|
|
final String id;
|
|
const StreakForgiven(this.id);
|
|
@override
|
|
List<Object?> get props => [id];
|
|
}
|
|
|
|
class StreakFrozen extends StreakEvent {
|
|
final String id;
|
|
final DateTime until;
|
|
const StreakFrozen(this.id, this.until);
|
|
@override
|
|
List<Object?> get props => [id, until];
|
|
}
|
|
|
|
// ── States ─────────────────────────────────────────────────────────
|
|
|
|
sealed class StreakState extends Equatable {
|
|
const StreakState();
|
|
@override
|
|
List<Object?> get props => [];
|
|
}
|
|
|
|
class StreakInitial extends StreakState {}
|
|
|
|
class StreakLoading extends StreakState {}
|
|
|
|
class StreakLoaded extends StreakState {
|
|
final List<Streak> streaks;
|
|
const StreakLoaded(this.streaks);
|
|
@override
|
|
List<Object?> get props => [streaks];
|
|
}
|
|
|
|
class StreakError extends StreakState {
|
|
final String message;
|
|
const StreakError(this.message);
|
|
@override
|
|
List<Object?> get props => [message];
|
|
}
|
|
|
|
// ── BLoC ───────────────────────────────────────────────────────────
|
|
|
|
class StreakBloc extends Bloc<StreakEvent, StreakState> {
|
|
final ApiClient _api = getIt<ApiClient>();
|
|
|
|
StreakBloc() : super(StreakInitial()) {
|
|
on<StreaksLoaded>(_onLoaded);
|
|
on<StreakCompleted>(_onCompleted);
|
|
on<StreakForgiven>(_onForgiven);
|
|
on<StreakFrozen>(_onFrozen);
|
|
}
|
|
|
|
Future<void> _onLoaded(StreaksLoaded event, Emitter<StreakState> emit) async {
|
|
emit(StreakLoading());
|
|
try {
|
|
final response = await _api.fetchStreaks();
|
|
final data = response.data;
|
|
if (data != null && data['status'] == 'success') {
|
|
final items = (data['data'] as List<dynamic>)
|
|
.map((e) => Streak.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
emit(StreakLoaded(items));
|
|
} else {
|
|
emit(const StreakError('Could not load streaks.'));
|
|
}
|
|
} catch (_) {
|
|
emit(const StreakError('Something went wrong loading streaks.'));
|
|
}
|
|
}
|
|
|
|
Future<void> _onCompleted(StreakCompleted event, Emitter<StreakState> emit) async {
|
|
try {
|
|
await _api.completeStreak(event.id);
|
|
add(const StreaksLoaded());
|
|
} catch (_) {
|
|
emit(const StreakError('Could not complete streak entry.'));
|
|
}
|
|
}
|
|
|
|
Future<void> _onForgiven(StreakForgiven event, Emitter<StreakState> emit) async {
|
|
try {
|
|
await _api.forgiveStreak(event.id);
|
|
add(const StreaksLoaded());
|
|
} catch (_) {
|
|
emit(const StreakError('Could not forgive streak.'));
|
|
}
|
|
}
|
|
|
|
Future<void> _onFrozen(StreakFrozen event, Emitter<StreakState> emit) async {
|
|
// TODO: API call to freeze streak until `event.until`.
|
|
add(const StreaksLoaded());
|
|
}
|
|
}
|