format dart files

formats all dart files using `dart format .` from the root project dir

this makes the code style repeatable by new contributors and makes PR review easier
This commit is contained in:
446564
2026-02-04 08:32:35 -08:00
parent 488a286701
commit b34d684e67
66 changed files with 2882 additions and 1848 deletions
+1 -4
View File
@@ -29,10 +29,7 @@ BatteryUi batteryUiForPercent(int? percent) {
class BatteryIndicator extends StatefulWidget {
final MeshCoreConnector connector;
const BatteryIndicator({
super.key,
required this.connector,
});
const BatteryIndicator({super.key, required this.connector});
@override
State<BatteryIndicator> createState() => _BatteryIndicatorState();
+20 -6
View File
@@ -5,7 +5,11 @@ import '../connector/meshcore_protocol.dart';
/// Debug widget to show the hex dump of a frame
class DebugFrameViewer {
static void showFrameDebug(BuildContext context, Uint8List frame, String title) {
static void showFrameDebug(
BuildContext context,
Uint8List frame,
String title,
) {
final hexString = frame
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(' ');
@@ -14,16 +18,26 @@ class DebugFrameViewer {
details.writeln(context.l10n.debugFrame_length(frame.length));
details.writeln('');
details.writeln(
context.l10n.debugFrame_command(frame[0].toRadixString(16).padLeft(2, '0')),
context.l10n.debugFrame_command(
frame[0].toRadixString(16).padLeft(2, '0'),
),
);
if (frame[0] == cmdSendTxtMsg && frame.length > 37) {
details.writeln('');
details.writeln(context.l10n.debugFrame_textMessageHeader);
details.writeln(context.l10n.debugFrame_destinationPubKey(pubKeyToHex(frame.sublist(1, 33))));
details.writeln(context.l10n.debugFrame_timestamp(readUint32LE(frame, 33)));
details.writeln(
context.l10n.debugFrame_flags(frame[37].toRadixString(16).padLeft(2, '0')),
context.l10n.debugFrame_destinationPubKey(
pubKeyToHex(frame.sublist(1, 33)),
),
);
details.writeln(
context.l10n.debugFrame_timestamp(readUint32LE(frame, 33)),
);
details.writeln(
context.l10n.debugFrame_flags(
frame[37].toRadixString(16).padLeft(2, '0'),
),
);
final txtType = (frame[37] >> 2) & 0x03;
final typeLabel = txtType == txtTypeCliData
@@ -34,7 +48,7 @@ class DebugFrameViewer {
final textBytes = frame.sublist(38);
final nullIdx = textBytes.indexOf(0);
final text = String.fromCharCodes(
nullIdx >= 0 ? textBytes.sublist(0, nullIdx) : textBytes
nullIdx >= 0 ? textBytes.sublist(0, nullIdx) : textBytes,
);
details.writeln(context.l10n.debugFrame_text(text));
}
+4 -12
View File
@@ -7,18 +7,14 @@ class DeviceTile extends StatelessWidget {
final ScanResult scanResult;
final VoidCallback onTap;
const DeviceTile({
super.key,
required this.scanResult,
required this.onTap,
});
const DeviceTile({super.key, required this.scanResult, required this.onTap});
@override
Widget build(BuildContext context) {
final device = scanResult.device;
final rssi = scanResult.rssi;
final name = device.platformName.isNotEmpty
? device.platformName
final name = device.platformName.isNotEmpty
? device.platformName
: scanResult.advertisementData.advName;
return ListTile(
@@ -58,12 +54,8 @@ class DeviceTile extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Text(
'$rssi dBm',
style: TextStyle(fontSize: 10, color: color),
),
Text('$rssi dBm', style: TextStyle(fontSize: 10, color: color)),
],
);
}
}
+196 -26
View File
@@ -5,32 +5,196 @@ import '../l10n/l10n.dart';
class EmojiPicker extends StatelessWidget {
final Function(String) onEmojiSelected;
const EmojiPicker({
super.key,
required this.onEmojiSelected,
});
const EmojiPicker({super.key, required this.onEmojiSelected});
static const List<String> quickEmojis = ['👍', '❤️', '😂', '🎉', '👏', '🔥'];
static const List<String> smileys = [
'😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘',
'😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥸', '🤩', '🥳', '😏',
'😒', '😞', '😔', '😟', '😕', '🙁', '😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡',
'🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫', '🤥', '😶',
];
'😀',
'😃',
'😄',
'😁',
'😅',
'😂',
'🤣',
'😊',
'😇',
'🙂',
'🙃',
'😉',
'😌',
'😍',
'🥰',
'😘',
'😗',
'😙',
'😚',
'😋',
'😛',
'😝',
'😜',
'🤪',
'🤨',
'🧐',
'🤓',
'😎',
'🥸',
'🤩',
'🥳',
'😏',
'😒',
'😞',
'😔',
'😟',
'😕',
'🙁',
'😣',
'😖',
'😫',
'😩',
'🥺',
'😢',
'😭',
'😤',
'😠',
'😡',
'🤬',
'🤯',
'😳',
'🥵',
'🥶',
'😱',
'😨',
'😰',
'😥',
'😓',
'🤗',
'🤔',
'🤭',
'🤫',
'🤥',
'😶',
];
static const List<String> gestures = [
'👍', '👎', '👊', '', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '🤌', '🤏', '👈', '👉', '👆',
'👇', '☝️', '👋', '🤚', '🖐️', '', '🖖', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪',
];
'👍',
'👎',
'👊',
'',
'🤛',
'🤜',
'🤞',
'✌️',
'🤟',
'🤘',
'👌',
'🤌',
'🤏',
'👈',
'👉',
'👆',
'👇',
'☝️',
'👋',
'🤚',
'🖐️',
'',
'🖖',
'👏',
'🙌',
'👐',
'🤲',
'🤝',
'🙏',
'✍️',
'💅',
'🤳',
'💪',
];
static const List<String> hearts = [
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❤️‍🔥', '❤️‍🩹', '💕', '💞', '💓', '💗',
'💖', '💘', '💝', '💟', '💌', '💢', '💥', '💫', '💦', '💨', '🕳️', '💬', '👁️‍🗨️', '🗨️', '🗯️', '💭',
];
'❤️',
'🧡',
'💛',
'💚',
'💙',
'💜',
'🖤',
'🤍',
'🤎',
'💔',
'❤️‍🔥',
'❤️‍🩹',
'💕',
'💞',
'💓',
'💗',
'💖',
'💘',
'💝',
'💟',
'💌',
'💢',
'💥',
'💫',
'💦',
'💨',
'🕳️',
'💬',
'👁️‍🗨️',
'🗨️',
'🗯️',
'💭',
];
static const List<String> objects = [
'🎉', '🎊', '🎈', '🎁', '🎀', '🪅', '🪆', '🏆', '🥇', '🥈', '🥉', '', '', '🥎', '🏀', '🏐',
'🏈', '🏉', '🎾', '🥏', '🎳', '🏏', '🏑', '🏒', '🥍', '🏓', '🏸', '🥊', '🥋', '🥅', '', '🔥',
'', '🌟', '', '', '💡', '🔦', '🏮', '🪔', '📱', '💻', '', '📷', '📺', '📻', '🎵', '🎶', '🚀',
];
'🎉',
'🎊',
'🎈',
'🎁',
'🎀',
'🪅',
'🪆',
'🏆',
'🥇',
'🥈',
'🥉',
'',
'',
'🥎',
'🏀',
'🏐',
'🏈',
'🏉',
'🎾',
'🥏',
'🎳',
'🏏',
'🏑',
'🏒',
'🥍',
'🏓',
'🏸',
'🥊',
'🥋',
'🥅',
'',
'🔥',
'',
'🌟',
'',
'',
'💡',
'🔦',
'🏮',
'🪔',
'📱',
'💻',
'',
'📷',
'📺',
'📻',
'🎵',
'🎶',
'🚀',
];
Map<String, List<String>> _emojiCategories(AppLocalizations l10n) {
return {
@@ -60,7 +224,10 @@ class EmojiPicker extends StatelessWidget {
children: [
Text(
l10n.chat_addReaction,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
@@ -83,7 +250,9 @@ class EmojiPicker extends StatelessWidget {
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
color: Theme.of(
context,
).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
@@ -114,11 +283,12 @@ class EmojiPicker extends StatelessWidget {
.map(
(emojis) => GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: emojis.length,
itemBuilder: (context, index) => InkWell(
onTap: () {
+2 -8
View File
@@ -23,10 +23,7 @@ class EmptyState extends StatelessWidget {
children: [
Icon(icon, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
title,
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
Text(title, style: TextStyle(fontSize: 16, color: Colors.grey[600])),
if (subtitle != null) ...[
const SizedBox(height: 8),
Text(
@@ -35,10 +32,7 @@ class EmptyState extends StatelessWidget {
textAlign: TextAlign.center,
),
],
if (action != null) ...[
const SizedBox(height: 24),
action!,
],
if (action != null) ...[const SizedBox(height: 24), action!],
],
),
);
+27 -33
View File
@@ -7,10 +7,7 @@ import '../l10n/l10n.dart';
class GifPicker extends StatefulWidget {
final Function(String gifId) onGifSelected;
const GifPicker({
super.key,
required this.onGifSelected,
});
const GifPicker({super.key, required this.onGifSelected});
@override
State<GifPicker> createState() => _GifPickerState();
@@ -45,11 +42,13 @@ class _GifPickerState extends State<GifPicker> {
});
try {
final response = await http.get(
Uri.parse(
'https://api.giphy.com/v1/gifs/trending?api_key=$_giphyApiKey&limit=25&rating=g',
),
).timeout(const Duration(seconds: 10));
final response = await http
.get(
Uri.parse(
'https://api.giphy.com/v1/gifs/trending?api_key=$_giphyApiKey&limit=25&rating=g',
),
)
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final data = json.decode(response.body);
@@ -85,11 +84,13 @@ class _GifPickerState extends State<GifPicker> {
});
try {
final response = await http.get(
Uri.parse(
'https://api.giphy.com/v1/gifs/search?api_key=$_giphyApiKey&q=${Uri.encodeComponent(query)}&limit=25&rating=g',
),
).timeout(const Duration(seconds: 10));
final response = await http
.get(
Uri.parse(
'https://api.giphy.com/v1/gifs/search?api_key=$_giphyApiKey&q=${Uri.encodeComponent(query)}&limit=25&rating=g',
),
)
.timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final data = json.decode(response.body);
@@ -127,7 +128,10 @@ class _GifPickerState extends State<GifPicker> {
const SizedBox(width: 8),
Text(
context.l10n.gifPicker_title,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
@@ -170,18 +174,13 @@ class _GifPickerState extends State<GifPicker> {
const SizedBox(height: 16),
// GIF grid
Expanded(
child: _buildContent(),
),
Expanded(child: _buildContent()),
// Powered by Giphy attribution
const SizedBox(height: 8),
Text(
context.l10n.gifPicker_poweredBy,
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
@@ -190,9 +189,7 @@ class _GifPickerState extends State<GifPicker> {
Widget _buildContent() {
if (_isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
@@ -244,7 +241,8 @@ class _GifPickerState extends State<GifPicker> {
itemBuilder: (context, index) {
final gif = _gifs[index];
final gifId = gif['id'] as String;
final previewUrl = gif['images']?['fixed_height_small']?['url'] as String?;
final previewUrl =
gif['images']?['fixed_height_small']?['url'] as String?;
return GestureDetector(
onTap: () {
@@ -265,20 +263,16 @@ class _GifPickerState extends State<GifPicker> {
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Icon(Icons.error_outline),
);
return const Center(child: Icon(Icons.error_outline));
},
)
: const Center(
child: Icon(Icons.gif_box),
),
: const Center(child: Icon(Icons.gif_box)),
),
),
);
+1 -4
View File
@@ -4,10 +4,7 @@ import '../helpers/chat_scroll_controller.dart';
class JumpToBottomButton extends StatelessWidget {
final ChatScrollController scrollController;
const JumpToBottomButton({
super.key,
required this.scrollController,
});
const JumpToBottomButton({super.key, required this.scrollController});
@override
Widget build(BuildContext context) {
+6 -16
View File
@@ -1,18 +1,9 @@
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
enum ContactSortOption {
lastSeen,
recentMessages,
name,
}
enum ContactSortOption { lastSeen, recentMessages, name }
enum ContactTypeFilter {
all,
users,
repeaters,
rooms,
}
enum ContactTypeFilter { all, users, repeaters, rooms }
class SortFilterMenuOption {
final int value;
@@ -30,10 +21,7 @@ class SortFilterMenuSection {
final String title;
final List<SortFilterMenuOption> options;
const SortFilterMenuSection({
required this.title,
required this.options,
});
const SortFilterMenuSection({required this.title, required this.options});
}
class SortFilterMenu extends StatelessWidget {
@@ -62,7 +50,9 @@ class SortFilterMenu extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
);
final visibleSections = sections.where((section) => section.options.isNotEmpty).toList();
final visibleSections = sections
.where((section) => section.options.isNotEmpty)
.toList();
final entries = <PopupMenuEntry<int>>[];
for (int i = 0; i < visibleSections.length; i++) {
final section = visibleSections[i];
+67 -27
View File
@@ -10,15 +10,10 @@ import '../services/path_history_service.dart';
import 'path_selection_dialog.dart';
class PathManagementDialog {
static Future<void> show(
BuildContext context, {
required Contact contact,
}) {
static Future<void> show(BuildContext context, {required Contact contact}) {
return showDialog<void>(
context: context,
builder: (context) => _PathManagementDialog(
contact: contact,
),
builder: (context) => _PathManagementDialog(contact: contact),
);
}
}
@@ -26,9 +21,7 @@ class PathManagementDialog {
class _PathManagementDialog extends StatelessWidget {
final Contact contact;
const _PathManagementDialog({
required this.contact,
});
const _PathManagementDialog({required this.contact});
Contact _resolveContact(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
@@ -83,7 +76,9 @@ class _PathManagementDialog extends StatelessWidget {
Contact currentContact,
) async {
final l10n = context.l10n;
if (currentContact.pathLength > 0 && currentContact.path.isEmpty && connector.isConnected) {
if (currentContact.pathLength > 0 &&
currentContact.path.isEmpty &&
connector.isConnected) {
connector.getContacts();
}
@@ -140,13 +135,19 @@ class _PathManagementDialog extends StatelessWidget {
if (paths.isNotEmpty) ...[
Text(
l10n.chat_recentAckPaths,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
if (paths.length >= 100) ...[
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.amberAccent,
borderRadius: BorderRadius.circular(8),
@@ -165,7 +166,9 @@ class _PathManagementDialog extends StatelessWidget {
dense: true,
leading: CircleAvatar(
radius: 16,
backgroundColor: path.wasFloodDiscovery ? Colors.blue : Colors.green,
backgroundColor: path.wasFloodDiscovery
? Colors.blue
: Colors.green,
child: Text(
'${path.hopCount}',
style: const TextStyle(fontSize: 12),
@@ -193,16 +196,27 @@ class _PathManagementDialog extends StatelessWidget {
},
),
path.wasFloodDiscovery
? const Icon(Icons.waves, size: 16, color: Colors.grey)
: const Icon(Icons.route, size: 16, color: Colors.grey),
? const Icon(
Icons.waves,
size: 16,
color: Colors.grey,
)
: const Icon(
Icons.route,
size: 16,
color: Colors.grey,
),
],
),
onLongPress: () => _showFullPathDialog(context, path.pathBytes),
onLongPress: () =>
_showFullPathDialog(context, path.pathBytes),
onTap: () async {
if (path.pathBytes.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.chat_pathDetailsNotAvailable),
content: Text(
l10n.chat_pathDetailsNotAvailable,
),
duration: const Duration(seconds: 2),
),
);
@@ -222,7 +236,9 @@ class _PathManagementDialog extends StatelessWidget {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.path_usingHopsPath(path.hopCount)),
content: Text(
l10n.path_usingHopsPath(path.hopCount),
),
duration: const Duration(seconds: 2),
),
);
@@ -238,7 +254,10 @@ class _PathManagementDialog extends StatelessWidget {
const SizedBox(height: 8),
Text(
l10n.chat_pathActions,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
const SizedBox(height: 8),
ListTile(
@@ -248,8 +267,14 @@ class _PathManagementDialog extends StatelessWidget {
backgroundColor: Colors.purple,
child: Icon(Icons.edit_road, size: 16),
),
title: Text(l10n.chat_setCustomPath, style: const TextStyle(fontSize: 14)),
subtitle: Text(l10n.chat_setCustomPathSubtitle, style: const TextStyle(fontSize: 11)),
title: Text(
l10n.chat_setCustomPath,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.chat_setCustomPathSubtitle,
style: const TextStyle(fontSize: 11),
),
onTap: () async {
await _setCustomPath(context, connector, currentContact);
},
@@ -261,8 +286,14 @@ class _PathManagementDialog extends StatelessWidget {
backgroundColor: Colors.orange,
child: Icon(Icons.clear_all, size: 16),
),
title: Text(l10n.chat_clearPath, style: const TextStyle(fontSize: 14)),
subtitle: Text(l10n.chat_clearPathSubtitle, style: const TextStyle(fontSize: 11)),
title: Text(
l10n.chat_clearPath,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.chat_clearPathSubtitle,
style: const TextStyle(fontSize: 11),
),
onTap: () async {
await connector.clearContactPath(currentContact);
if (!context.mounted) return;
@@ -282,10 +313,19 @@ class _PathManagementDialog extends StatelessWidget {
backgroundColor: Colors.blue,
child: Icon(Icons.waves, size: 16),
),
title: Text(l10n.chat_forceFloodMode, style: const TextStyle(fontSize: 14)),
subtitle: Text(l10n.chat_floodModeSubtitle, style: const TextStyle(fontSize: 11)),
title: Text(
l10n.chat_forceFloodMode,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.chat_floodModeSubtitle,
style: const TextStyle(fontSize: 11),
),
onTap: () async {
await connector.setPathOverride(currentContact, pathLen: -1);
await connector.setPathOverride(
currentContact,
pathLen: -1,
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
+48 -16
View File
@@ -70,12 +70,15 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
}
void _updateTextFromContacts() {
final pathParts = _selectedContacts.map((contact) {
if (contact.publicKeyHex.length >= 2) {
return contact.publicKeyHex.substring(0, 2);
}
return '';
}).where((s) => s.isNotEmpty).toList();
final pathParts = _selectedContacts
.map((contact) {
if (contact.publicKeyHex.length >= 2) {
return contact.publicKeyHex.substring(0, 2);
}
return '';
})
.where((s) => s.isNotEmpty)
.toList();
_controller.text = pathParts.join(',');
}
@@ -107,7 +110,11 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
}
// Parse comma-separated hex prefixes
final pathIds = path.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
final pathIds = path
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
final pathBytesList = <int>[];
final invalidPrefixes = <String>[];
@@ -132,7 +139,9 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
if (invalidPrefixes.isNotEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))),
content: Text(
l10n.path_invalidHexPrefixes(invalidPrefixes.join(", ")),
),
duration: const Duration(seconds: 3),
backgroundColor: Colors.red,
),
@@ -180,7 +189,10 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
children: [
Text(
l10n.path_currentPathLabel,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (widget.onRefresh != null)
@@ -225,7 +237,10 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
children: [
Text(
l10n.path_selectFromContacts,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (_selectedContacts.isNotEmpty)
@@ -242,7 +257,11 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Icon(Icons.info_outline, size: 48, color: Colors.grey),
const Icon(
Icons.info_outline,
size: 48,
color: Colors.grey,
),
const SizedBox(height: 16),
Text(
l10n.path_noRepeatersFound,
@@ -252,7 +271,10 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
const SizedBox(height: 8),
Text(
l10n.path_customPathsRequire,
style: const TextStyle(fontSize: 12, color: Colors.grey),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
],
@@ -275,20 +297,30 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
radius: 16,
backgroundColor: isSelected
? Colors.green
: (contact.type == 2 ? Colors.blue : Colors.purple),
: (contact.type == 2
? Colors.blue
: Colors.purple),
child: Icon(
contact.type == 2 ? Icons.router : Icons.meeting_room,
contact.type == 2
? Icons.router
: Icons.meeting_room,
size: 16,
color: Colors.white,
),
),
title: Text(contact.name, style: const TextStyle(fontSize: 14)),
title: Text(
contact.name,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
'${contact.typeLabel}${contact.publicKeyHex.substring(0, 2)}',
style: const TextStyle(fontSize: 10),
),
trailing: isSelected
? const Icon(Icons.check_circle, color: Colors.green)
? const Icon(
Icons.check_circle,
color: Colors.green,
)
: const Icon(Icons.add_circle_outline),
onTap: () => _toggleContact(contact),
);
+64 -50
View File
@@ -8,13 +8,9 @@ import '../connector/meshcore_protocol.dart';
import '../models/contact.dart';
import '../widgets/snr_indicator.dart';
import '../l10n/l10n.dart';
class PathTraceDialog extends StatefulWidget {
const PathTraceDialog({
super.key,
required this.title,
required this.path,
});
class PathTraceDialog extends StatefulWidget {
const PathTraceDialog({super.key, required this.title, required this.path});
final String title;
final Uint8List path;
@@ -31,7 +27,7 @@ class _PathTraceDialogState extends State<PathTraceDialog> {
bool _failed2Loaded = false;
bool _hasData = false;
Uint8List _pathData = Uint8List(0);
Uint8List _snrData = Uint8List(0) ;
Uint8List _snrData = Uint8List(0);
Map<int, Contact> _pathContacts = {};
@override
@@ -49,13 +45,13 @@ class _PathTraceDialogState extends State<PathTraceDialog> {
}
Future<void> _doPathTrace() async {
if(mounted) {
if (mounted) {
setState(() {
_isLoading = true;
_failed2Loaded = false;
});
}
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
@@ -92,18 +88,19 @@ class _PathTraceDialogState extends State<PathTraceDialog> {
}
// Check if it's a binary response
if (code == pushCodeTraceData && listEquals(frame.sublist(4, 8), tagData)) {
if (code == pushCodeTraceData &&
listEquals(frame.sublist(4, 8), tagData)) {
_timeoutTimer?.cancel();
if (!mounted) return;
frameBuffer.skipBytes(3); //reserved + path length + flag
if(listEquals(frameBuffer.readBytes(4), tagData)){
if (listEquals(frameBuffer.readBytes(4), tagData)) {
_handleTraceResponse(frame);
}
}
});
}
Future<void> _handleTraceResponse(Uint8List frame)async {
Future<void> _handleTraceResponse(Uint8List frame) async {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final buffer = BufferReader(frame);
@@ -116,9 +113,7 @@ class _PathTraceDialogState extends State<PathTraceDialog> {
Map<int, Contact> pathContacts = {};
connector.contacts.where((c) => c.type != advTypeChat).forEach((
repeater,
) {
connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
for (var neighbourData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
@@ -143,21 +138,26 @@ class _PathTraceDialogState extends State<PathTraceDialog> {
if (index == 0) {
return context.l10n.pathTrace_you;
} else {
return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}";
return _pathContacts[_pathData[_pathData.length - 1]]?.name ??
"0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}";
}
} else {
return _pathContacts[_pathData[index-1]]?.name ?? "0x${_pathData[index-1].toRadixString(16).toUpperCase()}";
return _pathContacts[_pathData[index - 1]]?.name ??
"0x${_pathData[index - 1].toRadixString(16).toUpperCase()}";
}
}
String formatDirectionSubText(int index) {
if (index == 0 || index == _snrData.length - 1) {
if (index == 0) {
return _pathContacts[_pathData[0]]?.name ?? "0x${_pathData[0].toRadixString(16).toUpperCase()}";
return _pathContacts[_pathData[0]]?.name ??
"0x${_pathData[0].toRadixString(16).toUpperCase()}";
} else {
return context.l10n.pathTrace_you;
}
} else {
return _pathContacts[_pathData[index]]?.name ?? "0x${_pathData[index].toRadixString(16).toUpperCase()}";
return _pathContacts[_pathData[index]]?.name ??
"0x${_pathData[index].toRadixString(16).toUpperCase()}";
}
}
@@ -165,47 +165,61 @@ class _PathTraceDialogState extends State<PathTraceDialog> {
Widget build(BuildContext context) {
final l10n = context.l10n;
return AlertDialog(
title: Column( children: [
FittedBox(fit: BoxFit.scaleDown, child: Text(widget.title, style: const TextStyle(fontSize: 24))),
if(_failed2Loaded)
Text(l10n.pathTrace_failed, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.error),),
title: Column(
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(widget.title, style: const TextStyle(fontSize: 24)),
),
if (_failed2Loaded)
Text(
l10n.pathTrace_failed,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.error,
),
),
],
),
content: SafeArea(
child: RefreshIndicator(
onRefresh: _doPathTrace,
child: !_hasData
? Center(
child: Text(l10n.pathTrace_notAvailable),
)
: ListView.builder(
itemCount: _snrData.length,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made),
title: Text(
formatDirectionText(index), style: const TextStyle(fontSize: 14),
),
subtitle: Text(
formatDirectionSubText(index),
style: const TextStyle(fontSize: 14),
),
trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0),
onTap: () {
// Handle item tap
},
? Center(child: Text(l10n.pathTrace_notAvailable))
: ListView.builder(
itemCount: _snrData.length,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
leading: index >= _snrData.length / 2
? Icon(Icons.call_received)
: Icon(Icons.call_made),
title: Text(
formatDirectionText(index),
style: const TextStyle(fontSize: 14),
),
if (index < _snrData.length - 1) const Divider(height: 0.0),
],
);
},
),
subtitle: Text(
formatDirectionSubText(index),
style: const TextStyle(fontSize: 14),
),
trailing: SNRIcon(
snr: _snrData[index].toSigned(8) / 4.0,
),
onTap: () {
// Handle item tap
},
),
if (index < _snrData.length - 1)
const Divider(height: 0.0),
],
);
},
),
),
),
actions: [
IconButton(
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
+2 -8
View File
@@ -121,10 +121,7 @@ class QrCodeDisplay extends StatelessWidget {
size: size,
backgroundColor: bgColor,
errorCorrectionLevel: errorCorrectionLevel,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
eyeStyle: QrEyeStyle(eyeShape: QrEyeShape.square, color: fgColor),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
@@ -143,10 +140,7 @@ class QrCodeDisplay extends StatelessWidget {
backgroundColor: bgColor,
// Use higher error correction when embedding image
errorCorrectionLevel: QrErrorCorrectLevel.H,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
eyeStyle: QrEyeStyle(eyeShape: QrEyeShape.square, color: fgColor),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
+6 -10
View File
@@ -215,10 +215,7 @@ class _QrScannerWidgetState extends State<QrScannerWidget>
),
child: Text(
widget.instructions!,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
style: const TextStyle(color: Colors.white, fontSize: 14),
textAlign: TextAlign.center,
),
),
@@ -274,7 +271,8 @@ class _QrScannerWidgetState extends State<QrScannerWidget>
switch (error.errorCode) {
case MobileScannerErrorCode.permissionDenied:
message = 'Camera permission denied.\nPlease enable camera access in settings.';
message =
'Camera permission denied.\nPlease enable camera access in settings.';
icon = Icons.no_photography;
break;
case MobileScannerErrorCode.unsupported:
@@ -282,7 +280,8 @@ class _QrScannerWidgetState extends State<QrScannerWidget>
icon = Icons.videocam_off;
break;
default:
message = 'Failed to start camera.\n${error.errorDetails?.message ?? ''}';
message =
'Failed to start camera.\n${error.errorDetails?.message ?? ''}';
icon = Icons.error_outline;
}
@@ -297,10 +296,7 @@ class _QrScannerWidgetState extends State<QrScannerWidget>
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
style: TextStyle(color: Colors.grey[600], fontSize: 16),
),
],
),
+178 -148
View File
@@ -44,8 +44,9 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
}
Future<void> _loadSavedPassword() async {
final savedPassword =
await _storage.getRepeaterPassword(widget.repeater.publicKeyHex);
final savedPassword = await _storage.getRepeaterPassword(
widget.repeater.publicKeyHex,
);
if (savedPassword != null) {
setState(() {
_passwordController.text = savedPassword;
@@ -102,12 +103,10 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
);
final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs);
final selectionLabel =
selection.useFlood ? 'flood' : '${selection.hopCount} hops';
appLogger.info(
'Login routing: $selectionLabel',
tag: 'RepeaterLogin',
);
final selectionLabel = selection.useFlood
? 'flood'
: '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin');
bool? loginResult;
for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return;
@@ -119,9 +118,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
'Sending login attempt ${attempt + 1}/$_maxAttempts',
tag: 'RepeaterLogin',
);
await _connector.sendFrame(
loginFrame,
);
await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout);
if (loginResult == true) {
@@ -171,7 +168,9 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
// Save password if requested
if (_savePassword) {
await _storage.saveRepeaterPassword(
widget.repeater.publicKeyHex, password);
widget.repeater.publicKeyHex,
password,
);
} else {
// Remove saved password if user unchecked the box
await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex);
@@ -269,152 +268,183 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.login_repeaterDescription,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
if (_loginError != null) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.error, size: 18, color: Theme.of(context).colorScheme.error),
const SizedBox(width: 8),
Expanded(
child: Text(
_loginError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 13,
),
),
),
],
),
const SizedBox(height: 12),
],
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: l10n.login_password,
hintText: l10n.login_enterPassword,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
onChanged: (_) {
if (_loginError != null && mounted) {
setState(() {
_loginError = null;
});
}
},
onSubmitted: (_) => _handleLogin(),
autofocus: !(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
_passwordController.text.isEmpty,
),
const SizedBox(height: 12),
CheckboxListTile(
value: _savePassword,
onChanged: (value) {
setState(() {
_savePassword = value ?? false;
});
},
title: Text(
l10n.login_savePassword,
Text(
l10n.login_repeaterDescription,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.login_savePasswordSubtitle,
style: const TextStyle(fontSize: 12),
),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
const Divider(),
Row(
children: [
Text(
l10n.login_routing,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
const Spacer(),
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.login_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
const SizedBox(width: 8),
Text(
l10n.login_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
),
),
],
),
const SizedBox(height: 16),
if (_loginError != null) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.error,
size: 18,
color: Theme.of(context).colorScheme.error,
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
const SizedBox(width: 8),
Text(
l10n.login_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
),
),
],
const SizedBox(width: 8),
Expanded(
child: Text(
_loginError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 13,
),
),
),
],
),
const SizedBox(height: 12),
],
),
const SizedBox(height: 4),
Text(
repeater.pathLabel,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () => PathManagementDialog.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: l10n.login_password,
hintText: l10n.login_enterPassword,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
onChanged: (_) {
if (_loginError != null && mounted) {
setState(() {
_loginError = null;
});
}
},
onSubmitted: (_) => _handleLogin(),
autofocus:
!(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
_passwordController.text.isEmpty,
),
),
],
const SizedBox(height: 12),
CheckboxListTile(
value: _savePassword,
onChanged: (value) {
setState(() {
_savePassword = value ?? false;
});
},
title: Text(
l10n.login_savePassword,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.login_savePasswordSubtitle,
style: const TextStyle(fontSize: 12),
),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
const Divider(),
Row(
children: [
Text(
l10n.login_routing,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.login_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(
repeater,
pathLen: -1,
);
} else {
await connector.setPathOverride(
repeater,
pathLen: null,
);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.login_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.login_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
],
),
const SizedBox(height: 4),
Text(
repeater.pathLabel,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
+159 -152
View File
@@ -15,11 +15,7 @@ class RoomLoginDialog extends StatefulWidget {
final Contact room;
final Function(String password) onLogin;
const RoomLoginDialog({
super.key,
required this.room,
required this.onLogin,
});
const RoomLoginDialog({super.key, required this.room, required this.onLogin});
@override
State<RoomLoginDialog> createState() => _RoomLoginDialogState();
@@ -43,8 +39,9 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
}
Future<void> _loadSavedPassword() async {
final savedPassword =
await _storage.getRepeaterPassword(widget.room.publicKeyHex);
final savedPassword = await _storage.getRepeaterPassword(
widget.room.publicKeyHex,
);
if (savedPassword != null) {
setState(() {
_passwordController.text = savedPassword;
@@ -100,12 +97,10 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
);
final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs);
final selectionLabel =
selection.useFlood ? 'flood' : '${selection.hopCount} hops';
appLogger.info(
'Login routing: $selectionLabel',
tag: 'RoomLogin',
);
final selectionLabel = selection.useFlood
? 'flood'
: '${selection.hopCount} hops';
appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin');
bool? loginResult;
for (int attempt = 0; attempt < _maxAttempts; attempt++) {
if (!mounted) return;
@@ -117,23 +112,15 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
'Sending login attempt ${attempt + 1}/$_maxAttempts',
tag: 'RoomLogin',
);
await _connector.sendFrame(
loginFrame,
);
await _connector.sendFrame(loginFrame);
loginResult = await _awaitLoginResponse(timeout);
if (loginResult == true) {
appLogger.info(
'Login succeeded for ${room.name}',
tag: 'RoomLogin',
);
appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin');
break;
}
if (loginResult == false) {
appLogger.warn(
'Login failed for ${room.name}',
tag: 'RoomLogin',
);
appLogger.warn('Login failed for ${room.name}', tag: 'RoomLogin');
throw Exception('Wrong password or node is unreachable');
}
appLogger.warn(
@@ -143,10 +130,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
}
if (loginResult == null) {
appLogger.warn(
'Login timed out for ${room.name}',
tag: 'RoomLogin',
);
appLogger.warn('Login timed out for ${room.name}', tag: 'RoomLogin');
}
if (loginResult == true) {
@@ -162,8 +146,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
// If we got a response, login succeeded
// Save password if requested
if (_savePassword) {
await _storage.saveRepeaterPassword(
widget.room.publicKeyHex, password);
await _storage.saveRepeaterPassword(widget.room.publicKeyHex, password);
} else {
// Remove saved password if user unchecked the box
await _storage.removeRepeaterPassword(widget.room.publicKeyHex);
@@ -175,10 +158,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
}
} catch (e) {
final room = _resolveRepeater(_connector);
appLogger.warn(
'Login error for ${room.name}: $e',
tag: 'RoomLogin',
);
appLogger.warn('Login error for ${room.name}: $e', tag: 'RoomLogin');
if (mounted) {
setState(() {
_isLoggingIn = false;
@@ -262,130 +242,157 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
),
)
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.login_roomDescription,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: l10n.login_password,
hintText: l10n.login_enterPassword,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
onSubmitted: (_) => _handleLogin(),
autofocus: !(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
_passwordController.text.isEmpty,
),
const SizedBox(height: 12),
CheckboxListTile(
value: _savePassword,
onChanged: (value) {
setState(() {
_savePassword = value ?? false;
});
},
title: Text(
l10n.login_savePassword,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.login_roomDescription,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.login_savePasswordSubtitle,
style: const TextStyle(fontSize: 12),
),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
const Divider(),
Row(
children: [
Text(
l10n.login_routing,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
const Spacer(),
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.login_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
const SizedBox(width: 8),
Text(
l10n.login_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
),
),
],
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: l10n.login_password,
hintText: l10n.login_enterPassword,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
const SizedBox(width: 8),
Text(
l10n.login_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
],
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
],
),
const SizedBox(height: 4),
Text(
repeater.pathLabel,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () => PathManagementDialog.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths),
onSubmitted: (_) => _handleLogin(),
autofocus:
!(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) &&
_passwordController.text.isEmpty,
),
),
],
const SizedBox(height: 12),
CheckboxListTile(
value: _savePassword,
onChanged: (value) {
setState(() {
_savePassword = value ?? false;
});
},
title: Text(
l10n.login_savePassword,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
l10n.login_savePasswordSubtitle,
style: const TextStyle(fontSize: 12),
),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
const Divider(),
Row(
children: [
Text(
l10n.login_routing,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.login_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(
repeater,
pathLen: -1,
);
} else {
await connector.setPathOverride(
repeater,
pathLen: null,
);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.login_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.login_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
],
),
const SizedBox(height: 4),
Text(
repeater.pathLabel,
style: const TextStyle(fontSize: 11, color: Colors.grey),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: TextButton.icon(
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18),
label: Text(l10n.login_managePaths),
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
+1 -4
View File
@@ -3,10 +3,7 @@ import 'package:flutter/material.dart';
class UnreadBadge extends StatelessWidget {
final int count;
const UnreadBadge({
super.key,
required this.count,
});
const UnreadBadge({super.key, required this.count});
@override
Widget build(BuildContext context) {