mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-22 02:14:28 +10:00
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:
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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: () {
|
||||
|
||||
@@ -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
@@ -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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user