From 4dc893acfd06c757739c2b832a3d87bb823fe81a Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sat, 21 Feb 2026 08:29:37 +0100 Subject: [PATCH] improve icon handling --- app/lib/domain/services/generic_website.dart | 49 +++++++++++++++---- .../domain/services/generic_website.g.dart | 2 +- .../browser_modules/bottom_app_bar.dart | 13 ++--- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/app/lib/domain/services/generic_website.dart b/app/lib/domain/services/generic_website.dart index 399a5e5b..264fb88e 100644 --- a/app/lib/domain/services/generic_website.dart +++ b/app/lib/domain/services/generic_website.dart @@ -17,7 +17,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import 'dart:async'; import 'dart:io'; import 'dart:ui'; @@ -57,6 +56,12 @@ const _typeMap = { "msapplication-TileImage": IconType.microsoftTile, }; +final class _InFlightFetch { + final Future> future; + + const _InFlightFetch(this.future); +} + Iterable sizesToList(String? sizes) sync* { if (sizes != null) { final splitted = sizes @@ -82,12 +87,9 @@ Iterable sizesToList(String? sizes) sync* { class GenericWebsiteService extends _$GenericWebsiteService { final GeckoIconService _iconsService; - //Global icon cache late CacheRepository _cacheRepository; - //Local decoded icon cache late final LRUCache _browserIconCache; - //In-flight fetch deduplication - final _inFlightFetches = >>{}; + final _inFlightFetches = {}; GenericWebsiteService() : _iconsService = GeckoIconService(), @@ -127,6 +129,10 @@ class GenericWebsiteService extends _$GenericWebsiteService { return uri; } + static bool _isHttpUrl(Uri url) { + return url.scheme.startsWith('https') || url.scheme.startsWith('http'); + } + static List _extractIcons(Uri baseUrl, Document document) { final List icons = []; @@ -293,7 +299,7 @@ class GenericWebsiteService extends _$GenericWebsiteService { } Future getCachedIcon(Uri url) async { - if (url.scheme.startsWith('https') || url.scheme.startsWith('http')) { + if (_isHttpUrl(url)) { final cachedBrowserIcon = _browserIconCache.get(url.origin); if (cachedBrowserIcon?.image.value != null) { return cachedBrowserIcon; @@ -346,14 +352,38 @@ class GenericWebsiteService extends _$GenericWebsiteService { } Future> _deduplicatedFetchPageInfo(Uri url) { - return _inFlightFetches.putIfAbsent(url, () { - return fetchPageInfo(url: url, isImageRequest: true, proxyPort: null) - .whenComplete(() => _inFlightFetches.remove(url)); + final existing = _inFlightFetches[url]; + if (existing != null) { + return existing.future; + } + + late final Future> inFlightFetch; + inFlightFetch = fetchPageInfo( + url: url, + isImageRequest: true, + proxyPort: null, + ).timeout( + const Duration(seconds: 20), + onTimeout: () => Result.failure( + const ErrorMessage(source: 'icon', message: 'Icon fetch timeout'), + ), + ).whenComplete(() { + // Only clear this entry if it is still the active in-flight request. + if (identical(_inFlightFetches[url]?.future, inFlightFetch)) { + _inFlightFetches.remove(url); + } }); + + _inFlightFetches[url] = _InFlightFetch(inFlightFetch); + return inFlightFetch; } Future getUrlIcon(List urlList) async { for (final url in urlList) { + if (!_isHttpUrl(url)) { + continue; + } + final cachedIcon = await getCachedIcon(url); if (cachedIcon != null) { @@ -365,7 +395,6 @@ class GenericWebsiteService extends _$GenericWebsiteService { if (result.isSuccess) { if (result.value.favicon case final BrowserIcon favicon) { - //If it was a `isImageRequest` hit, we need to cache it at this point if (!_browserIconCache.contains(url.origin)) { _browserIconCache.set(url.origin, favicon); } diff --git a/app/lib/domain/services/generic_website.g.dart b/app/lib/domain/services/generic_website.g.dart index ef781b70..d256c006 100644 --- a/app/lib/domain/services/generic_website.g.dart +++ b/app/lib/domain/services/generic_website.g.dart @@ -42,7 +42,7 @@ final class GenericWebsiteServiceProvider } String _$genericWebsiteServiceHash() => - r'437e1b11b6e63d38151354522deda755d9333c47'; + r'b320635f651574a500ee6764688ef26b6d39ef8f'; abstract class _$GenericWebsiteService extends $Notifier { void build(); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index ca393331..ac7c849c 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -44,6 +44,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart'; @@ -57,7 +58,6 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart import 'package:weblibre/presentation/hooks/cached_future.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/icons/weblibre_icons.dart'; -import 'package:weblibre/presentation/widgets/safe_raw_image.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; import 'package:weblibre/utils/ui_helper.dart'; @@ -567,15 +567,10 @@ class QuickTabSwitcher extends HookConsumerWidget { ); }, itemAvatar: (item) => - item.tabState?.icon.mapNotNull( - (icon) => SafeRawImage( - image: icon, - height: 24, - width: 24, - fallback: UrlIcon([item.url], iconSize: 24), - ), + item.tabState.mapNotNull( + (tabState) => TabIcon(tabState: tabState, iconSize: 20), ) ?? - UrlIcon([item.url], iconSize: 16), + UrlIcon([item.url], iconSize: 20), itemBackgroundColor: (item) => item.color != null ? ContainerColors.forChip(item.color!) : null, onSelected: (item) async {