-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Feature: Add disk based thumbnail caching system #17991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 18 commits
4b089c4
3d2bab2
69cc86f
a876d95
f10cf97
626a129
7d22445
83f39a6
2f39834
e0fb9ae
1b36c80
224c558
14e6e26
28a5074
db1f496
be95e17
f5043b5
3693dec
d747203
d7f7698
c1af7a1
acc050c
b2902ad
3ab385b
1acc69a
589e89e
d9c9ea1
a1e01a4
40c3003
0291ad0
d7fabcc
71dd378
44e7bdf
5dad312
5a16d66
7d37bc8
00a6f07
be94e38
589dcd3
1544b9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Copyright (c) Files Community | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Files.App.Data.Contracts | ||
| { | ||
| public interface IShellChangeNotifyService : IDisposable | ||
| { | ||
| event Action<string>? ItemUpdated; | ||
|
|
||
| event Action<string>? AttributesChanged; | ||
|
|
||
| void StartMonitoring(string path); | ||
|
|
||
| void StopMonitoring(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // Copyright (c) Files Community | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Files.App.Data.Contracts | ||
| { | ||
| /// <summary> | ||
| /// Stores and retrieves cached thumbnails. | ||
| /// </summary> | ||
| public interface IThumbnailCache | ||
| { | ||
| /// <summary> | ||
| /// Retrieves a cached thumbnail. | ||
| /// </summary> | ||
| /// <returns>Thumbnail bytes, or null if not cached.</returns> | ||
| Task<byte[]?> GetAsync(string path, int size, IconOptions options, CancellationToken ct); | ||
|
|
||
| /// <summary> | ||
| /// Stores a thumbnail in the cache. | ||
| /// </summary> | ||
| Task SetAsync(string path, int size, IconOptions options, byte[] thumbnail, CancellationToken ct); | ||
|
|
||
| /// <summary> | ||
| /// Gets the current cache size in bytes. | ||
| /// </summary> | ||
| Task<long> GetSizeAsync(); | ||
|
|
||
| /// <summary> | ||
| /// Reduces cache size to the specified target in bytes. | ||
| /// </summary> | ||
| Task EvictToSizeAsync(long targetSizeBytes); | ||
|
|
||
| /// <summary> | ||
| /// Removes all cached thumbnails. | ||
| /// </summary> | ||
| Task ClearAsync(); | ||
|
|
||
| /// <summary> | ||
| /// Retrieves a cached icon from the in-memory icon cache. | ||
| /// </summary> | ||
| /// <returns>Icon bytes, or null if not cached.</returns> | ||
| byte[]? GetIcon(string extension, int size); | ||
|
|
||
| /// <summary> | ||
| /// Stores an icon in the in-memory icon cache. | ||
| /// </summary> | ||
| void SetIcon(string extension, int size, byte[] iconData); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| // Copyright (c) Files Community | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Files.App.Data.Contracts | ||
| { | ||
| /// <summary> | ||
| /// Generates thumbnails for specific file types. | ||
| /// </summary> | ||
| public interface IThumbnailGenerator | ||
| { | ||
| /// <summary> | ||
| /// Gets the file extensions this generator supports. | ||
| /// </summary> | ||
| IEnumerable<string> SupportedTypes { get; } | ||
|
|
||
| /// <summary> | ||
| /// Generates a thumbnail for the specified path. | ||
| /// </summary> | ||
| /// <returns>Thumbnail bytes, or null if generation fails.</returns> | ||
| Task<byte[]?> GenerateAsync( | ||
| string path, | ||
| int size, | ||
| bool isFolder, | ||
| IconOptions options, | ||
| CancellationToken ct); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // Copyright (c) Files Community | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Files.App.Data.Contracts | ||
| { | ||
| /// <summary> | ||
| /// Provides thumbnail retrieval and cache management. | ||
| /// </summary> | ||
| public interface IThumbnailService | ||
| { | ||
| /// <summary> | ||
| /// Gets a thumbnail for the specified path. | ||
| /// </summary> | ||
| /// <returns>Thumbnail bytes, or null if unavailable.</returns> | ||
| Task<byte[]?> GetThumbnailAsync( | ||
| string path, | ||
| int size, | ||
| bool isFolder, | ||
| IconOptions options = IconOptions.None, | ||
| CancellationToken ct = default); | ||
|
|
||
| /// <summary> | ||
| /// Gets a cached thumbnail without calling the Shell API. | ||
| /// Returns null if not found in the cache. | ||
| /// </summary> | ||
| Task<byte[]?> GetCachedThumbnailAsync( | ||
| string path, | ||
| int size, | ||
| bool isFolder, | ||
| IconOptions options, | ||
| CancellationToken ct = default); | ||
|
|
||
| /// <summary> | ||
| /// Registers a thumbnail generator. | ||
| /// </summary> | ||
| void RegisterGenerator(IThumbnailGenerator generator); | ||
|
|
||
| /// <summary> | ||
| /// Clears all cached thumbnails. | ||
| /// </summary> | ||
| Task ClearCacheAsync(); | ||
|
|
||
| /// <summary> | ||
| /// Gets the current cache size in bytes. | ||
| /// </summary> | ||
| Task<long> GetCacheSizeAsync(); | ||
|
|
||
| /// <summary> | ||
| /// Reduces cache size to the specified target in bytes. | ||
| /// </summary> | ||
| Task EvictCacheAsync(long targetSizeBytes); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| // Copyright (c) Files Community | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.Extensions.Logging; | ||
| using System.Runtime.InteropServices; | ||
| using Windows.Win32; | ||
| using Windows.Win32.Foundation; | ||
| using Windows.Win32.UI.WindowsAndMessaging; | ||
|
|
||
| namespace Files.App.Services | ||
| { | ||
| public sealed partial class ShellChangeNotifyService : IShellChangeNotifyService | ||
| { | ||
| private readonly ILogger _logger = Ioc.Default.GetRequiredService<ILogger<ShellChangeNotifyService>>(); | ||
| private readonly WNDPROC _windowProcedure; | ||
|
|
||
| private HWND _windowHandle; | ||
| private uint _notifyId; | ||
| private bool _disposed; | ||
|
|
||
| public event Action<string>? ItemUpdated; | ||
|
|
||
| public event Action<string>? AttributesChanged; | ||
|
|
||
| public ShellChangeNotifyService() | ||
| { | ||
| _windowProcedure = WindowProc; | ||
| } | ||
|
|
||
| public unsafe void StartMonitoring(string path) | ||
| { | ||
| StopMonitoring(); | ||
|
|
||
| string className = "FilesShellNotify_" + Environment.TickCount64; | ||
|
|
||
| fixed (char* ptr = className) | ||
| { | ||
| var pWindProc = Marshal.GetFunctionPointerForDelegate(_windowProcedure); | ||
| var pfnWndProc = (delegate* unmanaged[Stdcall]<HWND, uint, WPARAM, LPARAM, LRESULT>)pWindProc; | ||
|
|
||
| WNDCLASSEXW param = new() | ||
| { | ||
| cbSize = (uint)Marshal.SizeOf(typeof(WNDCLASSEXW)), | ||
| lpfnWndProc = pfnWndProc, | ||
| hInstance = PInvoke.GetModuleHandle(default(PCWSTR)), | ||
| lpszClassName = ptr | ||
| }; | ||
|
|
||
| PInvoke.RegisterClassEx(in param); | ||
|
yair100 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| _windowHandle = PInvoke.CreateWindowEx( | ||
| WINDOW_EX_STYLE.WS_EX_LEFT, | ||
| className, | ||
| string.Empty, | ||
| WINDOW_STYLE.WS_OVERLAPPED, | ||
| 0, 0, 0, 0, | ||
| new HWND(-3), | ||
| null, null, null); | ||
|
|
||
| if (_windowHandle == default) | ||
| return; | ||
|
|
||
| IntPtr pidl = Win32PInvoke.ILCreateFromPath(path); | ||
|
Check failure on line 64 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| if (pidl == IntPtr.Zero) | ||
| return; | ||
|
|
||
| try | ||
| { | ||
| var entry = new Win32PInvoke.SHChangeNotifyEntry | ||
|
Check failure on line 70 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| { | ||
| pidl = pidl, | ||
| fRecursive = false | ||
| }; | ||
|
|
||
| _notifyId = Win32PInvoke.SHChangeNotifyRegister( | ||
|
Check failure on line 76 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| _windowHandle, | ||
| Win32PInvoke.SHCNRF_SHELLLEVEL | Win32PInvoke.SHCNRF_INTERRUPTLEVEL, | ||
|
Check failure on line 78 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| Win32PInvoke.SHCNE_UPDATEITEM | Win32PInvoke.SHCNE_ATTRIBUTES, | ||
|
Check failure on line 79 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| (uint)Win32PInvoke.WM_SHNOTIFY, | ||
|
Check failure on line 80 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| 1, | ||
| ref entry); | ||
|
|
||
| if (_notifyId != 0) | ||
| _logger.LogInformation("Shell notify registered for {Path} (ID: {Id})", path, _notifyId); | ||
| else | ||
| _logger.LogWarning("Shell notify registration failed for {Path}", path); | ||
| } | ||
| finally | ||
| { | ||
| Win32PInvoke.ILFree(pidl); | ||
|
Check failure on line 91 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| } | ||
| } | ||
|
|
||
| public void StopMonitoring() | ||
| { | ||
| if (_notifyId != 0) | ||
| { | ||
| Win32PInvoke.SHChangeNotifyDeregister(_notifyId); | ||
|
Check failure on line 99 in src/Files.App/Services/ShellChangeNotifyService.cs
|
||
| _notifyId = 0; | ||
| } | ||
|
|
||
| if (_windowHandle != default) | ||
| { | ||
| PInvoke.DestroyWindow(_windowHandle); | ||
| _windowHandle = default; | ||
| } | ||
| } | ||
|
|
||
| private LRESULT WindowProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) | ||
| { | ||
| if (uMsg == (uint)Win32PInvoke.WM_SHNOTIFY) | ||
| ProcessNotification(wParam, lParam); | ||
|
|
||
| return PInvoke.DefWindowProc(hWnd, uMsg, wParam, lParam); | ||
| } | ||
|
|
||
| private void ProcessNotification(WPARAM wParam, LPARAM lParam) | ||
| { | ||
| uint eventId = (uint)lParam.Value; | ||
| string path = GetPathFromNotification(wParam); | ||
|
|
||
| if (string.IsNullOrEmpty(path)) | ||
| return; | ||
|
|
||
| if (eventId == Win32PInvoke.SHCNE_UPDATEITEM) | ||
| { | ||
| _logger.LogInformation("SHCNE_UPDATEITEM: {Path}", path); | ||
| ItemUpdated?.Invoke(path); | ||
| } | ||
| else if (eventId == Win32PInvoke.SHCNE_ATTRIBUTES) | ||
| { | ||
| _logger.LogInformation("SHCNE_ATTRIBUTES: {Path}", path); | ||
| AttributesChanged?.Invoke(path); | ||
| } | ||
| } | ||
|
|
||
| private static string GetPathFromNotification(WPARAM wParam) | ||
| { | ||
| if (wParam.Value == 0) | ||
| return string.Empty; | ||
|
|
||
| var notifyStruct = Marshal.PtrToStructure<Win32PInvoke.SHNOTIFYSTRUCT>((nint)wParam.Value); | ||
| if (notifyStruct.dwItem1 == IntPtr.Zero) | ||
| return string.Empty; | ||
|
|
||
| IntPtr buffer = Marshal.AllocHGlobal(Win32PInvoke.MAX_PATH * 2); | ||
| try | ||
| { | ||
| if (Win32PInvoke.SHGetPathFromIDList(notifyStruct.dwItem1, buffer)) | ||
| return Marshal.PtrToStringUni(buffer) ?? string.Empty; | ||
|
|
||
| return string.Empty; | ||
| } | ||
| finally | ||
| { | ||
| Marshal.FreeHGlobal(buffer); | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| if (_disposed) | ||
| return; | ||
|
|
||
| _logger.LogInformation("Shell notify service disposed"); | ||
| StopMonitoring(); | ||
| _disposed = true; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.