From 24f5ba6562f58110e29667f4d0162c4051bd5a98 Mon Sep 17 00:00:00 2001 From: 0x5BFA Date: Sun, 8 Sep 2024 14:31:08 +0900 Subject: [PATCH 1/6] Init --- src/Files.App.CsWin32/NativeMethods.txt | 15 + src/Files.App.CsWin32/Windows.Win32.Extras.cs | 14 - src/Files.App/App.xaml.cs | 2 - .../Contracts/IWindowsRecentItemsService.cs | 58 ++++ src/Files.App/Data/Items/WidgetRecentItem.cs | 65 ++++ src/Files.App/GlobalUsings.cs | 1 - .../Helpers/Application/AppLifecycleHelper.cs | 2 +- .../Helpers/Navigation/NavigationHelpers.cs | 7 +- .../Helpers/UI/UIFilesystemHelpers.cs | 5 +- .../Storage/StorageTrashBinService.cs | 4 +- .../Windows/WindowsRecentItemsService.cs | 321 ++++++++++++++++++ .../Widgets/RecentFilesWidget.xaml | 13 +- .../Widgets/RecentFilesWidget.xaml.cs | 2 +- src/Files.App/Utils/RecentItem/RecentItem.cs | 108 ------ src/Files.App/Utils/RecentItem/RecentItems.cs | 310 ----------------- .../Utils/RecentItem/RecentItemsManager.cs | 83 ----- .../Widgets/BaseWidgetViewModel.cs | 1 + .../Widgets/RecentFilesWidgetViewModel.cs | 47 ++- 18 files changed, 513 insertions(+), 545 deletions(-) create mode 100644 src/Files.App/Data/Contracts/IWindowsRecentItemsService.cs create mode 100644 src/Files.App/Data/Items/WidgetRecentItem.cs create mode 100644 src/Files.App/Services/Windows/WindowsRecentItemsService.cs delete mode 100644 src/Files.App/Utils/RecentItem/RecentItem.cs delete mode 100644 src/Files.App/Utils/RecentItem/RecentItems.cs delete mode 100644 src/Files.App/Utils/RecentItem/RecentItemsManager.cs diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 9d7c337d7c4d..ba07857e520d 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -133,3 +133,18 @@ IFileOperation IShellItem2 PSGetPropertyKeyFromName ShellExecuteEx +SHAddToRecentDocs +SHARD +BHID_EnumItems +FOLDERID_RecycleBinFolder +CoTaskMemFree +SHGetIDListFromObject +SHCreateItemFromIDList +BHID_SFUIObject +IContextMenu +CMF_NORMAL +CMF_OPTIMIZEFORINVOKE +IApplicationDestinations +ApplicationDestinations +IApplicationDocumentLists +ApplicationDocumentLists diff --git a/src/Files.App.CsWin32/Windows.Win32.Extras.cs b/src/Files.App.CsWin32/Windows.Win32.Extras.cs index 357578425b8f..c12b9bc62569 100644 --- a/src/Files.App.CsWin32/Windows.Win32.Extras.cs +++ b/src/Files.App.CsWin32/Windows.Win32.Extras.cs @@ -1,7 +1,6 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -using System; using System.Runtime.InteropServices; using Windows.Win32.Foundation; @@ -18,17 +17,4 @@ namespace UI.WindowsAndMessaging [UnmanagedFunctionPointer(CallingConvention.Winapi)] public delegate LRESULT WNDPROC(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam); } - - namespace UI.Shell - { - public static partial class FOLDERID - { - public readonly static Guid FOLDERID_RecycleBinFolder = new(0xB7534046, 0x3ECB, 0x4C18, 0xBE, 0x4E, 0x64, 0xCD, 0x4C, 0xB7, 0xD6, 0xAC); - } - - public static partial class BHID - { - public readonly static Guid BHID_EnumItems = new(0x94f60519, 0x2850, 0x4924, 0xaa, 0x5a, 0xd1, 0x5e, 0x84, 0x86, 0x80, 0x39); - } - } } diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index 083b1faab5fe..4d305570a86d 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -39,7 +39,6 @@ public static CommandBarFlyout? LastOpenedFlyout public static QuickAccessManager QuickAccessManager { get; private set; } = null!; public static StorageHistoryWrapper HistoryWrapper { get; private set; } = null!; public static FileTagsManager FileTagsManager { get; private set; } = null!; - public static RecentItems RecentItemsManager { get; private set; } = null!; public static LibraryManager LibraryManager { get; private set; } = null!; public static AppModel AppModel { get; private set; } = null!; public static ILogger Logger { get; private set; } = null!; @@ -114,7 +113,6 @@ async Task ActivateAsync() QuickAccessManager = Ioc.Default.GetRequiredService(); HistoryWrapper = Ioc.Default.GetRequiredService(); FileTagsManager = Ioc.Default.GetRequiredService(); - RecentItemsManager = Ioc.Default.GetRequiredService(); LibraryManager = Ioc.Default.GetRequiredService(); Logger = Ioc.Default.GetRequiredService>(); AppModel = Ioc.Default.GetRequiredService(); diff --git a/src/Files.App/Data/Contracts/IWindowsRecentItemsService.cs b/src/Files.App/Data/Contracts/IWindowsRecentItemsService.cs new file mode 100644 index 000000000000..efd2f64afddb --- /dev/null +++ b/src/Files.App/Data/Contracts/IWindowsRecentItemsService.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using System.Collections.Specialized; + +namespace Files.App.Data.Contracts +{ + /// + /// Provides manager of recent files and folders of File Explorer on Windows. + /// + public interface IWindowsRecentItemsService + { + /// + /// Gets recent files of File Explorer. + /// + IReadOnlyList RecentFiles { get; } + + /// + /// Gets recent folders of File Explorer. + /// + IReadOnlyList RecentFolders { get; } + + /// + /// Gets invoked when recent files of File Explorer have changed. + /// + event EventHandler? RecentFilesChanged; + + /// + /// Gets invoked when recent folders of File Explorer have changed. + /// + event EventHandler? RecentFoldersChanged; + + /// + /// Updates recent files of File Explorer. + /// + Task UpdateRecentFilesAsync(); + + /// + /// Updates recent folders of File Explorer. + /// + Task UpdateRecentFoldersAsync(); + + /// + /// Adds a recent file for File Explorer. + /// + bool Add(string path); + + /// + /// Removes a recent folder for File Explorer. + /// + bool Remove(RecentItem item); + + /// + /// Clears recent files and folders of File Explorer. + /// + bool Clear(); + } +} diff --git a/src/Files.App/Data/Items/WidgetRecentItem.cs b/src/Files.App/Data/Items/WidgetRecentItem.cs new file mode 100644 index 000000000000..ccf8f921a45c --- /dev/null +++ b/src/Files.App/Data/Items/WidgetRecentItem.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Win32; +using Windows.Win32.UI.Shell; + +namespace Files.App.Data.Items +{ + /// + /// Represents an item for recent item of File Explorer on Windows. + /// + public sealed class RecentItem : WidgetCardItem, IEquatable, IDisposable + { + private BitmapImage? _Icon; + /// + /// Gets or sets thumbnail icon of the recent item. + /// + public BitmapImage? Icon + { + get => _Icon; + set => SetProperty(ref _Icon, value); + } + + /// + /// Gets or sets name of the recent item. + /// + public required string Name { get; set; } + + /// + /// Gets or sets target path of the recent item. + /// + public required DateTime LastModified { get; set; } + + /// + /// Gets or initializes PIDL of the recent item. + /// + /// + /// This has to be removed in the future. + /// + public unsafe required ComPtr ShellItem { get; init; } + + /// + /// Loads thumbnail icon of the recent item. + /// + /// + public async Task LoadRecentItemIconAsync() + { + var result = await FileThumbnailHelper.GetIconAsync(Path, Constants.ShellIconSizes.Small, false, IconOptions.UseCurrentScale); + + var bitmapImage = await result.ToBitmapAsync(); + if (bitmapImage is not null) + Icon = bitmapImage; + } + + public override int GetHashCode() => (Path, Name).GetHashCode(); + public override bool Equals(object? other) => other is RecentItem item && Equals(item); + public bool Equals(RecentItem? other) => other is not null && other.Name == Name && other.Path == Path; + + public unsafe void Dispose() + { + ShellItem.Dispose(); + } + } +} diff --git a/src/Files.App/GlobalUsings.cs b/src/Files.App/GlobalUsings.cs index 9d36ad014fd3..f6806222bde0 100644 --- a/src/Files.App/GlobalUsings.cs +++ b/src/Files.App/GlobalUsings.cs @@ -29,7 +29,6 @@ global using global::Files.App.Utils.FileTags; global using global::Files.App.Utils.Git; global using global::Files.App.Utils.Library; -global using global::Files.App.Utils.RecentItem; global using global::Files.App.Utils.Serialization; global using global::Files.App.Utils.Shell; global using global::Files.App.Utils.StatusCenter; diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 8c2926268ea5..4804f6c001e4 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -169,6 +169,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() // Services + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -224,7 +225,6 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() ).Build(); diff --git a/src/Files.App/Helpers/Navigation/NavigationHelpers.cs b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs index 20fcf44f09c6..a19bfe720e6c 100644 --- a/src/Files.App/Helpers/Navigation/NavigationHelpers.cs +++ b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs @@ -12,6 +12,7 @@ namespace Files.App.Helpers { public static class NavigationHelpers { + private static readonly IWindowsRecentItemsService WindowsRecentItemsService = Ioc.Default.GetRequiredService(); private static MainPageViewModel MainPageViewModel { get; } = Ioc.Default.GetRequiredService(); private static DrivesViewModel DrivesViewModel { get; } = Ioc.Default.GetRequiredService(); private static INetworkService NetworkService { get; } = Ioc.Default.GetRequiredService(); @@ -524,7 +525,7 @@ private static async Task OpenDirectory(string path, IShellPag { // Add location to Recent Items List if (childFolder.Item is SystemStorageFolder) - App.RecentItemsManager.AddToRecentItems(childFolder.Path); + WindowsRecentItemsService.Add(childFolder.Path); }); if (!opened) opened = (FilesystemResult)FolderHelpers.CheckFolderAccessWithWin32(path); @@ -556,7 +557,7 @@ private static async Task OpenFile(string path, IShellPage ass StorageFileWithPath childFile = await associatedInstance.ShellViewModel.GetFileWithPathFromPathAsync(shortcutInfo.TargetPath); // Add location to Recent Items List if (childFile?.Item is SystemStorageFile) - App.RecentItemsManager.AddToRecentItems(childFile.Path); + WindowsRecentItemsService.Add(childFile.Path); } await Win32Helper.InvokeWin32ComponentAsync(shortcutInfo.TargetPath, associatedInstance, $"{args} {shortcutInfo.Arguments}", shortcutInfo.RunAsAdmin, shortcutInfo.WorkingDirectory); } @@ -573,7 +574,7 @@ private static async Task OpenFile(string path, IShellPage ass { // Add location to Recent Items List if (childFile.Item is SystemStorageFile) - App.RecentItemsManager.AddToRecentItems(childFile.Path); + WindowsRecentItemsService.Add(childFile.Path); if (openViaApplicationPicker) { diff --git a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs index 72f12c286621..f23da9e77492 100644 --- a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs +++ b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs @@ -117,7 +117,10 @@ public static async Task CreateFileFromDialogResultTypeAsync(AddItemDialogItemTy // Add newly created item to recent files list if (created.Status == ReturnResult.Success && created.Item?.Path is not null) - App.RecentItemsManager.AddToRecentItems(created.Item.Path); + { + IWindowsRecentItemsService windowsRecentItemsService = Ioc.Default.GetRequiredService(); + windowsRecentItemsService.Add(created.Item.Path); + } else if (created.Status == ReturnResult.AccessUnauthorized) { await DialogDisplayHelper.ShowDialogAsync diff --git a/src/Files.App/Services/Storage/StorageTrashBinService.cs b/src/Files.App/Services/Storage/StorageTrashBinService.cs index fcca2d532977..79620cfba3e1 100644 --- a/src/Files.App/Services/Storage/StorageTrashBinService.cs +++ b/src/Files.App/Services/Storage/StorageTrashBinService.cs @@ -90,13 +90,13 @@ public unsafe bool RestoreAllTrashes() try { // Get IShellItem for Recycle Bin - var recycleBinFolderId = FOLDERID.FOLDERID_RecycleBinFolder; + var recycleBinFolderId = PInvoke.FOLDERID_RecycleBinFolder; var shellItemGuid = typeof(IShellItem).GUID; PInvoke.SHGetKnownFolderItem(&recycleBinFolderId, KNOWN_FOLDER_FLAG.KF_FLAG_DEFAULT, HANDLE.Null, &shellItemGuid, (void**)&recycleBinFolderShellItem); // Get IEnumShellItems for Recycle Bin Guid enumShellItemGuid = typeof(IEnumShellItems).GUID; - var enumItemsBHID = BHID.BHID_EnumItems; + var enumItemsBHID = PInvoke.BHID_EnumItems; recycleBinFolderShellItem->BindToHandler(null, &enumItemsBHID, &enumShellItemGuid, (void**)&enumShellItems); // Initialize how to perform the operation diff --git a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs new file mode 100644 index 000000000000..cd031a1e054c --- /dev/null +++ b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs @@ -0,0 +1,321 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Microsoft.Extensions.Logging; +using System.Collections.Specialized; +using System.Runtime.InteropServices; +using System.Text; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.SystemServices; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Files.App.Services +{ + /// + public class WindowsRecentItemsService : IWindowsRecentItemsService + { + // Dependency injections + + private readonly IFoldersSettingsService FoldersSettingsService = Ioc.Default.GetRequiredService(); + + // Fields + + private readonly SystemIO.FileSystemWatcher? _watcher; + + // Properties + + private readonly List _RecentFiles = []; + /// + public IReadOnlyList RecentFiles + { + get + { + lock (_RecentFiles) + return _RecentFiles.ToList().AsReadOnly(); + } + } + + private readonly List _RecentFolders = []; + /// + public IReadOnlyList RecentFolders + { + get + { + lock (_RecentFolders) + return _RecentFolders.ToList().AsReadOnly(); + } + } + + // Events + + /// + public event EventHandler? RecentFilesChanged; + + /// + public event EventHandler? RecentFoldersChanged; + + // Constructor + + public WindowsRecentItemsService() + { + _watcher = new() + { + Path = SystemIO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Recent), "AutomaticDestinations"), + Filter = "5f7b5f1e01b83767.automaticDestinations-ms", + NotifyFilter = SystemIO.NotifyFilters.DirectoryName | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.LastWrite, + }; + + _watcher.Changed += Watcher_Changed; + _watcher.Deleted += Watcher_Changed; + _watcher.EnableRaisingEvents = true; + } + + // Methods + + /// + public async Task UpdateRecentFilesAsync() + { + return await Task.Run(() => + { + return UpdateRecentItems(false); + }); + } + + /// + public async Task UpdateRecentFoldersAsync() + { + return await Task.Run(() => + { + return UpdateRecentItems(true); + }); + } + + /// + public unsafe bool Add(string path) + { + try + { + fixed (char* cPath = path) + PInvoke.SHAddToRecentDocs((uint)SHARD.SHARD_PATHW, cPath); + + return true; + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, ex.Message); + return false; + } + } + + /// + public unsafe bool Remove(RecentItem item) + { + try + { + var bhid = PInvoke.BHID_SFUIObject; + var contextMenuIid = typeof(IContextMenu).GUID; + using ComPtr pContextMenu = default; + HRESULT hr = item.ShellItem.Get()->BindToHandler(null, &bhid, &contextMenuIid, (void**)pContextMenu.GetAddressOf()); + HMENU hMenu = PInvoke.CreatePopupMenu(); + hr = pContextMenu.Get()->QueryContextMenu(hMenu, 0, 1, 0x7FFF, PInvoke.CMF_OPTIMIZEFORINVOKE); + + // Initialize invocation info + CMINVOKECOMMANDINFO cmi = default; + cmi.cbSize = (uint)sizeof(CMINVOKECOMMANDINFO); + cmi.nShow = (int)SHOW_WINDOW_CMD.SW_HIDE; + + // Try unpin first for pinned files + fixed (byte* pVerb = Encoding.ASCII.GetBytes("unpinfromhome")) + cmi.lpVerb = new(pVerb); + hr = pContextMenu.Get()->InvokeCommand(cmi); + + // Remove recent files + fixed (byte* pVerb = Encoding.ASCII.GetBytes("remove")) + cmi.lpVerb = new(pVerb); + hr = pContextMenu.Get()->InvokeCommand(cmi); + + return true; + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, ex.Message); + return false; + } + } + + /// + public unsafe bool Clear() + { + try + { + PInvoke.SHAddToRecentDocs((uint)SHARD.SHARD_PIDL, null); + + return true; + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, ex.Message); + return false; + } + } + + private unsafe bool UpdateRecentItems(bool isFolder) + { + try + { + HRESULT hr = default; + + string szFolderShellPath = + isFolder + ? "Shell:::{22877A6D-37A1-461A-91B0-DBDA5AAEBC99}" // Recent Places folder (recent folders) + : "Shell:::{679F85CB-0220-4080-B29B-5540CC05AAB6}"; // Quick Access folder (recent files) + + // Get IShellItem of the shell folder + var shellItemIid = typeof(IShellItem).GUID; + using ComPtr pFolderShellItem = default; + fixed (char* pszFolderShellPath = szFolderShellPath) + hr = PInvoke.SHCreateItemFromParsingName(pszFolderShellPath, null, &shellItemIid, (void**)pFolderShellItem.GetAddressOf()); + + // Get IEnumShellItems of the quick access shell folder + var enumItemsBHID = PInvoke.BHID_EnumItems; + Guid enumShellItemIid = typeof(IEnumShellItems).GUID; + using ComPtr pEnumShellItems = default; + hr = pFolderShellItem.Get()->BindToHandler(null, &enumItemsBHID, &enumShellItemIid, (void**)pEnumShellItems.GetAddressOf()); + + // Enumerate recent items and populate the list + int index = 0; + List recentItems = []; + ComPtr pShellItem = default; // Do not dispose in this method to use later to prepare for its deletion + while (pEnumShellItems.Get()->Next(1, pShellItem.GetAddressOf()) == HRESULT.S_OK) + { + // Get top 20 items + if (index is 20) + break; + + // Exclude folders + if (pShellItem.Get()->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var attribute) == HRESULT.S_OK && + (attribute & SFGAO_FLAGS.SFGAO_FOLDER) == SFGAO_FLAGS.SFGAO_FOLDER) + continue; + + // Get the target path + pShellItem.Get()->GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEEDITING, out var szDisplayName); + var targetPath = szDisplayName.ToString(); + PInvoke.CoTaskMemFree(szDisplayName.Value); + + // Get the display name + pShellItem.Get()->GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY, out szDisplayName); + var fileName = szDisplayName.ToString(); + PInvoke.CoTaskMemFree(szDisplayName.Value); + + // Strip the file extension except when the file name only contains extension (e.g. ".gitignore") + if (!FoldersSettingsService.ShowFileExtensions && + SystemIO.Path.GetFileNameWithoutExtension(fileName) is string fileNameWithoutExtension) + fileName = string.IsNullOrEmpty(fileNameWithoutExtension) ? SystemIO.Path.GetFileName(fileName) : fileNameWithoutExtension; + + // Get the date last modified + var shellItem2Iid = typeof(IShellItem2).GUID; + using ComPtr pShellItem2 = default; + hr = pShellItem.Get()->QueryInterface(&shellItem2Iid, (void**)pShellItem2.GetAddressOf()); + hr = PInvoke.PSGetPropertyKeyFromName("System.DateModified", out var propertyKey); + hr = pShellItem2.Get()->GetString(propertyKey, out var szPropertyValue); + if (DateTime.TryParse(szPropertyValue.ToString(), out var lastModified)) + lastModified = DateTime.MinValue; + + recentItems.Add(new() + { + Path = targetPath, + Name = fileName, + ShellItem = pShellItem, + LastModified = lastModified, + }); + + index++; + } + + if (recentItems.Count is 0) + return false; + + // Sort by the display name + var orderedRecentItems = recentItems.OrderBy(x => x.Name).ToList(); + + var snapshot = isFolder ? RecentFolders : RecentFiles; + + if (isFolder) + { + lock (_RecentFolders) + { + _RecentFolders.Clear(); + _RecentFolders.AddRange(orderedRecentItems); + } + } + else + { + lock (_RecentFiles) + { + _RecentFiles.Clear(); + _RecentFiles.AddRange(orderedRecentItems); + } + } + + var eventArgs = GetChangedActionEventArgs(snapshot, orderedRecentItems); + + if (isFolder) + RecentFoldersChanged?.Invoke(this, eventArgs); + else + RecentFilesChanged?.Invoke(this, eventArgs); + + return true; + } + catch + { + return false; + } + } + + private void Watcher_Changed(object sender, SystemIO.FileSystemEventArgs e) + { + _ = UpdateRecentFilesAsync(); + _ = UpdateRecentFoldersAsync(); + } + + private NotifyCollectionChangedEventArgs GetChangedActionEventArgs(IReadOnlyList oldItems, IList newItems) + { + if (oldItems.Count - newItems.Count is 1) + { + var differences = newItems.Except(oldItems); + if (differences.Take(2).Count() == 1) + return new(NotifyCollectionChangedAction.Add, newItems.First()); + } + else if (newItems.Count - oldItems.Count is 1) + { + var differences = oldItems.Except(newItems); + if (differences.Take(2).Count() == 1) + { + for (int i = 0; i < oldItems.Count; i++) + { + if (i >= newItems.Count || !newItems[i].Equals(oldItems[i])) + return new(NotifyCollectionChangedAction.Remove, oldItems[i], index: i); + } + } + } + else if (newItems.Count - oldItems.Count is 0) + { + var differences = oldItems.Except(newItems); + if (differences.Any()) + return new(NotifyCollectionChangedAction.Reset); + + // First diff from reversed is the designated item + for (int i = oldItems.Count - 1; i >= 0; i--) + { + if (!oldItems[i].Equals(newItems[i])) + return new(NotifyCollectionChangedAction.Move, oldItems[i], index: 0, oldIndex: i); + } + } + + return new(NotifyCollectionChangedAction.Reset); + } + } +} diff --git a/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml b/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml index 660abcc1feff..84ca70ff8014 100644 --- a/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml +++ b/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml @@ -4,9 +4,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dataitems="using:Files.App.Data.Items" xmlns:helpers="using:Files.App.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:recent="using:Files.App.Utils.RecentItem" DataContext="{x:Bind ViewModel, Mode=OneWay}" mc:Ignorable="d"> @@ -46,28 +46,29 @@ SelectionMode="None"> - + + ToolTipService.ToolTip="{x:Bind Path}"> + diff --git a/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs b/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs index 4ae597f560e0..c7b0d1e338b3 100644 --- a/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs +++ b/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs @@ -23,7 +23,7 @@ private void RecentFilesListView_ItemClick(object sender, ItemClickEventArgs e) if (e.ClickedItem is not RecentItem item) return; - ViewModel.NavigateToPath(item.RecentPath); + ViewModel.NavigateToPath(item.Path); } private void RecentFilesListView_RightTapped(object sender, RightTappedRoutedEventArgs e) diff --git a/src/Files.App/Utils/RecentItem/RecentItem.cs b/src/Files.App/Utils/RecentItem/RecentItem.cs deleted file mode 100644 index 42ad32922cc4..000000000000 --- a/src/Files.App/Utils/RecentItem/RecentItem.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using Microsoft.UI.Xaml.Media.Imaging; - -namespace Files.App.Utils.RecentItem -{ - public sealed class RecentItem : WidgetCardItem, IEquatable - { - private BitmapImage _fileImg; - public BitmapImage FileImg - { - get => _fileImg; - set => SetProperty(ref _fileImg, value); - } - public string LinkPath { get; set; } // path of shortcut item (this is unique) - public string RecentPath { get; set; } // path to target item - public string Name { get; set; } - public DateTime LastModified { get; set; } - public byte[] PIDL { get; set; } - public override string Path => RecentPath; - - public RecentItem() - { - - } - - /// - /// Create a RecentItem instance from a link path. - /// This is usually needed if a shortcut is deleted -- the metadata is lost (i.e. the target item). - /// - /// The location that shortcut lives/lived in - public RecentItem(string linkPath) : base() - { - LinkPath = linkPath; - } - - /// - /// Create a RecentItem from a ShellLinkItem (usually from shortcuts in `Windows\Recent`) - /// - public RecentItem(ShellLinkItem linkItem, bool showFileExtension) : base() - { - LinkPath = linkItem.FilePath; - RecentPath = linkItem.TargetPath; - Name = showFileExtension ? linkItem.FileName : NameOrPathWithoutExtension(linkItem.FileName); - LastModified = linkItem.ModifiedDate; - PIDL = linkItem.PIDL; - } - - /// - /// Create a RecentItem from a ShellFileItem (usually from enumerating Quick Access directly). - /// - /// The shell file item - public RecentItem(ShellFileItem fileItem, bool showFileExtension) : base() - { - LinkPath = ShellStorageFolder.IsShellPath(fileItem.FilePath) ? fileItem.RecyclePath : fileItem.FilePath; // use true path on disk for shell items - RecentPath = LinkPath; // intentionally the same - Name = showFileExtension ? fileItem.FileName : NameOrPathWithoutExtension(fileItem.FileName); - LastModified = fileItem.ModifiedDate; - PIDL = fileItem.PIDL; - } - - public async Task LoadRecentItemIconAsync() - { - var result = await FileThumbnailHelper.GetIconAsync( - RecentPath, - Constants.ShellIconSizes.Small, - false, - IconOptions.UseCurrentScale); - - var bitmapImage = await result.ToBitmapAsync(); - if (bitmapImage is not null) - FileImg = bitmapImage; - } - - /// - /// Test equality for generic collection methods such as Remove(...) - /// - public bool Equals(RecentItem other) - { - if (other is null) - { - return false; - } - - // do not include LastModified or anything else here; otherwise, Remove(...) will fail since we lose metadata on deletion! - // when constructing a RecentItem from a deleted link, the only thing we have is the LinkPath (where the link use to be) - return LinkPath == other.LinkPath && - RecentPath == other.RecentPath; - } - - public override int GetHashCode() => (LinkPath, RecentPath).GetHashCode(); - public override bool Equals(object? o) => o is RecentItem other && Equals(other); - - /** - * Strips a name from an extension while aware of some edge cases. - * - * example.min.js => example.min - * example.js => example - * .gitignore => .gitignore - */ - private static string NameOrPathWithoutExtension(string nameOrPath) - { - string strippedExtension = System.IO.Path.GetFileNameWithoutExtension(nameOrPath); - return string.IsNullOrEmpty(strippedExtension) ? System.IO.Path.GetFileName(nameOrPath) : strippedExtension; - } - } -} diff --git a/src/Files.App/Utils/RecentItem/RecentItems.cs b/src/Files.App/Utils/RecentItem/RecentItems.cs deleted file mode 100644 index a3b67a5a54b3..000000000000 --- a/src/Files.App/Utils/RecentItem/RecentItems.cs +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using Microsoft.Extensions.Logging; -using Microsoft.Win32; -using System.Collections.Specialized; -using System.IO; -using Vanara.PInvoke; -using Vanara.Windows.Shell; - -namespace Files.App.Utils.RecentItem -{ - public sealed class RecentItems : IDisposable - { - private const string QuickAccessGuid = "::{679f85cb-0220-4080-b29b-5540cc05aab6}"; - - public EventHandler? RecentFilesChanged; - public EventHandler? RecentFoldersChanged; - - // recent files - private readonly List recentFiles = []; - public IReadOnlyList RecentFiles // already sorted - { - get - { - lock (recentFiles) - { - return recentFiles.ToList().AsReadOnly(); - } - } - } - - // recent folders - private readonly List recentFolders = []; - public IReadOnlyList RecentFolders // already sorted - { - get - { - lock (recentFolders) - { - return recentFolders.ToList().AsReadOnly(); - } - } - } - - private readonly IUserSettingsService UserSettingsService; - - private bool ShowFileExtensions => UserSettingsService.FoldersSettingsService.ShowFileExtensions; - - - public RecentItems(IUserSettingsService userSettingsService) - { - RecentItemsManager.Default.RecentItemsChanged += OnRecentItemsChangedAsync; - UserSettingsService = userSettingsService; - } - - private async void OnRecentItemsChangedAsync(object? sender, EventArgs e) - { - await UpdateRecentFilesAsync(); - } - - /// - /// Refetch recent files to `recentFiles`. - /// - public async Task UpdateRecentFilesAsync() - { - // enumerate with fulltrust process - List enumeratedFiles = await ListRecentFilesAsync(); - if (enumeratedFiles is not null) - { - var recentFilesSnapshot = RecentFiles; - - lock (recentFiles) - { - recentFiles.Clear(); - recentFiles.AddRange(enumeratedFiles); - // do not sort here, enumeration order *is* the correct order since we get it from Quick Access - } - - var changedActionEventArgs = GetChangedActionEventArgs(recentFilesSnapshot, enumeratedFiles); - RecentFilesChanged?.Invoke(this, changedActionEventArgs); - } - } - - /// - /// Refetch recent folders to `recentFolders`. - /// - public async Task UpdateRecentFoldersAsync() - { - var enumeratedFolders = await Task.Run(ListRecentFoldersAsync); // run off the UI thread - if (enumeratedFolders is not null) - { - lock (recentFolders) - { - recentFolders.Clear(); - recentFolders.AddRange(enumeratedFolders); - - // shortcut modifications in `Windows\Recent` consist of a delete + add operation; - // thus, last modify date is reset and we can sort off it - recentFolders.Sort((x, y) => y.LastModified.CompareTo(x.LastModified)); - } - - RecentFoldersChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - } - - /// - /// Enumerate recently accessed files via `Quick Access`. - /// - public async Task> ListRecentFilesAsync() - { - // Since the maximum number of recent files is 20, we set the count to 20 to avoid loading of unnecessary shell items. - return (await Win32Helper.GetShellFolderAsync(QuickAccessGuid, false, true, 0, 20)).Enumerate - .Where(link => !link.IsFolder) - .Select(link => new RecentItem(link, ShowFileExtensions)).ToList(); - } - - /// - /// Enumerate recently accessed folders via `Windows\Recent`. - /// - public async Task> ListRecentFoldersAsync() - { - var excludeMask = FileAttributes.Hidden; - var linkFilePaths = Directory.EnumerateFiles(Constants.UserEnvironmentPaths.RecentItemsPath).Where(f => (new FileInfo(f).Attributes & excludeMask) == 0); - - Task GetRecentItemFromLink(string linkPath) - { - return Task.Run(() => - { - try - { - using var link = new ShellLink(linkPath, LinkResolution.NoUIWithMsgPump, default, TimeSpan.FromMilliseconds(100)); - - if (!string.IsNullOrEmpty(link.TargetPath) && link.Target.IsFolder) - { - var shellLinkItem = ShellFolderExtensions.GetShellLinkItem(link); - return new RecentItem(shellLinkItem, ShowFileExtensions); - } - } - catch (FileNotFoundException) - { - // occurs when shortcut or shortcut target is deleted and accessed (link.Target) - // consequently, we shouldn't include the item as a recent item - } - catch (Exception ex) - { - // other error (usually a COMException) - } - - return null; - }); - } - - var recentItems = await Task.WhenAll(linkFilePaths.Select(GetRecentItemFromLink)); - return recentItems.OfType().ToList(); - } - - /// - /// Adds a shortcut to `Windows\Recent`. The path can be to a file or folder. - /// It will update to `recentFiles` or `recentFolders` respectively. - /// - /// Path to a file or folder - /// Whether the action was successfully handled or not - public bool AddToRecentItems(string path) - { - try - { - Shell32.SHAddToRecentDocs(Shell32.SHARD.SHARD_PATHW, path); - return true; - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, ex.Message); - return false; - } - } - - /// - /// Clears both `recentFiles` and `recentFolders`. - /// This will also clear the Recent Files (and its jumplist) in File Explorer. - /// - /// Whether the action was successfully handled or not - public bool ClearRecentItems() - { - try - { - Shell32.SHAddToRecentDocs(Shell32.SHARD.SHARD_PIDL, (string)null); - return true; - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, ex.Message); - return false; - } - } - - /// - /// Unpin (or remove) a file from `recentFiles`. - /// This will also unpin the item from the Recent Files in File Explorer. - /// - /// Whether the action was successfully handled or not - public Task UnpinFromRecentFiles(RecentItem item) - { - return SafetyExtensions.IgnoreExceptions(() => Task.Run(async () => - { - using var pidl = new Shell32.PIDL(item.PIDL); - using var shellItem = ShellItem.Open(pidl); - using var cMenu = await ContextMenu.GetContextMenuForFiles(new[] { shellItem }, Shell32.CMF.CMF_NORMAL); - if (cMenu is not null) - return await cMenu.InvokeVerb("remove"); - return false; - })); - } - - private NotifyCollectionChangedEventArgs GetChangedActionEventArgs(IReadOnlyList oldItems, IList newItems) - { - // a single item was added - if (newItems.Count == oldItems.Count + 1) - { - var differences = newItems.Except(oldItems); - if (differences.Take(2).Count() == 1) - { - return new(NotifyCollectionChangedAction.Add, newItems.First()); - } - } - // a single item was removed - else if (newItems.Count == oldItems.Count - 1) - { - var differences = oldItems.Except(newItems); - if (differences.Take(2).Count() == 1) - { - for (int i = 0; i < oldItems.Count; i++) - { - if (i >= newItems.Count || !newItems[i].Equals(oldItems[i])) - { - return new(NotifyCollectionChangedAction.Remove, oldItems[i], index: i); - } - } - } - } - // a single item was moved - else if (newItems.Count == oldItems.Count) - { - var differences = oldItems.Except(newItems); - // desync due to skipped/batched calls, reset the list - if (differences.Any()) - { - return new(NotifyCollectionChangedAction.Reset); - } - - // first diff from reversed is the designated item - for (int i = oldItems.Count - 1; i >= 0; i--) - { - if (!oldItems[i].Equals(newItems[i])) - { - return new(NotifyCollectionChangedAction.Move, oldItems[i], index: 0, oldIndex: i); - } - } - } - - return new(NotifyCollectionChangedAction.Reset); - } - - public bool CheckIsRecentFilesEnabled() - { - using var subkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer"); - using var advSubkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"); - using var userPolicySubkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer"); - using var sysPolicySubkey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer"); - - if (subkey is not null) - { - // quick access: show recent files option - bool showRecentValue = Convert.ToBoolean(subkey.GetValue("ShowRecent", true)); // 1 by default - if (!showRecentValue) - { - return false; - } - } - - if (advSubkey is not null) - { - // settings: personalization > start > show recently opened items - bool startTrackDocsValue = Convert.ToBoolean(advSubkey.GetValue("Start_TrackDocs", true)); // 1 by default - if (!startTrackDocsValue) - { - return false; - } - } - - // for users in group policies - var policySubkey = userPolicySubkey ?? sysPolicySubkey; - if (policySubkey is not null) - { - bool noRecentDocsHistoryValue = Convert.ToBoolean(policySubkey.GetValue("NoRecentDocsHistory", false)); // 0 by default - if (noRecentDocsHistoryValue) - { - return false; - } - } - - return true; - } - - public void Dispose() - { - RecentItemsManager.Default.RecentItemsChanged -= OnRecentItemsChangedAsync; - } - } -} \ No newline at end of file diff --git a/src/Files.App/Utils/RecentItem/RecentItemsManager.cs b/src/Files.App/Utils/RecentItem/RecentItemsManager.cs deleted file mode 100644 index 8e5e4faf25a8..000000000000 --- a/src/Files.App/Utils/RecentItem/RecentItemsManager.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using System.IO; - -namespace Files.App.Utils.RecentItem -{ - public sealed class RecentItemsManager - { - private static readonly Lazy lazy = new(() => new RecentItemsManager()); - private static readonly string recentItemsPath = Environment.GetFolderPath(Environment.SpecialFolder.Recent); - private static readonly string automaticDestinationsPath = Path.Combine(recentItemsPath, "AutomaticDestinations"); - private const string QuickAccessJumpListFileName = "5f7b5f1e01b83767.automaticDestinations-ms"; - private DateTime quickAccessLastReadTime = DateTime.MinValue; - private FileSystemWatcher? quickAccessJumpListWatcher; - - public event EventHandler? RecentItemsChanged; - - public static RecentItemsManager Default - { - get - { - return lazy.Value; - } - } - - private RecentItemsManager() - { - Initialize(); - } - - private void Initialize() - { - StartQuickAccessJumpListWatcher(); - } - - private void StartQuickAccessJumpListWatcher() - { - if (quickAccessJumpListWatcher is not null) - { - return; - } - - quickAccessJumpListWatcher = new FileSystemWatcher - { - Path = automaticDestinationsPath, - Filter = QuickAccessJumpListFileName, - NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastWrite, - }; - quickAccessJumpListWatcher.Changed += QuickAccessJumpList_Changed; - quickAccessJumpListWatcher.Deleted += QuickAccessJumpList_Changed; - quickAccessJumpListWatcher.EnableRaisingEvents = true; - } - - private void QuickAccessJumpList_Changed(object sender, FileSystemEventArgs e) - { - System.Diagnostics.Debug.WriteLine($"{nameof(QuickAccessJumpList_Changed)}: {e.ChangeType}, {e.FullPath}"); - - // skip if multiple events occurred for singular change - var lastWriteTime = File.GetLastWriteTime(e.FullPath); - if (quickAccessLastReadTime >= lastWriteTime) - { - return; - } - else - { - quickAccessLastReadTime = lastWriteTime; - } - - RecentItemsChanged?.Invoke(this, e); - } - - private void Unregister() - { - quickAccessJumpListWatcher?.Dispose(); - } - - ~RecentItemsManager() - { - Unregister(); - } - } -} diff --git a/src/Files.App/ViewModels/UserControls/Widgets/BaseWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/BaseWidgetViewModel.cs index cec3cb3398c5..e6219896d722 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/BaseWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/BaseWidgetViewModel.cs @@ -17,6 +17,7 @@ public abstract class BaseWidgetViewModel : ObservableObject { // Dependency injections + protected IWindowsRecentItemsService WindowsRecentItemsService { get; } = Ioc.Default.GetRequiredService(); protected IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); protected IQuickAccessService QuickAccessService { get; } = Ioc.Default.GetRequiredService(); protected IStorageService StorageService { get; } = Ioc.Default.GetRequiredService(); diff --git a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs index c81a7efcd9bb..df279a8e50da 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml.Controls; +using Microsoft.Win32; using System.Collections.Specialized; using System.IO; using Windows.Foundation.Metadata; @@ -54,7 +55,7 @@ public RecentFilesWidgetViewModel() // recent files could have changed while widget wasn't loaded _ = RefreshWidgetAsync(); - App.RecentItemsManager.RecentFilesChanged += Manager_RecentFilesChanged; + WindowsRecentItemsService.RecentFilesChanged += Manager_RecentFilesChanged; RemoveRecentItemCommand = new AsyncRelayCommand(ExecuteRemoveRecentItemCommand); ClearAllItemsCommand = new AsyncRelayCommand(ExecuteClearRecentItemsCommand); @@ -66,10 +67,11 @@ public RecentFilesWidgetViewModel() public async Task RefreshWidgetAsync() { - IsRecentFilesDisabledInWindows = App.RecentItemsManager.CheckIsRecentFilesEnabled() is false; - await App.RecentItemsManager.UpdateRecentFilesAsync(); + IsRecentFilesDisabledInWindows = !CheckIsRecentItemsEnabled(); + await WindowsRecentItemsService.UpdateRecentFilesAsync(); } + public override List GetItemMenuItems(WidgetCardItem item, bool isPinned, bool isFolder = false) { return new List() @@ -178,7 +180,7 @@ private async Task UpdateRecentFilesListAsync(NotifyCollectionChangedEventArgs e // case NotifyCollectionChangedAction.Reset: default: - var recentFiles = App.RecentItemsManager.RecentFiles; // already sorted, add all in order + var recentFiles = WindowsRecentItemsService.RecentFiles; // already sorted, add all in order if (!recentFiles.SequenceEqual(Items)) { Items.Clear(); @@ -232,6 +234,22 @@ public void NavigateToPath(string path) catch (Exception) { } } + public bool CheckIsRecentItemsEnabled() + { + using var explorerSubKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer"); + using var advSubkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"); + using var userPolicySubkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer"); + using var sysPolicySubkey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer"); + var policySubkey = userPolicySubkey ?? sysPolicySubkey; + + if (Convert.ToBoolean(explorerSubKey?.GetValue("ShowRecent", true)) && + Convert.ToBoolean(advSubkey?.GetValue("Start_TrackDocs", true)) && + !Convert.ToBoolean(policySubkey?.GetValue("NoRecentDocsHistory", false))) + return true; + + return false; + } + // Event methods private async void Manager_RecentFilesChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -254,7 +272,10 @@ private async Task ExecuteRemoveRecentItemCommand(RecentItem? item) try { - await App.RecentItemsManager.UnpinFromRecentFiles(item); + await Task.Run(() => + { + return WindowsRecentItemsService.Remove(item); + }); } finally { @@ -265,13 +286,10 @@ private async Task ExecuteRemoveRecentItemCommand(RecentItem? item) private async Task ExecuteClearRecentItemsCommand() { await _refreshRecentFilesSemaphore.WaitAsync(); + try { - Items.Clear(); - bool success = App.RecentItemsManager.ClearRecentItems(); - - if (success) - IsEmptyRecentFilesTextVisible = true; + WindowsRecentItemsService.Clear(); } finally { @@ -284,8 +302,8 @@ private void ExecuteOpenFileLocationCommand(RecentItem? item) if (item is null) return; - var itemPath = Directory.GetParent(item.RecentPath)?.FullName ?? string.Empty; - var itemName = Path.GetFileName(item.RecentPath); + var itemPath = Directory.GetParent(item.Path)?.FullName ?? string.Empty; + var itemName = Path.GetFileName(item.Path); ContentPageContext.ShellPage!.NavigateWithArguments( ContentPageContext.ShellPage!.InstanceViewModel.FolderSettings.GetLayoutType(itemPath), @@ -338,7 +356,10 @@ private void ExecuteOpenPropertiesCommand(RecentItem? item) public void Dispose() { - App.RecentItemsManager.RecentFilesChanged -= Manager_RecentFilesChanged; + WindowsRecentItemsService.RecentFilesChanged -= Manager_RecentFilesChanged; + + foreach (var item in Items) + item.Dispose(); } } } From ed1d43eecc1e296826f406afac7c7a1f405218e1 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:02:37 +0900 Subject: [PATCH 2/6] Update WindowsRecentItemsService.cs --- src/Files.App/Services/Windows/WindowsRecentItemsService.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs index cd031a1e054c..a56f4a3d40d8 100644 --- a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs +++ b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs @@ -238,9 +238,6 @@ private unsafe bool UpdateRecentItems(bool isFolder) if (recentItems.Count is 0) return false; - // Sort by the display name - var orderedRecentItems = recentItems.OrderBy(x => x.Name).ToList(); - var snapshot = isFolder ? RecentFolders : RecentFiles; if (isFolder) From 9b458566db08dad600d242eda4f5a9de1de8b83a Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:50:14 +0900 Subject: [PATCH 3/6] Update WindowsRecentItemsService.cs --- src/Files.App/Services/Windows/WindowsRecentItemsService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs index a56f4a3d40d8..8b1ba4253578 100644 --- a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs +++ b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs @@ -245,7 +245,7 @@ private unsafe bool UpdateRecentItems(bool isFolder) lock (_RecentFolders) { _RecentFolders.Clear(); - _RecentFolders.AddRange(orderedRecentItems); + _RecentFolders.AddRange(recentItems); } } else @@ -253,11 +253,11 @@ private unsafe bool UpdateRecentItems(bool isFolder) lock (_RecentFiles) { _RecentFiles.Clear(); - _RecentFiles.AddRange(orderedRecentItems); + _RecentFiles.AddRange(recentItems); } } - var eventArgs = GetChangedActionEventArgs(snapshot, orderedRecentItems); + var eventArgs = GetChangedActionEventArgs(snapshot, recentItems); if (isFolder) RecentFoldersChanged?.Invoke(this, eventArgs); From aca8a59817ecada86d333ef57ca3b28f1b87f3f5 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:02:35 +0900 Subject: [PATCH 4/6] Update WindowsRecentItemsService.cs --- .../Services/Windows/WindowsRecentItemsService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs index 8b1ba4253578..60467d70476c 100644 --- a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs +++ b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs @@ -133,7 +133,7 @@ public unsafe bool Remove(RecentItem item) hr = pContextMenu.Get()->InvokeCommand(cmi); // Remove recent files - fixed (byte* pVerb = Encoding.ASCII.GetBytes("remove")) + fixed (byte* pVerb = Encoding.ASCII.GetBytes("removefromhome")) cmi.lpVerb = new(pVerb); hr = pContextMenu.Get()->InvokeCommand(cmi); @@ -280,13 +280,13 @@ private void Watcher_Changed(object sender, SystemIO.FileSystemEventArgs e) private NotifyCollectionChangedEventArgs GetChangedActionEventArgs(IReadOnlyList oldItems, IList newItems) { - if (oldItems.Count - newItems.Count is 1) + if (newItems.Count - oldItems.Count is 1) { var differences = newItems.Except(oldItems); if (differences.Take(2).Count() == 1) return new(NotifyCollectionChangedAction.Add, newItems.First()); } - else if (newItems.Count - oldItems.Count is 1) + else if (oldItems.Count - newItems.Count is 1) { var differences = oldItems.Except(newItems); if (differences.Take(2).Count() == 1) @@ -298,7 +298,7 @@ private NotifyCollectionChangedEventArgs GetChangedActionEventArgs(IReadOnlyList } } } - else if (newItems.Count - oldItems.Count is 0) + else if (newItems.Count == oldItems.Count) { var differences = oldItems.Except(newItems); if (differences.Any()) From 968d8eec27a8eded2b994789021266795572dfdf Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:20:36 +0900 Subject: [PATCH 5/6] Update WindowsRecentItemsService.cs --- .../Windows/WindowsRecentItemsService.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs index 60467d70476c..57c78b31159d 100644 --- a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs +++ b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs @@ -127,15 +127,32 @@ public unsafe bool Remove(RecentItem item) cmi.cbSize = (uint)sizeof(CMINVOKECOMMANDINFO); cmi.nShow = (int)SHOW_WINDOW_CMD.SW_HIDE; - // Try unpin first for pinned files - fixed (byte* pVerb = Encoding.ASCII.GetBytes("unpinfromhome")) - cmi.lpVerb = new(pVerb); - hr = pContextMenu.Get()->InvokeCommand(cmi); - - // Remove recent files - fixed (byte* pVerb = Encoding.ASCII.GetBytes("removefromhome")) - cmi.lpVerb = new(pVerb); - hr = pContextMenu.Get()->InvokeCommand(cmi); + // Unpin the item + fixed (byte* pVerb1 = Encoding.ASCII.GetBytes("remove"), + pVerb2 = Encoding.ASCII.GetBytes("unpinfromhome"), + pVerb3 = Encoding.ASCII.GetBytes("removefromhome")) + { + // Try unpin files + cmi.lpVerb = new(pVerb1); + hr = pContextMenu.Get()->InvokeCommand(cmi); + if (hr == HRESULT.S_OK) + return; + + // Try unpin folders + cmi.lpVerb = new(pVerb2); + hr = pContextMenu.Get()->InvokeCommand(cmi); + if (hr == HRESULT.S_OK) + return; + + // NOTE: + // There seems to be an issue with unpinfromhome where some shell folders + // won't be removed via unpinfromhome verb. + // Try unpin folders again + cmi.lpVerb = new(pVerb3); + hr = pContextMenu.Get()->InvokeCommand(cmi); + if (hr == HRESULT.S_OK) + return; + } return true; } From 8ea304b7d05d335396827011753a85e31beb9d3f Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:31:25 +0900 Subject: [PATCH 6/6] Update WindowsRecentItemsService.cs --- src/Files.App/Services/Windows/WindowsRecentItemsService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs index 57c78b31159d..3b243708242b 100644 --- a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs +++ b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs @@ -136,13 +136,13 @@ public unsafe bool Remove(RecentItem item) cmi.lpVerb = new(pVerb1); hr = pContextMenu.Get()->InvokeCommand(cmi); if (hr == HRESULT.S_OK) - return; + return true; // Try unpin folders cmi.lpVerb = new(pVerb2); hr = pContextMenu.Get()->InvokeCommand(cmi); if (hr == HRESULT.S_OK) - return; + return true; // NOTE: // There seems to be an issue with unpinfromhome where some shell folders @@ -151,7 +151,7 @@ public unsafe bool Remove(RecentItem item) cmi.lpVerb = new(pVerb3); hr = pContextMenu.Get()->InvokeCommand(cmi); if (hr == HRESULT.S_OK) - return; + return true; } return true;