diff --git a/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs b/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs index 419af421f7fc..f5e0f9ed44d5 100644 --- a/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs +++ b/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs @@ -173,7 +173,6 @@ public static async Task CreateFromPropertiesAsync(StorageFolder root ShowProperties = true }; item.Path = string.IsNullOrEmpty(root.Path) ? $"\\\\?\\{root.Name}\\" : root.Path; - App.Logger.Warn(item.Path); item.DeviceID = deviceId; item.Root = root; diff --git a/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs b/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs index c2311f86781d..e76d4e28635b 100644 --- a/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs +++ b/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs @@ -72,6 +72,8 @@ public bool IsExpanded public ContextMenuOptions MenuOptions { get; set; } + public bool IsHeader { get; set; } + public int CompareTo(INavigationControlItem other) => Text.CompareTo(other.Text); diff --git a/src/Files.App/DataModels/SidebarPinnedModel.cs b/src/Files.App/DataModels/SidebarPinnedModel.cs index df3c90aa19f3..c5c27992cb92 100644 --- a/src/Files.App/DataModels/SidebarPinnedModel.cs +++ b/src/Files.App/DataModels/SidebarPinnedModel.cs @@ -30,7 +30,7 @@ public class SidebarPinnedModel public List FavoriteItems { get; set; } = new List(); - private readonly List favoriteList = new(); + public readonly List favoriteList = new(); [JsonIgnore] public IReadOnlyList Favorites @@ -205,6 +205,8 @@ public async void LoadAsync(object? sender, FileSystemEventArgs e) } public async Task LoadAsync() - => await UpdateItemsWithExplorer(); + { + await UpdateItemsWithExplorer(); + } } } diff --git a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml new file mode 100644 index 000000000000..836eb11bc81c --- /dev/null +++ b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs new file mode 100644 index 000000000000..ab3533fc471e --- /dev/null +++ b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs @@ -0,0 +1,96 @@ +using CommunityToolkit.WinUI.UI; +using Files.App.DataModels.NavigationControlItems; +using Files.App.Extensions; +using Files.App.Filesystem; +using Files.App.ServicesImplementation; +using Files.App.ViewModels.Dialogs; +using Files.Backend.ViewModels.Dialogs; +using Files.Shared.Enums; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; + +namespace Files.App.Dialogs +{ + public sealed partial class ReorderSidebarItemsDialog : ContentDialog, IDialog + { + public ReorderSidebarItemsDialogViewModel ViewModel + { + get => (ReorderSidebarItemsDialogViewModel)DataContext; + set => DataContext = value; + } + + public ReorderSidebarItemsDialog() + { + InitializeComponent(); + } + + private async void MoveItem(object sender, PointerRoutedEventArgs e) + { + var properties = e.GetCurrentPoint(null).Properties; + var icon = sender as FontIcon; + if (!properties.IsLeftButtonPressed) + return; + + var navItem = icon?.FindAscendant(); + if (navItem is not null) + await navItem.StartDragAsync(e.GetCurrentPoint(navItem)); + } + + private void ListViewItem_DragStarting(object sender, DragStartingEventArgs e) + { + if (sender is not Grid nav || nav.DataContext is not LocationItem) + return; + + // Adding the original Location item dragged to the DragEvents data view + e.Data.Properties.Add("sourceLocationItem", nav); + e.AllowedOperations = DataPackageOperation.Move; + } + + + private void ListViewItem_DragOver(object sender, DragEventArgs e) + { + if ((sender as Grid)?.DataContext is not LocationItem locationItem) + return; + var deferral = e.GetDeferral(); + + if ((e.DataView.Properties["sourceLocationItem"] as Grid)?.DataContext is LocationItem sourceLocationItem) + { + DragOver_SetCaptions(sourceLocationItem, locationItem, e); + } + + deferral.Complete(); + } + + private void DragOver_SetCaptions(LocationItem senderLocationItem, LocationItem sourceLocationItem, DragEventArgs e) + { + // If the location item is the same as the original dragged item + if (sourceLocationItem.CompareTo(senderLocationItem) == 0) + { + e.AcceptedOperation = DataPackageOperation.None; + e.DragUIOverride.IsCaptionVisible = false; + } + else + { + e.DragUIOverride.IsCaptionVisible = true; + e.DragUIOverride.Caption = "MoveItemsDialogPrimaryButtonText".GetLocalizedResource(); + e.AcceptedOperation = DataPackageOperation.Move; + } + } + + private void ListViewItem_Drop(object sender, DragEventArgs e) + { + if (sender is not Grid navView || navView.DataContext is not LocationItem locationItem) + return; + + if ((e.DataView.Properties["sourceLocationItem"] as Grid)?.DataContext is LocationItem sourceLocationItem) + ViewModel.SidebarFavoriteItems.Move(ViewModel.SidebarFavoriteItems.IndexOf(sourceLocationItem), ViewModel.SidebarFavoriteItems.IndexOf(locationItem)); + } + + public new async Task ShowAsync() => (DialogResult)await base.ShowAsync(); + } +} \ No newline at end of file diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj index 626de7a047ac..77cc7905b056 100644 --- a/src/Files.App/Files.App.csproj +++ b/src/Files.App/Files.App.csproj @@ -129,4 +129,4 @@ $(DefaultXamlRuntime) - \ No newline at end of file + diff --git a/src/Files.App/ServicesImplementation/DialogService.cs b/src/Files.App/ServicesImplementation/DialogService.cs index 9d6c7f619252..40cfce2fbd7b 100644 --- a/src/Files.App/ServicesImplementation/DialogService.cs +++ b/src/Files.App/ServicesImplementation/DialogService.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Threading.Tasks; using Windows.Foundation.Metadata; @@ -29,7 +30,8 @@ public DialogService() { typeof(FileSystemDialogViewModel), () => new FilesystemOperationDialog() }, { typeof(DecompressArchiveDialogViewModel), () => new DecompressArchiveDialog() }, { typeof(SettingsDialogViewModel), () => new SettingsDialog() }, - { typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() } + { typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() }, + { typeof(ReorderSidebarItemsDialogViewModel), () => new ReorderSidebarItemsDialog() } }; } @@ -62,7 +64,9 @@ public Task ShowDialogAsync(TViewModel viewModel) } catch (Exception ex) { - _ = ex; + App.Logger.Warn(ex, "Failed to show dialog"); + + Debugger.Break(); } return Task.FromResult(DialogResult.None); diff --git a/src/Files.App/ServicesImplementation/QuickAccessService.cs b/src/Files.App/ServicesImplementation/QuickAccessService.cs index f0102e262ce5..2d8e2e81dbcd 100644 --- a/src/Files.App/ServicesImplementation/QuickAccessService.cs +++ b/src/Files.App/ServicesImplementation/QuickAccessService.cs @@ -1,12 +1,9 @@ using Files.App.Shell; using Files.App.UserControls.Widgets; -using Files.Sdk.Storage.LocatableStorage; using Files.Shared; using Files.Shared.Extensions; -using Microsoft.UI.Xaml.Shapes; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; @@ -23,19 +20,24 @@ public async Task> GetPinnedFoldersAsync() } public Task PinToSidebar(string folderPath) - => PinToSidebar(new[] { folderPath }); + { + return PinToSidebar(new[] { folderPath }); + } public async Task PinToSidebar(string[] folderPaths) { - await ContextMenu.InvokeVerb("pintohome", folderPaths); + foreach (string folderPath in folderPaths) + await ContextMenu.InvokeVerb("pintohome", new[] {folderPath}); await App.QuickAccessManager.Model.LoadAsync(); App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, new ModifyQuickAccessEventArgs(folderPaths, true)); } - + public Task UnpinFromSidebar(string folderPath) - => UnpinFromSidebar(new[] { folderPath }); + { + return UnpinFromSidebar(new[] { folderPath }); + } public async Task UnpinFromSidebar(string[] folderPaths) { @@ -43,12 +45,22 @@ public async Task UnpinFromSidebar(string[] folderPaths) object? shell = Activator.CreateInstance(shellAppType); dynamic? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, new object[] { $"shell:{guid}" }); + if (folderPaths.Length == 0) + folderPaths = (await GetPinnedFoldersAsync()) + .Where(link => (bool?)link.Properties["System.Home.IsPinned"] ?? false) + .Select(link => link.FilePath).ToArray(); + foreach (dynamic? fi in f2.Items()) + { if (folderPaths.Contains((string)fi.Path) || (string.Equals(fi.Path, "::{645FF040-5081-101B-9F08-00AA002F954E}") && folderPaths.Contains(Constants.CommonPaths.RecycleBinPath))) - await SafetyExtensions.IgnoreExceptions(async () => { + { + await SafetyExtensions.IgnoreExceptions(async () => + { await fi.InvokeVerb("unpinfromhome"); }); + } + } await App.QuickAccessManager.Model.LoadAsync(); @@ -59,5 +71,20 @@ public bool IsItemPinned(string folderPath) { return App.QuickAccessManager.Model.FavoriteItems.Contains(folderPath); } + + public async Task Save(string[] items) + { + if (Equals(items, App.QuickAccessManager.Model.FavoriteItems.ToArray())) + return; + + App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = false; + + // Unpin every item that is below this index and then pin them all in order + await UnpinFromSidebar(Array.Empty()); + + await PinToSidebar(items); + App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = true; + await App.QuickAccessManager.Model.LoadAsync(); + } } } diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 4af7abd9d79d..644611b4c0d9 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -2595,6 +2595,9 @@ Multiselect + + Reorder sidebar items + Hashes diff --git a/src/Files.App/UserControls/SidebarControl.xaml b/src/Files.App/UserControls/SidebarControl.xaml index 5d7f8ac8e791..029f164753a5 100644 --- a/src/Files.App/UserControls/SidebarControl.xaml +++ b/src/Files.App/UserControls/SidebarControl.xaml @@ -62,7 +62,6 @@ DragEnter="NavigationViewItem_DragEnter" DragLeave="NavigationViewItem_DragLeave" DragOver="NavigationViewLocationItem_DragOver" - DragStarting="NavigationViewItem_DragStarting" Drop="NavigationViewLocationItem_Drop" IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}" IsRightTapEnabled="True" diff --git a/src/Files.App/UserControls/SidebarControl.xaml.cs b/src/Files.App/UserControls/SidebarControl.xaml.cs index 0c05994c9db4..e43947b00bec 100644 --- a/src/Files.App/UserControls/SidebarControl.xaml.cs +++ b/src/Files.App/UserControls/SidebarControl.xaml.cs @@ -12,6 +12,8 @@ using Files.App.ServicesImplementation; using Files.App.Shell; using Files.App.ViewModels; +using Files.App.ViewModels.Dialogs; +using Files.Backend.Services; using Files.Backend.Services.Settings; using Files.Shared.Extensions; using Microsoft.UI.Input; @@ -122,6 +124,8 @@ public UIElement TabContent private ICommand OpenPropertiesCommand { get; } + private ICommand ReorderItemsCommand { get; } + private bool IsInPointerPressed = false; private readonly DispatcherQueueTimer dragOverSectionTimer, dragOverItemTimer; @@ -142,6 +146,7 @@ public SidebarControl() EjectDeviceCommand = new RelayCommand(EjectDevice); FormatDriveCommand = new RelayCommand(FormatDrive); OpenPropertiesCommand = new RelayCommand(OpenProperties); + ReorderItemsCommand = new RelayCommand(ReorderItems); } public SidebarViewModel ViewModel @@ -254,6 +259,13 @@ private List GetLocationItemMenuItems(INavigatio ShowItem = options.ShowUnpinItem || isDriveItemPinned }, new ContextMenuFlyoutItemViewModel() + { + Text = "ReorderSidebarItemsDialogText".GetLocalizedResource(), + Glyph = "\uE8D8", + Command = ReorderItemsCommand, + ShowItem = isFavoriteItem || item.Section is SectionType.Favorites + }, + new ContextMenuFlyoutItemViewModel() { Text = string.Format("SideBarHideSectionFromSideBar/Text".GetLocalizedResource(), rightClickedItem.Text), Glyph = "\uE77A", @@ -331,6 +343,13 @@ private void HideSection() } } + private async void ReorderItems() + { + var dialog = new ReorderSidebarItemsDialogViewModel(); + var dialogService = Ioc.Default.GetRequiredService(); + var result = await dialogService.ShowDialogAsync(dialog); + } + private async void OpenInNewPane() { if (await DriveHelpers.CheckEmptyDrive(rightClickedItem.Path)) @@ -457,15 +476,6 @@ private void NavigationViewItem_RightTapped(object sender, RightTappedRoutedEven e.Handled = true; } - private void NavigationViewItem_DragStarting(UIElement sender, DragStartingEventArgs args) - { - if (sender is not NavigationViewItem navItem || navItem.DataContext is not LocationItem) - return; - - // Adding the original Location item dragged to the DragEvents data view - args.Data.Properties.Add("sourceLocationItem", navItem); - } - private void NavigationViewItem_DragEnter(object sender, DragEventArgs e) { var navView = sender as NavigationViewItem; @@ -611,12 +621,7 @@ private async void NavigationViewLocationItem_DragOver(object sender, DragEventA } CompleteDragEventArgs(e, captionText, operationType); } - } - else if ((e.DataView.Properties["sourceLocationItem"] as NavigationViewItem)?.DataContext is LocationItem sourceLocationItem) - { - // else if the drag over event is called over a location item - NavigationViewLocationItem_DragOver_SetCaptions(locationItem, sourceLocationItem, e); - } + } deferral.Complete(); } @@ -629,20 +634,6 @@ private DragEventArgs CompleteDragEventArgs(DragEventArgs e, string captionText, return e; } - private void NavigationViewLocationItem_DragOver_SetCaptions(LocationItem senderLocationItem, LocationItem sourceLocationItem, DragEventArgs e) - { - // If the location item is the same as the original dragged item - if (sourceLocationItem.Equals(senderLocationItem)) - { - e.AcceptedOperation = DataPackageOperation.None; - e.DragUIOverride.IsCaptionVisible = false; - } - else - { - CompleteDragEventArgs(e, "PinToSidebarByDraggingCaptionText".GetLocalizedResource(), DataPackageOperation.Move); - } - } - private async void NavigationViewLocationItem_Drop(object sender, DragEventArgs e) { if (lockFlag) diff --git a/src/Files.App/ViewModels/Dialogs/ReorderSidebarItemsDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/ReorderSidebarItemsDialogViewModel.cs new file mode 100644 index 000000000000..aec3fc1de364 --- /dev/null +++ b/src/Files.App/ViewModels/Dialogs/ReorderSidebarItemsDialogViewModel.cs @@ -0,0 +1,40 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; +using Files.App.DataModels.NavigationControlItems; +using Files.App.Extensions; +using Files.App.Helpers; +using Files.App.ServicesImplementation; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Files.App.ViewModels.Dialogs +{ + public class ReorderSidebarItemsDialogViewModel : ObservableObject + { + private readonly IQuickAccessService quickAccessService = Ioc.Default.GetRequiredService(); + + public string HeaderText = "ReorderSidebarItemsDialogText".GetLocalizedResource(); + public ICommand PrimaryButtonCommand { get; private set; } + + public ObservableCollection SidebarFavoriteItems = new(App.QuickAccessManager.Model.favoriteList + .Where(x => x is LocationItem loc && loc.Section is Filesystem.SectionType.Favorites && !loc.IsHeader) + .Cast()); + + public ReorderSidebarItemsDialogViewModel() + { + //App.Logger.Warn(string.Join(", ", SidebarFavoriteItems.Select(x => x.Path))); + PrimaryButtonCommand = new RelayCommand(SaveChanges); + } + + public void SaveChanges() + { + quickAccessService.Save(SidebarFavoriteItems.Select(x => x.Path).ToArray()); + } + } +} diff --git a/src/Files.App/ViewModels/SidebarViewModel.cs b/src/Files.App/ViewModels/SidebarViewModel.cs index 4f32d6a931cf..53a6bf301d32 100644 --- a/src/Files.App/ViewModels/SidebarViewModel.cs +++ b/src/Files.App/ViewModels/SidebarViewModel.cs @@ -389,6 +389,7 @@ private async Task CreateSection(SectionType sectionType) section.Path = "Home"; section.Font = App.AppModel.SymbolFontFamily; section.Icon = new BitmapImage(new Uri(Constants.FluentIconsPaths.HomeIcon)); + section.IsHeader = true; break; } @@ -403,6 +404,7 @@ private async Task CreateSection(SectionType sectionType) section = BuildSection("SidebarFavorites".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); section.Font = App.AppModel.SymbolFontFamily; icon = new BitmapImage(new Uri(Constants.FluentIconsPaths.FavoritesIcon)); + section.IsHeader = true; break; } @@ -415,6 +417,7 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("SidebarLibraries".GetLocalizedResource(), sectionType, new ContextMenuOptions { IsLibrariesHeader = true, ShowHideSection = true }, false); iconIdex = Constants.ImageRes.Libraries; + section.IsHeader = true; break; } @@ -427,6 +430,7 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("Drives".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); iconIdex = Constants.ImageRes.ThisPC; + section.IsHeader = true; break; } @@ -439,6 +443,7 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("SidebarCloudDrives".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); icon = new BitmapImage(new Uri(Constants.FluentIconsPaths.CloudDriveIcon)); + section.IsHeader = true; break; } @@ -451,6 +456,7 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("SidebarNetworkDrives".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); iconIdex = Constants.ImageRes.NetworkDrives; + section.IsHeader = true; break; } @@ -463,6 +469,7 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("WSL".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); icon = new BitmapImage(new Uri(Constants.WslIconsPaths.GenericIcon)); + section.IsHeader = true; break; } @@ -475,6 +482,7 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("FileTags".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); icon = new BitmapImage(new Uri(Constants.FluentIconsPaths.FileTagsIcon)); + section.IsHeader = true; break; } diff --git a/src/Files.Backend/Services/IQuickAccessService.cs b/src/Files.Backend/Services/IQuickAccessService.cs index a7b6ef3afc5e..d28f1fa780e9 100644 --- a/src/Files.Backend/Services/IQuickAccessService.cs +++ b/src/Files.Backend/Services/IQuickAccessService.cs @@ -46,5 +46,12 @@ public interface IQuickAccessService /// The path of the folder /// true if the item is pinned bool IsItemPinned(string folderPath); + + /// + /// Saves a state of favorite items in the sidebar + /// + /// The array of items to save + /// + Task Save(string[] items); } } \ No newline at end of file