diff --git a/src/Files.App/Data/Contracts/IGeneralSettingsService.cs b/src/Files.App/Data/Contracts/IGeneralSettingsService.cs index e473508791fe..8889f13c8113 100644 --- a/src/Files.App/Data/Contracts/IGeneralSettingsService.cs +++ b/src/Files.App/Data/Contracts/IGeneralSettingsService.cs @@ -304,5 +304,20 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty /// Gets or sets a value whether the filter header should be displayed. /// bool ShowFilterHeader { get; set; } + + /// + /// Gets or sets the preferred search engine. + /// + PreferredSearchEngine PreferredSearchEngine { get; set; } + + /// + /// Gets or sets a value indicating whether to use Everything for folder size calculations. + /// + bool UseEverythingForFolderSizes { get; set; } + + /// + /// Gets or sets the maximum number of results Everything should return for folder size calculations. + /// + int EverythingMaxFolderSizeResults { get; set; } } } diff --git a/src/Files.App/Data/Enums/PreferredSearchEngine.cs b/src/Files.App/Data/Enums/PreferredSearchEngine.cs new file mode 100644 index 000000000000..a8dd3d665a43 --- /dev/null +++ b/src/Files.App/Data/Enums/PreferredSearchEngine.cs @@ -0,0 +1,21 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Data.Enums +{ + /// + /// Defines constants that specify the preferred search engine. + /// + public enum PreferredSearchEngine + { + /// + /// Windows Search engine. + /// + Windows, + + /// + /// Everything search engine. + /// + Everything, + } +} diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj index 6ed8fc5d9166..3b6d8fccfa5a 100644 --- a/src/Files.App/Files.App.csproj +++ b/src/Files.App/Files.App.csproj @@ -59,6 +59,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/src/Files.App/GlobalUsings.cs b/src/Files.App/GlobalUsings.cs index 438eebbcdbea..f845de8d5c4b 100644 --- a/src/Files.App/GlobalUsings.cs +++ b/src/Files.App/GlobalUsings.cs @@ -15,6 +15,9 @@ global using global::System.Text.Json.Serialization; global using SystemIO = global::System.IO; +// Microsoft Extensions +global using global::Microsoft.Extensions.Logging; + // CommunityToolkit.Mvvm global using global::CommunityToolkit.Mvvm.ComponentModel; global using global::CommunityToolkit.Mvvm.DependencyInjection; diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 9be8c4bd051f..05985346f250 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -4,6 +4,7 @@ using Files.App.Helpers.Application; using Files.App.Services.SizeProvider; using Files.App.Utils.Logger; +using Files.App.Utils.Storage.Search; using Files.App.ViewModels.Settings; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -246,6 +247,11 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() + // Search Engine Services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() // ViewModels .AddSingleton() .AddSingleton() diff --git a/src/Files.App/Libraries/Everything32.dll b/src/Files.App/Libraries/Everything32.dll new file mode 100644 index 000000000000..fed6852d8a65 Binary files /dev/null and b/src/Files.App/Libraries/Everything32.dll differ diff --git a/src/Files.App/Libraries/Everything64.dll b/src/Files.App/Libraries/Everything64.dll new file mode 100644 index 000000000000..f34745ac8d7f Binary files /dev/null and b/src/Files.App/Libraries/Everything64.dll differ diff --git a/src/Files.App/Libraries/EverythingARM64.dll b/src/Files.App/Libraries/EverythingARM64.dll new file mode 100644 index 000000000000..81dbd4e62bd8 Binary files /dev/null and b/src/Files.App/Libraries/EverythingARM64.dll differ diff --git a/src/Files.App/Services/Search/EverythingSdk3Service.cs b/src/Files.App/Services/Search/EverythingSdk3Service.cs new file mode 100644 index 000000000000..247bd057b1ab --- /dev/null +++ b/src/Files.App/Services/Search/EverythingSdk3Service.cs @@ -0,0 +1,317 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Files.App.Services.Search +{ + /// + /// Everything SDK3 (v1.5) implementation for improved performance + /// + internal sealed class EverythingSdk3Service : IDisposable + { + #region SDK3 Definitions + + private const uint EVERYTHING3_OK = 0; + private const uint EVERYTHING3_ERROR_OUT_OF_MEMORY = 0xE0000001; + private const uint EVERYTHING3_ERROR_IPC_PIPE_NOT_FOUND = 0xE0000002; + private const uint EVERYTHING3_ERROR_DISCONNECTED = 0xE0000003; + private const uint EVERYTHING3_ERROR_INVALID_PARAMETER = 0xE0000004; + private const uint EVERYTHING3_ERROR_BAD_REQUEST = 0xE0000005; + private const uint EVERYTHING3_ERROR_CANCELLED = 0xE0000006; + private const uint EVERYTHING3_ERROR_PROPERTY_NOT_FOUND = 0xE0000007; + private const uint EVERYTHING3_ERROR_SERVER = 0xE0000008; + private const uint EVERYTHING3_ERROR_INVALID_COMMAND = 0xE0000009; + private const uint EVERYTHING3_ERROR_BAD_RESPONSE = 0xE000000A; + private const uint EVERYTHING3_ERROR_INSUFFICIENT_BUFFER = 0xE000000B; + private const uint EVERYTHING3_ERROR_SHUTDOWN = 0xE000000C; + + // Property IDs + private const uint EVERYTHING3_PROPERTY_SIZE = 0x00000001; + private const uint EVERYTHING3_PROPERTY_DATE_MODIFIED = 0x00000002; + private const uint EVERYTHING3_PROPERTY_DATE_CREATED = 0x00000003; + private const uint EVERYTHING3_PROPERTY_ATTRIBUTES = 0x00000004; + private const uint EVERYTHING3_PROPERTY_PATH = 0x00000005; + private const uint EVERYTHING3_PROPERTY_NAME = 0x00000006; + private const uint EVERYTHING3_PROPERTY_EXTENSION = 0x00000007; + private const uint EVERYTHING3_PROPERTY_TYPE_NAME = 0x00000008; + + // Result types + private const uint EVERYTHING3_RESULT_TYPE_FILE = 1; + private const uint EVERYTHING3_RESULT_TYPE_FOLDER = 2; + + #endregion + + #region P/Invoke Declarations + + [DllImport("Everything3", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything3_ConnectW(string instance_name); + + [DllImport("Everything3")] + private static extern bool Everything3_DestroyClient(IntPtr client); + + [DllImport("Everything3")] + private static extern bool Everything3_ShutdownClient(IntPtr client); + + [DllImport("Everything3", CharSet = CharSet.Unicode)] + private static extern ulong Everything3_GetFolderSizeFromFilenameW(IntPtr client, string filename); + + [DllImport("Everything3", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything3_CreateQuery(IntPtr client, string search_string); + + [DllImport("Everything3")] + private static extern bool Everything3_DestroyQuery(IntPtr query); + + [DllImport("Everything3")] + private static extern bool Everything3_SetMax(IntPtr query, uint max); + + [DllImport("Everything3")] + private static extern bool Everything3_SetOffset(IntPtr query, uint offset); + + [DllImport("Everything3")] + private static extern bool Everything3_SetRequestProperties(IntPtr query, uint property_ids, uint property_count); + + [DllImport("Everything3")] + private static extern bool Everything3_Execute(IntPtr query); + + [DllImport("Everything3")] + private static extern uint Everything3_GetCount(IntPtr query); + + [DllImport("Everything3")] + private static extern uint Everything3_GetResultType(IntPtr query, uint index); + + [DllImport("Everything3", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything3_GetResultPathW(IntPtr query, uint index); + + [DllImport("Everything3", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything3_GetResultNameW(IntPtr query, uint index); + + [DllImport("Everything3")] + private static extern ulong Everything3_GetResultSize(IntPtr query, uint index); + + [DllImport("Everything3")] + private static extern ulong Everything3_GetResultDateModified(IntPtr query, uint index); + + [DllImport("Everything3")] + private static extern ulong Everything3_GetResultDateCreated(IntPtr query, uint index); + + [DllImport("Everything3")] + private static extern uint Everything3_GetResultAttributes(IntPtr query, uint index); + + [DllImport("Everything3")] + private static extern uint Everything3_GetLastError(); + + #endregion + + private IntPtr _client; + private readonly object _lock = new object(); + private bool _disposed; + + public bool IsConnected => _client != IntPtr.Zero; + + public bool Connect() + { + lock (_lock) + { + if (_client != IntPtr.Zero) + return true; + + try + { + // Try to connect to unnamed instance first + _client = Everything3_ConnectW(null); + if (_client != IntPtr.Zero) + { + App.Logger?.LogInformation("[Everything SDK3] Connected to unnamed instance"); + return true; + } + + // Try to connect to 1.5a instance + _client = Everything3_ConnectW("1.5a"); + if (_client != IntPtr.Zero) + { + App.Logger?.LogInformation("[Everything SDK3] Connected to 1.5a instance"); + return true; + } + + App.Logger?.LogWarning("[Everything SDK3] Failed to connect to Everything 1.5"); + return false; + } + catch (DllNotFoundException) + { + App.Logger?.LogInformation("[Everything SDK3] SDK3 DLL not found - Everything 1.5 not installed"); + return false; + } + catch (EntryPointNotFoundException) + { + App.Logger?.LogWarning("[Everything SDK3] SDK3 entry point not found - incompatible DLL"); + return false; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[Everything SDK3] Error connecting to Everything 1.5"); + return false; + } + } + } + + public ulong GetFolderSize(string folderPath) + { + if (!IsConnected || string.IsNullOrEmpty(folderPath)) + return 0; + + lock (_lock) + { + try + { + var size = Everything3_GetFolderSizeFromFilenameW(_client, folderPath); + + // Check for errors (-1 indicates error) + if (size == ulong.MaxValue) + { + var error = Everything3_GetLastError(); + App.Logger?.LogWarning($"[Everything SDK3] GetFolderSize failed for {folderPath}, error: 0x{error:X8}"); + return 0; + } + + App.Logger?.LogInformation($"[Everything SDK3] Got folder size for {folderPath}: {size} bytes"); + return size; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, $"[Everything SDK3] Error getting folder size for {folderPath}"); + return 0; + } + } + } + + public async Task> SearchAsync( + string searchQuery, + uint maxResults = 1000, + CancellationToken cancellationToken = default) + { + if (!IsConnected || string.IsNullOrEmpty(searchQuery)) + return new List<(string, string, bool, ulong, DateTime, DateTime, uint)>(); + + return await Task.Run(() => + { + lock (_lock) + { + IntPtr query = IntPtr.Zero; + var results = new List<(string Path, string Name, bool IsFolder, ulong Size, DateTime DateModified, DateTime DateCreated, uint Attributes)>(); + + try + { + query = Everything3_CreateQuery(_client, searchQuery); + if (query == IntPtr.Zero) + { + App.Logger?.LogWarning("[Everything SDK3] Failed to create query"); + return results; + } + + // Set max results + Everything3_SetMax(query, maxResults); + + // Request properties we need + uint[] properties = { + EVERYTHING3_PROPERTY_PATH, + EVERYTHING3_PROPERTY_NAME, + EVERYTHING3_PROPERTY_SIZE, + EVERYTHING3_PROPERTY_DATE_MODIFIED, + EVERYTHING3_PROPERTY_DATE_CREATED, + EVERYTHING3_PROPERTY_ATTRIBUTES + }; + + GCHandle propertiesHandle = GCHandle.Alloc(properties, GCHandleType.Pinned); + try + { + Everything3_SetRequestProperties(query, (uint)propertiesHandle.AddrOfPinnedObject(), (uint)properties.Length); + } + finally + { + propertiesHandle.Free(); + } + + // Execute query + if (!Everything3_Execute(query)) + { + var error = Everything3_GetLastError(); + App.Logger?.LogWarning($"[Everything SDK3] Query execution failed, error: 0x{error:X8}"); + return results; + } + + var count = Everything3_GetCount(query); + App.Logger?.LogInformation($"[Everything SDK3] Query returned {count} results"); + + for (uint i = 0; i < count; i++) + { + if (cancellationToken.IsCancellationRequested) + break; + + try + { + var type = Everything3_GetResultType(query, i); + var isFolder = type == EVERYTHING3_RESULT_TYPE_FOLDER; + + var path = Marshal.PtrToStringUni(Everything3_GetResultPathW(query, i)) ?? string.Empty; + var name = Marshal.PtrToStringUni(Everything3_GetResultNameW(query, i)) ?? string.Empty; + var size = Everything3_GetResultSize(query, i); + var dateModified = DateTime.FromFileTimeUtc((long)Everything3_GetResultDateModified(query, i)); + var dateCreated = DateTime.FromFileTimeUtc((long)Everything3_GetResultDateCreated(query, i)); + var attributes = Everything3_GetResultAttributes(query, i); + + results.Add((path, name, isFolder, size, dateModified, dateCreated, attributes)); + } + catch (Exception ex) + { + App.Logger?.LogError(ex, $"[Everything SDK3] Error processing result {i}"); + } + } + + return results; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[Everything SDK3] Error during search"); + return results; + } + finally + { + if (query != IntPtr.Zero) + Everything3_DestroyQuery(query); + } + } + }, cancellationToken); + } + + public void Dispose() + { + lock (_lock) + { + if (_disposed) + return; + + if (_client != IntPtr.Zero) + { + try + { + Everything3_ShutdownClient(_client); + Everything3_DestroyClient(_client); + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[Everything SDK3] Error during cleanup"); + } + _client = IntPtr.Zero; + } + + _disposed = true; + } + } + } +} diff --git a/src/Files.App/Services/Search/EverythingSearchService.cs b/src/Files.App/Services/Search/EverythingSearchService.cs new file mode 100644 index 000000000000..b2e411923cef --- /dev/null +++ b/src/Files.App/Services/Search/EverythingSearchService.cs @@ -0,0 +1,601 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Data.Models; +using Files.App.ViewModels; +using System.Runtime.InteropServices; +using System.Text; +using System.IO; +using Windows.Storage; +using Microsoft.Extensions.Logging; + +namespace Files.App.Services.Search +{ + public sealed class EverythingSearchService : IEverythingSearchService + { + // Everything API constants + private const int EVERYTHING_OK = 0; + private const int EVERYTHING_ERROR_IPC = 2; + + private const int EVERYTHING_REQUEST_FILE_NAME = 0x00000001; + private const int EVERYTHING_REQUEST_PATH = 0x00000002; + private const int EVERYTHING_REQUEST_DATE_MODIFIED = 0x00000040; + private const int EVERYTHING_REQUEST_SIZE = 0x00000010; + private const int EVERYTHING_REQUEST_DATE_CREATED = 0x00000020; + private const int EVERYTHING_REQUEST_ATTRIBUTES = 0x00000100; + + // Architecture-aware DLL name + private static readonly string EverythingDllName = GetArchitectureSpecificDllName(); + + private static string GetArchitectureSpecificDllName() + { + // Check for ARM64 first + if (System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64) + { + // Use native ARM64 DLL for better performance + return "EverythingARM64.dll"; + } + + // Standard x86/x64 detection + return Environment.Is64BitProcess ? "Everything64.dll" : "Everything32.dll"; + } + + // Everything API imports - using architecture-aware DLL resolution + [DllImport("Everything", EntryPoint = "Everything_SetSearchW", CharSet = CharSet.Unicode)] + private static extern uint Everything_SetSearchW(string lpSearchString); + + [DllImport("Everything", EntryPoint = "Everything_SetMatchPath")] + private static extern void Everything_SetMatchPath(bool bEnable); + + [DllImport("Everything", EntryPoint = "Everything_SetMatchCase")] + private static extern void Everything_SetMatchCase(bool bEnable); + + [DllImport("Everything", EntryPoint = "Everything_SetMatchWholeWord")] + private static extern void Everything_SetMatchWholeWord(bool bEnable); + + [DllImport("Everything", EntryPoint = "Everything_SetRegex")] + private static extern void Everything_SetRegex(bool bEnable); + + [DllImport("Everything", EntryPoint = "Everything_SetMax")] + private static extern void Everything_SetMax(uint dwMax); + + [DllImport("Everything", EntryPoint = "Everything_SetOffset")] + private static extern void Everything_SetOffset(uint dwOffset); + + [DllImport("Everything", EntryPoint = "Everything_SetRequestFlags")] + private static extern void Everything_SetRequestFlags(uint dwRequestFlags); + + [DllImport("Everything", EntryPoint = "Everything_QueryW")] + private static extern bool Everything_QueryW(bool bWait); + + [DllImport("Everything", EntryPoint = "Everything_GetNumResults")] + private static extern uint Everything_GetNumResults(); + + [DllImport("Everything", EntryPoint = "Everything_GetLastError")] + private static extern uint Everything_GetLastError(); + + [DllImport("Everything", EntryPoint = "Everything_IsFileResult")] + private static extern bool Everything_IsFileResult(uint nIndex); + + [DllImport("Everything", EntryPoint = "Everything_IsFolderResult")] + private static extern bool Everything_IsFolderResult(uint nIndex); + + [DllImport("Everything", EntryPoint = "Everything_GetResultPath", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything_GetResultPath(uint nIndex); + + [DllImport("Everything", EntryPoint = "Everything_GetResultFileName", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything_GetResultFileName(uint nIndex); + + [DllImport("Everything", EntryPoint = "Everything_GetResultDateModified")] + private static extern bool Everything_GetResultDateModified(uint nIndex, out long lpFileTime); + + [DllImport("Everything", EntryPoint = "Everything_GetResultDateCreated")] + private static extern bool Everything_GetResultDateCreated(uint nIndex, out long lpFileTime); + + [DllImport("Everything", EntryPoint = "Everything_GetResultSize")] + private static extern bool Everything_GetResultSize(uint nIndex, out long lpFileSize); + + [DllImport("Everything", EntryPoint = "Everything_GetResultAttributes")] + private static extern uint Everything_GetResultAttributes(uint nIndex); + + [DllImport("Everything", EntryPoint = "Everything_Reset")] + private static extern void Everything_Reset(); + + [DllImport("Everything", EntryPoint = "Everything_SetSort")] + private static extern void Everything_SetSort(uint dwSortType); + + // Note: Everything_CleanUp is intentionally not imported as it can cause access violations + // Everything_Reset() is sufficient for resetting the query state between searches + + [DllImport("Everything", EntryPoint = "Everything_IsDBLoaded")] + private static extern bool Everything_IsDBLoaded(); + + // Win32 API imports for DLL loading + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr LoadLibrary(string lpLibFileName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool FreeLibrary(IntPtr hLibModule); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool SetDllDirectory(string lpPathName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr AddDllDirectory(string newDirectory); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool RemoveDllDirectory(IntPtr cookie); + + private readonly IUserSettingsService _userSettingsService; + private static readonly object _dllSetupLock = new object(); + private static IntPtr _everythingModule = IntPtr.Zero; + private static readonly List _dllDirectoryCookies = new(); + private static bool _dllDirectorySet = false; + private static bool _everythingAvailable = false; + private static bool _availabilityChecked = false; + private static DateTime _lastAvailabilityCheck = default; + + // SDK3 support + private static EverythingSdk3Service _sdk3Service; + private static bool _sdk3Available = false; + private static bool _sdk3Checked = false; + + static EverythingSearchService() + { + // Set up DLL import resolver for architecture-aware loading + NativeLibrary.SetDllImportResolver(typeof(EverythingSearchService).Assembly, DllImportResolver); + } + + public EverythingSearchService(IUserSettingsService userSettingsService) + { + _userSettingsService = userSettingsService; + + // Set up DLL search path if not already done + lock (_dllSetupLock) + { + if (!_dllDirectorySet) + { + SetupDllSearchPath(); + _dllDirectorySet = true; + } + } + } + + private static IntPtr DllImportResolver(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName == "Everything") + { + lock (_dllSetupLock) + { + if (_everythingModule != IntPtr.Zero) + { + return _everythingModule; + } + + + // Try to load Everything DLL from various locations + var appDirectory = AppContext.BaseDirectory; + var possiblePaths = new[] + { + Path.Combine(appDirectory, "Libraries", EverythingDllName), + Path.Combine(appDirectory, EverythingDllName), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Everything", EverythingDllName), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Everything", EverythingDllName), + Path.Combine(@"C:\Program Files\Everything", EverythingDllName), + Path.Combine(@"C:\Program Files (x86)\Everything", EverythingDllName), + EverythingDllName // Try system path + }; + + foreach (var path in possiblePaths) + { + if (File.Exists(path)) + { + try + { + _everythingModule = LoadLibrary(path); + if (_everythingModule != IntPtr.Zero) + { + return _everythingModule; + } + else + { + var error = Marshal.GetLastWin32Error(); + } + } + catch (Exception ex) + { + } + } + else + { + } + } + + // If not found, let the default resolver handle it + return IntPtr.Zero; + } + } + + // Use default resolver for other libraries + return NativeLibrary.Load(libraryName, assembly, searchPath); + } + + private static void SetupDllSearchPath() + { + try + { + // Get the application directory + var appDirectory = AppContext.BaseDirectory; + var searchPaths = new[] + { + appDirectory, + Path.Combine(appDirectory, "Libraries"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Everything"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Everything") + }; + + foreach (var path in searchPaths) + { + if (Directory.Exists(path)) + { + var cookie = AddDllDirectory(path); + if (cookie != IntPtr.Zero) + { + _dllDirectoryCookies.Add(cookie); + } + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error setting up DLL search paths: {ex.Message}"); + } + } + + public static void CleanupDllDirectories() + { + lock (_dllSetupLock) + { + foreach (var cookie in _dllDirectoryCookies) + { + RemoveDllDirectory(cookie); + } + _dllDirectoryCookies.Clear(); + + if (_everythingModule != IntPtr.Zero) + { + FreeLibrary(_everythingModule); + _everythingModule = IntPtr.Zero; + } + } + } + + public bool IsEverythingAvailable() + { + lock (_dllSetupLock) + { + // Re-check availability every 30 seconds to detect if Everything is started/stopped + if (_availabilityChecked && _lastAvailabilityCheck != default && + DateTime.UtcNow - _lastAvailabilityCheck < TimeSpan.FromSeconds(30)) + { + return _everythingAvailable || _sdk3Available; + } + + // Check SDK3 first (Everything 1.5) + // Note: SDK3 DLLs are not included and must be obtained separately from: + // https://github.com/voidtools/everything_sdk3 + if (!_sdk3Checked) + { + try + { + if (_sdk3Service == null) + _sdk3Service = new EverythingSdk3Service(); + + _sdk3Available = _sdk3Service.Connect(); + _sdk3Checked = true; + + if (_sdk3Available) + { + App.Logger?.LogInformation("[Everything] Everything SDK3 (v1.5) is available"); + _lastAvailabilityCheck = DateTime.UtcNow; + return true; + } + } + catch (Exception ex) + { + App.Logger?.LogWarning(ex, "[Everything] SDK3 not available, falling back to SDK2"); + _sdk3Available = false; + _sdk3Checked = true; + } + } + + try + { + // First check if Everything process is running + var everythingProcesses = System.Diagnostics.Process.GetProcessesByName("Everything"); + + if (everythingProcesses.Length == 0) + { + App.Logger?.LogInformation("[Everything] Everything process not found - Everything is not running"); + _everythingAvailable = false; + _availabilityChecked = true; + _lastAvailabilityCheck = DateTime.UtcNow; + return false; + } + + // Try to perform a simple query to test if Everything is accessible + bool queryExecuted = false; + try + { + Everything_Reset(); + Everything_SetSearchW("test"); + Everything_SetMax(1); + + queryExecuted = Everything_QueryW(false); + var lastError = Everything_GetLastError(); + + _everythingAvailable = queryExecuted && lastError == EVERYTHING_OK; + _availabilityChecked = true; + _lastAvailabilityCheck = DateTime.UtcNow; + + if (_everythingAvailable) + { + App.Logger?.LogInformation("[Everything] Everything SDK2 (v1.4) is available and responding"); + } + else + { + App.Logger?.LogWarning($"[Everything] Everything is not available. Query result: {queryExecuted}, Error: {lastError}"); + } + } + finally + { + // Note: Not calling Everything_CleanUp() to avoid access violations + // Everything_Reset() will be called on the next query which handles cleanup + } + + return _everythingAvailable; + } + catch (Exception ex) + { + _everythingAvailable = false; + _availabilityChecked = true; + _lastAvailabilityCheck = DateTime.UtcNow; + return false; + } + } + } + + public async Task> SearchAsync(string query, string searchPath = null, CancellationToken cancellationToken = default) + { + if (!IsEverythingAvailable()) + { + return new List(); + } + + // Try SDK3 first if available + if (_sdk3Available && _sdk3Service != null) + { + try + { + var searchQuery = BuildOptimizedQuery(query, searchPath); + App.Logger?.LogInformation($"[Everything SDK3] Executing search query: '{searchQuery}'"); + + var sdk3Results = await _sdk3Service.SearchAsync(searchQuery, 1000, cancellationToken); + var results = new List(); + + foreach (var (path, name, isFolder, size, dateModified, dateCreated, attributes) in sdk3Results) + { + if (cancellationToken.IsCancellationRequested) + break; + + var fullPath = string.IsNullOrEmpty(path) ? name : Path.Combine(path, name); + + // Skip if it doesn't match our filter criteria + if (!string.IsNullOrEmpty(searchPath) && searchPath != "Home" && + !fullPath.StartsWith(searchPath, StringComparison.OrdinalIgnoreCase)) + continue; + + var isHidden = (attributes & 0x02) != 0; // FILE_ATTRIBUTE_HIDDEN + + // Check user settings for hidden items + if (isHidden && !_userSettingsService.FoldersSettingsService.ShowHiddenItems) + continue; + + // Check for dot files + if (name.StartsWith('.') && !_userSettingsService.FoldersSettingsService.ShowDotFiles) + continue; + + var item = new ListedItem(null) + { + PrimaryItemAttribute = isFolder ? StorageItemTypes.Folder : StorageItemTypes.File, + ItemNameRaw = name, + ItemPath = fullPath, + ItemDateModifiedReal = dateModified, + ItemDateCreatedReal = dateCreated, + IsHiddenItem = isHidden, + LoadFileIcon = false, + FileExtension = isFolder ? null : Path.GetExtension(fullPath), + FileSizeBytes = isFolder ? 0 : (long)size, + FileSize = isFolder ? null : ByteSizeLib.ByteSize.FromBytes(size).ToBinaryString(), + Opacity = isHidden ? Constants.UI.DimItemOpacity : 1 + }; + + if (!isFolder) + { + item.ItemType = item.FileExtension?.Trim('.') + " " + Strings.File.GetLocalizedResource(); + } + + results.Add(item); + } + + App.Logger?.LogInformation($"[Everything SDK3] Search completed with {results.Count} results"); + return results; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[Everything SDK3] Search failed, falling back to SDK2"); + // Fall through to SDK2 + } + } + + // SDK2 fallback + return await Task.Run(() => + { + var results = new List(); + bool queryExecuted = false; + + try + { + Everything_Reset(); + + // Set up the search query + var searchQuery = BuildOptimizedQuery(query, searchPath); + Everything_SetSearchW(searchQuery); + Everything_SetMatchCase(false); + Everything_SetRequestFlags( + EVERYTHING_REQUEST_FILE_NAME | + EVERYTHING_REQUEST_PATH | + EVERYTHING_REQUEST_DATE_MODIFIED | + EVERYTHING_REQUEST_DATE_CREATED | + EVERYTHING_REQUEST_SIZE | + EVERYTHING_REQUEST_ATTRIBUTES); + + // Limit results to prevent overwhelming the UI + Everything_SetMax(1000); + + // Execute the query + App.Logger?.LogInformation($"[Everything SDK2] Executing search query: '{searchQuery}'"); + queryExecuted = Everything_QueryW(true); + if (!queryExecuted) + { + var error = Everything_GetLastError(); + if (error == EVERYTHING_ERROR_IPC) + { + return results; + } + else + { + return results; + } + } + + var numResults = Everything_GetNumResults(); + App.Logger?.LogInformation($"[Everything SDK2] Query returned {numResults} results"); + + for (uint i = 0; i < numResults; i++) + { + if (cancellationToken.IsCancellationRequested) + break; + + try + { + var fileName = Marshal.PtrToStringUni(Everything_GetResultFileName(i)); + var path = Marshal.PtrToStringUni(Everything_GetResultPath(i)); + + if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(path)) + continue; + + var fullPath = Path.Combine(path, fileName); + + // Skip if it doesn't match our filter criteria + if (!string.IsNullOrEmpty(searchPath) && searchPath != "Home" && + !fullPath.StartsWith(searchPath, StringComparison.OrdinalIgnoreCase)) + continue; + + var isFolder = Everything_IsFolderResult(i); + var attributes = Everything_GetResultAttributes(i); + var isHidden = (attributes & 0x02) != 0; // FILE_ATTRIBUTE_HIDDEN + + // Check user settings for hidden items + if (isHidden && !_userSettingsService.FoldersSettingsService.ShowHiddenItems) + continue; + + // Check for dot files + if (fileName.StartsWith('.') && !_userSettingsService.FoldersSettingsService.ShowDotFiles) + continue; + + Everything_GetResultDateModified(i, out long dateModified); + Everything_GetResultDateCreated(i, out long dateCreated); + Everything_GetResultSize(i, out long size); + + var item = new ListedItem(null) + { + PrimaryItemAttribute = isFolder ? StorageItemTypes.Folder : StorageItemTypes.File, + ItemNameRaw = fileName, + ItemPath = fullPath, + ItemDateModifiedReal = DateTime.FromFileTime(dateModified), + ItemDateCreatedReal = DateTime.FromFileTime(dateCreated), + IsHiddenItem = isHidden, + LoadFileIcon = false, + FileExtension = isFolder ? null : Path.GetExtension(fullPath), + FileSizeBytes = isFolder ? 0 : size, + FileSize = isFolder ? null : ByteSizeLib.ByteSize.FromBytes((ulong)size).ToBinaryString(), + Opacity = isHidden ? Constants.UI.DimItemOpacity : 1 + }; + + if (!isFolder) + { + item.ItemType = item.FileExtension?.Trim('.') + " " + Strings.File.GetLocalizedResource(); + } + + results.Add(item); + } + catch (Exception ex) + { + // Skip items that cause errors + System.Diagnostics.Debug.WriteLine($"Error processing Everything result {i}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[Everything SDK2] Search error"); + } + finally + { + // Note: We're not calling Everything_CleanUp() here as it can cause access violations + // The Everything API manages its own memory and calling CleanUp can interfere with + // the API's internal state, especially when multiple queries are executed in sequence + // Everything_Reset() at the start of each query is sufficient for cleanup + } + + return results; + }, cancellationToken); + } + + private string BuildOptimizedQuery(string query, string searchPath) + { + if (string.IsNullOrEmpty(searchPath) || searchPath == "Home") + { + return query; + } + else if (searchPath.Length <= 3) // Root drive like C:\ + { + return $"path:\"{searchPath}\" {query}"; + } + else + { + var escapedPath = searchPath.Replace("\"", "\"\""); + return $"path:\"{escapedPath}\" {query}"; + } + } + + public async Task> FilterItemsAsync(IEnumerable items, string query, CancellationToken cancellationToken = default) + { + // For filtering existing items, we'll use Everything's search on the current directory + var firstItem = items.FirstOrDefault(); + if (firstItem == null) + return new List(); + + // Get the directory path from the first item + var directoryPath = Path.GetDirectoryName(firstItem.ItemPath); + + // Search within this directory + var searchResults = await SearchAsync(query, directoryPath, cancellationToken); + + // Return only items that exist in the original collection + var itemPaths = new HashSet(items.Select(i => i.ItemPath), StringComparer.OrdinalIgnoreCase); + return searchResults.Where(r => itemPaths.Contains(r.ItemPath)).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Files.App/Services/Search/Everything_SDK3_README.md b/src/Files.App/Services/Search/Everything_SDK3_README.md new file mode 100644 index 000000000000..2c2bd10ad268 --- /dev/null +++ b/src/Files.App/Services/Search/Everything_SDK3_README.md @@ -0,0 +1,41 @@ +# Everything SDK3 (v1.5) Implementation + +## Overview +This implementation adds support for Everything SDK3 (v1.5) with automatic fallback to SDK2 (v1.4). + +## Features +- **SDK3 Support**: Uses Everything 1.5's new SDK3 for improved performance +- **Direct Folder Size Query**: SDK3's `Everything3_GetFolderSizeFromFilenameW()` provides instant folder sizes +- **Automatic Fallback**: Falls back to SDK2 if Everything 1.5 is not installed +- **Architecture Support**: Works with x86, x64, and ARM64 + +## Implementation Status +✅ SDK3 service implementation (`EverythingSdk3Service.cs`) +✅ Integration with main Everything service +✅ Folder size calculation optimization +✅ Search functionality with SDK3 +✅ Graceful fallback handling + +## Requirements +- Everything 1.5 alpha or later (for SDK3 features) +- SDK3 DLLs (not included - must be obtained separately) + +## DLL Requirements +The SDK3 implementation requires the following DLLs: +- `Everything3.dll` (x86) +- `Everything3-x64.dll` (x64) +- `Everything3-arm64.dll` (ARM64) + +These DLLs are not included in the Files repository and must be obtained from: +https://github.com/voidtools/everything_sdk3 + +## Usage +The implementation automatically detects and uses SDK3 when available: +1. On startup, it attempts to connect to Everything 1.5 +2. If successful, SDK3 features are used for improved performance +3. If not available, it falls back to SDK2 (Everything 1.4) + +## Performance Benefits +- **Folder Size Calculation**: Near-instant with SDK3 vs enumeration with SDK2 +- **Search Performance**: Improved query handling in SDK3 +- **Memory Usage**: More efficient memory management in SDK3 \ No newline at end of file diff --git a/src/Files.App/Services/Search/IEverythingSearchService.cs b/src/Files.App/Services/Search/IEverythingSearchService.cs new file mode 100644 index 000000000000..9834230d423f --- /dev/null +++ b/src/Files.App/Services/Search/IEverythingSearchService.cs @@ -0,0 +1,12 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Services.Search +{ + public interface IEverythingSearchService + { + bool IsEverythingAvailable(); + Task> SearchAsync(string query, string searchPath = null, CancellationToken cancellationToken = default); + Task> FilterItemsAsync(IEnumerable items, string query, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/Files.App/Services/Settings/GeneralSettingsService.cs b/src/Files.App/Services/Settings/GeneralSettingsService.cs index addc268ce18a..768f02cc50ad 100644 --- a/src/Files.App/Services/Settings/GeneralSettingsService.cs +++ b/src/Files.App/Services/Settings/GeneralSettingsService.cs @@ -369,13 +369,31 @@ public bool ShowShelfPane set => Set(value); } - public bool ShowFilterHeader - { - get => Get(false); - set => Set(value); - } + public bool ShowFilterHeader + { + get => Get(false); + set => Set(value); + } + + public PreferredSearchEngine PreferredSearchEngine + { + get => (PreferredSearchEngine)Get((long)PreferredSearchEngine.Windows); + set => Set((long)value); + } + + public bool UseEverythingForFolderSizes + { + get => Get(false); + set => Set(value); + } + + public int EverythingMaxFolderSizeResults + { + get => Get(1000); + set => Set(value); + } - protected override void RaiseOnSettingChangedEvent(object sender, SettingChangedEventArgs e) + protected override void RaiseOnSettingChangedEvent(object sender, SettingChangedEventArgs e) { base.RaiseOnSettingChangedEvent(sender, e); } diff --git a/src/Files.App/Services/SizeProvider/EverythingSizeProvider.cs b/src/Files.App/Services/SizeProvider/EverythingSizeProvider.cs new file mode 100644 index 000000000000..a521348f399e --- /dev/null +++ b/src/Files.App/Services/SizeProvider/EverythingSizeProvider.cs @@ -0,0 +1,357 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Services.Search; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Files.App.Services.SizeProvider +{ + public sealed class EverythingSizeProvider : ISizeProvider + { + private readonly ConcurrentDictionary sizes = new(); + private readonly IEverythingSearchService everythingService; + private readonly IGeneralSettingsService generalSettings; + private static EverythingSdk3Service _sdk3Service; + private readonly SemaphoreSlim _calculationSemaphore = new(3); // Limit concurrent calculations + + public event EventHandler? SizeChanged; + + // Everything API imports for folder size calculation + [DllImport("Everything", EntryPoint = "Everything_SetSearchW", CharSet = CharSet.Unicode)] + private static extern uint Everything_SetSearchW(string lpSearchString); + + [DllImport("Everything", EntryPoint = "Everything_SetRequestFlags")] + private static extern void Everything_SetRequestFlags(uint dwRequestFlags); + + [DllImport("Everything", EntryPoint = "Everything_SetMax")] + private static extern void Everything_SetMax(uint dwMax); + + [DllImport("Everything", EntryPoint = "Everything_QueryW")] + private static extern bool Everything_QueryW(bool bWait); + + [DllImport("Everything", EntryPoint = "Everything_GetNumResults")] + private static extern uint Everything_GetNumResults(); + + [DllImport("Everything", EntryPoint = "Everything_GetResultSize")] + private static extern bool Everything_GetResultSize(uint nIndex, out long lpFileSize); + + [DllImport("Everything", EntryPoint = "Everything_Reset")] + private static extern void Everything_Reset(); + + [DllImport("Everything", EntryPoint = "Everything_SetSort")] + private static extern void Everything_SetSort(uint dwSortType); + + [DllImport("Everything", EntryPoint = "Everything_IsFileResult")] + private static extern bool Everything_IsFileResult(uint nIndex); + + [DllImport("Everything", EntryPoint = "Everything_GetResultPath", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything_GetResultPath(uint nIndex); + + [DllImport("Everything", EntryPoint = "Everything_GetResultFileName", CharSet = CharSet.Unicode)] + private static extern IntPtr Everything_GetResultFileName(uint nIndex); + + // Note: Everything_CleanUp is intentionally not imported as it can cause access violations + // Everything_Reset() is sufficient for resetting the query state between searches + + private const int EVERYTHING_REQUEST_SIZE = 0x00000010; + + public EverythingSizeProvider(IEverythingSearchService everythingSearchService, IGeneralSettingsService generalSettingsService) + { + everythingService = everythingSearchService; + generalSettings = generalSettingsService; + } + + public Task CleanAsync() => Task.CompletedTask; + + public Task ClearAsync() + { + sizes.Clear(); + return Task.CompletedTask; + } + + public async Task UpdateAsync(string path, CancellationToken cancellationToken) + { + // Return cached size immediately if available + if (sizes.TryGetValue(path, out ulong cachedSize)) + { + RaiseSizeChanged(path, cachedSize, SizeChangedValueState.Final); + return; + } + + // Indicate that calculation is starting + RaiseSizeChanged(path, 0, SizeChangedValueState.None); + + // Run the entire calculation on a background thread to avoid blocking + await Task.Run(async () => + { + // Limit concurrent calculations + await _calculationSemaphore.WaitAsync(cancellationToken); + try + { + // Check if Everything is available + if (!everythingService.IsEverythingAvailable()) + { + await FallbackCalculateAsync(path, cancellationToken); + return; + } + + try + { + // Calculate using Everything + ulong totalSize = await CalculateWithEverythingAsync(path, cancellationToken); + + if (totalSize == 0) + { + // Everything returned 0, use fallback + await FallbackCalculateAsync(path, cancellationToken); + return; + } + sizes[path] = totalSize; + RaiseSizeChanged(path, totalSize, SizeChangedValueState.Final); + } + catch (Exception ex) + { + // Fall back to standard calculation on error + await FallbackCalculateAsync(path, cancellationToken); + } + } + finally + { + _calculationSemaphore.Release(); + } + }, cancellationToken).ConfigureAwait(false); + } + + private async Task CalculateWithEverythingAsync(string path, CancellationToken cancellationToken) + { + // Try SDK3 first if available + if (_sdk3Service == null) + { + try + { + _sdk3Service = new EverythingSdk3Service(); + if (_sdk3Service.Connect()) + { + App.Logger?.LogInformation("[EverythingSizeProvider] Connected to Everything SDK3 for folder size calculation"); + } + else + { + _sdk3Service?.Dispose(); + _sdk3Service = null; + } + } + catch (Exception ex) + { + App.Logger?.LogWarning(ex, "[EverythingSizeProvider] SDK3 not available"); + _sdk3Service = null; + } + } + + if (_sdk3Service != null && _sdk3Service.IsConnected) + { + try + { + // Use a timeout for SDK3 queries to prevent hanging + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(2)); // 2 second timeout + + var size = await Task.Run(() => _sdk3Service.GetFolderSize(path), cts.Token); + if (size > 0) + { + App.Logger?.LogInformation($"[EverythingSizeProvider SDK3] Got folder size for {path}: {size} bytes"); + return size; + } + } + catch (OperationCanceledException) + { + App.Logger?.LogWarning($"[EverythingSizeProvider SDK3] Timeout getting folder size for {path}"); + } + catch (Exception ex) + { + App.Logger?.LogError(ex, $"[EverythingSizeProvider SDK3] Error getting folder size for {path}"); + } + } + + // Fall back to SDK2 + return await Task.Run(() => + { + bool queryExecuted = false; + try + { + // IMPORTANT: For large directories like C:\, this query can return millions of results + // causing Everything to run out of memory. For root drives, fall back to standard calculation. + if (path.Length <= 3 && path.EndsWith(":\\")) + { + return 0UL; // Will trigger fallback calculation + } + + // For large known directories, also skip Everything + var knownLargePaths = new[] { + @"C:\Windows", + @"C:\Program Files", + @"C:\Program Files (x86)", + @"C:\Users", + @"C:\ProgramData", + @"C:\$Recycle.Bin", + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + }; + + if (knownLargePaths.Any(largePath => + string.Equals(path, largePath, StringComparison.OrdinalIgnoreCase) || + string.Equals(Path.GetFullPath(path), Path.GetFullPath(largePath), StringComparison.OrdinalIgnoreCase))) + { + return 0UL; // Will trigger fallback calculation + } + + // Reset Everything state + Everything_Reset(); + + // Use an optimized query that only returns files (not folders) to reduce result count + // The folder: syntax searches recursively within the specified folder + // Adding file: ensures we only get files, not directories + var searchQuery = $"folder:\"{path}\" file:"; + Everything_SetSearchW(searchQuery); + + // Request only size information to optimize performance + Everything_SetRequestFlags(EVERYTHING_REQUEST_SIZE); + + // Sort by size descending to prioritize large files if we hit the limit + Everything_SetSort(13); // EVERYTHING_SORT_SIZE_DESCENDING + + // Use configurable max results limit + var maxResults = (uint)generalSettings.EverythingMaxFolderSizeResults; + Everything_SetMax(maxResults); + + queryExecuted = Everything_QueryW(true); + if (!queryExecuted) + return 0UL; + + var numResults = Everything_GetNumResults(); + + // If we hit the limit, we're still getting the largest files first + // This gives a more accurate estimate even with limited results + if (numResults >= maxResults) + { + App.Logger?.LogInformation($"[EverythingSizeProvider SDK2] Hit result limit ({maxResults}) for {path}, results may be incomplete"); + } + + ulong totalSize = 0; + int validResults = 0; + + for (uint i = 0; i < numResults; i++) + { + if (cancellationToken.IsCancellationRequested) + break; + + if (Everything_GetResultSize(i, out long size) && size > 0) + { + totalSize += (ulong)size; + validResults++; + } + } + + // If we got very few results or hit the limit for a folder that should have more files, + // fall back to standard calculation + if (numResults >= maxResults && validResults < 100) + { + App.Logger?.LogInformation($"[EverythingSizeProvider SDK2] Too few valid results ({validResults}) for {path}, using fallback"); + return 0UL; // Will trigger fallback calculation + } + + App.Logger?.LogInformation($"[EverythingSizeProvider SDK2] Calculated {totalSize} bytes for {path} ({validResults} files)"); + return totalSize; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, $"[EverythingSizeProvider SDK2] Error calculating with Everything for {path}"); + return 0UL; + } + finally + { + // Note: We're not calling Everything_CleanUp() here as it can cause access violations + // The Everything API manages its own memory and calling CleanUp can interfere with + // the API's internal state, especially when multiple queries are executed in sequence + // Everything_Reset() at the start of each query is sufficient for cleanup + } + }, cancellationToken); + } + + private async Task FallbackCalculateAsync(string path, CancellationToken cancellationToken) + { + // Fallback to directory enumeration if Everything is not available + ulong size = await CalculateRecursive(path, cancellationToken); + sizes[path] = size; + RaiseSizeChanged(path, size, SizeChangedValueState.Final); + + async Task CalculateRecursive(string currentPath, CancellationToken ct, int level = 0) + { + if (string.IsNullOrEmpty(currentPath)) + return 0; + + ulong totalSize = 0; + + try + { + var directory = new DirectoryInfo(currentPath); + + // Get files in current directory + foreach (var file in directory.GetFiles()) + { + if (ct.IsCancellationRequested) + break; + + totalSize += (ulong)file.Length; + } + + // Recursively process subdirectories + foreach (var subDirectory in directory.GetDirectories()) + { + if (ct.IsCancellationRequested) + break; + + // Skip symbolic links and junctions + if ((subDirectory.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + continue; + + var subDirSize = await CalculateRecursive(subDirectory.FullName, ct, level + 1); + totalSize += subDirSize; + } + + // Update intermediate results for top-level calculation + // Note: Removed stopwatch tracking for simplicity after logging removal + } + catch (UnauthorizedAccessException) + { + // Skip directories we can't access + } + catch (DirectoryNotFoundException) + { + // Directory was deleted during enumeration + } + + return totalSize; + } + } + + public bool TryGetSize(string path, out ulong size) => sizes.TryGetValue(path, out size); + + public void Dispose() + { + _calculationSemaphore?.Dispose(); + _sdk3Service?.Dispose(); + } + + private void RaiseSizeChanged(string path, ulong newSize, SizeChangedValueState valueState) + => SizeChanged?.Invoke(this, new SizeChangedEventArgs(path, newSize, valueState)); + } +} \ No newline at end of file diff --git a/src/Files.App/Services/SizeProvider/UserSizeProvider.cs b/src/Files.App/Services/SizeProvider/UserSizeProvider.cs index a86413955440..fb5525aa2c8f 100644 --- a/src/Files.App/Services/SizeProvider/UserSizeProvider.cs +++ b/src/Files.App/Services/SizeProvider/UserSizeProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Files.App.Services.SizeProvider; +using Files.App.Services.Search; namespace Files.App.Services { @@ -9,6 +10,10 @@ public sealed partial class UserSizeProvider : ISizeProvider { private readonly IFoldersSettingsService folderPreferences = Ioc.Default.GetRequiredService(); + private readonly IGeneralSettingsService generalSettings + = Ioc.Default.GetRequiredService(); + private readonly IEverythingSearchService everythingSearchService + = Ioc.Default.GetRequiredService(); private ISizeProvider provider; @@ -20,6 +25,7 @@ public UserSizeProvider() provider.SizeChanged += Provider_SizeChanged; folderPreferences.PropertyChanged += FolderPreferences_PropertyChanged; + generalSettings.PropertyChanged += GeneralSettings_PropertyChanged; } public Task CleanAsync() @@ -38,10 +44,29 @@ public void Dispose() { provider.Dispose(); folderPreferences.PropertyChanged -= FolderPreferences_PropertyChanged; + generalSettings.PropertyChanged -= GeneralSettings_PropertyChanged; } private ISizeProvider GetProvider() - => folderPreferences.CalculateFolderSizes ? new DrivesSizeProvider() : new NoSizeProvider(); + { + if (!folderPreferences.CalculateFolderSizes) + return new NoSizeProvider(); + + // Use Everything for folder sizes if it's selected and available + if (generalSettings.PreferredSearchEngine == Data.Enums.PreferredSearchEngine.Everything) + { + if (everythingSearchService.IsEverythingAvailable()) + { + return new EverythingSizeProvider(everythingSearchService, generalSettings); + } + else + { + } + } + + // Fall back to standard provider + return new DrivesSizeProvider(); + } private async void FolderPreferences_PropertyChanged(object sender, PropertyChangedEventArgs e) { @@ -54,6 +79,22 @@ private async void FolderPreferences_PropertyChanged(object sender, PropertyChan } } + private async void GeneralSettings_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(IGeneralSettingsService.PreferredSearchEngine) || + e.PropertyName is nameof(IGeneralSettingsService.EverythingMaxFolderSizeResults)) + { + // Only update if folder size calculation is enabled + if (folderPreferences.CalculateFolderSizes) + { + await provider.ClearAsync(); + provider.SizeChanged -= Provider_SizeChanged; + provider = GetProvider(); + provider.SizeChanged += Provider_SizeChanged; + } + } + } + private void Provider_SizeChanged(object sender, SizeChangedEventArgs e) => SizeChanged?.Invoke(this, e); } diff --git a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs index bd86be9c4d3a..71852a955ecd 100644 --- a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs +++ b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs @@ -78,8 +78,18 @@ Func, Task> intermediateAction folder.FileSizeBytes = (long)size; folder.FileSize = size.ToSizeString(); } - - _ = folderSizeProvider.UpdateAsync(folder.ItemPath, cancellationToken); + else + { + // Fire and forget - calculate size in background without blocking + _ = Task.Run(async () => + { + try + { + await folderSizeProvider.UpdateAsync(folder.ItemPath, cancellationToken); + } + catch { /* Ignore errors in background size calculation */ } + }); + } } } } diff --git a/src/Files.App/Utils/Storage/Search/EverythingSearchEngineService.cs b/src/Files.App/Utils/Storage/Search/EverythingSearchEngineService.cs new file mode 100644 index 000000000000..4a74fae70ed1 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/EverythingSearchEngineService.cs @@ -0,0 +1,136 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Utils; +using Files.App.Helpers.Application; +using Files.App.Services.Search; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Windows.Storage; +using Microsoft.Extensions.Logging; +using FileAttributes = System.IO.FileAttributes; +using WIN32_FIND_DATA = Files.App.Helpers.Win32PInvoke.WIN32_FIND_DATA; + +namespace Files.App.Utils.Storage.Search +{ + public sealed class EverythingSearchEngineService : ISearchEngineService + { + private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService(); + private readonly IEverythingSearchService _everythingService = Ioc.Default.GetRequiredService(); + private const int MaxSuggestionResults = 10; + private const int MaxSearchResults = 1000; + + // State for fallback notification (show only once per session) + private static bool _hasNotifiedEverythingUnavailable = false; + private readonly object _notificationLock = new object(); + + public string Name => "Everything"; + + public bool IsAvailable => _everythingService.IsEverythingAvailable(); + + public async Task> SearchAsync(string query, string? path, CancellationToken ct) + { + App.Logger?.LogInformation("[SearchEngine: Everything] Starting search - Query: '{Query}', Path: '{Path}'", query, path ?? ""); + + if (!IsAvailable) + { + App.Logger?.LogWarning("[SearchEngine: Everything] Everything search unavailable, performing fallback to Windows Search"); + await NotifyEverythingUnavailableOnce(); + return await FallbackToWindowsSearch(query, path, MaxSearchResults, ct); + } + + try + { + var results = await _everythingService.SearchAsync(query, path, ct); + App.Logger?.LogInformation("[SearchEngine: Everything] Search completed - Found {ResultCount} results", results.Count); + return results; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[SearchEngine: Everything] Search failed, falling back to Windows Search"); + return await FallbackToWindowsSearch(query, path, MaxSearchResults, ct); + } + } + + public async Task> SuggestAsync(string query, string? path, CancellationToken ct) + { + App.Logger?.LogInformation("[SearchEngine: Everything] Starting suggestions - Query: '{Query}', Path: '{Path}'", query, path ?? ""); + + if (!IsAvailable) + { + App.Logger?.LogWarning("[SearchEngine: Everything] Everything search unavailable, performing fallback to Windows Search for suggestions"); + await NotifyEverythingUnavailableOnce(); + return await FallbackToWindowsSearch(query, path, MaxSuggestionResults, ct); + } + + try + { + // Use Everything API with limited results for suggestions + var results = await _everythingService.SearchAsync(query, path, ct); + // Limit to suggestion count + var limitedResults = results.Take(MaxSuggestionResults).ToList(); + App.Logger?.LogInformation("[SearchEngine: Everything] Suggestions completed - Found {ResultCount} results", limitedResults.Count); + return limitedResults; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[SearchEngine: Everything] Suggestions failed, falling back to Windows Search"); + return await FallbackToWindowsSearch(query, path, MaxSuggestionResults, ct); + } + } + + /// + /// Notifies user once per session that Everything is unavailable and Windows Search fallback is being used + /// + private async Task NotifyEverythingUnavailableOnce() + { + lock (_notificationLock) + { + if (_hasNotifiedEverythingUnavailable) + return; + + _hasNotifiedEverythingUnavailable = true; + } + + try + { + App.Logger?.LogInformation("[SearchEngine: Everything] Showing fallback notification to user"); + + // Everything is not available - search will use Windows Search instead + } + catch (Exception ex) + { + App.Logger?.LogWarning(ex, "[SearchEngine: Everything] Failed to show fallback notification"); + } + } + + /// + /// Fallback to Windows Search when Everything is unavailable + /// + private async Task> FallbackToWindowsSearch(string query, string? path, int maxResults, CancellationToken ct) + { + try + { + App.Logger?.LogInformation("[SearchEngine: Everything] Falling back to Windows Search"); + + // Use Windows Search service as fallback + var windowsSearchService = Ioc.Default.GetRequiredService(); + var results = maxResults == MaxSuggestionResults + ? await windowsSearchService.SuggestAsync(query, path, ct) + : await windowsSearchService.SearchAsync(query, path, ct); + + App.Logger?.LogInformation("[SearchEngine: Everything] Windows Search fallback completed - Found {ResultCount} results", results.Count); + return results; + } + catch (Exception ex) + { + App.Logger?.LogError(ex, "[SearchEngine: Everything] Windows Search fallback failed"); + return new List(); + } + } + } +} \ No newline at end of file diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index 29042ca0f959..a076a0732455 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -67,29 +67,74 @@ public string AQSQuery } } - public Task SearchAsync(IList results, CancellationToken token) + public async Task SearchAsync(IList results, CancellationToken token) { try { + // Check if we should use Everything for global search + var searchEngine = UserSettingsService.GeneralSettingsService.PreferredSearchEngine; + + if (searchEngine == Files.App.Data.Enums.PreferredSearchEngine.Everything) + { + var everythingService = Ioc.Default.GetService(); + if (everythingService != null && everythingService.IsEverythingAvailable()) + { + try + { + var everythingResults = await everythingService.SearchAsync(Query, Folder, token); + + if (everythingResults != null && everythingResults.Count > 0) + { + // Fix: UsedMaxItemCount can be uint.MaxValue which overflows when cast to int + var itemsToTake = UsedMaxItemCount == uint.MaxValue ? everythingResults.Count : Math.Min(everythingResults.Count, (int)UsedMaxItemCount); + + foreach (var item in everythingResults.Take(itemsToTake)) + { + if (item == null) + continue; + if (token.IsCancellationRequested) + break; + + results.Add(item); + + if (results.Count == 32 || results.Count % 300 == 0) + { + SearchTick?.Invoke(this, EventArgs.Empty); + } + } + SearchTick?.Invoke(this, EventArgs.Empty); + return; + } + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) + { + // Fall through to use default search + } + } + } + + // Fall back to Windows Search if (App.LibraryManager.TryGetLibrary(Folder, out var library)) { - return AddItemsForLibraryAsync(library, results, token); + await AddItemsForLibraryAsync(library, results, token); } else if (Folder == "Home") { - return AddItemsForHomeAsync(results, token); + await AddItemsForHomeAsync(results, token); } else { - return AddItemsAsync(Folder, results, token); + await AddItemsAsync(Folder, results, token); } } catch (Exception e) { - App.Logger.LogWarning(e, "Search failure"); + App.Logger?.LogWarning(e, "Search failure"); } - - return Task.CompletedTask; } private async Task AddItemsForHomeAsync(IList results, CancellationToken token) @@ -128,7 +173,7 @@ public async Task> SearchAsync() } catch (Exception e) { - App.Logger.LogWarning(e, "Search failure"); + App.Logger?.LogWarning(e, "Search failure"); } return results; @@ -160,7 +205,7 @@ private async Task SearchAsync(BaseStorageFolder folder, IList resul } catch (Exception ex) { - App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); + App.Logger?.LogWarning(ex, "Error creating ListedItem from StorageItem"); } if (results.Count == 32 || results.Count % 300 == 0 /*|| sampler.CheckNow()*/) @@ -242,7 +287,7 @@ private async Task SearchTagsAsync(string folder, IList results, Can } catch (Exception ex) { - App.Logger.LogWarning(ex, "Error creating ListedItem from StorageItem"); + App.Logger?.LogWarning(ex, "Error creating ListedItem from StorageItem"); } } diff --git a/src/Files.App/Utils/Storage/Search/ISearchEngineSelector.cs b/src/Files.App/Utils/Storage/Search/ISearchEngineSelector.cs new file mode 100644 index 000000000000..7face2020f83 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/ISearchEngineSelector.cs @@ -0,0 +1,35 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Utils.Storage.Search +{ + /// + /// Service for selecting the appropriate search engine based on user settings + /// + public interface ISearchEngineSelector + { + /// + /// Gets the currently selected search engine service + /// + ISearchEngineService Current { get; } + + /// + /// Gets the currently selected search engine service + /// + /// The active search engine service + ISearchEngineService GetCurrentSearchEngine(); + + /// + /// Gets the search engine service by name + /// + /// The name of the search engine + /// The requested search engine service, or null if not found + ISearchEngineService? GetSearchEngineByName(string name); + + /// + /// Gets all available search engine services + /// + /// Collection of all search engine services + IEnumerable GetAllSearchEngines(); + } +} diff --git a/src/Files.App/Utils/Storage/Search/ISearchEngineService.cs b/src/Files.App/Utils/Storage/Search/ISearchEngineService.cs new file mode 100644 index 000000000000..88035e0ec8dd --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/ISearchEngineService.cs @@ -0,0 +1,15 @@ +using Files.App.Utils; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Files.App.Utils.Storage.Search +{ + public interface ISearchEngineService + { + Task> SearchAsync(string query, string? path, CancellationToken ct); + Task> SuggestAsync(string query, string? path, CancellationToken ct); + string Name { get; } // "Windows Search", "Everything" + bool IsAvailable { get; } + } +} diff --git a/src/Files.App/Utils/Storage/Search/SearchEngineSelector.cs b/src/Files.App/Utils/Storage/Search/SearchEngineSelector.cs new file mode 100644 index 000000000000..4eb156bac7b3 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/SearchEngineSelector.cs @@ -0,0 +1,112 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Data.Contracts; +using Files.App.Data.Enums; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Files.App.Utils.Storage.Search +{ + /// + /// Service for selecting the appropriate search engine based on user settings and availability + /// + public sealed class SearchEngineSelector : ISearchEngineSelector + { + private readonly IServiceProvider _serviceProvider; + private readonly IUserSettingsService _userSettingsService; + private readonly List _searchEngines; + + public SearchEngineSelector(IServiceProvider serviceProvider, IUserSettingsService userSettingsService) + { + _serviceProvider = serviceProvider; + _userSettingsService = userSettingsService; + + // Get all search engine service instances + _searchEngines = new List + { + _serviceProvider.GetRequiredService(), + _serviceProvider.GetRequiredService() + }; + } + + /// + public ISearchEngineService Current => GetCurrentSearchEngine(); + + /// + public ISearchEngineService GetCurrentSearchEngine() + { + try + { + App.Logger.LogDebug("[SearchEngineSelector] Determining current search engine"); + + // Get user's preferred search engine from settings + // For now, we'll use a simple property check (this would need to be added to user settings) + // In a real implementation, you'd have a setting like: + // var preferredEngine = _userSettingsService.GeneralSettingsService.PreferredSearchEngine; + + // Fallback logic: Try to get preferred engine by name, fallback to available engines + var preferredEngineName = GetPreferredSearchEngineName(); + App.Logger.LogDebug("[SearchEngineSelector] Preferred engine: '{PreferredEngine}'", preferredEngineName); + + // First, try to get the preferred engine if it's available + var preferredEngine = _searchEngines.FirstOrDefault(engine => + engine.Name.Equals(preferredEngineName, StringComparison.OrdinalIgnoreCase) && engine.IsAvailable); + + if (preferredEngine != null) + { + App.Logger.LogInformation("[SearchEngineSelector] Using preferred search engine: '{EngineName}'", preferredEngine.Name); + return preferredEngine; + } + + App.Logger.LogWarning("[SearchEngineSelector] Preferred engine '{PreferredEngine}' not available, falling back", preferredEngineName); + + // Fallback to first available engine (Windows Search should always be available) + var fallbackEngine = _searchEngines.FirstOrDefault(engine => engine.IsAvailable); + + if (fallbackEngine != null) + { + App.Logger.LogInformation("[SearchEngineSelector] Using fallback search engine: '{EngineName}'", fallbackEngine.Name); + return fallbackEngine; + } + + // If no engines are available, return Windows Search as final fallback + var finalFallback = _searchEngines.First(engine => engine is WindowsSearchEngineService); + App.Logger.LogWarning("[SearchEngineSelector] No engines available, using final fallback: '{EngineName}'", finalFallback.Name); + return finalFallback; + } + catch (Exception ex) + { + App.Logger.LogError(ex, "[SearchEngineSelector] Error determining current search engine, falling back to Windows Search"); + return _searchEngines.First(engine => engine is WindowsSearchEngineService); + } + } + + /// + public ISearchEngineService? GetSearchEngineByName(string name) + { + return _searchEngines.FirstOrDefault(engine => + engine.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + + /// + public IEnumerable GetAllSearchEngines() + { + return _searchEngines.AsReadOnly(); + } + + /// + /// Gets the preferred search engine name from user settings + /// + private string GetPreferredSearchEngineName() + { + var preferredEngine = _userSettingsService.GeneralSettingsService.PreferredSearchEngine; + return preferredEngine switch + { + PreferredSearchEngine.Everything => "Everything", + PreferredSearchEngine.Windows => "Windows Search", + _ => "Windows Search" + }; + } + } +} diff --git a/src/Files.App/Utils/Storage/Search/WindowsSearchEngineService.cs b/src/Files.App/Utils/Storage/Search/WindowsSearchEngineService.cs new file mode 100644 index 000000000000..769c6bbfb7a2 --- /dev/null +++ b/src/Files.App/Utils/Storage/Search/WindowsSearchEngineService.cs @@ -0,0 +1,90 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Utils; +using Files.App.Utils.Storage; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Files.App.Utils.Storage.Search +{ + public sealed class WindowsSearchEngineService : ISearchEngineService + { + public string Name => "Windows Search"; + + public bool IsAvailable => true; // Windows Search is generally available + + /// + /// Builds an optimized query string for Windows Search with optional path scoping + /// + /// The search query + /// Optional path to scope the search to. Pass null for global search. + /// The query (Windows Search handles path scoping internally via FolderSearch) + public string BuildOptimizedQuery(string query, string? searchPath) + { + // For Windows Search, FolderSearch handles path scoping internally + // so we just return the original query + return query ?? string.Empty; + } + + public async Task> SearchAsync(string query, string? path, CancellationToken ct) + { + App.Logger.LogInformation("[SearchEngine: Windows Search] Starting search - Query: '{Query}', Path: '{Path}'", query, path ?? ""); + + // Handle the path scoping logic to match EverythingSearchEngineService behavior + var searchPath = GetSearchPath(path); + App.Logger.LogDebug("[SearchEngine: Windows Search] Resolved search path: '{SearchPath}'", searchPath ?? ""); + + var folderSearch = new FolderSearch + { + Query = query, + Folder = searchPath + }; + + var results = new List(); + await folderSearch.SearchAsync(results, ct); + + App.Logger.LogInformation("[SearchEngine: Windows Search] Search completed - Found {ResultCount} results", results.Count); + return results; + } + + public async Task> SuggestAsync(string query, string? path, CancellationToken ct) + { + App.Logger.LogInformation("[SearchEngine: Windows Search] Starting suggestions - Query: '{Query}', Path: '{Path}'", query, path ?? ""); + + // Handle the path scoping logic to match EverythingSearchEngineService behavior + var searchPath = GetSearchPath(path); + App.Logger.LogDebug("[SearchEngine: Windows Search] Resolved search path for suggestions: '{SearchPath}'", searchPath ?? ""); + + var folderSearch = new FolderSearch + { + Query = query, + Folder = searchPath, + MaxItemCount = 10 // Limit suggestions to reasonable number + }; + + var results = new List(); + await folderSearch.SearchAsync(results, ct); + + App.Logger.LogInformation("[SearchEngine: Windows Search] Suggestions completed - Found {ResultCount} results", results.Count); + return results; + } + + /// + /// Gets the appropriate search path, handling global search by passing null + /// + /// The requested search path + /// The path to use for FolderSearch, or null for global search + private string? GetSearchPath(string? path) + { + // If path is null or empty, return null for global search + if (string.IsNullOrEmpty(path)) + return null; + + // Return the path as-is - FolderSearch will handle Home, library paths, etc. + return path; + } + } +} diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs index 2d8344bffaf2..371e22c2c013 100644 --- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs @@ -11,6 +11,8 @@ using Windows.Storage; using Windows.Storage.Pickers; using Windows.Win32.Storage.FileSystem; +using Files.App.Utils.Storage.Search; +using System.Runtime.CompilerServices; namespace Files.App.ViewModels.Settings { @@ -27,6 +29,9 @@ public sealed partial class AdvancedViewModel : ObservableObject public ICommand ExportSettingsCommand { get; } public ICommand ImportSettingsCommand { get; } public AsyncRelayCommand OpenFilesOnWindowsStartupCommand { get; } + public ICommand OpenEverythingDownloadCommand { get; } + + public Dictionary SearchEngineTypes { get; private set; } = []; public AdvancedViewModel() @@ -39,6 +44,12 @@ public AdvancedViewModel() ExportSettingsCommand = new AsyncRelayCommand(ExportSettingsAsync); ImportSettingsCommand = new AsyncRelayCommand(ImportSettingsAsync); OpenFilesOnWindowsStartupCommand = new AsyncRelayCommand(OpenFilesOnWindowsStartupAsync); + OpenEverythingDownloadCommand = new RelayCommand(OpenEverythingDownload); + + // Initialize search engine types + SearchEngineTypes.Add(PreferredSearchEngine.Windows, "Windows Search"); + SearchEngineTypes.Add(PreferredSearchEngine.Everything, "Everything"); + SelectedSearchEngineType = SearchEngineTypes[UserSettingsService.GeneralSettingsService.PreferredSearchEngine]; _ = DetectOpenFilesAtStartupAsync(); } @@ -354,6 +365,71 @@ public bool ShowFlattenOptions OnPropertyChanged(); } } + + public PreferredSearchEngine PreferredSearchEngine + { + get => UserSettingsService.GeneralSettingsService.PreferredSearchEngine; + set + { + if (value == UserSettingsService.GeneralSettingsService.PreferredSearchEngine) + return; + + UserSettingsService.GeneralSettingsService.PreferredSearchEngine = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsEverythingSearchSelected)); + } + } + + public bool IsEverythingSearchAvailable + { + get => new EverythingSearchEngineService().IsAvailable; + } + + public bool IsEverythingSearchSelected + { + get => PreferredSearchEngine == PreferredSearchEngine.Everything; + } + + + public int EverythingMaxFolderSizeResults + { + get => UserSettingsService.GeneralSettingsService.EverythingMaxFolderSizeResults; + set + { + if (value != UserSettingsService.GeneralSettingsService.EverythingMaxFolderSizeResults) + { + UserSettingsService.GeneralSettingsService.EverythingMaxFolderSizeResults = value; + OnPropertyChanged(); + } + } + } + + private string selectedSearchEngineType; + public string SelectedSearchEngineType + { + get => selectedSearchEngineType; + set + { + // Check if user is trying to select Everything but it's not available + if (value == "Everything" && !IsEverythingSearchAvailable) + { + // Don't change the selection, show warning + ShowEverythingNotInstalledWarning = true; + // Force the UI to refresh back to current value + OnPropertyChanged(nameof(SelectedSearchEngineType)); + return; + } + + // Hide warning if shown + ShowEverythingNotInstalledWarning = false; + + if (SetProperty(ref selectedSearchEngineType, value)) + { + UserSettingsService.GeneralSettingsService.PreferredSearchEngine = SearchEngineTypes.First(e => e.Value == value).Key; + OnPropertyChanged(nameof(IsEverythingSearchSelected)); + } + } + } public async Task OpenFilesOnWindowsStartupAsync() { var stateMode = await ReadState(); @@ -412,5 +488,34 @@ public async Task ReadState() var state = await StartupTask.GetAsync("3AA55462-A5FA-4933-88C4-712D0B6CDEBB"); return state.State; } + + private void OpenEverythingDownload() + { + var url = "https://www.voidtools.com/"; + Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + } + + private bool showEverythingNotInstalledWarning; + public bool ShowEverythingNotInstalledWarning + { + get => showEverythingNotInstalledWarning; + set => SetProperty(ref showEverythingNotInstalledWarning, value); + } + + public bool ShowEverythingFolderSizeInfo + { + get => IsEverythingSearchAvailable && SelectedSearchEngineType == "Everything" && UserSettingsService.FoldersSettingsService.CalculateFolderSizes; + } + + public bool CanSelectSearchEngine(string searchEngine) + { + if (searchEngine == "Everything") + { + return IsEverythingSearchAvailable; + } + return true; + } + + } } diff --git a/src/Files.App/ViewModels/Settings/FoldersViewModel.cs b/src/Files.App/ViewModels/Settings/FoldersViewModel.cs index bf73b83a098b..9fa278fa2d18 100644 --- a/src/Files.App/ViewModels/Settings/FoldersViewModel.cs +++ b/src/Files.App/ViewModels/Settings/FoldersViewModel.cs @@ -18,6 +18,19 @@ public FoldersViewModel() SizeUnitsOptions.Add(SizeUnitTypes.BinaryUnits, Strings.Binary.GetLocalizedResource()); SizeUnitsOptions.Add(SizeUnitTypes.DecimalUnits, Strings.Decimal.GetLocalizedResource()); SizeUnitFormat = SizeUnitsOptions[UserSettingsService.FoldersSettingsService.SizeUnitFormat]; + + // Listen for search engine changes to update folder size info + UserSettingsService.GeneralSettingsService.PropertyChanged += GeneralSettingsService_PropertyChanged; + } + + private void GeneralSettingsService_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(IGeneralSettingsService.PreferredSearchEngine)) + { + OnPropertyChanged(nameof(IsEverythingEnabled)); + OnPropertyChanged(nameof(FolderSizeWarningMessage)); + OnPropertyChanged(nameof(FolderSizeInfoMessage)); + } } // Properties @@ -146,7 +159,38 @@ public bool CalculateFolderSizes UserSettingsService.FoldersSettingsService.CalculateFolderSizes = value; OnPropertyChanged(); + OnPropertyChanged(nameof(FolderSizeWarningMessage)); + OnPropertyChanged(nameof(FolderSizeInfoMessage)); + } + } + } + + public bool IsEverythingEnabled + { + get => UserSettingsService.GeneralSettingsService.PreferredSearchEngine == Data.Enums.PreferredSearchEngine.Everything; + } + + public string FolderSizeWarningMessage + { + get + { + if (IsEverythingEnabled) + { + return ""; // No warning when Everything is enabled + } + return Strings.ShowFolderSizesWarning.GetLocalizedResource(); + } + } + + public string FolderSizeInfoMessage + { + get + { + if (IsEverythingEnabled) + { + return "Everything search is enabled. Folder sizes will be calculated using Everything's fast indexing."; } + return ""; } } diff --git a/src/Files.App/Views/Settings/AdvancedPage.xaml b/src/Files.App/Views/Settings/AdvancedPage.xaml index 4a001c502182..48efdb102bc1 100644 --- a/src/Files.App/Views/Settings/AdvancedPage.xaml +++ b/src/Files.App/Views/Settings/AdvancedPage.xaml @@ -10,6 +10,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:Files.App.ViewModels.Settings" xmlns:wctcontrols="using:CommunityToolkit.WinUI.Controls" + xmlns:uc="using:Files.App.UserControls" mc:Ignorable="d"> @@ -168,6 +169,49 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Files.App/Views/Settings/FoldersPage.xaml b/src/Files.App/Views/Settings/FoldersPage.xaml index 83033b9c4305..7362168e9d11 100644 --- a/src/Files.App/Views/Settings/FoldersPage.xaml +++ b/src/Files.App/Views/Settings/FoldersPage.xaml @@ -9,8 +9,14 @@ xmlns:uc="using:Files.App.UserControls" xmlns:vm="using:Files.App.ViewModels.Settings" xmlns:wctcontrols="using:CommunityToolkit.WinUI.Controls" + xmlns:wctconverters="using:CommunityToolkit.WinUI.Converters" mc:Ignorable="d"> + + + + + @@ -185,13 +191,27 @@ - + + + + + + +