/
githubmirror
/
Files
Обзор
Документация
Войти
/
githubmirror
/
Files
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Files.App/ViewModels/ShellViewModel.cs
3 225 строк
104 KB
yair100
Code Quality: Performance optimizations (#18801)
06 авг 2026, 17:33
Не верифицирован
06 авг 2026, 17:33
7dc84dd
Код
Авторство
О чём код?
// Copyright (c) Files Community // Licensed under the MIT License. using Files.App.Services.SizeProvider; using Files.Shared.Helpers; using LibGit2Sharp; using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using System.Collections.Concurrent; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Vanara.Windows.Shell; using Windows.Foundation; using Windows.Storage; using Windows.Storage.FileProperties; using Windows.Storage.Search; using Windows.Win32; using Windows.Win32.System.SystemServices; using static Files.App.Helpers.Win32PInvoke; using ByteSize = ByteSizeLib.ByteSize; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; using FileAttributes = System.IO.FileAttributes; namespace Files.App.ViewModels { /// <summary> /// Represents view model of <see cref="IShellPage"/>. /// </summary> public sealed partial class ShellViewModel : ObservableObject, IDisposable { private readonly SemaphoreSlim enumFolderSemaphore; private readonly SemaphoreSlim getFileOrFolderSemaphore; private readonly SemaphoreSlim bulkOperationSemaphore; private readonly SemaphoreSlim loadThumbnailSemaphore; private readonly ConcurrentDictionary<string, CancellationTokenSource> thumbnailRetryDebounce; private readonly ConcurrentQueue<(uint Action, string FileName)> operationQueue; private readonly ConcurrentQueue<uint> gitChangesQueue; private readonly ConcurrentDictionary<string, bool> itemLoadQueue; private readonly AsyncManualResetEvent operationEvent; private readonly AsyncManualResetEvent gitChangedEvent; private readonly DispatcherQueue dispatcherQueue; private readonly JsonElement defaultJson = JsonSerializer.SerializeToElement("{}"); private readonly string folderTypeTextLocalized = Strings.Folder.GetLocalizedResource(); private Task? aProcessQueueAction; private Task? gitProcessQueueAction; private volatile Task? desktopIniUpdateTask; // Files and folders list for manipulating private ConcurrentCollection<ListedItem> filesAndFolders; private readonly IWindowsIniService WindowsIniService = Ioc.Default.GetRequiredService<IWindowsIniService>(); private readonly IWindowsJumpListService jumpListService = Ioc.Default.GetRequiredService<IWindowsJumpListService>(); private readonly IDialogService dialogService = Ioc.Default.GetRequiredService<IDialogService>(); private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService<IUserSettingsService>(); private readonly INetworkService NetworkService = Ioc.Default.GetRequiredService<INetworkService>(); private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService<IFileTagsSettingsService>(); private readonly ISizeProvider folderSizeProvider = Ioc.Default.GetRequiredService<ISizeProvider>(); private readonly IStorageCacheService fileListCache = Ioc.Default.GetRequiredService<IStorageCacheService>(); private readonly IWindowsSecurityService WindowsSecurityService = Ioc.Default.GetRequiredService<IWindowsSecurityService>(); private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService<IStorageTrashBinService>(); private readonly IContentPageContext ContentPageContext = Ioc.Default.GetRequiredService<IContentPageContext>(); // Only used for Binding and ApplyFilesAndFoldersChangesAsync, don't manipulate on this! public BulkConcurrentObservableCollection<ListedItem> FilesAndFolders { get; } private LayoutPreferencesManager folderSettings = null; private ListedItem? currentFolder; public ListedItem? CurrentFolder { get => currentFolder; private set => SetProperty(ref currentFolder, value); } private string? _SolutionFilePath; public string? SolutionFilePath { get => _SolutionFilePath; private set => SetProperty(ref _SolutionFilePath, value); } private ImageSource? _FolderBackgroundImageSource; public ImageSource? FolderBackgroundImageSource { get => _FolderBackgroundImageSource; private set => SetProperty(ref _FolderBackgroundImageSource, value); } private float _FolderBackgroundImageOpacity = 1f; public float FolderBackgroundImageOpacity { get => _FolderBackgroundImageOpacity; private set => SetProperty(ref _FolderBackgroundImageOpacity, value); } private Stretch _FolderBackgroundImageFit = Stretch.UniformToFill; public Stretch FolderBackgroundImageFit { get => _FolderBackgroundImageFit; private set => SetProperty(ref _FolderBackgroundImageFit, value); } private VerticalAlignment _FolderBackgroundImageVerticalAlignment = VerticalAlignment.Center; public VerticalAlignment FolderBackgroundImageVerticalAlignment { get => _FolderBackgroundImageVerticalAlignment; private set => SetProperty(ref _FolderBackgroundImageVerticalAlignment, value); } private HorizontalAlignment _FolderBackgroundImageHorizontalAlignment = HorizontalAlignment.Center; public HorizontalAlignment FolderBackgroundImageHorizontalAlignment { get => _FolderBackgroundImageHorizontalAlignment; private set => SetProperty(ref _FolderBackgroundImageHorizontalAlignment, value); } private ImageSource? _FolderThumbnailImageSource; public ImageSource? FolderThumbnailImageSource { get => _FolderThumbnailImageSource; private set => SetProperty(ref _FolderThumbnailImageSource, value); } private BitmapImage? _SearchIconBitmapImage; public BitmapImage? SearchIconBitmapImage { get => _SearchIconBitmapImage; private set => SetProperty(ref _SearchIconBitmapImage, value); } public bool ShowFilterHeader => UserSettingsService.GeneralSettingsService.ShowFilterHeader && WorkingDirectory != "Home" && WorkingDirectory != "ReleaseNotes" && WorkingDirectory != "Settings"; private GitProperties _EnabledGitProperties; public GitProperties EnabledGitProperties { get => _EnabledGitProperties; set { if (SetProperty(ref _EnabledGitProperties, value) && value is not GitProperties.None) { filesAndFolders.ToList().ForEach(async item => { if (item is IGitItem gitItem && (!gitItem.StatusPropertiesInitialized && value is GitProperties.All or GitProperties.Status || !gitItem.CommitPropertiesInitialized && value is GitProperties.All or GitProperties.Commit)) { await LoadGitPropertiesAsync(gitItem); } }); } } } public CollectionViewSource viewSource; private FileSystemWatcher watcher; private static BitmapImage shieldIcon; private CancellationTokenSource addFilesCTS; private CancellationTokenSource semaphoreCTS; private CancellationTokenSource loadPropsCTS; private CancellationTokenSource watcherCTS; private CancellationTokenSource searchCTS; private CancellationTokenSource updateTagGroupCTS; private CancellationTokenSource? filterDebounceCS; private CancellationTokenSource? networkAvailabilityCTS; public event EventHandler FocusFilterHeader; public event EventHandler DirectoryInfoUpdated; public event EventHandler GitDirectoryUpdated; public event EventHandler<List<ListedItem>> OnSelectionRequestedEvent; public string WorkingDirectory { get; private set; } public string? GitDirectory { get; private set; } public bool IsValidGitDirectory { get; private set; } public List<IniSectionDataItem> DesktopIni { get; private set; } private StorageFolderWithPath? currentStorageFolder; private StorageFolderWithPath workingRoot; public delegate void WorkingDirectoryModifiedEventHandler(object sender, WorkingDirectoryModifiedEventArgs e); public event WorkingDirectoryModifiedEventHandler WorkingDirectoryModified; public delegate void PageTypeUpdatedEventHandler(object sender, PageTypeUpdatedEventArgs e); public event PageTypeUpdatedEventHandler PageTypeUpdated; public delegate void ItemLoadStatusChangedEventHandler(object sender, ItemLoadStatusChangedEventArgs e); public event ItemLoadStatusChangedEventHandler ItemLoadStatusChanged; public void InvokeFocusFilterHeader() { FocusFilterHeader?.Invoke(this, EventArgs.Empty); } public async Task SetWorkingDirectoryAsync(string? value) { if (string.IsNullOrWhiteSpace(value)) return; var isLibrary = false; string? name = null; if (App.LibraryManager.TryGetLibrary(value, out LibraryLocationItem library)) { isLibrary = true; name = library.Text; } WorkingDirectoryModified?.Invoke(this, new WorkingDirectoryModifiedEventArgs { Path = value, IsLibrary = isLibrary, Name = name }); if (isLibrary || !Path.IsPathRooted(value)) workingRoot = currentStorageFolder = null; else if (!Path.IsPathRooted(WorkingDirectory) || Path.GetPathRoot(WorkingDirectory) != Path.GetPathRoot(value)) workingRoot = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(value)); if (value == "Home" || value == "ReleaseNotes" || value == "Settings") currentStorageFolder = null; else _ = Task.Run(() => jumpListService.AddFolderAsync(value)); WorkingDirectory = value; string? pathRoot = null; if (!FtpHelpers.IsFtpPath(WorkingDirectory)) { pathRoot = Path.GetPathRoot(WorkingDirectory); } GitDirectory = GitHelpers.GetGitRepositoryPath(WorkingDirectory, pathRoot); IsValidGitDirectory = !string.IsNullOrEmpty((await GitHelpers.GetRepositoryHead(GitDirectory))?.Name); _ = UpdateFolderThumbnailImageSource(); OnPropertyChanged(nameof(WorkingDirectory)); OnPropertyChanged(nameof(ShowFilterHeader)); } public async Task<FilesystemResult<BaseStorageFolder>> GetFolderFromPathAsync(string value, CancellationToken cancellationToken = default) { await getFileOrFolderSemaphore.WaitAsync(cancellationToken); try { return await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(value, workingRoot, currentStorageFolder)); } finally { getFileOrFolderSemaphore.Release(); } } public async Task<FilesystemResult<BaseStorageFile>> GetFileFromPathAsync(string value, CancellationToken cancellationToken = default) { await getFileOrFolderSemaphore.WaitAsync(cancellationToken); try { return await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(value, workingRoot, currentStorageFolder)); } finally { getFileOrFolderSemaphore.Release(); } } public async Task<FilesystemResult<StorageFolderWithPath>> GetFolderWithPathFromPathAsync(string value, CancellationToken cancellationToken = default) { await getFileOrFolderSemaphore.WaitAsync(cancellationToken); try { return await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(value, workingRoot, currentStorageFolder)); } finally { getFileOrFolderSemaphore.Release(); } } public async Task<FilesystemResult<StorageFileWithPath>> GetFileWithPathFromPathAsync(string value, CancellationToken cancellationToken = default) { await getFileOrFolderSemaphore.WaitAsync(cancellationToken); try { return await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileWithPathFromPathAsync(value, workingRoot, currentStorageFolder)); } finally { getFileOrFolderSemaphore.Release(); } } private EmptyTextType emptyTextType; public EmptyTextType EmptyTextType { get => emptyTextType; set => SetProperty(ref emptyTextType, value); } private bool isNetworkDiscoveryInfoBarOpen; public bool IsNetworkDiscoveryInfoBarOpen { get => isNetworkDiscoveryInfoBarOpen; set => SetProperty(ref isNetworkDiscoveryInfoBarOpen, value); } private bool isLocationUnavailable; public bool IsLocationUnavailable { get => isLocationUnavailable; set { if (SetProperty(ref isLocationUnavailable, value)) UpdateEmptyTextType(); } } private string? locationUnavailableGlyph; public string? LocationUnavailableGlyph { get => locationUnavailableGlyph; set => SetProperty(ref locationUnavailableGlyph, value); } private string? locationUnavailableTitle; public string? LocationUnavailableTitle { get => locationUnavailableTitle; set => SetProperty(ref locationUnavailableTitle, value); } private string? locationUnavailableMessage; public string? LocationUnavailableMessage { get => locationUnavailableMessage; set => SetProperty(ref locationUnavailableMessage, value); } private enum LocationUnavailableKind { AccessDenied, NotFound, DriveUnplugged, PasswordRequired, } private void ShowLocationUnavailable(LocationUnavailableKind kind, string? message = null) { (LocationUnavailableGlyph, LocationUnavailableTitle, LocationUnavailableMessage) = kind switch { LocationUnavailableKind.AccessDenied => ("\uE72E", Strings.AccessDenied.GetLocalizedResource(), Strings.AccessDeniedToFolder.GetLocalizedResource()), LocationUnavailableKind.NotFound => ("\uE838", Strings.FolderNotFoundDialogTitle.GetLocalizedResource(), Strings.FolderNotFoundDialogText.GetLocalizedResource()), LocationUnavailableKind.PasswordRequired => ("\uE8D7", Strings.PasswordRequired.GetLocalizedResource(), Strings.PasswordRequiredMessage.GetLocalizedResource()), _ => ("\uE7BA", Strings.DriveUnpluggedDialogTitle.GetLocalizedResource(), message ?? Strings.DriveUnpluggedDialogText.GetLocalizedResource()), }; IsLocationUnavailable = true; } private void ShowLocationInaccessibleOrMissing(string path) { // A folder pending deletion fails enumeration with ERROR_ACCESS_DENIED; // Directory.Exists is false for it but true for folders that deny listing if (Directory.Exists(path)) { ShowLocationUnavailable(LocationUnavailableKind.AccessDenied); } else { ShowLocationUnavailable(LocationUnavailableKind.NotFound); WatchForLocationRestoration(path); } } private FileSystemWatcher? locationRestorationWatcher; private void WatchForLocationRestoration(string path) { StopWatchingForLocationRestoration(); var trimmedPath = path.TrimEnd(Path.DirectorySeparatorChar); var parentPath = Path.GetDirectoryName(trimmedPath); var folderName = Path.GetFileName(trimmedPath); if (string.IsNullOrEmpty(parentPath) || string.IsNullOrEmpty(folderName) || !Directory.Exists(parentPath)) return; try { var restorationWatcher = new FileSystemWatcher(parentPath, folderName) { NotifyFilter = NotifyFilters.DirectoryName }; restorationWatcher.Created += LocationRestorationWatcher_Restored; restorationWatcher.Renamed += LocationRestorationWatcher_Restored; locationRestorationWatcher = restorationWatcher; restorationWatcher.EnableRaisingEvents = true; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // Parent folder was removed or is inaccessible StopWatchingForLocationRestoration(); return; } // The folder may have been restored before the watcher was armed if (Directory.Exists(path)) LocationRestorationWatcher_Restored(this, new FileSystemEventArgs(WatcherChangeTypes.Created, parentPath, folderName)); } private void StopWatchingForLocationRestoration() { if (Interlocked.Exchange(ref locationRestorationWatcher, null) is FileSystemWatcher restorationWatcher) restorationWatcher.Dispose(); } private async void LocationRestorationWatcher_Restored(object sender, FileSystemEventArgs e) { if (Interlocked.Exchange(ref locationRestorationWatcher, null) is not FileSystemWatcher restorationWatcher) return; restorationWatcher.Dispose(); await dispatcherQueue.EnqueueOrInvokeAsync(() => RefreshItems(null)); } private NetworkAvailability networkAvailability = NetworkAvailability.All; public NetworkAvailability NetworkAvailability { get => networkAvailability; private set { if (SetProperty(ref networkAvailability, value)) OnPropertyChanged(nameof(NetworkDiscoveryInfoBarMessage)); } } public string NetworkDiscoveryInfoBarMessage { get { return NetworkAvailability switch { NetworkAvailability.None => Strings.NetworkDiscoveryAndFileSharingTurnedOffInfoBarMessage.GetLocalizedResource(), NetworkAvailability.Discovery => Strings.FileSharingTurnedOffInfoBarMessage.GetLocalizedResource(), NetworkAvailability.Sharing => Strings.NetworkDiscoveryTurnedOffInfoBarMessage.GetLocalizedResource(), _ => string.Empty, }; } } public async Task UpdateFolderThumbnailImageSource() { FolderThumbnailImageSource = await NavigationHelpers.GetIconForPathAsync(WorkingDirectory); } public async Task UpdateSortOptionStatusAsync() { OnPropertyChanged(nameof(IsSortedByName)); OnPropertyChanged(nameof(IsSortedByDate)); OnPropertyChanged(nameof(IsSortedByType)); OnPropertyChanged(nameof(IsSortedBySize)); OnPropertyChanged(nameof(IsSortedByPath)); OnPropertyChanged(nameof(IsSortedByOriginalPath)); OnPropertyChanged(nameof(IsSortedByDateDeleted)); OnPropertyChanged(nameof(IsSortedByDateCreated)); OnPropertyChanged(nameof(IsSortedBySyncStatus)); OnPropertyChanged(nameof(IsSortedByFileTag)); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } public async Task UpdateSortDirectionStatusAsync() { OnPropertyChanged(nameof(IsSortedAscending)); OnPropertyChanged(nameof(IsSortedDescending)); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } public async Task UpdateSortDirectoriesAlongsideFilesAsync() { OnPropertyChanged(nameof(AreDirectoriesSortedAlongsideFiles)); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } public async Task UpdateSortFilesFirstAsync() { OnPropertyChanged(nameof(AreFilesSortedFirst)); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } private void UpdateSortAndGroupOptions() { OnPropertyChanged(nameof(IsSortedByName)); OnPropertyChanged(nameof(IsSortedByDate)); OnPropertyChanged(nameof(IsSortedByType)); OnPropertyChanged(nameof(IsSortedBySize)); OnPropertyChanged(nameof(IsSortedByPath)); OnPropertyChanged(nameof(IsSortedByOriginalPath)); OnPropertyChanged(nameof(IsSortedByDateDeleted)); OnPropertyChanged(nameof(IsSortedByDateCreated)); OnPropertyChanged(nameof(IsSortedBySyncStatus)); OnPropertyChanged(nameof(IsSortedByFileTag)); OnPropertyChanged(nameof(IsSortedAscending)); OnPropertyChanged(nameof(IsSortedDescending)); OnPropertyChanged(nameof(AreDirectoriesSortedAlongsideFiles)); OnPropertyChanged(nameof(AreFilesSortedFirst)); } public bool IsSortedByName { get => folderSettings.DirectorySortOption == SortOption.Name; set { if (value) { folderSettings.DirectorySortOption = SortOption.Name; OnPropertyChanged(nameof(IsSortedByName)); } } } public bool IsSortedByPath { get => folderSettings.DirectorySortOption == SortOption.Path; set { if (value) { folderSettings.DirectorySortOption = SortOption.Path; OnPropertyChanged(nameof(IsSortedByPath)); } } } public bool IsSortedByOriginalPath { get => folderSettings.DirectorySortOption == SortOption.OriginalFolder; set { if (value) { folderSettings.DirectorySortOption = SortOption.OriginalFolder; OnPropertyChanged(nameof(IsSortedByOriginalPath)); } } } public bool IsSortedByDateDeleted { get => folderSettings.DirectorySortOption == SortOption.DateDeleted; set { if (value) { folderSettings.DirectorySortOption = SortOption.DateDeleted; OnPropertyChanged(nameof(IsSortedByDateDeleted)); } } } public bool IsSortedByDate { get => folderSettings.DirectorySortOption == SortOption.DateModified; set { if (value) { folderSettings.DirectorySortOption = SortOption.DateModified; OnPropertyChanged(nameof(IsSortedByDate)); } } } public bool IsSortedByDateCreated { get => folderSettings.DirectorySortOption == SortOption.DateCreated; set { if (value) { folderSettings.DirectorySortOption = SortOption.DateCreated; OnPropertyChanged(nameof(IsSortedByDateCreated)); } } } public bool IsSortedByType { get => folderSettings.DirectorySortOption == SortOption.FileType; set { if (value) { folderSettings.DirectorySortOption = SortOption.FileType; OnPropertyChanged(nameof(IsSortedByType)); } } } public bool IsSortedBySize { get => folderSettings.DirectorySortOption == SortOption.Size; set { if (value) { folderSettings.DirectorySortOption = SortOption.Size; OnPropertyChanged(nameof(IsSortedBySize)); } } } public bool IsSortedBySyncStatus { get => folderSettings.DirectorySortOption == SortOption.SyncStatus; set { if (value) { folderSettings.DirectorySortOption = SortOption.SyncStatus; OnPropertyChanged(nameof(IsSortedBySyncStatus)); } } } public bool IsSortedByFileTag { get => folderSettings.DirectorySortOption == SortOption.FileTag; set { if (value) { folderSettings.DirectorySortOption = SortOption.FileTag; OnPropertyChanged(nameof(IsSortedByFileTag)); } } } public bool IsSortedAscending { get => folderSettings.DirectorySortDirection == SortDirection.Ascending; set { folderSettings.DirectorySortDirection = value ? SortDirection.Ascending : SortDirection.Descending; OnPropertyChanged(nameof(IsSortedAscending)); OnPropertyChanged(nameof(IsSortedDescending)); } } public bool IsSortedDescending { get => !IsSortedAscending; set { folderSettings.DirectorySortDirection = value ? SortDirection.Descending : SortDirection.Ascending; OnPropertyChanged(nameof(IsSortedAscending)); OnPropertyChanged(nameof(IsSortedDescending)); } } public bool AreDirectoriesSortedAlongsideFiles { get => folderSettings.SortDirectoriesAlongsideFiles; set { folderSettings.SortDirectoriesAlongsideFiles = value; OnPropertyChanged(nameof(AreDirectoriesSortedAlongsideFiles)); } } public bool AreFilesSortedFirst { get => folderSettings.SortFilesFirst; set { folderSettings.SortFilesFirst = value; OnPropertyChanged(nameof(AreFilesSortedFirst)); } } public bool HasNoWatcher { get; private set; } public ShellViewModel(LayoutPreferencesManager folderSettingsViewModel) { folderSettings = folderSettingsViewModel; filesAndFolders = []; FilesAndFolders = []; operationQueue = new ConcurrentQueue<(uint Action, string FileName)>(); gitChangesQueue = new ConcurrentQueue<uint>(); itemLoadQueue = new ConcurrentDictionary<string, bool>(); thumbnailRetryDebounce = new ConcurrentDictionary<string, CancellationTokenSource>(); addFilesCTS = new CancellationTokenSource(); semaphoreCTS = new CancellationTokenSource(); loadPropsCTS = new CancellationTokenSource(); watcherCTS = new CancellationTokenSource(); operationEvent = new AsyncManualResetEvent(); gitChangedEvent = new AsyncManualResetEvent(); enumFolderSemaphore = new SemaphoreSlim(1, 1); getFileOrFolderSemaphore = new SemaphoreSlim(50); bulkOperationSemaphore = new SemaphoreSlim(1, 1); loadThumbnailSemaphore = new SemaphoreSlim(1, 1); dispatcherQueue = DispatcherQueue.GetForCurrentThread(); UserSettingsService.OnSettingChangedEvent += UserSettingsService_OnSettingChangedEvent; fileTagsSettingsService.OnSettingImportedEvent += FileTagsSettingsService_OnSettingUpdated; fileTagsSettingsService.OnTagsUpdated += FileTagsSettingsService_OnSettingUpdated; folderSizeProvider.SizeChanged += FolderSizeProvider_SizeChanged; folderSettings.LayoutModeChangeRequested += LayoutModeChangeRequested; StorageTrashBinService.Watcher.ItemAdded += RecycleBinItemCreatedAsync; StorageTrashBinService.Watcher.ItemDeleted += RecycleBinItemDeletedAsync; StorageTrashBinService.Watcher.RefreshRequested += RecycleBinRefreshRequestedAsync; } private async void LayoutModeChangeRequested(object? sender, LayoutModeEventArgs e) { await dispatcherQueue.EnqueueOrInvokeAsync(CheckForBackgroundImage, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } private async void RecycleBinRefreshRequestedAsync(object sender, FileSystemEventArgs e) { if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; await dispatcherQueue.EnqueueOrInvokeAsync(() => { RefreshItems(null); }); } private async void RecycleBinItemDeletedAsync(object sender, FileSystemEventArgs e) { if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; var removedItem = await RemoveFileOrFolderAsync(e.FullPath); if (removedItem is not null) await ApplyFilesAndFoldersChangesAsync(); } private async void RecycleBinItemCreatedAsync(object sender, FileSystemEventArgs e) { if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(e.FullPath)); if (folderItem is null) return; var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem); var newListedItem = await AddFileOrFolderFromShellFile(shellFileItem); if (newListedItem is null) return; await AddFileOrFolderAsync(newListedItem); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } private async void FolderSizeProvider_SizeChanged(object? sender, Services.SizeProvider.SizeChangedEventArgs e) { try { await enumFolderSemaphore.WaitAsync(semaphoreCTS.Token); } catch (OperationCanceledException) { return; } try { var matchingItem = filesAndFolders.ToList().FirstOrDefault(x => x.ItemPath == e.Path); if (matchingItem is not null && (e.ValueState is not SizeChangedValueState.Intermediate || (long)e.NewSize > matchingItem.FileSizeBytes)) { await dispatcherQueue.EnqueueOrInvokeAsync(() => { if (e.ValueState is SizeChangedValueState.None) { matchingItem.FileSizeBytes = 0; matchingItem.IsCalculatingSize = true; matchingItem.ShowCalculatingText = true; } else if (e.ValueState is SizeChangedValueState.Final || (long)e.NewSize > matchingItem.FileSizeBytes) { matchingItem.FileSizeBytes = (long)e.NewSize; matchingItem.FileSize = e.NewSize.ToSizeString(); if (e.ValueState is SizeChangedValueState.Final) { matchingItem.ShowCalculatingText = false; matchingItem.IsCalculatingSize = false; } } DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } } finally { enumFolderSemaphore.Release(); } } private async void FileTagsSettingsService_OnSettingUpdated(object? sender, EventArgs e) { await dispatcherQueue.EnqueueOrInvokeAsync(() => { if (WorkingDirectory != "Home" && WorkingDirectory != "ReleaseNotes" && WorkingDirectory != "Settings") RefreshItems(null); }); } private async void UserSettingsService_OnSettingChangedEvent(object? sender, SettingChangedEventArgs e) { switch (e.SettingName) { case nameof(UserSettingsService.FoldersSettingsService.ShowFileExtensions): case nameof(UserSettingsService.FoldersSettingsService.ShowThumbnails): case nameof(UserSettingsService.FoldersSettingsService.ShowHiddenItems): case nameof(UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles): case nameof(UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible): case nameof(UserSettingsService.FoldersSettingsService.ShowDotFiles): case nameof(UserSettingsService.FoldersSettingsService.CalculateFolderSizes): case nameof(UserSettingsService.FoldersSettingsService.SelectFilesOnHover): case nameof(UserSettingsService.FoldersSettingsService.ShowCheckboxesWhenSelectingItems): case nameof(UserSettingsService.FoldersSettingsService.SizeUnitFormat): await dispatcherQueue.EnqueueOrInvokeAsync(() => { if (WorkingDirectory != "Home" && WorkingDirectory != "ReleaseNotes" && WorkingDirectory != "Settings") RefreshItems(null); }); break; case nameof(UserSettingsService.LayoutSettingsService.DefaultSortOption): case nameof(UserSettingsService.LayoutSettingsService.DefaultGroupOption): case nameof(UserSettingsService.LayoutSettingsService.DefaultSortDirectoriesAlongsideFiles): case nameof(UserSettingsService.LayoutSettingsService.DefaultSortFilesFirst): case nameof(UserSettingsService.LayoutSettingsService.SyncFolderPreferencesAcrossDirectories): case nameof(UserSettingsService.LayoutSettingsService.DefaultGroupByDateUnit): case nameof(UserSettingsService.LayoutSettingsService.DefaultLayoutMode): await dispatcherQueue.EnqueueOrInvokeAsync(() => { folderSettings.OnDefaultPreferencesChanged(WorkingDirectory, e.SettingName); UpdateSortAndGroupOptions(); }); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); break; case nameof(UserSettingsService.GeneralSettingsService.ShowFilterHeader): OnPropertyChanged(nameof(ShowFilterHeader)); break; } } private bool IsLoadingCancelled { get; set; } public void CancelLoadAndClearFiles() { Debug.WriteLine("CancelLoadAndClearFiles"); CloseWatcher(); CancelNetworkAvailabilityUpdate(); IsNetworkDiscoveryInfoBarOpen = false; if (IsLoadingItems) { IsLoadingCancelled = true; addFilesCTS.Cancel(); addFilesCTS = new CancellationTokenSource(); } CancelExtendedPropertiesLoading(); foreach (var cts in thumbnailRetryDebounce.Values) { cts.Cancel(); cts.Dispose(); } thumbnailRetryDebounce.Clear(); filesAndFolders.Clear(); FilesAndFolders.Clear(); CancelSearch(); } public void CancelExtendedPropertiesLoading() { loadPropsCTS.Cancel(); loadPropsCTS = new CancellationTokenSource(); } public void CancelExtendedPropertiesLoadingForItem(ListedItem item) { itemLoadQueue.TryUpdate(item.ItemPath, true, false); } private bool _isSearchResults; public bool IsSearchResults { get => _isSearchResults; set { if (SetProperty(ref _isSearchResults, value)) { if (!value) SearchHeaderTitle = string.Empty; } } } private string? _searchHeaderTitle; public string? SearchHeaderTitle { get => _searchHeaderTitle; set => SetProperty(ref _searchHeaderTitle, value); } public void UpdateEmptyTextType() { var isFolderEmpty = FilesAndFolders.Count == 0 && !IsLocationUnavailable; EmptyTextType = isFolderEmpty ? (IsSearchResults ? EmptyTextType.NoSearchResultsFound : EmptyTextType.FolderEmpty) : EmptyTextType.None; } public void UpdateNetworkAvailabilityInfoBar() { var shouldCheckNetworkAvailability = !IsSearchResults && IsNetworkFolder(WorkingDirectory); if (shouldCheckNetworkAvailability) QueueNetworkAvailabilityUpdate(WorkingDirectory); else { CancelNetworkAvailabilityUpdate(); IsNetworkDiscoveryInfoBarOpen = false; } } private void QueueNetworkAvailabilityUpdate(string workingDirectory) { CancelNetworkAvailabilityUpdate(); networkAvailabilityCTS = new CancellationTokenSource(); IsNetworkDiscoveryInfoBarOpen = false; _ = UpdateNetworkAvailabilityInfoBarAsync(workingDirectory, networkAvailabilityCTS.Token); } private void CancelNetworkAvailabilityUpdate() { networkAvailabilityCTS?.Cancel(); networkAvailabilityCTS?.Dispose(); networkAvailabilityCTS = null; } private async Task UpdateNetworkAvailabilityInfoBarAsync(string workingDirectory, CancellationToken cancellationToken) { var availability = await NetworkService.GetNetworkAvailabilityAsync(); if (cancellationToken.IsCancellationRequested) return; await dispatcherQueue.EnqueueOrInvokeAsync(() => { if (cancellationToken.IsCancellationRequested || IsSearchResults || !string.Equals(WorkingDirectory, workingDirectory, StringComparison.OrdinalIgnoreCase)) { return; } if (availability is null or NetworkAvailability.All) { IsNetworkDiscoveryInfoBarOpen = false; return; } NetworkAvailability = availability.Value; IsNetworkDiscoveryInfoBarOpen = true; }); } private static bool IsNetworkFolder(string path) { return string.Equals(path, Constants.UserEnvironmentPaths.NetworkFolderPath, StringComparison.OrdinalIgnoreCase); } private string? _filesAndFoldersFilter; public string? FilesAndFoldersFilter { get => _filesAndFoldersFilter; set { if (SetProperty(ref _filesAndFoldersFilter, value)) { FilesAndFolderFilterUpdated(); } } } private void FilesAndFolderFilterUpdated() { if (filterDebounceCS is not null) { filterDebounceCS.Cancel(); filterDebounceCS.Dispose(); } filterDebounceCS = new CancellationTokenSource(); var token = filterDebounceCS.Token; _ = Task.Delay(250, token) .ContinueWith(_ => ApplyFilesAndFoldersChangesAsync(), token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default) .Unwrap(); } // Apply changes immediately after manipulating on filesAndFolders completed public async Task ApplyFilesAndFoldersChangesAsync() { try { if (filesAndFolders is null || filesAndFolders.Count == 0) { void ClearDisplay() { FilesAndFolders.Clear(); UpdateEmptyTextType(); UpdateNetworkAvailabilityInfoBar(); DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); } if (dispatcherQueue.HasThreadAccess) ClearDisplay(); else await dispatcherQueue.EnqueueOrInvokeAsync(ClearDisplay); return; } var filesAndFoldersLocal = filesAndFolders.ToList(); // CollectionChanged will cause UI update, which may cause significant performance degradation, // so suppress CollectionChanged event here while loading items heavily. // Note that both DataGrid and GridView don't support multi-items changes notification, so here // we have to call BeginBulkOperation to suppress CollectionChanged and call EndBulkOperation // in the end to fire a CollectionChanged event with NotifyCollectionChangedAction.Reset await bulkOperationSemaphore.WaitAsync(addFilesCTS.Token); var isSemaphoreReleased = false; try { await dispatcherQueue.EnqueueOrInvokeAsync(() => { try { FilesAndFolders.BeginBulkOperation(); if (addFilesCTS.IsCancellationRequested) return; FilesAndFolders.Clear(); if (string.IsNullOrEmpty(FilesAndFoldersFilter)) FilesAndFolders.AddRange(filesAndFoldersLocal); else FilesAndFolders.AddRange(filesAndFoldersLocal.Where(x => x.Name.Contains(FilesAndFoldersFilter, StringComparison.OrdinalIgnoreCase))); if (folderSettings.DirectoryGroupOption != GroupOption.None) OrderGroups(); // Trigger CollectionChanged with NotifyCollectionChangedAction.Reset // once loading is completed so that UI can be updated FilesAndFolders.EndBulkOperation(); UpdateEmptyTextType(); UpdateNetworkAvailabilityInfoBar(); DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); } finally { isSemaphoreReleased = true; bulkOperationSemaphore.Release(); } }); // The semaphore will be released in UI thread isSemaphoreReleased = true; } finally { if (!isSemaphoreReleased) bulkOperationSemaphore.Release(); } } catch (Exception ex) { App.Logger.LogWarning(ex, ex.Message); } } private Task RequestSelectionAsync(List<ListedItem> itemsToSelect) { // Don't notify if shell page is not the active pane (eg. Dual Pane) // https://github.com/files-community/Files/issues/17427 if (WorkingDirectory != ContentPageContext.ShellPage!.ShellViewModel.WorkingDirectory) return Task.CompletedTask; // Don't notify if there weren't listed items if (itemsToSelect is null || itemsToSelect.IsEmpty()) return Task.CompletedTask; return dispatcherQueue.EnqueueOrInvokeAsync(() => { OnSelectionRequestedEvent?.Invoke(this, itemsToSelect); }); } private Task OrderFilesAndFoldersAsync() { void OrderEntries() { if (filesAndFolders.Count == 0) return; filesAndFolders = new ConcurrentCollection<ListedItem>(SortingHelper.OrderFileList(filesAndFolders.ToList(), folderSettings.DirectorySortOption, folderSettings.DirectorySortDirection, folderSettings.SortDirectoriesAlongsideFiles, folderSettings.SortFilesFirst)); } if (dispatcherQueue.HasThreadAccess) return Task.Run(OrderEntries); OrderEntries(); return Task.CompletedTask; } private void OrderGroups(CancellationToken token = default) { var gps = FilesAndFolders.GroupedCollection?.Where(x => !x.IsSorted); if (gps is null) return; foreach (var gp in gps) { if (token.IsCancellationRequested) return; gp.Order(list => SortingHelper.OrderFileList(list, folderSettings.DirectorySortOption, folderSettings.DirectorySortDirection, folderSettings.SortDirectoriesAlongsideFiles, folderSettings.SortFilesFirst)); } if (FilesAndFolders.GroupedCollection is null || FilesAndFolders.GroupedCollection.IsSorted) return; if (folderSettings.DirectoryGroupDirection == SortDirection.Ascending) { if (folderSettings.DirectoryGroupOption == GroupOption.Size) // Always show file sections below folders FilesAndFolders.GroupedCollection.Order(x => x.OrderBy(y => y.First().PrimaryItemAttribute != StorageItemTypes.Folder || y.First().IsArchive) .ThenBy(y => y.Model.SortIndexOverride).ThenBy(y => y.Model.Text)); else FilesAndFolders.GroupedCollection.Order(x => x.OrderBy(y => y.Model.SortIndexOverride).ThenBy(y => y.Model.Text)); } else { if (folderSettings.DirectoryGroupOption == GroupOption.Size) // Always show file sections below folders FilesAndFolders.GroupedCollection.Order(x => x.OrderBy(y => y.First().PrimaryItemAttribute != StorageItemTypes.Folder || y.First().IsArchive) .ThenByDescending(y => y.Model.SortIndexOverride).ThenByDescending(y => y.Model.Text)); else FilesAndFolders.GroupedCollection.Order(x => x.OrderByDescending(y => y.Model.SortIndexOverride).ThenByDescending(y => y.Model.Text)); } FilesAndFolders.GroupedCollection.IsSorted = true; } public async Task GroupOptionsUpdatedAsync(CancellationToken token) { try { // Conflicts will occur if re-grouping is run while items are still being enumerated, // so wait for enumeration to complete first await enumFolderSemaphore.WaitAsync(token); } catch (OperationCanceledException) { return; } try { await bulkOperationSemaphore.WaitAsync(token); var isSemaphoreReleased = false; try { await dispatcherQueue.EnqueueOrInvokeAsync(() => { try { FilesAndFolders.BeginBulkOperation(); UpdateGroupOptions(); if (FilesAndFolders.IsGrouped) { FilesAndFolders.ResetGroups(token); if (token.IsCancellationRequested) return; OrderGroups(); } if (token.IsCancellationRequested) return; FilesAndFolders.EndBulkOperation(); } finally { isSemaphoreReleased = true; bulkOperationSemaphore.Release(); } }); // The semaphore will be released in UI thread isSemaphoreReleased = true; } finally { if (!isSemaphoreReleased) bulkOperationSemaphore.Release(); } } catch (Exception ex) { App.Logger.LogWarning(ex, ex.Message); } finally { enumFolderSemaphore.Release(); } } public Task ReloadItemGroupHeaderImagesAsync() { // This is needed to update the group icons for file type groups if (folderSettings.DirectoryGroupOption != GroupOption.FileType || FilesAndFolders.GroupedCollection is null) return Task.CompletedTask; return Task.Run(async () => { foreach (var gp in FilesAndFolders.GroupedCollection.ToList()) { var img = await GetItemTypeGroupIcon(gp.FirstOrDefault()); await dispatcherQueue.EnqueueOrInvokeAsync(() => { gp.Model.ImageSource = img; }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } }); } public void UpdateGroupOptions() { FilesAndFolders.ItemGroupKeySelector = GroupingHelper.GetItemGroupKeySelector(folderSettings.DirectoryGroupOption, folderSettings.DirectoryGroupByDateUnit); var groupInfoSelector = GroupingHelper.GetGroupInfoSelector(folderSettings.DirectoryGroupOption, folderSettings.DirectoryGroupByDateUnit); FilesAndFolders.GetGroupHeaderInfo = groupInfoSelector.Item1; FilesAndFolders.GetExtendedGroupHeaderInfo = groupInfoSelector.Item2; } private bool isLoadingItems = false; public bool IsLoadingItems { get => isLoadingItems; set => isLoadingItems = value; } private async Task<BitmapImage> GetShieldIcon() { shieldIcon ??= await UIHelpers.GetShieldIconResource(); return shieldIcon; } private async Task LoadThumbnailAsync(ListedItem item, CancellationToken cancellationToken, bool scheduleTimerRetry = true) { var loadNonCachedThumbnail = false; var thumbnailSize = LayoutSizeKindHelper.GetIconSize(folderSettings.LayoutMode); var returnIconOnly = UserSettingsService.FoldersSettingsService.ShowThumbnails == false || thumbnailSize < 48; // TODO Remove this property when all the layouts can support different icon sizes var useCurrentScale = folderSettings.LayoutMode == FolderLayoutModes.DetailsView || folderSettings.LayoutMode == FolderLayoutModes.ListView || folderSettings.LayoutMode == FolderLayoutModes.ColumnView || folderSettings.LayoutMode == FolderLayoutModes.CardsView; byte[]? result = null; // Non-cached thumbnails take longer to generate if (item.IsFolder || !FileExtensionHelpers.IsExecutableFile(item.FileExtension)) { if (!returnIconOnly) { // Get cached thumbnail result = await FileThumbnailHelper.GetIconAsync( item.ItemPath, thumbnailSize, item.IsFolder, IconOptions.ReturnThumbnailOnly | IconOptions.ReturnOnlyIfCached | (useCurrentScale ? IconOptions.UseCurrentScale : IconOptions.None)); cancellationToken.ThrowIfCancellationRequested(); loadNonCachedThumbnail = true; } if (result is null) { // Get icon result = await FileThumbnailHelper.GetIconAsync( item.ItemPath, thumbnailSize, item.IsFolder, IconOptions.ReturnIconOnly | (useCurrentScale ? IconOptions.UseCurrentScale : IconOptions.None)); cancellationToken.ThrowIfCancellationRequested(); } } else { // Get icon or thumbnail result = await FileThumbnailHelper.GetIconAsync( item.ItemPath, thumbnailSize, item.IsFolder, (returnIconOnly ? IconOptions.ReturnIconOnly : IconOptions.None) | (useCurrentScale ? IconOptions.UseCurrentScale : IconOptions.None)); cancellationToken.ThrowIfCancellationRequested(); } if (result is not null) { await dispatcherQueue.EnqueueOrInvokeAsync(async () => { // Assign FileImage property var image = await result.ToBitmapAsync(); if (image is not null) item.FileImage = image; }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); cancellationToken.ThrowIfCancellationRequested(); } // Get icon overlay var iconOverlay = await FileThumbnailHelper.GetIconOverlayAsync(item.ItemPath, true); cancellationToken.ThrowIfCancellationRequested(); if (iconOverlay is not null) { await dispatcherQueue.EnqueueOrInvokeAsync(async () => { item.IconOverlay = await iconOverlay.ToBitmapAsync(); item.ShieldIcon = await GetShieldIcon(); }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); cancellationToken.ThrowIfCancellationRequested(); } if (loadNonCachedThumbnail) { // Get non-cached thumbnail asynchronously _ = Task.Run(async () => { await loadThumbnailSemaphore.WaitAsync(cancellationToken); try { result = await FileThumbnailHelper.GetIconAsync( item.ItemPath, thumbnailSize, item.IsFolder, IconOptions.ReturnThumbnailOnly | (useCurrentScale ? IconOptions.UseCurrentScale : IconOptions.None)); } finally { loadThumbnailSemaphore.Release(); } cancellationToken.ThrowIfCancellationRequested(); if (result is null) { item.NeedsDelayedThumbnailLoad = true; // Some writers never emit a FILE_ACTION_MODIFIED event after finalizing the file, so the normal event-driven retry never fires. // Schedule a 2s timer as a fallback; the FILE_ACTION_MODIFIED handler cancels it if the event arrives first. if (scheduleTimerRetry) { var retryCts = new CancellationTokenSource(); if (thumbnailRetryDebounce.TryAdd(item.ItemPath, retryCts)) { App.Logger.LogWarning("Thumbnail load failed [{Id}] '{Extension}'; scheduling 2s timer retry.", item.ItemPath.GetHashCode(), Path.GetExtension(item.ItemPath)); var retryToken = retryCts.Token; _ = Task.Delay(2000, retryToken) .ContinueWith(_ => { if (thumbnailRetryDebounce.TryRemove(item.ItemPath, out var cts)) cts.Dispose(); App.Logger.LogInformation("Timer-based thumbnail retry firing [{Id}] '{Extension}'.", item.ItemPath.GetHashCode(), Path.GetExtension(item.ItemPath)); item.NeedsDelayedThumbnailLoad = false; return LoadThumbnailAsync(item, retryToken, scheduleTimerRetry: false); }, retryToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default) .Unwrap(); } else { App.Logger.LogWarning("Thumbnail load failed [{Id}] '{Extension}'; mod-retry already pending, skipping timer.", item.ItemPath.GetHashCode(), Path.GetExtension(item.ItemPath)); retryCts.Dispose(); } } else { App.Logger.LogWarning("Thumbnail load failed [{Id}] '{Extension}' on timer retry; awaiting next FILE_ACTION_MODIFIED.", item.ItemPath.GetHashCode(), Path.GetExtension(item.ItemPath)); } } else { await dispatcherQueue.EnqueueOrInvokeAsync(async () => { // Assign FileImage property var image = await result.ToBitmapAsync(); if (image is not null) item.FileImage = image; }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); } }, cancellationToken); } } private static void SetFileTag(ListedItem item) { var dbInstance = FileTagsHelper.GetDbInstance(); dbInstance.SetTags(item.ItemPath, item.FileFRN, item.FileTags ?? []); } // This works for recycle bin as well as GetFileFromPathAsync/GetFolderFromPathAsync work // for file inside the recycle bin (but not on the recycle bin folder itself) public async Task LoadExtendedItemPropertiesAsync(ListedItem item) { if (item is null) return; itemLoadQueue[item.ItemPath] = false; var cts = loadPropsCTS; try { cts.Token.ThrowIfCancellationRequested(); if (itemLoadQueue.TryGetValue(item.ItemPath, out var canceled) && canceled) return; item.ItemPropertiesInitialized = true; var wasSyncStatusLoaded = false; var loadGroupHeaderInfo = false; ImageSource? groupImage = null; GroupedCollection<ListedItem>? gp = null; try { var isFileTypeGroupMode = folderSettings.DirectoryGroupOption == GroupOption.FileType; BaseStorageFile? matchingStorageFile = null; if (item.Key is not null && FilesAndFolders.IsGrouped && FilesAndFolders.GetExtendedGroupHeaderInfo is not null) { gp = FilesAndFolders.GroupedCollection?.ToList().FirstOrDefault(x => x.Model.Key == item.Key); loadGroupHeaderInfo = gp is not null && !gp.Model.Initialized && gp.GetExtendedGroupHeaderInfo is not null; } cts.Token.ThrowIfCancellationRequested(); await LoadThumbnailAsync(item, cts.Token); cts.Token.ThrowIfCancellationRequested(); if (item.IsLibrary || item.PrimaryItemAttribute == StorageItemTypes.File || item.IsArchive) { if (!item.IsShortcut && !FtpHelpers.IsFtpPath(item.ItemPath)) { matchingStorageFile = await GetFileFromPathAsync(item.ItemPath, cts.Token); if (matchingStorageFile is not null) { cts.Token.ThrowIfCancellationRequested(); var syncStatus = await CheckCloudDriveSyncStatusAsync(matchingStorageFile); var fileFRN = await FileTagsHelper.GetFileFRN(matchingStorageFile); var fileTag = FileTagsHelper.ReadFileTag(item.ItemPath); var itemType = (item.ItemType == Strings.Folder.GetLocalizedResource()) ? item.ItemType : matchingStorageFile.DisplayType; var extraProperties = await GetExtraProperties(matchingStorageFile); cts.Token.ThrowIfCancellationRequested(); await dispatcherQueue.EnqueueOrInvokeAsync(() => { item.FolderRelativeId = matchingStorageFile.FolderRelativeId; item.ItemType = itemType; item.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); item.FileFRN = fileFRN; item.FileTags = fileTag; item.IsElevationRequired = CheckElevationRights(item); item.ImageDimensions = extraProperties?.Result["System.Image.Dimensions"]?.ToString() ?? string.Empty; item.FileVersion = extraProperties?.Result["System.FileVersion"]?.ToString() ?? string.Empty; item.MediaDuration = ulong.TryParse(extraProperties?.Result["System.Media.Duration"]?.ToString(), out ulong duration) ? TimeSpan.FromTicks((long)duration).ToString(@"hh\:mm\:ss") : string.Empty; switch (true) { case var _ when !string.IsNullOrEmpty(item.ImageDimensions): item.ContextualProperty = $"{Strings.PropertyDimensions.GetLocalizedResource()}: {item.ImageDimensions}"; break; case var _ when !string.IsNullOrEmpty(item.MediaDuration): item.ContextualProperty = $"{Strings.PropertyDuration.GetLocalizedResource()}: {item.MediaDuration}"; break; case var _ when !string.IsNullOrEmpty(item.FileVersion): item.ContextualProperty = $"{Strings.PropertyVersion.GetLocalizedResource()}: {item.FileVersion}"; break; default: item.ContextualProperty = $"{Strings.Modified.GetLocalizedResource()}: {item.ItemDateModified}"; break; } }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); SetFileTag(item); wasSyncStatusLoaded = true; } } } else { if (!item.IsShortcut && !item.IsHiddenItem && !FtpHelpers.IsFtpPath(item.ItemPath)) { BaseStorageFolder matchingStorageFolder = await GetFolderFromPathAsync(item.ItemPath, cts.Token); if (matchingStorageFolder is not null) { if (matchingStorageFolder.DisplayName != item.Name && !matchingStorageFolder.DisplayName.StartsWith("$R", StringComparison.Ordinal)) { cts.Token.ThrowIfCancellationRequested(); await dispatcherQueue.EnqueueOrInvokeAsync(() => { item.ItemNameRaw = matchingStorageFolder.DisplayName; }); await fileListCache.AddDisplayName(item.ItemPath, matchingStorageFolder.DisplayName); if (folderSettings.DirectorySortOption == SortOption.Name && !isLoadingItems) { await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } } cts.Token.ThrowIfCancellationRequested(); var syncStatus = await CheckCloudDriveSyncStatusAsync(matchingStorageFolder); var fileFRN = await FileTagsHelper.GetFileFRN(matchingStorageFolder); var fileTag = FileTagsHelper.ReadFileTag(item.ItemPath); var itemType = (item.ItemType == Strings.Folder.GetLocalizedResource()) ? item.ItemType : matchingStorageFolder.DisplayType; var extraProperties = await GetExtraProperties(matchingStorageFolder); cts.Token.ThrowIfCancellationRequested(); await dispatcherQueue.EnqueueOrInvokeAsync(() => { item.FolderRelativeId = matchingStorageFolder.FolderRelativeId; item.ItemType = itemType; item.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); item.FileFRN = fileFRN; item.FileTags = fileTag; if (extraProperties is not null) { // Drive Storage Details if (extraProperties.Result["System.SFGAOFlags"] is uint attributesRaw && extraProperties.Result["System.Capacity"] is ulong capacityRaw && extraProperties.Result["System.FreeSpace"] is ulong freeSpaceRaw && ((SFGAO_FLAGS)attributesRaw).HasFlag(SFGAO_FLAGS.SFGAO_REMOVABLE) && !((SFGAO_FLAGS)attributesRaw).HasFlag(SFGAO_FLAGS.SFGAO_FILESYSTEM)) { var maxSpace = ByteSize.FromBytes(capacityRaw); var freeSpace = ByteSize.FromBytes(freeSpaceRaw); item.MaxSpace = maxSpace; item.SpaceUsed = maxSpace - freeSpace; item.FileSize = string.Format(Strings.DriveFreeSpaceAndCapacity.GetLocalizedResource(), freeSpace.ToSizeString(), maxSpace.ToSizeString()); item.ShowDriveStorageDetails = true; } } else { item.ContextualProperty = $"{Strings.Modified.GetLocalizedResource()}: {item.ItemDateModified}"; } }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); SetFileTag(item); wasSyncStatusLoaded = true; } } } if (loadGroupHeaderInfo && isFileTypeGroupMode) groupImage = await GetItemTypeGroupIcon(item, matchingStorageFile); } catch (Exception) { } finally { if (!wasSyncStatusLoaded) { cts.Token.ThrowIfCancellationRequested(); await FilesystemTasks.Wrap(async () => { var fileTag = FileTagsHelper.ReadFileTag(item.ItemPath); await dispatcherQueue.EnqueueOrInvokeAsync(() => { // Reset cloud sync status icon item.SyncStatusUI = new CloudDriveSyncStatusUI(); item.FileTags = fileTag; }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); SetFileTag(item); }); } else { // Try loading thumbnail for cloud files in case they weren't cached the first time if (item.SyncStatusUI.SyncStatus != CloudDriveSyncStatus.NotSynced && item.SyncStatusUI.SyncStatus != CloudDriveSyncStatus.Unknown) { _ = Task.Run(async () => { await Task.Delay(500); cts.Token.ThrowIfCancellationRequested(); await LoadThumbnailAsync(item, cts.Token); }); } } if (loadGroupHeaderInfo) { cts.Token.ThrowIfCancellationRequested(); await SafetyExtensions.IgnoreExceptions(() => dispatcherQueue.EnqueueOrInvokeAsync(() => { gp.Model.ImageSource = groupImage; gp.InitializeExtendedGroupHeaderInfoAsync(); })); } } } catch (OperationCanceledException) { // Ignored } finally { itemLoadQueue.TryRemove(item.ItemPath, out _); await RefreshTagGroups(); } } public async Task RefreshTagGroups() { if (FilesAndFolders.IsGrouped && folderSettings.DirectoryGroupOption is GroupOption.FileTag && itemLoadQueue.IsEmpty()) { updateTagGroupCTS?.Cancel(); updateTagGroupCTS = new(); await GroupOptionsUpdatedAsync(updateTagGroupCTS.Token); } } public Task UpdateItemsTags(Dictionary<string, string[]> newTags) { return dispatcherQueue.EnqueueOrInvokeAsync(() => { int count = newTags.Count; foreach (var item in FilesAndFolders) { if (newTags.TryGetValue(item.ItemPath, out var tags)) { item.FileTags = tags; if (--count == 0) break; } } }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } private bool CheckElevationRights(ListedItem item) { if (item.SyncStatusUI.LoadSyncStatus) return false; return WindowsSecurityService.IsElevationRequired(item.IsShortcut ? ((IShortcutItem)item).TargetPath : item.ItemPath); } public async Task LoadGitPropertiesAsync(IGitItem gitItem) { var getStatus = EnabledGitProperties is GitProperties.All or GitProperties.Status && !gitItem.StatusPropertiesInitialized; var getCommit = EnabledGitProperties is GitProperties.All or GitProperties.Commit && !gitItem.CommitPropertiesInitialized; if (!getStatus && !getCommit) return; var cts = loadPropsCTS; try { await Task.Run(async () => { if (GitHelpers.IsRepositoryEx(gitItem.ItemPath, out var repoPath) && !string.IsNullOrEmpty(repoPath)) { cts.Token.ThrowIfCancellationRequested(); if (getStatus) gitItem.StatusPropertiesInitialized = true; if (getCommit) gitItem.CommitPropertiesInitialized = true; await SafetyExtensions.IgnoreExceptions(() => { return dispatcherQueue.EnqueueOrInvokeAsync(() => { var repo = new Repository(repoPath); GitItemModel gitItemModel = GitHelpers.GetGitInformationForItem(repo, gitItem.ItemPath, getStatus, getCommit); if (getStatus) { gitItem.UnmergedGitStatusIcon = gitItemModel.Status switch { ChangeKind.Added => (Microsoft.UI.Xaml.Style)Microsoft.UI.Xaml.Application.Current.Resources["App.ThemedIcons.Status.Added"], ChangeKind.Deleted => (Microsoft.UI.Xaml.Style)Microsoft.UI.Xaml.Application.Current.Resources["App.ThemedIcons.Status.Removed"], ChangeKind.Modified => (Microsoft.UI.Xaml.Style)Microsoft.UI.Xaml.Application.Current.Resources["App.ThemedIcons.Status.Modified"], ChangeKind.Untracked => (Microsoft.UI.Xaml.Style)Microsoft.UI.Xaml.Application.Current.Resources["App.ThemedIcons.Status.Removed"], _ => null, }; gitItem.UnmergedGitStatusName = gitItemModel.StatusHumanized; } if (getCommit) { gitItem.GitLastCommitDate = gitItemModel.LastCommit?.Author.When; gitItem.GitLastCommitMessage = gitItemModel.LastCommit?.MessageShort; gitItem.GitLastCommitAuthor = gitItemModel.LastCommit?.Author.Name; gitItem.GitLastCommitSha = gitItemModel.LastCommit?.Sha.Substring(0, 7); gitItem.GitLastCommitFullSha = gitItemModel.LastCommit?.Sha; } repo.Dispose(); }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); }); } }, cts.Token); } catch (OperationCanceledException) { // Ignored } } private async Task<ImageSource?> GetItemTypeGroupIcon(ListedItem item, BaseStorageFile? matchingStorageItem = null) { ImageSource? groupImage = null; if (item.PrimaryItemAttribute != StorageItemTypes.Folder || item.IsArchive) { var result = await FileThumbnailHelper.GetIconAsync( item.ItemPath, Constants.ShellIconSizes.Large, false, IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale); if (result is not null && !item.IsShortcut) groupImage = await dispatcherQueue.EnqueueOrInvokeAsync(() => result.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); // The groupImage is null if loading icon from fulltrust process failed if (!item.IsShortcut && !item.IsHiddenItem && !FtpHelpers.IsFtpPath(item.ItemPath) && groupImage is null) { matchingStorageItem ??= await GetFileFromPathAsync(item.ItemPath); if (matchingStorageItem is not null) { using StorageItemThumbnail headerThumbnail = await FilesystemTasks.Wrap(() => matchingStorageItem.GetThumbnailAsync(ThumbnailMode.DocumentsView, 36, ThumbnailOptions.UseCurrentScale).AsTask()); if (headerThumbnail is not null) { await dispatcherQueue.EnqueueOrInvokeAsync(async () => { var bmp = new BitmapImage(); await bmp.SetSourceAsync(headerThumbnail); groupImage = bmp; }); } } } } // This prevents both the shortcut glyph and folder icon being shown else if (!item.IsShortcut) { await dispatcherQueue.EnqueueOrInvokeAsync(() => groupImage = new SvgImageSource(new Uri("ms-appx:///Assets/FolderIcon2.svg")) { RasterizePixelHeight = 128, RasterizePixelWidth = 128, }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } return groupImage; } public void RefreshItems(string? previousDir, Action postLoadCallback = null) { _ = RapidAddItemsToCollectionAsync(WorkingDirectory, previousDir, postLoadCallback); } private async Task RapidAddItemsToCollectionAsync(string path, string? previousDir, Action postLoadCallback) { IsSearchResults = false; HasNoWatcher = false; IsLocationUnavailable = false; StopWatchingForLocationRestoration(); ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Starting }); CancelLoadAndClearFiles(); if (string.IsNullOrEmpty(path)) return; try { // Only one instance at a time should access this function // Wait here until the previous one has ended // If we're waiting and a new update request comes through simply drop this instance await enumFolderSemaphore.WaitAsync(semaphoreCTS.Token); } catch (OperationCanceledException) { return; } try { // Drop all the other waiting instances semaphoreCTS.Cancel(); semaphoreCTS = new CancellationTokenSource(); IsLoadingItems = true; filesAndFolders.Clear(); FilesAndFolders.Clear(); desktopIniUpdateTask = null; ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.InProgress }); if (path.ToLowerInvariant().EndsWith(ShellLibraryItem.EXTENSION, StringComparison.Ordinal)) { if (App.LibraryManager.TryGetLibrary(path, out LibraryLocationItem library) && !library.IsEmpty) { var libItem = new LibraryItem(library); foreach (var folder in library.Folders) await RapidAddItemsToCollectionAsync(folder, libItem); } } else { await RapidAddItemsToCollectionAsync(path); } ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Complete, PreviousDirectory = previousDir, Path = path }); IsLoadingItems = false; if (Interlocked.Exchange(ref desktopIniUpdateTask, null) is Task task) await task; AdaptiveLayoutHelpers.ApplyAdaptativeLayout(folderSettings, filesAndFolders.ToList()); } finally { // Make sure item count is updated DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); enumFolderSemaphore.Release(); } postLoadCallback?.Invoke(); } private async Task RapidAddItemsToCollectionAsync(string? path, LibraryItem? library = null) { if (string.IsNullOrEmpty(path)) return; var stopwatch = new Stopwatch(); stopwatch.Start(); var isRecycleBin = path.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal); var enumerated = await EnumerateItemsFromStandardFolderAsync(path, addFilesCTS.Token, library); // Hide progressbar after enumeration IsLoadingItems = false; switch (enumerated) { // Enumerated with FindFirstFileExFromApp // Is folder synced to cloud storage? case 0: currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); var syncStatus = await CheckCloudDriveSyncStatusAsync(currentStorageFolder?.Item); PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown, IsTypeGitRepository = IsValidGitDirectory }); if (!HasNoWatcher) WatchForDirectoryChanges(path, syncStatus); if (IsValidGitDirectory) WatchForGitChanges(); break; // Enumerated with StorageFolder case 1: PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false, IsTypeRecycleBin = isRecycleBin }); currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); if (!HasNoWatcher) _ = WatchForStorageFolderChangesAsync(currentStorageFolder?.Item); break; // Watch for changes using Win32 in Box Drive folder (#7428) and network drives (#5869) case 2: PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false }); if (!HasNoWatcher) WatchForWin32FolderChanges(path); break; // Enumeration failed case -1: default: break; } if (IsLoadingCancelled) { IsLoadingCancelled = false; IsLoadingItems = false; return; } stopwatch.Stop(); Debug.WriteLine($"Loading of items in {path} completed in {stopwatch.ElapsedMilliseconds} milliseconds.\n"); } public void CloseWatcher() { App.Logger.LogInformation($"CloseWatcher: aProcessQueueAction={aProcessQueueAction?.Status.ToString()}, gitProcessQueueAction={gitProcessQueueAction?.Status.ToString()}"); watcher?.Dispose(); watcher = null; aProcessQueueAction = null; gitProcessQueueAction = null; watcherCTS?.Cancel(); watcherCTS = new CancellationTokenSource(); } private async Task<int> EnumerateItemsFromStandardFolderAsync(string path, CancellationToken cancellationToken, LibraryItem? library = null) { // Flag to use FindFirstFileExFromApp or StorageFolder enumeration - Use storage folder for Box Drive (#4629) var isBoxFolder = CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder && path.StartsWith(boxFolder); bool isWslDistro = path.StartsWith(@"\\wsl$\", StringComparison.OrdinalIgnoreCase) || path.StartsWith(@"\\wsl.localhost\", StringComparison.OrdinalIgnoreCase) || path.Equals(@"\\wsl$", StringComparison.OrdinalIgnoreCase) || path.Equals(@"\\wsl.localhost", StringComparison.OrdinalIgnoreCase); bool isMtp = path.StartsWith(@"\\?\", StringComparison.Ordinal); bool isShellFolder = path.StartsWith(@"\\SHELL\", StringComparison.Ordinal); bool isNetwork = path.StartsWith(@"\\", StringComparison.Ordinal) && !isMtp && !isShellFolder && !isWslDistro; bool isNetdisk = false; try { // Special handling for network drives if (!isNetwork) isNetdisk = (new DriveInfo(path).DriveType == System.IO.DriveType.Network); } catch { } bool isFtp = FtpHelpers.IsFtpPath(path); bool enumFromStorageFolder = isBoxFolder || isFtp; BaseStorageFolder? rootFolder = null; if (isNetwork || isNetdisk) { var auth = await NetworkService.AuthenticateNetworkShare(path); if (!auth) return -1; } if (!enumFromStorageFolder && FolderHelpers.CheckFolderAccessWithWin32(path)) { // Will enumerate with FindFirstFileExFromApp, rootFolder only used for Bitlocker currentStorageFolder = null; } else if (workingRoot is not null) { var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder)); if (!res) return -1; currentStorageFolder = res.Result; rootFolder = currentStorageFolder.Item; enumFromStorageFolder = true; } else { var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder)); if (res) { currentStorageFolder = res.Result; rootFolder = currentStorageFolder.Item; } else if (res == FileSystemStatusCode.Unauthorized) { ShowLocationInaccessibleOrMissing(path); return -1; } else if (res == FileSystemStatusCode.NotFound) { ShowLocationInaccessibleOrMissing(path); return -1; } else { ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged, res.ErrorCode.ToString()); return -1; } } var pathRoot = Path.GetPathRoot(path); if (Path.IsPathRooted(path) && pathRoot == path) { rootFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); if (await FolderHelpers.CheckBitlockerStatusAsync(rootFolder, WorkingDirectory)) await ContextMenu.InvokeVerb("unlock-bde", pathRoot); } HasNoWatcher = isFtp || isWslDistro || isMtp || currentStorageFolder?.Item is ZipStorageFolder; if (enumFromStorageFolder) { var basicProps = await rootFolder?.GetBasicPropertiesAsync(); var currentFolder = library ?? new ListedItem(rootFolder?.FolderRelativeId ?? string.Empty) { PrimaryItemAttribute = StorageItemTypes.Folder, ItemPropertiesInitialized = true, ItemNameRaw = rootFolder?.DisplayName ?? string.Empty, ItemDateModifiedReal = basicProps.DateModified, ItemType = rootFolder?.DisplayType ?? string.Empty, FileImage = null, LoadFileIcon = false, ItemPath = string.IsNullOrEmpty(rootFolder?.Path) ? currentStorageFolder?.Path ?? string.Empty : rootFolder.Path, FileSize = null, FileSizeBytes = 0, }; if (library is null) currentFolder.ItemDateCreatedReal = rootFolder?.DateCreated ?? DateTimeOffset.Now; CurrentFolder = currentFolder; await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken); // Workaround for #7428 return isBoxFolder ? 2 : 1; } else { (IntPtr hFile, WIN32_FIND_DATA findData, int errorCode) = await Task.Run(() => { var findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic; var additionalFlags = FIND_FIRST_EX_LARGE_FETCH; IntPtr hFileTsk = FindFirstFileExFromApp( path + "\\*.*", findInfoLevel, out WIN32_FIND_DATA findDataTsk, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, additionalFlags); return (hFileTsk, findDataTsk, hFileTsk.ToInt64() == -1 ? Marshal.GetLastWin32Error() : 0); }) .WithTimeoutAsync(TimeSpan.FromSeconds(5)); var itemModifiedDate = DateTime.Now; var itemCreatedDate = DateTime.Now; try { FileTimeToSystemTime(ref findData.ftLastWriteTime, out var systemModifiedTimeOutput); itemModifiedDate = systemModifiedTimeOutput.ToDateTime(); FileTimeToSystemTime(ref findData.ftCreationTime, out SYSTEMTIME systemCreatedTimeOutput); itemCreatedDate = systemCreatedTimeOutput.ToDateTime(); } catch (ArgumentException) { } var isHidden = (((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden); var opacity = isHidden ? Constants.UI.DimItemOpacity : 1d; var currentFolder = library ?? new ListedItem(null) { PrimaryItemAttribute = StorageItemTypes.Folder, ItemPropertiesInitialized = true, ItemNameRaw = rootFolder?.DisplayName ?? Path.GetFileName(path.TrimEnd('\\')), ItemDateModifiedReal = itemModifiedDate, ItemDateCreatedReal = itemCreatedDate, ItemType = folderTypeTextLocalized, FileImage = null, IsHiddenItem = isHidden, Opacity = opacity, LoadFileIcon = false, ItemPath = path, FileSize = null, FileSizeBytes = 0, }; CurrentFolder = currentFolder; if (hFile == IntPtr.Zero) { ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged); return -1; } else if (hFile.ToInt64() == -1) { await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken); // errorCode == ERROR_ACCESS_DENIED if (filesAndFolders.Count == 0 && errorCode == 0x5) { ShowLocationInaccessibleOrMissing(path); return -1; } return 1; } else { await Task.Run(async () => { List<ListedItem> fileList = await Win32StorageEnumerator.ListEntries(path, hFile, findData, cancellationToken, -1, intermediateAction: async (intermediateList) => { filesAndFolders.AddRange(intermediateList); await ApplyFilesAndFoldersChangesAsync(); }); filesAndFolders.AddRange(fileList); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); // Not awaited here: with Low priority these don't run until the UI thread goes idle // after the final list update, which would delay load completion and watcher setup. // The desktop.ini task is awaited before applying the adaptive layout, which reads DesktopIni. _ = dispatcherQueue.EnqueueOrInvokeAsync(CheckForSolutionFile, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); desktopIniUpdateTask = dispatcherQueue.EnqueueOrInvokeAsync(() => { GetDesktopIniFileData(); CheckForBackgroundImage(); FilesAndFoldersFilter = null; }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); }); rootFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); if (rootFolder is not null) { if (rootFolder.DisplayName is not null) currentFolder.ItemNameRaw = rootFolder.DisplayName; if (!string.Equals(path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) { var syncStatus = await CheckCloudDriveSyncStatusAsync(rootFolder); currentFolder.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); } } return 0; } } } private async Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath currentStorageFolder, CancellationToken cancellationToken) { if (rootFolder is null) return; if (rootFolder is IPasswordProtectedItem ppis) ppis.PasswordRequestedCallback = async (item) => { await dispatcherQueue.EnqueueOrInvokeAsync(() => ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired)); return await UIFilesystemHelpers.RequestPassword(item); }; try { await Task.Run(async () => { List<ListedItem> finalList = await UniversalStorageEnumerator.ListEntries( rootFolder, currentStorageFolder, cancellationToken, -1, async (intermediateList) => { filesAndFolders.AddRange(intermediateList); // Sorting the growing list on every intermediate batch is O(batches x n log n); // append unsorted here (matching the Win32 path) and sort once when enumeration completes. await ApplyFilesAndFoldersChangesAsync(); }); filesAndFolders.AddRange(finalList); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); }, cancellationToken); IsLocationUnavailable = false; } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) // Password dialog dismissed { ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired); } if (rootFolder is IPasswordProtectedItem ppiu) ppiu.PasswordRequestedCallback = null; } private void CheckForSolutionFile() { SolutionFilePath = filesAndFolders.ToList().AsParallel() .Where(item => FileExtensionHelpers.HasExtension(item.FileExtension, ".sln", ".slnx")) .FirstOrDefault()?.ItemPath; } private void GetDesktopIniFileData() { var path = Path.Combine(WorkingDirectory, "desktop.ini"); DesktopIni = WindowsIniService.GetData(path); } public void CheckForBackgroundImage() { if (WorkingDirectory == "Home" || WorkingDirectory == "ReleaseNotes" || WorkingDirectory == "Settings") { FolderBackgroundImageSource = null; return; } var filesAppSection = DesktopIni?.FirstOrDefault(x => x.SectionName == "FilesApp"); if (filesAppSection is null || folderSettings.LayoutMode is FolderLayoutModes.ColumnView) { FolderBackgroundImageSource = null; return; } // Image source var backgroundImage = filesAppSection.Parameters.FirstOrDefault(x => x.Key == "Files_BackgroundImage").Value; if (string.IsNullOrEmpty(backgroundImage)) { FolderBackgroundImageSource = null; return; } else { try { FolderBackgroundImageSource = new BitmapImage { UriSource = new Uri(backgroundImage, UriKind.RelativeOrAbsolute), CreateOptions = BitmapCreateOptions.IgnoreImageCache }; } catch (Exception ex) { // Handle errors with setting the URI App.Logger.LogWarning(ex, ex.Message); } } // Opacity var backgroundOpacity = filesAppSection.Parameters.FirstOrDefault(x => x.Key == "Files_BackgroundOpacity").Value; if (float.TryParse(backgroundOpacity, out var opacity)) FolderBackgroundImageOpacity = opacity; else FolderBackgroundImageOpacity = 1f; // Stretch var backgroundFit = filesAppSection.Parameters.FirstOrDefault(x => x.Key == "Files_BackgroundFit").Value; if (Enum.TryParse<Stretch>(backgroundFit, out var fit)) FolderBackgroundImageFit = fit; else FolderBackgroundImageFit = Stretch.UniformToFill; // Vertical alignment var verticalAlignment = filesAppSection.Parameters.FirstOrDefault(x => x.Key == "Files_BackgroundVerticalAlignment").Value; if (Enum.TryParse<VerticalAlignment>(verticalAlignment, out var vAlignment)) FolderBackgroundImageVerticalAlignment = vAlignment; else FolderBackgroundImageVerticalAlignment = VerticalAlignment.Center; // Horizontal alignment var horizontalAlignment = filesAppSection.Parameters.FirstOrDefault(x => x.Key == "Files_BackgroundHorizontalAlignment").Value; if (Enum.TryParse<HorizontalAlignment>(horizontalAlignment, out var hAlignment)) FolderBackgroundImageHorizontalAlignment = hAlignment; else FolderBackgroundImageHorizontalAlignment = HorizontalAlignment.Center; } public async Task<CloudDriveSyncStatus> CheckCloudDriveSyncStatusAsync(IStorageItem item) { int? syncStatus = null; if (item is BaseStorageFile file && file.Properties is not null) { var extraProperties = await FilesystemTasks.Wrap(() => file.Properties.RetrievePropertiesAsync(["System.FilePlaceholderStatus"]).AsTask()); if (extraProperties) syncStatus = (int?)(uint?)extraProperties.Result["System.FilePlaceholderStatus"]; } else if (item is BaseStorageFolder folder && folder.Properties is not null) { var extraProperties = await FilesystemTasks.Wrap(() => folder.Properties.RetrievePropertiesAsync(["System.FilePlaceholderStatus", "System.FileOfflineAvailabilityStatus"]).AsTask()); if (extraProperties) { syncStatus = (int?)(uint?)extraProperties.Result["System.FileOfflineAvailabilityStatus"]; // If no FileOfflineAvailabilityStatus, check FilePlaceholderStatus syncStatus ??= (int?)(uint?)extraProperties.Result["System.FilePlaceholderStatus"]; } } if (syncStatus is null || !Enum.IsDefined(typeof(CloudDriveSyncStatus), syncStatus)) return CloudDriveSyncStatus.Unknown; return (CloudDriveSyncStatus)syncStatus; } private async Task<FilesystemResult<IDictionary<string, object>>?> GetExtraProperties(IStorageItem matchingStorageItem) { if (matchingStorageItem is BaseStorageFile file && file.Properties != null) return await FilesystemTasks.Wrap(() => file.Properties.RetrievePropertiesAsync(["System.Image.Dimensions", "System.Media.Duration", "System.FileVersion"]).AsTask()); else if (matchingStorageItem is BaseStorageFolder folder && folder.Properties != null) return await FilesystemTasks.Wrap(() => folder.Properties.RetrievePropertiesAsync(["System.FreeSpace", "System.Capacity", "System.SFGAOFlags"]).AsTask()); return null; } private async Task WatchForStorageFolderChangesAsync(BaseStorageFolder? rootFolder) { if (rootFolder is null) return; await Task.Factory.StartNew(() => { var options = new QueryOptions() { FolderDepth = FolderDepth.Shallow, IndexerOption = IndexerOption.OnlyUseIndexerAndOptimizeForIndexedProperties }; options.SetPropertyPrefetch(PropertyPrefetchOptions.None, null); options.SetThumbnailPrefetch(ThumbnailMode.ListView, 0, ThumbnailOptions.ReturnOnlyIfCached); if (rootFolder.AreQueryOptionsSupported(options)) { var itemQueryResult = rootFolder.CreateItemQueryWithOptions(options).ToStorageItemQueryResult(); itemQueryResult.ContentsChanged += ItemQueryResult_ContentsChanged; // Just get one item to start getting notifications var watchedItemsOperation = itemQueryResult.GetItemsAsync(0, 1); watcherCTS.Token.Register(() => { itemQueryResult.ContentsChanged -= ItemQueryResult_ContentsChanged; watchedItemsOperation?.Cancel(); }); } }, default, TaskCreationOptions.LongRunning, TaskScheduler.Default); } private void WatchForWin32FolderChanges(string? folderPath) { if (!Directory.Exists(folderPath)) return; // NOTE: Suppressed NullReferenceException caused by EnableRaisingEvents SafetyExtensions.IgnoreExceptions(() => { watcher = new FileSystemWatcher { Path = folderPath, Filter = "*.*", NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName }; watcher.Created += DirectoryWatcher_Changed; watcher.Deleted += DirectoryWatcher_Changed; watcher.Renamed += DirectoryWatcher_Changed; watcher.EnableRaisingEvents = true; }, App.Logger); } private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e) { Debug.WriteLine($"Directory watcher event: {e.ChangeType}, {e.FullPath}"); await dispatcherQueue.EnqueueOrInvokeAsync(() => { RefreshItems(null); }); } private async void ItemQueryResult_ContentsChanged(IStorageQueryResultBase sender, object args) { // Query options have to be reapplied otherwise old results are returned var options = new QueryOptions() { FolderDepth = FolderDepth.Shallow, IndexerOption = IndexerOption.OnlyUseIndexerAndOptimizeForIndexedProperties }; options.SetPropertyPrefetch(PropertyPrefetchOptions.None, null); options.SetThumbnailPrefetch(ThumbnailMode.ListView, 0, ThumbnailOptions.ReturnOnlyIfCached); sender.ApplyNewQueryOptions(options); await dispatcherQueue.EnqueueOrInvokeAsync(() => { RefreshItems(null); }); } private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStatus) { Debug.WriteLine($"WatchForDirectoryChanges: {path}"); var hWatchDir = Win32PInvoke.CreateFileFromApp(path, 1, 1 | 2 | 4, IntPtr.Zero, 3, (uint)Win32PInvoke.File_Attributes.BackupSemantics | (uint)Win32PInvoke.File_Attributes.Overlapped, IntPtr.Zero); if (hWatchDir.ToInt64() == -1) return; var hasSyncStatus = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown; aProcessQueueAction ??= Task.Factory.StartNew(() => ProcessOperationQueueAsync(watcherCTS.Token, hasSyncStatus), default, TaskCreationOptions.LongRunning, TaskScheduler.Default); var aWatcherAction = Windows.System.Threading.ThreadPool.RunAsync((x) => { var buff = new byte[4096]; var rand = Guid.NewGuid(); var notifyFilters = FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE; if (hasSyncStatus) notifyFilters |= FILE_NOTIFY_CHANGE_ATTRIBUTES; var overlapped = new OVERLAPPED(); using var eventHandle = PInvoke.CreateEvent(null, false, false, null); overlapped.hEvent = eventHandle.DangerousGetHandle(); const uint INFINITE = 0xFFFFFFFF; while (x.Status != AsyncStatus.Canceled) { unsafe { fixed (byte* pBuff = buff) { ref var notifyInformation = ref Unsafe.As<byte, FILE_NOTIFY_INFORMATION>(ref buff[0]); if (x.Status != AsyncStatus.Canceled) { ReadDirectoryChangesW(hWatchDir, pBuff, 4096, false, notifyFilters, null, ref overlapped, null); } else { break; } Debug.WriteLine("waiting: {0}", rand); if (x.Status == AsyncStatus.Canceled) break; var rc = WaitForSingleObjectEx(overlapped.hEvent, INFINITE, true); Debug.WriteLine("wait done: {0}", rand); uint offset = 0; ref var notifyInfo = ref Unsafe.As<byte, FILE_NOTIFY_INFORMATION>(ref buff[offset]); if (x.Status == AsyncStatus.Canceled) break; do { notifyInfo = ref Unsafe.As<byte, FILE_NOTIFY_INFORMATION>(ref buff[offset]); string? FileName = null; unsafe { fixed (char* name = notifyInfo.FileName) { FileName = Path.Combine(path, new string(name, 0, (int)notifyInfo.FileNameLength / 2)); } } uint action = notifyInfo.Action; Debug.WriteLine("action: {0}", action); operationQueue.Enqueue((action, FileName)); offset += notifyInfo.NextEntryOffset; } while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled); operationEvent.Set(); //ResetEvent(overlapped.hEvent); Debug.WriteLine("Task running..."); } } } operationQueue.Clear(); Debug.WriteLine("aWatcherAction done: {0}", rand); }); watcherCTS.Token.Register(() => { if (aWatcherAction is not null) { aWatcherAction?.Cancel(); // Prevent duplicate execution of this block aWatcherAction = null; Debug.WriteLine("watcher canceled"); } CancelIoEx(hWatchDir, IntPtr.Zero); CloseHandle(hWatchDir); }); } private void WatchForGitChanges() { var hWatchDir = Win32PInvoke.CreateFileFromApp( GitDirectory!, 1, 1 | 2 | 4, IntPtr.Zero, 3, (uint)Win32PInvoke.File_Attributes.BackupSemantics | (uint)Win32PInvoke.File_Attributes.Overlapped, IntPtr.Zero); if (hWatchDir.ToInt64() == -1) return; gitProcessQueueAction ??= Task.Factory.StartNew(() => ProcessGitChangesQueueAsync(watcherCTS.Token), default, TaskCreationOptions.LongRunning, TaskScheduler.Default); var gitWatcherAction = Windows.System.Threading.ThreadPool.RunAsync((x) => { var buff = new byte[4096]; var rand = Guid.NewGuid(); var notifyFilters = FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_CREATION; var overlapped = new OVERLAPPED(); using var eventHandle = PInvoke.CreateEvent(null, false, false, null); overlapped.hEvent = eventHandle.DangerousGetHandle(); const uint INFINITE = 0xFFFFFFFF; while (x.Status != AsyncStatus.Canceled) { unsafe { fixed (byte* pBuff = buff) { ref var notifyInformation = ref Unsafe.As<byte, FILE_NOTIFY_INFORMATION>(ref buff[0]); if (x.Status == AsyncStatus.Canceled) break; ReadDirectoryChangesW(hWatchDir, pBuff, 4096, true, notifyFilters, null, ref overlapped, null); if (x.Status == AsyncStatus.Canceled) break; var rc = WaitForSingleObjectEx(overlapped.hEvent, INFINITE, true); uint offset = 0; ref var notifyInfo = ref Unsafe.As<byte, FILE_NOTIFY_INFORMATION>(ref buff[offset]); if (x.Status == AsyncStatus.Canceled) break; do { notifyInfo = ref Unsafe.As<byte, FILE_NOTIFY_INFORMATION>(ref buff[offset]); uint action = notifyInfo.Action; gitChangesQueue.Enqueue(action); offset += notifyInfo.NextEntryOffset; } while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled); gitChangedEvent.Set(); } } } gitChangesQueue.Clear(); }); watcherCTS.Token.Register(() => { if (gitWatcherAction is not null) { gitWatcherAction?.Cancel(); // Prevent duplicate execution of this block gitWatcherAction = null; } CancelIoEx(hWatchDir, IntPtr.Zero); CloseHandle(hWatchDir); }); } private async Task ProcessGitChangesQueueAsync(CancellationToken cancellationToken) { const int DELAY = 200; var sampler = new IntervalSampler(100); int changes = 0; try { while (!cancellationToken.IsCancellationRequested) { if (await gitChangedEvent.WaitAsync(DELAY, cancellationToken)) { gitChangedEvent.Reset(); while (gitChangesQueue.TryDequeue(out var _)) ++changes; if (changes != 0 && sampler.CheckNow()) { await dispatcherQueue.EnqueueOrInvokeAsync(() => GitDirectoryUpdated?.Invoke(null, null!)); changes = 0; } } } } catch { } } private async Task ProcessOperationQueueAsync(CancellationToken cancellationToken, bool hasSyncStatus) { const uint FILE_ACTION_ADDED = 0x00000001; const uint FILE_ACTION_REMOVED = 0x00000002; const uint FILE_ACTION_MODIFIED = 0x00000003; const uint FILE_ACTION_RENAMED_OLD_NAME = 0x00000004; const uint FILE_ACTION_RENAMED_NEW_NAME = 0x00000005; const int UPDATE_BATCH_SIZE = 32; var sampler = new IntervalSampler(200); var updateQueue = new Queue<string>(); var anyEdits = false; ListedItem? lastItemAdded = null; var rand = Guid.NewGuid(); // Call when any edits have occurred async Task HandleChangesOccurredAsync() { await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); if (lastItemAdded is not null) { await RequestSelectionAsync([lastItemAdded]); lastItemAdded = null; } anyEdits = false; } try { while (!cancellationToken.IsCancellationRequested) { if (await operationEvent.WaitAsync(200, cancellationToken)) { operationEvent.Reset(); while (operationQueue.TryDequeue(out var operation)) { if (cancellationToken.IsCancellationRequested) break; try { switch (operation.Action) { case FILE_ACTION_ADDED: case FILE_ACTION_RENAMED_NEW_NAME: lastItemAdded = await AddFileOrFolderAsync(operation.FileName); if (lastItemAdded is not null) anyEdits = true; break; case FILE_ACTION_MODIFIED: if (!updateQueue.Contains(operation.FileName)) updateQueue.Enqueue(operation.FileName); break; case FILE_ACTION_REMOVED: var itemRemoved = await RemoveFileOrFolderAsync(operation.FileName); if (itemRemoved is not null) anyEdits = true; break; case FILE_ACTION_RENAMED_OLD_NAME: // Pair OLD_NAME with the following NEW_NAME so the item can be updated // in place and stay at its current position instead of jumping to a new // sorted slot after a rename (issue #4214). Leaving anyEdits false skips // OrderFilesAndFoldersAsync; PropertyChanged keeps the visible name in sync. if (operationQueue.TryPeek(out var nextOp) && nextOp.Action == FILE_ACTION_RENAMED_NEW_NAME && filesAndFolders.ToList().FirstOrDefault(x => x.ItemPath.Equals(operation.FileName, StringComparison.OrdinalIgnoreCase)) is { } renamed) { operationQueue.TryDequeue(out _); var newPath = nextOp.FileName; await dispatcherQueue.EnqueueOrInvokeAsync(() => { renamed.ItemPath = newPath; renamed.ItemNameRaw = Path.GetFileName(newPath); if (renamed.PrimaryItemAttribute == StorageItemTypes.File) renamed.FileExtension = Path.GetExtension(newPath); }); } else { var itemRenamedOld = await RemoveFileOrFolderAsync(operation.FileName); if (itemRenamedOld is not null) anyEdits = true; } break; } } catch (Exception ex) { App.Logger.LogWarning(ex, ex.Message); } if (anyEdits && sampler.CheckNow()) await HandleChangesOccurredAsync(); } var itemsToUpdate = new List<string>(); for (var i = 0; i < UPDATE_BATCH_SIZE && updateQueue.Count > 0; i++) itemsToUpdate.Add(updateQueue.Dequeue()); await UpdateFilesOrFoldersAsync(itemsToUpdate, hasSyncStatus); } if (updateQueue.Count > 0) { var itemsToUpdate = new List<string>(); for (var i = 0; i < UPDATE_BATCH_SIZE && updateQueue.Count > 0; i++) itemsToUpdate.Add(updateQueue.Dequeue()); await UpdateFilesOrFoldersAsync(itemsToUpdate, hasSyncStatus); } if (anyEdits && sampler.CheckNow()) await HandleChangesOccurredAsync(); } } catch { // Prevent disposed cancellation token } Debug.WriteLine("aProcessQueueAction done: {0}", rand); } public Task<ListedItem> AddFileOrFolderFromShellFile(ShellFileItem item) { return item.IsFolder ? UniversalStorageEnumerator.AddFolderAsync(ShellStorageFolder.FromShellItem(item), currentStorageFolder, addFilesCTS.Token) : UniversalStorageEnumerator.AddFileAsync(ShellStorageFile.FromShellItem(item), currentStorageFolder, addFilesCTS.Token); } private async Task AddFileOrFolderAsync(ListedItem? item) { if (item is null) return; try { await enumFolderSemaphore.WaitAsync(semaphoreCTS.Token); } catch (OperationCanceledException) { return; } if (!filesAndFolders.ToList().Any(x => x.ItemPath.Equals(item.ItemPath, StringComparison.OrdinalIgnoreCase))) // Avoid adding duplicate items { filesAndFolders.Add(item); if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible) { // New file added, enumerate ADS foreach (var ads in Win32Helper.GetAlternateStreams(item.ItemPath)) { var adsItem = Win32StorageEnumerator.GetAlternateStream(ads, item); filesAndFolders.Add(adsItem); } } } enumFolderSemaphore.Release(); } private async Task<ListedItem?> AddFileOrFolderAsync(string fileOrFolderPath) { FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic; var additionalFlags = FIND_FIRST_EX_CASE_SENSITIVE; IntPtr hFile = FindFirstFileExFromApp(fileOrFolderPath, findInfoLevel, out WIN32_FIND_DATA findData, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, additionalFlags); if (hFile.ToInt64() == -1) { // If we cannot find the file (probably since it doesn't exist anymore) simply exit without adding it return null; } FindClose(hFile); var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System; var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; var startWithDot = findData.cFileName.StartsWith('.'); if ((isHidden && (!UserSettingsService.FoldersSettingsService.ShowHiddenItems || (isSystem && !UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) || (startWithDot && !UserSettingsService.FoldersSettingsService.ShowDotFiles)) { // Do not add to file list if hidden/system attribute is set and system/hidden file are not to be shown return null; } ListedItem listedItem; // FILE_ATTRIBUTE_DIRECTORY if ((findData.dwFileAttributes & 0x10) > 0) listedItem = await Win32StorageEnumerator.GetFolder(findData, Directory.GetParent(fileOrFolderPath).FullName, IsValidGitDirectory, addFilesCTS.Token); else listedItem = await Win32StorageEnumerator.GetFile(findData, Directory.GetParent(fileOrFolderPath).FullName, IsValidGitDirectory, addFilesCTS.Token); await AddFileOrFolderAsync(listedItem); return listedItem; } private async Task<(ListedItem Item, CloudDriveSyncStatus? SyncStatus, long? Size, DateTimeOffset Created, DateTimeOffset Modified)?> GetFileOrFolderUpdateInfoAsync(ListedItem item, bool hasSyncStatus) { IStorageItem? storageItem = null; if (item.PrimaryItemAttribute == StorageItemTypes.File) storageItem = (await GetFileFromPathAsync(item.ItemPath)).Result; else if (item.PrimaryItemAttribute == StorageItemTypes.Folder) storageItem = (await GetFolderFromPathAsync(item.ItemPath)).Result; if (storageItem is not null) { CloudDriveSyncStatus? syncStatus = hasSyncStatus ? await CheckCloudDriveSyncStatusAsync(storageItem) : null; long? size = null; DateTimeOffset created = default, modified = default; if (storageItem.IsOfType(StorageItemTypes.File)) { var properties = await storageItem.AsBaseStorageFile().GetBasicPropertiesAsync(); size = (long)properties.Size; modified = properties.DateModified; created = properties.DateCreated; } else if (storageItem.IsOfType(StorageItemTypes.Folder)) { var properties = await storageItem.AsBaseStorageFolder().GetBasicPropertiesAsync(); size = item.IsArchive ? (long)properties.Size : null; modified = properties.DateModified; created = properties.DateCreated; } return (item, syncStatus, size, created, modified); } return null; } private async Task UpdateFilesOrFoldersAsync(IEnumerable<string> 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) { App.Logger.LogInformation("FILE_ACTION_MODIFIED thumbnail retry triggered [{Id}] '{Extension}'.", path.GetHashCode(), Path.GetExtension(path)); 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); } catch (OperationCanceledException) { return; } try { var matchingItems = filesAndFolders.ToList().Where(x => paths.Any(p => p.Equals(x.ItemPath, StringComparison.OrdinalIgnoreCase))); var results = await Task.WhenAll(matchingItems.Select(x => GetFileOrFolderUpdateInfoAsync(x, hasSyncStatus))); await dispatcherQueue.EnqueueOrInvokeAsync(() => { var itemsRegrouped = false; foreach (var result in results) { if (result is not null) { var item = result.Value.Item; item.ItemDateModifiedReal = result.Value.Modified; item.ItemDateCreatedReal = result.Value.Created; if (result.Value.SyncStatus is not null) item.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(result.Value.SyncStatus.Value); if (result.Value.Size is not null) { item.FileSizeBytes = result.Value.Size.Value; item.FileSize = item.FileSizeBytes.ToSizeString(); } // Move the item to its correct group when the updated properties change // its group key, e.g. when the date modified is restored after // extracting an archive, see #14461 itemsRegrouped |= FilesAndFolders.UpdateItemGroup(item); } } // Sort the changed groups and their position among the other groups if (itemsRegrouped) OrderGroups(); }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } finally { enumFolderSemaphore.Release(); } } public async Task<ListedItem?> RemoveFileOrFolderAsync(string path) { try { await enumFolderSemaphore.WaitAsync(semaphoreCTS.Token); } catch (OperationCanceledException) { return null; } try { var matchingItem = filesAndFolders.ToList().FirstOrDefault(x => x.ItemPath.Equals(path, StringComparison.OrdinalIgnoreCase)); if (matchingItem is not null) { 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 foreach (var adsItem in filesAndFolders.ToList().Where(x => x is AlternateStreamItem ads && ads.MainStreamPath == matchingItem.ItemPath)) filesAndFolders.Remove(adsItem); } return matchingItem; } } finally { enumFolderSemaphore.Release(); } return null; } public async Task AddSearchResultsToCollectionAsync(ObservableCollection<ListedItem> searchItems, string currentSearchPath) { filesAndFolders.Clear(); filesAndFolders.AddRange(searchItems); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } public async Task SearchAsync(FolderSearch search) { ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Starting }); CancelSearch(); searchCTS = new CancellationTokenSource(); filesAndFolders.Clear(); IsLoadingItems = true; IsSearchResults = true; HasNoWatcher = true; await ApplyFilesAndFoldersChangesAsync(); EmptyTextType = EmptyTextType.None; SearchHeaderTitle = !string.IsNullOrEmpty(search.Query) ? string.Format(Strings.SearchResultsFor.GetLocalizedResource(), search.Query) : string.Empty; if (SearchIconBitmapImage is null) SearchIconBitmapImage ??= await UIHelpers.GetSearchIconResource(); ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.InProgress }); var results = new List<ListedItem>(); search.SearchTick += async (s, e) => { filesAndFolders = new ConcurrentCollection<ListedItem>(results); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); }; await search.SearchAsync(results, searchCTS.Token); filesAndFolders = new ConcurrentCollection<ListedItem>(results); await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Complete }); IsLoadingItems = false; } public void CancelSearch() { searchCTS?.Cancel(); } public void UpdateDateDisplay() { App.Logger.LogDebug($"UpdateDateDisplay: itemCount={filesAndFolders?.Count}"); filesAndFolders.ToList().AsParallel().ForAll(async item => { if (item.IsRealChanges) await dispatcherQueue.EnqueueOrInvokeAsync(item.UpdateReal); }); } public void Dispose() { CancelLoadAndClearFiles(); StopWatchingForLocationRestoration(); filterDebounceCS?.Cancel(); filterDebounceCS?.Dispose(); networkAvailabilityCTS?.Dispose(); App.Logger.LogInformation($"ShellViewModel.Dispose: CurrentFolder={LogPathHelper.GetPathIdentifier(CurrentFolder?.ItemPath)}"); StorageTrashBinService.Watcher.ItemAdded -= RecycleBinItemCreatedAsync; StorageTrashBinService.Watcher.ItemDeleted -= RecycleBinItemDeletedAsync; StorageTrashBinService.Watcher.RefreshRequested -= RecycleBinRefreshRequestedAsync; UserSettingsService.OnSettingChangedEvent -= UserSettingsService_OnSettingChangedEvent; fileTagsSettingsService.OnSettingImportedEvent -= FileTagsSettingsService_OnSettingUpdated; fileTagsSettingsService.OnTagsUpdated -= FileTagsSettingsService_OnSettingUpdated; folderSizeProvider.SizeChanged -= FolderSizeProvider_SizeChanged; folderSettings.LayoutModeChangeRequested -= LayoutModeChangeRequested; } } public sealed class PageTypeUpdatedEventArgs { public bool IsTypeCloudDrive { get; set; } public bool IsTypeRecycleBin { get; set; } public bool IsTypeGitRepository { get; set; } public bool IsTypeSearchResults { get; set; } } public sealed class WorkingDirectoryModifiedEventArgs : EventArgs { public string? Path { get; set; } public string? Name { get; set; } public bool IsLibrary { get; set; } } public sealed class ItemLoadStatusChangedEventArgs : EventArgs { public enum ItemLoadStatus { Starting, InProgress, Complete } public ItemLoadStatus Status { get; set; } /// <summary> /// This property may not be provided consistently if Status is not Complete /// </summary> public string? PreviousDirectory { get; set; } /// <summary> /// This property may not be provided consistently if Status is not Complete /// </summary> public string? Path { get; set; } } public enum GitProperties { None, Status, Commit, All, } }