friend_interaction_content.dart 9.29 KB
import 'package:doublefeel_flutter/data/models/interaction/interaction_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/widgets.dart';

import '../../report_common/utils/report_date_formatter.dart';
import '../../report_common/utils/report_localization.dart';

/// The display payload of a friend-interaction sheet.
///
/// The interaction type is an API contract; this class deliberately contains
/// only what the sheet needs to render. A host page can therefore provide its
/// own date-aware title and metrics without duplicating the sheet layout.
@immutable
class FriendInteractionContent {
  const FriendInteractionContent({
    required this.title,
    this.metrics = const [],
  });

  final String title;
  final List<FriendInteractionMetric> metrics;

  /// A title-only presentation used by trend reports, where the design has no
  /// numeric detail below the title. [period] supplies the date semantics
  /// from the table (today/yesterday/a date, a week range, month, or year).
  factory FriendInteractionContent.forType(
    BuildContext context,
    FriendInteractionType type, {
    String? title,
    FriendInteractionPeriod? period,
    List<FriendInteractionMetric> metrics = const [],
  }) => FriendInteractionContent(
    title: title ?? _titleFor(context, type, period),
    metrics: metrics,
  );

  static String _titleFor(
    BuildContext context,
    FriendInteractionType type,
    FriendInteractionPeriod? period,
  ) {
    if (type == FriendInteractionType.yearStressTrend) {
      return context.l10n.interactSceneTitle(
        InteractionSceneTitle.yearStressTrend,
        year: period?.start.year,
      );
    }
    final isCurrentPeriod = period?.isCurrent ?? false;
    final title = switch (type) {
      FriendInteractionType.defaultInteraction =>
        context.l10n.interactTodaySteps,
      FriendInteractionType.todayAverageHrv => context.l10n.interactSceneTitle(
        InteractionSceneTitle.todayAverageHrv,
      ),
      FriendInteractionType.todayRestingHeartRate =>
        context.l10n.interactSceneTitle(
          InteractionSceneTitle.todayRestingHeartRate,
        ),
      FriendInteractionType.todaySleepStatus ||
      FriendInteractionType.dailySleepStatus => context.l10n.interactSceneTitle(
        InteractionSceneTitle.todaySleepStatus,
      ),
      FriendInteractionType.todayFitnessStatus ||
      FriendInteractionType.dailyActivityBurn =>
        context.l10n.interactSceneTitle(
          type == FriendInteractionType.todayFitnessStatus
              ? InteractionSceneTitle.todayFitnessStatus
              : InteractionSceneTitle.todayActivityBurn,
        ),
      FriendInteractionType.weekStressTrend => context.l10n.interactSceneTitle(
        isCurrentPeriod
            ? InteractionSceneTitle.weekStressTrend
            : InteractionSceneTitle.stressTrend,
      ),
      FriendInteractionType.monthStressTrend => context.l10n.interactSceneTitle(
        isCurrentPeriod
            ? InteractionSceneTitle.monthStressTrend
            : InteractionSceneTitle.stressTrend,
      ),
      FriendInteractionType.weeklyActivityBurn ||
      FriendInteractionType.monthlyActivityBurn =>
        context.l10n.interactSceneTitle(
          type == FriendInteractionType.weeklyActivityBurn
              ? (isCurrentPeriod
                    ? InteractionSceneTitle.weekActivityBurn
                    : InteractionSceneTitle.activityBurn)
              : (isCurrentPeriod
                    ? InteractionSceneTitle.monthActivityBurn
                    : InteractionSceneTitle.activityBurn),
        ),
      FriendInteractionType.weeklySleepTime ||
      FriendInteractionType.monthlySleepTime => context.l10n.interactSceneTitle(
        type == FriendInteractionType.weeklySleepTime
            ? (isCurrentPeriod
                  ? InteractionSceneTitle.weekSleepTime
                  : InteractionSceneTitle.sleepTime)
            : (isCurrentPeriod
                  ? InteractionSceneTitle.monthSleepTime
                  : InteractionSceneTitle.sleepTime),
      ),
      FriendInteractionType.pkSteps => context.l10n.interactSceneTitle(
        InteractionSceneTitle.pkSteps,
      ),
      FriendInteractionType.pkCalories => context.l10n.interactSceneTitle(
        InteractionSceneTitle.pkCalories,
      ),
      FriendInteractionType.pkStandCount => context.l10n.interactSceneTitle(
        InteractionSceneTitle.pkStandTime,
      ),
      FriendInteractionType.pkMonthlyResult => context.l10n.interactSceneTitle(
        InteractionSceneTitle.pkMonthlyResult,
      ),
      FriendInteractionType.yearStressTrend => '',
    };
    final periodLabel = period?.labelFor(context);
    return periodLabel == null || periodLabel.isEmpty
        ? title
        : '$title ($periodLabel)';
  }
}

/// Describes the selected report time without coupling the sheet to a page
/// controller. This covers every date form in the interaction-scene table.
@immutable
class FriendInteractionPeriod {
  const FriendInteractionPeriod.day(this.start, {this.referenceDate})
    : kind = FriendInteractionPeriodKind.day,
      end = null,
      periodName = null;

  const FriendInteractionPeriod.week(
    this.start,
    this.end, {
    this.periodName,
    this.referenceDate,
  }) : kind = FriendInteractionPeriodKind.week;

  const FriendInteractionPeriod.month(
    this.start, {
    this.periodName,
    this.referenceDate,
  }) : kind = FriendInteractionPeriodKind.month,
       end = null;

  const FriendInteractionPeriod.year(
    this.start, {
    this.periodName,
    this.referenceDate,
  }) : kind = FriendInteractionPeriodKind.year,
       end = null;

  final FriendInteractionPeriodKind kind;
  final DateTime start;
  final DateTime? end;

  /// Localized page label when one exists, e.g. “This week”. It is optional
  /// because a locale-aware date range remains meaningful on its own.
  final String? periodName;
  final DateTime? referenceDate;

  /// Whether this period is the current natural period relative to the
  /// supplied reference date (or today). Historical periods intentionally use
  /// neutral titles rather than “this week” or “this month”.
  bool get isCurrent {
    final reference = _dateOnly(referenceDate ?? DateTime.now());
    final date = _dateOnly(start);
    return switch (kind) {
      FriendInteractionPeriodKind.day => date == reference,
      FriendInteractionPeriodKind.week => _isCurrentWeek(date, reference),
      FriendInteractionPeriodKind.month => _isCurrentMonth(date, reference),
      FriendInteractionPeriodKind.year => date.year == reference.year,
    };
  }

  String labelFor(BuildContext context) {
    final reference = _dateOnly(referenceDate ?? DateTime.now());
    final locale = reportLocaleName(context);
    final date = _dateOnly(start);
    return switch (kind) {
      FriendInteractionPeriodKind.day => _dayLabel(
        context,
        date,
        reference,
        locale,
      ),
      FriendInteractionPeriodKind.week => _join(
        periodName ??
            (_isCurrentWeek(date, reference)
                ? context.l10n.interactThisWeek
                : null),
        ReportDateFormatter.weekRange(
          date,
          _dateOnly(end ?? start),
          locale,
          referenceDate: reference,
        ),
      ),
      FriendInteractionPeriodKind.month => _join(
        periodName ??
            (_isCurrentMonth(date, reference)
                ? context.l10n.interactThisMonth
                : null),
        ReportDateFormatter.weekRange(
          date,
          DateTime(date.year, date.month + 1, 0),
          locale,
          referenceDate: reference,
        ),
      ),
      FriendInteractionPeriodKind.year => _join(
        periodName,
        ReportDateFormatter.year(date, locale),
      ),
    };
  }

  static String _dayLabel(
    BuildContext context,
    DateTime date,
    DateTime reference,
    String locale,
  ) {
    final difference = reference.difference(date).inDays;
    if (difference == 0) return context.l10n.today;
    if (difference == 1) return context.l10n.yesterday;
    return date.year == reference.year
        ? ReportDateFormatter.monthDay(date, locale)
        : ReportDateFormatter.yearMonthDay(date, locale);
  }

  static DateTime _dateOnly(DateTime value) =>
      DateTime(value.year, value.month, value.day);

  static bool _isCurrentWeek(DateTime date, DateTime reference) {
    final currentWeekStart = reference.subtract(
      Duration(days: reference.weekday - DateTime.monday),
    );
    return date == currentWeekStart;
  }

  static bool _isCurrentMonth(DateTime date, DateTime reference) =>
      date.year == reference.year && date.month == reference.month;

  static String _join(String? name, String date) =>
      name == null || name.isEmpty ? date : '$name, $date';
}

enum FriendInteractionPeriodKind { day, week, month, year }

/// One horizontally arranged data item in [FriendInteractionContent].
@immutable
class FriendInteractionMetric {
  const FriendInteractionMetric({
    required this.value,
    this.unit,
    this.label,
    this.isText = false,
  });

  final String value;
  final String? unit;

  /// Optional compact pill (for example, “Workout”, “Stand”, or “Quality”).
  final String? label;

  /// Textual states such as sleep quality use the smaller value treatment in
  /// the Figma design instead of the large numeric treatment.
  final bool isText;
}