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 {