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:
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user