at main 3.7 kB view raw
1import 'package:cached_network_image/cached_network_image.dart'; 2import 'package:flutter/material.dart'; 3 4import '../constants/app_colors.dart'; 5import '../models/post.dart'; 6import '../utils/url_launcher.dart'; 7 8/// External link bar widget for displaying clickable links 9/// 10/// Shows the domain favicon, domain name, and an external link icon. 11/// Taps launch the URL in an external browser with security validation. 12class ExternalLinkBar extends StatelessWidget { 13 const ExternalLinkBar({required this.embed, super.key}); 14 15 final ExternalEmbed embed; 16 17 @override 18 Widget build(BuildContext context) { 19 final domain = _extractDomain(); 20 return Semantics( 21 button: true, 22 label: 'Open link to $domain in external browser', 23 child: InkWell( 24 onTap: () async { 25 await UrlLauncher.launchExternalUrl(embed.uri, context: context); 26 }, 27 child: Container( 28 padding: const EdgeInsets.all(10), 29 decoration: BoxDecoration( 30 color: AppColors.backgroundSecondary, 31 borderRadius: BorderRadius.circular(8), 32 ), 33 child: Row( 34 children: [ 35 // Favicon 36 _buildFavicon(), 37 const SizedBox(width: 8), 38 Expanded( 39 child: Text( 40 domain, 41 style: TextStyle( 42 color: AppColors.textPrimary.withValues(alpha: 0.7), 43 fontSize: 13, 44 ), 45 maxLines: 1, 46 overflow: TextOverflow.ellipsis, 47 ), 48 ), 49 const SizedBox(width: 8), 50 Icon( 51 Icons.open_in_new, 52 size: 14, 53 color: AppColors.textPrimary.withValues(alpha: 0.7), 54 ), 55 ], 56 ), 57 ), 58 ), 59 ); 60 } 61 62 /// Extracts the domain from the embed 63 String _extractDomain() { 64 // Use domain field if available 65 if (embed.domain != null && embed.domain!.isNotEmpty) { 66 return embed.domain!; 67 } 68 69 // Otherwise parse from URI 70 try { 71 final uri = Uri.parse(embed.uri); 72 if (uri.host.isNotEmpty) { 73 return uri.host; 74 } 75 } on FormatException { 76 // Invalid URI, fall through to fallback 77 } 78 79 // Fallback to full URI if domain extraction fails 80 return embed.uri; 81 } 82 83 /// Builds the favicon widget 84 Widget _buildFavicon() { 85 // Extract domain for favicon URL 86 var domain = embed.domain; 87 if (domain == null || domain.isEmpty) { 88 try { 89 final uri = Uri.parse(embed.uri); 90 domain = uri.host; 91 } on FormatException { 92 domain = null; 93 } 94 } 95 96 if (domain == null || domain.isEmpty) { 97 // Fallback to link icon if we can't get the domain 98 return Icon( 99 Icons.link, 100 size: 18, 101 color: AppColors.textPrimary.withValues(alpha: 0.7), 102 ); 103 } 104 105 // Use Google's favicon service 106 final faviconUrl = 107 'https://www.google.com/s2/favicons?domain=$domain&sz=32'; 108 109 return ClipRRect( 110 borderRadius: BorderRadius.circular(4), 111 child: CachedNetworkImage( 112 imageUrl: faviconUrl, 113 width: 18, 114 height: 18, 115 fit: BoxFit.cover, 116 placeholder: 117 (context, url) => Icon( 118 Icons.link, 119 size: 18, 120 color: AppColors.textPrimary.withValues(alpha: 0.7), 121 ), 122 errorWidget: 123 (context, url, error) => Icon( 124 Icons.link, 125 size: 18, 126 color: AppColors.textPrimary.withValues(alpha: 0.7), 127 ), 128 ), 129 ); 130 } 131}