feat(ui): overhaul device details layout
This commit is contained in:
@ -713,9 +713,11 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final connectionData = ref.watch(connectionStatusProvider).valueOrNull;
|
final connectionData = ref.watch(connectionStatusProvider).valueOrNull;
|
||||||
final isCurrentConnected = connectionData != null &&
|
final currentConnectionStatus = connectionData != null &&
|
||||||
connectionData.$1 == ConnectionStatus.connected &&
|
connectionData.$2 == widget.deviceAddress
|
||||||
connectionData.$2 == widget.deviceAddress;
|
? connectionData.$1
|
||||||
|
: ConnectionStatus.disconnected;
|
||||||
|
final isCurrentConnected = currentConnectionStatus == ConnectionStatus.connected;
|
||||||
final canSelectFirmware =
|
final canSelectFirmware =
|
||||||
isCurrentConnected && !_isSelectingFirmware && !_isFirmwareUpdateBusy;
|
isCurrentConnected && !_isSelectingFirmware && !_isFirmwareUpdateBusy;
|
||||||
final canStartFirmware = isCurrentConnected &&
|
final canStartFirmware = isCurrentConnected &&
|
||||||
@ -731,7 +733,7 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Device Details'),
|
title: const Text('Device'),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: _exitPage,
|
onPressed: _exitPage,
|
||||||
@ -741,15 +743,25 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildDeviceInfo(context, ref, widget.deviceAddress),
|
_buildDeviceOverviewCard(
|
||||||
const SizedBox(height: 16),
|
context,
|
||||||
_buildConnectionStatus(context, ref, widget.deviceAddress),
|
ref,
|
||||||
const SizedBox(height: 16),
|
widget.deviceAddress,
|
||||||
|
connectionStatus: currentConnectionStatus,
|
||||||
|
status: _latestStatus,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
if (isCurrentConnected) ...[
|
if (isCurrentConnected) ...[
|
||||||
|
_TrainerConnectionCard(
|
||||||
|
status: _latestStatus,
|
||||||
|
onAssign: _isFirmwareUpdateBusy ? null : _connectButtonToBike,
|
||||||
|
onShowStatusConsole: _showStatusHistory,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
_StatusBanner(
|
_StatusBanner(
|
||||||
status: _latestStatus,
|
status: _latestStatus,
|
||||||
onTap: _showStatusHistory,
|
onTap: _showStatusHistory,
|
||||||
@ -763,16 +775,6 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: FilledButton.icon(
|
|
||||||
onPressed:
|
|
||||||
_isFirmwareUpdateBusy ? null : _connectButtonToBike,
|
|
||||||
icon: const Icon(Icons.link),
|
|
||||||
label: const Text('Connect Button to Bike'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_FirmwareUpdateCard(
|
_FirmwareUpdateCard(
|
||||||
selectedFirmware: _selectedFirmware,
|
selectedFirmware: _selectedFirmware,
|
||||||
progress: _dfuProgress,
|
progress: _dfuProgress,
|
||||||
@ -814,6 +816,8 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
] else ...[
|
||||||
|
const _DisconnectedDetailCard(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -827,8 +831,14 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: 24),
|
margin: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).cardColor,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.outlineVariant
|
||||||
|
.withValues(alpha: 0.55),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -916,23 +926,30 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Card(
|
||||||
decoration: BoxDecoration(
|
child: Padding(
|
||||||
borderRadius: BorderRadius.circular(16),
|
padding: const EdgeInsets.fromLTRB(18, 18, 18, 18),
|
||||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.55),
|
|
||||||
border: Border.all(
|
|
||||||
color: colorScheme.outlineVariant.withValues(alpha: 0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.system_update_alt_rounded, color: colorScheme.primary),
|
||||||
|
const SizedBox(width: 10),
|
||||||
const Text(
|
const Text(
|
||||||
'Firmware Update',
|
'Firmware Update',
|
||||||
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Select a firmware image, review the transfer state, and start the update when ready.',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.68),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
@ -966,12 +983,23 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 14),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.54),
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
selectedFirmware == null
|
selectedFirmware == null
|
||||||
? 'Selected file: none'
|
? 'Selected file: none'
|
||||||
: 'Selected file: ${selectedFirmware!.fileName}',
|
: 'Selected file: ${selectedFirmware!.fileName}',
|
||||||
style: theme.textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (selectedFirmware != null) ...[
|
if (selectedFirmware != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@ -980,12 +1008,17 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
|||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 10),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
Text('Phase: $phaseText', style: theme.textTheme.bodyMedium),
|
Text('Phase: $phaseText', style: theme.textTheme.bodyMedium),
|
||||||
if (_showProgress) ...[
|
if (_showProgress) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
value: progress.totalBytes > 0 ? progress.fractionComplete : 0,
|
value: progress.totalBytes > 0 ? progress.fractionComplete : 0,
|
||||||
|
minHeight: 10,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
@ -1016,6 +1049,7 @@ class _FirmwareUpdateCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1038,23 +1072,42 @@ class _StatusBanner extends StatelessWidget {
|
|||||||
final text = status?.statusLine ?? 'Waiting for status updates...';
|
final text = status?.statusLine ?? 'Waiting for status updates...';
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: color.withValues(alpha: 0.16),
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(22),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(22),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.32)),
|
||||||
|
color: color.withValues(alpha: 0.08),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.memory, color: color),
|
Icon(Icons.memory_rounded, color: color),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Live Status',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
text,
|
text,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (onErrorInfoTap != null &&
|
if (onErrorInfoTap != null &&
|
||||||
status != null &&
|
status != null &&
|
||||||
@ -1070,6 +1123,7 @@ class _StatusBanner extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1093,10 +1147,14 @@ class _StatusBanner extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDeviceInfo(
|
Widget _buildDeviceOverviewCard(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
String deviceAddress,
|
String deviceAddress,
|
||||||
|
{
|
||||||
|
required ConnectionStatus connectionStatus,
|
||||||
|
required CentralStatus? status,
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
final asyncSavedDevices = ref.watch(nConnectedDevicesProvider);
|
final asyncSavedDevices = ref.watch(nConnectedDevicesProvider);
|
||||||
|
|
||||||
@ -1112,79 +1170,351 @@ Widget _buildDeviceInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentDeviceData == null) {
|
if (currentDeviceData == null) {
|
||||||
return Center(
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
child: Text('Device details not found for $deviceAddress.'),
|
child: Text('Device details not found for $deviceAddress.'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
// TODO(yannik): Replace these overview placeholder metrics with actual
|
||||||
|
// battery, signal, and firmware values once the device exposes them.
|
||||||
|
return _DeviceOverviewCard(
|
||||||
|
device: currentDeviceData,
|
||||||
|
connectionStatus: connectionStatus,
|
||||||
|
status: status,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(18),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, stackTrace) => Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Text('Error loading device info: $error'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeviceOverviewCard extends StatelessWidget {
|
||||||
|
const _DeviceOverviewCard({
|
||||||
|
required this.device,
|
||||||
|
required this.connectionStatus,
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ConnectedDevice device;
|
||||||
|
final ConnectionStatus connectionStatus;
|
||||||
|
final CentralStatus? status;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final trainerAddress = status?.connectedTrainerAddr == null
|
||||||
|
? 'No trainer assigned yet'
|
||||||
|
: formatMacAddressFromLittleEndian(status!.connectedTrainerAddr!);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/shifter-wireframe.png',
|
||||||
|
width: 104,
|
||||||
|
height: 78,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Name: ${currentDeviceData.deviceName}',
|
device.deviceName,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
_DetailStatusChip(status: connectionStatus),
|
||||||
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
'Address: ${currentDeviceData.deviceAddress}',
|
trainerAddress,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.68),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
Row(
|
||||||
|
children: const [
|
||||||
|
Expanded(
|
||||||
|
child: _OverviewMetricTile(
|
||||||
|
label: 'Battery',
|
||||||
|
value: '--',
|
||||||
|
icon: Icons.battery_charging_full_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: _OverviewMetricTile(
|
||||||
|
label: 'Signal',
|
||||||
|
value: 'Ready',
|
||||||
|
icon: Icons.signal_cellular_alt_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: _OverviewMetricTile(
|
||||||
|
label: 'Firmware',
|
||||||
|
value: '--',
|
||||||
|
icon: Icons.memory_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Text(
|
||||||
|
device.deviceAddress,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.62),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Type: ${currentDeviceData.deviceType}',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
|
||||||
error: (error, stackTrace) =>
|
|
||||||
Center(child: Text('Error loading device info: $error')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConnectionStatus(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
String deviceAddress,
|
|
||||||
) {
|
|
||||||
final asyncConnectionStatus = ref.watch(connectionStatusProvider);
|
|
||||||
|
|
||||||
return asyncConnectionStatus.when(
|
|
||||||
data: (data) {
|
|
||||||
final (status, connectedDeviceId) = data;
|
|
||||||
String statusText;
|
|
||||||
final isCurrentDeviceConnected =
|
|
||||||
connectedDeviceId != null && connectedDeviceId == deviceAddress;
|
|
||||||
|
|
||||||
if (isCurrentDeviceConnected) {
|
|
||||||
switch (status) {
|
|
||||||
case ConnectionStatus.connected:
|
|
||||||
statusText = 'Status: Connected';
|
|
||||||
break;
|
|
||||||
case ConnectionStatus.connecting:
|
|
||||||
statusText = 'Status: Connecting...';
|
|
||||||
break;
|
|
||||||
case ConnectionStatus.disconnecting:
|
|
||||||
statusText = 'Status: Disconnecting...';
|
|
||||||
break;
|
|
||||||
case ConnectionStatus.disconnected:
|
|
||||||
statusText = 'Status: Disconnected';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statusText = 'Status: Disconnected';
|
|
||||||
}
|
|
||||||
|
|
||||||
return Text(statusText, style: Theme.of(context).textTheme.titleMedium);
|
|
||||||
},
|
|
||||||
loading: () => const Text(
|
|
||||||
'Status: Unknown',
|
|
||||||
style: TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
),
|
),
|
||||||
error: (error, stackTrace) => Text(
|
|
||||||
'Status: Error ($error)',
|
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrainerConnectionCard extends StatelessWidget {
|
||||||
|
const _TrainerConnectionCard({
|
||||||
|
required this.status,
|
||||||
|
required this.onAssign,
|
||||||
|
required this.onShowStatusConsole,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CentralStatus? status;
|
||||||
|
final VoidCallback? onAssign;
|
||||||
|
final VoidCallback onShowStatusConsole;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final trainerText = status?.connectedTrainerAddr == null
|
||||||
|
? 'No trainer assigned yet.'
|
||||||
|
: 'Assigned: ${formatMacAddressFromLittleEndian(status!.connectedTrainerAddr!)}';
|
||||||
|
final readinessText = status?.trainer.label ?? 'Waiting for trainer status';
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 46,
|
||||||
|
height: 46,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: colorScheme.primary.withValues(alpha: 0.12),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.pedal_bike_rounded, color: colorScheme.primary),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Trainer Assignment',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
trainerText,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.68),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.54),
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.bluetooth_connected_rounded, color: colorScheme.primary),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
readinessText,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: onAssign,
|
||||||
|
icon: const Icon(Icons.link_rounded),
|
||||||
|
label: const Text('Assign Trainer'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: onShowStatusConsole,
|
||||||
|
icon: const Icon(Icons.subject_rounded),
|
||||||
|
label: const Text('Status Console'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DisconnectedDetailCard extends StatelessWidget {
|
||||||
|
const _DisconnectedDetailCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
child: Text(
|
||||||
|
'This device is not currently connected. Reopen it from Devices to reconnect and manage trainer pairing, firmware, and gear ratios.',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.68),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DetailStatusChip extends StatelessWidget {
|
||||||
|
const _DetailStatusChip({required this.status});
|
||||||
|
|
||||||
|
final ConnectionStatus status;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final (label, color) = switch (status) {
|
||||||
|
ConnectionStatus.connected => ('Connected', const Color(0xFF40C979)),
|
||||||
|
ConnectionStatus.connecting => ('Connecting', const Color(0xFFFFB649)),
|
||||||
|
ConnectionStatus.disconnecting => ('Disconnecting', const Color(0xFFFFB649)),
|
||||||
|
ConnectionStatus.disconnected =>
|
||||||
|
('Disconnected', Theme.of(context).colorScheme.primary),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.24)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle_outline, size: 14, color: color),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverviewMetricTile extends StatelessWidget {
|
||||||
|
const _OverviewMetricTile({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.54),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: colorScheme.primary),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.62),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user