forked from eatvio/eatvio_app
185 lines
5.9 KiB
Dart
185 lines
5.9 KiB
Dart
import 'dart:developer';
|
|
|
|
import 'package:eatvio_app/core/error_reporting/report_from.dart';
|
|
import 'package:eatvio_app/features/insights/insights_feature_flags.dart';
|
|
import 'package:eatvio_app/features/insights/models/insights_period.dart';
|
|
import 'package:eatvio_app/features/insights/presentation/cubit/insights_panel.dart';
|
|
import 'package:eatvio_app/features/insights/presentation/cubit/insights_state.dart';
|
|
import 'package:eatvio_app/features/insights/repositories/insights_repository.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
class InsightsCubit extends Cubit<InsightsState> {
|
|
InsightsCubit({
|
|
required this.repository,
|
|
this.slug,
|
|
this.canAdd = false,
|
|
this.kbjuEnabled = InsightsFeatureFlags.kbjuEnabled,
|
|
}) : super(const InsightsState.initial());
|
|
|
|
final InsightsRepository repository;
|
|
|
|
/// `null` — собственные данные, иначе slug владельца (консультант → клиент).
|
|
final String? slug;
|
|
|
|
/// Показывать ли кнопку «+» (только владелец).
|
|
final bool canAdd;
|
|
|
|
final bool kbjuEnabled;
|
|
|
|
bool _isHeightLoaded = false;
|
|
int? _heightCm;
|
|
int _requestId = 0;
|
|
|
|
List<InsightsPanel> get panels =>
|
|
InsightsPanel.available(kbjuEnabled: kbjuEnabled);
|
|
|
|
Future<void> load() async {
|
|
emit(const InsightsState.loading());
|
|
await _fetch(bodyPeriod: BodyPeriod.threeMonths, previous: null);
|
|
}
|
|
|
|
/// Перезапрашивает данные, сохраняя вкладку, период, серии и раскрытие.
|
|
Future<void> refresh() async {
|
|
final current = state;
|
|
if (current is! InsightsStateLoaded) {
|
|
return load();
|
|
}
|
|
emit(current.copyWith(isRefreshing: true));
|
|
await _fetch(bodyPeriod: current.bodyPeriod, previous: current);
|
|
}
|
|
|
|
Future<void> setBodyPeriod(BodyPeriod period) async {
|
|
final current = state;
|
|
if (current is! InsightsStateLoaded || current.bodyPeriod == period) {
|
|
return;
|
|
}
|
|
emit(current.copyWith(bodyPeriod: period, isRefreshing: true));
|
|
await _fetch(bodyPeriod: period, previous: current);
|
|
}
|
|
|
|
void toggleExpanded() {
|
|
final current = state;
|
|
if (current is InsightsStateLoaded) {
|
|
emit(current.copyWith(isExpanded: !current.isExpanded));
|
|
}
|
|
}
|
|
|
|
void setPanel(InsightsPanel panel) {
|
|
final current = state;
|
|
if (current is! InsightsStateLoaded || !panels.contains(panel)) {
|
|
return;
|
|
}
|
|
emit(current.copyWith(panel: panel));
|
|
}
|
|
|
|
/// Включает/выключает серию [index] на вкладке [panel].
|
|
void toggleSeries(InsightsPanel panel, int index) {
|
|
final current = state;
|
|
if (current is! InsightsStateLoaded) {
|
|
return;
|
|
}
|
|
final visibility = current.seriesVisibility;
|
|
final source = switch (panel) {
|
|
InsightsPanel.measures => visibility.measure,
|
|
InsightsPanel.daily => visibility.daily,
|
|
InsightsPanel.intra => visibility.intra,
|
|
_ => null,
|
|
};
|
|
if (source == null || index < 0 || index >= source.length) {
|
|
return;
|
|
}
|
|
final updated = [...source]..[index] = !source[index];
|
|
final nextVisibility = switch (panel) {
|
|
InsightsPanel.measures => visibility.copyWith(measure: updated),
|
|
InsightsPanel.daily => visibility.copyWith(daily: updated),
|
|
_ => visibility.copyWith(intra: updated),
|
|
};
|
|
emit(current.copyWith(seriesVisibility: nextVisibility));
|
|
}
|
|
|
|
void toggleSleep() {
|
|
final current = state;
|
|
if (current is InsightsStateLoaded) {
|
|
emit(current.copyWith(showSleep: !current.showSleep));
|
|
}
|
|
}
|
|
|
|
/// Сохраняет вес и обновляет данные. Возвращает `false`, если сохранить не удалось.
|
|
Future<bool> addWeight({required double weight}) {
|
|
return _add(() => repository.addWeight(weight: weight));
|
|
}
|
|
|
|
/// Сохраняет замеры и обновляет данные. Возвращает `false`, если сохранить не удалось.
|
|
Future<bool> addCircumference({
|
|
required int chestCircumference,
|
|
required int waistCircumference,
|
|
required int hipCircumference,
|
|
}) {
|
|
return _add(
|
|
() => repository.addCircumference(
|
|
chestCircumference: chestCircumference,
|
|
waistCircumference: waistCircumference,
|
|
hipCircumference: hipCircumference,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<bool> _add(Future<void> Function() action) async {
|
|
try {
|
|
await action();
|
|
} catch (e, st) {
|
|
await reportFrom(e, st, this);
|
|
log(e.toString(), stackTrace: st);
|
|
return false;
|
|
}
|
|
await refresh();
|
|
return true;
|
|
}
|
|
|
|
Future<void> _fetch({
|
|
required BodyPeriod bodyPeriod,
|
|
required InsightsStateLoaded? previous,
|
|
}) async {
|
|
final requestId = ++_requestId;
|
|
try {
|
|
if (!_isHeightLoaded) {
|
|
_heightCm = await repository.getHeightCm(slug: slug);
|
|
_isHeightLoaded = true;
|
|
}
|
|
final payload = await repository.getInsights(
|
|
slug: slug,
|
|
bodyPeriod: bodyPeriod,
|
|
heightCm: _heightCm,
|
|
);
|
|
if (requestId != _requestId || isClosed) {
|
|
return;
|
|
}
|
|
emit(
|
|
previous == null
|
|
? InsightsState.loaded(payload: payload, bodyPeriod: bodyPeriod)
|
|
: previous.copyWith(
|
|
payload: payload,
|
|
bodyPeriod: bodyPeriod,
|
|
isRefreshing: false,
|
|
),
|
|
);
|
|
} on InsightsForbidden {
|
|
if (requestId == _requestId && !isClosed) {
|
|
emit(const InsightsState.hidden());
|
|
}
|
|
} catch (e, st) {
|
|
await reportFrom(e, st, this);
|
|
log(e.toString(), stackTrace: st);
|
|
if (requestId != _requestId || isClosed) {
|
|
return;
|
|
}
|
|
// Ошибка перезапроса не должна стирать уже показанные данные.
|
|
emit(
|
|
previous == null
|
|
? InsightsState.error(message: e.toString())
|
|
: previous.copyWith(isRefreshing: false),
|
|
);
|
|
}
|
|
}
|
|
}
|