From e9e2e7c3b1021b81c886182c1d11e76da8ee2404 Mon Sep 17 00:00:00 2001 From: Saravanan G Date: Tue, 28 Apr 2026 15:26:29 +0530 Subject: [PATCH] Fix: Thumbnail not updating for files created by external applications other than Files/Files Explorer --- src/Files.App/Data/Items/ListedItem.cs | 2 + src/Files.App/ViewModels/ShellViewModel.cs | 48 +++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Files.App/Data/Items/ListedItem.cs b/src/Files.App/Data/Items/ListedItem.cs index 3f1e2725925d..2a9798970da2 100644 --- a/src/Files.App/Data/Items/ListedItem.cs +++ b/src/Files.App/Data/Items/ListedItem.cs @@ -32,6 +32,8 @@ public partial class ListedItem : ObservableObject, IGroupableItem, IListedItem public byte[]? PreloadedIconData { get; set; } + public bool NeedsDelayedThumbnailLoad { get; set; } + private volatile int itemPropertiesInitialized = 0; public bool ItemPropertiesInitialized { diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index dafb429163f4..f016c95f0cf0 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -35,6 +35,7 @@ public sealed partial class ShellViewModel : ObservableObject, IDisposable private readonly SemaphoreSlim getFileOrFolderSemaphore; private readonly SemaphoreSlim bulkOperationSemaphore; private readonly SemaphoreSlim loadThumbnailSemaphore; + private readonly ConcurrentDictionary thumbnailRetryDebounce; private readonly ConcurrentQueue<(uint Action, string FileName)> operationQueue; private readonly ConcurrentQueue gitChangesQueue; private readonly ConcurrentDictionary itemLoadQueue; @@ -561,6 +562,7 @@ public ShellViewModel(LayoutPreferencesManager folderSettingsViewModel) operationQueue = new ConcurrentQueue<(uint Action, string FileName)>(); gitChangesQueue = new ConcurrentQueue(); itemLoadQueue = new ConcurrentDictionary(); + thumbnailRetryDebounce = new ConcurrentDictionary(); addFilesCTS = new CancellationTokenSource(); semaphoreCTS = new CancellationTokenSource(); loadPropsCTS = new CancellationTokenSource(); @@ -733,6 +735,12 @@ public void CancelLoadAndClearFiles() addFilesCTS = new CancellationTokenSource(); } CancelExtendedPropertiesLoading(); + foreach (var cts in thumbnailRetryDebounce.Values) + { + cts.Cancel(); + cts.Dispose(); + } + thumbnailRetryDebounce.Clear(); filesAndFolders.Clear(); FilesAndFolders.Clear(); CancelSearch(); @@ -1170,7 +1178,11 @@ await dispatcherQueue.EnqueueOrInvokeAsync(async () => cancellationToken.ThrowIfCancellationRequested(); - if (result is not null) + if (result is null) + { + item.NeedsDelayedThumbnailLoad = true; + } + else { await dispatcherQueue.EnqueueOrInvokeAsync(async () => { @@ -2650,6 +2662,34 @@ private async Task AddFileOrFolderAsync(ListedItem? item) private async Task UpdateFilesOrFoldersAsync(IEnumerable paths, bool hasSyncStatus) { + foreach (var path in paths) + { + var item = filesAndFolders.ToList().FirstOrDefault(x => x.ItemPath.Equals(path, StringComparison.OrdinalIgnoreCase)); + if (item is not null && item.NeedsDelayedThumbnailLoad) + { + if (thumbnailRetryDebounce.TryGetValue(path, out var existingCts)) + { + existingCts.Cancel(); + existingCts.Dispose(); + } + + var debounceCts = new CancellationTokenSource(); + thumbnailRetryDebounce[path] = debounceCts; + var debounceToken = debounceCts.Token; + + _ = Task.Delay(500, debounceToken) + .ContinueWith(_ => + { + if (thumbnailRetryDebounce.TryRemove(path, out var cts)) + cts.Dispose(); + + item.NeedsDelayedThumbnailLoad = false; + return LoadThumbnailAsync(item, debounceToken); + }, debounceToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default) + .Unwrap(); + } + } + try { await enumFolderSemaphore.WaitAsync(semaphoreCTS.Token); @@ -2712,6 +2752,12 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => { filesAndFolders.Remove(matchingItem); + if (thumbnailRetryDebounce.TryRemove(matchingItem.ItemPath, out var debounceCts)) + { + debounceCts.Cancel(); + debounceCts.Dispose(); + } + if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible) { // Main file is removed, remove connected ADS