improve image caching and disposal
This commit is contained in:
@@ -294,8 +294,10 @@ class GenericWebsiteService extends _$GenericWebsiteService {
|
|||||||
Future<BrowserIcon?> getCachedIcon(Uri url) async {
|
Future<BrowserIcon?> getCachedIcon(Uri url) async {
|
||||||
if (url.scheme.startsWith('https') || url.scheme.startsWith('http')) {
|
if (url.scheme.startsWith('https') || url.scheme.startsWith('http')) {
|
||||||
final cachedBrowserIcon = _browserIconCache.get(url.origin);
|
final cachedBrowserIcon = _browserIconCache.get(url.origin);
|
||||||
if (cachedBrowserIcon != null) {
|
if (cachedBrowserIcon?.image.value != null) {
|
||||||
return cachedBrowserIcon;
|
return cachedBrowserIcon;
|
||||||
|
} else if (cachedBrowserIcon != null) {
|
||||||
|
_browserIconCache.remove(url.origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
final cachedIcon = await _cacheRepository.getCachedIcon(url.origin);
|
final cachedIcon = await _cacheRepository.getCachedIcon(url.origin);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class GenericWebsiteServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$genericWebsiteServiceHash() =>
|
String _$genericWebsiteServiceHash() =>
|
||||||
r'32a561820bda432f3c17e05b9da9c85871efffb4';
|
r'80b0f7113368bdd6fbe43fc27d2ab7332fe4b692';
|
||||||
|
|
||||||
abstract class _$GenericWebsiteService extends $Notifier<void> {
|
abstract class _$GenericWebsiteService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -122,24 +122,24 @@ class TabStates extends _$TabStates {
|
|||||||
Future<void> _onIconChange(IconChangeEvent event) async {
|
Future<void> _onIconChange(IconChangeEvent event) async {
|
||||||
final IconChangeEvent(:tabId, :bytes) = event;
|
final IconChangeEvent(:tabId, :bytes) = event;
|
||||||
|
|
||||||
// Dispose old icon before replacing
|
|
||||||
state[tabId]?.icon?.dispose();
|
|
||||||
|
|
||||||
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
||||||
|
|
||||||
final current = state[tabId] ?? TabState.$default(tabId);
|
final current = state[tabId] ?? TabState.$default(tabId);
|
||||||
|
|
||||||
|
// Dispose old icon only after successfully creating new one
|
||||||
|
current.icon?.dispose();
|
||||||
|
|
||||||
state = {...state}..[tabId] = current.copyWith.icon(image);
|
state = {...state}..[tabId] = current.copyWith.icon(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
|
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
|
||||||
final ThumbnailEvent(:tabId, :bytes) = event;
|
final ThumbnailEvent(:tabId, :bytes) = event;
|
||||||
|
|
||||||
// Dispose old thumbnail before replacing
|
|
||||||
state[tabId]?.thumbnail?.dispose();
|
|
||||||
|
|
||||||
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
||||||
|
|
||||||
final current = state[tabId] ?? TabState.$default(tabId);
|
final current = state[tabId] ?? TabState.$default(tabId);
|
||||||
|
|
||||||
|
// Dispose old thumbnail only after successfully creating new one
|
||||||
|
current.thumbnail?.dispose();
|
||||||
|
|
||||||
state = {...state}..[tabId] = current.copyWith.thumbnail(image);
|
state = {...state}..[tabId] = current.copyWith.thumbnail(image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabStatesHash() => r'f2e6920017b3f46944d85254c09c54779eb89eb3';
|
String _$tabStatesHash() => r'67b497304b4137ce0115a66f06f3622c86e5727d';
|
||||||
|
|
||||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||||
Map<String, TabState> build();
|
Map<String, TabState> build();
|
||||||
|
|||||||
@@ -27,22 +27,31 @@ import 'package:weblibre/domain/entities/equatable_image.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/features/geckoview/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
|
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
|
||||||
|
import 'package:weblibre/utils/lru_cache.dart';
|
||||||
|
|
||||||
part 'web_extensions_state.g.dart';
|
part 'web_extensions_state.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class WebExtensionsState extends _$WebExtensionsState {
|
class WebExtensionsState extends _$WebExtensionsState {
|
||||||
final _imageCache = <String, EquatableImage>{};
|
late final LRUCache<String, EquatableImage> _imageCache;
|
||||||
|
|
||||||
|
WebExtensionsState()
|
||||||
|
: _imageCache = LRUCache(50, onEvict: (image) => image.dispose());
|
||||||
|
|
||||||
void _onExtensionUpdate(ExtensionDataEvent event) {
|
void _onExtensionUpdate(ExtensionDataEvent event) {
|
||||||
final ExtensionDataEvent(:extensionId, :data) = event;
|
final ExtensionDataEvent(:extensionId, :data) = event;
|
||||||
|
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
|
final cachedIcon = _imageCache.get(extensionId);
|
||||||
|
if (cachedIcon != null && cachedIcon.value == null) {
|
||||||
|
_imageCache.remove(extensionId);
|
||||||
|
}
|
||||||
|
|
||||||
final current =
|
final current =
|
||||||
state[extensionId] ??
|
state[extensionId] ??
|
||||||
WebExtensionState(
|
WebExtensionState(
|
||||||
extensionId: extensionId,
|
extensionId: extensionId,
|
||||||
icon: _imageCache[extensionId],
|
icon: _imageCache.get(extensionId),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -73,10 +82,10 @@ class WebExtensionsState extends _$WebExtensionsState {
|
|||||||
final image = await tryDecodeImage(bytes);
|
final image = await tryDecodeImage(bytes);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
// Dispose old image before replacing
|
// Dispose old image only after successfully creating new one
|
||||||
_imageCache[extensionId]?.dispose();
|
_imageCache.get(extensionId)?.dispose();
|
||||||
|
|
||||||
_imageCache[extensionId] = image;
|
_imageCache.set(extensionId, image);
|
||||||
|
|
||||||
if (state.containsKey(extensionId)) {
|
if (state.containsKey(extensionId)) {
|
||||||
state = {...state}
|
state = {...state}
|
||||||
@@ -110,9 +119,6 @@ class WebExtensionsState extends _$WebExtensionsState {
|
|||||||
|
|
||||||
ref.onDispose(() async {
|
ref.onDispose(() async {
|
||||||
// Dispose all cached images
|
// Dispose all cached images
|
||||||
for (final image in _imageCache.values) {
|
|
||||||
image.dispose();
|
|
||||||
}
|
|
||||||
_imageCache.clear();
|
_imageCache.clear();
|
||||||
|
|
||||||
// Cancel all stream subscriptions
|
// Cancel all stream subscriptions
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class WebExtensionsStateProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$webExtensionsStateHash() =>
|
String _$webExtensionsStateHash() =>
|
||||||
r'd7147f419889386f8cf526aed3318839ac22d893';
|
r'2664a50bb776197010e9da9b4e3a9379c849ba75';
|
||||||
|
|
||||||
final class WebExtensionsStateFamily extends $Family
|
final class WebExtensionsStateFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
+5
-1
@@ -36,7 +36,11 @@ class ExtensionBadgeIcon extends StatelessWidget {
|
|||||||
textColor: state.badgeTextColor,
|
textColor: state.badgeTextColor,
|
||||||
backgroundColor: state.badgeBackgroundColor,
|
backgroundColor: state.badgeBackgroundColor,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: RawImage(image: state.icon?.value, width: 24, height: 24),
|
child:
|
||||||
|
state.icon?.value.mapNotNull(
|
||||||
|
(image) => RawImage(image: image, width: 24, height: 24),
|
||||||
|
) ??
|
||||||
|
const SizedBox(width: 24, height: 24),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-9
@@ -174,17 +174,26 @@ class ShareScreenshotMenuItemButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (screenshot != null) {
|
if (screenshot != null) {
|
||||||
ui.decodeImageFromList(screenshot, (result) async {
|
ui.decodeImageFromList(screenshot, (result) async {
|
||||||
final png = await result.toByteData(format: ui.ImageByteFormat.png);
|
try {
|
||||||
|
final png = await result.toByteData(
|
||||||
if (png != null) {
|
format: ui.ImageByteFormat.png,
|
||||||
final file = XFile.fromData(
|
|
||||||
png.buffer.asUint8List(),
|
|
||||||
mimeType: 'image/png',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await SharePlus.instance.share(
|
if (png != null) {
|
||||||
ShareParams(files: [file], subject: tabState.titleOrAuthority),
|
final file = XFile.fromData(
|
||||||
);
|
png.buffer.asUint8List(),
|
||||||
|
mimeType: 'image/png',
|
||||||
|
);
|
||||||
|
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
files: [file],
|
||||||
|
subject: tabState.titleOrAuthority,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
result.dispose();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ 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 {
|
||||||
if (tabState.icon != null) {
|
final faviconIcon = tabState.icon?.value;
|
||||||
return tabState.icon!.value;
|
|
||||||
|
if (faviconIcon != null) {
|
||||||
|
return faviconIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
final icon = await ref
|
final icon = await ref
|
||||||
|
|||||||
+2
-2
@@ -97,8 +97,8 @@ class HistorySuggestions extends HookConsumerWidget {
|
|||||||
leading: RepaintBoundary(
|
leading: RepaintBoundary(
|
||||||
child:
|
child:
|
||||||
icon.data?.value.mapNotNull(
|
icon.data?.value.mapNotNull(
|
||||||
(favicon) => RawImage(
|
(image) => RawImage(
|
||||||
image: favicon,
|
image: image,
|
||||||
height: 24,
|
height: 24,
|
||||||
width: 24,
|
width: 24,
|
||||||
),
|
),
|
||||||
|
|||||||
+4
-2
@@ -184,8 +184,10 @@ class TabSearch extends HookConsumerWidget {
|
|||||||
leading: RepaintBoundary(
|
leading: RepaintBoundary(
|
||||||
child:
|
child:
|
||||||
result.icon.mapNotNull(
|
result.icon.mapNotNull(
|
||||||
(icon) =>
|
(icon) => icon.value.mapNotNull(
|
||||||
RawImage(image: icon.value, height: 24, width: 24),
|
(image) =>
|
||||||
|
RawImage(image: image, height: 24, width: 24),
|
||||||
|
),
|
||||||
) ??
|
) ??
|
||||||
UrlIcon([result.url], iconSize: 24),
|
UrlIcon([result.url], iconSize: 24),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ final _cache = LRUCache<int, EquatableImage>(
|
|||||||
onEvict: (image) => image.dispose(),
|
onEvict: (image) => image.dispose(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Clears the global image cache, disposing all cached images.
|
||||||
|
void clearImageCache() {
|
||||||
|
_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
Future<EquatableImage?> tryDecodeImage(
|
Future<EquatableImage?> tryDecodeImage(
|
||||||
Uint8List bytes, {
|
Uint8List bytes, {
|
||||||
int? targetWidth,
|
int? targetWidth,
|
||||||
@@ -38,8 +43,10 @@ Future<EquatableImage?> tryDecodeImage(
|
|||||||
final digest = secureHash(bytes);
|
final digest = secureHash(bytes);
|
||||||
|
|
||||||
final cached = _cache.get(digest);
|
final cached = _cache.get(digest);
|
||||||
if (cached != null && !cached.isDisposed) {
|
if (cached?.value != null) {
|
||||||
return cached;
|
return cached;
|
||||||
|
} else if (cached != null) {
|
||||||
|
_cache.remove(digest);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user