|
|
|
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;
|
|
|
|
} |
...
|
...
|
|