feat(location): redesign device banner to match reference UI
This commit is contained in:
@@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user