improve image lifecycle handling
This commit is contained in:
@@ -331,6 +331,11 @@ class TabStates extends _$TabStates {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ref.onDispose(() async {
|
ref.onDispose(() async {
|
||||||
|
// Dispose all remaining tab images
|
||||||
|
for (final tab in state.values) {
|
||||||
|
_disposeTabImages(tab);
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel all stream subscriptions
|
// Cancel all stream subscriptions
|
||||||
for (final sub in subscriptions) {
|
for (final sub in subscriptions) {
|
||||||
await sub.cancel();
|
await sub.cancel();
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabStatesHash() => r'2d18c0c2105ce53bd13e6a6dff1d33c1b531919d';
|
String _$tabStatesHash() => r'985d2eb87c59c3a113b8bbcf9d96267c42a2f543';
|
||||||
|
|
||||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||||
Map<String, TabState> build();
|
Map<String, TabState> build();
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ class WebExtensionsState extends _$WebExtensionsState {
|
|||||||
} else {
|
} else {
|
||||||
if (state.containsKey(extensionId)) {
|
if (state.containsKey(extensionId)) {
|
||||||
state = {...state}..remove(extensionId);
|
state = {...state}..remove(extensionId);
|
||||||
// Dispose the cached image when extension is removed
|
// remove() triggers onEvict which handles disposal
|
||||||
_imageCache.remove(extensionId)?.dispose();
|
_imageCache.remove(extensionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,9 +83,7 @@ class WebExtensionsState extends _$WebExtensionsState {
|
|||||||
final image = await tryDecodeImage(bytes);
|
final image = await tryDecodeImage(bytes);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
// Dispose old image only after successfully creating new one
|
// set() will evict the old entry via onEvict callback, which handles disposal
|
||||||
_imageCache.get(extensionId)?.dispose();
|
|
||||||
|
|
||||||
_imageCache.set(extensionId, image);
|
_imageCache.set(extensionId, image);
|
||||||
|
|
||||||
if (state.containsKey(extensionId)) {
|
if (state.containsKey(extensionId)) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class WebExtensionsStateProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$webExtensionsStateHash() =>
|
String _$webExtensionsStateHash() =>
|
||||||
r'd74f739a2f33395c10e30a81c8031f2f2630e418';
|
r'28cd9c99bc167fa1a051eccf3d23917a417e54ea';
|
||||||
|
|
||||||
final class WebExtensionsStateFamily extends $Family
|
final class WebExtensionsStateFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
+6
-2
@@ -56,6 +56,7 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
|
|||||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
import 'package:weblibre/presentation/icons/weblibre_icons.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/selectable_chips.dart';
|
||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
|
|
||||||
@@ -537,8 +538,11 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
itemAvatar: (item) =>
|
itemAvatar: (item) =>
|
||||||
item.tabState?.icon.mapNotNull(
|
item.tabState?.icon.mapNotNull(
|
||||||
(icon) => icon.value.mapNotNull(
|
(icon) => SafeRawImage(
|
||||||
(image) => RawImage(image: image, height: 24, width: 24),
|
image: icon,
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
fallback: UrlIcon([item.url], iconSize: 24),
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
UrlIcon([item.url], iconSize: 16),
|
UrlIcon([item.url], iconSize: 16),
|
||||||
|
|||||||
+6
-5
@@ -20,6 +20,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/web_extension.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/web_extension.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
|
|
||||||
class ExtensionBadgeIcon extends StatelessWidget {
|
class ExtensionBadgeIcon extends StatelessWidget {
|
||||||
final WebExtensionState state;
|
final WebExtensionState state;
|
||||||
@@ -36,11 +37,11 @@ class ExtensionBadgeIcon extends StatelessWidget {
|
|||||||
textColor: state.badgeTextColor,
|
textColor: state.badgeTextColor,
|
||||||
backgroundColor: state.badgeBackgroundColor,
|
backgroundColor: state.badgeBackgroundColor,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child:
|
child: SafeRawImage(
|
||||||
state.icon?.value.mapNotNull(
|
image: state.icon,
|
||||||
(image) => RawImage(image: image, width: 24, height: 24),
|
width: 24,
|
||||||
) ??
|
height: 24,
|
||||||
const SizedBox(width: 24, height: 24),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:weblibre/domain/entities/equatable_image.dart';
|
||||||
import 'package:weblibre/domain/services/generic_website.dart';
|
import 'package:weblibre/domain/services/generic_website.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
|
|
||||||
class TabIcon extends HookConsumerWidget {
|
class TabIcon extends HookConsumerWidget {
|
||||||
final TabState tabState;
|
final TabState tabState;
|
||||||
@@ -36,17 +38,16 @@ class TabIcon extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final icon = useCachedFuture(() async {
|
final icon = useCachedFuture(() async {
|
||||||
final faviconIcon = tabState.icon?.value;
|
if (tabState.icon case final EquatableImage tabIcon
|
||||||
|
when !tabIcon.isDisposed) {
|
||||||
if (faviconIcon != null) {
|
return tabIcon;
|
||||||
return faviconIcon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final icon = await ref
|
final cachedIcon = await ref
|
||||||
.read(genericWebsiteServiceProvider.notifier)
|
.read(genericWebsiteServiceProvider.notifier)
|
||||||
.getCachedIcon(tabState.url);
|
.getCachedIcon(tabState.url);
|
||||||
|
|
||||||
return icon?.image.value;
|
return cachedIcon?.image;
|
||||||
}, [tabState.icon, tabState.url]);
|
}, [tabState.icon, tabState.url]);
|
||||||
|
|
||||||
return Skeletonizer(
|
return Skeletonizer(
|
||||||
@@ -56,8 +57,12 @@ class TabIcon extends HookConsumerWidget {
|
|||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child:
|
child:
|
||||||
icon.data.mapNotNull(
|
icon.data.mapNotNull(
|
||||||
(icon) =>
|
(image) => SafeRawImage(
|
||||||
RawImage(image: icon, height: iconSize, width: iconSize),
|
image: image,
|
||||||
|
height: iconSize,
|
||||||
|
width: iconSize,
|
||||||
|
fallback: Icon(MdiIcons.web, size: iconSize),
|
||||||
|
),
|
||||||
) ??
|
) ??
|
||||||
Icon(MdiIcons.web, size: iconSize),
|
Icon(MdiIcons.web, size: iconSize),
|
||||||
),
|
),
|
||||||
|
|||||||
+7
-6
@@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/features/find_in_page/domain/entitie
|
|||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
if (tabState.thumbnail?.value != null)
|
if (tabState.thumbnail != null && !tabState.thumbnail!.isDisposed)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
@@ -220,8 +221,8 @@ class GridTabPreview extends HookConsumerWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: RawImage(
|
child: SafeRawImage(
|
||||||
image: tabState.thumbnail!.value,
|
image: tabState.thumbnail,
|
||||||
fit: BoxFit.fitWidth,
|
fit: BoxFit.fitWidth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -290,10 +291,10 @@ class ListTabPreview extends HookConsumerWidget {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
contentPadding: const EdgeInsets.only(left: 4),
|
contentPadding: const EdgeInsets.only(left: 4),
|
||||||
leading: (tabState.thumbnail?.value != null)
|
leading: (tabState.thumbnail != null && !tabState.thumbnail!.isDisposed)
|
||||||
? RepaintBoundary(
|
? RepaintBoundary(
|
||||||
child: RawImage(
|
child: SafeRawImage(
|
||||||
image: tabState.thumbnail!.value,
|
image: tabState.thumbnail,
|
||||||
fit: BoxFit.fitHeight,
|
fit: BoxFit.fitHeight,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-9
@@ -29,6 +29,7 @@ import 'package:weblibre/features/geckoview/features/search/domain/providers/eng
|
|||||||
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
|
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
|
||||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
|
|
||||||
class HistorySuggestions extends HookConsumerWidget {
|
class HistorySuggestions extends HookConsumerWidget {
|
||||||
@@ -92,15 +93,12 @@ class HistorySuggestions extends HookConsumerWidget {
|
|||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: RepaintBoundary(
|
leading: RepaintBoundary(
|
||||||
child:
|
child: SafeRawImage(
|
||||||
icon.data?.value.mapNotNull(
|
image: icon.data,
|
||||||
(image) => RawImage(
|
height: 24,
|
||||||
image: image,
|
width: 24,
|
||||||
height: 24,
|
fallback: const Icon(MdiIcons.web, size: 24),
|
||||||
width: 24,
|
),
|
||||||
),
|
|
||||||
) ??
|
|
||||||
const Icon(MdiIcons.web, size: 24),
|
|
||||||
),
|
),
|
||||||
title: suggestion.title.mapNotNull(
|
title: suggestion.title.mapNotNull(
|
||||||
(title) => Text(
|
(title) => Text(
|
||||||
|
|||||||
+6
-3
@@ -39,6 +39,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec
|
|||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
import 'package:weblibre/utils/text_highlight.dart';
|
import 'package:weblibre/utils/text_highlight.dart';
|
||||||
@@ -187,9 +188,11 @@ class TabSearch extends HookConsumerWidget {
|
|||||||
leading: RepaintBoundary(
|
leading: RepaintBoundary(
|
||||||
child:
|
child:
|
||||||
result.icon.mapNotNull(
|
result.icon.mapNotNull(
|
||||||
(icon) => icon.value.mapNotNull(
|
(icon) => SafeRawImage(
|
||||||
(image) =>
|
image: icon,
|
||||||
RawImage(image: image, height: 24, width: 24),
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
fallback: UrlIcon([result.url], iconSize: 24),
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
UrlIcon([result.url], iconSize: 24),
|
UrlIcon([result.url], iconSize: 24),
|
||||||
|
|||||||
@@ -25,12 +25,9 @@ import 'package:weblibre/core/logger.dart';
|
|||||||
import 'package:weblibre/domain/entities/equatable_image.dart';
|
import 'package:weblibre/domain/entities/equatable_image.dart';
|
||||||
import 'package:weblibre/utils/lru_cache.dart';
|
import 'package:weblibre/utils/lru_cache.dart';
|
||||||
|
|
||||||
final _cache = LRUCache<int, EquatableImage>(
|
final _cache = LRUCache<int, EquatableImage>(100);
|
||||||
100,
|
|
||||||
onEvict: (image) => image.dispose(),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Clears the global image cache, disposing all cached images.
|
/// Clears the global image decode cache.
|
||||||
void clearImageCache() {
|
void clearImageCache() {
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* 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 'package:flutter/widgets.dart';
|
||||||
|
import 'package:weblibre/domain/entities/equatable_image.dart';
|
||||||
|
|
||||||
|
/// A safe wrapper around [RawImage] that guards against disposed images.
|
||||||
|
///
|
||||||
|
/// Checks [EquatableImage.isDisposed] before rendering. When the image
|
||||||
|
/// is null or disposed, renders [fallback] (defaults to an empty SizedBox
|
||||||
|
/// matching the requested dimensions).
|
||||||
|
class SafeRawImage extends StatelessWidget {
|
||||||
|
final EquatableImage? image;
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
|
final BoxFit? fit;
|
||||||
|
final Widget? fallback;
|
||||||
|
|
||||||
|
const SafeRawImage({
|
||||||
|
super.key,
|
||||||
|
required this.image,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.fit,
|
||||||
|
this.fallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final uiImage = image?.value;
|
||||||
|
|
||||||
|
if (uiImage == null) {
|
||||||
|
return fallback ?? SizedBox(width: width, height: height);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RawImage(
|
||||||
|
image: uiImage,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:weblibre/domain/services/generic_website.dart';
|
import 'package:weblibre/domain/services/generic_website.dart';
|
||||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
|
|
||||||
class UrlIcon extends HookConsumerWidget {
|
class UrlIcon extends HookConsumerWidget {
|
||||||
final double iconSize;
|
final double iconSize;
|
||||||
@@ -46,11 +47,12 @@ class UrlIcon extends HookConsumerWidget {
|
|||||||
dimension: iconSize,
|
dimension: iconSize,
|
||||||
child: (icon.data != null)
|
child: (icon.data != null)
|
||||||
? RepaintBoundary(
|
? RepaintBoundary(
|
||||||
child: RawImage(
|
child: SafeRawImage(
|
||||||
image: icon.data?.image.value,
|
image: icon.data?.image,
|
||||||
height: iconSize,
|
height: iconSize,
|
||||||
width: iconSize,
|
width: iconSize,
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
|
fallback: Icon(MdiIcons.web, size: iconSize),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Icon(MdiIcons.web, size: iconSize),
|
: Icon(MdiIcons.web, size: iconSize),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||||
import 'package:weblibre/presentation/controllers/website_title.dart';
|
import 'package:weblibre/presentation/controllers/website_title.dart';
|
||||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
|
|
||||||
class WebsiteTitleTile extends HookConsumerWidget {
|
class WebsiteTitleTile extends HookConsumerWidget {
|
||||||
@@ -45,10 +46,11 @@ class WebsiteTitleTile extends HookConsumerWidget {
|
|||||||
leading: RepaintBoundary(
|
leading: RepaintBoundary(
|
||||||
child:
|
child:
|
||||||
info.favicon.mapNotNull(
|
info.favicon.mapNotNull(
|
||||||
(favicon) => RawImage(
|
(favicon) => SafeRawImage(
|
||||||
image: favicon.image.value,
|
image: favicon.image,
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
|
fallback: const Icon(MdiIcons.web, size: 24),
|
||||||
),
|
),
|
||||||
) ??
|
) ??
|
||||||
const Icon(MdiIcons.web, size: 24),
|
const Icon(MdiIcons.web, size: 24),
|
||||||
@@ -71,8 +73,8 @@ class WebsiteTitleTile extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => ListTile(
|
loading: () => ListTile(
|
||||||
leading: RawImage(
|
leading: SafeRawImage(
|
||||||
image: initialTabState.favicon?.image.value,
|
image: initialTabState.favicon?.image,
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user