health_trend_content.dart 11.1 KB
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../../../../data/local/user_preferences_storage.dart';
import '../../../routes/app_pages.dart';
import '../../activity_burn_report/controllers/activity_burn_report_logic.dart';
import '../../activity_burn_report/views/activity_burn_report_view.dart';
import '../../home/widgets/trend/trend_type_tab_bar.dart';
import '../../hrv_report/controllers/hrv_report_logic.dart';
import '../../hrv_report/views/hrv_report_view.dart';
import '../../report_common/models/health_report_query.dart';
import '../../report_common/models/report_period.dart';
import '../../report_common/widgets/health_report_subject_scope.dart';
import '../../sleep_report/controllers/sleep_report_logic.dart';
import '../../sleep_report/views/sleep_report_view.dart';

class HealthTrendContent extends StatefulWidget {
  const HealthTrendContent({
    super.key,
    required this.query,
    required this.selectedTypeIndex,
    required this.onTypeChanged,
    required this.onPeriodChanged,
    required this.onDateChanged,
  });

  final HealthReportQuery query;
  final int selectedTypeIndex;
  final ValueChanged<int> onTypeChanged;
  final ValueChanged<ReportPeriod> onPeriodChanged;
  final ValueChanged<DateTime> onDateChanged;

  @override
  State<HealthTrendContent> createState() => _HealthTrendContentState();
}

class _HealthTrendContentState extends State<HealthTrendContent>
    with SingleTickerProviderStateMixin {
  static const _typeCount = 3;

  late final TabController _tabController;

  @override
  void initState() {
    super.initState();
    final initialIndex = _normalizedTypeIndex(widget.selectedTypeIndex);
    _tabController = TabController(
      length: _typeCount,
      vsync: this,
      initialIndex: initialIndex,
    );
    _tabController.addListener(() {
      if (!_tabController.indexIsChanging) {
        widget.onTypeChanged(_tabController.index);
      }
    });
    _correctInvalidTypeIndex(widget.selectedTypeIndex, initialIndex);
  }

  @override
  void didUpdateWidget(covariant HealthTrendContent oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.selectedTypeIndex != widget.selectedTypeIndex) {
      final nextIndex = _normalizedTypeIndex(widget.selectedTypeIndex);
      if (_tabController.index != nextIndex) {
        _tabController.animateTo(nextIndex);
      }
      _correctInvalidTypeIndex(widget.selectedTypeIndex, nextIndex);
    }
  }

  int _normalizedTypeIndex(int index) {
    return index >= 0 && index < _typeCount ? index : 0;
  }

  void _correctInvalidTypeIndex(int requestedIndex, int normalizedIndex) {
    if (requestedIndex == normalizedIndex) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) widget.onTypeChanged(normalizedIndex);
    });
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final userPrefs = Get.find<UserPreferencesStorage>();
    return Obx(() {
      final preferences = userPrefs.preferences.value;
      final vipInfo = preferences.vipInfo;
      final isVip = vipInfo?.isVip ?? false;
      void openPurchase() => Get.toNamed(Routes.PURCHASE);

      return HealthReportSubjectScope(
        isSelf: widget.query.isSelf,
        child: Column(
          children: [
            TrendTypeTabBar(tabController: _tabController),
            Expanded(
              child: TabBarView(
                controller: _tabController,
                children: [
                  _KeepAliveWrapper(
                    child: _HrvTrendSection(
                      query: widget.query,
                      isVip: isVip,
                      onSubscribe: openPurchase,
                      onPeriodChanged: widget.onPeriodChanged,
                      onDateChanged: widget.onDateChanged,
                    ),
                  ),
                  _KeepAliveWrapper(
                    child: _ActivityBurnTrendSection(
                      query: widget.query,
                      isVip: isVip,
                      onSubscribe: openPurchase,
                      onPeriodChanged: widget.onPeriodChanged,
                      onDateChanged: widget.onDateChanged,
                    ),
                  ),
                  _KeepAliveWrapper(
                    child: _SleepTrendSection(
                      query: widget.query,
                      isVip: isVip,
                      onSubscribe: openPurchase,
                      onPeriodChanged: widget.onPeriodChanged,
                      onDateChanged: widget.onDateChanged,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      );
    });
  }
}

class _HrvTrendSection extends StatefulWidget {
  const _HrvTrendSection({
    required this.query,
    required this.isVip,
    required this.onSubscribe,
    required this.onPeriodChanged,
    required this.onDateChanged,
  });

  final HealthReportQuery query;
  final bool isVip;
  final VoidCallback onSubscribe;
  final ValueChanged<ReportPeriod> onPeriodChanged;
  final ValueChanged<DateTime> onDateChanged;

  @override
  State<_HrvTrendSection> createState() => _HrvTrendSectionState();
}

class _HrvTrendSectionState extends State<_HrvTrendSection> {
  late final HrvReportLogic _logic;
  late final Worker _periodWorker;
  late final Worker _dateWorker;
  var _syncingExternalQuery = false;

  @override
  void initState() {
    super.initState();
    _logic = HrvReportLogic(initialTargetUserId: widget.query.targetUserId);
    _logic.initializeQuery(widget.query.period, widget.query.date);
    _periodWorker = ever(_logic.selectedPeriod, (period) {
      if (!_syncingExternalQuery) widget.onPeriodChanged(period);
    });
    _dateWorker = ever(_logic.selectedDate, (date) {
      if (!_syncingExternalQuery) widget.onDateChanged(date);
    });
    _logic.loadReport();
  }

  @override
  void didUpdateWidget(covariant _HrvTrendSection oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.query != widget.query) {
      _syncExternalQuery();
    }
  }

  Future<void> _syncExternalQuery() async {
    _syncingExternalQuery = true;
    _logic.targetUserId.value = widget.query.targetUserId;
    try {
      await _logic.selectQuery(widget.query.period, widget.query.date);
    } finally {
      _syncingExternalQuery = false;
    }
  }

  @override
  void dispose() {
    _periodWorker.dispose();
    _dateWorker.dispose();
    _logic.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => HrvReportView(
        logic: _logic,
        isVip: widget.isVip,
        onSubscribe: widget.onSubscribe,
      );
}

class _ActivityBurnTrendSection extends StatefulWidget {
  const _ActivityBurnTrendSection({
    required this.query,
    required this.isVip,
    required this.onSubscribe,
    required this.onPeriodChanged,
    required this.onDateChanged,
  });

  final HealthReportQuery query;
  final bool isVip;
  final VoidCallback onSubscribe;
  final ValueChanged<ReportPeriod> onPeriodChanged;
  final ValueChanged<DateTime> onDateChanged;

  @override
  State<_ActivityBurnTrendSection> createState() =>
      _ActivityBurnTrendSectionState();
}

class _ActivityBurnTrendSectionState extends State<_ActivityBurnTrendSection> {
  late final ActivityBurnReportLogic _logic;
  late final Worker _periodWorker;
  late final Worker _dateWorker;
  var _syncingExternalQuery = false;

  @override
  void initState() {
    super.initState();
    _logic = ActivityBurnReportLogic(
      initialTargetUserId: widget.query.targetUserId,
    );
    _logic.initializeQuery(widget.query.period, widget.query.date);
    _periodWorker = ever(_logic.selectedPeriod, (period) {
      if (!_syncingExternalQuery) widget.onPeriodChanged(period);
    });
    _dateWorker = ever(_logic.selectedDate, (date) {
      if (!_syncingExternalQuery) widget.onDateChanged(date);
    });
    _logic.loadReport();
  }

  @override
  void didUpdateWidget(covariant _ActivityBurnTrendSection oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.query != widget.query) {
      _syncExternalQuery();
    }
  }

  Future<void> _syncExternalQuery() async {
    _syncingExternalQuery = true;
    _logic.targetUserId.value = widget.query.targetUserId;
    try {
      await _logic.selectQuery(widget.query.period, widget.query.date);
    } finally {
      _syncingExternalQuery = false;
    }
  }

  @override
  void dispose() {
    _periodWorker.dispose();
    _dateWorker.dispose();
    _logic.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => ActivityBurnReportView(
        logic: _logic,
        isVip: widget.isVip,
        onSubscribe: widget.onSubscribe,
      );
}

class _SleepTrendSection extends StatefulWidget {
  const _SleepTrendSection({
    required this.query,
    required this.isVip,
    required this.onSubscribe,
    required this.onPeriodChanged,
    required this.onDateChanged,
  });

  final HealthReportQuery query;
  final bool isVip;
  final VoidCallback onSubscribe;
  final ValueChanged<ReportPeriod> onPeriodChanged;
  final ValueChanged<DateTime> onDateChanged;

  @override
  State<_SleepTrendSection> createState() => _SleepTrendSectionState();
}

class _SleepTrendSectionState extends State<_SleepTrendSection> {
  late final SleepReportLogic _logic;
  late final Worker _periodWorker;
  late final Worker _dateWorker;
  var _syncingExternalQuery = false;

  @override
  void initState() {
    super.initState();
    _logic = SleepReportLogic(
      initialTargetUserId: widget.query.targetUserId,
    );
    _logic.initializeQuery(widget.query.period, widget.query.date);
    _periodWorker = ever(_logic.selectedPeriod, (period) {
      if (!_syncingExternalQuery) widget.onPeriodChanged(period);
    });
    _dateWorker = ever(_logic.selectedDate, (date) {
      if (!_syncingExternalQuery) widget.onDateChanged(date);
    });
    _logic.loadReport();
  }

  @override
  void didUpdateWidget(covariant _SleepTrendSection oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.query != widget.query) {
      _syncExternalQuery();
    }
  }

  Future<void> _syncExternalQuery() async {
    _syncingExternalQuery = true;
    _logic.targetUserId.value = widget.query.targetUserId;
    try {
      await _logic.selectQuery(widget.query.period, widget.query.date);
    } finally {
      _syncingExternalQuery = false;
    }
  }

  @override
  void dispose() {
    _periodWorker.dispose();
    _dateWorker.dispose();
    _logic.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => SleepReportView(
        logic: _logic,
        isVip: widget.isVip,
        onSubscribe: widget.onSubscribe,
      );
}

class _KeepAliveWrapper extends StatefulWidget {
  const _KeepAliveWrapper({required this.child});

  final Widget child;

  @override
  State<_KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}

class _KeepAliveWrapperState extends State<_KeepAliveWrapper>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.child;
  }
}