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

This commit is contained in:
2026-04-26 05:52:27 +02:00
parent 2d87cd5aee
commit e901b22981
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_device_state/legacy_device_state.dart';
import 'package:legacy_theme/legacy_theme.dart'; import 'package:legacy_theme/legacy_theme.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart'; import 'package:sf_shared/sf_shared.dart';
import 'package:utils/utils.dart'; import 'package:utils/utils.dart';
@@ -63,12 +63,11 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final primaryColor = context.sfColors.legacyPrimary;
return Container( return Container(
width: SizeUtils.getByScreen(small: 340, big: 338),
margin: EdgeInsets.only( margin: EdgeInsets.only(
bottom: SizeUtils.getByScreen(small: 16, big: 14), bottom: SizeUtils.getByScreen(small: 16, big: 14),
left: 16,
right: 16,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
@@ -85,24 +84,26 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox( SizedBox(
height: 72, height: 150,
child: PageView.builder( child: PageView.builder(
controller: _pageController, controller: _pageController,
itemCount: widget.devices.length, itemCount: widget.devices.length,
onPageChanged: (index) => onPageChanged: (index) =>
widget.onDeviceChanged(widget.devices[index]), widget.onDeviceChanged(widget.devices[index]),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final dev = widget.devices[index]; final device = widget.devices[index];
final pos = widget.positions final position = widget.positions
.where((p) => p.deviceIdentificator == dev.identificator) .where(
(p) =>
p.deviceIdentificator == device.identificator,
)
.firstOrNull; .firstOrNull;
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: widget.onTap, onTap: widget.onTap,
child: _DeviceCard( child: _DeviceCard(
device: dev, device: device,
position: pos, position: position,
primaryColor: primaryColor,
), ),
); );
}, },
@@ -123,7 +124,7 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
color: i == _currentPage color: i == _currentPage
? primaryColor ? context.sfColors.legacyPrimary
: Theme.of(context).colorScheme.outline, : Theme.of(context).colorScheme.outline,
), ),
), ),
@@ -139,137 +140,148 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
class _DeviceCard extends StatelessWidget { class _DeviceCard extends StatelessWidget {
final DeviceEntity device; final DeviceEntity device;
final PositionEntity? position; final PositionEntity? position;
final Color primaryColor;
const _DeviceCard({ const _DeviceCard({
required this.device, required this.device,
required this.position, required this.position,
required this.primaryColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final name = device.carrierName; final primaryColor = context.sfColors.legacyPrimary;
final deviceName = name != null && name.isNotEmpty final valueStyle = TextStyle(
? name fontSize: 13,
: device.identificator; color: Theme.of(context).colorScheme.onSurface,
final initial = deviceName.isNotEmpty ? deviceName[0].toUpperCase() : '?'; );
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?.street,
position!.address?.city,
position!.address?.province, 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( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Container( Image.asset(
width: 42, 'assets/shared/images/iso_sf.png',
height: 42, width: 60,
decoration: BoxDecoration( height: 60,
color: primaryColor.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Center(
child: Text(
initial,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: primaryColor,
),
),
),
), ),
const SizedBox(width: 10), const SizedBox(width: 14),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
deviceName, deviceName,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurface, color: primaryColor,
), ),
overflow: TextOverflow.ellipsis, 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(
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( child: Row(
children: [ children: [
Icon( if (device.battery != null) ...[
Icons.location_on, Text('${device.battery}%', style: valueStyle),
size: 12, const SizedBox(width: 6),
color: Theme.of(context).colorScheme.outline, Icon(
), toBatteryIcon(device.battery!),
const SizedBox(width: 3), size: 18,
Expanded( color: device.battery! > 20
child: Text( ? Colors.green
addressText, : Colors.orange,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
overflow: TextOverflow.ellipsis,
), ),
), ],
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", "locationMapStyleStandard": "Standard",
"locationMapStyleVoyager": "Reisender", "locationMapStyleVoyager": "Reisender",
"locationFrequencyManual": "Manuell", "locationFrequencyManual": "Manuell",
"locationBannerAddress": "ADRESSE",
"locationBannerDateTime": "DATUM/UHRZEIT",
"locationBannerBattery": "BATTERIE",
"positionUpdated": "Letzte verfügbare Position aktualisiert", "positionUpdated": "Letzte verfügbare Position aktualisiert",
"locationMapStyleLight": "Hell", "locationMapStyleLight": "Hell",
"locationMapStyleDark": "Dunkel", "locationMapStyleDark": "Dunkel",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -620,6 +620,9 @@ class I18n {
static const String locationMapStyleStandard = 'locationMapStyleStandard'; static const String locationMapStyleStandard = 'locationMapStyleStandard';
static const String locationMapStyleVoyager = 'locationMapStyleVoyager'; static const String locationMapStyleVoyager = 'locationMapStyleVoyager';
static const String locationFrequencyManual = 'locationFrequencyManual'; 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 positionUpdated = 'positionUpdated';
static const String locationNewFrequentPlace = 'locationNewFrequentPlace'; static const String locationNewFrequentPlace = 'locationNewFrequentPlace';
static const String locationNewGeofence = 'locationNewGeofence'; static const String locationNewGeofence = 'locationNewGeofence';