Skip to content

Commit 1d5fd0e

Browse files
committed
#34: Rework the link list to list tiles for better usability and consistent design
1 parent 43da5fc commit 1d5fd0e

File tree

5 files changed

+186
-20
lines changed

5 files changed

+186
-20
lines changed

lib/components/link_list_tile.dart

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import 'package:cached_network_image/cached_network_image.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:iccm_eu_app/data/model/communication_data.dart';
4+
import 'package:iccm_eu_app/utils/url_functions.dart';
5+
6+
class LinkListTile extends StatelessWidget {
7+
final CommunicationData item;
8+
9+
const LinkListTile({
10+
super.key,
11+
required this.item,
12+
});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
UrlFunctions.getFaviconUrl(item.url).then((websiteIconUrl) {
17+
if (websiteIconUrl != null) {
18+
} else {
19+
}
20+
});
21+
return ListTile(
22+
leading: FutureBuilder<String?>(
23+
future: UrlFunctions.getFaviconUrl(item.url),
24+
builder: (BuildContext context, AsyncSnapshot<String?> snapshot) {
25+
if (snapshot.connectionState == ConnectionState.waiting) {
26+
// While waiting for the Future to complete, show a loading indicator
27+
return const SizedBox(
28+
width: 100,
29+
height: 100,
30+
child: FittedBox(
31+
child: CircularProgressIndicator(),
32+
),
33+
);
34+
} else if (snapshot.hasError) {
35+
// If there's an error, show an error icon
36+
return const SizedBox(
37+
width: 100,
38+
height: 100,
39+
child: FittedBox(
40+
child: Icon(Icons.error),
41+
),
42+
);
43+
} else if (snapshot.hasData && snapshot.data != null) {
44+
// If the Future completed successfully and has data, show the image
45+
return CircleAvatar(
46+
radius: 50.0,
47+
child: SizedBox(
48+
width: 100.0,
49+
height: 100.0,
50+
child: CachedNetworkImage(
51+
imageUrl: snapshot.data!,
52+
placeholder: (context, url) => const FittedBox(
53+
child: CircularProgressIndicator(),
54+
),
55+
errorWidget: (context, url, error) => const FittedBox(
56+
child: Icon(Icons.error),
57+
),
58+
),
59+
),
60+
);
61+
} else {
62+
// If the Future completed successfully but has no data (null), show an empty box
63+
return const SizedBox(
64+
height: 100,
65+
width: 100,
66+
);
67+
}
68+
},
69+
),
70+
title: RichText(
71+
text: TextSpan(
72+
style: DefaultTextStyle
73+
.of(context)
74+
.style,
75+
children: <TextSpan>[
76+
TextSpan(
77+
text: item.title,
78+
style: Theme
79+
.of(context)
80+
.textTheme
81+
.titleMedium,
82+
),
83+
],
84+
),
85+
softWrap: false,
86+
),
87+
subtitle: RichText(
88+
text: TextSpan(
89+
style: DefaultTextStyle
90+
.of(context)
91+
.style,
92+
children: <TextSpan>[
93+
TextSpan(
94+
text: item.url,
95+
style: Theme
96+
.of(context)
97+
.textTheme
98+
.bodySmall,
99+
),
100+
],
101+
),
102+
softWrap: true,
103+
),
104+
onTap: () {
105+
UrlFunctions.launch(item.url);
106+
},
107+
);
108+
}
109+
}

lib/pages/communication_page.dart

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'package:flutter/material.dart';
2-
import 'package:iccm_eu_app/components/url_button.dart';
2+
import 'package:iccm_eu_app/components/link_list_tile.dart';
33
import 'package:iccm_eu_app/data/dataProviders/communication_provider.dart';
44
import 'package:iccm_eu_app/data/model/communication_data.dart';
55
import 'package:provider/provider.dart';
@@ -34,24 +34,7 @@ class CommunicationPage extends StatelessWidget {
3434
!item.url.startsWith('https://')) {
3535
return SizedBox.shrink();
3636
}
37-
return Column(
38-
children: [
39-
Padding(
40-
padding: const EdgeInsets.all(16.0),
41-
child: UrlButton(
42-
title: item.title,
43-
url: item.url,
44-
),
45-
),
46-
Text(
47-
item.url,
48-
style: Theme
49-
.of(context)
50-
.textTheme
51-
.bodySmall,
52-
),
53-
],
54-
);
37+
return LinkListTile(item: item);
5538
},
5639
);
5740
},

lib/utils/url_functions.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import 'package:iccm_eu_app/utils/debug.dart';
12
import 'package:url_launcher/url_launcher.dart';
3+
import 'package:http/http.dart' as http;
4+
import 'package:html/parser.dart' as parser;
5+
import 'package:html/dom.dart' as dom;
26

37
class UrlFunctions {
48
static Future<void> launch(String url) async {
@@ -21,4 +25,73 @@ class UrlFunctions {
2125
// }
2226
return url;
2327
}
28+
29+
///////////////////////////////////////////////
30+
// Website icon parsing
31+
///////////////////////////////////////////////
32+
33+
static Future<String?> getFaviconUrl(String websiteUrl) async {
34+
try {
35+
// 1. Fetch the website's HTML
36+
final response = await http.get(Uri.parse(websiteUrl));
37+
38+
if (response.statusCode == 200) {
39+
// 2. Parse the HTML
40+
final document = parser.parse(response.body);
41+
42+
// 3. Find the favicon link
43+
final faviconLink = _findFaviconLink(document);
44+
45+
if (faviconLink != null) {
46+
// 4. Extract the favicon URL
47+
final faviconUrl = _extractFaviconUrl(websiteUrl, faviconLink);
48+
return faviconUrl;
49+
} else {
50+
// 5. Try default favicon location
51+
final defaultFaviconUrl = _getDefaultFaviconUrl(websiteUrl);
52+
return defaultFaviconUrl;
53+
}
54+
} else {
55+
Debug.msg('Failed to load website: ${response.statusCode}');
56+
return null;
57+
}
58+
} catch (e) {
59+
Debug.msg('Error getting favicon: $e');
60+
return null;
61+
}
62+
}
63+
64+
// Helper function to find the favicon link in the HTML
65+
static dom.Element? _findFaviconLink(dom.Document document) {
66+
// Look for <link> tags with rel="icon" or rel="shortcut icon"
67+
final links = document.querySelectorAll('link');
68+
for (final link in links) {
69+
final rel = link.attributes['rel']?.toLowerCase();
70+
if (rel == 'icon' || rel == 'shortcut icon') {
71+
return link;
72+
}
73+
}
74+
return null;
75+
}
76+
77+
// Helper function to extract the favicon URL from the link tag
78+
static String _extractFaviconUrl(String websiteUrl, dom.Element link) {
79+
final href = link.attributes['href'];
80+
if (href == null) {
81+
return '';
82+
}
83+
// Handle relative URLs
84+
if (href.startsWith('http')) {
85+
return href;
86+
} else if (href.startsWith('/')) {
87+
return Uri.parse(websiteUrl).origin + href;
88+
} else {
89+
return '${Uri.parse(websiteUrl).origin}/$href';
90+
}
91+
}
92+
93+
// Helper function to get the default favicon URL
94+
static String _getDefaultFaviconUrl(String websiteUrl) {
95+
return '${Uri.parse(websiteUrl).origin}/favicon.ico';
96+
}
2497
}

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ packages:
337337
source: hosted
338338
version: "13.2.0"
339339
html:
340-
dependency: transitive
340+
dependency: "direct main"
341341
description:
342342
name: html
343343
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ dependencies:
5959
flutter_timezone: ^4.0.0
6060
universal_html: ^2.2.4
6161
window_manager: ^0.4.3
62+
html: ^0.15.5
6263

6364
dev_dependencies:
6465
flutter_test:

0 commit comments

Comments
 (0)