From e901b229818c62e82bff3f3239c52e65794325cf Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Sun, 26 Apr 2026 05:52:27 +0200 Subject: [PATCH] feat(location): redesign device banner to match reference UI --- .../presentation/widgets/device_banner.dart | 218 +++++++++--------- packages/sf_localizations/assets/l10n/de.json | 3 + packages/sf_localizations/assets/l10n/en.json | 3 + packages/sf_localizations/assets/l10n/es.json | 3 + packages/sf_localizations/assets/l10n/fr.json | 3 + packages/sf_localizations/assets/l10n/it.json | 3 + packages/sf_localizations/assets/l10n/pt.json | 3 + .../lib/src/generated/i18n.dart | 3 + 8 files changed, 136 insertions(+), 103 deletions(-) diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart index 1e379646..e4273e7f 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart @@ -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 { @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 { 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 { 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 { 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().where((s) => s.isNotEmpty).join(', ') + position!.address?.country, + ] + .whereType() + .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), + ], + ); + } +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 2a6edad7..a587bd42 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -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", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index a47380bd..f890ee8f 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -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", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index d92331a6..bfde4563 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -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", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index 15e45118..270cad05 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -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", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index 2f722296..b5f1739d 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -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", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 7e3820d3..ddb6440d 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -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", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 18a2dd31..3c310560 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -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';