dispose images correctly
This commit is contained in:
@@ -45,6 +45,24 @@ part 'tab_state.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabStates extends _$TabStates {
|
||||
/// Disposes images from a TabState to free GPU memory.
|
||||
void _disposeTabImages(TabState tab) {
|
||||
tab.icon?.dispose();
|
||||
tab.thumbnail?.dispose();
|
||||
}
|
||||
|
||||
/// Updates state while disposing images from removed tabs.
|
||||
void _updateState(Map<String, TabState> newState) {
|
||||
// Find and dispose images from tabs that are being removed
|
||||
for (final tabId in state.keys) {
|
||||
if (!newState.containsKey(tabId)) {
|
||||
_disposeTabImages(state[tabId]!);
|
||||
}
|
||||
}
|
||||
|
||||
state = newState;
|
||||
}
|
||||
|
||||
Future<void> _onTabContentStateChange(TabContentState contentState) async {
|
||||
final current = await patchedState(contentState.id);
|
||||
|
||||
@@ -71,7 +89,7 @@ class TabStates extends _$TabStates {
|
||||
isLoading: contentState.isLoading,
|
||||
);
|
||||
|
||||
state = {...state}..[contentState.id] = newState;
|
||||
_updateState({...state}..[contentState.id] = newState);
|
||||
|
||||
if (newState.isFinishedLoading) {
|
||||
ref
|
||||
@@ -104,6 +122,9 @@ class TabStates extends _$TabStates {
|
||||
Future<void> _onIconChange(IconChangeEvent event) async {
|
||||
final IconChangeEvent(:tabId, :bytes) = event;
|
||||
|
||||
// Dispose old icon before replacing
|
||||
state[tabId]?.icon?.dispose();
|
||||
|
||||
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
||||
|
||||
final current = state[tabId] ?? TabState.$default(tabId);
|
||||
@@ -113,6 +134,9 @@ class TabStates extends _$TabStates {
|
||||
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
|
||||
final ThumbnailEvent(:tabId, :bytes) = event;
|
||||
|
||||
// Dispose old thumbnail before replacing
|
||||
state[tabId]?.thumbnail?.dispose();
|
||||
|
||||
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
|
||||
|
||||
final current = state[tabId] ?? TabState.$default(tabId);
|
||||
@@ -244,6 +268,12 @@ class TabStates extends _$TabStates {
|
||||
);
|
||||
|
||||
ref.onDispose(() async {
|
||||
// Dispose all images from all tabs
|
||||
for (final tab in state.values) {
|
||||
_disposeTabImages(tab);
|
||||
}
|
||||
|
||||
// Cancel all stream subscriptions
|
||||
for (final sub in subscriptions) {
|
||||
await sub.cancel();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabStatesHash() => r'cd73ea6ea479f2708a2c9e5ca86054c9d2464116';
|
||||
String _$tabStatesHash() => r'f2e6920017b3f46944d85254c09c54779eb89eb3';
|
||||
|
||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||
Map<String, TabState> build();
|
||||
|
||||
@@ -61,6 +61,8 @@ class WebExtensionsState extends _$WebExtensionsState {
|
||||
} else {
|
||||
if (state.containsKey(extensionId)) {
|
||||
state = {...state}..remove(extensionId);
|
||||
// Dispose the cached image when extension is removed
|
||||
_imageCache.remove(extensionId)?.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,13 +73,14 @@ class WebExtensionsState extends _$WebExtensionsState {
|
||||
final image = await tryDecodeImage(bytes);
|
||||
|
||||
if (image != null) {
|
||||
if (_imageCache[extensionId] != image) {
|
||||
_imageCache[extensionId] = image;
|
||||
// Dispose old image before replacing
|
||||
_imageCache[extensionId]?.dispose();
|
||||
|
||||
if (state.containsKey(extensionId)) {
|
||||
state = {...state}
|
||||
..[extensionId] = state[extensionId]!.copyWith.icon(image);
|
||||
}
|
||||
_imageCache[extensionId] = image;
|
||||
|
||||
if (state.containsKey(extensionId)) {
|
||||
state = {...state}
|
||||
..[extensionId] = state[extensionId]!.copyWith.icon(image);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,6 +109,13 @@ class WebExtensionsState extends _$WebExtensionsState {
|
||||
};
|
||||
|
||||
ref.onDispose(() async {
|
||||
// Dispose all cached images
|
||||
for (final image in _imageCache.values) {
|
||||
image.dispose();
|
||||
}
|
||||
_imageCache.clear();
|
||||
|
||||
// Cancel all stream subscriptions
|
||||
for (final sub in subscriptions) {
|
||||
await sub.cancel();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ final class WebExtensionsStateProvider
|
||||
}
|
||||
|
||||
String _$webExtensionsStateHash() =>
|
||||
r'6fb44ab543250f2b2554e410ee749fdd035cd4e7';
|
||||
r'd7147f419889386f8cf526aed3318839ac22d893';
|
||||
|
||||
final class WebExtensionsStateFamily extends $Family
|
||||
with
|
||||
|
||||
+2
-2
@@ -205,7 +205,7 @@ class GridTabPreview extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (tabState.thumbnail != null)
|
||||
if (tabState.thumbnail?.value != null)
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
@@ -284,7 +284,7 @@ class ListTabPreview extends HookConsumerWidget {
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
contentPadding: const EdgeInsets.only(left: 4),
|
||||
leading: (tabState.thumbnail != null)
|
||||
leading: (tabState.thumbnail?.value != null)
|
||||
? RepaintBoundary(
|
||||
child: RawImage(
|
||||
image: tabState.thumbnail!.value,
|
||||
|
||||
@@ -24,7 +24,10 @@ import 'package:fast_equatable/hash.dart';
|
||||
import 'package:weblibre/domain/entities/equatable_image.dart';
|
||||
import 'package:weblibre/utils/lru_cache.dart';
|
||||
|
||||
final _cache = LRUCache<int, EquatableImage>(100);
|
||||
final _cache = LRUCache<int, EquatableImage>(
|
||||
100,
|
||||
onEvict: (image) => image.dispose(),
|
||||
);
|
||||
|
||||
Future<EquatableImage?> tryDecodeImage(
|
||||
Uint8List bytes, {
|
||||
@@ -35,7 +38,7 @@ Future<EquatableImage?> tryDecodeImage(
|
||||
final digest = secureHash(bytes);
|
||||
|
||||
final cached = _cache.get(digest);
|
||||
if (cached != null) {
|
||||
if (cached != null && !cached.isDisposed) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
@@ -50,7 +53,7 @@ Future<EquatableImage?> tryDecodeImage(
|
||||
final frameInfo = await codec.getNextFrame();
|
||||
final image = EquatableImage(frameInfo.image, hash: digest);
|
||||
|
||||
if (image.value.width > 0) {
|
||||
if (image.value != null && image.value!.width > 0) {
|
||||
_cache.set(digest, image);
|
||||
return image;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user