device management features, settings module and contact sync

Device management:
  - Activity meter with steps charts and history
  - Apps usage with daily breakdown and top apps
  - Health monitoring (heart rate, oxygen, blood pressure)
  - Scheduled activities with timeline and CRUD
  - Contacts sync to device via contact-lists
  - Locate device, rewards refactor

  Settings (new module):
  - Block phone
  - SOS contacts
  - WiFi networks
  - Alarm refactor with full CRUD
  - Settings menu with feature stubs

  Account:
  - Personal data and account settings refactor

  Shared:
  - 100+ i18n keys in 6 languages
  - New routes in app_router
  - WeekDayChips, TimeRangeSelector shared widgets
  - Legacy dashboard shell simplified
This commit is contained in:
2026-03-16 08:37:52 +01:00
parent ec4e42b408
commit 440bbcac66
352 changed files with 27012 additions and 3898 deletions

View File

@@ -3,6 +3,7 @@ library legacy_shared;
export 'src/providers/selected_device_provider.dart';
export 'src/widgets/layouts/page_layout.dart';
export 'src/components/section_button.dart';
export 'src/widgets/week_day_chips.dart';
export 'src/components/menu_button.dart';
export 'src/data/models/device_response_model.dart';
export 'src/data/models/get_devices_response_model.dart';

View File

@@ -3,14 +3,23 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'send_command_request_model.freezed.dart';
part 'send_command_request_model.g.dart';
enum DeviceCommand {
@JsonValue('FIND_DEVICE')
findDevice,
@JsonValue('REWARDS')
rewards,
@JsonValue('SOUND')
sound,
}
@freezed
abstract class SendCommandRequestModel with _$SendCommandRequestModel {
const factory SendCommandRequestModel({
required String device,
required String command,
required DeviceCommand command,
Map<String, dynamic>? data,
}) = _SendCommandRequestModel;
factory SendCommandRequestModel.fromJson(Map<String, dynamic> json) =>
_$SendCommandRequestModelFromJson(json);
}
}

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SendCommandRequestModel {
String get device; String get command; Map<String, dynamic>? get data;
String get device; DeviceCommand get command; Map<String, dynamic>? get data;
/// Create a copy of SendCommandRequestModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -48,7 +48,7 @@ abstract mixin class $SendCommandRequestModelCopyWith<$Res> {
factory $SendCommandRequestModelCopyWith(SendCommandRequestModel value, $Res Function(SendCommandRequestModel) _then) = _$SendCommandRequestModelCopyWithImpl;
@useResult
$Res call({
String device, String command, Map<String, dynamic>? data
String device, DeviceCommand command, Map<String, dynamic>? data
});
@@ -69,7 +69,7 @@ class _$SendCommandRequestModelCopyWithImpl<$Res>
return _then(_self.copyWith(
device: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as String,command: null == command ? _self.command : command // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as DeviceCommand,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
));
}
@@ -155,7 +155,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String device, String command, Map<String, dynamic>? data)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String device, DeviceCommand command, Map<String, dynamic>? data)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SendCommandRequestModel() when $default != null:
return $default(_that.device,_that.command,_that.data);case _:
@@ -176,7 +176,7 @@ return $default(_that.device,_that.command,_that.data);case _:
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String device, String command, Map<String, dynamic>? data) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String device, DeviceCommand command, Map<String, dynamic>? data) $default,) {final _that = this;
switch (_that) {
case _SendCommandRequestModel():
return $default(_that.device,_that.command,_that.data);case _:
@@ -196,7 +196,7 @@ return $default(_that.device,_that.command,_that.data);case _:
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String device, String command, Map<String, dynamic>? data)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String device, DeviceCommand command, Map<String, dynamic>? data)? $default,) {final _that = this;
switch (_that) {
case _SendCommandRequestModel() when $default != null:
return $default(_that.device,_that.command,_that.data);case _:
@@ -215,7 +215,7 @@ class _SendCommandRequestModel implements SendCommandRequestModel {
factory _SendCommandRequestModel.fromJson(Map<String, dynamic> json) => _$SendCommandRequestModelFromJson(json);
@override final String device;
@override final String command;
@override final DeviceCommand command;
final Map<String, dynamic>? _data;
@override Map<String, dynamic>? get data {
final value = _data;
@@ -259,7 +259,7 @@ abstract mixin class _$SendCommandRequestModelCopyWith<$Res> implements $SendCom
factory _$SendCommandRequestModelCopyWith(_SendCommandRequestModel value, $Res Function(_SendCommandRequestModel) _then) = __$SendCommandRequestModelCopyWithImpl;
@override @useResult
$Res call({
String device, String command, Map<String, dynamic>? data
String device, DeviceCommand command, Map<String, dynamic>? data
});
@@ -280,7 +280,7 @@ class __$SendCommandRequestModelCopyWithImpl<$Res>
return _then(_SendCommandRequestModel(
device: null == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as String,command: null == command ? _self.command : command // ignore: cast_nullable_to_non_nullable
as String,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
as DeviceCommand,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
));
}

View File

@@ -10,7 +10,7 @@ _SendCommandRequestModel _$SendCommandRequestModelFromJson(
Map<String, dynamic> json,
) => _SendCommandRequestModel(
device: json['device'] as String,
command: json['command'] as String,
command: $enumDecode(_$DeviceCommandEnumMap, json['command']),
data: json['data'] as Map<String, dynamic>?,
);
@@ -18,6 +18,12 @@ Map<String, dynamic> _$SendCommandRequestModelToJson(
_SendCommandRequestModel instance,
) => <String, dynamic>{
'device': instance.device,
'command': instance.command,
'command': _$DeviceCommandEnumMap[instance.command]!,
'data': instance.data,
};
const _$DeviceCommandEnumMap = {
DeviceCommand.findDevice: 'FIND_DEVICE',
DeviceCommand.rewards: 'REWARDS',
DeviceCommand.sound: 'SOUND',
};

View File

@@ -4,11 +4,11 @@ import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
import 'package:utils/utils.dart';
class LegacyPageLayout extends StatelessWidget{
class LegacyPageLayout extends StatelessWidget {
final String title;
final Widget body;
final Widget? footer;
final bool showBack;
final bool showEdit;
final VoidCallback? onEditChange;
final ThemePort theme;
@@ -18,6 +18,7 @@ class LegacyPageLayout extends StatelessWidget{
required this.title,
required this.body,
this.footer,
this.showBack = true,
this.showEdit = false,
this.onEditChange,
required this.theme,
@@ -27,66 +28,63 @@ class LegacyPageLayout extends StatelessWidget{
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Container(
padding: SizeUtils.getByScreen(
small: EdgeInsets.fromLTRB(22, 20, 22, 0),
big: EdgeInsets.fromLTRB(21, 16, 21, 0),
appBar: AppBar(
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: true,
automaticallyImplyLeading: false,
leading: showBack
? IconButton(
onPressed: () => GetIt.I<NavigationContract>().goBack(),
icon: Icon(
Icons.adaptive.arrow_back,
color: theme.getColorFor(ThemeCode.legacyPrimary),
size: SizeUtils.getByScreen(small: 32, big: 28),
),
)
: null,
title: Text(
title.toUpperCase(),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
fontWeight: FontWeight.w500,
letterSpacing: 0,
color: theme.getColorFor(ThemeCode.legacyPrimary),
),
),
actions: [
if (showEdit)
Padding(
padding: EdgeInsets.only(
right: SizeUtils.getByScreen(small: 16, big: 14),
),
child: Stack(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(onPressed: () {
GetIt.I<NavigationContract>().goBack();
},
icon: Icon(Icons.arrow_back,
color: theme.getColorFor(ThemeCode.legacyPrimary),
size: 32,
),
padding: EdgeInsets.zero,
),
if (showEdit)
DecoratedBox(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.legacyPrimary),
shape: BoxShape.circle
),
child: IconButton(onPressed: onEditChange,
icon: Icon(Icons.edit_outlined,
color: Colors.white,
size: SizeUtils.getByScreen(small: 30, big: 28),
)
),
)
],
child: DecoratedBox(
decoration: BoxDecoration(
color: theme.getColorFor(ThemeCode.legacyPrimary),
shape: BoxShape.circle,
),
child: IconButton(
onPressed: onEditChange,
icon: Icon(
Icons.edit_outlined,
color: Colors.white,
size: SizeUtils.getByScreen(small: 24, big: 22),
),
SizedBox(
height: 50,
child: Center(
child: Text(title.toUpperCase(),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
fontWeight: FontWeight.w500,
letterSpacing: 0,
color: theme.getColorFor(ThemeCode.legacyPrimary)
),
)
)
)
],
),
),
),
SizedBox(height: SizeUtils.getByScreen(small: 30, big: 28)),
],
),
body: SafeArea(
top: false,
child: Column(
children: [
Expanded(child: body),
?footer
?footer,
],
)
),
),
);
}
}
}

View File

@@ -0,0 +1,135 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/widgets.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
const weekDayI18nKeys = {
1: I18n.monday,
2: I18n.tuesday,
3: I18n.wednesday,
4: I18n.thursday,
5: I18n.friday,
6: I18n.saturday,
7: I18n.sunday,
};
String weekDayShortLabel(BuildContext context, String i18nKey) {
final fullName = context.translate(i18nKey);
return fullName.length > 3 ? fullName.substring(0, 3) : fullName;
}
/// Shared weekday chip selector that supports both single and multi-select.
///
/// **Single select** (for scheduled activities):
/// ```dart
/// WeekDayChips.single(
/// selectedWeekDay: 1,
/// onChanged: (day) => ...,
/// theme: theme,
/// )
/// ```
///
/// **Multi select** (for alarms):
/// ```dart
/// WeekDayChips.multi(
/// selectedDays: [true, false, true, ...],
/// onToggle: (index) => ...,
/// theme: theme,
/// )
/// ```
class WeekDayChips extends StatelessWidget {
/// Single-select mode: which day is selected (1=Mon, 7=Sun).
final int? selectedWeekDay;
/// Single-select callback.
final ValueChanged<int>? onChanged;
/// Multi-select mode: 7 booleans (index 0=Mon, 6=Sun).
final List<bool>? selectedDays;
/// Multi-select callback with day index (0-6).
final ValueChanged<int>? onToggle;
final bool enabled;
final ThemePort theme;
const WeekDayChips.single({
super.key,
required int this.selectedWeekDay,
required ValueChanged<int> this.onChanged,
required this.theme,
this.enabled = true,
}) : selectedDays = null,
onToggle = null;
const WeekDayChips.multi({
super.key,
required List<bool> this.selectedDays,
required ValueChanged<int> this.onToggle,
required this.theme,
this.enabled = true,
}) : selectedWeekDay = null,
onChanged = null;
bool _isMulti() => selectedDays != null;
@override
Widget build(BuildContext context) {
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: weekDayI18nKeys.entries.map((e) {
final dayIndex = e.key - 1; // 0-based
final isSelected = _isMulti()
? selectedDays![dayIndex]
: e.key == selectedWeekDay;
final label = weekDayShortLabel(context, e.value);
return GestureDetector(
onTap: enabled
? () {
if (_isMulti()) {
onToggle!(dayIndex);
} else {
onChanged!(e.key);
}
}
: null,
child: Container(
width: SizeUtils.getByScreen(small: 42, big: 40),
height: SizeUtils.getByScreen(small: 38, big: 36),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected ? primaryColor : const Color(0x00000000),
borderRadius: BorderRadius.circular(
SizeUtils.getByScreen(small: 8, big: 7),
),
border: Border.all(
color: isSelected
? primaryColor
: enabled
? const Color(0xFFBDBDBD)
: const Color(0xFFE0E0E0),
),
),
child: Text(
label,
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
color: isSelected
? const Color(0xFFFFFFFF)
: enabled
? theme.getColorFor(ThemeCode.textPrimary)
: theme
.getColorFor(ThemeCode.textPrimary)
.withValues(alpha: 0.4),
),
),
),
);
}).toList(),
);
}
}