feat(location): redesign device banner to match reference UI

This commit is contained in:
2026-04-26 05:52:27 +02:00
parent 4deb263c7e
commit 6ed36dba75
8 changed files with 136 additions and 103 deletions

View File

@@ -1,8 +1,8 @@
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:utils/utils.dart';
@@ -63,12 +63,11 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
@override
Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
return Container(
width: SizeUtils.getByScreen(small: 340, big: 338),
margin: EdgeInsets.only(
bottom: SizeUtils.getByScreen(small: 16, big: 14),
left: 16,
right: 16,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
@@ -85,24 +84,26 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 72,
height: 150,
child: PageView.builder(
controller: _pageController,
itemCount: widget.devices.length,
onPageChanged: (index) =>
widget.onDeviceChanged(widget.devices[index]),
itemBuilder: (context, index) {
final dev = widget.devices[index];
final pos = widget.positions
.where((p) => p.deviceIdentificator == dev.identificator)
final device = widget.devices[index];
final position = widget.positions
.where(
(p) =>
p.deviceIdentificator == device.identificator,
)
.firstOrNull;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.onTap,
child: _DeviceCard(
device: dev,
position: pos,
primaryColor: primaryColor,
device: device,
position: position,
),
);
},
@@ -123,7 +124,7 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: i == _currentPage
? primaryColor
? context.sfColors.legacyPrimary
: Theme.of(context).colorScheme.outline,
),
),
@@ -139,137 +140,148 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
class _DeviceCard extends StatelessWidget {
final DeviceEntity device;
final PositionEntity? position;
final Color primaryColor;
const _DeviceCard({
required this.device,
required this.position,
required this.primaryColor,
});
@override
Widget build(BuildContext context) {
final name = device.carrierName;
final deviceName = name != null && name.isNotEmpty
? name
: device.identificator;
final initial = deviceName.isNotEmpty ? deviceName[0].toUpperCase() : '?';
final primaryColor = context.sfColors.legacyPrimary;
final valueStyle = TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurface,
);
final labelStyle = TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: primaryColor,
);
final addressText = position != null
final name = device.carrierName;
final deviceName =
name != null && name.isNotEmpty ? name : device.identificator;
final address = position != null
? [
position!.address?.street,
position!.address?.city,
position!.address?.province,
].whereType<String>().where((s) => s.isNotEmpty).join(', ')
position!.address?.country,
]
.whereType<String>()
.where((s) => s.isNotEmpty && s != 'Unknown')
.join(', ')
: '';
final dateText =
position != null ? formatPositionDate(position!.positionDate) : '';
final positionType = position?.type ?? '';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Center(
child: Text(
initial,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
),
Image.asset(
'assets/shared/images/iso_sf.png',
width: 60,
height: 60,
),
const SizedBox(width: 10),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
deviceName,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w700,
color: primaryColor,
),
overflow: TextOverflow.ellipsis,
),
if (addressText.isNotEmpty)
const SizedBox(height: 8),
if (address.isNotEmpty)
_InfoRow(
label: context.translate(I18n.locationBannerAddress),
labelStyle: labelStyle,
child: Text(
address,
style: valueStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (dateText.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
padding: const EdgeInsets.only(top: 4),
child: _InfoRow(
label: context.translate(I18n.locationBannerDateTime),
labelStyle: labelStyle,
child: Text(dateText, style: valueStyle),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: _InfoRow(
label: context.translate(I18n.locationBannerBattery),
labelStyle: labelStyle,
child: Row(
children: [
Icon(
Icons.location_on,
size: 12,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(width: 3),
Expanded(
child: Text(
addressText,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
overflow: TextOverflow.ellipsis,
if (device.battery != null) ...[
Text('${device.battery}%', style: valueStyle),
const SizedBox(width: 6),
Icon(
toBatteryIcon(device.battery!),
size: 18,
color: device.battery! > 20
? Colors.green
: Colors.orange,
),
),
],
if (positionType.isNotEmpty) ...[
const SizedBox(width: 12),
Text(positionType, style: valueStyle),
],
],
),
),
),
],
),
),
if (position != null || device.battery != null)
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (device.battery != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
toBatteryIcon(device.battery!),
size: 16,
color: device.battery! > 20
? primaryColor
: Colors.orange,
),
const SizedBox(width: 3),
Text(
'${device.battery}%',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: device.battery! > 20
? primaryColor
: Colors.orange,
),
),
],
),
if (position != null)
Padding(
padding: const EdgeInsets.only(top: 3),
child: Text(
formatPositionDate(position!.positionDate),
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
],
),
);
}
}
class _InfoRow extends StatelessWidget {
final String label;
final TextStyle labelStyle;
final Widget child;
const _InfoRow({
required this.label,
required this.labelStyle,
required this.child,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(label, style: labelStyle),
),
Expanded(child: child),
],
);
}
}

View File

@@ -664,6 +664,9 @@
"locationMapStyleStandard": "Standard",
"locationMapStyleVoyager": "Reisender",
"locationFrequencyManual": "Manuell",
"locationBannerAddress": "ADRESSE",
"locationBannerDateTime": "DATUM/UHRZEIT",
"locationBannerBattery": "BATTERIE",
"positionUpdated": "Letzte verfügbare Position aktualisiert",
"locationMapStyleLight": "Hell",
"locationMapStyleDark": "Dunkel",

View File

@@ -844,6 +844,9 @@
"locationMapStyleStandard": "Standard",
"locationMapStyleVoyager": "Voyager",
"locationFrequencyManual": "Manual",
"locationBannerAddress": "ADDRESS",
"locationBannerDateTime": "DATE/TIME",
"locationBannerBattery": "BATTERY",
"positionUpdated": "Updated to latest available position",
"locationMapStyleLight": "Light",
"locationMapStyleDark": "Dark",

View File

@@ -845,6 +845,9 @@
"locationMapStyleStandard": "Estándar",
"locationMapStyleVoyager": "Viajero",
"locationFrequencyManual": "Manual",
"locationBannerAddress": "DIRECCIÓN",
"locationBannerDateTime": "FECHA/HORA",
"locationBannerBattery": "BATERÍA",
"positionUpdated": "Última posición disponible actualizada",
"locationMapStyleLight": "Claro",
"locationMapStyleDark": "Oscuro",

View File

@@ -664,6 +664,9 @@
"locationMapStyleStandard": "Standard",
"locationMapStyleVoyager": "Voyageur",
"locationFrequencyManual": "Manuel",
"locationBannerAddress": "ADRESSE",
"locationBannerDateTime": "DATE/HEURE",
"locationBannerBattery": "BATTERIE",
"positionUpdated": "Dernière position disponible mise à jour",
"locationMapStyleLight": "Clair",
"locationMapStyleDark": "Sombre",

View File

@@ -664,6 +664,9 @@
"locationMapStyleStandard": "Standard",
"locationMapStyleVoyager": "Viaggiatore",
"locationFrequencyManual": "Manuale",
"locationBannerAddress": "INDIRIZZO",
"locationBannerDateTime": "DATA/ORA",
"locationBannerBattery": "BATTERIA",
"positionUpdated": "Ultima posizione disponibile aggiornata",
"locationMapStyleLight": "Chiaro",
"locationMapStyleDark": "Scuro",

View File

@@ -664,6 +664,9 @@
"locationMapStyleStandard": "Padrão",
"locationMapStyleVoyager": "Viajante",
"locationFrequencyManual": "Manual",
"locationBannerAddress": "ENDEREÇO",
"locationBannerDateTime": "DATA/HORA",
"locationBannerBattery": "BATERIA",
"positionUpdated": "Última posição disponível atualizada",
"locationMapStyleLight": "Claro",
"locationMapStyleDark": "Escuro",

View File

@@ -620,6 +620,9 @@ class I18n {
static const String locationMapStyleStandard = 'locationMapStyleStandard';
static const String locationMapStyleVoyager = 'locationMapStyleVoyager';
static const String locationFrequencyManual = 'locationFrequencyManual';
static const String locationBannerAddress = 'locationBannerAddress';
static const String locationBannerDateTime = 'locationBannerDateTime';
static const String locationBannerBattery = 'locationBannerBattery';
static const String positionUpdated = 'positionUpdated';
static const String locationNewFrequentPlace = 'locationNewFrequentPlace';
static const String locationNewGeofence = 'locationNewGeofence';