refactor(activity-meter): redesign screen with honest per-range stats
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:sca_treezor/sca_treezor.dart';
|
||||
import 'package:sf_app_platform/config/env/environment_enum.dart';
|
||||
@@ -19,14 +20,12 @@ import 'package:sf_tracking/sf_tracking.dart';
|
||||
Future<void> initApp(EnvironmentEnum env) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
await initializeDateFormatting();
|
||||
|
||||
navigationModule();
|
||||
scaTreezorModule();
|
||||
themePackages();
|
||||
|
||||
// Order matters: Firebase → sfTracking (FirebaseTrackingClient touches
|
||||
// FirebaseAnalytics.instance) → router (SaveFamilyApp wires sfTracking
|
||||
// into SfRouterListener at construction time).
|
||||
await setupFirebase(env);
|
||||
await setupNotifications();
|
||||
initSfTracking();
|
||||
|
||||
@@ -94,6 +94,7 @@ dependencies:
|
||||
#dependencies go here
|
||||
cupertino_icons: ^1.0.8
|
||||
flutter_svg: ^2.2.2
|
||||
intl: ^0.20.2
|
||||
go_router_builder: ^4.1.1
|
||||
build_runner: ^2.7.1
|
||||
|
||||
|
||||
@@ -5,13 +5,18 @@ import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../../core/presentation/format_date.dart';
|
||||
import '../../../core/presentation/time_range.dart';
|
||||
import '../../../core/presentation/widgets/time_range_selector.dart';
|
||||
import 'state/activity_meter_view_model.dart';
|
||||
import 'state/activity_meter_view_state.dart';
|
||||
import 'widgets/activity_footers.dart';
|
||||
import 'widgets/hourly_bar_chart.dart';
|
||||
import 'widgets/pedometer_toggle.dart';
|
||||
import 'widgets/section_header.dart';
|
||||
import 'widgets/steps_bar_chart.dart';
|
||||
import 'widgets/steps_history_section.dart';
|
||||
import 'widgets/steps_progress_ring.dart';
|
||||
import 'widgets/steps_stats_row.dart';
|
||||
|
||||
class ActivityMeterScreen extends ConsumerWidget {
|
||||
const ActivityMeterScreen({super.key});
|
||||
@@ -19,145 +24,66 @@ class ActivityMeterScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final state = ref.watch(activityMeterViewModelProvider);
|
||||
final vm = ref.read(activityMeterViewModelProvider.notifier);
|
||||
final device = ref.watch(selectedDeviceProvider).value;
|
||||
final isLoading = ref.watch(
|
||||
activityMeterViewModelProvider.select((s) => s.isLoading),
|
||||
);
|
||||
|
||||
ref.listen(activityMeterViewModelProvider.select((s) => s.errorEvent), (
|
||||
previous,
|
||||
_,
|
||||
next,
|
||||
) {
|
||||
if (next != null) {
|
||||
final message = switch (next) {
|
||||
ActivityMeterErrorEvent.loadData => context.translate(
|
||||
I18n.errorActivityData,
|
||||
),
|
||||
ActivityMeterErrorEvent.loadMore => context.translate(
|
||||
I18n.errorActivityData,
|
||||
),
|
||||
ActivityMeterErrorEvent.pedometer => context.translate(
|
||||
I18n.errorPedometer,
|
||||
),
|
||||
};
|
||||
showTopSnackbar(context, message: message, type: MessageType.error);
|
||||
}
|
||||
if (next == null) return;
|
||||
final message = switch (next) {
|
||||
ActivityMeterErrorEvent.loadData ||
|
||||
ActivityMeterErrorEvent.loadMore => context.translate(
|
||||
I18n.errorActivityData,
|
||||
),
|
||||
ActivityMeterErrorEvent.pedometer => context.translate(
|
||||
I18n.errorPedometer,
|
||||
),
|
||||
};
|
||||
showTopSnackbar(context, message: message, type: MessageType.error);
|
||||
});
|
||||
|
||||
final hasData =
|
||||
state.todayTotal > 0 ||
|
||||
state.chartData.isNotEmpty ||
|
||||
state.historyData.isNotEmpty;
|
||||
|
||||
return LegacyPageLayout(
|
||||
theme: theme,
|
||||
title: context.translate(I18n.activityMeter),
|
||||
body: state.isLoading
|
||||
body: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!hasData)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 12, big: 10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
size: SizeUtils.getByScreen(small: 20, big: 22),
|
||||
),
|
||||
SizedBox(
|
||||
width: SizeUtils.getByScreen(small: 8, big: 10),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
context.translate(I18n.noActivityData),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(
|
||||
small: 14,
|
||||
big: 15,
|
||||
),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withAlpha(178),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 4, big: 4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.translate(I18n.activityMeterPedometer),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 14, big: 15),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: device?.settings.pedometer ?? false,
|
||||
activeTrackColor: theme.getColorFor(
|
||||
ThemeCode.legacyPrimary,
|
||||
),
|
||||
onChanged: (value) async {
|
||||
final success = await vm.togglePedometer(
|
||||
enabled: value,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(
|
||||
value
|
||||
? I18n.activityMeterPedometerEnabled
|
||||
: I18n.activityMeterPedometerDisabled,
|
||||
),
|
||||
type: MessageType.success,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
StepsProgressRing(
|
||||
steps: state.todayTotal,
|
||||
goal: state.dailyGoal,
|
||||
theme: theme,
|
||||
),
|
||||
TimeRangeSelector(
|
||||
selected: state.timeRange,
|
||||
onSelected: (range) => vm.selectTimeRange(range),
|
||||
onCustomTap: () => _pickCustomRange(context, vm),
|
||||
theme: theme,
|
||||
),
|
||||
StepsBarChart(data: state.chartData, theme: theme),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)),
|
||||
StepsStatsRow(stats: state.stats, theme: theme),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)),
|
||||
StepsHistorySection(
|
||||
items: state.historyData,
|
||||
dailyGoal: state.dailyGoal,
|
||||
hasMore: state.hasMoreHistory,
|
||||
isLoadingMore: state.isLoadingMore,
|
||||
onLoadMore: () => vm.loadMoreHistory(),
|
||||
theme: theme,
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 24, big: 20)),
|
||||
],
|
||||
),
|
||||
),
|
||||
: const _ActivityMeterBody(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityMeterBody extends ConsumerWidget {
|
||||
const _ActivityMeterBody();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final vm = ref.read(activityMeterViewModelProvider.notifier);
|
||||
final timeRange = ref.watch(
|
||||
activityMeterViewModelProvider.select((s) => s.timeRange),
|
||||
);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const PedometerToggle(),
|
||||
SectionHeader(title: context.translate(I18n.activityMeterSectionToday)),
|
||||
const _TodaySection(),
|
||||
const _ActivityRangeHeader(),
|
||||
TimeRangeSelector(
|
||||
selected: timeRange,
|
||||
onSelected: vm.selectTimeRange,
|
||||
onCustomTap: () => _pickCustomRange(context, vm),
|
||||
theme: ref.watch(themePortProvider),
|
||||
),
|
||||
const _ActivitySection(),
|
||||
if (timeRange != TimeRange.today) const _HistorySection(),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 24, big: 20)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,3 +102,164 @@ class ActivityMeterScreen extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TodaySection extends ConsumerWidget {
|
||||
const _TodaySection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final (todayTotal, dailyGoal) = ref.watch(
|
||||
activityMeterViewModelProvider.select(
|
||||
(s) => (s.todayTotal, s.dailyGoal),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
StepsProgressRing(steps: todayTotal, goal: dailyGoal, theme: theme),
|
||||
if (todayTotal == 0)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 32, big: 28),
|
||||
vertical: SizeUtils.getByScreen(small: 4, big: 2),
|
||||
),
|
||||
child: Text(
|
||||
context.translate(I18n.activityMeterNoStepsToday),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityRangeHeader extends ConsumerWidget {
|
||||
const _ActivityRangeHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final (start, end, timeRange) = ref.watch(
|
||||
activityMeterViewModelProvider.select(
|
||||
(s) => (s.rangeStart, s.rangeEnd, s.timeRange),
|
||||
),
|
||||
);
|
||||
|
||||
return SectionHeader(
|
||||
title: context.translate(I18n.activityMeterSectionActivity),
|
||||
subtitle: _subtitle(context, start, end, timeRange),
|
||||
);
|
||||
}
|
||||
|
||||
String? _subtitle(
|
||||
BuildContext context,
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
TimeRange timeRange,
|
||||
) {
|
||||
if (start == null || end == null) return null;
|
||||
if (timeRange == TimeRange.today) {
|
||||
return context
|
||||
.translate(I18n.activityMeterRangeLabelToday)
|
||||
.replaceAll('{date}', formatDayHeader(context, start));
|
||||
}
|
||||
return '${formatDayHeader(context, start)} — ${formatDayHeader(context, end)}';
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivitySection extends ConsumerWidget {
|
||||
const _ActivitySection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final (timeRange, hourly, daily, stats, activeHours, hasData) = ref.watch(
|
||||
activityMeterViewModelProvider.select(
|
||||
(s) => (
|
||||
s.timeRange,
|
||||
s.hourlyData,
|
||||
s.chartData,
|
||||
s.stats,
|
||||
s.activeHoursToday,
|
||||
s.hasActivityData,
|
||||
),
|
||||
),
|
||||
);
|
||||
final isToday = timeRange == TimeRange.today;
|
||||
|
||||
if (!hasData) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 24, big: 20),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
context.translate(I18n.activityMeterNoStepsPeriod),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (isToday)
|
||||
HourlyBarChart(data: hourly)
|
||||
else
|
||||
StepsBarChart(data: daily),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)),
|
||||
if (isToday)
|
||||
TodayActivityFooter(activeHours: activeHours)
|
||||
else
|
||||
PeriodActivityFooter(stats: stats),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HistorySection extends ConsumerWidget {
|
||||
const _HistorySection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final vm = ref.read(activityMeterViewModelProvider.notifier);
|
||||
final (history, dailyGoal, hasMore, isLoadingMore) = ref.watch(
|
||||
activityMeterViewModelProvider.select(
|
||||
(s) => (s.historyData, s.dailyGoal, s.hasMoreHistory, s.isLoadingMore),
|
||||
),
|
||||
);
|
||||
|
||||
if (history.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SectionHeader(
|
||||
title: context.translate(I18n.activityMeterSectionHistory),
|
||||
),
|
||||
StepsHistorySection(
|
||||
items: history.where((d) => d.totalSteps > 0).toList(),
|
||||
dailyGoal: dailyGoal,
|
||||
hasMore: hasMore,
|
||||
isLoadingMore: isLoadingMore,
|
||||
onLoadMore: vm.loadMoreHistory,
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import '../../../../core/domain/entities/steps_entity.dart';
|
||||
import '../../../../core/presentation/time_range.dart';
|
||||
import 'activity_meter_view_state.dart';
|
||||
|
||||
List<DailySteps> groupByDay(List<StepsEntity> steps) {
|
||||
final Map<DateTime, int> groups = {};
|
||||
|
||||
for (final step in steps) {
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(step.occurredAt);
|
||||
final dayKey = DateTime(date.year, date.month, date.day);
|
||||
groups[dayKey] = (groups[dayKey] ?? 0) + step.steps;
|
||||
}
|
||||
|
||||
final sorted = groups.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
|
||||
return sorted
|
||||
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<HourlySteps> groupByHour(List<StepsEntity> steps) {
|
||||
final Map<int, int> groups = {};
|
||||
|
||||
for (final step in steps) {
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(step.occurredAt);
|
||||
groups[date.hour] = (groups[date.hour] ?? 0) + step.steps;
|
||||
}
|
||||
|
||||
return List<HourlySteps>.generate(
|
||||
24,
|
||||
(h) => HourlySteps(hour: h, totalSteps: groups[h] ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
int countActiveHours(List<StepsEntity> steps) {
|
||||
final Set<int> hours = {};
|
||||
for (final s in steps) {
|
||||
if (s.steps <= 0) continue;
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(s.occurredAt);
|
||||
hours.add(date.hour);
|
||||
}
|
||||
return hours.length;
|
||||
}
|
||||
|
||||
List<DailySteps> mergeHistory(
|
||||
List<DailySteps> existing,
|
||||
List<DailySteps> newItems,
|
||||
) {
|
||||
final Map<DateTime, int> merged = {
|
||||
for (final d in existing) d.date: d.totalSteps,
|
||||
};
|
||||
for (final d in newItems) {
|
||||
merged[d.date] = (merged[d.date] ?? 0) + d.totalSteps;
|
||||
}
|
||||
|
||||
final sorted = merged.entries.toList()
|
||||
..sort((a, b) => b.key.compareTo(a.key));
|
||||
|
||||
return sorted
|
||||
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
StepsStats computeStats(List<DailySteps> daily, int rangeDays) {
|
||||
if (daily.isEmpty) {
|
||||
return StepsStats(rangeDays: rangeDays);
|
||||
}
|
||||
|
||||
final total = daily.fold(0, (sum, d) => sum + d.totalSteps);
|
||||
final activeDays = daily.where((d) => d.totalSteps > 0).length;
|
||||
final best = daily.reduce((a, b) => a.totalSteps >= b.totalSteps ? a : b);
|
||||
|
||||
final divisor = rangeDays > 0 ? rangeDays : daily.length;
|
||||
|
||||
return StepsStats(
|
||||
total: total,
|
||||
avgPerDay: (total / divisor).round(),
|
||||
activeDays: activeDays,
|
||||
rangeDays: rangeDays,
|
||||
bestDay: total > 0 ? best : null,
|
||||
);
|
||||
}
|
||||
|
||||
int countDays(DateTime start, DateTime end) {
|
||||
final s = DateTime.utc(start.year, start.month, start.day);
|
||||
final e = DateTime.utc(end.year, end.month, end.day);
|
||||
return e.difference(s).inDays + 1;
|
||||
}
|
||||
|
||||
(DateTime, DateTime) resolveRange(
|
||||
TimeRange timeRange, {
|
||||
required DateTime now,
|
||||
DateTime? customStart,
|
||||
DateTime? customEnd,
|
||||
}) {
|
||||
final todayStart = DateTime(now.year, now.month, now.day);
|
||||
|
||||
switch (timeRange) {
|
||||
case TimeRange.today:
|
||||
return (todayStart, now);
|
||||
case TimeRange.sevenDays:
|
||||
return (DateTime(now.year, now.month, now.day - 6), now);
|
||||
case TimeRange.thirtyDays:
|
||||
return (DateTime(now.year, now.month, now.day - 29), now);
|
||||
case TimeRange.custom:
|
||||
final s = customStart ?? todayStart;
|
||||
final e = customEnd ?? now;
|
||||
return (DateTime(s.year, s.month, s.day), e);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
import '../../../../core/data/datasources/health_query_builder.dart';
|
||||
import '../../../../core/presentation/time_range.dart';
|
||||
import '../../../../core/domain/repositories/steps_repository.dart';
|
||||
import '../../../../core/providers/steps_repository_provider.dart';
|
||||
import '../../../../core/domain/entities/steps_entity.dart';
|
||||
import '../../../../core/domain/repositories/steps_repository.dart';
|
||||
import '../../../../core/presentation/time_range.dart';
|
||||
import '../../../../core/providers/steps_repository_provider.dart';
|
||||
import 'activity_meter_aggregator.dart';
|
||||
import 'activity_meter_view_state.dart';
|
||||
|
||||
final activityMeterViewModelProvider =
|
||||
@@ -27,11 +28,12 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
ActivityMeterViewState build() {
|
||||
_repository = ref.read(stepsRepositoryProvider);
|
||||
_tracking = ref.read(sfTrackingProvider);
|
||||
_init();
|
||||
Future.microtask(_loadFilteredData);
|
||||
return const ActivityMeterViewState();
|
||||
}
|
||||
|
||||
String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator;
|
||||
String? get _identificator =>
|
||||
ref.read(selectedDeviceProvider).value?.identificator;
|
||||
|
||||
Future<void> selectTimeRange(TimeRange range) async {
|
||||
if (range == state.timeRange) return;
|
||||
@@ -77,7 +79,11 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
|
||||
try {
|
||||
final nextPage = state.currentHistoryPage + 1;
|
||||
final filters = _buildTimeFilters();
|
||||
final (start, end) = _resolveRange();
|
||||
final filters = HealthQueryBuilder.timeRangeFilters(
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
|
||||
final steps = await _repository.getSteps(
|
||||
identificator: identificator,
|
||||
@@ -90,8 +96,8 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final newDaily = _groupByDay(steps);
|
||||
final merged = _mergeHistory(state.historyData, newDaily);
|
||||
final newDaily = groupByDay(steps);
|
||||
final merged = mergeHistory(state.historyData, newDaily);
|
||||
|
||||
state = state.copyWith(
|
||||
historyData: merged,
|
||||
@@ -99,7 +105,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
hasMoreHistory: steps.length >= _historyPageSize,
|
||||
isLoadingMore: false,
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isLoadingMore: false,
|
||||
@@ -108,42 +114,6 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
final identificator = _identificator;
|
||||
if (identificator == null) {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final todayStart = DateTime.now();
|
||||
final todayFilters = HealthQueryBuilder.timeRangeFilters(
|
||||
start: DateTime(todayStart.year, todayStart.month, todayStart.day),
|
||||
end: todayStart,
|
||||
);
|
||||
|
||||
final todaySteps = await _repository.getSteps(
|
||||
identificator: identificator,
|
||||
queryParameters: HealthQueryBuilder.build(
|
||||
orderDirection: OrderDirection.desc,
|
||||
filters: todayFilters,
|
||||
),
|
||||
);
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final todayTotal = todaySteps.fold(0, (sum, s) => sum + s.steps);
|
||||
state = state.copyWith(todayTotal: todayTotal);
|
||||
|
||||
await _loadFilteredData();
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
errorEvent: ActivityMeterErrorEvent.loadData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFilteredData() async {
|
||||
final identificator = _identificator;
|
||||
if (identificator == null) {
|
||||
@@ -152,9 +122,13 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
}
|
||||
|
||||
try {
|
||||
final filters = _buildTimeFilters();
|
||||
final (start, end) = _resolveRange();
|
||||
final filters = HealthQueryBuilder.timeRangeFilters(
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
|
||||
final (chartSteps, histSteps) = await (
|
||||
final (chartSteps, histSteps, todaySteps) = await (
|
||||
_repository.getSteps(
|
||||
identificator: identificator,
|
||||
queryParameters: HealthQueryBuilder.build(
|
||||
@@ -171,22 +145,37 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
filters: filters,
|
||||
),
|
||||
),
|
||||
_fetchTodaySteps(identificator),
|
||||
).wait;
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final chartDaily = _groupByDay(chartSteps);
|
||||
final historyDaily = _groupByDay(histSteps).reversed.toList();
|
||||
final todayTotal = todaySteps.fold(0, (sum, s) => sum + s.steps);
|
||||
final activeHoursToday = countActiveHours(todaySteps);
|
||||
|
||||
final chartDaily = groupByDay(chartSteps);
|
||||
final hourly = state.timeRange == TimeRange.today
|
||||
? groupByHour(todaySteps)
|
||||
: const <HourlySteps>[];
|
||||
final historyDaily = groupByDay(histSteps).reversed.toList();
|
||||
|
||||
final rangeDays = countDays(start, end);
|
||||
final stats = computeStats(chartDaily, rangeDays);
|
||||
|
||||
state = state.copyWith(
|
||||
todayTotal: todayTotal,
|
||||
chartData: chartDaily,
|
||||
hourlyData: hourly,
|
||||
activeHoursToday: activeHoursToday,
|
||||
historyData: historyDaily,
|
||||
currentHistoryPage: 1,
|
||||
hasMoreHistory: histSteps.length >= _historyPageSize,
|
||||
stats: _computeStats(chartDaily),
|
||||
stats: stats,
|
||||
rangeStart: start,
|
||||
rangeEnd: end,
|
||||
isLoading: false,
|
||||
errorEvent: null,
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
@@ -195,81 +184,27 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
}
|
||||
}
|
||||
|
||||
List<DailySteps> _groupByDay(List<StepsEntity> steps) {
|
||||
final Map<DateTime, int> groups = {};
|
||||
|
||||
for (final step in steps) {
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(step.occurredAt);
|
||||
final dayKey = DateTime(date.year, date.month, date.day);
|
||||
groups[dayKey] = (groups[dayKey] ?? 0) + step.steps;
|
||||
}
|
||||
|
||||
final sorted = groups.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
|
||||
return sorted
|
||||
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<DailySteps> _mergeHistory(
|
||||
List<DailySteps> existing,
|
||||
List<DailySteps> newItems,
|
||||
) {
|
||||
final Map<DateTime, int> merged = {
|
||||
for (final d in existing) d.date: d.totalSteps,
|
||||
};
|
||||
for (final d in newItems) {
|
||||
merged[d.date] = (merged[d.date] ?? 0) + d.totalSteps;
|
||||
}
|
||||
|
||||
final sorted = merged.entries.toList()
|
||||
..sort((a, b) => b.key.compareTo(a.key));
|
||||
|
||||
return sorted
|
||||
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
StepsStats _computeStats(List<DailySteps> daily) {
|
||||
if (daily.isEmpty) return const StepsStats();
|
||||
|
||||
final total = daily.fold(0, (sum, d) => sum + d.totalSteps);
|
||||
final best = daily.reduce((a, b) => a.totalSteps >= b.totalSteps ? a : b);
|
||||
|
||||
return StepsStats(
|
||||
avgPerDay: (total / daily.length).round(),
|
||||
total: total,
|
||||
bestDaySteps: best.totalSteps,
|
||||
Future<List<StepsEntity>> _fetchTodaySteps(String identificator) {
|
||||
final now = DateTime.now();
|
||||
final todayStart = DateTime(now.year, now.month, now.day);
|
||||
return _repository.getSteps(
|
||||
identificator: identificator,
|
||||
queryParameters: HealthQueryBuilder.build(
|
||||
orderDirection: OrderDirection.asc,
|
||||
filters: HealthQueryBuilder.timeRangeFilters(
|
||||
start: todayStart,
|
||||
end: now,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<HealthFilter>? _buildTimeFilters() {
|
||||
final range = _getTimeRange();
|
||||
if (range == null) return null;
|
||||
|
||||
final (start, end) = range;
|
||||
return HealthQueryBuilder.timeRangeFilters(start: start, end: end);
|
||||
}
|
||||
|
||||
(DateTime, DateTime)? _getTimeRange() {
|
||||
final now = DateTime.now();
|
||||
final todayStart = DateTime(now.year, now.month, now.day);
|
||||
|
||||
switch (state.timeRange) {
|
||||
case TimeRange.today:
|
||||
return (todayStart, now);
|
||||
case TimeRange.sevenDays:
|
||||
return (todayStart.subtract(const Duration(days: 6)), now);
|
||||
case TimeRange.thirtyDays:
|
||||
return (todayStart.subtract(const Duration(days: 29)), now);
|
||||
case TimeRange.custom:
|
||||
if (state.customStart != null && state.customEnd != null) {
|
||||
return (state.customStart!, state.customEnd!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
(DateTime, DateTime) _resolveRange() => resolveRange(
|
||||
state.timeRange,
|
||||
now: DateTime.now(),
|
||||
customStart: state.customStart,
|
||||
customEnd: state.customEnd,
|
||||
);
|
||||
|
||||
Future<bool> togglePedometer({required bool enabled}) async {
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
@@ -290,7 +225,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
|
||||
unawaited(_tracking.legacyDeviceActivityPedometerToggled(enabled));
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
if (!ref.mounted) return false;
|
||||
state = state.copyWith(errorEvent: ActivityMeterErrorEvent.pedometer);
|
||||
return false;
|
||||
|
||||
@@ -12,12 +12,20 @@ abstract class DailySteps with _$DailySteps {
|
||||
_DailySteps;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class HourlySteps with _$HourlySteps {
|
||||
const factory HourlySteps({required int hour, required int totalSteps}) =
|
||||
_HourlySteps;
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class StepsStats with _$StepsStats {
|
||||
const factory StepsStats({
|
||||
@Default(0) int avgPerDay,
|
||||
@Default(0) int total,
|
||||
@Default(0) int bestDaySteps,
|
||||
@Default(0) int avgPerDay,
|
||||
@Default(0) int activeDays,
|
||||
@Default(0) int rangeDays,
|
||||
DailySteps? bestDay,
|
||||
}) = _StepsStats;
|
||||
}
|
||||
|
||||
@@ -27,6 +35,8 @@ abstract class ActivityMeterViewState with _$ActivityMeterViewState {
|
||||
@Default(0) int todayTotal,
|
||||
@Default(8000) int dailyGoal,
|
||||
@Default([]) List<DailySteps> chartData,
|
||||
@Default([]) List<HourlySteps> hourlyData,
|
||||
@Default(0) int activeHoursToday,
|
||||
@Default([]) List<DailySteps> historyData,
|
||||
@Default(1) int currentHistoryPage,
|
||||
@Default(false) bool hasMoreHistory,
|
||||
@@ -34,8 +44,16 @@ abstract class ActivityMeterViewState with _$ActivityMeterViewState {
|
||||
@Default(TimeRange.today) TimeRange timeRange,
|
||||
DateTime? customStart,
|
||||
DateTime? customEnd,
|
||||
DateTime? rangeStart,
|
||||
DateTime? rangeEnd,
|
||||
@Default(true) bool isLoading,
|
||||
@Default(false) bool isLoadingMore,
|
||||
ActivityMeterErrorEvent? errorEvent,
|
||||
}) = _ActivityMeterViewState;
|
||||
}
|
||||
|
||||
extension ActivityMeterViewStateX on ActivityMeterViewState {
|
||||
bool get hasActivityData => timeRange == TimeRange.today
|
||||
? hourlyData.any((h) => h.totalSteps > 0)
|
||||
: stats.total > 0;
|
||||
}
|
||||
|
||||
@@ -269,12 +269,272 @@ as int,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$HourlySteps {
|
||||
|
||||
int get hour; int get totalSteps;
|
||||
/// Create a copy of HourlySteps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$HourlyStepsCopyWith<HourlySteps> get copyWith => _$HourlyStepsCopyWithImpl<HourlySteps>(this as HourlySteps, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is HourlySteps&&(identical(other.hour, hour) || other.hour == hour)&&(identical(other.totalSteps, totalSteps) || other.totalSteps == totalSteps));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,hour,totalSteps);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HourlySteps(hour: $hour, totalSteps: $totalSteps)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $HourlyStepsCopyWith<$Res> {
|
||||
factory $HourlyStepsCopyWith(HourlySteps value, $Res Function(HourlySteps) _then) = _$HourlyStepsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int hour, int totalSteps
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$HourlyStepsCopyWithImpl<$Res>
|
||||
implements $HourlyStepsCopyWith<$Res> {
|
||||
_$HourlyStepsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final HourlySteps _self;
|
||||
final $Res Function(HourlySteps) _then;
|
||||
|
||||
/// Create a copy of HourlySteps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? hour = null,Object? totalSteps = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
hour: null == hour ? _self.hour : hour // ignore: cast_nullable_to_non_nullable
|
||||
as int,totalSteps: null == totalSteps ? _self.totalSteps : totalSteps // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [HourlySteps].
|
||||
extension HourlyStepsPatterns on HourlySteps {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _HourlySteps value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _HourlySteps() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _HourlySteps value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _HourlySteps():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _HourlySteps value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _HourlySteps() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int hour, int totalSteps)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _HourlySteps() when $default != null:
|
||||
return $default(_that.hour,_that.totalSteps);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int hour, int totalSteps) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _HourlySteps():
|
||||
return $default(_that.hour,_that.totalSteps);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int hour, int totalSteps)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _HourlySteps() when $default != null:
|
||||
return $default(_that.hour,_that.totalSteps);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _HourlySteps implements HourlySteps {
|
||||
const _HourlySteps({required this.hour, required this.totalSteps});
|
||||
|
||||
|
||||
@override final int hour;
|
||||
@override final int totalSteps;
|
||||
|
||||
/// Create a copy of HourlySteps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$HourlyStepsCopyWith<_HourlySteps> get copyWith => __$HourlyStepsCopyWithImpl<_HourlySteps>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HourlySteps&&(identical(other.hour, hour) || other.hour == hour)&&(identical(other.totalSteps, totalSteps) || other.totalSteps == totalSteps));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,hour,totalSteps);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HourlySteps(hour: $hour, totalSteps: $totalSteps)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$HourlyStepsCopyWith<$Res> implements $HourlyStepsCopyWith<$Res> {
|
||||
factory _$HourlyStepsCopyWith(_HourlySteps value, $Res Function(_HourlySteps) _then) = __$HourlyStepsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int hour, int totalSteps
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$HourlyStepsCopyWithImpl<$Res>
|
||||
implements _$HourlyStepsCopyWith<$Res> {
|
||||
__$HourlyStepsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _HourlySteps _self;
|
||||
final $Res Function(_HourlySteps) _then;
|
||||
|
||||
/// Create a copy of HourlySteps
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? hour = null,Object? totalSteps = null,}) {
|
||||
return _then(_HourlySteps(
|
||||
hour: null == hour ? _self.hour : hour // ignore: cast_nullable_to_non_nullable
|
||||
as int,totalSteps: null == totalSteps ? _self.totalSteps : totalSteps // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$StepsStats {
|
||||
|
||||
int get avgPerDay; int get total; int get bestDaySteps;
|
||||
int get total; int get avgPerDay; int get activeDays; int get rangeDays; DailySteps? get bestDay;
|
||||
/// Create a copy of StepsStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -285,16 +545,16 @@ $StepsStatsCopyWith<StepsStats> get copyWith => _$StepsStatsCopyWithImpl<StepsSt
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is StepsStats&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.total, total) || other.total == total)&&(identical(other.bestDaySteps, bestDaySteps) || other.bestDaySteps == bestDaySteps));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is StepsStats&&(identical(other.total, total) || other.total == total)&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.activeDays, activeDays) || other.activeDays == activeDays)&&(identical(other.rangeDays, rangeDays) || other.rangeDays == rangeDays)&&(identical(other.bestDay, bestDay) || other.bestDay == bestDay));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,avgPerDay,total,bestDaySteps);
|
||||
int get hashCode => Object.hash(runtimeType,total,avgPerDay,activeDays,rangeDays,bestDay);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StepsStats(avgPerDay: $avgPerDay, total: $total, bestDaySteps: $bestDaySteps)';
|
||||
return 'StepsStats(total: $total, avgPerDay: $avgPerDay, activeDays: $activeDays, rangeDays: $rangeDays, bestDay: $bestDay)';
|
||||
}
|
||||
|
||||
|
||||
@@ -305,11 +565,11 @@ abstract mixin class $StepsStatsCopyWith<$Res> {
|
||||
factory $StepsStatsCopyWith(StepsStats value, $Res Function(StepsStats) _then) = _$StepsStatsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int avgPerDay, int total, int bestDaySteps
|
||||
int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay
|
||||
});
|
||||
|
||||
|
||||
|
||||
$DailyStepsCopyWith<$Res>? get bestDay;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -322,15 +582,29 @@ class _$StepsStatsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of StepsStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? avgPerDay = null,Object? total = null,Object? bestDaySteps = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? total = null,Object? avgPerDay = null,Object? activeDays = null,Object? rangeDays = null,Object? bestDay = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int,bestDaySteps: null == bestDaySteps ? _self.bestDaySteps : bestDaySteps // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int,avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,activeDays: null == activeDays ? _self.activeDays : activeDays // ignore: cast_nullable_to_non_nullable
|
||||
as int,rangeDays: null == rangeDays ? _self.rangeDays : rangeDays // ignore: cast_nullable_to_non_nullable
|
||||
as int,bestDay: freezed == bestDay ? _self.bestDay : bestDay // ignore: cast_nullable_to_non_nullable
|
||||
as DailySteps?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of StepsStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$DailyStepsCopyWith<$Res>? get bestDay {
|
||||
if (_self.bestDay == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $DailyStepsCopyWith<$Res>(_self.bestDay!, (value) {
|
||||
return _then(_self.copyWith(bestDay: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -412,10 +686,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int avgPerDay, int total, int bestDaySteps)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _StepsStats() when $default != null:
|
||||
return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
|
||||
return $default(_that.total,_that.avgPerDay,_that.activeDays,_that.rangeDays,_that.bestDay);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -433,10 +707,10 @@ return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int avgPerDay, int total, int bestDaySteps) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _StepsStats():
|
||||
return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
|
||||
return $default(_that.total,_that.avgPerDay,_that.activeDays,_that.rangeDays,_that.bestDay);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -453,10 +727,10 @@ return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int avgPerDay, int total, int bestDaySteps)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _StepsStats() when $default != null:
|
||||
return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
|
||||
return $default(_that.total,_that.avgPerDay,_that.activeDays,_that.rangeDays,_that.bestDay);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -468,12 +742,14 @@ return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
|
||||
|
||||
|
||||
class _StepsStats implements StepsStats {
|
||||
const _StepsStats({this.avgPerDay = 0, this.total = 0, this.bestDaySteps = 0});
|
||||
const _StepsStats({this.total = 0, this.avgPerDay = 0, this.activeDays = 0, this.rangeDays = 0, this.bestDay});
|
||||
|
||||
|
||||
@override@JsonKey() final int avgPerDay;
|
||||
@override@JsonKey() final int total;
|
||||
@override@JsonKey() final int bestDaySteps;
|
||||
@override@JsonKey() final int avgPerDay;
|
||||
@override@JsonKey() final int activeDays;
|
||||
@override@JsonKey() final int rangeDays;
|
||||
@override final DailySteps? bestDay;
|
||||
|
||||
/// Create a copy of StepsStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -485,16 +761,16 @@ _$StepsStatsCopyWith<_StepsStats> get copyWith => __$StepsStatsCopyWithImpl<_Ste
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _StepsStats&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.total, total) || other.total == total)&&(identical(other.bestDaySteps, bestDaySteps) || other.bestDaySteps == bestDaySteps));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _StepsStats&&(identical(other.total, total) || other.total == total)&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.activeDays, activeDays) || other.activeDays == activeDays)&&(identical(other.rangeDays, rangeDays) || other.rangeDays == rangeDays)&&(identical(other.bestDay, bestDay) || other.bestDay == bestDay));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,avgPerDay,total,bestDaySteps);
|
||||
int get hashCode => Object.hash(runtimeType,total,avgPerDay,activeDays,rangeDays,bestDay);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StepsStats(avgPerDay: $avgPerDay, total: $total, bestDaySteps: $bestDaySteps)';
|
||||
return 'StepsStats(total: $total, avgPerDay: $avgPerDay, activeDays: $activeDays, rangeDays: $rangeDays, bestDay: $bestDay)';
|
||||
}
|
||||
|
||||
|
||||
@@ -505,11 +781,11 @@ abstract mixin class _$StepsStatsCopyWith<$Res> implements $StepsStatsCopyWith<$
|
||||
factory _$StepsStatsCopyWith(_StepsStats value, $Res Function(_StepsStats) _then) = __$StepsStatsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int avgPerDay, int total, int bestDaySteps
|
||||
int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay
|
||||
});
|
||||
|
||||
|
||||
|
||||
@override $DailyStepsCopyWith<$Res>? get bestDay;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -522,22 +798,36 @@ class __$StepsStatsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of StepsStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? avgPerDay = null,Object? total = null,Object? bestDaySteps = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? total = null,Object? avgPerDay = null,Object? activeDays = null,Object? rangeDays = null,Object? bestDay = freezed,}) {
|
||||
return _then(_StepsStats(
|
||||
avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int,bestDaySteps: null == bestDaySteps ? _self.bestDaySteps : bestDaySteps // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int,avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
|
||||
as int,activeDays: null == activeDays ? _self.activeDays : activeDays // ignore: cast_nullable_to_non_nullable
|
||||
as int,rangeDays: null == rangeDays ? _self.rangeDays : rangeDays // ignore: cast_nullable_to_non_nullable
|
||||
as int,bestDay: freezed == bestDay ? _self.bestDay : bestDay // ignore: cast_nullable_to_non_nullable
|
||||
as DailySteps?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of StepsStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$DailyStepsCopyWith<$Res>? get bestDay {
|
||||
if (_self.bestDay == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $DailyStepsCopyWith<$Res>(_self.bestDay!, (value) {
|
||||
return _then(_self.copyWith(bestDay: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ActivityMeterViewState {
|
||||
|
||||
int get todayTotal; int get dailyGoal; List<DailySteps> get chartData; List<DailySteps> get historyData; int get currentHistoryPage; bool get hasMoreHistory; StepsStats get stats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; bool get isLoading; bool get isLoadingMore; ActivityMeterErrorEvent? get errorEvent;
|
||||
int get todayTotal; int get dailyGoal; List<DailySteps> get chartData; List<HourlySteps> get hourlyData; int get activeHoursToday; List<DailySteps> get historyData; int get currentHistoryPage; bool get hasMoreHistory; StepsStats get stats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; DateTime? get rangeStart; DateTime? get rangeEnd; bool get isLoading; bool get isLoadingMore; ActivityMeterErrorEvent? get errorEvent;
|
||||
/// Create a copy of ActivityMeterViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -548,16 +838,16 @@ $ActivityMeterViewStateCopyWith<ActivityMeterViewState> get copyWith => _$Activi
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other.chartData, chartData)&&const DeepCollectionEquality().equals(other.historyData, historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other.chartData, chartData)&&const DeepCollectionEquality().equals(other.hourlyData, hourlyData)&&(identical(other.activeHoursToday, activeHoursToday) || other.activeHoursToday == activeHoursToday)&&const DeepCollectionEquality().equals(other.historyData, historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.rangeStart, rangeStart) || other.rangeStart == rangeStart)&&(identical(other.rangeEnd, rangeEnd) || other.rangeEnd == rangeEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(chartData),const DeepCollectionEquality().hash(historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorEvent);
|
||||
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(chartData),const DeepCollectionEquality().hash(hourlyData),activeHoursToday,const DeepCollectionEquality().hash(historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,rangeStart,rangeEnd,isLoading,isLoadingMore,errorEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
|
||||
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, hourlyData: $hourlyData, activeHoursToday: $activeHoursToday, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, rangeStart: $rangeStart, rangeEnd: $rangeEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
|
||||
}
|
||||
|
||||
|
||||
@@ -568,7 +858,7 @@ abstract mixin class $ActivityMeterViewStateCopyWith<$Res> {
|
||||
factory $ActivityMeterViewStateCopyWith(ActivityMeterViewState value, $Res Function(ActivityMeterViewState) _then) = _$ActivityMeterViewStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
|
||||
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
|
||||
});
|
||||
|
||||
|
||||
@@ -585,18 +875,22 @@ class _$ActivityMeterViewStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of ActivityMeterViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? hourlyData = null,Object? activeHoursToday = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? rangeStart = freezed,Object? rangeEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
todayTotal: null == todayTotal ? _self.todayTotal : todayTotal // ignore: cast_nullable_to_non_nullable
|
||||
as int,dailyGoal: null == dailyGoal ? _self.dailyGoal : dailyGoal // ignore: cast_nullable_to_non_nullable
|
||||
as int,chartData: null == chartData ? _self.chartData : chartData // ignore: cast_nullable_to_non_nullable
|
||||
as List<DailySteps>,historyData: null == historyData ? _self.historyData : historyData // ignore: cast_nullable_to_non_nullable
|
||||
as List<DailySteps>,hourlyData: null == hourlyData ? _self.hourlyData : hourlyData // ignore: cast_nullable_to_non_nullable
|
||||
as List<HourlySteps>,activeHoursToday: null == activeHoursToday ? _self.activeHoursToday : activeHoursToday // ignore: cast_nullable_to_non_nullable
|
||||
as int,historyData: null == historyData ? _self.historyData : historyData // ignore: cast_nullable_to_non_nullable
|
||||
as List<DailySteps>,currentHistoryPage: null == currentHistoryPage ? _self.currentHistoryPage : currentHistoryPage // ignore: cast_nullable_to_non_nullable
|
||||
as int,hasMoreHistory: null == hasMoreHistory ? _self.hasMoreHistory : hasMoreHistory // ignore: cast_nullable_to_non_nullable
|
||||
as bool,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
|
||||
as StepsStats,timeRange: null == timeRange ? _self.timeRange : timeRange // ignore: cast_nullable_to_non_nullable
|
||||
as TimeRange,customStart: freezed == customStart ? _self.customStart : customStart // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,rangeStart: freezed == rangeStart ? _self.rangeStart : rangeStart // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,rangeEnd: freezed == rangeEnd ? _self.rangeEnd : rangeEnd // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
@@ -694,10 +988,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActivityMeterViewState() when $default != null:
|
||||
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
|
||||
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.hourlyData,_that.activeHoursToday,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.rangeStart,_that.rangeEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -715,10 +1009,10 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActivityMeterViewState():
|
||||
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
|
||||
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.hourlyData,_that.activeHoursToday,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.rangeStart,_that.rangeEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -735,10 +1029,10 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActivityMeterViewState() when $default != null:
|
||||
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
|
||||
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.hourlyData,_that.activeHoursToday,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.rangeStart,_that.rangeEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -750,7 +1044,7 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
|
||||
|
||||
|
||||
class _ActivityMeterViewState implements ActivityMeterViewState {
|
||||
const _ActivityMeterViewState({this.todayTotal = 0, this.dailyGoal = 8000, final List<DailySteps> chartData = const [], final List<DailySteps> historyData = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.stats = const StepsStats(), this.timeRange = TimeRange.today, this.customStart, this.customEnd, this.isLoading = true, this.isLoadingMore = false, this.errorEvent}): _chartData = chartData,_historyData = historyData;
|
||||
const _ActivityMeterViewState({this.todayTotal = 0, this.dailyGoal = 8000, final List<DailySteps> chartData = const [], final List<HourlySteps> hourlyData = const [], this.activeHoursToday = 0, final List<DailySteps> historyData = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.stats = const StepsStats(), this.timeRange = TimeRange.today, this.customStart, this.customEnd, this.rangeStart, this.rangeEnd, this.isLoading = true, this.isLoadingMore = false, this.errorEvent}): _chartData = chartData,_hourlyData = hourlyData,_historyData = historyData;
|
||||
|
||||
|
||||
@override@JsonKey() final int todayTotal;
|
||||
@@ -762,6 +1056,14 @@ class _ActivityMeterViewState implements ActivityMeterViewState {
|
||||
return EqualUnmodifiableListView(_chartData);
|
||||
}
|
||||
|
||||
final List<HourlySteps> _hourlyData;
|
||||
@override@JsonKey() List<HourlySteps> get hourlyData {
|
||||
if (_hourlyData is EqualUnmodifiableListView) return _hourlyData;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_hourlyData);
|
||||
}
|
||||
|
||||
@override@JsonKey() final int activeHoursToday;
|
||||
final List<DailySteps> _historyData;
|
||||
@override@JsonKey() List<DailySteps> get historyData {
|
||||
if (_historyData is EqualUnmodifiableListView) return _historyData;
|
||||
@@ -775,6 +1077,8 @@ class _ActivityMeterViewState implements ActivityMeterViewState {
|
||||
@override@JsonKey() final TimeRange timeRange;
|
||||
@override final DateTime? customStart;
|
||||
@override final DateTime? customEnd;
|
||||
@override final DateTime? rangeStart;
|
||||
@override final DateTime? rangeEnd;
|
||||
@override@JsonKey() final bool isLoading;
|
||||
@override@JsonKey() final bool isLoadingMore;
|
||||
@override final ActivityMeterErrorEvent? errorEvent;
|
||||
@@ -789,16 +1093,16 @@ _$ActivityMeterViewStateCopyWith<_ActivityMeterViewState> get copyWith => __$Act
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other._chartData, _chartData)&&const DeepCollectionEquality().equals(other._historyData, _historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other._chartData, _chartData)&&const DeepCollectionEquality().equals(other._hourlyData, _hourlyData)&&(identical(other.activeHoursToday, activeHoursToday) || other.activeHoursToday == activeHoursToday)&&const DeepCollectionEquality().equals(other._historyData, _historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.rangeStart, rangeStart) || other.rangeStart == rangeStart)&&(identical(other.rangeEnd, rangeEnd) || other.rangeEnd == rangeEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(_chartData),const DeepCollectionEquality().hash(_historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorEvent);
|
||||
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(_chartData),const DeepCollectionEquality().hash(_hourlyData),activeHoursToday,const DeepCollectionEquality().hash(_historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,rangeStart,rangeEnd,isLoading,isLoadingMore,errorEvent);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
|
||||
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, hourlyData: $hourlyData, activeHoursToday: $activeHoursToday, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, rangeStart: $rangeStart, rangeEnd: $rangeEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
|
||||
}
|
||||
|
||||
|
||||
@@ -809,7 +1113,7 @@ abstract mixin class _$ActivityMeterViewStateCopyWith<$Res> implements $Activity
|
||||
factory _$ActivityMeterViewStateCopyWith(_ActivityMeterViewState value, $Res Function(_ActivityMeterViewState) _then) = __$ActivityMeterViewStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
|
||||
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
|
||||
});
|
||||
|
||||
|
||||
@@ -826,18 +1130,22 @@ class __$ActivityMeterViewStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of ActivityMeterViewState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? hourlyData = null,Object? activeHoursToday = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? rangeStart = freezed,Object? rangeEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
|
||||
return _then(_ActivityMeterViewState(
|
||||
todayTotal: null == todayTotal ? _self.todayTotal : todayTotal // ignore: cast_nullable_to_non_nullable
|
||||
as int,dailyGoal: null == dailyGoal ? _self.dailyGoal : dailyGoal // ignore: cast_nullable_to_non_nullable
|
||||
as int,chartData: null == chartData ? _self._chartData : chartData // ignore: cast_nullable_to_non_nullable
|
||||
as List<DailySteps>,historyData: null == historyData ? _self._historyData : historyData // ignore: cast_nullable_to_non_nullable
|
||||
as List<DailySteps>,hourlyData: null == hourlyData ? _self._hourlyData : hourlyData // ignore: cast_nullable_to_non_nullable
|
||||
as List<HourlySteps>,activeHoursToday: null == activeHoursToday ? _self.activeHoursToday : activeHoursToday // ignore: cast_nullable_to_non_nullable
|
||||
as int,historyData: null == historyData ? _self._historyData : historyData // ignore: cast_nullable_to_non_nullable
|
||||
as List<DailySteps>,currentHistoryPage: null == currentHistoryPage ? _self.currentHistoryPage : currentHistoryPage // ignore: cast_nullable_to_non_nullable
|
||||
as int,hasMoreHistory: null == hasMoreHistory ? _self.hasMoreHistory : hasMoreHistory // ignore: cast_nullable_to_non_nullable
|
||||
as bool,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
|
||||
as StepsStats,timeRange: null == timeRange ? _self.timeRange : timeRange // ignore: cast_nullable_to_non_nullable
|
||||
as TimeRange,customStart: freezed == customStart ? _self.customStart : customStart // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,rangeStart: freezed == rangeStart ? _self.rangeStart : rangeStart // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,rangeEnd: freezed == rangeEnd ? _self.rangeEnd : rangeEnd // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class ActivityBarChartBase extends ConsumerWidget {
|
||||
final List<BarChartGroupData> barGroups;
|
||||
final AxisTitles bottomTitles;
|
||||
final BarTouchTooltipData tooltip;
|
||||
final double maxY;
|
||||
final bool isEmpty;
|
||||
|
||||
const ActivityBarChartBase({
|
||||
super.key,
|
||||
required this.barGroups,
|
||||
required this.bottomTitles,
|
||||
required this.tooltip,
|
||||
required this.maxY,
|
||||
required this.isEmpty,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final height = SizeUtils.getByScreen<double>(small: 180, big: 160);
|
||||
|
||||
if (isEmpty) {
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'—',
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final labelColor = theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.4);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
maxY: maxY * 1.15,
|
||||
barGroups: barGroups,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: _computeInterval(maxY),
|
||||
getDrawingHorizontalLine: (_) => FlLine(
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.08),
|
||||
strokeWidth: 1,
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: SizeUtils.getByScreen(small: 36, big: 32),
|
||||
getTitlesWidget: (value, _) => Text(
|
||||
_formatAxis(value),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
|
||||
color: labelColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: bottomTitles,
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barTouchData: BarTouchData(touchTooltipData: tooltip),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static double _computeInterval(double maxY) {
|
||||
if (maxY <= 0) return 1;
|
||||
return (maxY / 4).ceilToDouble();
|
||||
}
|
||||
|
||||
static String _formatAxis(double value) {
|
||||
if (value >= 1000) return '${(value / 1000).toStringAsFixed(0)}k';
|
||||
return value.toInt().toString();
|
||||
}
|
||||
|
||||
static BarChartRodData buildRod({
|
||||
required double toY,
|
||||
required Color color,
|
||||
required double width,
|
||||
}) => BarChartRodData(
|
||||
toY: toY,
|
||||
color: color,
|
||||
width: width,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(SizeUtils.getByScreen(small: 4, big: 3)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../../../../core/presentation/format_date.dart';
|
||||
import '../format_steps.dart';
|
||||
import '../state/activity_meter_view_state.dart';
|
||||
import 'period_stats_cards.dart';
|
||||
|
||||
class TodayActivityFooter extends ConsumerWidget {
|
||||
final int activeHours;
|
||||
|
||||
const TodayActivityFooter({super.key, required this.activeHours});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 4, big: 3),
|
||||
),
|
||||
child: Text(
|
||||
context
|
||||
.translate(I18n.activityMeterActiveHoursToday)
|
||||
.replaceAll('{hours}', activeHours.toString()),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PeriodActivityFooter extends ConsumerWidget {
|
||||
final StepsStats stats;
|
||||
|
||||
const PeriodActivityFooter({super.key, required this.stats});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final best = stats.bestDay;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
PeriodStatsCards(stats: stats),
|
||||
if (best != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 4, big: 3),
|
||||
),
|
||||
child: Text(
|
||||
context
|
||||
.translate(I18n.activityMeterBestDay)
|
||||
.replaceAll('{date}', formatDayHeader(context, best.date))
|
||||
.replaceAll('{steps}', formatStepsNumber(best.totalSteps)),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../state/activity_meter_view_state.dart';
|
||||
import 'activity_bar_chart_base.dart';
|
||||
|
||||
class HourlyBarChart extends ConsumerWidget {
|
||||
final List<HourlySteps> data;
|
||||
|
||||
const HourlyBarChart({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final isEmpty = data.isEmpty || data.every((h) => h.totalSteps == 0);
|
||||
final maxY = isEmpty
|
||||
? 0.0
|
||||
: data.map((h) => h.totalSteps.toDouble()).reduce((a, b) => a > b ? a : b);
|
||||
|
||||
final labelColor = theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.4);
|
||||
|
||||
return ActivityBarChartBase(
|
||||
isEmpty: isEmpty,
|
||||
maxY: maxY,
|
||||
barGroups: data
|
||||
.map(
|
||||
(bucket) => BarChartGroupData(
|
||||
x: bucket.hour,
|
||||
barRods: [
|
||||
ActivityBarChartBase.buildRod(
|
||||
toY: bucket.totalSteps.toDouble(),
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
width: SizeUtils.getByScreen(small: 6, big: 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
interval: 3,
|
||||
getTitlesWidget: (value, _) {
|
||||
final hour = value.toInt();
|
||||
if (hour < 0 || hour > 23 || hour % 6 != 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
'${hour.toString().padLeft(2, '0')}h',
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 9, big: 8),
|
||||
color: labelColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
tooltip: BarTouchTooltipData(
|
||||
getTooltipColor: (_) =>
|
||||
theme.getColorFor(ThemeCode.backgroundSecondary),
|
||||
getTooltipItem: (group, _, rod, __) => BarTooltipItem(
|
||||
'${group.x.toString().padLeft(2, '0')}h\n${rod.toY.toInt()}',
|
||||
TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.getColorFor(ThemeCode.textPrimary),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:legacy_shared/legacy_shared.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../state/activity_meter_view_model.dart';
|
||||
|
||||
class PedometerToggle extends ConsumerWidget {
|
||||
const PedometerToggle({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final enabled = ref.watch(
|
||||
selectedDeviceProvider.select(
|
||||
(d) => d.value?.settings.pedometer ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.translate(I18n.activityMeterPedometer),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 14, big: 15),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: enabled,
|
||||
activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
onChanged: (value) async {
|
||||
final success = await ref
|
||||
.read(activityMeterViewModelProvider.notifier)
|
||||
.togglePedometer(enabled: value);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(
|
||||
value
|
||||
? I18n.activityMeterPedometerEnabled
|
||||
: I18n.activityMeterPedometerDisabled,
|
||||
),
|
||||
type: MessageType.success,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../format_steps.dart';
|
||||
import '../state/activity_meter_view_state.dart';
|
||||
|
||||
class PeriodStatsCards extends StatelessWidget {
|
||||
final StepsStats stats;
|
||||
|
||||
const PeriodStatsCards({super.key, required this.stats});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasData = stats.total > 0;
|
||||
final gap = SizeUtils.getByScreen<double>(small: 8, big: 6);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
label: context.translate(I18n.activityMeterTotalSteps),
|
||||
value: hasData ? formatStepsNumber(stats.total) : '—',
|
||||
unit: context.translate(I18n.unitSteps),
|
||||
),
|
||||
),
|
||||
SizedBox(width: gap),
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
label: context.translate(I18n.activityMeterAverageDaily),
|
||||
value: hasData ? formatStepsNumber(stats.avgPerDay) : '—',
|
||||
unit: context.translate(I18n.unitStepsPerDay),
|
||||
),
|
||||
),
|
||||
SizedBox(width: gap),
|
||||
Expanded(
|
||||
child: _StatCard(
|
||||
label: context.translate(I18n.activityMeterActiveDays),
|
||||
value: stats.rangeDays > 0
|
||||
? '${stats.activeDays}/${stats.rangeDays}'
|
||||
: '—',
|
||||
unit: context.translate(I18n.unitDays),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatCard extends ConsumerWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final String unit;
|
||||
|
||||
const _StatCard({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.unit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 10, big: 8),
|
||||
vertical: SizeUtils.getByScreen(small: 12, big: 10),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.getColorFor(ThemeCode.backgroundSecondary),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(SizeUtils.getByScreen(small: 12, big: 10)),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 6, big: 4)),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 18, big: 16),
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.getColorFor(ThemeCode.textPrimary),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 2, big: 1)),
|
||||
Text(
|
||||
unit,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
class SectionHeader extends ConsumerWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
|
||||
const SectionHeader({super.key, required this.title, this.subtitle});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
right: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
top: SizeUtils.getByScreen(small: 20, big: 18),
|
||||
bottom: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.2,
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 4, big: 3)),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,156 +1,82 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../state/activity_meter_view_state.dart';
|
||||
import 'activity_bar_chart_base.dart';
|
||||
|
||||
class StepsBarChart extends StatelessWidget {
|
||||
class StepsBarChart extends ConsumerWidget {
|
||||
final List<DailySteps> data;
|
||||
final ThemePort theme;
|
||||
|
||||
const StepsBarChart({super.key, required this.data, required this.theme});
|
||||
const StepsBarChart({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (data.isEmpty) {
|
||||
return SizedBox(
|
||||
height: SizeUtils.getByScreen(small: 180, big: 160),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'--',
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.3),
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themePortProvider);
|
||||
final isEmpty = data.isEmpty;
|
||||
final maxY = isEmpty
|
||||
? 0.0
|
||||
: data.map((d) => d.totalSteps.toDouble()).reduce((a, b) => a > b ? a : b);
|
||||
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final labelColor = theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.4);
|
||||
|
||||
return ActivityBarChartBase(
|
||||
isEmpty: isEmpty,
|
||||
maxY: maxY,
|
||||
barGroups: data.asMap().entries.map((entry) {
|
||||
return BarChartGroupData(
|
||||
x: entry.key,
|
||||
barRods: [
|
||||
ActivityBarChartBase.buildRod(
|
||||
toY: entry.value.totalSteps.toDouble(),
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
width: _barWidth(data.length),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: data.length <= 14,
|
||||
getTitlesWidget: (value, _) {
|
||||
final index = value.toInt();
|
||||
if (index < 0 || index >= data.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
DateFormat('d MMM', locale).format(data[index].date),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 9, big: 8),
|
||||
color: labelColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final maxY = data
|
||||
.map((d) => d.totalSteps.toDouble())
|
||||
.reduce((a, b) => a > b ? a : b);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: SizeUtils.getByScreen(small: 180, big: 160),
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
maxY: maxY * 1.15,
|
||||
barGroups: data.asMap().entries.map((entry) {
|
||||
return BarChartGroupData(
|
||||
x: entry.key,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: entry.value.totalSteps.toDouble(),
|
||||
color: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
width: _barWidth,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(
|
||||
SizeUtils.getByScreen(small: 4, big: 3),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: _computeInterval(maxY),
|
||||
getDrawingHorizontalLine: (value) => FlLine(
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.08),
|
||||
strokeWidth: 1,
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: SizeUtils.getByScreen(small: 36, big: 32),
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
_formatAxis(value),
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: data.length <= 14,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index < 0 || index >= data.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final date = data[index].date;
|
||||
return Text(
|
||||
'${date.day}/${date.month}',
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 9, big: 8),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.4),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barTouchData: BarTouchData(
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipColor: (_) =>
|
||||
theme.getColorFor(ThemeCode.backgroundSecondary),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
return BarTooltipItem(
|
||||
rod.toY.toInt().toString(),
|
||||
TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.getColorFor(ThemeCode.textPrimary),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
tooltip: BarTouchTooltipData(
|
||||
getTooltipColor: (_) =>
|
||||
theme.getColorFor(ThemeCode.backgroundSecondary),
|
||||
getTooltipItem: (group, _, rod, __) => BarTooltipItem(
|
||||
'${DateFormat('d MMM y', locale).format(data[group.x].date)}\n${rod.toY.toInt()}',
|
||||
TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.getColorFor(ThemeCode.textPrimary),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double get _barWidth {
|
||||
if (data.length <= 7) return SizeUtils.getByScreen(small: 24, big: 20);
|
||||
if (data.length <= 14) return SizeUtils.getByScreen(small: 14, big: 12);
|
||||
double _barWidth(int length) {
|
||||
if (length <= 7) return SizeUtils.getByScreen(small: 24, big: 20);
|
||||
if (length <= 14) return SizeUtils.getByScreen(small: 14, big: 12);
|
||||
return SizeUtils.getByScreen(small: 8, big: 6);
|
||||
}
|
||||
|
||||
double _computeInterval(double maxY) {
|
||||
if (maxY <= 0) return 1;
|
||||
return (maxY / 4).ceilToDouble();
|
||||
}
|
||||
|
||||
String _formatAxis(double value) {
|
||||
if (value >= 1000) return '${(value / 1000).toStringAsFixed(0)}k';
|
||||
return value.toInt().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sf_localizations/sf_localizations.dart';
|
||||
import 'package:utils/utils.dart';
|
||||
|
||||
import '../format_steps.dart';
|
||||
import '../state/activity_meter_view_state.dart';
|
||||
|
||||
class StepsStatsRow extends StatelessWidget {
|
||||
final StepsStats stats;
|
||||
final ThemePort theme;
|
||||
|
||||
const StepsStatsRow({super.key, required this.stats, required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasData = stats.total > 0;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
|
||||
vertical: SizeUtils.getByScreen(small: 8, big: 6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _StatItem(
|
||||
label: context.translate(I18n.average),
|
||||
value: hasData ? formatStepsNumber(stats.avgPerDay) : '--',
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _StatItem(
|
||||
label: context.translate(I18n.totalSteps),
|
||||
value: hasData ? formatStepsNumber(stats.total) : '--',
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _StatItem(
|
||||
label: context.translate(I18n.bestDay),
|
||||
value: hasData ? formatStepsNumber(stats.bestDaySteps) : '--',
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatItem extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final ThemePort theme;
|
||||
|
||||
const _StatItem({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 11, big: 10),
|
||||
color: theme
|
||||
.getColorFor(ThemeCode.textPrimary)
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 4, big: 2)),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: SizeUtils.getByScreen(small: 20, big: 18),
|
||||
fontWeight: FontWeight.w700,
|
||||
color: theme.getColorFor(ThemeCode.textPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ dependencies:
|
||||
uuid: ^4.5.3
|
||||
flutter_contacts: ^1.1.9+2
|
||||
fl_chart: ^1.1.1
|
||||
intl: ^0.20.2
|
||||
lottie: ^3.3.1
|
||||
image_picker: ^1.2.1
|
||||
|
||||
|
||||
@@ -868,5 +868,19 @@
|
||||
"openSettings": "Einstellungen öffnen",
|
||||
"errorMessageCodeIsEmpty": "Der Code darf nicht leer sein",
|
||||
"callWatchSubtitle": "Gib die Telefonnummer des Geräts ein, das du anrufen möchtest.",
|
||||
"spyCallSubtitle": "Das Gerät ruft diese Nummer an. Gib deine Telefonnummer ein, um den Anruf von der Uhr zu erhalten."
|
||||
"spyCallSubtitle": "Das Gerät ruft diese Nummer an. Gib deine Telefonnummer ein, um den Anruf von der Uhr zu erhalten.",
|
||||
"activityMeterSectionToday": "Heute",
|
||||
"activityMeterSectionActivity": "Aktivität",
|
||||
"activityMeterSectionHistory": "Verlauf",
|
||||
"activityMeterDailyGoal": "Tagesziel: {goal} ({percent}%)",
|
||||
"activityMeterAverageDaily": "Tagesdurchschnitt",
|
||||
"activityMeterTotalSteps": "Gesamt",
|
||||
"activityMeterActiveDays": "Aktive Tage",
|
||||
"activityMeterActiveHoursToday": "Aktive Stunden heute: {hours} von 24",
|
||||
"activityMeterBestDay": "Bester Tag: {date} ({steps} Schritte)",
|
||||
"activityMeterRangeLabelToday": "Heute, {date}",
|
||||
"activityMeterNoStepsToday": "Heute noch keine Schritte erfasst",
|
||||
"activityMeterNoStepsPeriod": "Keine Aktivität in diesem Zeitraum",
|
||||
"unitStepsPerDay": "Schritte/Tag",
|
||||
"unitDays": "Tage"
|
||||
}
|
||||
|
||||
@@ -868,5 +868,19 @@
|
||||
"openSettings": "Open settings",
|
||||
"errorMessageCodeIsEmpty": "The code cannot be empty",
|
||||
"callWatchSubtitle": "Enter the phone number of the device you want to call.",
|
||||
"spyCallSubtitle": "The device will call this number. Enter your phone to receive the call from the watch."
|
||||
"spyCallSubtitle": "The device will call this number. Enter your phone to receive the call from the watch.",
|
||||
"activityMeterSectionToday": "Today",
|
||||
"activityMeterSectionActivity": "Activity",
|
||||
"activityMeterSectionHistory": "History",
|
||||
"activityMeterDailyGoal": "Daily goal: {goal} ({percent}%)",
|
||||
"activityMeterAverageDaily": "Daily average",
|
||||
"activityMeterTotalSteps": "Total",
|
||||
"activityMeterActiveDays": "Active days",
|
||||
"activityMeterActiveHoursToday": "Active hours today: {hours} of 24",
|
||||
"activityMeterBestDay": "Best day: {date} ({steps} steps)",
|
||||
"activityMeterRangeLabelToday": "Today, {date}",
|
||||
"activityMeterNoStepsToday": "No steps recorded yet today",
|
||||
"activityMeterNoStepsPeriod": "No activity recorded in this period",
|
||||
"unitStepsPerDay": "steps/day",
|
||||
"unitDays": "days"
|
||||
}
|
||||
|
||||
@@ -868,5 +868,19 @@
|
||||
"openSettings": "Abrir ajustes",
|
||||
"errorMessageCodeIsEmpty": "El código no puede estar vacío",
|
||||
"callWatchSubtitle": "Introduce el número de teléfono del dispositivo al que quieres llamar.",
|
||||
"spyCallSubtitle": "El dispositivo llamará a este número. Introduce tu teléfono para recibir la llamada desde el reloj."
|
||||
"spyCallSubtitle": "El dispositivo llamará a este número. Introduce tu teléfono para recibir la llamada desde el reloj.",
|
||||
"activityMeterSectionToday": "Hoy",
|
||||
"activityMeterSectionActivity": "Actividad",
|
||||
"activityMeterSectionHistory": "Historial",
|
||||
"activityMeterDailyGoal": "Meta diaria: {goal} ({percent}%)",
|
||||
"activityMeterAverageDaily": "Promedio diario",
|
||||
"activityMeterTotalSteps": "Total",
|
||||
"activityMeterActiveDays": "Días activos",
|
||||
"activityMeterActiveHoursToday": "Horas activas hoy: {hours} de 24",
|
||||
"activityMeterBestDay": "Mejor día: {date} ({steps} pasos)",
|
||||
"activityMeterRangeLabelToday": "Hoy, {date}",
|
||||
"activityMeterNoStepsToday": "Aún no se registraron pasos hoy",
|
||||
"activityMeterNoStepsPeriod": "No hay actividad registrada en este período",
|
||||
"unitStepsPerDay": "pasos/día",
|
||||
"unitDays": "días"
|
||||
}
|
||||
|
||||
@@ -868,5 +868,19 @@
|
||||
"openSettings": "Ouvrir les paramètres",
|
||||
"errorMessageCodeIsEmpty": "Le code ne peut pas être vide",
|
||||
"callWatchSubtitle": "Saisis le numéro de téléphone de l'appareil que tu veux appeler.",
|
||||
"spyCallSubtitle": "L'appareil appellera ce numéro. Saisis ton téléphone pour recevoir l'appel de la montre."
|
||||
"spyCallSubtitle": "L'appareil appellera ce numéro. Saisis ton téléphone pour recevoir l'appel de la montre.",
|
||||
"activityMeterSectionToday": "Aujourd'hui",
|
||||
"activityMeterSectionActivity": "Activité",
|
||||
"activityMeterSectionHistory": "Historique",
|
||||
"activityMeterDailyGoal": "Objectif quotidien : {goal} ({percent}%)",
|
||||
"activityMeterAverageDaily": "Moyenne quotidienne",
|
||||
"activityMeterTotalSteps": "Total",
|
||||
"activityMeterActiveDays": "Jours actifs",
|
||||
"activityMeterActiveHoursToday": "Heures actives aujourd'hui : {hours} sur 24",
|
||||
"activityMeterBestDay": "Meilleur jour : {date} ({steps} pas)",
|
||||
"activityMeterRangeLabelToday": "Aujourd'hui, {date}",
|
||||
"activityMeterNoStepsToday": "Aucun pas enregistré aujourd'hui",
|
||||
"activityMeterNoStepsPeriod": "Aucune activité enregistrée sur cette période",
|
||||
"unitStepsPerDay": "pas/jour",
|
||||
"unitDays": "jours"
|
||||
}
|
||||
|
||||
@@ -868,5 +868,19 @@
|
||||
"openSettings": "Apri impostazioni",
|
||||
"errorMessageCodeIsEmpty": "Il codice non può essere vuoto",
|
||||
"callWatchSubtitle": "Inserisci il numero di telefono del dispositivo che vuoi chiamare.",
|
||||
"spyCallSubtitle": "Il dispositivo chiamerà questo numero. Inserisci il tuo telefono per ricevere la chiamata dall'orologio."
|
||||
"spyCallSubtitle": "Il dispositivo chiamerà questo numero. Inserisci il tuo telefono per ricevere la chiamata dall'orologio.",
|
||||
"activityMeterSectionToday": "Oggi",
|
||||
"activityMeterSectionActivity": "Attività",
|
||||
"activityMeterSectionHistory": "Cronologia",
|
||||
"activityMeterDailyGoal": "Obiettivo giornaliero: {goal} ({percent}%)",
|
||||
"activityMeterAverageDaily": "Media giornaliera",
|
||||
"activityMeterTotalSteps": "Totale",
|
||||
"activityMeterActiveDays": "Giorni attivi",
|
||||
"activityMeterActiveHoursToday": "Ore attive oggi: {hours} di 24",
|
||||
"activityMeterBestDay": "Giorno migliore: {date} ({steps} passi)",
|
||||
"activityMeterRangeLabelToday": "Oggi, {date}",
|
||||
"activityMeterNoStepsToday": "Nessun passo registrato oggi",
|
||||
"activityMeterNoStepsPeriod": "Nessuna attività registrata in questo periodo",
|
||||
"unitStepsPerDay": "passi/giorno",
|
||||
"unitDays": "giorni"
|
||||
}
|
||||
|
||||
@@ -868,5 +868,19 @@
|
||||
"openSettings": "Abrir definições",
|
||||
"errorMessageCodeIsEmpty": "O código não pode estar vazio",
|
||||
"callWatchSubtitle": "Digita o número de telefone do dispositivo que queres chamar.",
|
||||
"spyCallSubtitle": "O dispositivo ligará para este número. Digita o teu telefone para receber a chamada do relógio."
|
||||
"spyCallSubtitle": "O dispositivo ligará para este número. Digita o teu telefone para receber a chamada do relógio.",
|
||||
"activityMeterSectionToday": "Hoje",
|
||||
"activityMeterSectionActivity": "Atividade",
|
||||
"activityMeterSectionHistory": "Histórico",
|
||||
"activityMeterDailyGoal": "Meta diária: {goal} ({percent}%)",
|
||||
"activityMeterAverageDaily": "Média diária",
|
||||
"activityMeterTotalSteps": "Total",
|
||||
"activityMeterActiveDays": "Dias ativos",
|
||||
"activityMeterActiveHoursToday": "Horas ativas hoje: {hours} de 24",
|
||||
"activityMeterBestDay": "Melhor dia: {date} ({steps} passos)",
|
||||
"activityMeterRangeLabelToday": "Hoje, {date}",
|
||||
"activityMeterNoStepsToday": "Ainda não há passos registados hoje",
|
||||
"activityMeterNoStepsPeriod": "Sem atividade registada neste período",
|
||||
"unitStepsPerDay": "passos/dia",
|
||||
"unitDays": "dias"
|
||||
}
|
||||
|
||||
@@ -121,6 +121,20 @@ class I18n {
|
||||
static const String callOutgoing = 'callOutgoing';
|
||||
static const String callWatch = 'callWatch';
|
||||
static const String callWatchSubtitle = 'callWatchSubtitle';
|
||||
static const String activityMeterSectionToday = 'activityMeterSectionToday';
|
||||
static const String activityMeterSectionActivity = 'activityMeterSectionActivity';
|
||||
static const String activityMeterSectionHistory = 'activityMeterSectionHistory';
|
||||
static const String activityMeterDailyGoal = 'activityMeterDailyGoal';
|
||||
static const String activityMeterAverageDaily = 'activityMeterAverageDaily';
|
||||
static const String activityMeterTotalSteps = 'activityMeterTotalSteps';
|
||||
static const String activityMeterActiveDays = 'activityMeterActiveDays';
|
||||
static const String activityMeterActiveHoursToday = 'activityMeterActiveHoursToday';
|
||||
static const String activityMeterBestDay = 'activityMeterBestDay';
|
||||
static const String activityMeterRangeLabelToday = 'activityMeterRangeLabelToday';
|
||||
static const String activityMeterNoStepsToday = 'activityMeterNoStepsToday';
|
||||
static const String activityMeterNoStepsPeriod = 'activityMeterNoStepsPeriod';
|
||||
static const String unitStepsPerDay = 'unitStepsPerDay';
|
||||
static const String unitDays = 'unitDays';
|
||||
static const String spyCallSubtitle = 'spyCallSubtitle';
|
||||
static const String cancel = 'cancel';
|
||||
static const String cardPinChange = 'cardPinChange';
|
||||
|
||||
Reference in New Issue
Block a user