Main coves client
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}