meshcore-open/lib/helpers/link_handler.dart
zjs81 834850fb51 Add companion radio stats, adaptive backoff, path hash width, and UI improvements
- Companion radio stats: poll and display noise floor, RSSI, SNR, airtime
  with dedicated ValueNotifier and ref-counted polling
- Adaptive RF-aware TX backoff based on radio conditions instead of fixed 5s
- Variable-width path hash support (1-3 bytes per hop)
- Air activity dot indicator in app bar with tap to open stats screen
- Jump to oldest unread setting for chat screens
- 1s send cooldown on DM and channel messages
- Link style: theme-aware orange, added EmailLinkifier
- New languages: Hungarian, Japanese, Korean
- Remove dead DeviceScreen and BatteryIndicatorChip
- Remove wakelock_plus dependency
- TX power fields now read as signed int8
2026-03-23 19:26:05 -07:00

115 lines
3.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
class LinkHandler {
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
final brightness = Theme.of(context).brightness;
final orange = brightness == Brightness.dark
? const Color(0xFFFFB74D)
: const Color(0xFFE65100);
return base.copyWith(color: orange, decoration: TextDecoration.underline);
}
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
static Widget buildLinkifyText({
required BuildContext context,
required String text,
required TextStyle style,
TextStyle? linkStyle,
}) {
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) {
return SelectableLinkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
return Linkify(
text: text,
style: style,
linkStyle: effectiveLinkStyle,
options: options,
linkifiers: linkifiers,
onOpen: onOpen,
);
}
static Future<void> handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog
final shouldOpen = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.chat_openLink),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.chat_openLinkConfirmation,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
url,
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: Text(context.l10n.chat_open),
),
],
),
);
if (shouldOpen != true) return;
// Launch URL
try {
final uri = Uri.parse(url);
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_couldNotOpenLink(url)),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.chat_invalidLink),
backgroundColor: Colors.red,
),
);
}
}
}
}