From c7e32d139910f96d123e1d9efcfa0d05edd3fefe Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Wed, 15 Apr 2026 21:51:08 +0200 Subject: [PATCH] refactor(activity-meter): redesign screen with honest per-range stats --- apps/mobile_app/lib/core/init_app.dart | 5 +- apps/mobile_app/pubspec.yaml | 1 + .../presentation/activity_meter_screen.dart | 347 +++++++++------ .../state/activity_meter_aggregator.dart | 111 +++++ .../state/activity_meter_view_model.dart | 183 +++----- .../state/activity_meter_view_state.dart | 22 +- .../activity_meter_view_state.freezed.dart | 408 +++++++++++++++--- .../widgets/activity_bar_chart_base.dart | 122 ++++++ .../widgets/activity_footers.dart | 75 ++++ .../widgets/hourly_bar_chart.dart | 77 ++++ .../widgets/pedometer_toggle.dart | 62 +++ .../widgets/period_stats_cards.dart | 121 ++++++ .../presentation/widgets/section_header.dart | 51 +++ .../presentation/widgets/steps_bar_chart.dart | 192 +++------ .../presentation/widgets/steps_stats_row.dart | 89 ---- .../modules/device_management/pubspec.yaml | 1 + packages/sf_localizations/assets/l10n/de.json | 16 +- packages/sf_localizations/assets/l10n/en.json | 16 +- packages/sf_localizations/assets/l10n/es.json | 16 +- packages/sf_localizations/assets/l10n/fr.json | 16 +- packages/sf_localizations/assets/l10n/it.json | 16 +- packages/sf_localizations/assets/l10n/pt.json | 16 +- .../lib/src/generated/i18n.dart | 14 + 23 files changed, 1440 insertions(+), 537 deletions(-) create mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_aggregator.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_bar_chart_base.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_footers.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/hourly_bar_chart.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/period_stats_cards.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/section_header.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_stats_row.dart diff --git a/apps/mobile_app/lib/core/init_app.dart b/apps/mobile_app/lib/core/init_app.dart index e598e643..84c630d4 100644 --- a/apps/mobile_app/lib/core/init_app.dart +++ b/apps/mobile_app/lib/core/init_app.dart @@ -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 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(); diff --git a/apps/mobile_app/pubspec.yaml b/apps/mobile_app/pubspec.yaml index 82952f97..fade1815 100644 --- a/apps/mobile_app/pubspec.yaml +++ b/apps/mobile_app/pubspec.yaml @@ -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 diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart index 32e63515..eb587661 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart @@ -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, + ), + ], + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_aggregator.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_aggregator.dart new file mode 100644 index 00000000..4cf8821a --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_aggregator.dart @@ -0,0 +1,111 @@ +import '../../../../core/domain/entities/steps_entity.dart'; +import '../../../../core/presentation/time_range.dart'; +import 'activity_meter_view_state.dart'; + +List groupByDay(List steps) { + final Map 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 groupByHour(List steps) { + final Map groups = {}; + + for (final step in steps) { + final date = DateTime.fromMillisecondsSinceEpoch(step.occurredAt); + groups[date.hour] = (groups[date.hour] ?? 0) + step.steps; + } + + return List.generate( + 24, + (h) => HourlySteps(hour: h, totalSteps: groups[h] ?? 0), + ); +} + +int countActiveHours(List steps) { + final Set 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 mergeHistory( + List existing, + List newItems, +) { + final Map 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 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); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart index 8fe5961e..0f22f305 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart @@ -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 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 selectTimeRange(TimeRange range) async { if (range == state.timeRange) return; @@ -77,7 +79,11 @@ class ActivityMeterViewModel extends Notifier { 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 { ); 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 { 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 { } } - Future _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 _loadFilteredData() async { final identificator = _identificator; if (identificator == null) { @@ -152,9 +122,13 @@ class ActivityMeterViewModel extends Notifier { } 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 { 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 []; + 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 { } } - List _groupByDay(List steps) { - final Map 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 _mergeHistory( - List existing, - List newItems, - ) { - final Map 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 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> _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? _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 togglePedometer({required bool enabled}) async { final device = ref.read(selectedDeviceProvider).value; @@ -290,7 +225,7 @@ class ActivityMeterViewModel extends Notifier { unawaited(_tracking.legacyDeviceActivityPedometerToggled(enabled)); return true; - } catch (e) { + } catch (_) { if (!ref.mounted) return false; state = state.copyWith(errorEvent: ActivityMeterErrorEvent.pedometer); return false; diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.dart index deb606f0..59706421 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.dart @@ -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 chartData, + @Default([]) List hourlyData, + @Default(0) int activeHoursToday, @Default([]) List 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; +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.freezed.dart index 03204c6b..d34cd7ee 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.freezed.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_state.freezed.dart @@ -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 get copyWith => _$HourlyStepsCopyWithImpl(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 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 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? 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 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 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? 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 get copyWith => _$StepsStatsCopyWithImpl 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 Function( int avgPerDay, int total, int bestDaySteps)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(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 Function( int avgPerDay, int total, int bestDaySteps) $default,) {final _that = this; +@optionalTypeArgs TResult when(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? Function( int avgPerDay, int total, int bestDaySteps)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(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 get chartData; List 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 get chartData; List get hourlyData; int get activeHoursToday; List 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 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 chartData, List historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent + int todayTotal, int dailyGoal, List chartData, List hourlyData, int activeHoursToday, List 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,historyData: null == historyData ? _self.historyData : historyData // ignore: cast_nullable_to_non_nullable +as List,hourlyData: null == hourlyData ? _self.hourlyData : hourlyData // ignore: cast_nullable_to_non_nullable +as List,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,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 Function( int todayTotal, int dailyGoal, List chartData, List 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 Function( int todayTotal, int dailyGoal, List chartData, List hourlyData, int activeHoursToday, List 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 Function( int todayTotal, int dailyGoal, List chartData, List 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 Function( int todayTotal, int dailyGoal, List chartData, List hourlyData, int activeHoursToday, List 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? Function( int todayTotal, int dailyGoal, List chartData, List 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? Function( int todayTotal, int dailyGoal, List chartData, List hourlyData, int activeHoursToday, List 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 chartData = const [], final List 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 chartData = const [], final List hourlyData = const [], this.activeHoursToday = 0, final List 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 _hourlyData; +@override@JsonKey() List get hourlyData { + if (_hourlyData is EqualUnmodifiableListView) return _hourlyData; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_hourlyData); +} + +@override@JsonKey() final int activeHoursToday; final List _historyData; @override@JsonKey() List 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 chartData, List historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent + int todayTotal, int dailyGoal, List chartData, List hourlyData, int activeHoursToday, List 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,historyData: null == historyData ? _self._historyData : historyData // ignore: cast_nullable_to_non_nullable +as List,hourlyData: null == hourlyData ? _self._hourlyData : hourlyData // ignore: cast_nullable_to_non_nullable +as List,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,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 diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_bar_chart_base.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_bar_chart_base.dart new file mode 100644 index 00000000..3a29abd9 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_bar_chart_base.dart @@ -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 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(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)), + ), + ); +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_footers.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_footers.dart new file mode 100644 index 00000000..b34bfd08 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/activity_footers.dart @@ -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), + ), + ), + ), + ], + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/hourly_bar_chart.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/hourly_bar_chart.dart new file mode 100644 index 00000000..c9c10bf8 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/hourly_bar_chart.dart @@ -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 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), + ), + ), + ), + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart new file mode 100644 index 00000000..4fc5a8e0 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart @@ -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, + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/period_stats_cards.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/period_stats_cards.dart new file mode 100644 index 00000000..9470fda1 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/period_stats_cards.dart @@ -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(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), + ), + ), + ], + ), + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/section_header.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/section_header.dart new file mode 100644 index 00000000..7434c4af --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/section_header.dart @@ -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), + ), + ), + ], + ], + ), + ); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_bar_chart.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_bar_chart.dart index 72daa79c..eb186c47 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_bar_chart.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_bar_chart.dart @@ -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 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(); - } } diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_stats_row.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_stats_row.dart deleted file mode 100644 index 4971bd64..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/steps_stats_row.dart +++ /dev/null @@ -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), - ), - ), - ], - ); - } -} diff --git a/modules/legacy/modules/device_management/pubspec.yaml b/modules/legacy/modules/device_management/pubspec.yaml index 25375956..4660f8f4 100644 --- a/modules/legacy/modules/device_management/pubspec.yaml +++ b/modules/legacy/modules/device_management/pubspec.yaml @@ -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 diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 72d91e10..05f51c81 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -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" } diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index d2268475..5ec5f6de 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -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" } diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index f3f46de2..ef1758d2 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -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" } diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index 0a5953e0..ee740af2 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -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" } diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index 6ec397a1..aa62404d 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -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" } diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 4bdf7946..1df78003 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -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" } diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 3fefeffb..1d776115 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -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';