refactor(activity-meter): redesign screen with honest per-range stats

This commit is contained in:
2026-04-15 21:51:08 +02:00
parent 4e21e8d698
commit c7e32d1399
23 changed files with 1440 additions and 537 deletions

View File

@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:design_system/design_system.dart';
import 'package:sca_treezor/sca_treezor.dart';
import 'package:sf_app_platform/config/env/environment_enum.dart';
@@ -19,14 +20,12 @@ import 'package:sf_tracking/sf_tracking.dart';
Future<void> initApp(EnvironmentEnum env) async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await initializeDateFormatting();
navigationModule();
scaTreezorModule();
themePackages();
// Order matters: Firebase → sfTracking (FirebaseTrackingClient touches
// FirebaseAnalytics.instance) → router (SaveFamilyApp wires sfTracking
// into SfRouterListener at construction time).
await setupFirebase(env);
await setupNotifications();
initSfTracking();

View File

@@ -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

View File

@@ -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,
),
],
);
}
}

View File

@@ -0,0 +1,111 @@
import '../../../../core/domain/entities/steps_entity.dart';
import '../../../../core/presentation/time_range.dart';
import 'activity_meter_view_state.dart';
List<DailySteps> groupByDay(List<StepsEntity> steps) {
final Map<DateTime, int> groups = {};
for (final step in steps) {
final date = DateTime.fromMillisecondsSinceEpoch(step.occurredAt);
final dayKey = DateTime(date.year, date.month, date.day);
groups[dayKey] = (groups[dayKey] ?? 0) + step.steps;
}
final sorted = groups.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
return sorted
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
.toList();
}
List<HourlySteps> groupByHour(List<StepsEntity> steps) {
final Map<int, int> groups = {};
for (final step in steps) {
final date = DateTime.fromMillisecondsSinceEpoch(step.occurredAt);
groups[date.hour] = (groups[date.hour] ?? 0) + step.steps;
}
return List<HourlySteps>.generate(
24,
(h) => HourlySteps(hour: h, totalSteps: groups[h] ?? 0),
);
}
int countActiveHours(List<StepsEntity> steps) {
final Set<int> hours = {};
for (final s in steps) {
if (s.steps <= 0) continue;
final date = DateTime.fromMillisecondsSinceEpoch(s.occurredAt);
hours.add(date.hour);
}
return hours.length;
}
List<DailySteps> mergeHistory(
List<DailySteps> existing,
List<DailySteps> newItems,
) {
final Map<DateTime, int> merged = {
for (final d in existing) d.date: d.totalSteps,
};
for (final d in newItems) {
merged[d.date] = (merged[d.date] ?? 0) + d.totalSteps;
}
final sorted = merged.entries.toList()
..sort((a, b) => b.key.compareTo(a.key));
return sorted
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
.toList();
}
StepsStats computeStats(List<DailySteps> daily, int rangeDays) {
if (daily.isEmpty) {
return StepsStats(rangeDays: rangeDays);
}
final total = daily.fold(0, (sum, d) => sum + d.totalSteps);
final activeDays = daily.where((d) => d.totalSteps > 0).length;
final best = daily.reduce((a, b) => a.totalSteps >= b.totalSteps ? a : b);
final divisor = rangeDays > 0 ? rangeDays : daily.length;
return StepsStats(
total: total,
avgPerDay: (total / divisor).round(),
activeDays: activeDays,
rangeDays: rangeDays,
bestDay: total > 0 ? best : null,
);
}
int countDays(DateTime start, DateTime end) {
final s = DateTime.utc(start.year, start.month, start.day);
final e = DateTime.utc(end.year, end.month, end.day);
return e.difference(s).inDays + 1;
}
(DateTime, DateTime) resolveRange(
TimeRange timeRange, {
required DateTime now,
DateTime? customStart,
DateTime? customEnd,
}) {
final todayStart = DateTime(now.year, now.month, now.day);
switch (timeRange) {
case TimeRange.today:
return (todayStart, now);
case TimeRange.sevenDays:
return (DateTime(now.year, now.month, now.day - 6), now);
case TimeRange.thirtyDays:
return (DateTime(now.year, now.month, now.day - 29), now);
case TimeRange.custom:
final s = customStart ?? todayStart;
final e = customEnd ?? now;
return (DateTime(s.year, s.month, s.day), e);
}
}

View File

@@ -5,10 +5,11 @@ import 'package:legacy_shared/legacy_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import '../../../../core/data/datasources/health_query_builder.dart';
import '../../../../core/presentation/time_range.dart';
import '../../../../core/domain/repositories/steps_repository.dart';
import '../../../../core/providers/steps_repository_provider.dart';
import '../../../../core/domain/entities/steps_entity.dart';
import '../../../../core/domain/repositories/steps_repository.dart';
import '../../../../core/presentation/time_range.dart';
import '../../../../core/providers/steps_repository_provider.dart';
import 'activity_meter_aggregator.dart';
import 'activity_meter_view_state.dart';
final activityMeterViewModelProvider =
@@ -27,11 +28,12 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
ActivityMeterViewState build() {
_repository = ref.read(stepsRepositoryProvider);
_tracking = ref.read(sfTrackingProvider);
_init();
Future.microtask(_loadFilteredData);
return const ActivityMeterViewState();
}
String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator;
String? get _identificator =>
ref.read(selectedDeviceProvider).value?.identificator;
Future<void> selectTimeRange(TimeRange range) async {
if (range == state.timeRange) return;
@@ -77,7 +79,11 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
try {
final nextPage = state.currentHistoryPage + 1;
final filters = _buildTimeFilters();
final (start, end) = _resolveRange();
final filters = HealthQueryBuilder.timeRangeFilters(
start: start,
end: end,
);
final steps = await _repository.getSteps(
identificator: identificator,
@@ -90,8 +96,8 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
);
if (!ref.mounted) return;
final newDaily = _groupByDay(steps);
final merged = _mergeHistory(state.historyData, newDaily);
final newDaily = groupByDay(steps);
final merged = mergeHistory(state.historyData, newDaily);
state = state.copyWith(
historyData: merged,
@@ -99,7 +105,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
hasMoreHistory: steps.length >= _historyPageSize,
isLoadingMore: false,
);
} catch (e) {
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isLoadingMore: false,
@@ -108,42 +114,6 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
}
}
Future<void> _init() async {
final identificator = _identificator;
if (identificator == null) {
state = state.copyWith(isLoading: false);
return;
}
try {
final todayStart = DateTime.now();
final todayFilters = HealthQueryBuilder.timeRangeFilters(
start: DateTime(todayStart.year, todayStart.month, todayStart.day),
end: todayStart,
);
final todaySteps = await _repository.getSteps(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.desc,
filters: todayFilters,
),
);
if (!ref.mounted) return;
final todayTotal = todaySteps.fold(0, (sum, s) => sum + s.steps);
state = state.copyWith(todayTotal: todayTotal);
await _loadFilteredData();
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorEvent: ActivityMeterErrorEvent.loadData,
);
}
}
Future<void> _loadFilteredData() async {
final identificator = _identificator;
if (identificator == null) {
@@ -152,9 +122,13 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
}
try {
final filters = _buildTimeFilters();
final (start, end) = _resolveRange();
final filters = HealthQueryBuilder.timeRangeFilters(
start: start,
end: end,
);
final (chartSteps, histSteps) = await (
final (chartSteps, histSteps, todaySteps) = await (
_repository.getSteps(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
@@ -171,22 +145,37 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
filters: filters,
),
),
_fetchTodaySteps(identificator),
).wait;
if (!ref.mounted) return;
final chartDaily = _groupByDay(chartSteps);
final historyDaily = _groupByDay(histSteps).reversed.toList();
final todayTotal = todaySteps.fold(0, (sum, s) => sum + s.steps);
final activeHoursToday = countActiveHours(todaySteps);
final chartDaily = groupByDay(chartSteps);
final hourly = state.timeRange == TimeRange.today
? groupByHour(todaySteps)
: const <HourlySteps>[];
final historyDaily = groupByDay(histSteps).reversed.toList();
final rangeDays = countDays(start, end);
final stats = computeStats(chartDaily, rangeDays);
state = state.copyWith(
todayTotal: todayTotal,
chartData: chartDaily,
hourlyData: hourly,
activeHoursToday: activeHoursToday,
historyData: historyDaily,
currentHistoryPage: 1,
hasMoreHistory: histSteps.length >= _historyPageSize,
stats: _computeStats(chartDaily),
stats: stats,
rangeStart: start,
rangeEnd: end,
isLoading: false,
errorEvent: null,
);
} catch (e) {
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
@@ -195,81 +184,27 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
}
}
List<DailySteps> _groupByDay(List<StepsEntity> steps) {
final Map<DateTime, int> groups = {};
for (final step in steps) {
final date = DateTime.fromMillisecondsSinceEpoch(step.occurredAt);
final dayKey = DateTime(date.year, date.month, date.day);
groups[dayKey] = (groups[dayKey] ?? 0) + step.steps;
}
final sorted = groups.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
return sorted
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
.toList();
}
List<DailySteps> _mergeHistory(
List<DailySteps> existing,
List<DailySteps> newItems,
) {
final Map<DateTime, int> merged = {
for (final d in existing) d.date: d.totalSteps,
};
for (final d in newItems) {
merged[d.date] = (merged[d.date] ?? 0) + d.totalSteps;
}
final sorted = merged.entries.toList()
..sort((a, b) => b.key.compareTo(a.key));
return sorted
.map((e) => DailySteps(date: e.key, totalSteps: e.value))
.toList();
}
StepsStats _computeStats(List<DailySteps> daily) {
if (daily.isEmpty) return const StepsStats();
final total = daily.fold(0, (sum, d) => sum + d.totalSteps);
final best = daily.reduce((a, b) => a.totalSteps >= b.totalSteps ? a : b);
return StepsStats(
avgPerDay: (total / daily.length).round(),
total: total,
bestDaySteps: best.totalSteps,
Future<List<StepsEntity>> _fetchTodaySteps(String identificator) {
final now = DateTime.now();
final todayStart = DateTime(now.year, now.month, now.day);
return _repository.getSteps(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.asc,
filters: HealthQueryBuilder.timeRangeFilters(
start: todayStart,
end: now,
),
),
);
}
List<HealthFilter>? _buildTimeFilters() {
final range = _getTimeRange();
if (range == null) return null;
final (start, end) = range;
return HealthQueryBuilder.timeRangeFilters(start: start, end: end);
}
(DateTime, DateTime)? _getTimeRange() {
final now = DateTime.now();
final todayStart = DateTime(now.year, now.month, now.day);
switch (state.timeRange) {
case TimeRange.today:
return (todayStart, now);
case TimeRange.sevenDays:
return (todayStart.subtract(const Duration(days: 6)), now);
case TimeRange.thirtyDays:
return (todayStart.subtract(const Duration(days: 29)), now);
case TimeRange.custom:
if (state.customStart != null && state.customEnd != null) {
return (state.customStart!, state.customEnd!);
}
return null;
}
}
(DateTime, DateTime) _resolveRange() => resolveRange(
state.timeRange,
now: DateTime.now(),
customStart: state.customStart,
customEnd: state.customEnd,
);
Future<bool> togglePedometer({required bool enabled}) async {
final device = ref.read(selectedDeviceProvider).value;
@@ -290,7 +225,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
unawaited(_tracking.legacyDeviceActivityPedometerToggled(enabled));
return true;
} catch (e) {
} catch (_) {
if (!ref.mounted) return false;
state = state.copyWith(errorEvent: ActivityMeterErrorEvent.pedometer);
return false;

View File

@@ -12,12 +12,20 @@ abstract class DailySteps with _$DailySteps {
_DailySteps;
}
@freezed
abstract class HourlySteps with _$HourlySteps {
const factory HourlySteps({required int hour, required int totalSteps}) =
_HourlySteps;
}
@freezed
abstract class StepsStats with _$StepsStats {
const factory StepsStats({
@Default(0) int avgPerDay,
@Default(0) int total,
@Default(0) int bestDaySteps,
@Default(0) int avgPerDay,
@Default(0) int activeDays,
@Default(0) int rangeDays,
DailySteps? bestDay,
}) = _StepsStats;
}
@@ -27,6 +35,8 @@ abstract class ActivityMeterViewState with _$ActivityMeterViewState {
@Default(0) int todayTotal,
@Default(8000) int dailyGoal,
@Default([]) List<DailySteps> chartData,
@Default([]) List<HourlySteps> hourlyData,
@Default(0) int activeHoursToday,
@Default([]) List<DailySteps> historyData,
@Default(1) int currentHistoryPage,
@Default(false) bool hasMoreHistory,
@@ -34,8 +44,16 @@ abstract class ActivityMeterViewState with _$ActivityMeterViewState {
@Default(TimeRange.today) TimeRange timeRange,
DateTime? customStart,
DateTime? customEnd,
DateTime? rangeStart,
DateTime? rangeEnd,
@Default(true) bool isLoading,
@Default(false) bool isLoadingMore,
ActivityMeterErrorEvent? errorEvent,
}) = _ActivityMeterViewState;
}
extension ActivityMeterViewStateX on ActivityMeterViewState {
bool get hasActivityData => timeRange == TimeRange.today
? hourlyData.any((h) => h.totalSteps > 0)
: stats.total > 0;
}

View File

@@ -269,12 +269,272 @@ as int,
}
}
/// @nodoc
mixin _$HourlySteps {
int get hour; int get totalSteps;
/// Create a copy of HourlySteps
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$HourlyStepsCopyWith<HourlySteps> get copyWith => _$HourlyStepsCopyWithImpl<HourlySteps>(this as HourlySteps, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is HourlySteps&&(identical(other.hour, hour) || other.hour == hour)&&(identical(other.totalSteps, totalSteps) || other.totalSteps == totalSteps));
}
@override
int get hashCode => Object.hash(runtimeType,hour,totalSteps);
@override
String toString() {
return 'HourlySteps(hour: $hour, totalSteps: $totalSteps)';
}
}
/// @nodoc
abstract mixin class $HourlyStepsCopyWith<$Res> {
factory $HourlyStepsCopyWith(HourlySteps value, $Res Function(HourlySteps) _then) = _$HourlyStepsCopyWithImpl;
@useResult
$Res call({
int hour, int totalSteps
});
}
/// @nodoc
class _$HourlyStepsCopyWithImpl<$Res>
implements $HourlyStepsCopyWith<$Res> {
_$HourlyStepsCopyWithImpl(this._self, this._then);
final HourlySteps _self;
final $Res Function(HourlySteps) _then;
/// Create a copy of HourlySteps
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? hour = null,Object? totalSteps = null,}) {
return _then(_self.copyWith(
hour: null == hour ? _self.hour : hour // ignore: cast_nullable_to_non_nullable
as int,totalSteps: null == totalSteps ? _self.totalSteps : totalSteps // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// Adds pattern-matching-related methods to [HourlySteps].
extension HourlyStepsPatterns on HourlySteps {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _HourlySteps value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _HourlySteps() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _HourlySteps value) $default,){
final _that = this;
switch (_that) {
case _HourlySteps():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _HourlySteps value)? $default,){
final _that = this;
switch (_that) {
case _HourlySteps() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int hour, int totalSteps)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _HourlySteps() when $default != null:
return $default(_that.hour,_that.totalSteps);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int hour, int totalSteps) $default,) {final _that = this;
switch (_that) {
case _HourlySteps():
return $default(_that.hour,_that.totalSteps);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int hour, int totalSteps)? $default,) {final _that = this;
switch (_that) {
case _HourlySteps() when $default != null:
return $default(_that.hour,_that.totalSteps);case _:
return null;
}
}
}
/// @nodoc
class _HourlySteps implements HourlySteps {
const _HourlySteps({required this.hour, required this.totalSteps});
@override final int hour;
@override final int totalSteps;
/// Create a copy of HourlySteps
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$HourlyStepsCopyWith<_HourlySteps> get copyWith => __$HourlyStepsCopyWithImpl<_HourlySteps>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HourlySteps&&(identical(other.hour, hour) || other.hour == hour)&&(identical(other.totalSteps, totalSteps) || other.totalSteps == totalSteps));
}
@override
int get hashCode => Object.hash(runtimeType,hour,totalSteps);
@override
String toString() {
return 'HourlySteps(hour: $hour, totalSteps: $totalSteps)';
}
}
/// @nodoc
abstract mixin class _$HourlyStepsCopyWith<$Res> implements $HourlyStepsCopyWith<$Res> {
factory _$HourlyStepsCopyWith(_HourlySteps value, $Res Function(_HourlySteps) _then) = __$HourlyStepsCopyWithImpl;
@override @useResult
$Res call({
int hour, int totalSteps
});
}
/// @nodoc
class __$HourlyStepsCopyWithImpl<$Res>
implements _$HourlyStepsCopyWith<$Res> {
__$HourlyStepsCopyWithImpl(this._self, this._then);
final _HourlySteps _self;
final $Res Function(_HourlySteps) _then;
/// Create a copy of HourlySteps
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? hour = null,Object? totalSteps = null,}) {
return _then(_HourlySteps(
hour: null == hour ? _self.hour : hour // ignore: cast_nullable_to_non_nullable
as int,totalSteps: null == totalSteps ? _self.totalSteps : totalSteps // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
mixin _$StepsStats {
int get avgPerDay; int get total; int get bestDaySteps;
int get total; int get avgPerDay; int get activeDays; int get rangeDays; DailySteps? get bestDay;
/// Create a copy of StepsStats
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -285,16 +545,16 @@ $StepsStatsCopyWith<StepsStats> get copyWith => _$StepsStatsCopyWithImpl<StepsSt
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is StepsStats&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.total, total) || other.total == total)&&(identical(other.bestDaySteps, bestDaySteps) || other.bestDaySteps == bestDaySteps));
return identical(this, other) || (other.runtimeType == runtimeType&&other is StepsStats&&(identical(other.total, total) || other.total == total)&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.activeDays, activeDays) || other.activeDays == activeDays)&&(identical(other.rangeDays, rangeDays) || other.rangeDays == rangeDays)&&(identical(other.bestDay, bestDay) || other.bestDay == bestDay));
}
@override
int get hashCode => Object.hash(runtimeType,avgPerDay,total,bestDaySteps);
int get hashCode => Object.hash(runtimeType,total,avgPerDay,activeDays,rangeDays,bestDay);
@override
String toString() {
return 'StepsStats(avgPerDay: $avgPerDay, total: $total, bestDaySteps: $bestDaySteps)';
return 'StepsStats(total: $total, avgPerDay: $avgPerDay, activeDays: $activeDays, rangeDays: $rangeDays, bestDay: $bestDay)';
}
@@ -305,11 +565,11 @@ abstract mixin class $StepsStatsCopyWith<$Res> {
factory $StepsStatsCopyWith(StepsStats value, $Res Function(StepsStats) _then) = _$StepsStatsCopyWithImpl;
@useResult
$Res call({
int avgPerDay, int total, int bestDaySteps
int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay
});
$DailyStepsCopyWith<$Res>? get bestDay;
}
/// @nodoc
@@ -322,15 +582,29 @@ class _$StepsStatsCopyWithImpl<$Res>
/// Create a copy of StepsStats
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? avgPerDay = null,Object? total = null,Object? bestDaySteps = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? total = null,Object? avgPerDay = null,Object? activeDays = null,Object? rangeDays = null,Object? bestDay = freezed,}) {
return _then(_self.copyWith(
avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
as int,bestDaySteps: null == bestDaySteps ? _self.bestDaySteps : bestDaySteps // ignore: cast_nullable_to_non_nullable
as int,
total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
as int,avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
as int,activeDays: null == activeDays ? _self.activeDays : activeDays // ignore: cast_nullable_to_non_nullable
as int,rangeDays: null == rangeDays ? _self.rangeDays : rangeDays // ignore: cast_nullable_to_non_nullable
as int,bestDay: freezed == bestDay ? _self.bestDay : bestDay // ignore: cast_nullable_to_non_nullable
as DailySteps?,
));
}
/// Create a copy of StepsStats
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DailyStepsCopyWith<$Res>? get bestDay {
if (_self.bestDay == null) {
return null;
}
return $DailyStepsCopyWith<$Res>(_self.bestDay!, (value) {
return _then(_self.copyWith(bestDay: value));
});
}
}
@@ -412,10 +686,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int avgPerDay, int total, int bestDaySteps)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _StepsStats() when $default != null:
return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
return $default(_that.total,_that.avgPerDay,_that.activeDays,_that.rangeDays,_that.bestDay);case _:
return orElse();
}
@@ -433,10 +707,10 @@ return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int avgPerDay, int total, int bestDaySteps) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay) $default,) {final _that = this;
switch (_that) {
case _StepsStats():
return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
return $default(_that.total,_that.avgPerDay,_that.activeDays,_that.rangeDays,_that.bestDay);case _:
throw StateError('Unexpected subclass');
}
@@ -453,10 +727,10 @@ return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int avgPerDay, int total, int bestDaySteps)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay)? $default,) {final _that = this;
switch (_that) {
case _StepsStats() when $default != null:
return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
return $default(_that.total,_that.avgPerDay,_that.activeDays,_that.rangeDays,_that.bestDay);case _:
return null;
}
@@ -468,12 +742,14 @@ return $default(_that.avgPerDay,_that.total,_that.bestDaySteps);case _:
class _StepsStats implements StepsStats {
const _StepsStats({this.avgPerDay = 0, this.total = 0, this.bestDaySteps = 0});
const _StepsStats({this.total = 0, this.avgPerDay = 0, this.activeDays = 0, this.rangeDays = 0, this.bestDay});
@override@JsonKey() final int avgPerDay;
@override@JsonKey() final int total;
@override@JsonKey() final int bestDaySteps;
@override@JsonKey() final int avgPerDay;
@override@JsonKey() final int activeDays;
@override@JsonKey() final int rangeDays;
@override final DailySteps? bestDay;
/// Create a copy of StepsStats
/// with the given fields replaced by the non-null parameter values.
@@ -485,16 +761,16 @@ _$StepsStatsCopyWith<_StepsStats> get copyWith => __$StepsStatsCopyWithImpl<_Ste
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _StepsStats&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.total, total) || other.total == total)&&(identical(other.bestDaySteps, bestDaySteps) || other.bestDaySteps == bestDaySteps));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _StepsStats&&(identical(other.total, total) || other.total == total)&&(identical(other.avgPerDay, avgPerDay) || other.avgPerDay == avgPerDay)&&(identical(other.activeDays, activeDays) || other.activeDays == activeDays)&&(identical(other.rangeDays, rangeDays) || other.rangeDays == rangeDays)&&(identical(other.bestDay, bestDay) || other.bestDay == bestDay));
}
@override
int get hashCode => Object.hash(runtimeType,avgPerDay,total,bestDaySteps);
int get hashCode => Object.hash(runtimeType,total,avgPerDay,activeDays,rangeDays,bestDay);
@override
String toString() {
return 'StepsStats(avgPerDay: $avgPerDay, total: $total, bestDaySteps: $bestDaySteps)';
return 'StepsStats(total: $total, avgPerDay: $avgPerDay, activeDays: $activeDays, rangeDays: $rangeDays, bestDay: $bestDay)';
}
@@ -505,11 +781,11 @@ abstract mixin class _$StepsStatsCopyWith<$Res> implements $StepsStatsCopyWith<$
factory _$StepsStatsCopyWith(_StepsStats value, $Res Function(_StepsStats) _then) = __$StepsStatsCopyWithImpl;
@override @useResult
$Res call({
int avgPerDay, int total, int bestDaySteps
int total, int avgPerDay, int activeDays, int rangeDays, DailySteps? bestDay
});
@override $DailyStepsCopyWith<$Res>? get bestDay;
}
/// @nodoc
@@ -522,22 +798,36 @@ class __$StepsStatsCopyWithImpl<$Res>
/// Create a copy of StepsStats
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? avgPerDay = null,Object? total = null,Object? bestDaySteps = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? total = null,Object? avgPerDay = null,Object? activeDays = null,Object? rangeDays = null,Object? bestDay = freezed,}) {
return _then(_StepsStats(
avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
as int,total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
as int,bestDaySteps: null == bestDaySteps ? _self.bestDaySteps : bestDaySteps // ignore: cast_nullable_to_non_nullable
as int,
total: null == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
as int,avgPerDay: null == avgPerDay ? _self.avgPerDay : avgPerDay // ignore: cast_nullable_to_non_nullable
as int,activeDays: null == activeDays ? _self.activeDays : activeDays // ignore: cast_nullable_to_non_nullable
as int,rangeDays: null == rangeDays ? _self.rangeDays : rangeDays // ignore: cast_nullable_to_non_nullable
as int,bestDay: freezed == bestDay ? _self.bestDay : bestDay // ignore: cast_nullable_to_non_nullable
as DailySteps?,
));
}
/// Create a copy of StepsStats
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DailyStepsCopyWith<$Res>? get bestDay {
if (_self.bestDay == null) {
return null;
}
return $DailyStepsCopyWith<$Res>(_self.bestDay!, (value) {
return _then(_self.copyWith(bestDay: value));
});
}
}
/// @nodoc
mixin _$ActivityMeterViewState {
int get todayTotal; int get dailyGoal; List<DailySteps> get chartData; List<DailySteps> get historyData; int get currentHistoryPage; bool get hasMoreHistory; StepsStats get stats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; bool get isLoading; bool get isLoadingMore; ActivityMeterErrorEvent? get errorEvent;
int get todayTotal; int get dailyGoal; List<DailySteps> get chartData; List<HourlySteps> get hourlyData; int get activeHoursToday; List<DailySteps> get historyData; int get currentHistoryPage; bool get hasMoreHistory; StepsStats get stats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; DateTime? get rangeStart; DateTime? get rangeEnd; bool get isLoading; bool get isLoadingMore; ActivityMeterErrorEvent? get errorEvent;
/// Create a copy of ActivityMeterViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -548,16 +838,16 @@ $ActivityMeterViewStateCopyWith<ActivityMeterViewState> get copyWith => _$Activi
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other.chartData, chartData)&&const DeepCollectionEquality().equals(other.historyData, historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other.chartData, chartData)&&const DeepCollectionEquality().equals(other.hourlyData, hourlyData)&&(identical(other.activeHoursToday, activeHoursToday) || other.activeHoursToday == activeHoursToday)&&const DeepCollectionEquality().equals(other.historyData, historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.rangeStart, rangeStart) || other.rangeStart == rangeStart)&&(identical(other.rangeEnd, rangeEnd) || other.rangeEnd == rangeEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(chartData),const DeepCollectionEquality().hash(historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorEvent);
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(chartData),const DeepCollectionEquality().hash(hourlyData),activeHoursToday,const DeepCollectionEquality().hash(historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,rangeStart,rangeEnd,isLoading,isLoadingMore,errorEvent);
@override
String toString() {
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, hourlyData: $hourlyData, activeHoursToday: $activeHoursToday, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, rangeStart: $rangeStart, rangeEnd: $rangeEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
}
@@ -568,7 +858,7 @@ abstract mixin class $ActivityMeterViewStateCopyWith<$Res> {
factory $ActivityMeterViewStateCopyWith(ActivityMeterViewState value, $Res Function(ActivityMeterViewState) _then) = _$ActivityMeterViewStateCopyWithImpl;
@useResult
$Res call({
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
});
@@ -585,18 +875,22 @@ class _$ActivityMeterViewStateCopyWithImpl<$Res>
/// Create a copy of ActivityMeterViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? hourlyData = null,Object? activeHoursToday = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? rangeStart = freezed,Object? rangeEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
return _then(_self.copyWith(
todayTotal: null == todayTotal ? _self.todayTotal : todayTotal // ignore: cast_nullable_to_non_nullable
as int,dailyGoal: null == dailyGoal ? _self.dailyGoal : dailyGoal // ignore: cast_nullable_to_non_nullable
as int,chartData: null == chartData ? _self.chartData : chartData // ignore: cast_nullable_to_non_nullable
as List<DailySteps>,historyData: null == historyData ? _self.historyData : historyData // ignore: cast_nullable_to_non_nullable
as List<DailySteps>,hourlyData: null == hourlyData ? _self.hourlyData : hourlyData // ignore: cast_nullable_to_non_nullable
as List<HourlySteps>,activeHoursToday: null == activeHoursToday ? _self.activeHoursToday : activeHoursToday // ignore: cast_nullable_to_non_nullable
as int,historyData: null == historyData ? _self.historyData : historyData // ignore: cast_nullable_to_non_nullable
as List<DailySteps>,currentHistoryPage: null == currentHistoryPage ? _self.currentHistoryPage : currentHistoryPage // ignore: cast_nullable_to_non_nullable
as int,hasMoreHistory: null == hasMoreHistory ? _self.hasMoreHistory : hasMoreHistory // ignore: cast_nullable_to_non_nullable
as bool,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
as StepsStats,timeRange: null == timeRange ? _self.timeRange : timeRange // ignore: cast_nullable_to_non_nullable
as TimeRange,customStart: freezed == customStart ? _self.customStart : customStart // ignore: cast_nullable_to_non_nullable
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,rangeStart: freezed == rangeStart ? _self.rangeStart : rangeStart // ignore: cast_nullable_to_non_nullable
as DateTime?,rangeEnd: freezed == rangeEnd ? _self.rangeEnd : rangeEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
@@ -694,10 +988,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ActivityMeterViewState() when $default != null:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.hourlyData,_that.activeHoursToday,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.rangeStart,_that.rangeEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
return orElse();
}
@@ -715,10 +1009,10 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent) $default,) {final _that = this;
switch (_that) {
case _ActivityMeterViewState():
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.hourlyData,_that.activeHoursToday,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.rangeStart,_that.rangeEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
throw StateError('Unexpected subclass');
}
@@ -735,10 +1029,10 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,) {final _that = this;
switch (_that) {
case _ActivityMeterViewState() when $default != null:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.hourlyData,_that.activeHoursToday,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.rangeStart,_that.rangeEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
return null;
}
@@ -750,7 +1044,7 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
class _ActivityMeterViewState implements ActivityMeterViewState {
const _ActivityMeterViewState({this.todayTotal = 0, this.dailyGoal = 8000, final List<DailySteps> chartData = const [], final List<DailySteps> historyData = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.stats = const StepsStats(), this.timeRange = TimeRange.today, this.customStart, this.customEnd, this.isLoading = true, this.isLoadingMore = false, this.errorEvent}): _chartData = chartData,_historyData = historyData;
const _ActivityMeterViewState({this.todayTotal = 0, this.dailyGoal = 8000, final List<DailySteps> chartData = const [], final List<HourlySteps> hourlyData = const [], this.activeHoursToday = 0, final List<DailySteps> historyData = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.stats = const StepsStats(), this.timeRange = TimeRange.today, this.customStart, this.customEnd, this.rangeStart, this.rangeEnd, this.isLoading = true, this.isLoadingMore = false, this.errorEvent}): _chartData = chartData,_hourlyData = hourlyData,_historyData = historyData;
@override@JsonKey() final int todayTotal;
@@ -762,6 +1056,14 @@ class _ActivityMeterViewState implements ActivityMeterViewState {
return EqualUnmodifiableListView(_chartData);
}
final List<HourlySteps> _hourlyData;
@override@JsonKey() List<HourlySteps> get hourlyData {
if (_hourlyData is EqualUnmodifiableListView) return _hourlyData;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_hourlyData);
}
@override@JsonKey() final int activeHoursToday;
final List<DailySteps> _historyData;
@override@JsonKey() List<DailySteps> get historyData {
if (_historyData is EqualUnmodifiableListView) return _historyData;
@@ -775,6 +1077,8 @@ class _ActivityMeterViewState implements ActivityMeterViewState {
@override@JsonKey() final TimeRange timeRange;
@override final DateTime? customStart;
@override final DateTime? customEnd;
@override final DateTime? rangeStart;
@override final DateTime? rangeEnd;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isLoadingMore;
@override final ActivityMeterErrorEvent? errorEvent;
@@ -789,16 +1093,16 @@ _$ActivityMeterViewStateCopyWith<_ActivityMeterViewState> get copyWith => __$Act
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other._chartData, _chartData)&&const DeepCollectionEquality().equals(other._historyData, _historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other._chartData, _chartData)&&const DeepCollectionEquality().equals(other._hourlyData, _hourlyData)&&(identical(other.activeHoursToday, activeHoursToday) || other.activeHoursToday == activeHoursToday)&&const DeepCollectionEquality().equals(other._historyData, _historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.rangeStart, rangeStart) || other.rangeStart == rangeStart)&&(identical(other.rangeEnd, rangeEnd) || other.rangeEnd == rangeEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(_chartData),const DeepCollectionEquality().hash(_historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorEvent);
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(_chartData),const DeepCollectionEquality().hash(_hourlyData),activeHoursToday,const DeepCollectionEquality().hash(_historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,rangeStart,rangeEnd,isLoading,isLoadingMore,errorEvent);
@override
String toString() {
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, hourlyData: $hourlyData, activeHoursToday: $activeHoursToday, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, rangeStart: $rangeStart, rangeEnd: $rangeEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
}
@@ -809,7 +1113,7 @@ abstract mixin class _$ActivityMeterViewStateCopyWith<$Res> implements $Activity
factory _$ActivityMeterViewStateCopyWith(_ActivityMeterViewState value, $Res Function(_ActivityMeterViewState) _then) = __$ActivityMeterViewStateCopyWithImpl;
@override @useResult
$Res call({
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<HourlySteps> hourlyData, int activeHoursToday, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, DateTime? rangeStart, DateTime? rangeEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
});
@@ -826,18 +1130,22 @@ class __$ActivityMeterViewStateCopyWithImpl<$Res>
/// Create a copy of ActivityMeterViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? hourlyData = null,Object? activeHoursToday = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? rangeStart = freezed,Object? rangeEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
return _then(_ActivityMeterViewState(
todayTotal: null == todayTotal ? _self.todayTotal : todayTotal // ignore: cast_nullable_to_non_nullable
as int,dailyGoal: null == dailyGoal ? _self.dailyGoal : dailyGoal // ignore: cast_nullable_to_non_nullable
as int,chartData: null == chartData ? _self._chartData : chartData // ignore: cast_nullable_to_non_nullable
as List<DailySteps>,historyData: null == historyData ? _self._historyData : historyData // ignore: cast_nullable_to_non_nullable
as List<DailySteps>,hourlyData: null == hourlyData ? _self._hourlyData : hourlyData // ignore: cast_nullable_to_non_nullable
as List<HourlySteps>,activeHoursToday: null == activeHoursToday ? _self.activeHoursToday : activeHoursToday // ignore: cast_nullable_to_non_nullable
as int,historyData: null == historyData ? _self._historyData : historyData // ignore: cast_nullable_to_non_nullable
as List<DailySteps>,currentHistoryPage: null == currentHistoryPage ? _self.currentHistoryPage : currentHistoryPage // ignore: cast_nullable_to_non_nullable
as int,hasMoreHistory: null == hasMoreHistory ? _self.hasMoreHistory : hasMoreHistory // ignore: cast_nullable_to_non_nullable
as bool,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
as StepsStats,timeRange: null == timeRange ? _self.timeRange : timeRange // ignore: cast_nullable_to_non_nullable
as TimeRange,customStart: freezed == customStart ? _self.customStart : customStart // ignore: cast_nullable_to_non_nullable
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,rangeStart: freezed == rangeStart ? _self.rangeStart : rangeStart // ignore: cast_nullable_to_non_nullable
as DateTime?,rangeEnd: freezed == rangeEnd ? _self.rangeEnd : rangeEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable

View File

@@ -0,0 +1,122 @@
import 'package:design_system/design_system.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:utils/utils.dart';
class ActivityBarChartBase extends ConsumerWidget {
final List<BarChartGroupData> barGroups;
final AxisTitles bottomTitles;
final BarTouchTooltipData tooltip;
final double maxY;
final bool isEmpty;
const ActivityBarChartBase({
super.key,
required this.barGroups,
required this.bottomTitles,
required this.tooltip,
required this.maxY,
required this.isEmpty,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final height = SizeUtils.getByScreen<double>(small: 180, big: 160);
if (isEmpty) {
return SizedBox(
height: height,
child: Center(
child: Text(
'',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 16, big: 14),
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.3),
),
),
),
);
}
final labelColor = theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.4);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
),
child: SizedBox(
height: height,
child: BarChart(
BarChartData(
maxY: maxY * 1.15,
barGroups: barGroups,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: _computeInterval(maxY),
getDrawingHorizontalLine: (_) => FlLine(
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.08),
strokeWidth: 1,
),
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: SizeUtils.getByScreen(small: 36, big: 32),
getTitlesWidget: (value, _) => Text(
_formatAxis(value),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
color: labelColor,
),
),
),
),
bottomTitles: bottomTitles,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(touchTooltipData: tooltip),
),
),
),
);
}
static double _computeInterval(double maxY) {
if (maxY <= 0) return 1;
return (maxY / 4).ceilToDouble();
}
static String _formatAxis(double value) {
if (value >= 1000) return '${(value / 1000).toStringAsFixed(0)}k';
return value.toInt().toString();
}
static BarChartRodData buildRod({
required double toY,
required Color color,
required double width,
}) => BarChartRodData(
toY: toY,
color: color,
width: width,
borderRadius: BorderRadius.vertical(
top: Radius.circular(SizeUtils.getByScreen(small: 4, big: 3)),
),
);
}

View File

@@ -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),
),
),
),
],
);
}
}

View File

@@ -0,0 +1,77 @@
import 'package:design_system/design_system.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:utils/utils.dart';
import '../state/activity_meter_view_state.dart';
import 'activity_bar_chart_base.dart';
class HourlyBarChart extends ConsumerWidget {
final List<HourlySteps> data;
const HourlyBarChart({super.key, required this.data});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final isEmpty = data.isEmpty || data.every((h) => h.totalSteps == 0);
final maxY = isEmpty
? 0.0
: data.map((h) => h.totalSteps.toDouble()).reduce((a, b) => a > b ? a : b);
final labelColor = theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.4);
return ActivityBarChartBase(
isEmpty: isEmpty,
maxY: maxY,
barGroups: data
.map(
(bucket) => BarChartGroupData(
x: bucket.hour,
barRods: [
ActivityBarChartBase.buildRod(
toY: bucket.totalSteps.toDouble(),
color: theme.getColorFor(ThemeCode.legacyPrimary),
width: SizeUtils.getByScreen(small: 6, big: 5),
),
],
),
)
.toList(),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 3,
getTitlesWidget: (value, _) {
final hour = value.toInt();
if (hour < 0 || hour > 23 || hour % 6 != 0) {
return const SizedBox.shrink();
}
return Text(
'${hour.toString().padLeft(2, '0')}h',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 9, big: 8),
color: labelColor,
),
);
},
),
),
tooltip: BarTouchTooltipData(
getTooltipColor: (_) =>
theme.getColorFor(ThemeCode.backgroundSecondary),
getTooltipItem: (group, _, rod, __) => BarTooltipItem(
'${group.x.toString().padLeft(2, '0')}h\n${rod.toY.toInt()}',
TextStyle(
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
fontWeight: FontWeight.w600,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
),
);
}
}

View File

@@ -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,
);
}
},
),
],
),
);
}
}

View File

@@ -0,0 +1,121 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import '../format_steps.dart';
import '../state/activity_meter_view_state.dart';
class PeriodStatsCards extends StatelessWidget {
final StepsStats stats;
const PeriodStatsCards({super.key, required this.stats});
@override
Widget build(BuildContext context) {
final hasData = stats.total > 0;
final gap = SizeUtils.getByScreen<double>(small: 8, big: 6);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
vertical: SizeUtils.getByScreen(small: 8, big: 6),
),
child: Row(
children: [
Expanded(
child: _StatCard(
label: context.translate(I18n.activityMeterTotalSteps),
value: hasData ? formatStepsNumber(stats.total) : '',
unit: context.translate(I18n.unitSteps),
),
),
SizedBox(width: gap),
Expanded(
child: _StatCard(
label: context.translate(I18n.activityMeterAverageDaily),
value: hasData ? formatStepsNumber(stats.avgPerDay) : '',
unit: context.translate(I18n.unitStepsPerDay),
),
),
SizedBox(width: gap),
Expanded(
child: _StatCard(
label: context.translate(I18n.activityMeterActiveDays),
value: stats.rangeDays > 0
? '${stats.activeDays}/${stats.rangeDays}'
: '',
unit: context.translate(I18n.unitDays),
),
),
],
),
);
}
}
class _StatCard extends ConsumerWidget {
final String label;
final String value;
final String unit;
const _StatCard({
required this.label,
required this.value,
required this.unit,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Container(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 10, big: 8),
vertical: SizeUtils.getByScreen(small: 12, big: 10),
),
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.backgroundSecondary),
borderRadius: BorderRadius.all(
Radius.circular(SizeUtils.getByScreen(small: 12, big: 10)),
),
),
child: Column(
children: [
Text(
label.toUpperCase(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.5),
),
),
SizedBox(height: SizeUtils.getByScreen(small: 6, big: 4)),
Text(
value,
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 18, big: 16),
fontWeight: FontWeight.w700,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
SizedBox(height: SizeUtils.getByScreen(small: 2, big: 1)),
Text(
unit,
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.4),
),
),
],
),
);
}
}

View File

@@ -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),
),
),
],
],
),
);
}
}

View File

@@ -1,156 +1,82 @@
import 'package:design_system/design_system.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:utils/utils.dart';
import '../state/activity_meter_view_state.dart';
import 'activity_bar_chart_base.dart';
class StepsBarChart extends StatelessWidget {
class StepsBarChart extends ConsumerWidget {
final List<DailySteps> data;
final ThemePort theme;
const StepsBarChart({super.key, required this.data, required this.theme});
const StepsBarChart({super.key, required this.data});
@override
Widget build(BuildContext context) {
if (data.isEmpty) {
return SizedBox(
height: SizeUtils.getByScreen(small: 180, big: 160),
child: Center(
child: Text(
'--',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 16, big: 14),
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.3),
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final isEmpty = data.isEmpty;
final maxY = isEmpty
? 0.0
: data.map((d) => d.totalSteps.toDouble()).reduce((a, b) => a > b ? a : b);
final locale = Localizations.localeOf(context).toString();
final labelColor = theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.4);
return ActivityBarChartBase(
isEmpty: isEmpty,
maxY: maxY,
barGroups: data.asMap().entries.map((entry) {
return BarChartGroupData(
x: entry.key,
barRods: [
ActivityBarChartBase.buildRod(
toY: entry.value.totalSteps.toDouble(),
color: theme.getColorFor(ThemeCode.legacyPrimary),
width: _barWidth(data.length),
),
),
],
);
}).toList(),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: data.length <= 14,
getTitlesWidget: (value, _) {
final index = value.toInt();
if (index < 0 || index >= data.length) {
return const SizedBox.shrink();
}
return Text(
DateFormat('d MMM', locale).format(data[index].date),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 9, big: 8),
color: labelColor,
),
);
},
),
);
}
final maxY = data
.map((d) => d.totalSteps.toDouble())
.reduce((a, b) => a > b ? a : b);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
),
child: SizedBox(
height: SizeUtils.getByScreen(small: 180, big: 160),
child: BarChart(
BarChartData(
maxY: maxY * 1.15,
barGroups: data.asMap().entries.map((entry) {
return BarChartGroupData(
x: entry.key,
barRods: [
BarChartRodData(
toY: entry.value.totalSteps.toDouble(),
color: theme.getColorFor(ThemeCode.legacyPrimary),
width: _barWidth,
borderRadius: BorderRadius.vertical(
top: Radius.circular(
SizeUtils.getByScreen(small: 4, big: 3),
),
),
),
],
);
}).toList(),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: _computeInterval(maxY),
getDrawingHorizontalLine: (value) => FlLine(
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.08),
strokeWidth: 1,
),
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: SizeUtils.getByScreen(small: 36, big: 32),
getTitlesWidget: (value, meta) => Text(
_formatAxis(value),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.4),
),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: data.length <= 14,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index < 0 || index >= data.length) {
return const SizedBox.shrink();
}
final date = data[index].date;
return Text(
'${date.day}/${date.month}',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 9, big: 8),
color: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.4),
),
);
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) =>
theme.getColorFor(ThemeCode.backgroundSecondary),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
rod.toY.toInt().toString(),
TextStyle(
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
fontWeight: FontWeight.w600,
color: theme.getColorFor(ThemeCode.textPrimary),
),
);
},
),
),
tooltip: BarTouchTooltipData(
getTooltipColor: (_) =>
theme.getColorFor(ThemeCode.backgroundSecondary),
getTooltipItem: (group, _, rod, __) => BarTooltipItem(
'${DateFormat('d MMM y', locale).format(data[group.x].date)}\n${rod.toY.toInt()}',
TextStyle(
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
fontWeight: FontWeight.w600,
color: theme.getColorFor(ThemeCode.textPrimary),
),
),
),
);
}
double get _barWidth {
if (data.length <= 7) return SizeUtils.getByScreen(small: 24, big: 20);
if (data.length <= 14) return SizeUtils.getByScreen(small: 14, big: 12);
double _barWidth(int length) {
if (length <= 7) return SizeUtils.getByScreen(small: 24, big: 20);
if (length <= 14) return SizeUtils.getByScreen(small: 14, big: 12);
return SizeUtils.getByScreen(small: 8, big: 6);
}
double _computeInterval(double maxY) {
if (maxY <= 0) return 1;
return (maxY / 4).ceilToDouble();
}
String _formatAxis(double value) {
if (value >= 1000) return '${(value / 1000).toStringAsFixed(0)}k';
return value.toInt().toString();
}
}

View File

@@ -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),
),
),
],
);
}
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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';