mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Replace hardcoded English strings in 32 Dart source files with l10n references. Covers settings labels, UI actions, map/SAR features, device config, notifications, profiles, spectrum scan, and status messages for sl, de, hr, pl, es, fr, it, el, ru, tr, uk, zh, pt.
388 lines
12 KiB
Dart
388 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../providers/connection_provider.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
|
|
class AddContactScreen extends StatefulWidget {
|
|
const AddContactScreen({super.key});
|
|
|
|
@override
|
|
State<AddContactScreen> createState() => _AddContactScreenState();
|
|
}
|
|
|
|
class _AddContactScreenState extends State<AddContactScreen> {
|
|
final TextEditingController _advertController = TextEditingController();
|
|
bool _isImporting = false;
|
|
bool _importSucceeded = false;
|
|
String? _validationError;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadClipboardIfPresent();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_advertController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadClipboardIfPresent() async {
|
|
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
|
final text = clipboardData?.text?.trim();
|
|
if (!mounted || text == null || text.isEmpty) {
|
|
return;
|
|
}
|
|
if (_normalizeAdvertText(text) == null) {
|
|
return;
|
|
}
|
|
_advertController.text = text;
|
|
}
|
|
|
|
String? _normalizeAdvertText(String value) {
|
|
final trimmed = value.trim();
|
|
if (trimmed.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
var normalized = trimmed;
|
|
if (normalized.startsWith('meshcore://')) {
|
|
normalized = normalized.substring('meshcore://'.length);
|
|
}
|
|
|
|
normalized = normalized.replaceAll(RegExp(r'\s+'), '');
|
|
if (normalized.isEmpty) {
|
|
return null;
|
|
}
|
|
|
|
final isHex = RegExp(r'^[0-9a-fA-F]+$').hasMatch(normalized);
|
|
if (!isHex || normalized.length.isOdd) {
|
|
return null;
|
|
}
|
|
|
|
return normalized.toLowerCase();
|
|
}
|
|
|
|
Uint8List _hexToBytes(String hex) {
|
|
final bytes = <int>[];
|
|
for (var i = 0; i < hex.length; i += 2) {
|
|
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
|
}
|
|
return Uint8List.fromList(bytes);
|
|
}
|
|
|
|
Future<void> _pasteFromClipboard() async {
|
|
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
|
final text = clipboardData?.text;
|
|
if (text == null || text.trim().isEmpty) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.clipboardIsEmpty)));
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_advertController.text = text.trim();
|
|
_importSucceeded = false;
|
|
_validationError = null;
|
|
});
|
|
}
|
|
|
|
Future<void> _importContact() async {
|
|
final normalized = _normalizeAdvertText(_advertController.text);
|
|
if (normalized == null) {
|
|
setState(() {
|
|
_validationError =
|
|
'Enter a valid meshcore:// advert or raw hexadecimal contact advert.';
|
|
});
|
|
return;
|
|
}
|
|
|
|
final advertBytes = _hexToBytes(normalized);
|
|
if (advertBytes.length < 98) {
|
|
setState(() {
|
|
_validationError =
|
|
'Advert is too short. Expected exported contact data.';
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isImporting = true;
|
|
_importSucceeded = false;
|
|
_validationError = null;
|
|
});
|
|
|
|
final connectionProvider = context.read<ConnectionProvider>();
|
|
await connectionProvider.importContactAdvert(advertBytes);
|
|
final importError = connectionProvider.error;
|
|
|
|
if (importError == null) {
|
|
await connectionProvider.getContacts();
|
|
}
|
|
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isImporting = false;
|
|
});
|
|
|
|
if (importError != null) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(importError)));
|
|
return;
|
|
}
|
|
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.contactImported)));
|
|
setState(() {
|
|
_importSucceeded = true;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final normalized = _normalizeAdvertText(_advertController.text);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(AppLocalizations.of(context)!.addContact)),
|
|
body: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: colorScheme.outlineVariant.withValues(alpha: 0.6),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primary.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Icon(
|
|
Icons.person_add_alt_1_outlined,
|
|
color: colorScheme.primary,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Import a shared contact advert',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Text(
|
|
'Paste a `meshcore://...` link or raw hexadecimal advert. The app will validate it and import the contact into the connected device.',
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
_ImportHintChip(
|
|
icon: Icons.link_outlined,
|
|
label: AppLocalizations.of(context)!.acceptsShareLinks,
|
|
),
|
|
_ImportHintChip(
|
|
icon: Icons.code_outlined,
|
|
label: AppLocalizations.of(context)!.supportsRawHex,
|
|
),
|
|
_ImportHintChip(
|
|
icon: Icons.content_paste_go_outlined,
|
|
label: AppLocalizations.of(context)!.clipboardfriendly,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: colorScheme.outlineVariant.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Advert payload',
|
|
style: theme.textTheme.titleSmall?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'You can paste the full share link or only the hex payload.',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _advertController,
|
|
minLines: 5,
|
|
maxLines: 9,
|
|
onChanged: (_) {
|
|
if (_validationError != null || _importSucceeded) {
|
|
setState(() {
|
|
_importSucceeded = false;
|
|
_validationError = null;
|
|
});
|
|
}
|
|
},
|
|
decoration: InputDecoration(
|
|
labelText: 'Contact advert',
|
|
hintText: 'Paste a share link or hex advert',
|
|
alignLabelWithHint: true,
|
|
border: const OutlineInputBorder(),
|
|
errorText: _validationError,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
normalized == null
|
|
? 'No valid advert detected yet'
|
|
: 'Advert size: ${normalized.length ~/ 2} bytes',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: _isImporting ? null : _pasteFromClipboard,
|
|
icon: Icon(Icons.content_paste_go_outlined),
|
|
label: Text(AppLocalizations.of(context)!.paste),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
FilledButton.icon(
|
|
onPressed: (_isImporting || _importSucceeded)
|
|
? null
|
|
: _importContact,
|
|
style: FilledButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
),
|
|
icon: _isImporting
|
|
? const SizedBox(
|
|
width: 16,
|
|
height: 16,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: _importSucceeded
|
|
? const Icon(Icons.check_circle_outline)
|
|
: const Icon(Icons.person_add_alt_1_outlined),
|
|
label: Text(
|
|
_isImporting
|
|
? 'Importing...'
|
|
: _importSucceeded
|
|
? 'Imported'
|
|
: 'Add Contact',
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (_importSucceeded)
|
|
Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primary.withValues(alpha: 0.08),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color: colorScheme.primary.withValues(alpha: 0.18),
|
|
),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(
|
|
Icons.check_circle,
|
|
size: 18,
|
|
color: colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'Contact imported. Paste another advert or edit the current one to import again.',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ImportHintChip extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
|
|
const _ImportHintChip({required this.icon, required this.label});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(999),
|
|
border: Border.all(
|
|
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.7),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 16, color: theme.colorScheme.onSurfaceVariant),
|
|
const SizedBox(width: 6),
|
|
Text(label, style: theme.textTheme.labelMedium),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|