mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add faster location update channel
This commit is contained in:
@@ -27,6 +27,8 @@ import '../utils/battery_display_helper.dart';
|
|||||||
|
|
||||||
enum _HomeTab { messages, contacts, sensors, map }
|
enum _HomeTab { messages, contacts, sensors, map }
|
||||||
|
|
||||||
|
enum _AdvertMode { flood, direct }
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
final Function(AppThemeMode) onThemeChanged;
|
final Function(AppThemeMode) onThemeChanged;
|
||||||
final Function(Locale?) onLocaleChanged;
|
final Function(Locale?) onLocaleChanged;
|
||||||
@@ -245,7 +247,187 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _advertiseDevice(BuildContext context) async {
|
Future<void> _triggerAdvertFeedback() async {
|
||||||
|
final platform = Theme.of(context).platform;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (platform == TargetPlatform.iOS) {
|
||||||
|
await HapticFeedback.lightImpact();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
await HapticFeedback.lightImpact();
|
||||||
|
} else {
|
||||||
|
if (await Vibration.hasVibrator()) {
|
||||||
|
await Vibration.vibrate(duration: 50);
|
||||||
|
} else {
|
||||||
|
await HapticFeedback.mediumImpact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Haptic feedback error: $e');
|
||||||
|
await HapticFeedback.vibrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_AdvertMode?> _showAdvertModeSheet(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return showModalBottomSheet<_AdvertMode>(
|
||||||
|
context: context,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (context) => SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Advert mode',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'Choose how far this announcement should travel.',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
leading: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.hub_rounded,
|
||||||
|
color: theme.colorScheme.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(l10n.flood),
|
||||||
|
subtitle: const Text('Relay through repeaters across the mesh'),
|
||||||
|
trailing: const Icon(Icons.chevron_right_rounded),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.of(context).pop(_AdvertMode.flood),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
leading: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.near_me_rounded,
|
||||||
|
color: theme.colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(l10n.direct),
|
||||||
|
subtitle: const Text('Nearby only, without repeater flooding'),
|
||||||
|
trailing: const Icon(Icons.chevron_right_rounded),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
onTap: () => Navigator.of(context).pop(_AdvertMode.direct),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActivityBadge({
|
||||||
|
required String label,
|
||||||
|
required int count,
|
||||||
|
required bool isActive,
|
||||||
|
required Color activeColor,
|
||||||
|
}) {
|
||||||
|
final color = isActive ? activeColor : Colors.grey;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'$label:$count',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCompactActivityIndicator({
|
||||||
|
required bool rxActive,
|
||||||
|
required bool txActive,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.06),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: rxActive ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Container(
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: txActive ? Colors.blue : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _advertiseDevice(
|
||||||
|
BuildContext context, {
|
||||||
|
bool floodMode = true,
|
||||||
|
}) async {
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
|
|
||||||
if (!connectionProvider.deviceInfo.isConnected) {
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
@@ -325,8 +507,14 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
// Small delay to ensure the lat/lon is set
|
// Small delay to ensure the lat/lon is set
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// Send flood advertisement
|
await connectionProvider.sendSelfAdvert(floodMode: floodMode);
|
||||||
await connectionProvider.sendSelfAdvert(floodMode: true);
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ToastLogger.success(
|
||||||
|
context,
|
||||||
|
floodMode ? 'Flood advert sent' : 'Direct advert sent',
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Failed to advertise device: $e');
|
debugPrint('❌ Failed to advertise device: $e');
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@@ -367,6 +555,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
appBar: shouldHideUI
|
appBar: shouldHideUI
|
||||||
? null
|
? null
|
||||||
: AppBar(
|
: AppBar(
|
||||||
|
toolbarHeight: 64,
|
||||||
|
titleSpacing: 8,
|
||||||
title: _buildCompactStatusBar(),
|
title: _buildCompactStatusBar(),
|
||||||
actions: [
|
actions: [
|
||||||
Consumer<ConnectionProvider>(
|
Consumer<ConnectionProvider>(
|
||||||
@@ -565,7 +755,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
final deviceInfo = provider.deviceInfo;
|
final deviceInfo = provider.deviceInfo;
|
||||||
final isConnected = deviceInfo.isConnected;
|
final isConnected = deviceInfo.isConnected;
|
||||||
final isTcpConnected = provider.connectionMode == ConnectionMode.tcp;
|
final isTcpConnected = provider.connectionMode == ConnectionMode.tcp;
|
||||||
final isBleConnected = isConnected && !isTcpConnected;
|
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
// Disconnected state: show connect button
|
// Disconnected state: show connect button
|
||||||
@@ -627,64 +816,74 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connected state: LEFT | CENTER | RIGHT layout
|
final theme = Theme.of(context);
|
||||||
|
final subtitleColor = theme.colorScheme.onSurfaceVariant;
|
||||||
|
final signalColor = isTcpConnected
|
||||||
|
? Colors.green
|
||||||
|
: (deviceInfo.signalRssi != null
|
||||||
|
? BatteryDisplayHelper.getSignalColor(deviceInfo.signalRssi!)
|
||||||
|
: Colors.grey);
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final isTight = constraints.maxWidth < 360;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
// LEFT: Name + BT/Battery + Cog
|
Flexible(
|
||||||
Expanded(
|
fit: FlexFit.loose,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest
|
||||||
|
.withValues(alpha: 0.55),
|
||||||
|
borderRadius: BorderRadius.circular(22),
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
deviceInfo.selfName ??
|
deviceInfo.selfName ??
|
||||||
AppLocalizations.of(context)!.appTitle,
|
AppLocalizations.of(context)!.appTitle,
|
||||||
style: const TextStyle(
|
style: (isTight
|
||||||
fontSize: 18,
|
? theme.textTheme.titleSmall
|
||||||
fontWeight: FontWeight.bold,
|
: theme.textTheme.titleMedium)
|
||||||
),
|
?.copyWith(fontWeight: FontWeight.w700),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isTcpConnected
|
isTcpConnected
|
||||||
? Icons.wifi
|
? Icons.wifi_rounded
|
||||||
: Icons.bluetooth_connected,
|
: Icons.bluetooth_connected_rounded,
|
||||||
color: isTcpConnected
|
|
||||||
? Colors.green
|
|
||||||
: (deviceInfo.signalRssi != null
|
|
||||||
? BatteryDisplayHelper.getSignalColor(
|
|
||||||
deviceInfo.signalRssi!,
|
|
||||||
)
|
|
||||||
: Colors.grey),
|
|
||||||
size: 13,
|
size: 13,
|
||||||
|
color: signalColor,
|
||||||
),
|
),
|
||||||
if (isBleConnected &&
|
if (!isTcpConnected &&
|
||||||
deviceInfo.signalRssi != null) ...[
|
deviceInfo.signalRssi != null) ...[
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 4),
|
||||||
Text(
|
SizedBox(
|
||||||
|
width: 28,
|
||||||
|
child: Text(
|
||||||
'${deviceInfo.signalRssi}',
|
'${deviceInfo.signalRssi}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
color: BatteryDisplayHelper.getSignalColor(
|
fontWeight: FontWeight.w600,
|
||||||
deviceInfo.signalRssi!,
|
color: signalColor,
|
||||||
),
|
),
|
||||||
),
|
maxLines: 1,
|
||||||
),
|
|
||||||
],
|
|
||||||
if (isTcpConnected) ...[
|
|
||||||
const SizedBox(width: 3),
|
|
||||||
Text(
|
|
||||||
'WiFi',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -694,30 +893,37 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
BatteryDisplayHelper.getBatteryIcon(
|
BatteryDisplayHelper.getBatteryIcon(
|
||||||
deviceInfo.batteryPercent!,
|
deviceInfo.batteryPercent!,
|
||||||
),
|
),
|
||||||
color: BatteryDisplayHelper.getBatteryColor(
|
|
||||||
deviceInfo.batteryPercent!,
|
|
||||||
),
|
|
||||||
size: 13,
|
size: 13,
|
||||||
|
color:
|
||||||
|
BatteryDisplayHelper.getBatteryColor(
|
||||||
|
deviceInfo.batteryPercent!,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 3),
|
),
|
||||||
Text(
|
const SizedBox(width: 4),
|
||||||
|
SizedBox(
|
||||||
|
width: 30,
|
||||||
|
child: Text(
|
||||||
'${deviceInfo.batteryPercent!.round()}%',
|
'${deviceInfo.batteryPercent!.round()}%',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
color: BatteryDisplayHelper.getBatteryColor(
|
fontWeight: FontWeight.w600,
|
||||||
|
color:
|
||||||
|
BatteryDisplayHelper.getBatteryColor(
|
||||||
deviceInfo.batteryPercent!,
|
deviceInfo.batteryPercent!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 4),
|
||||||
GestureDetector(
|
IconButton(
|
||||||
onTap: () {
|
onPressed: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -735,60 +941,65 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
tooltip: AppLocalizations.of(context)!.settings,
|
||||||
|
icon: const Icon(Icons.tune_rounded),
|
||||||
|
color: subtitleColor,
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: theme.colorScheme.surface,
|
||||||
|
foregroundColor: subtitleColor,
|
||||||
|
minimumSize: Size.square(isTight ? 38 : 40),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
await _triggerAdvertFeedback();
|
||||||
|
if (!mounted || !context.mounted) return;
|
||||||
|
await _advertiseDevice(context);
|
||||||
|
},
|
||||||
|
onLongPress: () async {
|
||||||
|
await _triggerAdvertFeedback();
|
||||||
|
if (!mounted || !context.mounted) return;
|
||||||
|
|
||||||
|
final mode = await _showAdvertModeSheet(context);
|
||||||
|
if (!mounted || !context.mounted || mode == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _advertiseDevice(
|
||||||
|
context,
|
||||||
|
floodMode: mode == _AdvertMode.flood,
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 32,
|
width: isTight ? 38 : 40,
|
||||||
height: 32,
|
height: isTight ? 38 : 40,
|
||||||
alignment: Alignment.center,
|
decoration: BoxDecoration(
|
||||||
child: const Icon(Icons.settings, size: 18),
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
theme.colorScheme.primary.withValues(
|
||||||
|
alpha: 0.78,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.campaign_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: isTight ? 18 : 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// CENTER: Broadcast button
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
// Capture platform before async operations
|
|
||||||
final platform = Theme.of(context).platform;
|
|
||||||
// iOS: Use haptic feedback (always works)
|
|
||||||
// Android: Try vibration package for better control
|
|
||||||
try {
|
|
||||||
if (platform == TargetPlatform.iOS) {
|
|
||||||
// iOS: Try multiple haptic types for reliability
|
|
||||||
await HapticFeedback.lightImpact();
|
|
||||||
await Future.delayed(const Duration(milliseconds: 50));
|
|
||||||
await HapticFeedback.lightImpact();
|
|
||||||
} else {
|
|
||||||
// Android vibration
|
|
||||||
if (await Vibration.hasVibrator()) {
|
|
||||||
await Vibration.vibrate(duration: 50);
|
|
||||||
} else {
|
|
||||||
await HapticFeedback.mediumImpact();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback if anything fails
|
|
||||||
debugPrint('Haptic feedback error: $e');
|
|
||||||
await HapticFeedback.vibrate();
|
|
||||||
}
|
|
||||||
if (!mounted) return;
|
|
||||||
if (!context.mounted) return;
|
|
||||||
_advertiseDevice(context);
|
|
||||||
},
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue.shade700,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
minimumSize: const Size(40, 40),
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.campaign, size: 20),
|
SizedBox(width: isTight ? 8 : 12),
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
|
|
||||||
// RIGHT: RX/TX indicators
|
|
||||||
if (_showRxTxIndicators)
|
if (_showRxTxIndicators)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
@@ -800,68 +1011,50 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
child: isTight
|
||||||
|
? _buildCompactActivityIndicator(
|
||||||
|
rxActive: provider.rxActivity,
|
||||||
|
txActive: provider.txActivity,
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHigh
|
||||||
|
.withValues(alpha: 0.85),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
_buildActivityBadge(
|
||||||
mainAxisSize: MainAxisSize.min,
|
label: 'RX',
|
||||||
children: [
|
count: provider.rxPacketCount,
|
||||||
Container(
|
isActive: provider.rxActivity,
|
||||||
width: 7,
|
activeColor: Colors.green,
|
||||||
height: 7,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: provider.rxActivity
|
|
||||||
? Colors.green
|
|
||||||
: Colors.grey.withValues(alpha: 0.3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 3),
|
|
||||||
Text(
|
|
||||||
'RX:${provider.rxPacketCount}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_buildActivityBadge(
|
||||||
|
label: 'TX',
|
||||||
|
count: provider.txPacketCount,
|
||||||
|
isActive: provider.txActivity,
|
||||||
|
activeColor: Colors.blue,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 7,
|
|
||||||
height: 7,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: provider.txActivity
|
|
||||||
? Colors.blue
|
|
||||||
: Colors.grey.withValues(alpha: 0.3),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 3),
|
|
||||||
Text(
|
|
||||||
'TX:${provider.txPacketCount}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const SizedBox(
|
SizedBox(width: isTight ? 24 : 74),
|
||||||
width: 52,
|
|
||||||
), // Placeholder to maintain layout balance
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build tab icon with badge showing count
|
/// Build tab icon with badge showing count
|
||||||
|
|||||||
Reference in New Issue
Block a user