improve icon handling

This commit is contained in:
Fabian Freund
2026-02-21 08:29:37 +01:00
parent 5bdc3c6027
commit 4dc893acfd
3 changed files with 44 additions and 20 deletions
+39 -10
View File
@@ -17,7 +17,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'dart:io';
import 'dart:ui';
@@ -57,6 +56,12 @@ const _typeMap = {
"msapplication-TileImage": IconType.microsoftTile,
};
final class _InFlightFetch {
final Future<Result<WebPageInfo>> future;
const _InFlightFetch(this.future);
}
Iterable<ResourceSize> sizesToList(String? sizes) sync* {
if (sizes != null) {
final splitted = sizes
@@ -82,12 +87,9 @@ Iterable<ResourceSize> sizesToList(String? sizes) sync* {
class GenericWebsiteService extends _$GenericWebsiteService {
final GeckoIconService _iconsService;
//Global icon cache
late CacheRepository _cacheRepository;
//Local decoded icon cache
late final LRUCache<String, BrowserIcon> _browserIconCache;
//In-flight fetch deduplication
final _inFlightFetches = <Uri, Future<Result<WebPageInfo>>>{};
final _inFlightFetches = <Uri, _InFlightFetch>{};
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<Resource> _extractIcons(Uri baseUrl, Document document) {
final List<Resource> icons = [];
@@ -293,7 +299,7 @@ class GenericWebsiteService extends _$GenericWebsiteService {
}
Future<BrowserIcon?> 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<Result<WebPageInfo>> _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<Result<WebPageInfo>> 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<BrowserIcon?> getUrlIcon(List<Uri> 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);
}
@@ -42,7 +42,7 @@ final class GenericWebsiteServiceProvider
}
String _$genericWebsiteServiceHash() =>
r'437e1b11b6e63d38151354522deda755d9333c47';
r'b320635f651574a500ee6764688ef26b6d39ef8f';
abstract class _$GenericWebsiteService extends $Notifier<void> {
void build();
@@ -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 {