diff --git a/src/Files.App/BaseLayout.cs b/src/Files.App/BaseLayout.cs index c70d81030a02..40e2f88603c2 100644 --- a/src/Files.App/BaseLayout.cs +++ b/src/Files.App/BaseLayout.cs @@ -55,20 +55,26 @@ public abstract class BaseLayout : Page, IBaseLayout, INotifyPropertyChanged public SelectedItemsPropertiesViewModel SelectedItemsPropertiesViewModel { get; } - public FolderSettingsViewModel? FolderSettings => ParentShellPageInstance?.InstanceViewModel.FolderSettings; + public FolderSettingsViewModel? FolderSettings + => ParentShellPageInstance?.InstanceViewModel.FolderSettings; - public CurrentInstanceViewModel? InstanceViewModel => ParentShellPageInstance?.InstanceViewModel; + public CurrentInstanceViewModel? InstanceViewModel + => ParentShellPageInstance?.InstanceViewModel; - public PreviewPaneViewModel PreviewPaneViewModel => App.PreviewPaneViewModel; + public PreviewPaneViewModel PreviewPaneViewModel + => App.PreviewPaneViewModel; + + public AppModel AppModel + => App.AppModel; - public AppModel AppModel => App.AppModel; public DirectoryPropertiesViewModel DirectoryPropertiesViewModel { get; } - public CommandBarFlyout ItemContextMenuFlyout { get; set; } = new CommandBarFlyout() + public CommandBarFlyout ItemContextMenuFlyout { get; set; } = new() { AlwaysExpanded = true, }; - public CommandBarFlyout BaseContextMenuFlyout { get; set; } = new CommandBarFlyout() + + public CommandBarFlyout BaseContextMenuFlyout { get; set; } = new() { AlwaysExpanded = true, }; @@ -78,6 +84,7 @@ public abstract class BaseLayout : Page, IBaseLayout, INotifyPropertyChanged public IShellPage? ParentShellPageInstance { get; private set; } = null; public bool IsRenamingItem { get; set; } = false; + public ListedItem? RenamingItem { get; set; } = null; public string? OldItemName { get; set; } = null; @@ -92,12 +99,14 @@ public bool IsMiddleClickToScrollEnabled if (isMiddleClickToScrollEnabled != value) { isMiddleClickToScrollEnabled = value; + NotifyPropertyChanged(nameof(IsMiddleClickToScrollEnabled)); } } } - protected AddressToolbar? NavToolbar => (App.Window.Content as Frame)?.FindDescendant(); + protected AddressToolbar? NavToolbar + => (App.Window.Content as Frame)?.FindDescendant(); private CollectionViewSource collectionViewSource = new() { @@ -111,10 +120,14 @@ public CollectionViewSource CollectionViewSource { if (collectionViewSource == value) return; + if (collectionViewSource.View is not null) collectionViewSource.View.VectorChanged -= View_VectorChanged; + collectionViewSource = value; + NotifyPropertyChanged(nameof(CollectionViewSource)); + if (collectionViewSource.View is not null) collectionViewSource.View.VectorChanged += View_VectorChanged; } @@ -123,7 +136,6 @@ public CollectionViewSource CollectionViewSource protected NavigationArguments? navigationArguments; private bool isItemSelected = false; - public bool IsItemSelected { get => isItemSelected; @@ -132,13 +144,13 @@ internal set if (value != isItemSelected) { isItemSelected = value; + NotifyPropertyChanged(nameof(IsItemSelected)); } } } private string jumpString = string.Empty; - public string JumpString { get => jumpString; @@ -182,6 +194,7 @@ public string JumpString // Restart the timer jumpTimer.Start(); } + jumpString = value; } } @@ -193,16 +206,17 @@ public List? SelectedItems get => selectedItems; internal set { - //if (!(value?.All(x => selectedItems?.Contains(x) ?? false) ?? value == selectedItems)) // check if the new list is different then the old one - if (value != selectedItems) // check if the new list is different then the old one + // Check if the new list is different then the old one + //if (!(value?.All(x => selectedItems?.Contains(x) ?? false) ?? value == selectedItems)) + if (value != selectedItems) { if (value?.FirstOrDefault() != selectedItems?.FirstOrDefault()) { - // update preview pane properties + // Update preview pane properties App.PreviewPaneViewModel.IsItemSelected = value?.Count > 0; App.PreviewPaneViewModel.SelectedItem = value?.Count == 1 ? value.First() : null; - // check if the preview pane is open before updating the model + // Check if the preview pane is open before updating the model if (PreviewPaneViewModel.IsEnabled) { var isPaneEnabled = ((App.Window.Content as Frame)?.Content as MainPage)?.ShouldPreviewPaneBeActive ?? false; @@ -212,11 +226,13 @@ internal set } selectedItems = value; + if (selectedItems?.Count == 0 || selectedItems?[0] is null) { IsItemSelected = false; SelectedItem = null; SelectedItemsPropertiesViewModel.IsItemSelected = false; + ResetRenameDoubleClick(); UpdateSelectionSize(); } @@ -225,6 +241,7 @@ internal set IsItemSelected = true; SelectedItem = selectedItems.First(); SelectedItemsPropertiesViewModel.IsItemSelected = true; + UpdateSelectionSize(); SelectedItemsPropertiesViewModel.SelectedItemsCount = selectedItems.Count; @@ -234,7 +251,8 @@ internal set SelectedItemsPropertiesViewModel.SelectedItemsCountString = $"{selectedItems.Count} {"ItemSelected/Text".GetLocalizedResource()}"; DispatcherQueue.EnqueueAsync(async () => { - await Task.Delay(50); // Tapped event must be executed first + // Tapped event must be executed first + await Task.Delay(50); preRenamingItem = SelectedItem; }); } @@ -308,6 +326,7 @@ private void JumpTimer_Tick(object sender, object e) var items = CollectionViewSource.IsSourceGrouped ? (CollectionViewSource.Source as BulkConcurrentObservableCollection>)?.SelectMany(g => g) // add all items from each group to the new list : CollectionViewSource.Source as IEnumerable; + return items ?? new List(); } @@ -316,6 +335,7 @@ public virtual void ResetItemOpacity() var items = GetAllItems(); if (items is null) return; + foreach (var item in items) { if (item is not null) @@ -355,6 +375,7 @@ protected virtual void BaseFolderSettings_LayoutModeChangeRequested(object? send // Remove old layout from back stack ParentShellPageInstance.RemoveLastPageFromBackStack(); } + ParentShellPageInstance.FilesystemViewModel.UpdateEmptyTextType(); } } @@ -369,16 +390,21 @@ protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "" protected override async void OnNavigatedTo(NavigationEventArgs eventArgs) { base.OnNavigatedTo(eventArgs); + // Add item jumping handler CharacterReceived += Page_CharacterReceived; + navigationArguments = (NavigationArguments)eventArgs.Parameter; ParentShellPageInstance = navigationArguments.AssociatedTabInstance; + InitializeCommandsViewModel(); IsItemSelected = false; + FolderSettings!.LayoutModeChangeRequested += BaseFolderSettings_LayoutModeChangeRequested; FolderSettings.GroupOptionPreferenceUpdated += FolderSettings_GroupOptionPreferenceUpdated; FolderSettings.GroupDirectionPreferenceUpdated += FolderSettings_GroupDirectionPreferenceUpdated; + ParentShellPageInstance.FilesystemViewModel.EmptyTextType = EmptyTextType.None; ParentShellPageInstance.ToolbarViewModel.UpdateSortAndGroupOptions(); ParentShellPageInstance.ToolbarViewModel.CanRefresh = true; @@ -404,6 +430,7 @@ protected override async void OnNavigatedTo(NavigationEventArgs eventArgs) ParentShellPageInstance.InstanceViewModel.IsPageTypeLibrary = LibraryManager.IsLibraryPath(workingDir); ParentShellPageInstance.InstanceViewModel.IsPageTypeSearchResults = false; ParentShellPageInstance.ToolbarViewModel.PathControlDisplayText = navigationArguments.NavPathParam; + if (!navigationArguments.IsLayoutSwitch || previousDir != workingDir) ParentShellPageInstance.FilesystemViewModel.RefreshItems(previousDir, SetSelectedItemsOnNavigation); else @@ -414,10 +441,14 @@ protected override async void OnNavigatedTo(NavigationEventArgs eventArgs) await ParentShellPageInstance.FilesystemViewModel.SetWorkingDirectoryAsync(navigationArguments.SearchPathParam); ParentShellPageInstance.ToolbarViewModel.CanGoForward = false; - ParentShellPageInstance.ToolbarViewModel.CanGoBack = true; // Impose no artificial restrictions on back navigation. Even in a search results page. + + // Impose no artificial restrictions on back navigation. Even in a search results page. + ParentShellPageInstance.ToolbarViewModel.CanGoBack = true; + ParentShellPageInstance.ToolbarViewModel.CanNavigateToParent = false; var workingDir = ParentShellPageInstance.FilesystemViewModel.WorkingDirectory ?? string.Empty; + ParentShellPageInstance.InstanceViewModel.IsPageTypeRecycleBin = workingDir.StartsWith(CommonPaths.RecycleBinPath, StringComparison.Ordinal); ParentShellPageInstance.InstanceViewModel.IsPageTypeMtpDevice = workingDir.StartsWith("\\\\?\\", StringComparison.Ordinal); ParentShellPageInstance.InstanceViewModel.IsPageTypeFtp = FtpHelpers.IsFtpPath(workingDir); @@ -436,12 +467,15 @@ protected override async void OnNavigatedTo(NavigationEventArgs eventArgs) ThumbnailSize = InstanceViewModel!.FolderSettings.GetIconSize(), SearchUnindexedItems = navigationArguments.SearchUnindexedItems }; + _ = ParentShellPageInstance.FilesystemViewModel.SearchAsync(searchInstance); } } - ParentShellPageInstance.InstanceViewModel.IsPageTypeNotHome = true; // show controls that were hidden on the home page + // Show controls that were hidden on the home page + ParentShellPageInstance.InstanceViewModel.IsPageTypeNotHome = true; ParentShellPageInstance.FilesystemViewModel.UpdateGroupOptions(); + UpdateCollectionViewSource(); FolderSettings.IsLayoutModeChanging = false; @@ -455,9 +489,11 @@ public void SetSelectedItemsOnNavigation() { try { - if (navigationArguments is not null && navigationArguments.SelectItems is not null && navigationArguments.SelectItems.Any()) + if (navigationArguments is not null && + navigationArguments.SelectItems is not null && + navigationArguments.SelectItems.Any()) { - List liItemsToSelect = new List(); + List liItemsToSelect = new(); foreach (string item in navigationArguments.SelectItems) liItemsToSelect.Add(ParentShellPageInstance!.FilesystemViewModel.FilesAndFolders.Where((li) => li.ItemNameRaw == item).First()); @@ -466,7 +502,8 @@ public void SetSelectedItemsOnNavigation() } else if (navigationArguments is not null && navigationArguments.FocusOnNavigation) { - ItemManipulationModel.FocusFileList(); // Set focus on layout specific file list control + // Set focus on layout specific file list control + ItemManipulationModel.FocusFileList(); } } catch (Exception) @@ -488,14 +525,18 @@ private async void GroupPreferenceUpdated() groupingCancellationToken?.Cancel(); groupingCancellationToken = new CancellationTokenSource(); var token = groupingCancellationToken.Token; + await ParentShellPageInstance!.FilesystemViewModel.GroupOptionsUpdated(token); + UpdateCollectionViewSource(); + await ParentShellPageInstance.FilesystemViewModel.ReloadItemGroupHeaderImagesAsync(); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { base.OnNavigatingFrom(e); + // Remove item jumping handler CharacterReceived -= Page_CharacterReceived; FolderSettings!.LayoutModeChangeRequested -= BaseFolderSettings_LayoutModeChangeRequested; @@ -515,6 +556,7 @@ public async void ItemContextFlyout_Opening(object? sender, object e) { if (!IsItemSelected && ((sender as CommandBarFlyout)?.Target as ListViewItem)?.Content is ListedItem li) // Workaround for item sometimes not getting selected ItemManipulationModel.SetSelectedItem(li); + if (IsItemSelected) await LoadMenuItemsAsync(); } @@ -530,18 +572,27 @@ public async void BaseContextFlyout_Opening(object? sender, object e) { try { + // Reset menu max height if (BaseContextMenuFlyout.GetValue(ContextMenuExtensions.ItemsControlProperty) is ItemsControl itc) - itc.MaxHeight = Constants.UI.ContextMenuMaxHeight; // Reset menu max height + itc.MaxHeight = Constants.UI.ContextMenuMaxHeight; + shellContextMenuItemCancellationToken?.Cancel(); shellContextMenuItemCancellationToken = new CancellationTokenSource(); + var shiftPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down); var items = ContextFlyoutItemHelper.GetBaseContextCommandsWithoutShellItems(currentInstanceViewModel: InstanceViewModel!, itemViewModel: ParentShellPageInstance!.FilesystemViewModel, commandsViewModel: CommandsViewModel!, shiftPressed: shiftPressed); + BaseContextMenuFlyout.PrimaryCommands.Clear(); BaseContextMenuFlyout.SecondaryCommands.Clear(); + var (primaryElements, secondaryElements) = ItemModelListToContextFlyoutHelper.GetAppBarItemsFromModel(items); + AddCloseHandler(BaseContextMenuFlyout, primaryElements, secondaryElements); + primaryElements.ForEach(i => BaseContextMenuFlyout.PrimaryCommands.Add(i)); - secondaryElements.OfType().ForEach(i => i.MinWidth = Constants.UI.ContextMenuItemsMaxWidth); // Set menu min width + + // Set menu min width + secondaryElements.OfType().ForEach(i => i.MinWidth = Constants.UI.ContextMenuItemsMaxWidth); secondaryElements.ForEach(i => BaseContextMenuFlyout.SecondaryCommands.Add(i)); if (!InstanceViewModel!.IsPageTypeSearchResults && !InstanceViewModel.IsPageTypeZipFolder) @@ -562,6 +613,7 @@ public void UpdateSelectionSize() var items = (selectedItems?.Any() ?? false) ? selectedItems : GetAllItems(); if (items is null) return; + var isSizeKnown = !items.Any(item => string.IsNullOrEmpty(item.FileSize)); if (isSizeKnown) { @@ -574,13 +626,16 @@ public void UpdateSelectionSize() SelectedItemsPropertiesViewModel.ItemSizeBytes = 0; SelectedItemsPropertiesViewModel.ItemSize = string.Empty; } + SelectedItemsPropertiesViewModel.ItemSizeVisibility = isSizeKnown; } private async Task LoadMenuItemsAsync() { + // Reset menu max height if (ItemContextMenuFlyout.GetValue(ContextMenuExtensions.ItemsControlProperty) is ItemsControl itc) - itc.MaxHeight = Constants.UI.ContextMenuMaxHeight; // Reset menu max height + itc.MaxHeight = Constants.UI.ContextMenuMaxHeight; + shellContextMenuItemCancellationToken?.Cancel(); shellContextMenuItemCancellationToken = new CancellationTokenSource(); SelectedItemsPropertiesViewModel.CheckAllFileExtensions(SelectedItems!.Select(selectedItem => selectedItem?.FileExtension).ToList()!); @@ -654,6 +709,7 @@ private void AddShellItemsToMenu(List shellMenuI var openedPopups = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetOpenPopups(App.Window); var secondaryMenu = openedPopups.FirstOrDefault(popup => popup.Name == "OverflowPopup"); + var itemsControl = secondaryMenu?.Child.FindDescendant(); if (itemsControl is not null && secondaryMenu is not null) { @@ -661,12 +717,16 @@ private void AddShellItemsToMenu(List shellMenuI var ttv = secondaryMenu.TransformToVisual(App.Window.Content); var cMenuPos = ttv.TransformPoint(new Point(0, 0)); + var requiredHeight = contextMenuFlyout.SecondaryCommands.Concat(mainItems).Where(x => x is not AppBarSeparator).Count() * Constants.UI.ContextMenuSecondaryItemsHeight; var availableHeight = App.Window.Bounds.Height - cMenuPos.Y - Constants.UI.ContextMenuPrimaryItemsHeight; + + // Set menu max height to current height (avoids menu repositioning) if (requiredHeight > availableHeight) - itemsControl.MaxHeight = Math.Min(Constants.UI.ContextMenuMaxHeight, Math.Max(itemsControl.ActualHeight, Math.Min(availableHeight, requiredHeight))); // Set menu max height to current height (avoids menu repositioning) + itemsControl.MaxHeight = Math.Min(Constants.UI.ContextMenuMaxHeight, Math.Max(itemsControl.ActualHeight, Math.Min(availableHeight, requiredHeight))); - mainItems.OfType().ForEach(x => x.MaxWidth = itemsControl.ActualWidth - Constants.UI.ContextMenuLabelMargin); // Set items max width to current menu width (#5555) + // Set items max width to current menu width (#5555) + mainItems.OfType().ForEach(x => x.MaxWidth = itemsControl.ActualWidth - Constants.UI.ContextMenuLabelMargin); } var overflowItem = contextMenuFlyout.SecondaryCommands.FirstOrDefault(x => x is AppBarButton appBarButton && (appBarButton.Tag as string) == "ItemOverflow") as AppBarButton; @@ -704,8 +764,9 @@ private void AddShellItemsToMenu(List shellMenuI mainItems.ForEach(x => contextMenuFlyout.SecondaryCommands.Add(x)); } - // add items to openwith dropdown + // Add items to openwith dropdown var openWithOverflow = contextMenuFlyout.SecondaryCommands.FirstOrDefault(x => x is AppBarButton abb && (abb.Tag as string) == "OpenWithOverflow") as AppBarButton; + var openWith = contextMenuFlyout.SecondaryCommands.FirstOrDefault(x => x is AppBarButton abb && (abb.Tag as string) == "OpenWith") as AppBarButton; if (openWithSubItems is not null && openWithOverflow is not null && openWith is not null) { @@ -725,9 +786,12 @@ private void AddShellItemsToMenu(List shellMenuI { itemsControl.Items.OfType().ForEach(item => { - if (item.FindDescendant("OverflowTextLabel") is TextBlock label) // Enable CharacterEllipsis text trimming for menu items + // Enable CharacterEllipsis text trimming for menu items + if (item.FindDescendant("OverflowTextLabel") is TextBlock label) label.TextTrimming = TextTrimming.CharacterEllipsis; - if ((item as AppBarButton)?.Flyout as MenuFlyout is MenuFlyout flyout) // Close main menu when clicking on subitems (#5508) + + // Close main menu when clicking on subitems (#5508) + if ((item as AppBarButton)?.Flyout as MenuFlyout is MenuFlyout flyout) { Action> clickAction = null!; clickAction = (items) => @@ -741,6 +805,7 @@ private void AddShellItemsToMenu(List shellMenuI clickAction(i.Items); }); }; + clickAction(flyout.Items); } }); @@ -776,8 +841,10 @@ protected void FileList_DragItemsStarting(object sender, DragItemsStartingEventA private void Item_DragLeave(object sender, DragEventArgs e) { var item = GetItemFromElement(sender); + + // Reset dragged over item if (item == dragOverItem) - dragOverItem = null; // Reset dragged over item + dragOverItem = null; } protected async void Item_DragOver(object sender, DragEventArgs e) @@ -804,7 +871,8 @@ protected async void Item_DragOver(object sender, DragEventArgs e) dragOverItem = null; _ = NavigationHelpers.OpenSelectedItems(ParentShellPageInstance!, false); } - }, TimeSpan.FromMilliseconds(1000), false); + }, + TimeSpan.FromMilliseconds(1000), false); } if (FilesystemHelpers.HasDraggedStorageItems(e.DataView)) @@ -824,11 +892,13 @@ protected async void Item_DragOver(object sender, DragEventArgs e) else { e.DragUIOverride.IsCaptionVisible = true; + if (item.IsExecutable) { e.DragUIOverride.Caption = $"{"OpenItemsWithCaptionText".GetLocalizedResource()} {item.Name}"; e.AcceptedOperation = DataPackageOperation.Link; - } // Items from the same drive as this folder are dragged into this folder, so we move the items instead of copy + } + // Items from the same drive as this folder are dragged into this folder, so we move the items instead of copy else if (e.Modifiers.HasFlag(DragDropModifiers.Alt) || e.Modifiers.HasFlag(DragDropModifiers.Control | DragDropModifiers.Shift)) { e.DragUIOverride.Caption = string.Format("LinkToFolderCaptionText".GetLocalizedResource(), item.Name); @@ -874,7 +944,9 @@ protected async void Item_Drop(object sender, DragEventArgs e) var deferral = e.GetDeferral(); e.Handled = true; - dragOverItem = null; // Reset dragged over item + + // Reset dragged over item + dragOverItem = null; var item = GetItemFromElement(sender); if (item is not null) @@ -943,6 +1015,7 @@ protected static void FileListItem_PointerPressed(object sender, PointerRoutedEv if (selectorItem.IsSelected && e.KeyModifiers == VirtualKeyModifiers.Control) { selectorItem.IsSelected = false; + // Prevent issues arising caused by the default handlers attempting to select the item that has just been deselected by ctrl + click e.Handled = true; } @@ -984,6 +1057,7 @@ selectedItems is not null && { if (ItemsControl.Items[i] == last || ItemsControl.Items[i] == hoveredItem) found++; + if (found != 0 && !selectedItems.Contains(ItemsControl.Items[i])) ItemManipulationModel.AddSelectedItem((ListedItem)ItemsControl.Items[i]); } @@ -992,9 +1066,11 @@ selectedItems is not null && { ItemManipulationModel.SetSelectedItem(hoveredItem); } + hoveredItem = null; } - }, TimeSpan.FromMilliseconds(600), false); + }, + TimeSpan.FromMilliseconds(600), false); } } @@ -1063,6 +1139,7 @@ private void UpdateCollectionViewSource() { if (ParentShellPageInstance is null) return; + if (ParentShellPageInstance.FilesystemViewModel.FilesAndFolders.IsGrouped) { CollectionViewSource = new CollectionViewSource() @@ -1085,8 +1162,10 @@ protected void SemanticZoom_ViewChangeStarted(object sender, SemanticZoomViewCha { if (e.IsSourceZoomedInView) return; + // According to the docs this isn't necessary, but it would crash otherwise var destination = e.DestinationItem.Item as GroupedCollection; + e.DestinationItem.Item = destination?.FirstOrDefault(); } @@ -1116,6 +1195,7 @@ private void ItemManipulationModel_RefreshItemsOpacityInvoked(object? sender, Ev var items = GetAllItems(); if (items is null) return; + foreach (ListedItem listedItem in items) { if (listedItem.IsHiddenItem) @@ -1131,7 +1211,9 @@ private void View_VectorChanged(IObservableVector sender, IVectorChanged ParentShellPageInstance.ToolbarViewModel.HasItem = CollectionViewSource.View.Any(); } - virtual public void StartRenameItem() { } + virtual public void StartRenameItem() + { + } private ListedItem? preRenamingItem = null; @@ -1148,7 +1230,8 @@ public void CheckRenameDoubleClick(object clickedItem) StartRenameItem(); tapDebounceTimer.Stop(); } - }, TimeSpan.FromMilliseconds(500)); + }, + TimeSpan.FromMilliseconds(500)); } else { @@ -1173,6 +1256,7 @@ protected async void ValidateItemNameInputText(TextBox textBox, TextBoxBeforeTex if (FilesystemHelpers.ContainsRestrictedCharacters(args.NewText)) { args.Cancel = true; + await DispatcherQueue.EnqueueAsync(() => { var oldSelection = textBox.SelectionStart + textBox.SelectionLength; diff --git a/src/Files.App/Behaviors/StickyHeaderBehavior.cs b/src/Files.App/Behaviors/StickyHeaderBehavior.cs index d85034a2f22c..6fb671921f10 100644 --- a/src/Files.App/Behaviors/StickyHeaderBehavior.cs +++ b/src/Files.App/Behaviors/StickyHeaderBehavior.cs @@ -1,7 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - using CommunityToolkit.WinUI.UI; using CommunityToolkit.WinUI.UI.Animations.Expressions; using CommunityToolkit.WinUI.UI.Behaviors; @@ -54,8 +50,12 @@ protected override bool Uninitialize() /// /// The UIElement that will be faded. /// - public static readonly DependencyProperty HeaderElementProperty = DependencyProperty.Register( - nameof(HeaderElement), typeof(UIElement), typeof(StickyHeaderBehavior), new PropertyMetadata(null, PropertyChangedCallback)); + public static readonly DependencyProperty HeaderElementProperty = + DependencyProperty.Register( + nameof(HeaderElement), + typeof(UIElement), + typeof(StickyHeaderBehavior), + new PropertyMetadata(null, PropertyChangedCallback)); private ScrollViewer _scrollViewer; private CompositionPropertySet _scrollProperties; @@ -71,8 +71,8 @@ protected override bool Uninitialize() /// public UIElement HeaderElement { - get { return (UIElement)GetValue(HeaderElementProperty); } - set { SetValue(HeaderElementProperty, value); } + get => (UIElement)GetValue(HeaderElementProperty); + set => SetValue(HeaderElementProperty, value); } /// @@ -119,7 +119,7 @@ private bool AssignAnimation() if (HeaderElement is null && listView is not null) HeaderElement = listView.Header as UIElement; - var headerElement = HeaderElement as FrameworkElement; + FrameworkElement? headerElement = HeaderElement as FrameworkElement; if (headerElement is null || headerElement.RenderSize.Height == 0) return false; @@ -212,6 +212,7 @@ private void ScrollViewer_GotFocus(object sender, RoutedEventArgs e) var scroller = (ScrollViewer)sender; object focusedElement; + if (IsXamlRootAvailable && scroller.XamlRoot is not null) { focusedElement = FocusManager.GetFocusedElement(scroller.XamlRoot); @@ -239,4 +240,4 @@ private void ScrollViewer_GotFocus(object sender, RoutedEventArgs e) } } } -} \ No newline at end of file +} diff --git a/src/Files.App/CommandLine/CommandLineParser.cs b/src/Files.App/CommandLine/CommandLineParser.cs index 4c92d402d425..c3895d89b02a 100644 --- a/src/Files.App/CommandLine/CommandLineParser.cs +++ b/src/Files.App/CommandLine/CommandLineParser.cs @@ -70,6 +70,7 @@ private static ParsedCommands ParseSplitArguments(List ParseData(string[] args, int index { string? key = null; var val = new List(); + if (args[index].StartsWith('-') || args[index].StartsWith('/')) { if (args[index].Contains(':', StringComparison.Ordinal)) { string argument = args[index]; int endIndex = argument.IndexOf(':'); - key = argument.Substring(1, endIndex - 1); // trim the '/' and the ':'. + + // Trim the '/' and the ':' + key = argument.Substring(1, endIndex - 1); + int valueStart = endIndex + 1; - val.Add(valueStart < argument.Length ? argument.Substring( - valueStart, argument.Length - valueStart) : null); + val.Add(valueStart < argument.Length + ? argument.Substring(valueStart, argument.Length - valueStart) + : null); } else { @@ -175,4 +181,4 @@ private static KeyValuePair ParseData(string[] args, int index return key is not null ? new KeyValuePair(key, val.ToArray()) : default; } } -} \ No newline at end of file +} diff --git a/src/Files.App/CommandLine/ParsedCommand.cs b/src/Files.App/CommandLine/ParsedCommand.cs index 76e5d18bc67a..2869453c888b 100644 --- a/src/Files.App/CommandLine/ParsedCommand.cs +++ b/src/Files.App/CommandLine/ParsedCommand.cs @@ -7,11 +7,12 @@ internal class ParsedCommand { public ParsedCommandType Type { get; set; } - public string Payload => Args.FirstOrDefault(); + public string Payload + => Args.FirstOrDefault(); public List Args { get; set; } public ParsedCommand() => Args = new List(); } -} \ No newline at end of file +} diff --git a/src/Files.App/CommandLine/ParsedCommandType.cs b/src/Files.App/CommandLine/ParsedCommandType.cs index 859d92c9a323..ae0ce4340c8b 100644 --- a/src/Files.App/CommandLine/ParsedCommandType.cs +++ b/src/Files.App/CommandLine/ParsedCommandType.cs @@ -10,4 +10,4 @@ internal enum ParsedCommandType SelectItem, TagFiles } -} \ No newline at end of file +} diff --git a/src/Files.App/CommandLine/ParsedCommands.cs b/src/Files.App/CommandLine/ParsedCommands.cs index 5d36c8bedf87..c40781258755 100644 --- a/src/Files.App/CommandLine/ParsedCommands.cs +++ b/src/Files.App/CommandLine/ParsedCommands.cs @@ -5,4 +5,4 @@ namespace Files.App.CommandLine internal class ParsedCommands : List { } -} \ No newline at end of file +} diff --git a/src/Files.App/Constants.cs b/src/Files.App/Constants.cs index f4a57c003dd9..62436e8547c9 100644 --- a/src/Files.App/Constants.cs +++ b/src/Files.App/Constants.cs @@ -92,11 +92,20 @@ public static class UI public const double MaximumSidebarWidth = 500; - public const double ContextMenuMaxHeight = 480; // For contextmenu hacks, must match WinUI style - public const double ContextMenuSecondaryItemsHeight = 32; // For contextmenu hacks, must match WinUI style - public const double ContextMenuPrimaryItemsHeight = 48; // For contextmenu hacks, must match WinUI style - public const double ContextMenuLabelMargin = 10; // For contextmenu hacks - public const double ContextMenuItemsMaxWidth = 250; // For contextmenu hacks + // For contextmenu hacks, must match WinUI style + public const double ContextMenuMaxHeight = 480; + + // For contextmenu hacks, must match WinUI style + public const double ContextMenuSecondaryItemsHeight = 32; + + // For contextmenu hacks, must match WinUI style + public const double ContextMenuPrimaryItemsHeight = 48; + + // For contextmenu hacks + public const double ContextMenuLabelMargin = 10; + + // For contextmenu hacks + public const double ContextMenuItemsMaxWidth = 250; } public static class Browser @@ -105,7 +114,8 @@ public static class GridViewBrowser { public const int GridViewIncrement = 20; - public const int GridViewSizeMax = 300; // Max achievable ctrl + scroll, not a default layout size. + // Max achievable ctrl + scroll, not a default layout size + public const int GridViewSizeMax = 300; public const int GridViewSizeLarge = 220; @@ -208,4 +218,4 @@ public static class GitHub public const string SupportUsUrl = @"https://github.com/sponsors/yaira2"; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Controllers/IJson.cs b/src/Files.App/Controllers/IJson.cs index f1457733c3aa..2a89e5f749c5 100644 --- a/src/Files.App/Controllers/IJson.cs +++ b/src/Files.App/Controllers/IJson.cs @@ -6,4 +6,4 @@ internal interface IJson void SaveModel(); } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/BoolToSelectionMode.cs b/src/Files.App/Converters/BoolToSelectionMode.cs index d3b559dff21a..f3a13540c19d 100644 --- a/src/Files.App/Converters/BoolToSelectionMode.cs +++ b/src/Files.App/Converters/BoolToSelectionMode.cs @@ -16,4 +16,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin return ((value as ListViewSelectionMode?) ?? ListViewSelectionMode.Extended) == ListViewSelectionMode.Multiple; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/Converters.cs b/src/Files.App/Converters/Converters.cs index a1a0c2525550..3fef8bdb12b1 100644 --- a/src/Files.App/Converters/Converters.cs +++ b/src/Files.App/Converters/Converters.cs @@ -9,8 +9,7 @@ namespace Files.App.Converters /// /// The source type. /// The target type. - public abstract class ValueConverter - : IValueConverter + public abstract class ValueConverter : IValueConverter { /// /// Converts a source value to the target type. @@ -88,8 +87,7 @@ public abstract class ValueConverter /// /// The base class for converting instances of type T to object and vice versa. /// - public abstract class ToObjectConverter - : ValueConverter + public abstract class ToObjectConverter : ValueConverter { /// /// Converts a source value to the target type. @@ -119,8 +117,7 @@ public abstract class ToObjectConverter /// /// Converts a boolean to and from a visibility value. /// - public class InverseBooleanConverter - : ValueConverter + public class InverseBooleanConverter : ValueConverter { /// /// Converts a source value to the target type. @@ -147,8 +144,7 @@ protected override bool ConvertBack(bool value, object? parameter, string? langu } } - public class NullToTrueConverter - : ValueConverter + public class NullToTrueConverter : ValueConverter { /// /// Determines whether an inverse conversion should take place. @@ -181,8 +177,7 @@ protected override bool Convert(object? value, object? parameter, string? langua } } - public class StringNullOrWhiteSpaceToTrueConverter - : ValueConverter + public class StringNullOrWhiteSpaceToTrueConverter : ValueConverter { /// /// Determines whether an inverse conversion should take place. @@ -214,4 +209,4 @@ protected override string ConvertBack(bool value, object? parameter, string? lan return string.Empty; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/DateTimeOffsetToString.cs b/src/Files.App/Converters/DateTimeOffsetToString.cs index e2318408d13f..22d776cd0b4d 100644 --- a/src/Files.App/Converters/DateTimeOffsetToString.cs +++ b/src/Files.App/Converters/DateTimeOffsetToString.cs @@ -28,4 +28,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/DoubleArrayToString.cs b/src/Files.App/Converters/DoubleArrayToString.cs index d35522bfa396..e8c9d83de134 100644 --- a/src/Files.App/Converters/DoubleArrayToString.cs +++ b/src/Files.App/Converters/DoubleArrayToString.cs @@ -27,6 +27,7 @@ public object ConvertBack(object value, Type targetType, object parameter, strin { var strArray = (value as string).Split("; "); var array = new double[strArray.Length]; + for (int i = 0; i < strArray.Length; i++) { try @@ -40,4 +41,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin return array; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/DoubleToString.cs b/src/Files.App/Converters/DoubleToString.cs index f2b7bdb289aa..c8205ed8b04a 100644 --- a/src/Files.App/Converters/DoubleToString.cs +++ b/src/Files.App/Converters/DoubleToString.cs @@ -27,4 +27,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/IntToGroupOption.cs b/src/Files.App/Converters/IntToGroupOption.cs index 666a13728154..a31e85fcc45c 100644 --- a/src/Files.App/Converters/IntToGroupOption.cs +++ b/src/Files.App/Converters/IntToGroupOption.cs @@ -23,4 +23,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/IntToSortDirection.cs b/src/Files.App/Converters/IntToSortDirection.cs index be543599a317..e58b89be5fc1 100644 --- a/src/Files.App/Converters/IntToSortDirection.cs +++ b/src/Files.App/Converters/IntToSortDirection.cs @@ -23,4 +23,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/IntToSortOption.cs b/src/Files.App/Converters/IntToSortOption.cs index 8124313f8705..d28eecdd9166 100644 --- a/src/Files.App/Converters/IntToSortOption.cs +++ b/src/Files.App/Converters/IntToSortOption.cs @@ -23,4 +23,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/MiddleTextEllipsisConverter.cs b/src/Files.App/Converters/MiddleTextEllipsisConverter.cs index d7e60aacd284..d4891ebd5135 100644 --- a/src/Files.App/Converters/MiddleTextEllipsisConverter.cs +++ b/src/Files.App/Converters/MiddleTextEllipsisConverter.cs @@ -36,4 +36,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/MultiBooleanConverter.cs b/src/Files.App/Converters/MultiBooleanConverter.cs index d1c7d7c50d08..6c95e52bdc92 100644 --- a/src/Files.App/Converters/MultiBooleanConverter.cs +++ b/src/Files.App/Converters/MultiBooleanConverter.cs @@ -23,4 +23,4 @@ public static Visibility OrConvertToVisibility(bool a, bool b) public static Visibility OrNotConvertToVisibility(bool a, bool b) => OrConvertToVisibility(a, !b); } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/StorageDeleteOptionToBooleanConverter.cs b/src/Files.App/Converters/StorageDeleteOptionToBooleanConverter.cs index 13ad842ff8bb..b5af2bb55e27 100644 --- a/src/Files.App/Converters/StorageDeleteOptionToBooleanConverter.cs +++ b/src/Files.App/Converters/StorageDeleteOptionToBooleanConverter.cs @@ -16,4 +16,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin return (value is bool bl && bl) ? StorageDeleteOption.PermanentDelete : StorageDeleteOption.Default; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/StringArrayToString.cs b/src/Files.App/Converters/StringArrayToString.cs index 8b33532bd99e..d3811e31c32a 100644 --- a/src/Files.App/Converters/StringArrayToString.cs +++ b/src/Files.App/Converters/StringArrayToString.cs @@ -27,4 +27,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin return (value as string).Split("; "); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/UInt32ToString.cs b/src/Files.App/Converters/UInt32ToString.cs index bf252bc7b1ea..f98285cc06cb 100644 --- a/src/Files.App/Converters/UInt32ToString.cs +++ b/src/Files.App/Converters/UInt32ToString.cs @@ -14,7 +14,7 @@ public object ConvertBack(object value, Type targetType, object parameter, strin { try { - return UInt32.Parse(value as string); + return uint.Parse(value as string); } catch (FormatException) { @@ -22,4 +22,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Converters/VisibilityInvertConverter.cs b/src/Files.App/Converters/VisibilityInvertConverter.cs index 6bde74d643ee..0ff6ddbc7afe 100644 --- a/src/Files.App/Converters/VisibilityInvertConverter.cs +++ b/src/Files.App/Converters/VisibilityInvertConverter.cs @@ -12,6 +12,7 @@ public object Convert(object value, Type targetType, object parameter, string la { return isVisible ? Visibility.Collapsed : Visibility.Visible; } + return (Visibility)value == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; } @@ -20,4 +21,4 @@ public object ConvertBack(object value, Type targetType, object parameter, strin throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/DataModels/AppModel.cs b/src/Files.App/DataModels/AppModel.cs index e0fd679d6b3a..586b7735ff44 100644 --- a/src/Files.App/DataModels/AppModel.cs +++ b/src/Files.App/DataModels/AppModel.cs @@ -28,7 +28,7 @@ public AppModel() FoldersSettings.PropertyChanged += FoldersSettings_PropertyChanged; ; Clipboard.ContentChanged += Clipboard_ContentChanged; - //todo: this doesn't belong here + // TODO: This doesn't belong here DetectFontName(); } @@ -40,7 +40,7 @@ private void FoldersSettings_PropertyChanged(object? sender, System.ComponentMod } } - //todo: refactor this method + // TODO: Refactor this method public void Clipboard_ContentChanged(object sender, object e) { try @@ -105,7 +105,7 @@ public FontFamily SymbolFontFamily set => SetProperty(ref symbolFontFamily, value); } - //todo: refactor this method + // TODO: Refactor this method private void DetectFontName() { var rawVersion = ulong.Parse(AnalyticsInfo.VersionInfo.DeviceFamilyVersion); @@ -116,4 +116,4 @@ private void DetectFontName() SymbolFontFamily = (isWindows11) ? new FontFamily("Segoe Fluent Icons") : new FontFamily("Segoe MDL2 Assets"); } } -} \ No newline at end of file +} diff --git a/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs b/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs index bf7413cbde2e..e766789c413b 100644 --- a/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs +++ b/src/Files.App/DataModels/NavigationControlItems/DriveItem.cs @@ -24,31 +24,43 @@ public BitmapImage Icon } //public Uri IconSource { get; set; } + public byte[] IconData { get; set; } private string path; public string Path { get => path; - set - { - path = value; - } + set => path = value; } public string ToolTipText { get; private set; } + public string DeviceID { get; set; } + public StorageFolder Root { get; set; } + public NavigationControlItemType ItemType { get; set; } = NavigationControlItemType.Drive; + public Visibility ItemVisibility { get; set; } = Visibility.Visible; - public bool IsRemovable => Type == DriveType.Removable || Type == DriveType.CDRom; - public bool IsNetwork => Type == DriveType.Network; - public bool IsPinned => App.QuickAccessManager.Model.FavoriteItems.Contains(path); + public bool IsRemovable + => Type == DriveType.Removable || Type == DriveType.CDRom; - public string MaxSpaceText => MaxSpace.ToSizeString(); - public string FreeSpaceText => FreeSpace.ToSizeString(); - public string UsedSpaceText => SpaceUsed.ToSizeString(); + public bool IsNetwork + => Type == DriveType.Network; + + public bool IsPinned + => App.QuickAccessManager.Model.FavoriteItems.Contains(path); + + public string MaxSpaceText + => MaxSpace.ToSizeString(); + + public string FreeSpaceText + => FreeSpace.ToSizeString(); + + public string UsedSpaceText + => SpaceUsed.ToSizeString(); private ByteSize maxSpace; public ByteSize MaxSpace @@ -59,6 +71,7 @@ public ByteSize MaxSpace if (SetProperty(ref maxSpace, value)) { ToolTipText = GetSizeString(); + OnPropertyChanged(nameof(MaxSpaceText)); OnPropertyChanged(nameof(ShowDriveDetails)); } @@ -74,6 +87,7 @@ public ByteSize FreeSpace if (SetProperty(ref freeSpace, value)) { ToolTipText = GetSizeString(); + OnPropertyChanged(nameof(FreeSpaceText)); } } @@ -92,7 +106,8 @@ public ByteSize SpaceUsed } } - public bool ShowDriveDetails => MaxSpace.Bytes > 0d; + public bool ShowDriveDetails + => MaxSpace.Bytes > 0d; public DriveType Type { get; set; } @@ -228,6 +243,7 @@ public async Task LoadDriveIcon() IconData ??= UIHelpers.GetIconResourceInfo(Constants.ImageRes.Folder).IconData; } + Icon = await IconData.ToBitmapAsync(); } diff --git a/src/Files.App/DataModels/NavigationControlItems/FileTagItem.cs b/src/Files.App/DataModels/NavigationControlItems/FileTagItem.cs index ba3059dc6611..18418ff2222d 100644 --- a/src/Files.App/DataModels/NavigationControlItems/FileTagItem.cs +++ b/src/Files.App/DataModels/NavigationControlItems/FileTagItem.cs @@ -8,7 +8,6 @@ public class FileTagItem : INavigationControlItem public string Text { get; set; } private string path; - public string Path { get => path; @@ -25,9 +24,11 @@ public string Path public ContextMenuOptions MenuOptions { get; set; } - public NavigationControlItemType ItemType => NavigationControlItemType.FileTag; + public NavigationControlItemType ItemType + => NavigationControlItemType.FileTag; - public int CompareTo(INavigationControlItem other) => Text.CompareTo(other.Text); + public int CompareTo(INavigationControlItem other) + => Text.CompareTo(other.Text); public TagViewModel FileTag { get; set; } } diff --git a/src/Files.App/DataModels/NavigationControlItems/INavigationControlItem.cs b/src/Files.App/DataModels/NavigationControlItems/INavigationControlItem.cs index df24b8c5068e..0e3473e1e0d8 100644 --- a/src/Files.App/DataModels/NavigationControlItems/INavigationControlItem.cs +++ b/src/Files.App/DataModels/NavigationControlItems/INavigationControlItem.cs @@ -56,4 +56,4 @@ public class ContextMenuOptions public bool ShowShellItems { get; set; } } -} \ No newline at end of file +} diff --git a/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs b/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs index f4d6d45559be..c2311f86781d 100644 --- a/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs +++ b/src/Files.App/DataModels/NavigationControlItems/LocationItem.cs @@ -15,7 +15,6 @@ namespace Files.App.DataModels.NavigationControlItems public class LocationItem : ObservableObject, INavigationControlItem { public BitmapImage icon; - public BitmapImage Icon { get => icon; @@ -23,26 +22,37 @@ public BitmapImage Icon } //public Uri IconSource { get; set; } + public byte[] IconData { get; set; } public string Text { get; set; } = ""; private string path; - public string Path { get => path; set { path = value; - ToolTipText = string.IsNullOrEmpty(Path) || Path.Contains('?', StringComparison.Ordinal) || Path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) || Path.EndsWith(ShellLibraryItem.EXTENSION, StringComparison.OrdinalIgnoreCase) || Path == "Home" ? Text : Path; + ToolTipText = string.IsNullOrEmpty(Path) || + Path.Contains('?', StringComparison.Ordinal) || + Path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) || + Path.EndsWith(ShellLibraryItem.EXTENSION, StringComparison.OrdinalIgnoreCase) || + Path == "Home" + ? Text + : Path; } } public virtual string ToolTipText { get; set; } + public FontFamily Font { get; set; } - public NavigationControlItemType ItemType => NavigationControlItemType.Location; + + public NavigationControlItemType ItemType + => NavigationControlItemType.Location; + public bool IsDefaultLocation { get; set; } + public BulkConcurrentObservableCollection ChildItems { get; set; } public bool SelectsOnInvoked { get; set; } = true; @@ -62,7 +72,8 @@ public bool IsExpanded public ContextMenuOptions MenuOptions { get; set; } - public int CompareTo(INavigationControlItem other) => Text.CompareTo(other.Text); + public int CompareTo(INavigationControlItem other) + => Text.CompareTo(other.Text); public static T Create() where T : LocationItem, new() { @@ -84,11 +95,13 @@ public ulong SpaceUsed set { SetProperty(ref spaceUsed, value); + App.Window.DispatcherQueue.EnqueueAsync(() => OnPropertyChanged(nameof(ToolTipText))); } } - public override string ToolTipText => SpaceUsed.ToSizeString(); + public override string ToolTipText + => SpaceUsed.ToSizeString(); public RecycleBinLocationItem() { diff --git a/src/Files.App/DataModels/NavigationControlItems/WslDistroItem.cs b/src/Files.App/DataModels/NavigationControlItems/WslDistroItem.cs index 8fef9a2f0597..abdf23a73e88 100644 --- a/src/Files.App/DataModels/NavigationControlItems/WslDistroItem.cs +++ b/src/Files.App/DataModels/NavigationControlItems/WslDistroItem.cs @@ -8,7 +8,6 @@ public class WslDistroItem : INavigationControlItem public string Text { get; set; } private string path; - public string Path { get => path; @@ -21,7 +20,8 @@ public string Path public string ToolTipText { get; private set; } - public NavigationControlItemType ItemType => NavigationControlItemType.LinuxDistro; + public NavigationControlItemType ItemType + => NavigationControlItemType.LinuxDistro; public Uri Logo { get; set; } @@ -31,4 +31,4 @@ public string Path public int CompareTo(INavigationControlItem other) => Text.CompareTo(other.Text); } -} \ No newline at end of file +} diff --git a/src/Files.App/DataModels/SidebarPinnedModel.cs b/src/Files.App/DataModels/SidebarPinnedModel.cs index c0e1f0e946b0..f62552b8db6e 100644 --- a/src/Files.App/DataModels/SidebarPinnedModel.cs +++ b/src/Files.App/DataModels/SidebarPinnedModel.cs @@ -28,6 +28,7 @@ public class SidebarPinnedModel public EventHandler? DataChanged; private readonly SemaphoreSlim addSyncSemaphore = new(1, 1); + public List FavoriteItems { get; set; } = new List(); private readonly List favoriteList = new(); @@ -62,7 +63,6 @@ public async Task UpdateItemsWithExplorer() addSyncSemaphore.Release(); } } - /// /// Returns the index of the location item in the navigation sidebar @@ -158,6 +158,7 @@ private void AddLocationItemToSidebar(LocationItem locationItem) insertIndex = lastItem is not null ? favoriteList.IndexOf(lastItem) + 1 : 0; favoriteList.Insert(insertIndex, locationItem); } + DataChanged?.Invoke(SectionType.Favorites, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, locationItem, insertIndex)); } @@ -195,6 +196,7 @@ public void RemoveStaleSidebarItems() public async void LoadAsync(object? sender, FileSystemEventArgs e) => await LoadAsync(); + public async Task LoadAsync() => await UpdateItemsWithExplorer(); } diff --git a/src/Files.App/DataModels/SuggestionModel.cs b/src/Files.App/DataModels/SuggestionModel.cs index 23e4f94966ed..72982e4b29b5 100644 --- a/src/Files.App/DataModels/SuggestionModel.cs +++ b/src/Files.App/DataModels/SuggestionModel.cs @@ -8,13 +8,16 @@ namespace Files.App.DataModels public class SuggestionModel : ObservableObject { public bool IsRecentSearch { get; set; } = false; + public bool LoadFileIcon { get; set; } = false; + public bool NeedsPlaceholderGlyph { get; set; } = true; + public string? ItemPath { get; set; } + public string Name { get; set; } private BitmapImage? fileImage; - public BitmapImage? FileImage { get => fileImage; @@ -24,6 +27,7 @@ public BitmapImage? FileImage { imgOld.ImageOpened -= Img_ImageOpened; } + if (SetProperty(ref fileImage, value)) { if (value is BitmapImage img) diff --git a/src/Files.App/EventArguments/Bundles/BundlesOpenPathEventArgs.cs b/src/Files.App/EventArguments/Bundles/BundlesOpenPathEventArgs.cs index e760e21f41c8..9ebe4528af51 100644 --- a/src/Files.App/EventArguments/Bundles/BundlesOpenPathEventArgs.cs +++ b/src/Files.App/EventArguments/Bundles/BundlesOpenPathEventArgs.cs @@ -24,4 +24,4 @@ public BundlesOpenPathEventArgs(string path, FilesystemItemType itemType, bool o this.selectItems = selectItems; } } -} \ No newline at end of file +} diff --git a/src/Files.App/EventArguments/LayoutModeEventArgs.cs b/src/Files.App/EventArguments/LayoutModeEventArgs.cs index 5ef9069b2b88..32114e63d649 100644 --- a/src/Files.App/EventArguments/LayoutModeEventArgs.cs +++ b/src/Files.App/EventArguments/LayoutModeEventArgs.cs @@ -14,4 +14,4 @@ internal LayoutModeEventArgs(FolderLayoutModes layoutMode, int gridViewSize) GridViewSize = gridViewSize; } } -} \ No newline at end of file +} diff --git a/src/Files.App/EventArguments/LayoutPreferenceEventArgs.cs b/src/Files.App/EventArguments/LayoutPreferenceEventArgs.cs index cde14e2b0468..8bedf9abbba2 100644 --- a/src/Files.App/EventArguments/LayoutPreferenceEventArgs.cs +++ b/src/Files.App/EventArguments/LayoutPreferenceEventArgs.cs @@ -14,4 +14,4 @@ internal LayoutPreferenceEventArgs(LayoutPreferences layoutPref) internal LayoutPreferenceEventArgs(LayoutPreferences layoutPref, bool isAdaptiveLayoutUpdateRequired) => (LayoutPreference, IsAdaptiveLayoutUpdateRequired) = (layoutPref, isAdaptiveLayoutUpdateRequired); } -} \ No newline at end of file +} diff --git a/src/Files.App/Extensions/EnumExtensions.cs b/src/Files.App/Extensions/EnumExtensions.cs index 979eab3cf659..849c4c2ce255 100644 --- a/src/Files.App/Extensions/EnumExtensions.cs +++ b/src/Files.App/Extensions/EnumExtensions.cs @@ -11,7 +11,8 @@ public static TEnum GetEnum(string text) where TEnum : struct { throw new InvalidOperationException("Generic parameter 'TEnum' must be an enum."); } + return (TEnum)Enum.Parse(typeof(TEnum), text); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Extensions/ImageSourceExtensions.cs b/src/Files.App/Extensions/ImageSourceExtensions.cs index b961e942c8e5..8eebb410c0ed 100644 --- a/src/Files.App/Extensions/ImageSourceExtensions.cs +++ b/src/Files.App/Extensions/ImageSourceExtensions.cs @@ -18,6 +18,7 @@ internal static async Task ToByteArrayAsync(this IInputStream stream) } using var readStream = stream.AsStreamForRead(); + return await readStream.ToByteArrayAsync(); } @@ -32,6 +33,7 @@ internal static async Task ToByteArrayAsync(this StorageFile file) { var bytes = new byte[fileStream.Size]; await fileStream.ReadAsync(bytes.AsBuffer(), (uint)fileStream.Size, InputStreamOptions.None); + return bytes; } } @@ -39,6 +41,7 @@ internal static async Task ToByteArrayAsync(this StorageFile file) private static async Task ToByteArrayAsync(this Stream stream, CancellationToken cancellationToken = default) { MemoryStream memoryStream; + if (stream.CanSeek) { var length = stream.Length - stream.Position; @@ -52,8 +55,9 @@ private static async Task ToByteArrayAsync(this Stream stream, Cancellat using (memoryStream) { await stream.CopyToAsync(memoryStream, bufferSize: 81920, cancellationToken).ConfigureAwait(false); + return memoryStream.ToArray(); } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Extensions/KeyboardAcceleratorExtensions.cs b/src/Files.App/Extensions/KeyboardAcceleratorExtensions.cs index 2d82e4d840e4..d18b32a5efef 100644 --- a/src/Files.App/Extensions/KeyboardAcceleratorExtensions.cs +++ b/src/Files.App/Extensions/KeyboardAcceleratorExtensions.cs @@ -8,10 +8,15 @@ public static class KeyboardAcceleratorExtensions { public static bool CheckIsPressed(this KeyboardAccelerator keyboardAccelerator) { - return Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(keyboardAccelerator.Key).HasFlag(CoreVirtualKeyStates.Down) && // check if the main key is pressed - (!keyboardAccelerator.Modifiers.HasFlag(VirtualKeyModifiers.Menu) || Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down)) && // check if menu (alt) key is a modifier, and if so check if it's pressed - (!keyboardAccelerator.Modifiers.HasFlag(VirtualKeyModifiers.Shift) || Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down)) && // check if shift key is a modifier, and if so check if it's pressed - (!keyboardAccelerator.Modifiers.HasFlag(VirtualKeyModifiers.Control) || Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down)); // check if ctrl key is a modifier, and if so check if it's pressed + return + // Check if the main key is pressed + Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(keyboardAccelerator.Key).HasFlag(CoreVirtualKeyStates.Down) && + // Check if menu (alt) key is a modifier, and if so check if it's pressed + (!keyboardAccelerator.Modifiers.HasFlag(VirtualKeyModifiers.Menu) || Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down)) && + // Check if shift key is a modifier, and if so check if it's pressed + (!keyboardAccelerator.Modifiers.HasFlag(VirtualKeyModifiers.Shift) || Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down)) && + // Check if ctrl key is a modifier, and if so check if it's pressed + (!keyboardAccelerator.Modifiers.HasFlag(VirtualKeyModifiers.Control) || Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down)); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Extensions/ShellNewEntryExtensions.cs b/src/Files.App/Extensions/ShellNewEntryExtensions.cs index 0a0a3a05c439..a1e318aa032a 100644 --- a/src/Files.App/Extensions/ShellNewEntryExtensions.cs +++ b/src/Files.App/Extensions/ShellNewEntryExtensions.cs @@ -17,11 +17,13 @@ public static class ShellNewEntryExtensions public static async Task> GetNewContextMenuEntries() { var shellEntryList = new List(); + var entries = await SafetyExtensions.IgnoreExceptions(() => ShellNewMenuHelper.GetNewContextMenuEntries(), App.Logger); if (entries is not null) { shellEntryList.AddRange(entries); } + return shellEntryList; } @@ -37,6 +39,7 @@ public static async Task> Create(this ShellNew { return await Create(shellEntry, parentFolder, filePath); } + return new FilesystemResult(null, parentFolder.ErrorCode); } @@ -44,6 +47,7 @@ public static async Task> Create(this ShellNew { FilesystemResult createdFile = null; var fileName = Path.GetFileName(filePath); + if (shellEntry.Template is null) { createdFile = await FilesystemTasks.Wrap(() => parentFolder.CreateFileAsync(fileName, CreationCollisionOption.GenerateUniqueName).AsTask()); @@ -53,13 +57,16 @@ public static async Task> Create(this ShellNew createdFile = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(shellEntry.Template)) .OnSuccess(t => t.CopyAsync(parentFolder, fileName, NameCollisionOption.GenerateUniqueName).AsTask()); } - if (createdFile && - shellEntry.Data is not null) + + if (createdFile && shellEntry.Data is not null) { - //await FileIO.WriteBytesAsync(createdFile.Result, shellEntry.Data); // Calls unsupported OpenTransactedWriteAsync + // Calls unsupported OpenTransactedWriteAsync + //await FileIO.WriteBytesAsync(createdFile.Result, shellEntry.Data); + await createdFile.Result.WriteBytesAsync(shellEntry.Data); } + return createdFile; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Extensions/StringExtensions.cs b/src/Files.App/Extensions/StringExtensions.cs index ac2d2588ba15..7d5ff6adeb53 100644 --- a/src/Files.App/Extensions/StringExtensions.cs +++ b/src/Files.App/Extensions/StringExtensions.cs @@ -17,11 +17,9 @@ public static class StringExtensions /// public static bool IsSubPathOf(this string path, string baseDirPath) { - string normalizedPath = Path.GetFullPath(path.Replace('/', '\\') - .WithEnding("\\")); + string normalizedPath = Path.GetFullPath(path.Replace('/', '\\').WithEnding("\\")); - string normalizedBaseDirPath = Path.GetFullPath(baseDirPath.Replace('/', '\\') - .WithEnding("\\")); + string normalizedBaseDirPath = Path.GetFullPath(baseDirPath.Replace('/', '\\').WithEnding("\\")); return normalizedPath.StartsWith(normalizedBaseDirPath, StringComparison.OrdinalIgnoreCase); } @@ -56,9 +54,10 @@ public static string WithEnding(this string str, string ending) } private static readonly ResourceMap resourcesTree = new ResourceManager().MainResourceMap.TryGetSubtree("Resources"); - private static readonly ConcurrentDictionary cachedResources = new ConcurrentDictionary(); - private static readonly Dictionary abbreviations = new Dictionary() + private static readonly ConcurrentDictionary cachedResources = new(); + + private static readonly Dictionary abbreviations = new() { { "KiB", "KiloByteSymbol".GetLocalizedResource() }, { "MiB", "MegaByteSymbol".GetLocalizedResource() }, @@ -75,6 +74,7 @@ public static string ConvertSizeAbbreviation(this string value) { value = value.Replace(item.Key, item.Value, StringComparison.Ordinal); } + return value; } @@ -94,7 +94,9 @@ public static string GetLocalizedResource(this string resourceKey) { return value; } + value = resourcesTree?.TryGetValue(resourceKey)?.ValueAsString; + return cachedResources[resourceKey] = value ?? string.Empty; } } diff --git a/src/Files.App/IAddressToolbar.cs b/src/Files.App/IAddressToolbar.cs index 422235eccfd5..6c52cdca74ec 100644 --- a/src/Files.App/IAddressToolbar.cs +++ b/src/Files.App/IAddressToolbar.cs @@ -10,14 +10,23 @@ namespace Files.App.UserControls public interface IAddressToolbar { public bool IsSearchBoxVisible { get; set; } + public bool IsEditModeEnabled { get; set; } + public bool CanRefresh { get; set; } + public bool CanCopyPathInPage { get; set; } + public bool CanNavigateToParent { get; set; } + public bool CanGoBack { get; set; } + public bool CanGoForward { get; set; } + public bool IsSingleItemOverride { get; set; } + public string PathControlDisplayText { get; set; } + public ObservableCollection PathComponents { get; } public delegate void ToolbarQuerySubmittedEventHandler(object sender, ToolbarQuerySubmittedEventArgs e); @@ -53,7 +62,9 @@ public class ToolbarQuerySubmittedEventArgs public class PathNavigationEventArgs { public string ItemPath { get; set; } + public string ItemName { get; set; } + public bool IsFile { get; set; } } @@ -65,6 +76,7 @@ public class ToolbarFlyoutOpenedEventArgs public class ToolbarPathItemLoadedEventArgs { public MenuFlyout OpenedFlyout { get; set; } + public PathBoxItem Item { get; set; } } @@ -76,8 +88,11 @@ public class AddressBarTextEnteredEventArgs public class PathBoxItemDroppedEventArgs { public DataPackageView Package { get; set; } + public string Path { get; set; } + public DataPackageOperation AcceptedOperation { get; set; } + public AsyncManualResetEvent SignalEvent { get; set; } } -} \ No newline at end of file +} diff --git a/src/Files.App/IBaseLayout.cs b/src/Files.App/IBaseLayout.cs index a40f9e5633d1..988e255c8ee0 100644 --- a/src/Files.App/IBaseLayout.cs +++ b/src/Files.App/IBaseLayout.cs @@ -24,8 +24,11 @@ public interface IBaseLayout : IDisposable PreviewPaneViewModel PreviewPaneViewModel { get; } public SelectedItemsPropertiesViewModel SelectedItemsPropertiesViewModel { get; } + public DirectoryPropertiesViewModel DirectoryPropertiesViewModel { get; } + public BaseLayoutCommandsViewModel? CommandsViewModel { get; } + public CommandBarFlyout ItemContextMenuFlyout { get; set; } } } diff --git a/src/Files.App/ISearchBox.cs b/src/Files.App/ISearchBox.cs index 1cc4d2598e91..3151327f0fc7 100644 --- a/src/Files.App/ISearchBox.cs +++ b/src/Files.App/ISearchBox.cs @@ -9,7 +9,9 @@ namespace Files.App public interface ISearchBox { event TypedEventHandler TextChanged; + event TypedEventHandler QuerySubmitted; + event EventHandler Escaped; bool WasQuerySubmitted { get; set; } @@ -27,7 +29,8 @@ public class SearchBoxTextChangedEventArgs { public SearchBoxTextChangeReason Reason { get; } - public SearchBoxTextChangedEventArgs(SearchBoxTextChangeReason reason) => Reason = reason; + public SearchBoxTextChangedEventArgs(SearchBoxTextChangeReason reason) + => Reason = reason; public SearchBoxTextChangedEventArgs(AutoSuggestionBoxTextChangeReason reason) { @@ -44,7 +47,8 @@ public class SearchBoxQuerySubmittedEventArgs { public SuggestionModel ChosenSuggestion { get; } - public SearchBoxQuerySubmittedEventArgs(SuggestionModel chosenSuggestion) => ChosenSuggestion = chosenSuggestion; + public SearchBoxQuerySubmittedEventArgs(SuggestionModel chosenSuggestion) + => ChosenSuggestion = chosenSuggestion; } public enum SearchBoxTextChangeReason : ushort @@ -53,4 +57,4 @@ public enum SearchBoxTextChangeReason : ushort ProgrammaticChange, SuggestionChosen, } -} \ No newline at end of file +} diff --git a/src/Files.App/IShellPage.cs b/src/Files.App/IShellPage.cs index e3ac77b0f0de..0bd184f1e0bc 100644 --- a/src/Files.App/IShellPage.cs +++ b/src/Files.App/IShellPage.cs @@ -60,8 +60,12 @@ public interface IShellPage : ITabItemContent, IMultiPaneInfo, IDisposable public interface IPaneHolder : IDisposable, INotifyPropertyChanged { public IShellPage ActivePane { get; set; } - public IShellPage ActivePaneOrColumn { get; } // if column view, returns the last column shell page, otherwise returns the active pane normally + + // If column view, returns the last column shell page, otherwise returns the active pane normally + public IShellPage ActivePaneOrColumn { get; } + public IFilesystemHelpers FilesystemHelpers { get; } + public TabItemArguments TabItemArguments { get; set; } public void OpenPathInNewPane(string path); @@ -69,15 +73,21 @@ public interface IPaneHolder : IDisposable, INotifyPropertyChanged public void CloseActivePane(); public bool IsLeftPaneActive { get; } + public bool IsRightPaneActive { get; } - public bool IsMultiPaneActive { get; } // Another pane is shown - public bool IsMultiPaneEnabled { get; } // Multi pane is enabled + // Another pane is shown + public bool IsMultiPaneActive { get; } + + // Multi pane is enabled + public bool IsMultiPaneEnabled { get; } } public interface IMultiPaneInfo { - public bool IsPageMainPane { get; } // The instance is the left (or only) pane + // The instance is the left (or only) pane + public bool IsPageMainPane { get; } + public IPaneHolder PaneHolder { get; } } } \ No newline at end of file diff --git a/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs b/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs index 1d6e7a02a4e2..a7dff68eac2c 100644 --- a/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs +++ b/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs @@ -89,8 +89,8 @@ public virtual void RenameItem(RoutedEventArgs e) public virtual async void CreateShortcut(RoutedEventArgs e) { var currentPath = associatedInstance.FilesystemViewModel.WorkingDirectory; - if (App.LibraryManager.TryGetLibrary(currentPath, out var library) && - !library.IsEmpty) + + if (App.LibraryManager.TryGetLibrary(currentPath, out var library) && !library.IsEmpty) { currentPath = library.DefaultSaveFolder; } @@ -196,7 +196,8 @@ public virtual async void DeleteItem(RoutedEventArgs e) await RecycleBinHelpers.DeleteItem(associatedInstance); } - public virtual void ShowFolderProperties(RoutedEventArgs e) => ShowProperties(e); + public virtual void ShowFolderProperties(RoutedEventArgs e) + => ShowProperties(e); public virtual void ShowProperties(RoutedEventArgs e) { @@ -247,6 +248,7 @@ public virtual void OpenParentFolder(RoutedEventArgs e) { var item = SlimContentPage.SelectedItem; var folderPath = Path.GetDirectoryName(item.ItemPath.TrimEnd('\\')); + associatedInstance.NavigateWithArguments(associatedInstance.InstanceViewModel.FolderSettings.GetLayoutType(folderPath), new NavigationArguments() { NavPathParam = folderPath, @@ -267,7 +269,8 @@ public virtual async void OpenDirectoryInNewTab(RoutedEventArgs e) await App.Window.DispatcherQueue.EnqueueAsync(async () => { await MainPageViewModel.AddNewTabByPathAsync(typeof(PaneHolderPage), (listedItem as ShortcutItem)?.TargetPath ?? listedItem.ItemPath); - }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + }, + Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } } @@ -279,6 +282,7 @@ public virtual void OpenDirectoryInNewPane(RoutedEventArgs e) public virtual async void OpenInNewWindowItem(RoutedEventArgs e) { List items = SlimContentPage.SelectedItems; + foreach (ListedItem listedItem in items) { var selectedItemPath = (listedItem as ShortcutItem)?.TargetPath ?? listedItem.ItemPath; @@ -316,6 +320,7 @@ public virtual void CopyPathOfSelectedItem(RoutedEventArgs e) path = path.Replace("\\", "/", StringComparison.Ordinal); DataPackage data = new(); data.SetText(path); + Clipboard.SetContent(data); Clipboard.Flush(); } @@ -330,6 +335,7 @@ public virtual void ShareItem(RoutedEventArgs e) { var interop = DataTransferManager.As(); IntPtr result = interop.GetForWindow(App.WindowHandle, UWPToWinAppSDKUpgradeHelpers.InteropHelpers.DataTransferManagerInteropIID); + var manager = WinRT.MarshalInterface.FromAbi(result); manager.DataRequested += new TypedEventHandler(Manager_DataRequested); @@ -341,8 +347,8 @@ async void Manager_DataRequested(DataTransferManager sender, DataRequestedEventA List items = new(); DataRequest dataRequest = args.Request; - /*dataRequest.Data.Properties.Title = "Data Shared From Files"; - dataRequest.Data.Properties.Description = "The items you selected will be shared";*/ + //dataRequest.Data.Properties.Title = "Data Shared From Files"; + //dataRequest.Data.Properties.Description = "The items you selected will be shared"; foreach (ListedItem item in SlimContentPage.SelectedItems) { @@ -354,6 +360,7 @@ async void Manager_DataRequested(DataTransferManager sender, DataRequestedEventA dataRequest.Data.Properties.Description = "ShareDialogSingleItemDescription".GetLocalizedResource(); dataRequest.Data.SetWebLink(new Uri(shItem.TargetPath)); dataRequestDeferral.Complete(); + return; } } @@ -378,6 +385,7 @@ async void Manager_DataRequested(DataTransferManager sender, DataRequestedEventA { dataRequest.FailWithDisplayText("ShareDialogFailMessage".GetLocalizedResource()); dataRequestDeferral.Complete(); + return; } else @@ -403,9 +411,9 @@ public virtual async void ItemPointerPressed(PointerRoutedEventArgs e) { if (e.GetCurrentPoint(null).Properties.IsMiddleButtonPressed) { - if ((e.OriginalSource as FrameworkElement)?.DataContext is ListedItem Item && Item.PrimaryItemAttribute == StorageItemTypes.Folder) + // If a folder item was clicked, disable middle mouse click to scroll to cancel the mouse scrolling state and re-enable it + if (e.OriginalSource is FrameworkElement { DataContext: ListedItem Item } && Item.PrimaryItemAttribute == StorageItemTypes.Folder) { - // If a folder item was clicked, disable middle mouse click to scroll to cancel the mouse scrolling state and re-enable it SlimContentPage.IsMiddleClickToScrollEnabled = false; SlimContentPage.IsMiddleClickToScrollEnabled = true; @@ -447,9 +455,11 @@ public virtual void PointerWheelChanged(PointerRoutedEventArgs e) { if (e.KeyModifiers == VirtualKeyModifiers.Control) { - if (e.GetCurrentPoint(null).Properties.MouseWheelDelta < 0) // Mouse wheel down + // Mouse wheel down + if (e.GetCurrentPoint(null).Properties.MouseWheelDelta < 0) GridViewSizeDecrease(null); - else // Mouse wheel up + // Mouse wheel up + else GridViewSizeIncrease(null); e.Handled = true; @@ -458,16 +468,20 @@ public virtual void PointerWheelChanged(PointerRoutedEventArgs e) public virtual void GridViewSizeDecrease(KeyboardAcceleratorInvokedEventArgs e) { + // Make Smaller if (associatedInstance.IsCurrentInstance) - associatedInstance.InstanceViewModel.FolderSettings.GridViewSize = associatedInstance.InstanceViewModel.FolderSettings.GridViewSize - Constants.Browser.GridViewBrowser.GridViewIncrement; // Make Smaller + associatedInstance.InstanceViewModel.FolderSettings.GridViewSize = associatedInstance.InstanceViewModel.FolderSettings.GridViewSize - Constants.Browser.GridViewBrowser.GridViewIncrement; + if (e is not null) e.Handled = true; } public virtual void GridViewSizeIncrease(KeyboardAcceleratorInvokedEventArgs e) { + // Make Larger if (associatedInstance.IsCurrentInstance) - associatedInstance.InstanceViewModel.FolderSettings.GridViewSize = associatedInstance.InstanceViewModel.FolderSettings.GridViewSize + Constants.Browser.GridViewBrowser.GridViewIncrement; // Make Larger + associatedInstance.InstanceViewModel.FolderSettings.GridViewSize = associatedInstance.InstanceViewModel.FolderSettings.GridViewSize + Constants.Browser.GridViewBrowser.GridViewIncrement; + if (e is not null) e.Handled = true; } @@ -526,8 +540,10 @@ public virtual async Task DragOver(DragEventArgs e) e.DragUIOverride.Caption = string.Format("MoveToFolderCaptionText".GetLocalizedResource(), folderName); e.AcceptedOperation = DataPackageOperation.Move; } - else if (draggedItems.Any(x => x.Item is ZipStorageFile || x.Item is ZipStorageFolder) - || ZipStorageFolder.IsZipPath(pwd)) + else if (draggedItems.Any(x => + x.Item is ZipStorageFile || + x.Item is ZipStorageFolder) || + ZipStorageFolder.IsZipPath(pwd)) { e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); e.AcceptedOperation = DataPackageOperation.Copy; @@ -638,10 +654,12 @@ public async Task CompressIntoSevenZip() string[] sources = associatedInstance.SlimContentPage.SelectedItems .Select(item => item.ItemPath) .ToArray(); + if (sources.Length is 0) return (sources, string.Empty, string.Empty); string directory = associatedInstance.FilesystemViewModel.WorkingDirectory.Normalize(); + if (App.LibraryManager.TryGetLibrary(directory, out var library) && !library.IsEmpty) directory = library.DefaultSaveFolder; @@ -664,11 +682,12 @@ private static async Task CompressArchiveAsync(IArchiveCreator creator) FileOperationType.Compressed, compressionToken ); - creator.Progress = banner.ProgressEventSource; + creator.Progress = banner.ProgressEventSource; bool isSuccess = await creator.RunCreationAsync(); banner.Remove(); + if (isSuccess) { App.OngoingTasksViewModel.PostBanner @@ -694,6 +713,7 @@ private static async Task CompressArchiveAsync(IArchiveCreator creator) ); } } + public async Task DecompressArchive() { BaseStorageFile archive = await StorageHelpers.ToStorageItem(associatedInstance.SlimContentPage.SelectedItems.Count != 0 @@ -756,11 +776,13 @@ public async Task DecompressArchiveHere() IsArchiveEncrypted = true, ShowPathSelection = false }; + decompressArchiveDialog.ViewModel = decompressArchiveViewModel; ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); if (option != ContentDialogResult.Primary) return; + password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); } @@ -773,6 +795,7 @@ public async Task DecompressArchiveToChildFolder() foreach (var selectedItem in associatedInstance.SlimContentPage.SelectedItems) { var password = string.Empty; + BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(associatedInstance.FilesystemViewModel.CurrentFolder.ItemPath); BaseStorageFolder destinationFolder = null; @@ -790,6 +813,7 @@ public async Task DecompressArchiveToChildFolder() ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); if (option != ContentDialogResult.Primary) return; + password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); } @@ -806,6 +830,7 @@ private static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFol return; CancellationTokenSource extractCancellation = new(); + PostedStatusBanner banner = App.OngoingTasksViewModel.PostOperationBanner( archive.Name.Length >= 30 ? archive.Name + "\n" : archive.Name, "ExtractingArchiveText".GetLocalizedResource(), @@ -867,4 +892,4 @@ public Task InstallFont() #endregion Command Implementation } -} \ No newline at end of file +} diff --git a/src/Files.App/Interacts/BaseLayoutCommandsViewModel.cs b/src/Files.App/Interacts/BaseLayoutCommandsViewModel.cs index 94ad861040bf..20ef5bdc687c 100644 --- a/src/Files.App/Interacts/BaseLayoutCommandsViewModel.cs +++ b/src/Files.App/Interacts/BaseLayoutCommandsViewModel.cs @@ -208,4 +208,4 @@ public void Dispose() #endregion IDisposable } -} \ No newline at end of file +} diff --git a/src/Files.App/Interacts/IBaseLayoutCommandImplementationModel.cs b/src/Files.App/Interacts/IBaseLayoutCommandImplementationModel.cs index 32879e80e6f3..5765e5fcb947 100644 --- a/src/Files.App/Interacts/IBaseLayoutCommandImplementationModel.cs +++ b/src/Files.App/Interacts/IBaseLayoutCommandImplementationModel.cs @@ -118,4 +118,4 @@ public interface IBaseLayoutCommandImplementationModel : IDisposable Task InstallFont(); } -} \ No newline at end of file +} diff --git a/src/Files.App/Interacts/IStatusCenterActions.cs b/src/Files.App/Interacts/IStatusCenterActions.cs index 15314f26dd98..f84da6e4a342 100644 --- a/src/Files.App/Interacts/IStatusCenterActions.cs +++ b/src/Files.App/Interacts/IStatusCenterActions.cs @@ -66,4 +66,4 @@ public interface IOngoingTasksActions /// void UpdateBanner(StatusBanner banner); } -} \ No newline at end of file +} diff --git a/src/Files.App/Interacts/ItemManipulationModel.cs b/src/Files.App/Interacts/ItemManipulationModel.cs index 44fa8fe56d3f..d9e7275e6ff8 100644 --- a/src/Files.App/Interacts/ItemManipulationModel.cs +++ b/src/Files.App/Interacts/ItemManipulationModel.cs @@ -125,4 +125,4 @@ public void RefreshItemsThumbnail() RefreshItemsThumbnailInvoked?.Invoke(this, EventArgs.Empty); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Interacts/RemovableDevice.cs b/src/Files.App/Interacts/RemovableDevice.cs index 6c2b83b7f19b..e3e9c5c66967 100644 --- a/src/Files.App/Interacts/RemovableDevice.cs +++ b/src/Files.App/Interacts/RemovableDevice.cs @@ -13,7 +13,9 @@ public class RemovableDevice public RemovableDevice(string letter) { driveLetter = letter[0]; + string filename = @"\\.\" + driveLetter + ":"; + handle = CreateFileFromAppW(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, @@ -35,7 +37,9 @@ public async Task EjectAsync() PreventRemovalOfVolume(false); result = AutoEjectVolume(); } + CloseVolume(); + return result; } @@ -49,12 +53,14 @@ private async Task LockVolumeAsync() { Debug.WriteLine("Lock successful!"); result = true; + break; } else { Debug.WriteLine($"Can't lock device, attempt {i + 1}, trying again... "); } + await Task.Delay(500); } @@ -70,6 +76,7 @@ private bool PreventRemovalOfVolume(bool prevent) { byte[] buf = new byte[1]; buf[0] = prevent ? (byte)1 : (byte)0; + return DeviceIoControl(handle, IOCTL_STORAGE_MEDIA_REMOVAL, buf, 1, IntPtr.Zero, 0, out _, IntPtr.Zero); } @@ -83,4 +90,4 @@ private bool CloseVolume() return CloseHandle(handle); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Program.cs b/src/Files.App/Program.cs index c014d5b25544..5eac555409f7 100644 --- a/src/Files.App/Program.cs +++ b/src/Files.App/Program.cs @@ -21,9 +21,10 @@ namespace Files.App internal class Program { // Note: We can't declare Main to be async because in a WinUI app - // this prevents Narrator from reading XAML elements. + // This prevents Narrator from reading XAML elements // https://github.com/microsoft/WindowsAppSDK-Samples/blob/main/Samples/AppLifecycle/Instancing/cs-winui-packaged/CsWinUiDesktopInstancing/CsWinUiDesktopInstancing/Program.cs - [STAThread] // STAThread has no effect if main is async, needed for Clipboard + // STAThread has no effect if main is async, needed for Clipboard + [STAThread] private static void Main() { WinRT.ComWrappersSupport.InitializeComWrappers(); @@ -48,9 +49,13 @@ private static void Main() if (!CommonPaths.ShellPlaces.ContainsKey(command.Payload.ToUpperInvariant())) { OpenShellCommandInExplorer(command.Payload, proc.Id); - return; // Exit + + // Exit + return; } + break; + default: break; } @@ -58,11 +63,12 @@ private static void Main() } // Always open a new instance for OpenDialog, never open new instance for "-Tag" command - if (parsedCommands is null || !parsedCommands.Any(x => x.Type == ParsedCommandType.OutputPath) - && (!alwaysOpenNewInstance || parsedCommands.Any(x => x.Type == ParsedCommandType.TagFiles))) + if (parsedCommands is null || !parsedCommands.Any(x => x.Type == ParsedCommandType.OutputPath) && + (!alwaysOpenNewInstance || parsedCommands.Any(x => x.Type == ParsedCommandType.TagFiles))) { var activePid = ApplicationData.Current.LocalSettings.Values.Get("INSTANCE_ACTIVE", -1); var instance = AppInstance.FindOrRegisterForKey(activePid.ToString()); + if (!instance.IsCurrent) { RedirectActivationTo(instance, activatedArgs); @@ -143,7 +149,7 @@ private static void OnActivated(object? sender, AppActivationArguments args) { if (App.Current is App thisApp) { - // WINUI3: verify if needed or OnLaunched is called + // WINUI3: Verify if needed or OnLaunched is called thisApp.OnActivated(args); } } @@ -151,20 +157,23 @@ private static void OnActivated(object? sender, AppActivationArguments args) private const uint CWMO_DEFAULT = 0; private const uint INFINITE = 0xFFFFFFFF; - // Do the redirection on another thread, and use a non-blocking - // wait method to wait for the redirection to complete. - public static void RedirectActivationTo( - AppInstance keyInstance, AppActivationArguments args) + // Do the redirection on another thread, and use a non-blocking wait method to wait for the redirection to complete + public static void RedirectActivationTo(AppInstance keyInstance, AppActivationArguments args) { IntPtr eventHandle = CreateEvent(IntPtr.Zero, true, false, null); + Task.Run(() => { keyInstance.RedirectActivationToAsync(args).AsTask().Wait(); SetEvent(eventHandle); }); + _ = CoWaitForMultipleObjects( - CWMO_DEFAULT, INFINITE, 1, - new IntPtr[] { eventHandle }, out uint handleIndex); + CWMO_DEFAULT, + INFINITE, + 1, + new IntPtr[] { eventHandle }, + out uint handleIndex); } public static void OpenShellCommandInExplorer(string shellCommand, int pid) @@ -173,14 +182,19 @@ public static void OpenShellCommandInExplorer(string shellCommand, int pid) public static void OpenFileFromTile(string filePath) { IntPtr eventHandle = CreateEvent(IntPtr.Zero, true, false, null); + Task.Run(() => { LaunchHelper.LaunchAppAsync(filePath, null, null).Wait(); SetEvent(eventHandle); }); + _ = CoWaitForMultipleObjects( - CWMO_DEFAULT, INFINITE, 1, - new IntPtr[] { eventHandle }, out uint handleIndex); + CWMO_DEFAULT, + INFINITE, + 1, + new IntPtr[] { eventHandle }, + out uint handleIndex); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Serialization/BaseJsonSettings.cs b/src/Files.App/Serialization/BaseJsonSettings.cs index 2752d432151f..56f1f3662833 100644 --- a/src/Files.App/Serialization/BaseJsonSettings.cs +++ b/src/Files.App/Serialization/BaseJsonSettings.cs @@ -111,4 +111,4 @@ protected virtual void RaiseOnSettingChangedEvent(object sender, SettingChangedE _settingsSharingContext?.Instance.RaiseOnSettingChangedEvent(sender, e); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Serialization/BaseObservableJsonSettings.cs b/src/Files.App/Serialization/BaseObservableJsonSettings.cs index f9d60ba6fee3..fca473700397 100644 --- a/src/Files.App/Serialization/BaseObservableJsonSettings.cs +++ b/src/Files.App/Serialization/BaseObservableJsonSettings.cs @@ -13,10 +13,11 @@ internal abstract class BaseObservableJsonSettings : BaseJsonSettings, INotifyPr return false; OnPropertyChanged(propertyName); + return true; } protected void OnPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } -} \ No newline at end of file +} diff --git a/src/Files.App/Serialization/IJsonSettingsSerializer.cs b/src/Files.App/Serialization/IJsonSettingsSerializer.cs index 31c66429e45e..08c2ee38f0a5 100644 --- a/src/Files.App/Serialization/IJsonSettingsSerializer.cs +++ b/src/Files.App/Serialization/IJsonSettingsSerializer.cs @@ -6,4 +6,4 @@ internal interface IJsonSettingsSerializer T? DeserializeFromJson(string json); } -} \ No newline at end of file +} diff --git a/src/Files.App/Serialization/ISettingsSerializer.cs b/src/Files.App/Serialization/ISettingsSerializer.cs index 7f5470381037..4c37379cb14b 100644 --- a/src/Files.App/Serialization/ISettingsSerializer.cs +++ b/src/Files.App/Serialization/ISettingsSerializer.cs @@ -8,4 +8,4 @@ internal interface ISettingsSerializer bool WriteToFile(string? text); } -} \ No newline at end of file +} diff --git a/src/Files.App/Serialization/ISettingsSharingContext.cs b/src/Files.App/Serialization/ISettingsSharingContext.cs index 218b2bbf393c..93ced477d180 100644 --- a/src/Files.App/Serialization/ISettingsSharingContext.cs +++ b/src/Files.App/Serialization/ISettingsSharingContext.cs @@ -4,4 +4,4 @@ public interface ISettingsSharingContext { internal BaseJsonSettings Instance { get; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Serialization/Implementation/CachingJsonSettingsDatabase.cs b/src/Files.App/Serialization/Implementation/CachingJsonSettingsDatabase.cs index 18809be65a21..7cac7729459f 100644 --- a/src/Files.App/Serialization/Implementation/CachingJsonSettingsDatabase.cs +++ b/src/Files.App/Serialization/Implementation/CachingJsonSettingsDatabase.cs @@ -24,9 +24,8 @@ public CachingJsonSettingsDatabase(ISettingsSerializer settingsSerializer, IJson else { if (base.SetValue(key, defaultValue)) - { _settingsCache.Add(key, defaultValue); - } + return defaultValue; } } @@ -38,16 +37,16 @@ public CachingJsonSettingsDatabase(ISettingsSerializer settingsSerializer, IJson if (!_settingsCache.ContainsKey(key)) { _settingsCache.Add(key, newValue); + return SaveSettings(_settingsCache); } else - { return UpdateValueInCache(_settingsCache[key]); - } bool UpdateValueInCache(object? value) { bool isDifferent; + if (newValue is IEnumerable enumerableNewValue && value is IEnumerable enumerableValue) { isDifferent = !enumerableValue.Cast().SequenceEqual(enumerableNewValue.Cast()); @@ -61,6 +60,7 @@ bool UpdateValueInCache(object? value) { // Values are different, update the value and reload the cache. _settingsCache[key] = newValue; + return SaveSettings(_settingsCache); } else @@ -83,8 +83,10 @@ public override bool ImportSettings(object? import) if (base.ImportSettings(import)) { _settingsCache = GetFreshSettings(); + return true; } + return false; } } diff --git a/src/Files.App/ServicesImplementation/DateTimeFormatter/AbstractDateTimeFormatter.cs b/src/Files.App/ServicesImplementation/DateTimeFormatter/AbstractDateTimeFormatter.cs index a3529092f90a..6c4be7b427cc 100644 --- a/src/Files.App/ServicesImplementation/DateTimeFormatter/AbstractDateTimeFormatter.cs +++ b/src/Files.App/ServicesImplementation/DateTimeFormatter/AbstractDateTimeFormatter.cs @@ -13,7 +13,9 @@ internal abstract class AbstractDateTimeFormatter : IDateTimeFormatter public abstract string Name { get; } public abstract string ToShortLabel(DateTimeOffset offset); - public virtual string ToLongLabel(DateTimeOffset offset) => ToShortLabel(offset); + + public virtual string ToLongLabel(DateTimeOffset offset) + => ToShortLabel(offset); public ITimeSpanLabel ToTimeSpanLabel(DateTimeOffset offset) { @@ -37,7 +39,8 @@ public ITimeSpanLabel ToTimeSpanLabel(DateTimeOffset offset) }; } - protected static string ToString(DateTimeOffset offset, string format) => offset.ToLocalTime().ToString(format, cultureInfo); + protected static string ToString(DateTimeOffset offset, string format) + => offset.ToLocalTime().ToString(format, cultureInfo); private static int GetWeekOfYear(DateTimeOffset t) { @@ -48,7 +51,9 @@ private static int GetWeekOfYear(DateTimeOffset t) private class Label : ITimeSpanLabel { public string Text { get; } + public string Glyph { get; } + public int Index { get; } public Label(string textKey, string glyph, int index) diff --git a/src/Files.App/ServicesImplementation/DateTimeFormatter/ApplicationDateTimeFormatter.cs b/src/Files.App/ServicesImplementation/DateTimeFormatter/ApplicationDateTimeFormatter.cs index 4f4624139f66..f26aa81ab199 100644 --- a/src/Files.App/ServicesImplementation/DateTimeFormatter/ApplicationDateTimeFormatter.cs +++ b/src/Files.App/ServicesImplementation/DateTimeFormatter/ApplicationDateTimeFormatter.cs @@ -5,7 +5,8 @@ namespace Files.App.ServicesImplementation.DateTimeFormatter { internal class ApplicationDateTimeFormatter : AbstractDateTimeFormatter { - public override string Name => "Application".GetLocalizedResource(); + public override string Name + => "Application".GetLocalizedResource(); public override string ToShortLabel(DateTimeOffset offset) { @@ -37,14 +38,13 @@ public override string ToLongLabel(DateTimeOffset offset) var elapsed = DateTimeOffset.Now - offset; if (offset.Year is <= 1601 or >= 9999) - { return " "; - } + var localTime = offset.ToLocalTime(); + if (elapsed.TotalDays < 7 && elapsed.TotalSeconds >= 0) - { return $"{localTime:D} {localTime:t} ({ToShortLabel(offset)})"; - } + return $"{localTime:D} {localTime:t}"; } } diff --git a/src/Files.App/ServicesImplementation/DateTimeFormatter/SystemDateTimeFormatter.cs b/src/Files.App/ServicesImplementation/DateTimeFormatter/SystemDateTimeFormatter.cs index 42659e5e4d3f..93315102bb9a 100644 --- a/src/Files.App/ServicesImplementation/DateTimeFormatter/SystemDateTimeFormatter.cs +++ b/src/Files.App/ServicesImplementation/DateTimeFormatter/SystemDateTimeFormatter.cs @@ -5,14 +5,14 @@ namespace Files.App.ServicesImplementation.DateTimeFormatter { internal class SystemDateTimeFormatter : AbstractDateTimeFormatter { - public override string Name => "SystemTimeStyle".GetLocalizedResource(); + public override string Name + => "SystemTimeStyle".GetLocalizedResource(); public override string ToShortLabel(DateTimeOffset offset) { if (offset.Year is <= 1601 or >= 9999) - { return " "; - } + return ToString(offset, "g"); } } diff --git a/src/Files.App/ServicesImplementation/DateTimeFormatter/UniversalDateTimeFormatter.cs b/src/Files.App/ServicesImplementation/DateTimeFormatter/UniversalDateTimeFormatter.cs index cb18654377ce..b9315c6615b3 100644 --- a/src/Files.App/ServicesImplementation/DateTimeFormatter/UniversalDateTimeFormatter.cs +++ b/src/Files.App/ServicesImplementation/DateTimeFormatter/UniversalDateTimeFormatter.cs @@ -5,14 +5,14 @@ namespace Files.App.ServicesImplementation.DateTimeFormatter { internal class UniversalDateTimeFormatter : AbstractDateTimeFormatter { - public override string Name => "Universal".GetLocalizedResource(); + public override string Name + => "Universal".GetLocalizedResource(); public override string ToShortLabel(DateTimeOffset offset) { if (offset.Year is <= 1601 or >= 9999) - { return " "; - } + return ToString(offset, "yyyy-MM-dd HH:mm:ss"); } } diff --git a/src/Files.App/ServicesImplementation/DateTimeFormatter/UserDateTimeFormatter.cs b/src/Files.App/ServicesImplementation/DateTimeFormatter/UserDateTimeFormatter.cs index 661251a2787c..05ecf7c455ef 100644 --- a/src/Files.App/ServicesImplementation/DateTimeFormatter/UserDateTimeFormatter.cs +++ b/src/Files.App/ServicesImplementation/DateTimeFormatter/UserDateTimeFormatter.cs @@ -12,32 +12,37 @@ internal class UserDateTimeFormatter : IDateTimeFormatter private IDateTimeFormatter formatter; - public string Name => formatter.Name; + public string Name + => formatter.Name; public UserDateTimeFormatter() { UserSettingsService.OnSettingChangedEvent += UserSettingsService_OnSettingChangedEvent; + Update(); } - public string ToShortLabel(DateTimeOffset offset) => formatter.ToShortLabel(offset); - public string ToLongLabel(DateTimeOffset offset) => formatter.ToLongLabel(offset); + public string ToShortLabel(DateTimeOffset offset) + => formatter.ToShortLabel(offset); + + public string ToLongLabel(DateTimeOffset offset) + => formatter.ToLongLabel(offset); - public ITimeSpanLabel ToTimeSpanLabel(DateTimeOffset offset) => formatter.ToTimeSpanLabel(offset); + public ITimeSpanLabel ToTimeSpanLabel(DateTimeOffset offset) + => formatter.ToTimeSpanLabel(offset); private void Update() { var dateTimeFormat = UserSettingsService.PreferencesSettingsService.DateTimeFormat; var factory = Ioc.Default.GetService(); + formatter = factory.GetDateTimeFormatter(dateTimeFormat); } private void UserSettingsService_OnSettingChangedEvent(object sender, SettingChangedEventArgs e) { if (e.SettingName is nameof(UserSettingsService.PreferencesSettingsService.DateTimeFormat)) - { Update(); - } } } } diff --git a/src/Files.App/ServicesImplementation/FileExplorerService.cs b/src/Files.App/ServicesImplementation/FileExplorerService.cs index 1ba637ff0ecc..98a0faf39213 100644 --- a/src/Files.App/ServicesImplementation/FileExplorerService.cs +++ b/src/Files.App/ServicesImplementation/FileExplorerService.cs @@ -67,6 +67,7 @@ private FileOpenPicker InitializeWithWindow(FileOpenPicker obj) private FolderPicker InitializeWithWindow(FolderPicker obj) { WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle); + return obj; } } diff --git a/src/Files.App/ServicesImplementation/Settings/FileTagsSettingsService.cs b/src/Files.App/ServicesImplementation/Settings/FileTagsSettingsService.cs index 06dfbd024b4f..6269d74fd19e 100644 --- a/src/Files.App/ServicesImplementation/Settings/FileTagsSettingsService.cs +++ b/src/Files.App/ServicesImplementation/Settings/FileTagsSettingsService.cs @@ -48,6 +48,7 @@ public TagViewModel GetTagById(string uid) } var tag = FileTagList.SingleOrDefault(x => x.Uid == uid); + if (!string.IsNullOrEmpty(uid) && tag is null) { tag = new TagViewModel("FileTagUnknown".GetLocalizedResource(), "#9ea3a1", uid); @@ -101,4 +102,4 @@ public override object ExportSettings() return JsonSettingsSerializer.SerializeToJson(FileTagList); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ServicesImplementation/Settings/UserSettingsService.cs b/src/Files.App/ServicesImplementation/Settings/UserSettingsService.cs index 41a57f802d28..b9246c464bcf 100644 --- a/src/Files.App/ServicesImplementation/Settings/UserSettingsService.cs +++ b/src/Files.App/ServicesImplementation/Settings/UserSettingsService.cs @@ -99,6 +99,7 @@ private TSettingsService GetSettingsService(ref TSettingsServi where TSettingsService : class, IBaseSettingsService { settingsServiceMember ??= Ioc.Default.GetService()!; + return settingsServiceMember; } } diff --git a/src/Files.App/ServicesImplementation/SideloadUpdateService.cs b/src/Files.App/ServicesImplementation/SideloadUpdateService.cs index a5388b5eee91..f3fe848ad59b 100644 --- a/src/Files.App/ServicesImplementation/SideloadUpdateService.cs +++ b/src/Files.App/ServicesImplementation/SideloadUpdateService.cs @@ -26,9 +26,6 @@ public sealed class SideloadUpdateService : ObservableObject, IUpdateService, ID private const string SIDELOAD_STABLE = "https://cdn.files.community/files/stable/Files.Package.appinstaller"; private const string SIDELOAD_PREVIEW = "https://cdn.files.community/files/preview/Files.Package.appinstaller"; - private bool _isUpdateAvailable; - private bool _isUpdating; - private readonly HttpClient _client = new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(1) }); private readonly Dictionary _sideloadVersion = new() @@ -43,17 +40,22 @@ public sealed class SideloadUpdateService : ObservableObject, IUpdateService, ID private string PackageName { get; } = Package.Current.Id.Name; - private Version PackageVersion { get; } = new(Package.Current.Id.Version.Major, - Package.Current.Id.Version.Minor, Package.Current.Id.Version.Build, Package.Current.Id.Version.Revision); + private Version PackageVersion { get; } = new( + Package.Current.Id.Version.Major, + Package.Current.Id.Version.Minor, + Package.Current.Id.Version.Build, + Package.Current.Id.Version.Revision); private Uri? DownloadUri { get; set; } + private bool _isUpdateAvailable; public bool IsUpdateAvailable { get => _isUpdateAvailable; private set => SetProperty(ref _isUpdateAvailable, value); } + private bool _isUpdating; public bool IsUpdating { get => _isUpdating; @@ -86,17 +88,16 @@ public Task DownloadMandatoryUpdates() var applicationVersion = $"{SystemInformation.Instance.ApplicationVersion.Major}.{SystemInformation.Instance.ApplicationVersion.Minor}.{SystemInformation.Instance.ApplicationVersion.Build}"; var releaseNotesLocation = string.Concat("https://raw.githubusercontent.com/files-community/Release-Notes/main/", applicationVersion, ".md"); - using (var client = new HttpClient()) + using var client = new HttpClient(); + + try { - try - { - var result = await client.GetStringAsync(releaseNotesLocation, cancellationToken); - return result == string.Empty ? null : result; - } - catch - { - return null; - } + var result = await client.GetStringAsync(releaseNotesLocation, cancellationToken); + return result == string.Empty ? null : result; + } + catch + { + return null; } } @@ -132,7 +133,7 @@ public async Task CheckForUpdates() Logger?.Info($"SIDELOAD: Current Version: {PackageVersion}"); Logger?.Info($"SIDELOAD: Remote Version: {remoteVersion}"); - // Check details and version number. + // Check details and version number if (appInstaller.MainBundle.Name.Equals(PackageName) && remoteVersion.CompareTo(PackageVersion) > 0) { Logger?.Info("SIDELOAD: Update found."); @@ -217,7 +218,7 @@ await Task.Run(async () => } finally { - // Reset fields. + // Reset fields IsUpdating = false; IsUpdateAvailable = false; DownloadUri = null; diff --git a/src/Files.App/ServicesImplementation/UpdateService.cs b/src/Files.App/ServicesImplementation/UpdateService.cs index 7b8ae4601c92..88f7b4823e93 100644 --- a/src/Files.App/ServicesImplementation/UpdateService.cs +++ b/src/Files.App/ServicesImplementation/UpdateService.cs @@ -143,6 +143,7 @@ private static async Task ShowDialogAsync() CloseButtonText = "Close".GetLocalizedResource(), PrimaryButtonText = "ConsentDialogPrimaryButtonText".GetLocalizedResource() }; + ContentDialogResult result = await SetContentDialogRoot(dialog).ShowAsync(); return result == ContentDialogResult.Primary; @@ -164,17 +165,16 @@ public async Task CheckLatestReleaseNotesAsync(CancellationToken cancellationTok var applicationVersion = $"{SystemInformation.Instance.ApplicationVersion.Major}.{SystemInformation.Instance.ApplicationVersion.Minor}.{SystemInformation.Instance.ApplicationVersion.Build}"; var releaseNotesLocation = string.Concat("https://raw.githubusercontent.com/files-community/Release-Notes/main/", applicationVersion, ".md"); - using (var client = new HttpClient()) + using var client = new HttpClient(); + + try { - try - { - var result = await client.GetStringAsync(releaseNotesLocation, cancellationToken); - return result == string.Empty ? null : result; - } - catch - { - return null; - } + var result = await client.GetStringAsync(releaseNotesLocation, cancellationToken); + return result == string.Empty ? null : result; + } + catch + { + return null; } } @@ -202,6 +202,7 @@ private void OnUpdateCompleted() { IsUpdating = false; IsUpdateAvailable = false; + _updatePackages?.Clear(); } diff --git a/src/Files.App/ServicesImplementation/VolumeInfoFactory.cs b/src/Files.App/ServicesImplementation/VolumeInfoFactory.cs index 27adcf3188d0..c1e668b960dd 100644 --- a/src/Files.App/ServicesImplementation/VolumeInfoFactory.cs +++ b/src/Files.App/ServicesImplementation/VolumeInfoFactory.cs @@ -9,6 +9,7 @@ internal class VolumeInfoFactory : IVolumeInfoFactory public VolumeInfo BuildVolumeInfo(string driveName) { string volumeId = GetVolumeID(driveName); + return new VolumeInfo(volumeId); } diff --git a/src/Files.App/Shell/ContextMenu.cs b/src/Files.App/Shell/ContextMenu.cs index 8ad5e99cc19b..e23e6c005cb6 100644 --- a/src/Files.App/Shell/ContextMenu.cs +++ b/src/Files.App/Shell/ContextMenu.cs @@ -18,8 +18,11 @@ namespace Files.App.Shell public class ContextMenu : Win32ContextMenu, IDisposable { private Shell32.IContextMenu cMenu; + private User32.SafeHMENU hMenu; + private ThreadWithMessageQueue owningThread; + public List ItemsPath { get; } private ContextMenu(Shell32.IContextMenu cMenu, User32.SafeHMENU hMenu, IEnumerable itemsPath, ThreadWithMessageQueue owningThread) @@ -34,6 +37,7 @@ private ContextMenu(Shell32.IContextMenu cMenu, User32.SafeHMENU hMenu, IEnumera public async static Task InvokeVerb(string verb, params string[] filePaths) { using var cMenu = await GetContextMenuForFiles(filePaths, Shell32.CMF.CMF_DEFAULTONLY); + return cMenu is not null && await cMenu.InvokeVerb(verb); } @@ -50,17 +54,19 @@ public async Task InvokeVerb(string? verb) lpVerb = new SafeResourceId(verb, CharSet.Ansi), nShow = ShowWindowCommand.SW_SHOWNORMAL, }; + pici.cbSize = (uint)Marshal.SizeOf(pici); + await owningThread.PostMethod(() => cMenu.InvokeCommand(pici)); Win32API.BringToForeground(currentWindows); + return true; } - catch (Exception ex) when ( - ex is COMException - || ex is UnauthorizedAccessException) + catch (Exception ex) when (ex is COMException || ex is UnauthorizedAccessException) { Debug.WriteLine(ex); } + return false; } @@ -77,13 +83,14 @@ public async Task InvokeItem(int itemID) lpVerb = Macros.MAKEINTRESOURCE(itemID), nShow = ShowWindowCommand.SW_SHOWNORMAL, }; + pici.cbSize = (uint)Marshal.SizeOf(pici); + await owningThread.PostMethod(() => cMenu.InvokeCommand(pici)); + Win32API.BringToForeground(currentWindows); } - catch (Exception ex) when ( - ex is COMException - || ex is UnauthorizedAccessException) + catch (Exception ex) when (ex is COMException || ex is UnauthorizedAccessException) { Debug.WriteLine(ex); } @@ -121,8 +128,8 @@ ex is COMException public async static Task GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, Func? itemFilter = null) { var owningThread = new ThreadWithMessageQueue(); - return await owningThread.PostMethod( - () => GetContextMenuForFiles(shellItems, flags, owningThread, itemFilter)); + return await owningThread.PostMethod(() + => GetContextMenuForFiles(shellItems, flags, owningThread, itemFilter)); } private static ContextMenu? GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, ThreadWithMessageQueue owningThread, Func? itemFilter = null) @@ -130,12 +137,15 @@ ex is COMException if (!shellItems.Any()) return null; - using var sf = shellItems[0].Parent; // HP: the items are all in the same folder + // HP: The items are all in the same folder + using var sf = shellItems[0].Parent; + Shell32.IContextMenu menu = sf.GetChildrenUIObjects(default, shellItems); var hMenu = User32.CreatePopupMenu(); menu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, flags); var contextMenu = new ContextMenu(menu, hMenu, shellItems.Select(x => x.ParsingName), owningThread); EnumMenuItems(menu, hMenu, contextMenu.Items, itemFilter); + return contextMenu; } @@ -150,10 +160,12 @@ ex is COMException shellFolder = new ShellFolder(folderPath); var sv = shellFolder.GetViewObject(default); Shell32.IContextMenu menu = sv.GetItemObject(Shell32.SVGIO.SVGIO_BACKGROUND); + var hMenu = User32.CreatePopupMenu(); menu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, flags); var contextMenu = new ContextMenu(menu, hMenu, new[] { shellFolder.ParsingName }, owningThread); - ContextMenu.EnumMenuItems(menu, hMenu, contextMenu.Items, itemFilter); + EnumMenuItems(menu, hMenu, contextMenu.Items, itemFilter); + return contextMenu; } catch (Exception ex) when (ex is ArgumentException || ex is FileNotFoundException) @@ -177,52 +189,72 @@ private static void EnumMenuItems( Func? itemFilter = null) { var itemCount = User32.GetMenuItemCount(hMenu); - var mii = new User32.MENUITEMINFO(); + + var mii = new User32.MENUITEMINFO() + { + fMask = User32.MenuItemInfoMask.MIIM_BITMAP | + User32.MenuItemInfoMask.MIIM_FTYPE | + User32.MenuItemInfoMask.MIIM_STRING | + User32.MenuItemInfoMask.MIIM_ID | + User32.MenuItemInfoMask.MIIM_SUBMENU, + }; + mii.cbSize = (uint)Marshal.SizeOf(mii); - mii.fMask = User32.MenuItemInfoMask.MIIM_BITMAP - | User32.MenuItemInfoMask.MIIM_FTYPE - | User32.MenuItemInfoMask.MIIM_STRING - | User32.MenuItemInfoMask.MIIM_ID - | User32.MenuItemInfoMask.MIIM_SUBMENU; + for (uint ii = 0; ii < itemCount; ii++) { var menuItem = new ContextMenuItem(); var container = new SafeCoTaskMemString(512); var cMenu2 = cMenu as Shell32.IContextMenu2; + mii.dwTypeData = (IntPtr)container; - mii.cch = (uint)container.Capacity - 1; // https://devblogs.microsoft.com/oldnewthing/20040928-00/?p=37723 + + // https://devblogs.microsoft.com/oldnewthing/20040928-00/?p=37723 + mii.cch = (uint)container.Capacity - 1; + var retval = User32.GetMenuItemInfo(hMenu, ii, true, ref mii); if (!retval) { container.Dispose(); continue; } + menuItem.Type = (MenuItemType)mii.fType; - menuItem.ID = (int)(mii.wID - 1); // wID - idCmdFirst + + // wID - idCmdFirst + menuItem.ID = (int)(mii.wID - 1); + if (menuItem.Type == MenuItemType.MFT_STRING) { Debug.WriteLine("Item {0} ({1}): {2}", ii, mii.wID, mii.dwTypeData); + menuItem.Label = mii.dwTypeData; menuItem.CommandString = GetCommandString(cMenu, mii.wID - 1); + if (itemFilter is not null && (itemFilter(menuItem.CommandString) || itemFilter(menuItem.Label))) { // Skip items implemented in UWP container.Dispose(); + continue; } + if (mii.hbmpItem != HBITMAP.NULL && !Enum.IsDefined(typeof(HBITMAP_HMENU), ((IntPtr)mii.hbmpItem).ToInt64())) { using var bitmap = Win32API.GetBitmapFromHBitmap(mii.hbmpItem); + if (bitmap is not null) { byte[] bitmapData = (byte[])new ImageConverter().ConvertTo(bitmap, typeof(byte[])); menuItem.Icon = bitmapData; } } + if (mii.hSubMenu != HMENU.NULL) { Debug.WriteLine("Item {0}: has submenu", ii); var subItems = new List(); + try { cMenu2?.HandleMenuMsg((uint)User32.WindowMessage.WM_INITMENUPOPUP, (IntPtr)mii.hSubMenu, new IntPtr(ii)); @@ -231,8 +263,10 @@ private static void EnumMenuItems( { // Only for dynamic/owner drawn? (open with, etc) } + EnumMenuItems(cMenu, mii.hSubMenu, subItems, itemFilter); menuItem.SubItems = subItems; + Debug.WriteLine("Item {0}: done submenu", ii); } } @@ -240,6 +274,7 @@ private static void EnumMenuItems( { Debug.WriteLine("Item {0}: {1}", ii, mii.fType.ToString()); } + container.Dispose(); menuItemsResult.Add(menuItem); } @@ -251,20 +286,25 @@ private static void EnumMenuItems( // notably the "Run with graphic processor" menu item of NVidia cards if (offset > 5000) return null; + SafeCoTaskMemString? commandString = null; + try { commandString = new SafeCoTaskMemString(512); cMenu.GetCommandString(new IntPtr(offset), flags, IntPtr.Zero, commandString, (uint)commandString.Capacity - 1); Debug.WriteLine("Verb {0}: {1}", offset, commandString); + return commandString.ToString(); } catch (Exception ex) when (ex is InvalidCastException || ex is ArgumentException) { - // TODO: investigate this.. + // TODO: Investigate this... Debug.WriteLine(ex); + return null; } + catch (Exception ex) when (ex is COMException || ex is NotImplementedException) { // Not every item has an associated verb @@ -278,7 +318,8 @@ private static void EnumMenuItems( #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + // To detect redundant calls + private bool disposedValue = false; protected virtual void Dispose(bool disposing) { @@ -286,7 +327,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - // TODO: dispose managed state (managed objects). + // TODO: Dispose managed state (managed objects) if (Items is not null) { foreach (var si in Items) @@ -298,7 +339,7 @@ protected virtual void Dispose(bool disposing) } } - // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: Free unmanaged resources (unmanaged objects) and override a finalizer below if (hMenu is not null) { User32.DestroyMenu(hMenu); diff --git a/src/Files.App/Shell/LaunchHelper.cs b/src/Files.App/Shell/LaunchHelper.cs index b09fc099d619..5b3dca737a40 100644 --- a/src/Files.App/Shell/LaunchHelper.cs +++ b/src/Files.App/Shell/LaunchHelper.cs @@ -46,12 +46,15 @@ private static async Task HandleApplicationLaunch(string application, stri using Process process = new Process(); process.StartInfo.UseShellExecute = false; process.StartInfo.FileName = application; + // Show window if workingDirectory (opening terminal) process.StartInfo.CreateNoWindow = string.IsNullOrEmpty(workingDirectory); + if (arguments == "runas") { process.StartInfo.UseShellExecute = true; process.StartInfo.Verb = "runas"; + if (FileExtensionHelpers.IsMsiFile(application)) { process.StartInfo.FileName = "msiexec.exe"; @@ -62,6 +65,7 @@ private static async Task HandleApplicationLaunch(string application, stri { process.StartInfo.UseShellExecute = true; process.StartInfo.Verb = "runasuser"; + if (FileExtensionHelpers.IsMsiFile(application)) { process.StartInfo.FileName = "msiexec.exe"; @@ -71,18 +75,24 @@ private static async Task HandleApplicationLaunch(string application, stri else { process.StartInfo.Arguments = arguments; + // Refresh env variables for the child process foreach (DictionaryEntry ent in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Machine)) process.StartInfo.EnvironmentVariables[(string)ent.Key] = (string)ent.Value; + foreach (DictionaryEntry ent in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)) process.StartInfo.EnvironmentVariables[(string)ent.Key] = (string)ent.Value; + process.StartInfo.EnvironmentVariables["PATH"] = string.Join(';', Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine), Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User)); } + process.StartInfo.WorkingDirectory = workingDirectory; process.Start(); + Win32API.BringToForeground(currentWindows); + return true; } catch (Win32Exception) @@ -93,10 +103,13 @@ private static async Task HandleApplicationLaunch(string application, stri process.StartInfo.CreateNoWindow = true; process.StartInfo.Arguments = arguments; process.StartInfo.WorkingDirectory = workingDirectory; + try { process.Start(); + Win32API.BringToForeground(currentWindows); + return true; } catch (Win32Exception) @@ -109,6 +122,7 @@ private static async Task HandleApplicationLaunch(string application, stri if (split.Count() == 1) { Process.Start(split.First()); + Win32API.BringToForeground(currentWindows); } else @@ -118,21 +132,22 @@ private static async Task HandleApplicationLaunch(string application, stri Dir = Path.GetDirectoryName(x), Prog = Win32API.GetFileAssociationAsync(x).Result ?? Path.GetExtension(x) }); + foreach (var group in groups) { if (!group.Any()) - { continue; - } + using var cMenu = await ContextMenu.GetContextMenuForFiles(group.ToArray(), Shell32.CMF.CMF_DEFAULTONLY); + if (cMenu is not null) - { await cMenu.InvokeVerb(Shell32.CMDSTR_OPEN); - } } } + return true; }); + if (!opened) { if (application.StartsWith(@"\\SHELL\", StringComparison.Ordinal)) @@ -140,14 +155,15 @@ private static async Task HandleApplicationLaunch(string application, stri opened = await Win32API.StartSTATask(async () => { using var cMenu = await ContextMenu.GetContextMenuForFiles(new[] { application }, Shell32.CMF.CMF_DEFAULTONLY); + if (cMenu is not null) - { await cMenu.InvokeItem(cMenu.Items.FirstOrDefault()?.ID ?? -1); - } + return true; }); } } + if (!opened) { var isAlternateStream = Regex.IsMatch(application, @"\w:\w"); @@ -169,10 +185,12 @@ private static async Task HandleApplicationLaunch(string application, stri await inStream.CopyToAsync(outStream); await outStream.FlushAsync(); } + opened = await HandleApplicationLaunch(tempPath, arguments, workingDirectory); } } } + return opened; } catch (Win32Exception) @@ -210,6 +228,7 @@ private static string GetMtpPath(string executable) var itemPath = Regex.Replace(executable, @"^\\\\\?\\[^\\]*\\?", ""); return deviceId is not null ? Path.Combine(deviceId, itemPath) : executable; } + return executable; } } diff --git a/src/Files.App/Shell/RecycleBinManager.cs b/src/Files.App/Shell/RecycleBinManager.cs index 74625ad30ff5..ce19edc61434 100644 --- a/src/Files.App/Shell/RecycleBinManager.cs +++ b/src/Files.App/Shell/RecycleBinManager.cs @@ -20,10 +20,7 @@ public sealed class RecycleBinManager public static RecycleBinManager Default { - get - { - return lazy.Value; - } + get => lazy.Value; } private RecycleBinManager() @@ -46,22 +43,25 @@ private void StartRecycleBinWatcher() // SHChangeNotifyRegister only works if recycle bin is open in explorer :( binWatchers = new List(); var sid = WindowsIdentity.GetCurrent().User.ToString(); + foreach (var drive in DriveInfo.GetDrives()) { var recyclePath = Path.Combine(drive.Name, "$RECYCLE.BIN", sid); + if (drive.DriveType == DriveType.Network || !Directory.Exists(recyclePath)) - { continue; - } + FileSystemWatcher watcher = new FileSystemWatcher { Path = recyclePath, Filter = "*.*", NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName }; + watcher.Created += RecycleBinWatcher_Changed; watcher.Deleted += RecycleBinWatcher_Changed; watcher.EnableRaisingEvents = true; + binWatchers.Add(watcher); } } diff --git a/src/Files.App/Shell/ShellFolderExtensions.cs b/src/Files.App/Shell/ShellFolderExtensions.cs index 337c44c4adcf..7d58625887e5 100644 --- a/src/Files.App/Shell/ShellFolderExtensions.cs +++ b/src/Files.App/Shell/ShellFolderExtensions.cs @@ -20,12 +20,14 @@ public static ShellLibraryItem GetShellLibraryItem(ShellLibrary2 library, string DisplayName = library.GetDisplayName(ShellItemDisplayString.NormalDisplay), IsPinned = library.PinnedToNavigationPane, }; + var folders = library.Folders; if (folders.Count > 0) { libraryItem.DefaultSaveFolder = SafetyExtensions.IgnoreExceptions(() => library.DefaultSaveFolder.FileSystemPath); libraryItem.Folders = folders.Select(f => f.FileSystemPath).ToArray(); } + return libraryItem; } @@ -33,20 +35,24 @@ private static T TryGetProperty(this ShellItemPropertyStore sip, Ole32.PROPER { T value = default; SafetyExtensions.IgnoreExceptions(() => sip.TryGetValue(key, out value)); + return value; } public static ShellFileItem GetShellFileItem(ShellItem folderItem) { if (folderItem is null) - { return null; - } + // Zip archives are also shell folders, check for STREAM attribute // Do not use folderItem's Attributes property, throws unimplemented for some shell folders + bool isFolder = folderItem.IsFolder && folderItem.IShellItem?.GetAttributes(Shell32.SFGAO.SFGAO_STREAM) is 0; var parsingPath = folderItem.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing); - parsingPath ??= folderItem.FileSystemPath; // True path on disk + + // True path on disk + parsingPath ??= folderItem.FileSystemPath; + if (parsingPath is null || !Path.IsPathRooted(parsingPath)) { parsingPath = parsingPath switch @@ -59,64 +65,69 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem) _ => $@"\\SHELL\{string.Join("\\", folderItem.PIDL.Select(x => x.GetBytes()).Select(x => Convert.ToBase64String(x, 0, x.Length).Replace("/", "_")))}" }; } + var fileName = folderItem.Properties.TryGetProperty(Ole32.PROPERTYKEY.System.ItemNameDisplay); fileName ??= Path.GetFileName(folderItem.Name); // Original file name fileName ??= folderItem.GetDisplayName(ShellItemDisplayString.ParentRelativeParsing); + var itemNameOrOriginalPath = folderItem.Name ?? fileName; + + // In recycle bin "Name" contains original file path + name string filePath = Path.IsPathRooted(itemNameOrOriginalPath) ? - itemNameOrOriginalPath : parsingPath; // In recycle bin "Name" contains original file path + name + itemNameOrOriginalPath : parsingPath; + if (!isFolder && !string.IsNullOrEmpty(parsingPath) && Path.GetExtension(parsingPath) is string realExtension && !string.IsNullOrEmpty(realExtension)) { if (!string.IsNullOrEmpty(fileName) && !fileName.EndsWith(realExtension, StringComparison.OrdinalIgnoreCase)) - { fileName = $"{fileName}{realExtension}"; - } + if (!string.IsNullOrEmpty(filePath) && !filePath.EndsWith(realExtension, StringComparison.OrdinalIgnoreCase)) - { filePath = $"{filePath}{realExtension}"; - } } + var fileTime = folderItem.Properties.TryGetProperty( Ole32.PROPERTYKEY.System.Recycle.DateDeleted); + var recycleDate = fileTime?.ToDateTime().ToLocalTime() ?? DateTime.Now; // This is LocalTime fileTime = folderItem.Properties.TryGetProperty( Ole32.PROPERTYKEY.System.DateModified); + var modifiedDate = fileTime?.ToDateTime().ToLocalTime() ?? SafetyExtensions.IgnoreExceptions(() => folderItem.FileInfo?.LastWriteTime) ?? DateTime.Now; // This is LocalTime fileTime = folderItem.Properties.TryGetProperty( Ole32.PROPERTYKEY.System.DateCreated); + var createdDate = fileTime?.ToDateTime().ToLocalTime() ?? SafetyExtensions.IgnoreExceptions(() => folderItem.FileInfo?.CreationTime) ?? DateTime.Now; // This is LocalTime var fileSizeBytes = folderItem.Properties.TryGetProperty(Ole32.PROPERTYKEY.System.Size); string fileSize = fileSizeBytes is not null ? folderItem.Properties.GetPropertyString(Ole32.PROPERTYKEY.System.Size) : null; var fileType = folderItem.Properties.TryGetProperty(Ole32.PROPERTYKEY.System.ItemTypeText); + return new ShellFileItem(isFolder, parsingPath, fileName, filePath, recycleDate, modifiedDate, createdDate, fileSize, fileSizeBytes ?? 0, fileType, folderItem.PIDL.GetBytes()); } public static ShellLinkItem GetShellLinkItem(ShellLink linkItem) { if (linkItem is null) - { return null; - } + var baseItem = GetShellFileItem(linkItem); if (baseItem is null) - { return null; - } + var link = new ShellLinkItem(baseItem); link.IsFolder = !string.IsNullOrEmpty(linkItem.TargetPath) && linkItem.Target.IsFolder; link.RunAsAdmin = linkItem.RunAsAdministrator; link.Arguments = linkItem.Arguments; link.WorkingDirectory = linkItem.WorkingDirectory; link.TargetPath = linkItem.TargetPath; + return link; } public static string GetParsingPath(this ShellItem item) { if (item is null) - { return null; - } + return item.IsFileSystem ? item.FileSystemPath : item.ParsingName; } @@ -125,15 +136,18 @@ public static bool GetStringAsPidl(string pathOrPidl, out Shell32.PIDL pidl) if (pathOrPidl.StartsWith(@"\\SHELL\", StringComparison.Ordinal)) { pidl = pathOrPidl.Replace(@"\\SHELL\", "", StringComparison.Ordinal) - .Replace("_", "/") // Avoid confusion with path separator + // Avoid confusion with path separator + .Replace("_", "/") .Split('\\', StringSplitOptions.RemoveEmptyEntries) .Select(pathSegment => new Shell32.PIDL(Convert.FromBase64String(pathSegment))) .Aggregate((x, y) => Shell32.PIDL.Combine(x, y)); + return true; } else { pidl = Shell32.PIDL.Null; + return false; } } diff --git a/src/Files.App/Shell/ShellLibrary2.cs b/src/Files.App/Shell/ShellLibrary2.cs index a09cfe63e6b3..53988322c1e0 100644 --- a/src/Files.App/Shell/ShellLibrary2.cs +++ b/src/Files.App/Shell/ShellLibrary2.cs @@ -24,6 +24,7 @@ public ShellLibrary2(KNOWNFOLDERID knownFolderId, bool readOnly = false) { lib = new IShellLibrary(); lib.LoadLibraryFromKnownFolder(knownFolderId.Guid(), readOnly ? STGM.STGM_READ : STGM.STGM_READWRITE); + Init(knownFolderId.GetIShellItem()); } @@ -36,6 +37,7 @@ public ShellLibrary2(string libraryName, KNOWNFOLDERID kf = KNOWNFOLDERID.FOLDER lib = new IShellLibrary(); name = libraryName; var item = lib.SaveInKnownFolder(kf.Guid(), libraryName, overwrite ? LIBRARYSAVEFLAGS.LSF_OVERRIDEEXISTING : LIBRARYSAVEFLAGS.LSF_FAILIFTHERE); + Init(item); } @@ -48,6 +50,7 @@ public ShellLibrary2(string libraryName, ShellFolder parent, bool overwrite = fa lib = new IShellLibrary(); name = libraryName; var item = lib.Save(parent.IShellItem, libraryName, overwrite ? LIBRARYSAVEFLAGS.LSF_OVERRIDEEXISTING : LIBRARYSAVEFLAGS.LSF_FAILIFTHERE); + Init(item); } @@ -58,6 +61,7 @@ public ShellLibrary2(IShellItem iItem, bool readOnly = false) { lib = new IShellLibrary(); lib.LoadLibraryFromItem(iItem, readOnly ? STGM.STGM_READ : STGM.STGM_READWRITE); + Init(iItem); } @@ -85,7 +89,11 @@ public void Reload() /// The default icon location. public IconLocation IconLocation { - get { _ = IconLocation.TryParse(lib.GetIcon(), out var l); return l; } + get + { + _ = IconLocation.TryParse(lib.GetIcon(), out var l); + return l; + } set => lib.SetIcon(value.ToString()); } @@ -105,7 +113,11 @@ public bool PinnedToNavigationPane public LibraryViewTemplate ViewTemplate { get => (LibraryViewTemplate)ShlGuidExt.Lookup(ViewTemplateId); - set { if (value != LibraryViewTemplate.Custom) ViewTemplateId = ((FOLDERTYPEID)value).Guid(); } + set + { + if (value != LibraryViewTemplate.Custom) + ViewTemplateId = ((FOLDERTYPEID)value).Guid(); + } } /// Gets or sets the library's View Template identifier. @@ -134,8 +146,8 @@ public override void Dispose() /// Gets the set of child folders that are contained in the library. /// A value that determines the folders to get. /// A containing the child folders. - public ShellLibraryFolders GetFilteredFolders(LibraryFolderFilter filter = LibraryFolderFilter.AllItems) => - new ShellLibraryFolders(lib, lib.GetFolders((LIBRARYFOLDERFILTER)filter)); + public ShellLibraryFolders GetFilteredFolders(LibraryFolderFilter filter = LibraryFolderFilter.AllItems) + => new(lib, lib.GetFolders((LIBRARYFOLDERFILTER)filter)); /// Resolves the target location of a library folder, even if the folder has been moved or renamed. /// A ShellItem object that represents the library folder to locate. @@ -144,7 +156,8 @@ public ShellLibraryFolders GetFilteredFolders(LibraryFolderFilter filter = Libra /// specified time elapses, an error is returned. /// /// The resulting target location. - public ShellItem ResolveFolder(ShellItem item, TimeSpan timeout) => Open(lib.ResolveFolder(item.IShellItem, Convert.ToUInt32(timeout.TotalMilliseconds))); + public ShellItem ResolveFolder(ShellItem item, TimeSpan timeout) + => Open(lib.ResolveFolder(item.IShellItem, Convert.ToUInt32(timeout.TotalMilliseconds))); /// Shows the library management dialog box, which enables users to manage the library folders and default save location. /// @@ -167,8 +180,8 @@ public void ShowLibraryManagementDialog(IWin32Window parentWindow = null, string } /// Folders of a . - /// - /// + /// + /// public class ShellLibraryFolders : ShellItemArray, ICollection { private IShellLibrary lib; @@ -176,10 +189,12 @@ public class ShellLibraryFolders : ShellItemArray, ICollection /// Initializes a new instance of the class. /// The library. /// The shell item array. - internal ShellLibraryFolders(IShellLibrary lib, IShellItemArray shellItemArray) : base(shellItemArray) => this.lib = lib; + internal ShellLibraryFolders(IShellLibrary lib, IShellItemArray shellItemArray) : base(shellItemArray) + => this.lib = lib; /// Gets a value indicating whether the is read-only. - bool ICollection.IsReadOnly => false; + bool ICollection.IsReadOnly + => false; /// Adds the specified location. /// The location. @@ -204,7 +219,9 @@ public override void Dispose() /// location public bool Remove(ShellItem location) { - if (location is null) throw new ArgumentNullException(nameof(location)); + if (location is null) + throw new ArgumentNullException(nameof(location)); + try { lib.RemoveFolder(location.IShellItem); @@ -218,7 +235,8 @@ public bool Remove(ShellItem location) /// Removes all items from the . /// - void ICollection.Clear() => throw new NotImplementedException(); + void ICollection.Clear() + => throw new NotImplementedException(); } } } diff --git a/src/Files.App/Shell/ShellNewMenuHelper.cs b/src/Files.App/Shell/ShellNewMenuHelper.cs index 279db0304c30..7f4e5f37de26 100644 --- a/src/Files.App/Shell/ShellNewMenuHelper.cs +++ b/src/Files.App/Shell/ShellNewMenuHelper.cs @@ -20,6 +20,7 @@ public static async Task> GetNewContextMenuEntries() { var newMenuItems = new List(); var shortcutExtensions = new string[] { ShellLibraryItem.EXTENSION, ".url", ".lnk" }; + foreach (var keyName in Registry.ClassesRoot.GetSubKeyNames().Where(x => x.StartsWith('.') && !shortcutExtensions.Contains(x, StringComparer.OrdinalIgnoreCase))) { using var key = Registry.ClassesRoot.OpenSubKeySafe(keyName); @@ -28,15 +29,13 @@ public static async Task> GetNewContextMenuEntries() { var ret = await GetShellNewRegistryEntries(key, key); if (ret is not null) - { newMenuItems.Add(ret); - } } } + if (!newMenuItems.Any(x => ".txt".Equals(x.Extension, StringComparison.OrdinalIgnoreCase))) - { newMenuItems.Add(await CreateShellNewEntry(".txt", null, null, null)); - } + return newMenuItems; } @@ -46,6 +45,7 @@ public static async Task GetNewContextMenuEntryForType(string ext return null; using var key = Registry.ClassesRoot.OpenSubKeySafe(extension); + return key is not null ? await GetShellNewRegistryEntries(key, key) : null; } @@ -59,9 +59,7 @@ private static async Task GetShellNewRegistryEntries(RegistryKey continue; if (keyName == "ShellNew") - { return await ParseShellNewRegistryEntry(key, root); - } else { var ret = await GetShellNewRegistryEntries(key, root); @@ -83,9 +81,7 @@ private static Task ParseShellNewRegistryEntry(RegistryKey key, R !valueNames.Contains("Command", StringComparer.OrdinalIgnoreCase) && !valueNames.Contains("ItemName", StringComparer.OrdinalIgnoreCase) && !valueNames.Contains("Data", StringComparer.OrdinalIgnoreCase)) - { return Task.FromResult(null); - } var extension = root.Name.Substring(root.Name.LastIndexOf('\\') + 1); var fileName = (string)key.GetValue("FileName"); diff --git a/src/Files.App/Shell/ThreadWithMessageQueue.cs b/src/Files.App/Shell/ThreadWithMessageQueue.cs index 5a45285618f7..5b29c2f3ecd8 100644 --- a/src/Files.App/Shell/ThreadWithMessageQueue.cs +++ b/src/Files.App/Shell/ThreadWithMessageQueue.cs @@ -10,6 +10,7 @@ namespace Files.App.Shell public class ThreadWithMessageQueue : Disposable { private readonly BlockingCollection messageQueue; + private readonly Thread thread; protected override void Dispose(bool disposing) @@ -26,6 +27,7 @@ public async Task PostMethod(Func payload) { var message = new Internal(payload); messageQueue.TryAdd(message); + return (V)await message.tcs.Task; } @@ -33,12 +35,14 @@ public Task PostMethod(Action payload) { var message = new Internal(payload); messageQueue.TryAdd(message); + return message.tcs.Task; } public ThreadWithMessageQueue() { messageQueue = new BlockingCollection(new ConcurrentQueue()); + thread = new Thread(new ThreadStart(() => { foreach (var message in messageQueue.GetConsumingEnumerable()) @@ -47,14 +51,19 @@ public ThreadWithMessageQueue() message.tcs.SetResult(res); } })); + thread.SetApartmentState(ApartmentState.STA); - thread.IsBackground = true; // Do not prevent app from closing + + // Do not prevent app from closing + thread.IsBackground = true; + thread.Start(); } private class Internal { public Func payload; + public TaskCompletionSource tcs; public Internal(Action payload) diff --git a/src/Files.App/Shell/Win32API.cs b/src/Files.App/Shell/Win32API.cs index d92f6a2d98ff..d3b30a8f573a 100644 --- a/src/Files.App/Shell/Win32API.cs +++ b/src/Files.App/Shell/Win32API.cs @@ -30,6 +30,7 @@ public static Task StartSTATask(Func func) Thread thread = new Thread(async () => { Ole32.OleInitialize(); + try { await func(); @@ -45,12 +46,15 @@ public static Task StartSTATask(Func func) Ole32.OleUninitialize(); } }) + { IsBackground = true, Priority = ThreadPriority.Normal }; + thread.SetApartmentState(ApartmentState.STA); thread.Start(); + return taskCompletionSource.Task; } @@ -60,6 +64,7 @@ public static Task StartSTATask(Action action) Thread thread = new Thread(() => { Ole32.OleInitialize(); + try { action(); @@ -75,21 +80,26 @@ public static Task StartSTATask(Action action) Ole32.OleUninitialize(); } }) + { IsBackground = true, Priority = ThreadPriority.Normal }; + thread.SetApartmentState(ApartmentState.STA); thread.Start(); + return taskCompletionSource.Task; } public static Task StartSTATask(Func func) { var taskCompletionSource = new TaskCompletionSource(); + Thread thread = new Thread(() => { Ole32.OleInitialize(); + try { taskCompletionSource.SetResult(func()); @@ -105,18 +115,22 @@ public static Task StartSTATask(Action action) Ole32.OleUninitialize(); } }) + { IsBackground = true, Priority = ThreadPriority.Normal }; + thread.SetApartmentState(ApartmentState.STA); thread.Start(); + return taskCompletionSource.Task; } public static Task StartSTATask(Func> func) { var taskCompletionSource = new TaskCompletionSource(); + Thread thread = new Thread(async () => { Ole32.OleInitialize(); @@ -135,12 +149,15 @@ public static Task StartSTATask(Action action) Ole32.OleUninitialize(); } }) + { IsBackground = true, Priority = ThreadPriority.Normal }; + thread.SetApartmentState(ApartmentState.STA); thread.Start(); + return taskCompletionSource.Task; } @@ -172,8 +189,10 @@ public static string ExtractStringFromDLL(string file, int number) { var lib = Kernel32.LoadLibrary(file); StringBuilder result = new StringBuilder(2048); + _ = User32.LoadString(lib, number, result, result.Capacity); Kernel32.FreeLibrary(lib); + return result.ToString(); } @@ -206,12 +225,14 @@ public static string ExtractStringFromDLL(string file, int number) private class IconAndOverlayCacheEntry { public byte[]? Icon { get; set; } + public byte[]? Overlay { get; set; } } private static readonly ConcurrentDictionary> _iconAndOverlayCache = new(); private static readonly object _lock = new object(); + public static (byte[]? icon, byte[]? overlay) GetFileIconAndOverlay(string path, int thumbnailSize, bool isFolder, bool getOverlay = true, bool onlyGetOverlay = false) { byte[]? iconData = null, overlayData = null; @@ -234,11 +255,14 @@ public static (byte[]? icon, byte[]? overlay) GetFileIconAndOverlay(string path, { if (!onlyGetOverlay) { - using var shellItem = SafetyExtensions.IgnoreExceptions(() => ShellFolderExtensions.GetShellItemFromPathOrPidl(path)); + using var shellItem = SafetyExtensions.IgnoreExceptions(() + => ShellFolderExtensions.GetShellItemFromPathOrPidl(path)); + if (shellItem is not null && shellItem.IShellItem is Shell32.IShellItemImageFactory fctry) { var flags = Shell32.SIIGBF.SIIGBF_BIGGERSIZEOK; if (thumbnailSize < 80) flags |= Shell32.SIIGBF.SIIGBF_ICONONLY; + var hres = fctry.GetImage(new SIZE(thumbnailSize, thumbnailSize), flags, out var hbitmap); if (hres == HRESULT.S_OK) { @@ -246,6 +270,7 @@ public static (byte[]? icon, byte[]? overlay) GetFileIconAndOverlay(string path, if (image is not null) iconData = (byte[]?)new ImageConverter().ConvertTo(image, typeof(byte[])); } + //Marshal.ReleaseComObject(fctry); } } @@ -254,7 +279,10 @@ public static (byte[]? icon, byte[]? overlay) GetFileIconAndOverlay(string path, { var shfi = new Shell32.SHFILEINFO(); var flags = Shell32.SHGFI.SHGFI_OVERLAYINDEX | Shell32.SHGFI.SHGFI_ICON | Shell32.SHGFI.SHGFI_SYSICONINDEX | Shell32.SHGFI.SHGFI_ICONLOCATION; - var useFileAttibutes = !onlyGetOverlay && iconData is null; // Cannot access file, use file attributes + + // Cannot access file, use file attributes + var useFileAttibutes = !onlyGetOverlay && iconData is null; + var ret = ShellFolderExtensions.GetStringAsPidl(path, out var pidl) ? Shell32.SHGetFileInfo(pidl, 0, ref shfi, Shell32.SHFILEINFO.Size, Shell32.SHGFI.SHGFI_PIDL | flags) : Shell32.SHGetFileInfo(path, isFolder ? FileAttributes.Directory : 0, ref shfi, Shell32.SHFILEINFO.Size, flags | (useFileAttibutes ? Shell32.SHGFI.SHGFI_USEFILEATTRIBUTES : 0)); @@ -270,6 +298,7 @@ public static (byte[]? icon, byte[]? overlay) GetFileIconAndOverlay(string path, <= 48 => Shell32.SHIL.SHIL_EXTRALARGE, _ => Shell32.SHIL.SHIL_JUMBO, }; + lock (_lock) { if (!Shell32.SHGetImageList(imageListSize, typeof(ComCtl32.IImageList).GUID, out var imageListOut).Succeeded) @@ -325,6 +354,7 @@ public static (byte[]? icon, byte[]? overlay) GetFileIconAndOverlay(string path, Marshal.ReleaseComObject(imageList); } + return (iconData, overlayData); } else @@ -350,16 +380,19 @@ public static bool RunPowershellCommand(string command, bool runAsAdmin) try { using Process process = new Process(); + if (runAsAdmin) { process.StartInfo.UseShellExecute = true; process.StartInfo.Verb = "runas"; } + process.StartInfo.FileName = "powershell.exe"; process.StartInfo.CreateNoWindow = true; process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; process.StartInfo.Arguments = command; process.Start(); + if (process.WaitForExit(30 * 1000)) return process.ExitCode == 0; @@ -377,6 +410,7 @@ public static bool RunPowershellCommand(string command, bool runAsAdmin) public static IList ExtractSelectedIconsFromDLL(string file, IList indexes, int iconSize = 48) { var iconsList = new List(); + foreach (int index in indexes) { if (_iconCache.TryGetValue((file, index, iconSize), out var iconInfo)) @@ -398,6 +432,7 @@ public static IList ExtractSelectedIconsFromDLL(string file, IList } } } + return iconsList; } @@ -405,6 +440,7 @@ public static IList ExtractSelectedIconsFromDLL(string file, IList { var iconsList = new List(); using var currentProc = Process.GetCurrentProcess(); + using var icoCnt = Shell32.ExtractIcon(currentProc.Handle, file, -1); if (icoCnt is null) return null; @@ -439,12 +475,15 @@ public static bool SetCustomDirectoryIcon(string? folderPath, string? iconFile, if (folderPath is null) return false; - var fcs = new Shell32.SHFOLDERCUSTOMSETTINGS(); + var fcs = new Shell32.SHFOLDERCUSTOMSETTINGS() + { + dwMask = Shell32.FOLDERCUSTOMSETTINGSMASK.FCSM_ICONFILE, + pszIconFile = iconFile, + cchIconFile = 0, + iIconIndex = iconIndex, + }; + fcs.dwSize = (uint)Marshal.SizeOf(fcs); - fcs.dwMask = Shell32.FOLDERCUSTOMSETTINGSMASK.FCSM_ICONFILE; - fcs.pszIconFile = iconFile; - fcs.cchIconFile = 0; - fcs.iIconIndex = iconIndex; var success = Shell32.SHGetSetFolderCustomSettings(ref fcs, folderPath, Shell32.FCS.FCS_FORCEWRITE).Succeeded; if (success) @@ -459,7 +498,9 @@ public static bool SetCustomFileIcon(string? filePath, string? iconFile, int ico return false; var success = FileOperationsHelpers.SetLinkIcon(filePath, iconFile, iconIndex); - if (success) _iconAndOverlayCache[filePath] = new(); + if (success) + _iconAndOverlayCache[filePath] = new(); + return success; } @@ -470,20 +511,20 @@ public static void UnlockBitlockerDrive(string drive, string password) public static void OpenFormatDriveDialog(string drive) { - // format requires elevation + // Format requires elevation int driveIndex = drive.ToUpperInvariant()[0] - 'A'; RunPowershellCommand($"-command \"$Signature = '[DllImport(\\\"shell32.dll\\\", SetLastError = false)]public static extern uint SHFormatDrive(IntPtr hwnd, uint drive, uint fmtID, uint options);'; $SHFormatDrive = Add-Type -MemberDefinition $Signature -Name \"Win32SHFormatDrive\" -Namespace Win32Functions -PassThru; $SHFormatDrive::SHFormatDrive(0, {driveIndex}, 0xFFFF, 0x0001)\"", true); } public static void SetVolumeLabel(string driveName, string newLabel) { - // rename requires elevation + // Rename requires elevation RunPowershellCommand($"-command \"$Signature = '[DllImport(\\\"kernel32.dll\\\", SetLastError = false)]public static extern bool SetVolumeLabel(string lpRootPathName, string lpVolumeName);'; $SetVolumeLabel = Add-Type -MemberDefinition $Signature -Name \"Win32SetVolumeLabel\" -Namespace Win32Functions -PassThru; $SetVolumeLabel::SetVolumeLabel('{driveName}', '{newLabel}')\"", true); } public static bool MountVhdDisk(string vhdPath) { - // mounting requires elevation + // Mounting requires elevation return RunPowershellCommand($"-command \"Mount-DiskImage -ImagePath '{vhdPath}'\"", true); } @@ -497,15 +538,19 @@ public static bool MountVhdDisk(string vhdPath) Rectangle bmBounds = new Rectangle(0, 0, bmp.Width, bmp.Height); var bmpData = bmp.LockBits(bmBounds, ImageLockMode.ReadOnly, bmp.PixelFormat); + if (IsAlphaBitmap(bmpData)) { var alpha = GetAlphaBitmapFromBitmapData(bmpData); + bmp.UnlockBits(bmpData); bmp.Dispose(); + return alpha; } bmp.UnlockBits(bmpData); + return bmp; } catch @@ -518,6 +563,7 @@ public static bool MountVhdDisk(string vhdPath) { var taskbar2 = new Shell32.ITaskbarList2(); taskbar2.HrInit(); + return taskbar2 as Shell32.ITaskbarList4; } @@ -525,10 +571,12 @@ private static Bitmap GetAlphaBitmapFromBitmapData(BitmapData bmpData) { using var tmp = new Bitmap(bmpData.Width, bmpData.Height, bmpData.Stride, PixelFormat.Format32bppArgb, bmpData.Scan0); Bitmap clone = new Bitmap(tmp.Width, tmp.Height, tmp.PixelFormat); + using (Graphics gr = Graphics.FromImage(clone)) { gr.DrawImage(tmp, new Rectangle(0, 0, clone.Width, clone.Height)); } + return clone; } @@ -557,7 +605,9 @@ private static bool IsAlphaBitmap(BitmapData bmpData) public struct SHQUERYRBINFO { public int cbSize; + public long i64Size; + public long i64NumItems; } @@ -565,6 +615,7 @@ public static IEnumerable GetDesktopWindows() { HWND prevHwnd = HWND.NULL; var windowsList = new List(); + while (true) { prevHwnd = User32.FindWindowEx(HWND.NULL, prevHwnd, null, null); @@ -573,6 +624,7 @@ public static IEnumerable GetDesktopWindows() windowsList.Add(prevHwnd); } + return windowsList; } @@ -592,11 +644,19 @@ public static void BringToForeground(IEnumerable currentWindows) { foreach (var newWindow in newWindows) { - User32.SetWindowPos(newWindow, User32.SpecialWindowHandles.HWND_TOPMOST, - 0, 0, 0, 0, User32.SetWindowPosFlags.SWP_NOSIZE | User32.SetWindowPosFlags.SWP_NOMOVE); - User32.SetWindowPos(newWindow, User32.SpecialWindowHandles.HWND_NOTOPMOST, - 0, 0, 0, 0, User32.SetWindowPosFlags.SWP_SHOWWINDOW | User32.SetWindowPosFlags.SWP_NOSIZE | User32.SetWindowPosFlags.SWP_NOMOVE); + User32.SetWindowPos( + newWindow, + User32.SpecialWindowHandles.HWND_TOPMOST, + 0, 0, 0, 0, + User32.SetWindowPosFlags.SWP_NOSIZE | User32.SetWindowPosFlags.SWP_NOMOVE); + + User32.SetWindowPos( + newWindow, + User32.SpecialWindowHandles.HWND_NOTOPMOST, + 0, 0, 0, 0, + User32.SetWindowPosFlags.SWP_SHOWWINDOW | User32.SetWindowPosFlags.SWP_NOSIZE | User32.SetWindowPosFlags.SWP_NOMOVE); } + break; } } @@ -612,16 +672,21 @@ public static void BringToForeground(IEnumerable currentWindows) public static string? PathFromFileId(ulong frn, string volumeHint) { string? volumePath = Path.GetPathRoot(volumeHint); + using var volumeHandle = Kernel32.CreateFile(volumePath, Kernel32.FileAccess.GENERIC_READ, FileShare.Read, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS); if (volumeHandle.IsInvalid) return null; + var fileId = new Kernel32.FILE_ID_DESCRIPTOR() { Type = 0, Id = new Kernel32.FILE_ID_DESCRIPTOR.DUMMYUNIONNAME() { FileId = (long)frn } }; fileId.dwSize = (uint)Marshal.SizeOf(fileId); + using var hFile = Kernel32.OpenFileById(volumeHandle, fileId, Kernel32.FileAccess.GENERIC_READ, FileShare.Read, null, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS); if (hFile.IsInvalid) return null; + var sb = new StringBuilder(4096); var ret = Kernel32.GetFinalPathNameByHandle(hFile, sb, 4095, 0); + return (ret != 0) ? sb.ToString() : null; } @@ -630,9 +695,7 @@ public class Win32Window : IWin32Window public IntPtr Handle { get; set; } public static Win32Window FromLong(long hwnd) - { - return new Win32Window() { Handle = new IntPtr(hwnd) }; - } + => new Win32Window() { Handle = new IntPtr(hwnd) }; } public static void OpenFolderInExistingShellWindow(string folderPath) @@ -648,6 +711,7 @@ public static void OpenFolderInExistingShellWindow(string folderPath) for (int i = 0; i < shellWindows.Count; i++) { var item = shellWindows.Item(i); + var serv = (Shell32.IServiceProvider)item; if (serv is not null) { @@ -655,6 +719,7 @@ public static void OpenFolderInExistingShellWindow(string folderPath) { var pUnk = Marshal.GetObjectForIUnknown(ppv); var shellBrowser = (Shell32.IShellBrowser)pUnk; + using var targetFolder = SafetyExtensions.IgnoreExceptions(() => new Vanara.Windows.Shell.ShellItem(folderPath)); if (targetFolder is not null) { @@ -663,6 +728,7 @@ public static void OpenFolderInExistingShellWindow(string folderPath) var folderView = (Shell32.IFolderView)shellView; var folder = folderView.GetFolder(); var folderPidl = new Shell32.PIDL(IntPtr.Zero); + if (folder.GetCurFolder(ref folderPidl).Succeeded) { if (folderPidl.IsParentOf(targetFolder.PIDL.DangerousGetHandle(), true) || @@ -671,21 +737,27 @@ public static void OpenFolderInExistingShellWindow(string folderPath) if (shellBrowser.BrowseObject(targetFolder.PIDL.DangerousGetHandle(), Shell32.SBSP.SBSP_SAMEBROWSER | Shell32.SBSP.SBSP_ABSOLUTE).Succeeded) { opened = true; + break; } } } + folderPidl.Dispose(); + Marshal.ReleaseComObject(folder); Marshal.ReleaseComObject(folderView); Marshal.ReleaseComObject(shellView); } } + Marshal.ReleaseComObject(shellBrowser); Marshal.ReleaseComObject(pUnk); } + Marshal.ReleaseComObject(serv); } + Marshal.ReleaseComObject(item); } @@ -695,7 +767,8 @@ public static void OpenFolderInExistingShellWindow(string folderPath) if (!opened) { - Shell32.ShellExecute(HWND.NULL, + Shell32.ShellExecute( + HWND.NULL, "open", Environment.ExpandEnvironmentVariables("%windir%\\explorer.exe"), folderPath, @@ -706,14 +779,14 @@ public static void OpenFolderInExistingShellWindow(string folderPath) // Get information from recycle bin. [DllImport(Lib.Shell32, SetLastError = false, CharSet = CharSet.Unicode)] - public static extern int SHQueryRecycleBin(string pszRootPath, - ref SHQUERYRBINFO pSHQueryRBInfo); + public static extern int SHQueryRecycleBin(string pszRootPath, ref SHQUERYRBINFO pSHQueryRBInfo); public static async Task InstallInf(string filePath) { try { var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(30 * 1000)); + using Process process = new Process(); process.StartInfo.FileName = "InfDefaultInstall.exe"; process.StartInfo.Verb = "runas"; @@ -721,7 +794,9 @@ public static async Task InstallInf(string filePath) process.StartInfo.CreateNoWindow = true; process.StartInfo.Arguments = $"{filePath}"; process.Start(); + await process.WaitForExitAsync(cts.Token); + return true; } catch (Win32Exception) @@ -734,6 +809,7 @@ public static void InstallFont(string fontFilePath) { var userFontDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Fonts"); var destName = Path.Combine(userFontDir, Path.GetFileName(fontFilePath)); + RunPowershellCommand($"-command \"Copy-Item '{fontFilePath}' '{userFontDir}'; New-ItemProperty -Name '{Path.GetFileNameWithoutExtension(fontFilePath)}' -Path 'HKCU:\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts' -PropertyType string -Value '{destName}'\"", false); } } diff --git a/src/Files.App/Shell/Win32Shell.cs b/src/Files.App/Shell/Win32Shell.cs index b0c1dab2b603..3d38497be93e 100644 --- a/src/Files.App/Shell/Win32Shell.cs +++ b/src/Files.App/Shell/Win32Shell.cs @@ -13,11 +13,14 @@ namespace Files.App.Shell { public class Win32Shell { - private static ShellFolder controlPanel, controlPanelCategoryView; + private static ShellFolder controlPanel; + + private static ShellFolder controlPanelCategoryView; static Win32Shell() { controlPanel = new ShellFolder(Shell32.KNOWNFOLDERID.FOLDERID_ControlPanelFolder); + controlPanelCategoryView = new ShellFolder("::{26EE0668-A00A-44D7-9371-BEB064C98683}"); } @@ -32,17 +35,21 @@ static Win32Shell() { var flc = new List(); var folder = (ShellFileItem)null; + try { using var shellFolder = ShellFolderExtensions.GetShellItemFromPathOrPidl(path) as ShellFolder; folder = ShellFolderExtensions.GetShellFileItem(shellFolder); - if ((controlPanel.PIDL.IsParentOf(shellFolder.PIDL, false) || controlPanelCategoryView.PIDL.IsParentOf(shellFolder.PIDL, false)) - && !shellFolder.Any()) + + if ((controlPanel.PIDL.IsParentOf(shellFolder.PIDL, false) || + controlPanelCategoryView.PIDL.IsParentOf(shellFolder.PIDL, false)) && + !shellFolder.Any()) { // Return null to force open unsupported items in explorer - // Only if inside control panel and folder appears empty + // only if inside control panel and folder appears empty return (null, flc); } + if (action == "Enumerate") { foreach (var folderItem in shellFolder.Skip(from).Take(count)) @@ -52,8 +59,10 @@ static Win32Shell() var shellFileItem = folderItem is ShellLink link ? ShellFolderExtensions.GetShellLinkItem(link) : ShellFolderExtensions.GetShellFileItem(folderItem); + foreach (var prop in properties) shellFileItem.Properties[prop] = SafetyExtensions.IgnoreExceptions(() => folderItem.Properties[prop]); + flc.Add(shellFileItem); } catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException) @@ -70,6 +79,7 @@ static Win32Shell() catch { } + return (folder, flc); }); } @@ -78,11 +88,13 @@ public static (bool HasRecycleBin, long NumItems, long BinSize) QueryRecycleBin( { Win32API.SHQUERYRBINFO queryBinInfo = new Win32API.SHQUERYRBINFO(); queryBinInfo.cbSize = Marshal.SizeOf(queryBinInfo); + var res = Win32API.SHQueryRecycleBin(drive, ref queryBinInfo); if (res == HRESULT.S_OK) { var numItems = queryBinInfo.i64NumItems; var binSize = queryBinInfo.i64Size; + return (true, numItems, binSize); } else @@ -91,4 +103,4 @@ public static (bool HasRecycleBin, long NumItems, long BinSize) QueryRecycleBin( } } } -} \ No newline at end of file +} diff --git a/src/Files.App/UWPToWinAppSDKUpgradeHelpers.cs b/src/Files.App/UWPToWinAppSDKUpgradeHelpers.cs index 664968ba7476..e67c79da5db7 100644 --- a/src/Files.App/UWPToWinAppSDKUpgradeHelpers.cs +++ b/src/Files.App/UWPToWinAppSDKUpgradeHelpers.cs @@ -12,8 +12,8 @@ namespace UWPToWinAppSDKUpgradeHelpers ComInterfaceType.InterfaceIsIUnknown)] interface IDataTransferManagerInterop { - IntPtr GetForWindow([In] IntPtr appWindow, - [In] ref Guid riid); + IntPtr GetForWindow([In] IntPtr appWindow, [In] ref Guid riid); + void ShowShareUIForWindow(IntPtr appWindow); } @@ -28,8 +28,8 @@ public static class InteropHelpers [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr CreateEvent( - IntPtr lpEventAttributes, bool bManualReset, - bool bInitialState, string lpName); + IntPtr lpEventAttributes, bool bManualReset, + bool bInitialState, string lpName); [DllImport("kernel32.dll")] public static extern bool SetEvent(IntPtr hEvent); diff --git a/src/Files.App/ValueConverters/GenericEnumConverter.cs b/src/Files.App/ValueConverters/GenericEnumConverter.cs index 287e105264d1..41ac346b9e1a 100644 --- a/src/Files.App/ValueConverters/GenericEnumConverter.cs +++ b/src/Files.App/ValueConverters/GenericEnumConverter.cs @@ -43,6 +43,7 @@ private object ConvertInternal(object value, Type targetType, object parameter, } } catch { } + try { return System.Convert.ChangeType(enumValue, targetType); diff --git a/src/Files.App/ViewModels/ColumnsViewModel.cs b/src/Files.App/ViewModels/ColumnsViewModel.cs index 17e745216884..04c5ae5c22d4 100644 --- a/src/Files.App/ViewModels/ColumnsViewModel.cs +++ b/src/Files.App/ViewModels/ColumnsViewModel.cs @@ -5,7 +5,7 @@ namespace Files.App.ViewModels { public class ColumnsViewModel : ObservableObject { - private ColumnViewModel iconColumn = new ColumnViewModel() + private ColumnViewModel iconColumn = new() { UserLength = new GridLength(24, GridUnitType.Pixel), IsResizeable = false, @@ -18,15 +18,14 @@ public ColumnViewModel IconColumn set => SetProperty(ref iconColumn, value); } - private ColumnViewModel tagColumn = new ColumnViewModel(); - + private ColumnViewModel tagColumn = new(); public ColumnViewModel TagColumn { get => tagColumn; set => SetProperty(ref tagColumn, value); } - private ColumnViewModel nameColumn = new ColumnViewModel() + private ColumnViewModel nameColumn = new() { NormalMaxLength = 1000d }; @@ -37,7 +36,7 @@ public ColumnViewModel NameColumn set => SetProperty(ref nameColumn, value); } - private ColumnViewModel statusColumn = new ColumnViewModel() + private ColumnViewModel statusColumn = new() { UserLength = new GridLength(50), NormalMaxLength = 80, @@ -49,15 +48,14 @@ public ColumnViewModel StatusColumn set => SetProperty(ref statusColumn, value); } - private ColumnViewModel dateModifiedColumn = new ColumnViewModel(); - + private ColumnViewModel dateModifiedColumn = new(); public ColumnViewModel DateModifiedColumn { get => dateModifiedColumn; set => SetProperty(ref dateModifiedColumn, value); } - private ColumnViewModel originalPathColumn = new ColumnViewModel() + private ColumnViewModel originalPathColumn = new() { NormalMaxLength = 500, }; @@ -68,23 +66,21 @@ public ColumnViewModel OriginalPathColumn set => SetProperty(ref originalPathColumn, value); } - private ColumnViewModel itemTypeColumn = new ColumnViewModel(); - + private ColumnViewModel itemTypeColumn = new(); public ColumnViewModel ItemTypeColumn { get => itemTypeColumn; set => SetProperty(ref itemTypeColumn, value); } - private ColumnViewModel dateDeletedColumn = new ColumnViewModel(); - + private ColumnViewModel dateDeletedColumn = new(); public ColumnViewModel DateDeletedColumn { get => dateDeletedColumn; set => SetProperty(ref dateDeletedColumn, value); } - private ColumnViewModel dateCreatedColumn = new ColumnViewModel() + private ColumnViewModel dateCreatedColumn = new() { UserCollapsed = true }; @@ -95,8 +91,7 @@ public ColumnViewModel DateCreatedColumn set => SetProperty(ref dateCreatedColumn, value); } - private ColumnViewModel sizeColumn = new ColumnViewModel(); - + private ColumnViewModel sizeColumn = new(); public ColumnViewModel SizeColumn { get => sizeColumn; @@ -112,6 +107,7 @@ public void SetDesiredSize(double width) if (TotalWidth > width || TotalWidth < width) { var proportion = width / TotalWidth; + //SetColumnSizeProportionally(proportion); } } @@ -137,8 +133,10 @@ public override bool Equals(object? obj) { if (obj is null) return false; + if (obj == this) return true; + if (obj is ColumnsViewModel model) { return ( @@ -152,6 +150,7 @@ public override bool Equals(object? obj) model.StatusColumn.Equals(StatusColumn) && model.TagColumn.Equals(TagColumn)); } + return base.Equals(obj); } @@ -166,6 +165,7 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ SizeColumn.GetHashCode(); hashCode = (hashCode * 397) ^ StatusColumn.GetHashCode(); hashCode = (hashCode * 397) ^ TagColumn.GetHashCode(); + return hashCode; } } @@ -308,8 +308,10 @@ public override bool Equals(object? obj) { if (obj is null) return false; + if (obj == this) return true; + if (obj is ColumnViewModel model) { return ( @@ -318,6 +320,7 @@ public override bool Equals(object? obj) model.LengthIncludingGridSplitter.Value == LengthIncludingGridSplitter.Value && model.UserLength.Value == UserLength.Value); } + return base.Equals(obj); } @@ -327,6 +330,7 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ Length.Value.GetHashCode(); hashCode = (hashCode * 397) ^ LengthIncludingGridSplitter.Value.GetHashCode(); hashCode = (hashCode * 397) ^ UserLength.Value.GetHashCode(); + return hashCode; } } diff --git a/src/Files.App/ViewModels/ContextMenuFlyoutItemViewModel.cs b/src/Files.App/ViewModels/ContextMenuFlyoutItemViewModel.cs index ea6d71b0b440..12bd472ae1b1 100644 --- a/src/Files.App/ViewModels/ContextMenuFlyoutItemViewModel.cs +++ b/src/Files.App/ViewModels/ContextMenuFlyoutItemViewModel.cs @@ -9,16 +9,27 @@ namespace Files.App.ViewModels public class ContextMenuFlyoutItemViewModel { public bool ShowItem { get; set; } = true; + public ICommand Command { get; set; } + public object CommandParameter { get; set; } + public string Glyph { get; set; } + public string GlyphFontFamilyName { get; set; } + public string KeyboardAcceleratorTextOverride { get; set; } + public string Text { get; set; } + public object Tag { get; set; } + public ItemType ItemType { get; set; } + public bool IsSubItem { get; set; } + public List Items { get; set; } + public BitmapImage BitmapIcon { get; set; } /// @@ -52,7 +63,9 @@ public class ContextMenuFlyoutItemViewModel public bool ShowInZipPage { get; set; } public KeyboardAccelerator KeyboardAccelerator { get; set; } + public bool IsChecked { get; set; } + public bool IsEnabled { get; set; } = true; /// @@ -82,6 +95,7 @@ public enum ItemType public struct ColoredIconModel { public string OverlayLayerGlyph { get; set; } + public string BaseLayerGlyph { get; set; } public ColoredIcon ToColoredIcon() => new() @@ -92,4 +106,4 @@ public struct ColoredIconModel public bool IsValid => !string.IsNullOrEmpty(BaseLayerGlyph); } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/CurrentInstanceViewModel.cs b/src/Files.App/ViewModels/CurrentInstanceViewModel.cs index dfb7b6090631..196d96d17213 100644 --- a/src/Files.App/ViewModels/CurrentInstanceViewModel.cs +++ b/src/Files.App/ViewModels/CurrentInstanceViewModel.cs @@ -5,12 +5,10 @@ namespace Files.App.ViewModels { public class CurrentInstanceViewModel : ObservableObject { - /* - * TODO: - * In the future, we should consolidate these public variables into - * a single enum property providing simplified customization of the - * values being manipulated inside the setter blocks. - */ + // TODO: + // In the future, we should consolidate these public variables into + // a single enum property providing simplified customization of the + // values being manipulated inside the setter blocks public FolderSettingsViewModel FolderSettings { get; } @@ -25,7 +23,6 @@ public CurrentInstanceViewModel(FolderLayoutModes rootLayoutMode) } private bool isPageTypeSearchResults = false; - public bool IsPageTypeSearchResults { get => isPageTypeSearchResults; @@ -43,7 +40,6 @@ public bool IsPageTypeSearchResults } private string currentSearchQuery; - public string CurrentSearchQuery { get => currentSearchQuery; @@ -51,7 +47,6 @@ public string CurrentSearchQuery } private bool searchedUnindexedItems; - public bool SearchedUnindexedItems { get => searchedUnindexedItems; @@ -70,7 +65,6 @@ public bool ShowSearchUnindexedItemsMessage } private bool isPageTypeNotHome = false; - public bool IsPageTypeNotHome { get => isPageTypeNotHome; @@ -88,7 +82,6 @@ public bool IsPageTypeNotHome } private bool isPageTypeMtpDevice = false; - public bool IsPageTypeMtpDevice { get => isPageTypeMtpDevice; @@ -106,7 +99,6 @@ public bool IsPageTypeMtpDevice } private bool isPageTypeRecycleBin = false; - public bool IsPageTypeRecycleBin { get => isPageTypeRecycleBin; @@ -124,7 +116,6 @@ public bool IsPageTypeRecycleBin } private bool isPageTypeFtp = false; - public bool IsPageTypeFtp { get => isPageTypeFtp; @@ -142,7 +133,6 @@ public bool IsPageTypeFtp } private bool isPageTypeCloudDrive = false; - public bool IsPageTypeCloudDrive { get => isPageTypeCloudDrive; @@ -160,7 +150,6 @@ public bool IsPageTypeCloudDrive } private bool isPageTypeZipFolder = false; - public bool IsPageTypeZipFolder { get => isPageTypeZipFolder; @@ -178,7 +167,6 @@ public bool IsPageTypeZipFolder } private bool isPageTypeLibrary = false; - public bool IsPageTypeLibrary { get => isPageTypeLibrary; @@ -225,4 +213,4 @@ public bool CanTagFilesInPage get => !isPageTypeRecycleBin && !isPageTypeFtp && !isPageTypeZipFolder; } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs index 37f343e17264..08341c30c915 100644 --- a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs @@ -17,7 +17,6 @@ public class DecompressArchiveDialogViewModel : ObservableObject public StorageFolder DestinationFolder { get; private set; } private string destinationFolderPath; - public string DestinationFolderPath { get => destinationFolderPath; @@ -25,7 +24,6 @@ public string DestinationFolderPath } private bool openDestinationFolderOnCompletion; - public bool OpenDestinationFolderOnCompletion { get => openDestinationFolderOnCompletion; @@ -33,7 +31,6 @@ public bool OpenDestinationFolderOnCompletion } private bool isArchiveEncrypted; - public bool IsArchiveEncrypted { get => isArchiveEncrypted; @@ -41,7 +38,6 @@ public bool IsArchiveEncrypted } private bool showPathSelection; - public bool ShowPathSelection { get => showPathSelection; @@ -86,4 +82,4 @@ private string DefaultDestinationFolderPath() return Path.Combine(Path.GetDirectoryName(archive.Path), Path.GetFileNameWithoutExtension(archive.Path)); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Dialogs/DynamicDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/DynamicDialogViewModel.cs index ab9265566516..eb93966b05e9 100644 --- a/src/Files.App/ViewModels/Dialogs/DynamicDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/DynamicDialogViewModel.cs @@ -221,19 +221,26 @@ public DynamicDialogButtons DynamicButtonsEnabled { if (!value.HasFlag(DynamicDialogButtons.Cancel)) { - Debugger.Break(); // Cannot disable the Close button! + // Cannot disable the Close button! + Debugger.Break(); } if (value.HasFlag(DynamicDialogButtons.None)) { - IsPrimaryButtonEnabled = false; // Hides this option - IsSecondaryButtonEnabled = false; // Hides this option + // Hides this option + IsPrimaryButtonEnabled = false; + + // Hides this option + IsSecondaryButtonEnabled = false; return; } - IsPrimaryButtonEnabled = value.HasFlag(DynamicDialogButtons.Primary); // Hides this option - IsSecondaryButtonEnabled = value.HasFlag(DynamicDialogButtons.Secondary); // Hides this option + // Hides this option + IsPrimaryButtonEnabled = value.HasFlag(DynamicDialogButtons.Primary); + + // Hides this option + IsSecondaryButtonEnabled = value.HasFlag(DynamicDialogButtons.Secondary); } } } @@ -438,4 +445,4 @@ public void Dispose() #endregion IDisposable } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs b/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs index 6fdd90c2ad94..b71f9c08b67d 100644 --- a/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs +++ b/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs @@ -5,11 +5,10 @@ namespace Files.App.ViewModels public class DirectoryPropertiesViewModel : ObservableObject { private string directoryItemCount; - public string DirectoryItemCount { get => directoryItemCount; set => SetProperty(ref directoryItemCount, value); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/FolderSettingsViewModel.cs b/src/Files.App/ViewModels/FolderSettingsViewModel.cs index a72b7d14f413..3fea53672a9a 100644 --- a/src/Files.App/ViewModels/FolderSettingsViewModel.cs +++ b/src/Files.App/ViewModels/FolderSettingsViewModel.cs @@ -19,11 +19,13 @@ namespace Files.App.ViewModels { public class FolderSettingsViewModel : ObservableObject { - public static string LayoutSettingsDbPath => IO.Path.Combine(ApplicationData.Current.LocalFolder.Path, "user_settings.db"); + public static string LayoutSettingsDbPath + => IO.Path.Combine(ApplicationData.Current.LocalFolder.Path, "user_settings.db"); private static readonly Lazy dbInstance = new(() => new LayoutPrefsDb(LayoutSettingsDbPath, true)); - public static LayoutPrefsDb GetDbInstance() => dbInstance.Value; + public static LayoutPrefsDb GetDbInstance() + => dbInstance.Value; public event EventHandler? LayoutPreferencesUpdateRequired; @@ -45,12 +47,14 @@ public FolderSettingsViewModel() ChangeGroupOptionCommand = new RelayCommand(ChangeGroupOption); ChangeGroupDirectionCommand = new RelayCommand(ChangeGroupDirection); } + public FolderSettingsViewModel(FolderLayoutModes modeOverride) : this() => (rootLayoutMode, LayoutPreference.IsAdaptiveLayoutOverridden) = (modeOverride, true); private readonly FolderLayoutModes? rootLayoutMode; - public bool IsLayoutModeFixed => rootLayoutMode is not null; + public bool IsLayoutModeFixed + => rootLayoutMode is not null; public bool IsAdaptiveLayoutEnabled { @@ -74,25 +78,30 @@ public FolderLayoutModes LayoutMode public uint GetIconSize() { + // ListView thumbnail if (LayoutMode == FolderLayoutModes.DetailsView) - return Constants.Browser.DetailsLayoutBrowser.DetailsViewSize; // ListView thumbnail - - if (LayoutMode == FolderLayoutModes.ColumnView) - return Constants.Browser.ColumnViewBrowser.ColumnViewSize; // ListView thumbnail + return Constants.Browser.DetailsLayoutBrowser.DetailsViewSize; + // ListView thumbnail + else if (LayoutMode == FolderLayoutModes.ColumnView) + return Constants.Browser.ColumnViewBrowser.ColumnViewSize; + // Small thumbnail else if (LayoutMode == FolderLayoutModes.TilesView) - return Constants.Browser.GridViewBrowser.GridViewSizeSmall; // Small thumbnail + return Constants.Browser.GridViewBrowser.GridViewSizeSmall; + // Small thumbnail else if (GridViewSize <= Constants.Browser.GridViewBrowser.GridViewSizeSmall) - return Constants.Browser.GridViewBrowser.GridViewSizeSmall; // Small thumbnail + return Constants.Browser.GridViewBrowser.GridViewSizeSmall; + // Medium thumbnail else if (GridViewSize <= Constants.Browser.GridViewBrowser.GridViewSizeMedium) - return Constants.Browser.GridViewBrowser.GridViewSizeMedium; // Medium thumbnail + return Constants.Browser.GridViewBrowser.GridViewSizeMedium; + // Large thumbnail else if (GridViewSize <= Constants.Browser.GridViewBrowser.GridViewSizeLarge) - return Constants.Browser.GridViewBrowser.GridViewSizeLarge; // Large thumbnail + return Constants.Browser.GridViewBrowser.GridViewSizeLarge; + // Extra large thumbnail else - return Constants.Browser.GridViewBrowser.GridViewSizeMax; // Extra large thumbnail + return Constants.Browser.GridViewBrowser.GridViewSizeMax; } private bool isLayoutModeChanging; - public bool IsLayoutModeChanging { get => isLayoutModeChanging; @@ -146,26 +155,32 @@ public int GridViewSize get => LayoutPreference.GridViewSize; set { - if (value < LayoutPreference.GridViewSize) // Size down + // Size down + if (value < LayoutPreference.GridViewSize) { - if (LayoutMode == FolderLayoutModes.TilesView) // Size down from tiles to list + // Size down from tiles to list + if (LayoutMode == FolderLayoutModes.TilesView) { LayoutPreference.IsAdaptiveLayoutOverridden = true; LayoutMode = FolderLayoutModes.DetailsView; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(LayoutMode, GridViewSize)); } - else if (LayoutMode == FolderLayoutModes.GridView && value < Constants.Browser.GridViewBrowser.GridViewSizeSmall) // Size down from grid to tiles + // Size down from grid to tiles + else if (LayoutMode == FolderLayoutModes.GridView && value < Constants.Browser.GridViewBrowser.GridViewSizeSmall) { LayoutPreference.IsAdaptiveLayoutOverridden = true; LayoutMode = FolderLayoutModes.TilesView; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(LayoutMode, GridViewSize)); } - else if (LayoutMode != FolderLayoutModes.DetailsView) // Resize grid view + // Resize grid view + else if (LayoutMode != FolderLayoutModes.DetailsView) { - var newValue = (value >= Constants.Browser.GridViewBrowser.GridViewSizeSmall) ? value : Constants.Browser.GridViewBrowser.GridViewSizeSmall; // Set grid size to allow immediate UI update + // Set grid size to allow immediate UI update + var newValue = (value >= Constants.Browser.GridViewBrowser.GridViewSizeSmall) ? value : Constants.Browser.GridViewBrowser.GridViewSizeSmall; SetProperty(ref LayoutPreference.GridViewSize, newValue, nameof(GridViewSize)); - if (LayoutMode != FolderLayoutModes.GridView) // Only update layout mode if it isn't already in grid view + // Only update layout mode if it isn't already in grid view + if (LayoutMode != FolderLayoutModes.GridView) { LayoutPreference.IsAdaptiveLayoutOverridden = true; LayoutMode = FolderLayoutModes.GridView; @@ -179,9 +194,11 @@ public int GridViewSize GridViewSizeChangeRequested?.Invoke(this, EventArgs.Empty); } } - else if (value > LayoutPreference.GridViewSize) // Size up + // Size up + else if (value > LayoutPreference.GridViewSize) { - if (LayoutMode == FolderLayoutModes.DetailsView) // Size up from list to tiles + // Size up from list to tiles + if (LayoutMode == FolderLayoutModes.DetailsView) { LayoutPreference.IsAdaptiveLayoutOverridden = true; LayoutMode = FolderLayoutModes.TilesView; @@ -189,10 +206,12 @@ public int GridViewSize } else // Size up from tiles to grid { - var newValue = (LayoutMode == FolderLayoutModes.TilesView) ? Constants.Browser.GridViewBrowser.GridViewSizeSmall : (value <= Constants.Browser.GridViewBrowser.GridViewSizeMax) ? value : Constants.Browser.GridViewBrowser.GridViewSizeMax; // Set grid size to allow immediate UI update + // Set grid size to allow immediate UI update + var newValue = (LayoutMode == FolderLayoutModes.TilesView) ? Constants.Browser.GridViewBrowser.GridViewSizeSmall : (value <= Constants.Browser.GridViewBrowser.GridViewSizeMax) ? value : Constants.Browser.GridViewBrowser.GridViewSizeMax; SetProperty(ref LayoutPreference.GridViewSize, newValue, nameof(GridViewSize)); - if (LayoutMode != FolderLayoutModes.GridView) // Only update layout mode if it isn't already in grid view + // Only update layout mode if it isn't already in grid view + if (LayoutMode != FolderLayoutModes.GridView) { LayoutPreference.IsAdaptiveLayoutOverridden = true; LayoutMode = FolderLayoutModes.GridView; @@ -203,7 +222,8 @@ public int GridViewSize LayoutPreferencesUpdateRequired?.Invoke(this, new LayoutPreferenceEventArgs(LayoutPreference)); } - if (value < Constants.Browser.GridViewBrowser.GridViewSizeMax) // Don't request a grid resize if it is already at the max size + // Don't request a grid resize if it is already at the max size + if (value < Constants.Browser.GridViewBrowser.GridViewSizeMax) GridViewSizeChangeRequested?.Invoke(this, EventArgs.Empty); } } @@ -330,6 +350,7 @@ public static void SetLayoutPreferencesForPath(string folderPath, LayoutPreferen { userSettingsService.FoldersSettingsService.DefaultLayoutMode = prefs.LayoutMode; userSettingsService.LayoutSettingsService.DefaultGridViewSize = prefs.GridViewSize; + // Do not save options which only work in recycle bin or cloud folders as global if (prefs.DirectorySortOption != SortOption.OriginalFolder && prefs.DirectorySortOption != SortOption.DateDeleted && @@ -337,6 +358,7 @@ public static void SetLayoutPreferencesForPath(string folderPath, LayoutPreferen { userSettingsService.FoldersSettingsService.DefaultSortOption = prefs.DirectorySortOption; } + if (prefs.DirectoryGroupOption != GroupOption.OriginalFolder && prefs.DirectoryGroupOption != GroupOption.DateDeleted && prefs.DirectoryGroupOption != GroupOption.FolderPath && @@ -344,6 +366,7 @@ public static void SetLayoutPreferencesForPath(string folderPath, LayoutPreferen { userSettingsService.FoldersSettingsService.DefaultGroupOption = prefs.DirectoryGroupOption; } + userSettingsService.FoldersSettingsService.DefaultDirectorySortDirection = prefs.DirectorySortDirection; userSettingsService.FoldersSettingsService.DefaultDirectoryGroupDirection = prefs.DirectoryGroupDirection; userSettingsService.FoldersSettingsService.DefaultSortDirectoriesAlongsideFiles = prefs.SortDirectoriesAlongsideFiles; @@ -372,10 +395,14 @@ public static void SetLayoutPreferencesForPath(string folderPath, LayoutPreferen private static LayoutPreferences ReadLayoutPreferencesFromAds(string folderPath, ulong? frn) { var str = NativeFileOperationsHelper.ReadStringFromFile($"{folderPath}:files_layoutmode"); + var adsPrefs = SafetyExtensions.IgnoreExceptions(() => string.IsNullOrEmpty(str) ? null : JsonSerializer.Deserialize(str)); - WriteLayoutPreferencesToDb(folderPath, frn, adsPrefs); // Port settings to DB, delete ADS + + // Port settings to DB, delete ADS + WriteLayoutPreferencesToDb(folderPath, frn, adsPrefs); NativeFileOperationsHelper.DeleteFileFromApp($"{folderPath}:files_layoutmode"); + return adsPrefs; } @@ -385,6 +412,7 @@ private static LayoutPreferences ReadLayoutPreferencesFromAds(string folderPath, return null; var dbInstance = GetDbInstance(); + return dbInstance.GetPreferences(folderPath, frn); } @@ -403,7 +431,8 @@ private static LayoutPreferences GetDefaultLayoutPreferences(string folderPath) // Default for libraries is to group by folder path return new LayoutPreferences() { DirectoryGroupOption = GroupOption.FolderPath }; else - return LayoutPreferences.DefaultLayoutPreferences; // Either global setting or smart guess + // Either global setting or smart guess + return LayoutPreferences.DefaultLayoutPreferences; } private static void WriteLayoutPreferencesToDb(string folderPath, ulong? frn, LayoutPreferences prefs) @@ -412,11 +441,13 @@ private static void WriteLayoutPreferencesToDb(string folderPath, ulong? frn, La return; var dbInstance = GetDbInstance(); - if (dbInstance.GetPreferences(folderPath, frn) is null) + if (dbInstance.GetPreferences(folderPath, frn) is null && + LayoutPreferences.DefaultLayoutPreferences.Equals(prefs)) { - if (LayoutPreferences.DefaultLayoutPreferences.Equals(prefs)) - return; // Do not create setting if it's default + // Do not create setting if it's default + return; } + dbInstance.SetPreferences(folderPath, frn, prefs); } @@ -447,9 +478,11 @@ public void ToggleLayoutModeGridViewLarge(bool manuallySet) { IsAdaptiveLayoutEnabled &= !manuallySet; - LayoutMode = FolderLayoutModes.GridView; // Grid View + // Grid View + LayoutMode = FolderLayoutModes.GridView; - GridViewSize = Constants.Browser.GridViewBrowser.GridViewSizeLarge; // Size + // Size + GridViewSize = Constants.Browser.GridViewBrowser.GridViewSizeLarge; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(FolderLayoutModes.GridView, GridViewSize)); } @@ -458,7 +491,8 @@ public void ToggleLayoutModeColumnView(bool manuallySet) { IsAdaptiveLayoutEnabled &= !manuallySet; - LayoutMode = FolderLayoutModes.ColumnView; // Column View + // Column View + LayoutMode = FolderLayoutModes.ColumnView; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(FolderLayoutModes.ColumnView, GridViewSize)); } @@ -467,9 +501,11 @@ public void ToggleLayoutModeGridViewMedium(bool manuallySet) { IsAdaptiveLayoutEnabled &= !manuallySet; - LayoutMode = FolderLayoutModes.GridView; // Grid View + // Grid View + LayoutMode = FolderLayoutModes.GridView; - GridViewSize = Constants.Browser.GridViewBrowser.GridViewSizeMedium; // Size + // Size + GridViewSize = Constants.Browser.GridViewBrowser.GridViewSizeMedium; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(FolderLayoutModes.GridView, GridViewSize)); } @@ -478,18 +514,22 @@ public void ToggleLayoutModeGridViewSmall(bool manuallySet) { IsAdaptiveLayoutEnabled &= !manuallySet; - LayoutMode = FolderLayoutModes.GridView; // Grid View + // Grid View + LayoutMode = FolderLayoutModes.GridView; - GridViewSize = Constants.Browser.GridViewBrowser.GridViewSizeSmall; // Size + // Size + GridViewSize = Constants.Browser.GridViewBrowser.GridViewSizeSmall; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(FolderLayoutModes.GridView, GridViewSize)); } public void ToggleLayoutModeGridView(int size) { - LayoutMode = FolderLayoutModes.GridView; // Grid View + // Grid View + LayoutMode = FolderLayoutModes.GridView; - GridViewSize = size; // Size + // Size + GridViewSize = size; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(LayoutMode, GridViewSize)); } @@ -498,7 +538,8 @@ public void ToggleLayoutModeTiles(bool manuallySet) { IsAdaptiveLayoutEnabled &= !manuallySet; - LayoutMode = FolderLayoutModes.TilesView; // Tiles View + // Tiles View + LayoutMode = FolderLayoutModes.TilesView; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(FolderLayoutModes.TilesView, GridViewSize)); } @@ -507,25 +548,29 @@ public void ToggleLayoutModeDetailsView(bool manuallySet) { IsAdaptiveLayoutEnabled &= !manuallySet; - LayoutMode = FolderLayoutModes.DetailsView; // Details View + // Details View + LayoutMode = FolderLayoutModes.DetailsView; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(FolderLayoutModes.DetailsView, GridViewSize)); } public void ToggleLayoutModeAdaptive() { - IsAdaptiveLayoutEnabled = true; // Adaptive + // Adaptive + IsAdaptiveLayoutEnabled = true; LayoutModeChangeRequested?.Invoke(this, new LayoutModeEventArgs(FolderLayoutModes.Adaptive, GridViewSize)); } - private void ChangeGroupOption(GroupOption option) => DirectoryGroupOption = option; + private void ChangeGroupOption(GroupOption option) + => DirectoryGroupOption = option; private void ChangeGroupDirection(SortDirection option) => DirectoryGroupDirection = option; public void OnDefaultPreferencesChanged(string folderPath, string settingsName) { var prefs = GetLayoutPreferencesForPath(folderPath); + switch (settingsName) { case nameof(UserSettingsService.FoldersSettingsService.DefaultSortDirectoriesAlongsideFiles): @@ -533,7 +578,7 @@ public void OnDefaultPreferencesChanged(string folderPath, string settingsName) break; case nameof(UserSettingsService.FoldersSettingsService.SyncFolderPreferencesAcrossDirectories): LayoutPreference = prefs; - // TODO: update layout + // TODO: Update layout break; } } diff --git a/src/Files.App/ViewModels/ItemViewModel.cs b/src/Files.App/ViewModels/ItemViewModel.cs index d01636cc1cfd..cebd85f2e8dd 100644 --- a/src/Files.App/ViewModels/ItemViewModel.cs +++ b/src/Files.App/ViewModels/ItemViewModel.cs @@ -64,7 +64,7 @@ public class ItemViewModel : ObservableObject, IDisposable private Task aProcessQueueAction; - // files and folders list for manipulating + // Files and folders list for manipulating private List filesAndFolders; private readonly IDialogService dialogService = Ioc.Default.GetRequiredService(); @@ -72,24 +72,27 @@ public class ItemViewModel : ObservableObject, IDisposable private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); private readonly ISizeProvider folderSizeProvider = Ioc.Default.GetRequiredService(); - // only used for Binding and ApplyFilesAndFoldersChangesAsync, don't manipulate on this! + // Only used for Binding and ApplyFilesAndFoldersChangesAsync, don't manipulate on this! public BulkConcurrentObservableCollection FilesAndFolders { get; } + private FolderSettingsViewModel folderSettings = null; public ListedItem CurrentFolder { get; private set; } public CollectionViewSource viewSource; private FileSystemWatcher watcher; - private CancellationTokenSource addFilesCTS, semaphoreCTS, loadPropsCTS, watcherCTS, searchCTS; + + private CancellationTokenSource addFilesCTS; + private CancellationTokenSource semaphoreCTS; + private CancellationTokenSource loadPropsCTS; + private CancellationTokenSource watcherCTS; + private CancellationTokenSource searchCTS; public event EventHandler DirectoryInfoUpdated; public event EventHandler> OnSelectionRequestedEvent; - public string WorkingDirectory - { - get; private set; - } + public string WorkingDirectory { get; private set; } private StorageFolderWithPath currentStorageFolder; private StorageFolderWithPath workingRoot; @@ -165,6 +168,7 @@ public async void UpdateSortOptionStatus() OnPropertyChanged(nameof(IsSortedByDateCreated)); OnPropertyChanged(nameof(IsSortedBySyncStatus)); OnPropertyChanged(nameof(IsSortedByFileTag)); + await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } @@ -173,6 +177,7 @@ public async void UpdateSortDirectionStatus() { OnPropertyChanged(nameof(IsSortedAscending)); OnPropertyChanged(nameof(IsSortedDescending)); + await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } @@ -180,6 +185,7 @@ public async void UpdateSortDirectionStatus() public async void UpdateSortDirectoriesAlongsideFiles() { OnPropertyChanged(nameof(AreDirectoriesSortedAlongsideFiles)); + await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } @@ -221,6 +227,7 @@ public bool IsSortedByOriginalPath if (value) { folderSettings.DirectorySortOption = SortOption.OriginalFolder; + OnPropertyChanged(nameof(IsSortedByOriginalPath)); } } @@ -234,6 +241,7 @@ public bool IsSortedByDateDeleted if (value) { folderSettings.DirectorySortOption = SortOption.DateDeleted; + OnPropertyChanged(nameof(IsSortedByDateDeleted)); } } @@ -247,6 +255,7 @@ public bool IsSortedByDate if (value) { folderSettings.DirectorySortOption = SortOption.DateModified; + OnPropertyChanged(nameof(IsSortedByDate)); } } @@ -260,6 +269,7 @@ public bool IsSortedByDateCreated if (value) { folderSettings.DirectorySortOption = SortOption.DateCreated; + OnPropertyChanged(nameof(IsSortedByDateCreated)); } } @@ -273,6 +283,7 @@ public bool IsSortedByType if (value) { folderSettings.DirectorySortOption = SortOption.FileType; + OnPropertyChanged(nameof(IsSortedByType)); } } @@ -376,6 +387,7 @@ private async void RecycleBinRefreshRequested(object sender, FileSystemEventArgs { if (!CommonPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; + await dispatcherQueue.EnqueueAsync(() => { RefreshItems(null); @@ -386,9 +398,10 @@ private async void RecycleBinItemDeleted(object sender, FileSystemEventArgs e) { if (!CommonPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; - // get the item that immediately follows matching item to be removed - // if the matching item is the last item, try to get the previous item; otherwise, null - // case must be ignored since $Recycle.Bin != $RECYCLE.BIN + + // Get the item that immediately follows matching item to be removed + // If the matching item is the last item, try to get the previous item; otherwise, null + // Case must be ignored since $Recycle.Bin != $RECYCLE.BIN var itemRemovedIndex = filesAndFolders.FindIndex(x => x.ItemPath.Equals(e.FullPath, StringComparison.OrdinalIgnoreCase)); var nextOfMatchingItem = filesAndFolders.ElementAtOrDefault(itemRemovedIndex + 1 < filesAndFolders.Count ? itemRemovedIndex + 1 : itemRemovedIndex - 1); var removedItem = await RemoveFileOrFolderAsync(e.FullPath); @@ -404,13 +417,17 @@ private async void RecycleBinItemCreated(object sender, FileSystemEventArgs e) { if (!CommonPaths.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 ApplySingleFileChangeAsync(newListedItem); @@ -444,8 +461,10 @@ await dispatcherQueue.EnqueueAsync(() => matchingItem.FileSizeBytes = (long)e.NewSize; matchingItem.FileSize = e.NewSize.ToSizeString(); } + DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); - }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + }, + Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } } finally @@ -547,7 +566,7 @@ public void UpdateEmptyTextType() EmptyTextType = FilesAndFolders.Count == 0 ? (IsSearchResults ? EmptyTextType.NoSearchResultsFound : EmptyTextType.FolderEmpty) : EmptyTextType.None; } - // apply changes immediately after manipulating on filesAndFolders completed + // Apply changes immediately after manipulating on filesAndFolders completed public async Task ApplyFilesAndFoldersChangesAsync() { try @@ -560,6 +579,7 @@ void ClearDisplay() UpdateEmptyTextType(); DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); } + if (NativeWinApiHelper.IsHasThreadAccessPropertyPresent && dispatcherQueue.HasThreadAccess) ClearDisplay(); else @@ -617,6 +637,7 @@ void ApplyBulkInsertEntries() { ApplyBulkInsertEntries(); FilesAndFolders.InsertRange(i, filesAndFolders.Skip(i)); + break; } } @@ -633,7 +654,7 @@ void ApplyBulkInsertEntries() void UpdateUI() { - // trigger CollectionChanged with NotifyCollectionChangedAction.Reset + // Trigger CollectionChanged with NotifyCollectionChangedAction.Reset // once loading is completed so that UI can be updated FilesAndFolders.EndBulkOperation(); UpdateEmptyTextType(); @@ -659,7 +680,7 @@ void UpdateUI() private Task RequestSelectionAsync(List itemsToSelect) { - // don't notify if there weren't listed items + // Don't notify if there weren't listed items if (itemsToSelect is null || itemsToSelect.IsEmpty()) return Task.CompletedTask; @@ -687,6 +708,7 @@ void OrderEntries() return Task.Run(OrderEntries); OrderEntries(); + return Task.CompletedTask; } @@ -706,6 +728,7 @@ private void OrderGroups(CancellationToken token = default) if (FilesAndFolders.GroupedCollection is null || FilesAndFolders.GroupedCollection.IsSorted) return; + if (folderSettings.DirectoryGroupDirection == SortDirection.Ascending) { if (folderSettings.DirectoryGroupOption == GroupOption.Size) @@ -724,6 +747,7 @@ private void OrderGroups(CancellationToken token = default) else FilesAndFolders.GroupedCollection.Order(x => x.OrderByDescending(y => y.Model.SortIndexOverride).ThenByDescending(y => y.Model.Text)); } + FilesAndFolders.GroupedCollection.IsSorted = true; } @@ -731,7 +755,8 @@ public async Task GroupOptionsUpdated(CancellationToken token) { try { - // Conflicts will occur if re-grouping is run while items are still being enumerated, so wait for enumeration to complete first + // 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) @@ -778,7 +803,7 @@ await dispatcherQueue.EnqueueAsync( public Task ReloadItemGroupHeaderImagesAsync() { - // this is needed to update the group icons for file type groups + // This is needed to update the group icons for file type groups if (folderSettings.DirectoryGroupOption != GroupOption.FileType || FilesAndFolders.GroupedCollection is null) return Task.CompletedTask; @@ -806,12 +831,15 @@ public void UpdateGroupOptions() public Dictionary DefaultIcons = new(); private uint currentDefaultIconSize = 0; + public async Task GetDefaultItemIcons(uint size) { if (currentDefaultIconSize == size) return; + // TODO: Add more than just the folder icon DefaultIcons.Clear(); + using StorageItemThumbnail icon = await FilesystemTasks.Wrap(() => StorageItemIconHelpers.GetIconForItemType(size, IconPersistenceOptions.Persist)); if (icon is not null) { @@ -819,18 +847,18 @@ public async Task GetDefaultItemIcons(uint size) await img.SetSourceAsync(icon); DefaultIcons.Add(string.Empty, img); } + currentDefaultIconSize = size; } private bool isLoadingItems = false; - public bool IsLoadingItems { get => isLoadingItems; set => isLoadingItems = value; } - // thumbnailSize is set to 96 so that unless we override it, mode is in turn set to SingleItem + // ThumbnailSize is set to 96 so that unless we override it, mode is in turn set to SingleItem private async Task LoadItemThumbnail(ListedItem item, uint thumbnailSize = 96, IStorageItem? matchingStorageItem = null) { var wasIconLoaded = false; @@ -1017,6 +1045,7 @@ await Task.Run(async () => var fileTag = FileTagsHelper.ReadFileTag(item.ItemPath); var itemType = (item.ItemType == "Folder".GetLocalizedResource()) ? item.ItemType : matchingStorageFile.DisplayType; cts.Token.ThrowIfCancellationRequested(); + await dispatcherQueue.EnqueueAsync(() => { item.FolderRelativeId = matchingStorageFile.FolderRelativeId; @@ -1024,7 +1053,9 @@ await dispatcherQueue.EnqueueAsync(() => item.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); item.FileFRN = fileFRN; item.FileTags = fileTag; - }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + }, + Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + SetFileTag(item); wasSyncStatusLoaded = true; } @@ -1064,6 +1095,7 @@ await dispatcherQueue.EnqueueAsync(() => var fileTag = FileTagsHelper.ReadFileTag(item.ItemPath); var itemType = (item.ItemType == "Folder".GetLocalizedResource()) ? item.ItemType : matchingStorageFolder.DisplayType; cts.Token.ThrowIfCancellationRequested(); + await dispatcherQueue.EnqueueAsync(() => { item.FolderRelativeId = matchingStorageFolder.FolderRelativeId; @@ -1071,7 +1103,9 @@ await dispatcherQueue.EnqueueAsync(() => item.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); item.FileFRN = fileFRN; item.FileTags = fileTag; - }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + }, + Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + SetFileTag(item); wasSyncStatusLoaded = true; } @@ -1100,11 +1134,16 @@ await dispatcherQueue.EnqueueAsync(() => await FilesystemTasks.Wrap(async () => { var fileTag = FileTagsHelper.ReadFileTag(item.ItemPath); + await dispatcherQueue.EnqueueAsync(() => { - item.SyncStatusUI = new CloudDriveSyncStatusUI(); // Reset cloud sync status icon + // Reset cloud sync status icon + item.SyncStatusUI = new CloudDriveSyncStatusUI(); + item.FileTags = fileTag; - }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + }, + Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + SetFileTag(item); }); } @@ -1112,8 +1151,8 @@ await dispatcherQueue.EnqueueAsync(() => if (loadGroupHeaderInfo) { cts.Token.ThrowIfCancellationRequested(); - await SafetyExtensions.IgnoreExceptions( - () => dispatcherQueue.EnqueueAsync(() => + await SafetyExtensions.IgnoreExceptions(() => + dispatcherQueue.EnqueueAsync(() => { gp.Model.ImageSource = groupImage; gp.InitializeExtendedGroupHeaderInfoAsync(); @@ -1124,7 +1163,7 @@ await SafetyExtensions.IgnoreExceptions( } catch (OperationCanceledException) { - // ignored + // Ignored } finally { @@ -1142,7 +1181,7 @@ await SafetyExtensions.IgnoreExceptions( if (headerIconInfo is not null && !item.IsShortcut) groupImage = await dispatcherQueue.EnqueueAsync(() => headerIconInfo.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); - // groupImage is null if loading icon from fulltrust process failed + // 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); @@ -1195,8 +1234,7 @@ private async void RapidAddItemsToCollectionAsync(string path, string? previousD { // 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 + // If we're waiting and a new update request comes through simply drop this instance await enumFolderSemaphore.WaitAsync(semaphoreCTS.Token); } catch (OperationCanceledException) @@ -1251,7 +1289,8 @@ private async void RapidAddItemsToCollectionAsync(string path, string? previousD } finally { - DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); // Make sure item count is updated + // Make sure item count is updated + DirectoryInfoUpdated?.Invoke(this, EventArgs.Empty); enumFolderSemaphore.Release(); } @@ -1278,29 +1317,36 @@ private async Task RapidAddItemsToCollection(string? path, LibraryItem? library { var isRecycleBin = path.StartsWith(CommonPaths.RecycleBinPath, StringComparison.Ordinal); var enumerated = await EnumerateItemsFromStandardFolderAsync(path, addFilesCTS.Token, library); - IsLoadingItems = false; // Hide progressbar after enumeration + + // Hide progressbar after enumeration + IsLoadingItems = false; + switch (enumerated) { - case 0: // Enumerated with FindFirstFileExFromApp - // Is folder synced to cloud storage? + // 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 }); WatchForDirectoryChanges(path, syncStatus); break; - case 1: // Enumerated with StorageFolder + // Enumerated with StorageFolder + case 1: PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false, IsTypeRecycleBin = isRecycleBin }); currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); WatchForStorageFolderChanges(currentStorageFolder?.Item); break; - case 2: // Watch for changes using FTP in Box Drive folder (#7428) and network drives (#5869) + // Watch for changes using FTP in Box Drive folder (#7428) and network drives (#5869) + case 2: PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false }); WatchForWin32FolderChanges(path); break; - case -1: // Enumeration failed + // Enumeration failed + case -1: default: break; } @@ -1338,7 +1384,7 @@ public async Task EnumerateItemsFromSpecialFolderAsync(string path) ItemNameRaw = path.StartsWith(CommonPaths.RecycleBinPath, StringComparison.Ordinal) ? ApplicationData.Current.LocalSettings.Values.Get("RecycleBin_Title", "Recycle Bin") : path.StartsWith(CommonPaths.NetworkFolderPath, StringComparison.Ordinal) ? "Network".GetLocalizedResource() : isFtp ? "FTP" : "Unknown", ItemDateModifiedReal = DateTimeOffset.Now, // Fake for now - ItemDateCreatedReal = DateTimeOffset.Now, // Fake for now + ItemDateCreatedReal = DateTimeOffset.Now, // Fake for now ItemType = "Folder".GetLocalizedResource(), FileImage = null, LoadFileIcon = false, @@ -1349,7 +1395,8 @@ public async Task EnumerateItemsFromSpecialFolderAsync(string path) if (!isFtp || !FtpHelpers.VerifyFtpPath(path)) return; - // TODO: show invalid path dialog + + // TODO: Show invalid path dialog using var client = new AsyncFtpClient(); client.Host = FtpHelpers.GetFtpHost(path); @@ -1410,7 +1457,7 @@ await dispatcherQueue.EnqueueAsync(async () => } catch { - // network issue + // Network issue FtpManager.Credentials.Remove(client.Host); } }); @@ -1452,6 +1499,7 @@ public async Task EnumerateItemsFromStandardFolderAsync(string path, Cancel await DialogDisplayHelper.ShowDialogAsync( "AccessDenied".GetLocalizedResource(), "AccessDeniedToFolder".GetLocalizedResource()); + return -1; } else if (res == FileSystemStatusCode.NotFound) @@ -1459,6 +1507,7 @@ await DialogDisplayHelper.ShowDialogAsync( await DialogDisplayHelper.ShowDialogAsync( "FolderNotFoundDialog/Title".GetLocalizedResource(), "FolderNotFoundDialog/Text".GetLocalizedResource()); + return -1; } else @@ -1466,6 +1515,7 @@ await DialogDisplayHelper.ShowDialogAsync( await DialogDisplayHelper.ShowDialogAsync( "DriveUnpluggedDialog/Title".GetLocalizedResource(), res.ErrorCode.ToString()); + return -1; } } @@ -1500,7 +1550,9 @@ await DialogDisplayHelper.ShowDialogAsync( CurrentFolder = currentFolder; await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken); - return isBoxFolder ? 2 : 1; // Workaround for #7428 + + // Workaround for #7428 + return isBoxFolder ? 2 : 1; } else { @@ -1508,13 +1560,22 @@ await DialogDisplayHelper.ShowDialogAsync( { 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, + + 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)); + }) + .WithTimeoutAsync(TimeSpan.FromSeconds(5)); var itemModifiedDate = DateTime.Now; var itemCreatedDate = DateTime.Now; + try { FileTimeToSystemTime(ref findData.ftLastWriteTime, out var systemModifiedTimeOutput); @@ -1523,7 +1584,9 @@ await DialogDisplayHelper.ShowDialogAsync( FileTimeToSystemTime(ref findData.ftCreationTime, out SYSTEMTIME systemCreatedTimeOutput); itemCreatedDate = systemCreatedTimeOutput.ToDateTime(); } - catch (ArgumentException) { } + catch (ArgumentException) + { + } var isHidden = (((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden); var opacity = isHidden ? Constants.UI.DimItemOpacity : 1d; @@ -1544,25 +1607,29 @@ await DialogDisplayHelper.ShowDialogAsync( FileSize = null, FileSizeBytes = 0, }; + CurrentFolder = currentFolder; if (hFile == IntPtr.Zero) { await DialogDisplayHelper.ShowDialogAsync("DriveUnpluggedDialog/Title".GetLocalizedResource(), ""); + return -1; } else if (hFile.ToInt64() == -1) { await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken); - // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- - if (!filesAndFolders.Any() && errorCode == 0x5) // ERROR_ACCESS_DENIED + // errorCode == ERROR_ACCESS_DENIED + if (!filesAndFolders.Any() && errorCode == 0x5) { await DialogDisplayHelper.ShowDialogAsync( "AccessDenied".GetLocalizedResource(), "AccessDeniedToFolder".GetLocalizedResource()); + return -1; } + return 1; } else @@ -1604,15 +1671,21 @@ await Task.Run(async () => async (intermediateList) => { filesAndFolders.AddRange(intermediateList); + await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); - }, defaultIconPairs: DefaultIcons); + }, + defaultIconPairs: DefaultIcons); + filesAndFolders.AddRange(finalList); + await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); }); stopwatch.Stop(); + + Debug.WriteLine($"Enumerating items in {path} (device) completed in {stopwatch.ElapsedMilliseconds} milliseconds.\n"); } @@ -1631,10 +1704,12 @@ private async Task CheckCloudDriveSyncStatusAsync(IStorage 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; @@ -1653,20 +1728,28 @@ await Task.Factory.StartNew(() => 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; - var watchedItemsOperation = itemQueryResult.GetItemsAsync(0, 1); // Just get one item to start getting notifications + + // 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); + }, + default, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); } private void WatchForWin32FolderChanges(string? folderPath) @@ -1679,6 +1762,7 @@ private void WatchForWin32FolderChanges(string? folderPath) Filter = "*.*", NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName }; + watcher.Created += DirectoryWatcher_Changed; watcher.Deleted += DirectoryWatcher_Changed; watcher.Renamed += DirectoryWatcher_Changed; @@ -1698,12 +1782,13 @@ await dispatcherQueue.EnqueueAsync(() => private async void ItemQueryResult_ContentsChanged(IStorageQueryResultBase sender, object args) { - //query options have to be reapplied otherwise old results are returned + // 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); @@ -1791,7 +1876,8 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat operationQueue.Enqueue((action, FileName)); offset += notifyInfo.NextEntryOffset; - } while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled); + } + while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled); operationEvent.Set(); @@ -1800,8 +1886,10 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat } } } + CloseHandle(overlapped.hEvent); operationQueue.Clear(); + Debug.WriteLine("aWatcherAction done: {0}", rand); }); @@ -1810,9 +1898,13 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat if (aWatcherAction is not null) { aWatcherAction?.Cancel(); - aWatcherAction = null; // Prevent duplicate execution of this block + + // Prevent duplicate execution of this block + aWatcherAction = null; + Debug.WriteLine("watcher canceled"); } + CancelIoEx(hWatchDir, IntPtr.Zero); CloseHandle(hWatchDir); }); @@ -1835,21 +1927,24 @@ private async Task ProcessOperationQueue(CancellationToken cancellationToken, bo ListedItem? nextOfLastItemRemoved = null; var rand = Guid.NewGuid(); - // call when any edits have occurred + // Call when any edits have occurred async Task HandleChangesOccurredAsync() { await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); + if (lastItemAdded is not null) { await RequestSelectionAsync(new List() { lastItemAdded }); lastItemAdded = null; } + if (nextOfLastItemRemoved is not null) { await RequestSelectionAsync(new List() { nextOfLastItemRemoved }); nextOfLastItemRemoved = null; } + anyEdits = false; } @@ -1863,7 +1958,9 @@ async Task HandleChangesOccurredAsync() while (operationQueue.TryDequeue(out var operation)) { - if (cancellationToken.IsCancellationRequested) break; + if (cancellationToken.IsCancellationRequested) + break; + try { switch (operation.Action) @@ -1881,8 +1978,8 @@ async Task HandleChangesOccurredAsync() break; case FILE_ACTION_REMOVED: - // get the item that immediately follows matching item to be removed - // if the matching item is the last item, try to get the previous item; otherwise, null + // Get the item that immediately follows matching item to be removed + // If the matching item is the last item, try to get the previous item; otherwise, null var itemRemovedIndex = filesAndFolders.FindIndex(x => x.ItemPath.Equals(operation.FileName)); nextOfLastItemRemoved = filesAndFolders.ElementAtOrDefault(itemRemovedIndex + 1 < filesAndFolders.Count ? itemRemovedIndex + 1 : itemRemovedIndex - 1); var itemRemoved = await RemoveFileOrFolderAsync(operation.FileName); @@ -1936,9 +2033,10 @@ async Task HandleChangesOccurredAsync() public Task AddFileOrFolderFromShellFile(ShellFileItem item) { - return item.IsFolder - ? UniversalStorageEnumerator.AddFolderAsync(ShellStorageFolder.FromShellItem(item), currentStorageFolder, addFilesCTS.Token) - : UniversalStorageEnumerator.AddFileAsync(ShellStorageFile.FromShellItem(item), currentStorageFolder, addFilesCTS.Token); + 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) @@ -1982,8 +2080,7 @@ private async Task AddFileOrFolderAsync(ListedItem? item) additionalFlags); if (hFile.ToInt64() == -1) { - // If we cannot find the file (probably since it doesn't exist anymore) - // simply exit without adding it + // If we cannot find the file (probably since it doesn't exist anymore) simply exit without adding it return null; } @@ -2002,7 +2099,9 @@ private async Task AddFileOrFolderAsync(ListedItem? item) } ListedItem listedItem; - if ((findData.dwFileAttributes & 0x10) > 0) // FILE_ATTRIBUTE_DIRECTORY + + // FILE_ATTRIBUTE_DIRECTORY + if ((findData.dwFileAttributes & 0x10) > 0) listedItem = await Win32StorageEnumerator.GetFolder(findData, Directory.GetParent(fileOrFolderPath).FullName, addFilesCTS.Token); else listedItem = await Win32StorageEnumerator.GetFile(findData, Directory.GetParent(fileOrFolderPath).FullName, addFilesCTS.Token); @@ -2019,6 +2118,7 @@ private async Task AddFileOrFolderAsync(ListedItem? item) 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; @@ -2081,7 +2181,8 @@ await dispatcherQueue.EnqueueAsync(() => } } } - }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + }, + Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } finally { @@ -2122,6 +2223,7 @@ await dispatcherQueue.EnqueueAsync(() => { enumFolderSemaphore.Release(); } + return null; } @@ -2129,6 +2231,7 @@ public async Task AddSearchResultsToCollection(ObservableCollection { filesAndFolders.Clear(); filesAndFolders.AddRange(searchItems); + await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); } @@ -2154,9 +2257,11 @@ public async Task SearchAsync(FolderSearch search) await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); }; + await search.SearchAsync(results, searchCTS.Token); filesAndFolders = new List(results); + await OrderFilesAndFoldersAsync(); await ApplyFilesAndFoldersChangesAsync(); @@ -2185,13 +2290,16 @@ public void Dispose() public class PageTypeUpdatedEventArgs { public bool IsTypeCloudDrive { get; set; } + public bool IsTypeRecycleBin { get; set; } } public class WorkingDirectoryModifiedEventArgs : EventArgs { public string? Path { get; set; } + public string? Name { get; set; } + public bool IsLibrary { get; set; } } diff --git a/src/Files.App/ViewModels/MainPageViewModel.cs b/src/Files.App/ViewModels/MainPageViewModel.cs index e998d3a90203..cdde80f4f02c 100644 --- a/src/Files.App/ViewModels/MainPageViewModel.cs +++ b/src/Files.App/ViewModels/MainPageViewModel.cs @@ -31,6 +31,7 @@ public class MainPageViewModel : ObservableObject private readonly IUserSettingsService userSettingsService = Ioc.Default.GetRequiredService(); public IMultitaskingControl? MultitaskingControl { get; set; } + public List MultitaskingControls { get; } = new List(); public static ObservableCollection AppInstances { get; private set; } = new ObservableCollection(); @@ -219,8 +220,10 @@ public async void UpdateInstanceProperties(object navigationArg) { (windowTitle, _, _) = await GetSelectedTabInfoAsync(pathArgs); } + if (AppInstances.Count > 1) windowTitle = $"{windowTitle} ({AppInstances.Count})"; + if (navigationArg == SelectedTabItem?.TabItemArguments?.NavigationArg) App.GetAppWindow(App.Window).Title = $"{windowTitle} - Files"; } @@ -372,7 +375,9 @@ public async void OnNavigatedTo(NavigationEventArgs e) var tabArgs = TabItemArguments.Deserialize(tabArgsString); await AddNewTabByParam(tabArgs.InitialPageType, tabArgs.NavigationArg); } + var defaultArg = new TabItemArguments() { InitialPageType = typeof(PaneHolderPage), NavigationArg = "Home" }; + userSettingsService.PreferencesSettingsService.LastSessionTabList = new List { defaultArg.Serialize() }; } else @@ -395,7 +400,6 @@ public async void OnNavigatedTo(NavigationEventArgs e) await AddNewTabByParam(tabArgs.InitialPageType, tabArgs.NavigationArg); } - // Load the app theme resources App.AppThemeResourcesHelper.LoadAppResources(); } @@ -448,7 +452,9 @@ public static async Task AddNewTabByParam(Type type, object tabViewItemArgs, int }; tabItem.Control.ContentChanged += Control_ContentChanged; + await UpdateTabInfo(tabItem, tabViewItemArgs); + var index = atIndex == -1 ? AppInstances.Count : atIndex; AppInstances.Insert(index, tabItem); App.AppModel.TabStripSelectedIndex = index; @@ -458,6 +464,7 @@ public static async void Control_ContentChanged(object? sender, TabItemArguments { if (sender is null) return; + var matchingTabItem = AppInstances.SingleOrDefault(x => x.Control == (TabItemControl)sender); if (matchingTabItem is null) return; diff --git a/src/Files.App/ViewModels/Pages/YourHomeViewModel.cs b/src/Files.App/ViewModels/Pages/YourHomeViewModel.cs index 15deb419d9a8..1fd7f4d8c74d 100644 --- a/src/Files.App/ViewModels/Pages/YourHomeViewModel.cs +++ b/src/Files.App/ViewModels/Pages/YourHomeViewModel.cs @@ -84,4 +84,4 @@ public void Dispose() #endregion IDisposable } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/PreviewPaneViewModel.cs b/src/Files.App/ViewModels/PreviewPaneViewModel.cs index d8b04d77b85d..9a7b656cf189 100644 --- a/src/Files.App/ViewModels/PreviewPaneViewModel.cs +++ b/src/Files.App/ViewModels/PreviewPaneViewModel.cs @@ -96,9 +96,7 @@ private async Task LoadPreviewControlAsync(CancellationToken token, bool downloa var control = await GetBuiltInPreviewControlAsync(SelectedItem, downloadItem); if (token.IsCancellationRequested) - { return; - } if (control is not null) { @@ -109,12 +107,12 @@ private async Task LoadPreviewControlAsync(CancellationToken token, bool downloa var basicModel = new BasicPreviewViewModel(SelectedItem); await basicModel.LoadAsync(); + control = new BasicPreview(basicModel); if (token.IsCancellationRequested) - { return; - } + PreviewPaneContent = control; PreviewPaneState = PreviewPaneStates.PreviewAndDetailsAvailable; } @@ -129,12 +127,14 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new FolderPreviewViewModel(item); await model.LoadAsync(); + return new FolderPreview(model); } else { var model = new BasicPreviewViewModel(SelectedItem); await model.LoadAsync(); + return new BasicPreview(model); } } @@ -143,6 +143,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new ShortcutPreviewViewModel(SelectedItem); await model.LoadAsync(); + return new BasicPreview(model); } @@ -150,6 +151,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new ArchivePreviewViewModel(item); await model.LoadAsync(); + return new BasicPreview(model); } @@ -157,17 +159,17 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new FolderPreviewViewModel(item); await model.LoadAsync(); + return new FolderPreview(model); } if (item.FileExtension is null) - { return null; - } if (item.SyncStatusUI.SyncStatus is CloudDriveSyncStatus.FileOnline && !downloadItem) { ShowCloudItemButton = true; + return null; } @@ -177,6 +179,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new MediaPreviewViewModel(item); await model.LoadAsync(); + return new MediaPreview(model); } @@ -184,6 +187,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new MarkdownPreviewViewModel(item); await model.LoadAsync(); + return new MarkdownPreview(model); } @@ -191,6 +195,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new ImagePreviewViewModel(item); await model.LoadAsync(); + return new ImagePreview(model); } @@ -198,6 +203,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new TextPreviewViewModel(item); await model.LoadAsync(); + return new TextPreview(model); } @@ -205,6 +211,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new PDFPreviewViewModel(item); await model.LoadAsync(); + return new PDFPreview(model); } @@ -212,6 +219,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new HtmlPreviewViewModel(item); await model.LoadAsync(); + return new HtmlPreview(model); } @@ -219,6 +227,7 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new RichTextPreviewViewModel(item); await model.LoadAsync(); + return new RichTextPreview(model); } @@ -226,10 +235,12 @@ private async Task GetBuiltInPreviewControlAsync(ListedItem item, b { var model = new CodePreviewViewModel(item); await model.LoadAsync(); + return new CodePreview(model); } var control = await TextPreviewViewModel.TryLoadAsTextAsync(item); + return control ?? null; } @@ -250,6 +261,7 @@ public async void UpdateSelectedItemPreview(bool downloadItem = false) { Debug.WriteLine(e); loadCancellationTokenSource?.Cancel(); + // If initial loading fails, attempt to load a basic preview (thumbnail and details only) // If that fails, revert to no preview/details available as long as the item is not a shortcut or folder if (SelectedItem is not null && !SelectedItem.IsShortcut && SelectedItem.PrimaryItemAttribute != StorageItemTypes.Folder) @@ -304,6 +316,7 @@ private async Task LoadBasicPreviewAsync() { var basicModel = new BasicPreviewViewModel(SelectedItem); await basicModel.LoadAsync(); + PreviewPaneContent = new BasicPreview(basicModel); PreviewPaneState = PreviewPaneStates.PreviewAndDetailsAvailable; } @@ -344,4 +357,4 @@ public enum PreviewPaneStates PreviewAndDetailsAvailable, LoadingPreview, } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/ArchivePreviewViewModel.cs b/src/Files.App/ViewModels/Previews/ArchivePreviewViewModel.cs index beb09b2b6c14..61fb48956c2d 100644 --- a/src/Files.App/ViewModels/Previews/ArchivePreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/ArchivePreviewViewModel.cs @@ -10,21 +10,31 @@ namespace Files.App.ViewModels.Previews { public class ArchivePreviewViewModel : BasePreviewModel { - public ArchivePreviewViewModel(ListedItem item) : base(item) { } + public ArchivePreviewViewModel(ListedItem item) + : base(item) + { + } public override async Task> LoadPreviewAndDetailsAsync() { var details = new List(); + using SevenZipExtractor zipFile = await FilesystemTasks.Wrap(async () => { var arch = new SevenZipExtractor(await Item.ItemFile.OpenStreamForReadAsync()); - return arch?.ArchiveFileData is null ? null : arch; // Force load archive (1665013614u) + + // Force load archive (1665013614u) + return arch?.ArchiveFileData is null ? null : arch; }); + if (zipFile is null) { - _ = await base.LoadPreviewAndDetailsAsync(); // Loads the thumbnail preview + // Loads the thumbnail preview + _ = await base.LoadPreviewAndDetailsAsync(); + return details; } + //zipFile.IsStreamOwner = true; var folderCount = 0; @@ -39,14 +49,16 @@ public override async Task> LoadPreviewAndDetailsAsync() totalSize += entry.Size; } } + folderCount = (int)zipFile.FilesCount - fileCount; string propertyItemCount = string.Format("DetailsArchiveItemCount".GetLocalizedResource(), zipFile.FilesCount, fileCount, folderCount); details.Add(GetFileProperty("PropertyItemCount", propertyItemCount)); details.Add(GetFileProperty("PropertyUncompressedSize", totalSize.ToLongSizeString())); - _ = await base.LoadPreviewAndDetailsAsync(); // Loads the thumbnail preview + // Loads the thumbnail preview + _ = await base.LoadPreviewAndDetailsAsync(); return details; } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/BasePreviewModel.cs b/src/Files.App/ViewModels/Previews/BasePreviewModel.cs index 20f265165e71..d8ceea2c713b 100644 --- a/src/Files.App/ViewModels/Previews/BasePreviewModel.cs +++ b/src/Files.App/ViewModels/Previews/BasePreviewModel.cs @@ -37,7 +37,8 @@ public BitmapImage FileImage /// public CancellationTokenSource LoadCancelledTokenSource { get; } = new CancellationTokenSource(); - public BasePreviewModel(ListedItem item) : base() => Item = item; + public BasePreviewModel(ListedItem item) : base() + => Item = item; public delegate void LoadedEventHandler(object sender, EventArgs e); @@ -58,11 +59,13 @@ public static Task ReadFileAsTextAsync(BaseStorageFile file, int maxLeng public virtual async Task LoadAsync() { List detailsFull = new(); + if (Item.ItemFile is null) { var rootItem = await FilesystemTasks.Wrap(() => DrivesManager.GetRootFromPathAsync(Item.ItemPath)); Item.ItemFile = await StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath, rootItem); } + await Task.Run(async () => { DetailsFromPreview = await LoadPreviewAndDetailsAsync(); @@ -91,6 +94,7 @@ await Task.Run(async () => public async virtual Task> LoadPreviewAndDetailsAsync() { var iconData = await FileThumbnailHelper.LoadIconFromStorageItemAsync(Item.ItemFile, 256, ThumbnailMode.SingleItem); + iconData ??= await FileThumbnailHelper.LoadIconWithoutOverlayAsync(Item.ItemPath, 256); if (iconData is not null) { @@ -109,7 +113,8 @@ public async virtual Task> LoadPreviewAndDetailsAsync() /// /// /// - public virtual void PreviewControlBase_Unloaded(object sender, RoutedEventArgs e) => LoadCancelledTokenSource.Cancel(); + public virtual void PreviewControlBase_Unloaded(object sender, RoutedEventArgs e) + => LoadCancelledTokenSource.Cancel(); protected static FileProperty GetFileProperty(string nameResource, object value) => new() { NameResource = nameResource, Value = value }; @@ -127,7 +132,7 @@ private async Task> GetSystemFilePropertiesAsync() (double?)list.Find(x => x.Property is "System.GPS.LongitudeDecimal").Value ); - // adds the value for the file tag + // Adds the value for the file tag list.FirstOrDefault(x => x.ID is "filetag").Value = Item.FileTagsUI is not null ? string.Join(',', Item.FileTagsUI.Select(x => x.Name)) : null; @@ -141,4 +146,4 @@ public DetailsOnlyPreviewModel(ListedItem item) : base(item) { } public override Task> LoadPreviewAndDetailsAsync() => Task.FromResult(DetailsFromPreview); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/BasicPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/BasicPreviewViewModel.cs index 6f1c07b772b0..e709ae7d8af3 100644 --- a/src/Files.App/ViewModels/Previews/BasicPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/BasicPreviewViewModel.cs @@ -6,4 +6,4 @@ public class BasicPreviewViewModel : BasePreviewModel { public BasicPreviewViewModel(ListedItem item) : base(item) { } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/CodePreviewViewModel.cs b/src/Files.App/ViewModels/Previews/CodePreviewViewModel.cs index f3f8d9061a2a..29703908b947 100644 --- a/src/Files.App/ViewModels/Previews/CodePreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/CodePreviewViewModel.cs @@ -29,7 +29,10 @@ public ILanguage CodeLanguage private set => SetProperty(ref codeLanguage, value); } - public CodePreviewViewModel(ListedItem item) : base(item) { } + public CodePreviewViewModel(ListedItem item) + : base(item) + { + } public static bool ContainsExtension(string extension) => extensions.Value.ContainsKey(extension); @@ -76,6 +79,7 @@ private static IReadOnlyDictionary GetDictionary() }; var dictionary = new Dictionary(); + foreach (var item in items) { var extensions = item.Value.Split(',').Select(ext => $".{ext}"); @@ -84,7 +88,8 @@ private static IReadOnlyDictionary GetDictionary() dictionary.Add(extension, item.Key); } } + return new ReadOnlyDictionary(dictionary); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/FolderPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/FolderPreviewViewModel.cs index 105ab00578ad..c0e66c38d49b 100644 --- a/src/Files.App/ViewModels/Previews/FolderPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/FolderPreviewViewModel.cs @@ -16,17 +16,20 @@ namespace Files.App.ViewModels.Previews public class FolderPreviewViewModel { private readonly IPreferencesSettingsService preferencesSettingsService = Ioc.Default.GetService(); + private static readonly IDateTimeFormatter dateTimeFormatter = Ioc.Default.GetService(); public ListedItem Item { get; } - public BitmapImage Thumbnail { get; set; } = new BitmapImage(); + public BitmapImage Thumbnail { get; set; } = new(); private BaseStorageFolder Folder { get; set; } - public FolderPreviewViewModel(ListedItem item) => Item = item; + public FolderPreviewViewModel(ListedItem item) + => Item = item; - public Task LoadAsync() => LoadPreviewAndDetailsAsync(); + public Task LoadAsync() + => LoadPreviewAndDetailsAsync(); private async Task LoadPreviewAndDetailsAsync() { @@ -41,6 +44,7 @@ private async Task LoadPreviewAndDetailsAsync() Thumbnail = await iconData.ToBitmapAsync(); var info = await Folder.GetBasicPropertiesAsync(); + Item.FileDetails = new() { GetFileProperty("PropertyItemCount", items.Count), @@ -56,4 +60,4 @@ private async Task LoadPreviewAndDetailsAsync() private static FileProperty GetFileProperty(string nameResource, object value) => new() { NameResource = nameResource, Value = value }; } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/HtmlPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/HtmlPreviewViewModel.cs index 8231e75a4153..1a36d6baf2f5 100644 --- a/src/Files.App/ViewModels/Previews/HtmlPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/HtmlPreviewViewModel.cs @@ -14,7 +14,10 @@ public string TextValue private set => SetProperty(ref textValue, value); } - public HtmlPreviewViewModel(ListedItem item) : base(item) { } + public HtmlPreviewViewModel(ListedItem item) + : base(item) + { + } public static bool ContainsExtension(string extension) => extension is ".htm" or ".html" or ".svg"; @@ -22,7 +25,8 @@ public static bool ContainsExtension(string extension) public async override Task> LoadPreviewAndDetailsAsync() { TextValue = await ReadFileAsTextAsync(Item.ItemFile); + return new List(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/ImagePreviewViewModel.cs b/src/Files.App/ViewModels/Previews/ImagePreviewViewModel.cs index 0bf379b5f92f..8f24953a995b 100644 --- a/src/Files.App/ViewModels/Previews/ImagePreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/ImagePreviewViewModel.cs @@ -20,14 +20,19 @@ public ImageSource ImageSource private set => SetProperty(ref imageSource, value); } - public ImagePreviewViewModel(ListedItem item) : base(item) { } + public ImagePreviewViewModel(ListedItem item) + : base(item) + { + } + // TODO: Use existing helper mothods public static bool ContainsExtension(string extension) => extension is ".png" or ".jpg" or ".jpeg" or ".bmp" or ".gif" or ".tiff" or ".ico" or ".webp"; public override async Task> LoadPreviewAndDetailsAsync() { using IRandomAccessStream stream = await Item.ItemFile.OpenAsync(FileAccessMode.Read); + await App.Window.DispatcherQueue.EnqueueAsync(async () => { BitmapImage bitmap = new(); @@ -38,4 +43,4 @@ await App.Window.DispatcherQueue.EnqueueAsync(async () => return new List(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/MarkdownPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/MarkdownPreviewViewModel.cs index 6391a334e133..1b604fc0dfb6 100644 --- a/src/Files.App/ViewModels/Previews/MarkdownPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/MarkdownPreviewViewModel.cs @@ -15,15 +15,20 @@ public string TextValue private set => SetProperty(ref textValue, value); } - public MarkdownPreviewViewModel(ListedItem item) : base(item) { } + public MarkdownPreviewViewModel(ListedItem item) + : base(item) + { + } - public static bool ContainsExtension(string extension) => extension is ".md" or ".markdown"; + public static bool ContainsExtension(string extension) + => extension is ".md" or ".markdown"; public override async Task> LoadPreviewAndDetailsAsync() { var text = await ReadFileAsTextAsync(Item.ItemFile); TextValue = text.Left(Constants.PreviewPane.TextCharacterLimit); + return new List(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/MediaPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/MediaPreviewViewModel.cs index 40a60ba36a6e..6d2ded81693e 100644 --- a/src/Files.App/ViewModels/Previews/MediaPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/MediaPreviewViewModel.cs @@ -21,23 +21,29 @@ public MediaSource Source public MediaPreviewViewModel(ListedItem item) : base(item) { } - public static bool ContainsExtension(string extension) => extension - is ".mp4" or ".webm" or ".ogg" or ".mov" or ".qt" or ".mp4" or ".m4v" // Video - or ".mp4v" or ".3g2" or ".3gp2" or ".3gp" or ".3gpp" or ".mkv" // Video - or ".mp3" or ".m4a" or ".wav" or ".wma" or ".aac" or ".adt" or ".adts" or ".cda" or ".flac"; // Audio + // TODO: Use existing helper mothods + public static bool ContainsExtension(string extension) + => extension is + // Video + ".mp4" or ".webm" or ".ogg" or ".mov" or ".qt" or ".mp4" or ".m4v" or ".mp4v" or ".3g2" or ".3gp2" or ".3gp" or ".3gpp" or ".mkv" + // Audio + or ".mp3" or ".m4a" or ".wav" or ".wma" or ".aac" or ".adt" or ".adts" or ".cda" or ".flac"; - public void TogglePlayback() => TogglePlaybackRequested?.Invoke(this, null); + public void TogglePlayback() + => TogglePlaybackRequested?.Invoke(this, null); public override Task> LoadPreviewAndDetailsAsync() { Source = MediaSource.CreateFromStorageFile(Item.ItemFile); + return Task.FromResult(new List()); } public override void PreviewControlBase_Unloaded(object sender, RoutedEventArgs e) { Source = null; + base.PreviewControlBase_Unloaded(sender, e); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/PDFPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/PDFPreviewViewModel.cs index a9cd930995d9..bd3fd54439af 100644 --- a/src/Files.App/ViewModels/Previews/PDFPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/PDFPreviewViewModel.cs @@ -23,7 +23,7 @@ public Visibility LoadingBarVisibility private set => SetProperty(ref loadingBarVisibility, value); } - // the pips pager will crash when binding directly to Pages.Count, so count the pages here + // The pips pager will crash when binding directly to Pages.Count, so count the pages here private int pageCount; public int PageCount { @@ -33,15 +33,20 @@ public int PageCount public ObservableCollection Pages { get; } = new(); - public PDFPreviewViewModel(ListedItem item) : base(item) { } + public PDFPreviewViewModel(ListedItem item) + : base(item) + { + } - public static bool ContainsExtension(string extension) => extension is ".pdf"; + public static bool ContainsExtension(string extension) + => extension is ".pdf"; public async override Task> LoadPreviewAndDetailsAsync() { var fileStream = await Item.ItemFile.OpenReadAsync(); var pdf = await PdfDocument.LoadFromStreamAsync(fileStream); TryLoadPagesAsync(pdf, fileStream); + var details = new List { // Add the number of pages to the details @@ -69,8 +74,7 @@ public async void TryLoadPagesAsync(PdfDocument pdf, IRandomAccessStream fileStr private async Task LoadPagesAsync(PdfDocument pdf) { - // This fixes an issue where loading an absurdly large PDF would take to much RAM - // and eventually cause a crash + // This fixes an issue where loading an absurdly large PDF would take to much RAM and eventually cause a crash var limit = Math.Clamp(pdf.PageCount, 0, Constants.PreviewPane.PDFPageLimit); for (uint i = 0; i < limit; i++) @@ -99,9 +103,11 @@ await App.Window.DispatcherQueue.EnqueueAsync(async () => await src.SetSourceAsync(stream); Pages.Add(pageData); + ++PageCount; }); } + LoadingBarVisibility = Visibility.Collapsed; } } @@ -109,7 +115,9 @@ await App.Window.DispatcherQueue.EnqueueAsync(async () => public struct PageViewModel { public int PageNumber { get; set; } + public BitmapImage PageImage { get; set; } + public SoftwareBitmap PageImageSB { get; set; } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/RichTextPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/RichTextPreviewViewModel.cs index 2f3da459d2fb..d0d5e9d183f8 100644 --- a/src/Files.App/ViewModels/Previews/RichTextPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/RichTextPreviewViewModel.cs @@ -13,12 +13,14 @@ public class RichTextPreviewViewModel : BasePreviewModel public RichTextPreviewViewModel(ListedItem item) : base(item) { } - public static bool ContainsExtension(string extension) => extension is ".rtf"; + public static bool ContainsExtension(string extension) + => extension is ".rtf"; public async override Task> LoadPreviewAndDetailsAsync() { Stream = await Item.ItemFile.OpenReadAsync(); + return new List(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/ShortcutPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/ShortcutPreviewViewModel.cs index 62cf50ff34b0..393f0ed6fa10 100644 --- a/src/Files.App/ViewModels/Previews/ShortcutPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/ShortcutPreviewViewModel.cs @@ -22,13 +22,16 @@ public async override Task> LoadPreviewAndDetailsAsync() GetFileProperty("PropertyItemTarget", item.TargetPath), GetFileProperty("Arguments", item.Arguments), }; + await LoadItemThumbnail(); + return details; } public override async Task LoadAsync() { var details = await LoadPreviewAndDetailsAsync(); + Item.FileDetails?.Clear(); Item.FileDetails = new(details.OfType()); } @@ -42,4 +45,4 @@ private async Task LoadItemThumbnail() } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Previews/TextPreviewViewModel.cs b/src/Files.App/ViewModels/Previews/TextPreviewViewModel.cs index 326e9367461f..ba7660b0eeae 100644 --- a/src/Files.App/ViewModels/Previews/TextPreviewViewModel.cs +++ b/src/Files.App/ViewModels/Previews/TextPreviewViewModel.cs @@ -18,9 +18,13 @@ public string TextValue private set => SetProperty(ref textValue, value); } - public TextPreviewViewModel(ListedItem item) : base(item) { } + public TextPreviewViewModel(ListedItem item) + : base(item) + { + } - public static bool ContainsExtension(string extension) => extension is ".txt"; + public static bool ContainsExtension(string extension) + => extension is ".txt"; public async override Task> LoadPreviewAndDetailsAsync() { @@ -70,6 +74,7 @@ public static async Task TryLoadAsTextAsync(ListedItem item) } } - private static bool ExcludedExtensions(string extension) => extension is ".iso"; + private static bool ExcludedExtensions(string extension) + => extension is ".iso"; } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/BaseProperties.cs b/src/Files.App/ViewModels/Properties/BaseProperties.cs index b9293d7ca61d..af8b43725f2c 100644 --- a/src/Files.App/ViewModels/Properties/BaseProperties.cs +++ b/src/Files.App/ViewModels/Properties/BaseProperties.cs @@ -20,6 +20,7 @@ public abstract class BaseProperties protected static readonly IDateTimeFormatter dateTimeFormatter = Ioc.Default.GetService(); public IShellPage AppInstance { get; set; } = null; + public SelectedItemsPropertiesViewModel ViewModel { get; set; } public CancellationTokenSource TokenSource { get; set; } @@ -33,10 +34,12 @@ public abstract class BaseProperties public async void GetOtherProperties(IStorageItemExtraProperties properties) { string dateAccessedProperty = "System.DateAccessed"; + List propertiesName = new() { dateAccessedProperty }; + IDictionary extraProperties = await properties.RetrievePropertiesAsync(propertiesName); // Cannot get date and owner in MTP devices @@ -56,8 +59,13 @@ public async Task CalculateFolderSizeAsync(string path, CancellationToken FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic; int additionalFlags = FIND_FIRST_EX_LARGE_FETCH; - IntPtr hFile = FindFirstFileExFromApp(path + "\\*.*", findInfoLevel, out WIN32_FIND_DATA findData, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, - additionalFlags); + IntPtr hFile = FindFirstFileExFromApp( + path + "\\*.*", + findInfoLevel, + out WIN32_FIND_DATA findData, + FINDEX_SEARCH_OPS.FindExSearchNameMatch, + IntPtr.Zero, + additionalFlags); var count = 0; if (hFile.ToInt64() != -1) @@ -89,15 +97,17 @@ await Dispatcher.EnqueueAsync(() => ViewModel.ItemSizeBytes = size; ViewModel.ItemSize = size.ToSizeString(); SetItemsCountString(); - }, DispatcherQueuePriority.Low); + }, + DispatcherQueuePriority.Low); } if (token.IsCancellationRequested) - { break; - } - } while (FindNextFile(hFile, out findData)); + } + while (FindNextFile(hFile, out findData)); + FindClose(hFile); + return size; } else @@ -118,4 +128,4 @@ public void SetItemsCountString() } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/CombinedProperties.cs b/src/Files.App/ViewModels/Properties/CombinedProperties.cs index 107f16cabab3..e6b923790ee7 100644 --- a/src/Files.App/ViewModels/Properties/CombinedProperties.cs +++ b/src/Files.App/ViewModels/Properties/CombinedProperties.cs @@ -16,8 +16,12 @@ internal class CombinedProperties : BaseProperties { public List List { get; } - public CombinedProperties(SelectedItemsPropertiesViewModel viewModel, CancellationTokenSource tokenSource, - DispatcherQueue coreDispatcher, List listedItems, IShellPage instance) + public CombinedProperties( + SelectedItemsPropertiesViewModel viewModel, + CancellationTokenSource tokenSource, + DispatcherQueue coreDispatcher, + List listedItems, + IShellPage instance) { ViewModel = viewModel; TokenSource = tokenSource; @@ -33,6 +37,7 @@ public override void GetBaseProperties() if (List is not null) { ViewModel.LoadCombinedItemsGlyph = true; + if (List.All(x => x.ItemType.Equals(List.First().ItemType))) { ViewModel.ItemType = string.Format("PropertiesDriveItemTypesEquals".GetLocalizedResource(), List.First().ItemType); @@ -41,21 +46,20 @@ public override void GetBaseProperties() { ViewModel.ItemType = "PropertiesDriveItemTypeDifferent".GetLocalizedResource(); } + var itemsPath = List.Select(Item => (Item as RecycleBinItem)?.ItemOriginalFolder ?? (Path.IsPathRooted(Item.ItemPath) ? Path.GetDirectoryName(Item.ItemPath) : Item.ItemPath)); + if (itemsPath.Distinct().Count() == 1) - { ViewModel.ItemPath = string.Format("PropertiesCombinedItemPath".GetLocalizedResource(), itemsPath.First()); - } } } public override async void GetSpecialProperties() { if (List.All(x => x.PrimaryItemAttribute == StorageItemTypes.File)) - { ViewModel.IsReadOnly = List.All(x => NativeFileOperationsHelper.HasFileAttribute(x.ItemPath, System.IO.FileAttributes.ReadOnly)); - } + ViewModel.IsHidden = List.All(x => NativeFileOperationsHelper.HasFileAttribute(x.ItemPath, System.IO.FileAttributes.Hidden)); ViewModel.LastSeparatorVisibility = false; @@ -69,6 +73,7 @@ public override async void GetSpecialProperties() long foldersSize = 0; ViewModel.ItemSizeProgressVisibility = true; + foreach (var item in List) { if (item.PrimaryItemAttribute == StorageItemTypes.Folder) @@ -76,8 +81,10 @@ public override async void GetSpecialProperties() var fileSizeTask = Task.Run(async () => { var size = await CalculateFolderSizeAsync(item.ItemPath, TokenSource.Token); + return size; }); + try { foldersSize += await fileSizeTask; @@ -88,10 +95,12 @@ public override async void GetSpecialProperties() } } } + ViewModel.ItemSizeProgressVisibility = false; totalSize = filesSize + foldersSize; ViewModel.ItemSize = totalSize.ToLongSizeString(); + SetItemsCountString(); } @@ -100,31 +109,36 @@ private void ViewModel_PropertyChanged(object sender, System.ComponentModel.Prop switch (e.PropertyName) { case "IsReadOnly": - if (ViewModel.IsReadOnly) { - List.ForEach(x => NativeFileOperationsHelper.SetFileAttribute( - x.ItemPath, System.IO.FileAttributes.ReadOnly)); - } - else - { - List.ForEach(x => NativeFileOperationsHelper.UnsetFileAttribute( - x.ItemPath, System.IO.FileAttributes.ReadOnly)); + if (ViewModel.IsReadOnly) + { + List.ForEach(x => NativeFileOperationsHelper.SetFileAttribute( + x.ItemPath, System.IO.FileAttributes.ReadOnly)); + } + else + { + List.ForEach(x => NativeFileOperationsHelper.UnsetFileAttribute( + x.ItemPath, System.IO.FileAttributes.ReadOnly)); + } } break; case "IsHidden": - if (ViewModel.IsHidden) - { - List.ForEach(x => NativeFileOperationsHelper.SetFileAttribute( - x.ItemPath, System.IO.FileAttributes.Hidden)); - } - else { - List.ForEach(x => NativeFileOperationsHelper.UnsetFileAttribute( - x.ItemPath, System.IO.FileAttributes.Hidden)); + if (ViewModel.IsHidden) + { + List.ForEach(x => NativeFileOperationsHelper.SetFileAttribute( + x.ItemPath, System.IO.FileAttributes.Hidden)); + } + else + { + List.ForEach(x => NativeFileOperationsHelper.UnsetFileAttribute( + x.ItemPath, System.IO.FileAttributes.Hidden)); + } + } break; } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/CompatibilityProperties.cs b/src/Files.App/ViewModels/Properties/CompatibilityProperties.cs index 601a95962992..19b8bf25185e 100644 --- a/src/Files.App/ViewModels/Properties/CompatibilityProperties.cs +++ b/src/Files.App/ViewModels/Properties/CompatibilityProperties.cs @@ -15,7 +15,8 @@ public class CompatibilityProperties : ObservableObject { public ListedItem Item { get; } - private string ExePath => Item is ShortcutItem sht ? sht.TargetPath : Item.ItemPath; + private string ExePath + => Item is ShortcutItem sht ? sht.TargetPath : Item.ItemPath; private CompatibilityOptions compatibilityOptions; public CompatibilityOptions CompatibilityOptions diff --git a/src/Files.App/ViewModels/Properties/DriveProperties.cs b/src/Files.App/ViewModels/Properties/DriveProperties.cs index f265bebb88be..596488305f31 100644 --- a/src/Files.App/ViewModels/Properties/DriveProperties.cs +++ b/src/Files.App/ViewModels/Properties/DriveProperties.cs @@ -25,13 +25,19 @@ public override void GetBaseProperties() if (Drive is null) return; - ViewModel.CustomIconSource = null; //Drive.IconSource; + //Drive.IconSource; + ViewModel.CustomIconSource = null; + ViewModel.IconData = Drive.IconData; - ViewModel.LoadCustomIcon = false; //Drive.IconSource is not null && Drive.IconData is null; + + // Drive.IconSource is not null && Drive.IconData is null; + ViewModel.LoadCustomIcon = false; ViewModel.LoadFileIcon = Drive.IconData is not null; + ViewModel.ItemName = Drive.Text; ViewModel.OriginalItemName = Drive.Text; - // Note: if DriveType enum changes, the corresponding resource keys should change too + + // NOTE: If DriveType enum changes, the corresponding resource keys should change too ViewModel.ItemType = string.Format("DriveType{0}", Drive.Type).GetLocalizedResource(); } @@ -51,12 +57,14 @@ public async override void GetSpecialProperties() { ViewModel.IconData = await FileThumbnailHelper.LoadIconWithoutOverlayAsync(Drive.Path, 80); } + ViewModel.IconData ??= await FileThumbnailHelper.LoadIconWithoutOverlayAsync(Drive.DeviceID, 80); // For network shortcuts } if (diskRoot is null || diskRoot.Properties is null) { ViewModel.LastSeparatorVisibility = false; + return; } @@ -80,4 +88,4 @@ public async override void GetSpecialProperties() } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/FileProperties.cs b/src/Files.App/ViewModels/Properties/FileProperties.cs index f49fb6750e2b..7201cfea8d6c 100644 --- a/src/Files.App/ViewModels/Properties/FileProperties.cs +++ b/src/Files.App/ViewModels/Properties/FileProperties.cs @@ -24,8 +24,12 @@ public class FileProperties : BaseProperties { public ListedItem Item { get; } - public FileProperties(SelectedItemsPropertiesViewModel viewModel, CancellationTokenSource tokenSource, - DispatcherQueue coreDispatcher, ListedItem item, IShellPage instance) + public FileProperties( + SelectedItemsPropertiesViewModel viewModel, + CancellationTokenSource tokenSource, + DispatcherQueue coreDispatcher, + ListedItem item, + IShellPage instance) { ViewModel = viewModel; TokenSource = tokenSource; @@ -59,8 +63,9 @@ public override void GetBaseProperties() var shortcutItem = (ShortcutItem)Item; - var isApplication = FileExtensionHelpers.IsExecutableFile(shortcutItem.TargetPath) || - FileExtensionHelpers.IsMsiFile(shortcutItem.TargetPath); + var isApplication = + FileExtensionHelpers.IsExecutableFile(shortcutItem.TargetPath) || + FileExtensionHelpers.IsMsiFile(shortcutItem.TargetPath); ViewModel.ShortcutItemType = isApplication ? "Application".GetLocalizedResource() : Item.IsLinkItem ? "PropertiesShortcutTypeLink".GetLocalizedResource() : "PropertiesShortcutTypeFile".GetLocalizedResource(); @@ -70,9 +75,12 @@ public override void GetBaseProperties() ViewModel.ShortcutItemWorkingDirVisibility = Item.IsLinkItem || shortcutItem.IsSymLink ? false : true; ViewModel.ShortcutItemArguments = shortcutItem.Arguments; ViewModel.ShortcutItemArgumentsVisibility = Item.IsLinkItem || shortcutItem.IsSymLink ? false : true; + if (isApplication) ViewModel.RunAsAdmin = shortcutItem.RunAsAdmin; + ViewModel.IsSelectedItemShortcut = FileExtensionHelpers.IsShortcutFile(Item.FileExtension); + ViewModel.ShortcutItemOpenLinkCommand = new RelayCommand(async () => { if (Item.IsLinkItem) @@ -85,7 +93,8 @@ public override void GetBaseProperties() await App.Window.DispatcherQueue.EnqueueAsync( () => NavigationHelpers.OpenPathInNewTab(Path.GetDirectoryName(ViewModel.ShortcutItemPath))); } - }, () => + }, + () => { return !string.IsNullOrWhiteSpace(ViewModel.ShortcutItemPath); }); @@ -157,8 +166,11 @@ public async void GetSystemFileProperties() var list = await FileProperty.RetrieveAndInitializePropertiesAsync(file); - list.Find(x => x.ID == "address").Value = await GetAddressFromCoordinatesAsync((double?)list.Find(x => x.Property == "System.GPS.LatitudeDecimal").Value, - (double?)list.Find(x => x.Property == "System.GPS.LongitudeDecimal").Value); + list.Find(x => x.ID == "address").Value = + await GetAddressFromCoordinatesAsync((double?)list.Find( + x => x.Property == "System.GPS.LatitudeDecimal").Value, + (double?)list.Find(x => x.Property == "System.GPS.LongitudeDecimal").Value); + // Find Encoding Bitrate property and convert it to kbps var encodingBitrate = list.Find(x => x.Property == "System.Audio.EncodingBitrate"); if (encodingBitrate?.Value is not null) @@ -175,6 +187,7 @@ public async void GetSystemFileProperties() .Select(group => new FilePropertySection(group) { Key = group.Key }) .Where(section => !section.All(fileProp => fileProp.Value is null)) .OrderBy(group => group.Priority); + ViewModel.PropertySections = new ObservableCollection(query); ViewModel.FileProperties = new ObservableCollection(list.Where(i => i.Value is not null)); } @@ -216,6 +229,7 @@ public async Task SyncPropertyChangesAsync() return; var failedProperties = ""; + foreach (var group in ViewModel.PropertySections) { foreach (FileProperty prop in group) @@ -304,6 +318,7 @@ private async void ViewModel_PropertyChanged(object sender, System.ComponentMode System.IO.FileAttributes.ReadOnly ); } + break; case "IsHidden": @@ -321,6 +336,7 @@ private async void ViewModel_PropertyChanged(object sender, System.ComponentMode System.IO.FileAttributes.Hidden ); } + break; case "RunAsAdmin": @@ -331,9 +347,9 @@ private async void ViewModel_PropertyChanged(object sender, System.ComponentMode return; await FileOperationsHelpers.CreateOrUpdateLinkAsync(Item.ItemPath, ViewModel.ShortcutItemPath, ViewModel.ShortcutItemArguments, ViewModel.ShortcutItemWorkingDir, ViewModel.RunAsAdmin); + break; } } } - } diff --git a/src/Files.App/ViewModels/Properties/FileProperty.cs b/src/Files.App/ViewModels/Properties/FileProperty.cs index a161ee978390..ff809e6a3d78 100644 --- a/src/Files.App/ViewModels/Properties/FileProperty.cs +++ b/src/Files.App/ViewModels/Properties/FileProperty.cs @@ -35,7 +35,8 @@ public class FileProperty : ObservableObject /// /// The name of the section to display /// - public string Section => SectionResource?.GetLocalizedResource(); + public string Section + => SectionResource?.GetLocalizedResource(); /// /// The name of the string resource for the section name @@ -155,6 +156,7 @@ public Task SaveValueToFile(BaseStorageFile file) var propsToSave = new Dictionary(); propsToSave.Add(Property, Converter.ConvertBack(Value, null, null, null)); + return file.Properties.SavePropertiesAsync(propsToSave).AsTask(); } @@ -275,6 +277,7 @@ public async static Task> RetrieveAndInitializePropertiesAsyn propsToGet.Add(prop.Property); } } + #if DEBUG // This makes it much easier to debug issues with the property list var keyValuePairs = new Dictionary(); @@ -301,6 +304,7 @@ public async static Task> RetrieveAndInitializePropertiesAsyn keyValuePairs = await file.Properties.RetrievePropertiesAsync(propsToGet); } #endif + foreach (var prop in list) { if (!string.IsNullOrEmpty(prop.Property)) @@ -328,4 +332,4 @@ public async static Task> RetrieveAndInitializePropertiesAsyn { "UnitMM" , input => $"{(double)input} mm"}, }; } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/FilePropertySection.cs b/src/Files.App/ViewModels/Properties/FilePropertySection.cs index ecd222fe9a43..d43c445ea2c0 100644 --- a/src/Files.App/ViewModels/Properties/FilePropertySection.cs +++ b/src/Files.App/ViewModels/Properties/FilePropertySection.cs @@ -9,7 +9,8 @@ namespace Files.App.ViewModels.Properties /// public class FilePropertySection : List { - public FilePropertySection(IEnumerable items) : base(items) + public FilePropertySection(IEnumerable items) + : base(items) { } @@ -17,9 +18,11 @@ public FilePropertySection(IEnumerable items) : base(items) public string Key { get; set; } - public string Title => Key.GetLocalizedResource(); + public string Title + => Key.GetLocalizedResource(); - public int Priority => sectionPriority.ContainsKey(Key) ? sectionPriority[Key] : 0; + public int Priority + => sectionPriority.ContainsKey(Key) ? sectionPriority[Key] : 0; /// /// This list sets the priorities for the sections @@ -30,4 +33,4 @@ public FilePropertySection(IEnumerable items) : base(items) {"PropertySectionCore", 1} }; } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/FolderProperties.cs b/src/Files.App/ViewModels/Properties/FolderProperties.cs index 08083a5af653..66052c24cec7 100644 --- a/src/Files.App/ViewModels/Properties/FolderProperties.cs +++ b/src/Files.App/ViewModels/Properties/FolderProperties.cs @@ -18,8 +18,12 @@ internal class FolderProperties : BaseProperties { public ListedItem Item { get; } - public FolderProperties(SelectedItemsPropertiesViewModel viewModel, CancellationTokenSource tokenSource, - DispatcherQueue coreDispatcher, ListedItem item, IShellPage instance) + public FolderProperties( + SelectedItemsPropertiesViewModel viewModel, + CancellationTokenSource tokenSource, + DispatcherQueue coreDispatcher, + ListedItem item, + IShellPage instance) { ViewModel = viewModel; TokenSource = tokenSource; @@ -62,7 +66,8 @@ public override void GetBaseProperties() { await App.Window.DispatcherQueue.EnqueueAsync( () => NavigationHelpers.OpenPathInNewTab(Path.GetDirectoryName(Environment.ExpandEnvironmentVariables(ViewModel.ShortcutItemPath)))); - }, () => + }, + () => { return !string.IsNullOrWhiteSpace(ViewModel.ShortcutItemPath); }); @@ -131,6 +136,7 @@ public async override void GetSpecialProperties() { ViewModel.FilesAndFoldersCountVisibility = false; } + ViewModel.ItemCreatedTimestampVisibility = false; ViewModel.ItemAccessedTimestampVisibility = false; ViewModel.ItemModifiedTimestampVisibility = false; @@ -159,6 +165,7 @@ private async void GetFolderSize(string folderPath, CancellationToken token) var size = await CalculateFolderSizeAsync(folderPath, token); return size; }); + try { var folderSize = await fileSizeTask; @@ -169,6 +176,7 @@ private async void GetFolderSize(string folderPath, CancellationToken token) { App.Logger.Warn(ex, ex.Message); } + ViewModel.ItemSizeProgressVisibility = false; SetItemsCountString(); @@ -204,4 +212,4 @@ private async void ViewModel_PropertyChanged(object sender, System.ComponentMode } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/LibraryProperties.cs b/src/Files.App/ViewModels/Properties/LibraryProperties.cs index ecb6804fc033..262c3761d6d3 100644 --- a/src/Files.App/ViewModels/Properties/LibraryProperties.cs +++ b/src/Files.App/ViewModels/Properties/LibraryProperties.cs @@ -145,6 +145,7 @@ private void ViewModel_PropertyChanged(object sender, System.ComponentModel.Prop { NativeFileOperationsHelper.UnsetFileAttribute(Library.ItemPath, System.IO.FileAttributes.ReadOnly); } + break; case "IsHidden": @@ -156,8 +157,9 @@ private void ViewModel_PropertyChanged(object sender, System.ComponentModel.Prop { NativeFileOperationsHelper.UnsetFileAttribute(Library.ItemPath, System.IO.FileAttributes.Hidden); } + break; } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/PropertiesTab.cs b/src/Files.App/ViewModels/Properties/PropertiesTab.cs index 31e835ed7440..16e667ddae07 100644 --- a/src/Files.App/ViewModels/Properties/PropertiesTab.cs +++ b/src/Files.App/ViewModels/Properties/PropertiesTab.cs @@ -78,4 +78,4 @@ protected override void OnNavigatedFrom(NavigationEventArgs e) public abstract void Dispose(); } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Properties/SecurityProperties.cs b/src/Files.App/ViewModels/Properties/SecurityProperties.cs index 03aeb868f782..76980f2e083f 100644 --- a/src/Files.App/ViewModels/Properties/SecurityProperties.cs +++ b/src/Files.App/ViewModels/Properties/SecurityProperties.cs @@ -19,6 +19,7 @@ public class SecurityProperties : ObservableObject public SecurityProperties(ListedItem item) { Item = item; + IsFolder = Item.PrimaryItemAttribute == Windows.Storage.StorageItemTypes.Folder && !Item.IsShortcut; InitCommands(); @@ -45,10 +46,7 @@ private void InitCommands() RemoveRulesForUserCommand = new RelayCommand(RemoveRulesForUser, () => FilePermissions is not null && FilePermissions.CanReadFilePermissions && SelectedRuleForUser is not null); AddAccessRuleCommand = new RelayCommand(AddAccessRule, () => FilePermissions is not null && FilePermissions.CanReadFilePermissions); RemoveAccessRuleCommand = new RelayCommand(RemoveAccessRule, () => FilePermissions is not null && FilePermissions.CanReadFilePermissions && SelectedAccessRules is not null); - DisableInheritanceCommand = new RelayCommand(DisableInheritance, () => - { - return FilePermissions is not null && FilePermissions.CanReadFilePermissions && (FilePermissions.AreAccessRulesProtected != isProtected); - }); + DisableInheritanceCommand = new RelayCommand(DisableInheritance, () => FilePermissions is not null && FilePermissions.CanReadFilePermissions && (FilePermissions.AreAccessRulesProtected != isProtected)); SetDisableInheritanceOptionCommand = new RelayCommand(SetDisableInheritanceOption); ReplaceChildPermissionsCommand = new RelayCommand(ReplaceChildPermissions, () => FilePermissions is not null && FilePermissions.CanReadFilePermissions); } @@ -63,7 +61,6 @@ private void InitCommands() public RelayCommand ReplaceChildPermissionsCommand { get; set; } private FilePermissionsManager filePermissions; - public FilePermissionsManager FilePermissions { get => filePermissions; @@ -83,7 +80,6 @@ public FilePermissionsManager FilePermissions } private RulesForUser selectedRuleForUser; - public RulesForUser SelectedRuleForUser { get => selectedRuleForUser; @@ -97,7 +93,6 @@ public RulesForUser SelectedRuleForUser } private List selectedAccessRules; - public List SelectedAccessRules { get => selectedAccessRules; @@ -111,10 +106,10 @@ public List SelectedAccessRules } } - public FileSystemAccessRuleForUI SelectedAccessRule => SelectedAccessRules?.FirstOrDefault(); + public FileSystemAccessRuleForUI SelectedAccessRule + => SelectedAccessRules?.FirstOrDefault(); private bool isFolder; - public bool IsFolder { get => isFolder; @@ -122,6 +117,7 @@ public bool IsFolder } private bool isProtected; + private bool preserveInheritance; public string DisableInheritanceOption @@ -160,7 +156,8 @@ private void SetDisableInheritanceOption(string options) } private void ReplaceChildPermissions() - { } + { + } private async void AddAccessRule() { @@ -183,9 +180,7 @@ private void RemoveAccessRule() if (SelectedAccessRules is not null) { foreach (var rule in SelectedAccessRules) - { FilePermissions.AccessRules.Remove(rule); - } } } @@ -194,10 +189,9 @@ private async void EditOwner() var pickedObject = await OpenObjectPicker(); if (pickedObject is not null) { + // Refresh file permissions if (SetFileOwner(pickedObject)) - { - GetFilePermissions(); // Refresh file permissions - } + GetFilePermissions(); } } @@ -246,6 +240,7 @@ public bool SetFilePermissions() public bool SetFileOwner(string ownerSid) { bool isFolder = Item.PrimaryItemAttribute == Windows.Storage.StorageItemTypes.Folder && !Item.IsShortcut; + return FileOperationsHelpers.SetFileOwner(Item.ItemPath, isFolder, ownerSid); } @@ -255,7 +250,8 @@ public bool SetFileOwner(string ownerSid) public bool SetAccessRuleProtection(bool isProtected, bool preserveInheritance) { bool isFolder = Item.PrimaryItemAttribute == Windows.Storage.StorageItemTypes.Folder && !Item.IsShortcut; + return FileOperationsHelpers.SetAccessRuleProtection(Item.ItemPath, isFolder, isProtected, preserveInheritance); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/SearchBoxViewModel.cs b/src/Files.App/ViewModels/SearchBoxViewModel.cs index dc85fbfcfbe3..298c67a409d2 100644 --- a/src/Files.App/ViewModels/SearchBoxViewModel.cs +++ b/src/Files.App/ViewModels/SearchBoxViewModel.cs @@ -24,12 +24,15 @@ public string Query public bool WasQuerySubmitted { get; set; } = false; public event TypedEventHandler? TextChanged; + public event TypedEventHandler? QuerySubmitted; + public event EventHandler? Escaped; private readonly SuggestionComparer suggestionComparer = new SuggestionComparer(); public ObservableCollection Suggestions { get; } = new ObservableCollection(); + private readonly List oldQueries = new List(); public void ClearSuggestions() @@ -74,6 +77,7 @@ public void SearchRegion_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQue return; WasQuerySubmitted = true; + if (e.ChosenSuggestion is SuggestionModel chosen && chosen.ItemPath is null) { Query = chosen.Name; @@ -111,9 +115,10 @@ public void SearchRegion_GotFocus(object sender, RoutedEventArgs e) public void SearchRegion_KeyDown(object sender, KeyRoutedEventArgs e) { - e.Handled = e.Key is VirtualKey.Left || - e.Key is VirtualKey.Right || - ((e.Key is VirtualKey.Up || e.Key is VirtualKey.Down) && Suggestions.Count == 0); + e.Handled = + e.Key is VirtualKey.Left || + e.Key is VirtualKey.Right || + ((e.Key is VirtualKey.Up || e.Key is VirtualKey.Down) && Suggestions.Count == 0); } public void AddRecentQueries() @@ -124,11 +129,14 @@ public void AddRecentQueries() public class SuggestionComparer : IEqualityComparer, IComparer { - public int Compare(SuggestionModel x, SuggestionModel y) => y.ItemPath.CompareTo(x.ItemPath); + public int Compare(SuggestionModel x, SuggestionModel y) + => y.ItemPath.CompareTo(x.ItemPath); - public bool Equals(SuggestionModel x, SuggestionModel y) => y.ItemPath.Equals(x.ItemPath); + public bool Equals(SuggestionModel x, SuggestionModel y) + => y.ItemPath.Equals(x.ItemPath); - public int GetHashCode(SuggestionModel o) => o.ItemPath.GetHashCode(); + public int GetHashCode(SuggestionModel o) + => o.ItemPath.GetHashCode(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/SelectedItemsPropertiesViewModel.cs b/src/Files.App/ViewModels/SelectedItemsPropertiesViewModel.cs index 05776bab6cfb..071ef9121e91 100644 --- a/src/Files.App/ViewModels/SelectedItemsPropertiesViewModel.cs +++ b/src/Files.App/ViewModels/SelectedItemsPropertiesViewModel.cs @@ -542,7 +542,6 @@ public bool LoadLinkIcon } private RelayCommand shortcutItemOpenLinkCommand; - public RelayCommand ShortcutItemOpenLinkCommand { get => shortcutItemOpenLinkCommand; @@ -562,16 +561,14 @@ public Uri FolderIconSource } } - private ObservableCollection propertySections = new ObservableCollection(); - + private ObservableCollection propertySections = new(); public ObservableCollection PropertySections { get => propertySections; set => SetProperty(ref propertySections, value); } - private ObservableCollection fileProperties = new ObservableCollection(); - + private ObservableCollection fileProperties = new(); public ObservableCollection FileProperties { get => fileProperties; @@ -621,4 +618,4 @@ public bool RunAsAdminEnabled set => SetProperty(ref runAsAdminEnabled, value); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/SettingsViewModel.cs b/src/Files.App/ViewModels/SettingsViewModel.cs index 0291b749efa1..91eab310017d 100644 --- a/src/Files.App/ViewModels/SettingsViewModel.cs +++ b/src/Files.App/ViewModels/SettingsViewModel.cs @@ -27,9 +27,10 @@ public SettingsViewModel() public bool Set(TValue value, [CallerMemberName] string propertyName = null) { - propertyName = propertyName is not null && propertyName.StartsWith("set_", StringComparison.OrdinalIgnoreCase) - ? propertyName.Substring(4) - : propertyName; + propertyName = + propertyName is not null && propertyName.StartsWith("set_", StringComparison.OrdinalIgnoreCase) ? + propertyName.Substring(4) : + propertyName; TValue originalValue = default; @@ -53,12 +54,12 @@ public bool Set(TValue value, [CallerMemberName] string propertyName = n public TValue Get<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TValue>(TValue defaultValue, [CallerMemberName] string propertyName = null) { - var name = propertyName ?? - throw new ArgumentNullException(nameof(propertyName), "Cannot store property of unnamed."); + var name = propertyName ?? throw new ArgumentNullException(nameof(propertyName), "Cannot store property of unnamed."); - name = name.StartsWith("get_", StringComparison.OrdinalIgnoreCase) - ? propertyName.Substring(4) - : propertyName; + name = + name.StartsWith("get_", StringComparison.OrdinalIgnoreCase) ? + propertyName.Substring(4) : + propertyName; if (localSettings.Values.ContainsKey(name)) { @@ -89,7 +90,9 @@ public bool Set(TValue value, [CallerMemberName] string propertyName = n tValue = (tryParseDelegate?.Invoke(stringValue, out tValue) ?? false) ? tValue : default; } - Set(tValue, propertyName); // Put the corrected value in settings. + // Put the corrected value in settings + Set(tValue, propertyName); + return tValue; } return tValue; diff --git a/src/Files.App/ViewModels/SettingsViewModels/AdvancedViewModel.cs b/src/Files.App/ViewModels/SettingsViewModels/AdvancedViewModel.cs index 3d78ac3c6631..f78edd50d063 100644 --- a/src/Files.App/ViewModels/SettingsViewModels/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/SettingsViewModels/AdvancedViewModel.cs @@ -26,20 +26,16 @@ namespace Files.App.ViewModels.SettingsViewModels public class AdvancedViewModel : ObservableObject { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); + private readonly IBundlesSettingsService bundlesSettingsService = Ioc.Default.GetRequiredService(); - private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); + private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); public ICommand EditFileTagsCommand { get; } - public ICommand SetAsDefaultExplorerCommand { get; } - public ICommand SetAsOpenFileDialogCommand { get; } - public ICommand ExportSettingsCommand { get; } - public ICommand ImportSettingsCommand { get; } - public ICommand OpenSettingsJsonCommand { get; } @@ -49,10 +45,8 @@ public AdvancedViewModel() IsSetAsOpenFileDialog = DetectIsSetAsOpenFileDialog(); EditFileTagsCommand = new AsyncRelayCommand(LaunchFileTagsConfigFile); - SetAsDefaultExplorerCommand = new AsyncRelayCommand(SetAsDefaultExplorer); SetAsOpenFileDialogCommand = new AsyncRelayCommand(SetAsOpenFileDialog); - ExportSettingsCommand = new AsyncRelayCommand(ExportSettings); ImportSettingsCommand = new AsyncRelayCommand(ImportSettings); OpenSettingsJsonCommand = new AsyncRelayCommand(OpenSettingsJson); @@ -80,14 +74,15 @@ private async Task LaunchFileTagsConfigFile() private async Task SetAsDefaultExplorer() { - await Task.Yield(); // Make sure IsSetAsDefaultFileManager is updated + // Make sure IsSetAsDefaultFileManager is updated + await Task.Yield(); + if (IsSetAsDefaultFileManager == DetectIsSetAsDefaultFileManager()) - { return; - } var destFolder = Path.Combine(ApplicationData.Current.LocalFolder.Path, "FilesOpenDialog"); Directory.CreateDirectory(destFolder); + foreach (var file in Directory.GetFiles(Path.Combine(Package.Current.InstalledLocation.Path, "Files.App", "Assets", "FilesOpenDialog"))) { if (!SafetyExtensions.IgnoreExceptions(() => File.Copy(file, Path.Combine(destFolder, Path.GetFileName(file)), true), App.Logger)) @@ -132,11 +127,10 @@ private async Task SetAsDefaultExplorer() private async Task SetAsOpenFileDialog() { - await Task.Yield(); // Make sure IsSetAsDefaultFileManager is updated + // Make sure IsSetAsDefaultFileManager is updated + await Task.Yield(); if (IsSetAsOpenFileDialog == DetectIsSetAsOpenFileDialog()) - { return; - } var destFolder = Path.Combine(ApplicationData.Current.LocalFolder.Path, "FilesOpenDialog"); Directory.CreateDirectory(destFolder); @@ -178,19 +172,21 @@ private async Task ImportSettings() { var zipFolder = await ZipStorageFolder.FromStorageFileAsync(file); if (zipFolder is null) - { return; - } + var localFolderPath = ApplicationData.Current.LocalFolder.Path; var settingsFolder = await StorageFolder.GetFolderFromPathAsync(Path.Combine(localFolderPath, Constants.LocalSettings.SettingsFolderName)); + // Import user settings var userSettingsFile = await zipFolder.GetFileAsync(Constants.LocalSettings.UserSettingsFileName); string importSettings = await userSettingsFile.ReadTextAsync(); UserSettingsService.ImportSettings(importSettings); + // Import bundles var bundles = await zipFolder.GetFileAsync(Constants.LocalSettings.BundlesSettingsFileName); string importBundles = await bundles.ReadTextAsync(); bundlesSettingsService.ImportSettings(importBundles); + // Import file tags list and DB var fileTagsList = await zipFolder.GetFileAsync(Constants.LocalSettings.FileTagSettingsFileName); string importTags = await fileTagsList.ReadTextAsync(); @@ -199,6 +195,7 @@ private async Task ImportSettings() string importTagsDB = await fileTagsDB.ReadTextAsync(); var tagDbInstance = FileTagsHelper.GetDbInstance(); tagDbInstance.Import(importTagsDB); + // Import layout preferences and DB var layoutPrefsDB = await zipFolder.GetFileAsync(Path.GetFileName(FolderSettingsViewModel.LayoutSettingsDbPath)); string importPrefsDB = await layoutPrefsDB.ReadTextAsync(); @@ -226,24 +223,28 @@ private async Task ExportSettings() try { await ZipStorageFolder.InitArchive(file, OutArchiveFormat.Zip); + var zipFolder = (ZipStorageFolder)await ZipStorageFolder.FromStorageFileAsync(file); if (zipFolder is null) - { return; - } + var localFolderPath = ApplicationData.Current.LocalFolder.Path; + // Export user settings var exportSettings = UTF8Encoding.UTF8.GetBytes((string)UserSettingsService.ExportSettings()); await zipFolder.CreateFileAsync(new MemoryStream(exportSettings), Constants.LocalSettings.UserSettingsFileName, CreationCollisionOption.ReplaceExisting); + // Export bundles var exportBundles = UTF8Encoding.UTF8.GetBytes((string)bundlesSettingsService.ExportSettings()); await zipFolder.CreateFileAsync(new MemoryStream(exportBundles), Constants.LocalSettings.BundlesSettingsFileName, CreationCollisionOption.ReplaceExisting); + // Export file tags list and DB var exportTags = UTF8Encoding.UTF8.GetBytes((string)fileTagsSettingsService.ExportSettings()); await zipFolder.CreateFileAsync(new MemoryStream(exportTags), Constants.LocalSettings.FileTagSettingsFileName, CreationCollisionOption.ReplaceExisting); var tagDbInstance = FileTagsHelper.GetDbInstance(); byte[] exportTagsDB = UTF8Encoding.UTF8.GetBytes(tagDbInstance.Export()); await zipFolder.CreateFileAsync(new MemoryStream(exportTagsDB), Path.GetFileName(FileTagsHelper.FileTagsDbPath), CreationCollisionOption.ReplaceExisting); + // Export layout preferences DB var layoutDbInstance = FolderSettingsViewModel.GetDbInstance(); byte[] exportPrefsDB = UTF8Encoding.UTF8.GetBytes(layoutDbInstance.Export()); @@ -260,17 +261,18 @@ private bool DetectIsSetAsDefaultFileManager() { using var subkey = Registry.ClassesRoot.OpenSubKey(@"Folder\shell\open\command"); var command = (string?)subkey?.GetValue(string.Empty); + return !string.IsNullOrEmpty(command) && command.Contains("FilesLauncher.exe"); } private bool DetectIsSetAsOpenFileDialog() { using var subkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Classes\CLSID\{DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7}"); + return subkey?.GetValue(string.Empty) as string == "FilesOpenDialog class"; } private bool isSetAsDefaultFileManager; - public bool IsSetAsDefaultFileManager { get => isSetAsDefaultFileManager; @@ -278,7 +280,6 @@ public bool IsSetAsDefaultFileManager } private bool isSetAsOpenFileDialog; - public bool IsSetAsOpenFileDialog { get => isSetAsOpenFileDialog; @@ -288,12 +289,14 @@ public bool IsSetAsOpenFileDialog private FileSavePicker InitializeWithWindow(FileSavePicker obj) { WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle); + return obj; } private FileOpenPicker InitializeWithWindow(FileOpenPicker obj) { WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle); + return obj; } } diff --git a/src/Files.App/ViewModels/SettingsViewModels/AppearanceViewModel.cs b/src/Files.App/ViewModels/SettingsViewModels/AppearanceViewModel.cs index 7ea856408ed1..95f948dcb467 100644 --- a/src/Files.App/ViewModels/SettingsViewModels/AppearanceViewModel.cs +++ b/src/Files.App/ViewModels/SettingsViewModels/AppearanceViewModel.cs @@ -31,6 +31,7 @@ public AppearanceViewModel() }; AppThemeResources = AppThemeResourceFactory.AppThemeResources; + UpdateSelectedResource(); } @@ -53,6 +54,7 @@ private void UpdateSelectedResource() BackgroundColor = themeBackgroundColor, Name = "Custom" }; + AppThemeResources.Add(appThemeBackgroundColor); } @@ -157,4 +159,4 @@ public string AppThemeBackgroundColor } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/SettingsViewModels/FoldersViewModel.cs b/src/Files.App/ViewModels/SettingsViewModels/FoldersViewModel.cs index a7285ec80495..2036dbe412e2 100644 --- a/src/Files.App/ViewModels/SettingsViewModels/FoldersViewModel.cs +++ b/src/Files.App/ViewModels/SettingsViewModels/FoldersViewModel.cs @@ -10,12 +10,10 @@ public class FoldersViewModel : ObservableObject { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); - - //FileTag combobox indexes (required to hide SyncStatus) + // FileTag combobox indexes (required to hide SyncStatus) private readonly int FileTagSortingIndex = 5; private readonly int FileTagGroupingIndex = 6; - public FoldersViewModel() { SelectedDefaultLayoutModeIndex = (int)DefaultLayoutMode; @@ -62,6 +60,7 @@ public bool SyncFolderPreferencesAcrossDirectories if (value != UserSettingsService.FoldersSettingsService.SyncFolderPreferencesAcrossDirectories) { UserSettingsService.FoldersSettingsService.SyncFolderPreferencesAcrossDirectories = value; + ResetLayoutPreferences(); OnPropertyChanged(); } @@ -76,6 +75,7 @@ public bool ShowFileTagColumn if (value != UserSettingsService.FoldersSettingsService.ShowFileTagColumn) { UserSettingsService.FoldersSettingsService.ShowFileTagColumn = value; + OnPropertyChanged(); } } @@ -89,6 +89,7 @@ public bool ShowSizeColumn if (value != UserSettingsService.FoldersSettingsService.ShowSizeColumn) { UserSettingsService.FoldersSettingsService.ShowSizeColumn = value; + OnPropertyChanged(); } } @@ -102,6 +103,7 @@ public bool ShowTypeColumn if (value != UserSettingsService.FoldersSettingsService.ShowTypeColumn) { UserSettingsService.FoldersSettingsService.ShowTypeColumn = value; + OnPropertyChanged(); } } @@ -115,6 +117,7 @@ public bool ShowDateCreatedColumn if (value != UserSettingsService.FoldersSettingsService.ShowDateCreatedColumn) { UserSettingsService.FoldersSettingsService.ShowDateCreatedColumn = value; + OnPropertyChanged(); } } @@ -128,6 +131,7 @@ public bool ShowDateColumn if (value != UserSettingsService.FoldersSettingsService.ShowDateColumn) { UserSettingsService.FoldersSettingsService.ShowDateColumn = value; + OnPropertyChanged(); } } @@ -141,6 +145,7 @@ public bool ShowSelectionCheckboxes if (value != UserSettingsService.FoldersSettingsService.ShowSelectionCheckboxes) { UserSettingsService.FoldersSettingsService.ShowSelectionCheckboxes = value; + OnPropertyChanged(); } } @@ -154,6 +159,7 @@ public FolderLayoutModes DefaultLayoutMode if (value != UserSettingsService.FoldersSettingsService.DefaultLayoutMode) { UserSettingsService.FoldersSettingsService.DefaultLayoutMode = value; + OnPropertyChanged(); } } @@ -167,6 +173,7 @@ public bool ShowHiddenItems if (value != UserSettingsService.FoldersSettingsService.ShowHiddenItems) { UserSettingsService.FoldersSettingsService.ShowHiddenItems = value; + OnPropertyChanged(); } } @@ -180,6 +187,7 @@ public bool ShowProtectedSystemFiles if (value != UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles) { UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles = value; + OnPropertyChanged(); } } @@ -193,6 +201,7 @@ public bool AreAlternateStreamsVisible if (value != UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible) { UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible = value; + OnPropertyChanged(); } } @@ -206,6 +215,7 @@ public bool ShowDotFiles if (value != UserSettingsService.FoldersSettingsService.ShowDotFiles) { UserSettingsService.FoldersSettingsService.ShowDotFiles = value; + OnPropertyChanged(); } } @@ -219,6 +229,7 @@ public bool OpenItemsWithOneClick if (value != UserSettingsService.FoldersSettingsService.OpenItemsWithOneClick) { UserSettingsService.FoldersSettingsService.OpenItemsWithOneClick = value; + OnPropertyChanged(); } } @@ -232,6 +243,7 @@ public bool ColumnLayoutOpenFoldersWithOneClick if (value != UserSettingsService.FoldersSettingsService.ColumnLayoutOpenFoldersWithOneClick) { UserSettingsService.FoldersSettingsService.ColumnLayoutOpenFoldersWithOneClick = value; + OnPropertyChanged(); } } @@ -245,6 +257,7 @@ public bool OpenFoldersNewTab if (value != UserSettingsService.FoldersSettingsService.OpenFoldersInNewTab) { UserSettingsService.FoldersSettingsService.OpenFoldersInNewTab = value; + OnPropertyChanged(); } } @@ -287,6 +300,7 @@ public bool ListAndSortDirectoriesAlongsideFiles if (value != UserSettingsService.FoldersSettingsService.DefaultSortDirectoriesAlongsideFiles) { UserSettingsService.FoldersSettingsService.DefaultSortDirectoriesAlongsideFiles = value; + OnPropertyChanged(); } } @@ -300,6 +314,7 @@ public bool CalculateFolderSizes if (value != UserSettingsService.FoldersSettingsService.CalculateFolderSizes) { UserSettingsService.FoldersSettingsService.CalculateFolderSizes = value; + OnPropertyChanged(); } } @@ -314,6 +329,7 @@ public int SelectedDefaultSortingIndex if (SetProperty(ref selectedDefaultSortingIndex, value)) { OnPropertyChanged(nameof(SelectedDefaultSortingIndex)); + UserSettingsService.FoldersSettingsService.DefaultSortOption = value == FileTagSortingIndex ? SortOption.FileTag : (SortOption)value; } } @@ -328,6 +344,7 @@ public int SelectedDefaultGroupingIndex if (SetProperty(ref selectedDefaultGroupingIndex, value)) { OnPropertyChanged(nameof(SelectedDefaultGroupingIndex)); + UserSettingsService.FoldersSettingsService.DefaultGroupOption = value == FileTagGroupingIndex ? GroupOption.FileTag : (GroupOption)value; // Raise an event for the 'Group in descending order' toggle switch availability OnPropertyChanged(nameof(isDefaultGrouped)); @@ -343,6 +360,7 @@ public bool ShowFileExtensions if (value != UserSettingsService.FoldersSettingsService.ShowFileExtensions) { UserSettingsService.FoldersSettingsService.ShowFileExtensions = value; + OnPropertyChanged(); } } @@ -356,6 +374,7 @@ public bool ShowThumbnails if (value != UserSettingsService.FoldersSettingsService.ShowThumbnails) { UserSettingsService.FoldersSettingsService.ShowThumbnails = value; + OnPropertyChanged(); } } @@ -369,6 +388,7 @@ public DeleteConfirmationPolicies DeleteConfirmationPolicy if (value != UserSettingsService.FoldersSettingsService.DeleteConfirmationPolicy) { UserSettingsService.FoldersSettingsService.DeleteConfirmationPolicy = value; + OnPropertyChanged(); } } @@ -382,6 +402,7 @@ public bool SelectFilesOnHover if (value != UserSettingsService.FoldersSettingsService.SelectFilesOnHover) { UserSettingsService.FoldersSettingsService.SelectFilesOnHover = value; + OnPropertyChanged(); } } @@ -395,18 +416,18 @@ public bool DoubleClickToGoUp if (value != UserSettingsService.FoldersSettingsService.DoubleClickToGoUp) { UserSettingsService.FoldersSettingsService.DoubleClickToGoUp = value; + OnPropertyChanged(); } } } - // Local methods - public void ResetLayoutPreferences() { // Is this proper practice? var dbInstance = FolderSettingsViewModel.GetDbInstance(); + dbInstance.ResetAll(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/SettingsViewModels/PreferencesViewModel.cs b/src/Files.App/ViewModels/SettingsViewModels/PreferencesViewModel.cs index 1159f89d9e9f..3e3b43daa925 100644 --- a/src/Files.App/ViewModels/SettingsViewModels/PreferencesViewModel.cs +++ b/src/Files.App/ViewModels/SettingsViewModels/PreferencesViewModel.cs @@ -28,17 +28,14 @@ public class PreferencesViewModel : ObservableObject, IDisposable private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); private bool disposed; - private ReadOnlyCollection addFlyoutItemsSource; - // Commands + private ReadOnlyCollection addFlyoutItemsSource; public AsyncRelayCommand OpenFilesAtStartupCommand { get; } public AsyncRelayCommand ChangePageCommand { get; } public RelayCommand RemovePageCommand { get; } public RelayCommand AddPageCommand { get; } - // Properties - private bool showRestartControl; public bool ShowRestartControl { @@ -96,9 +93,8 @@ public int SelectedAppLanguageIndex } } - // Lists - public List DateFormats { get; set; } + public ObservableCollection AppLanguages { get; set; } public PreferencesViewModel() @@ -130,16 +126,18 @@ private void AddDateTimeOptions() { DateTimeOffset sampleDate1 = DateTime.Now.AddSeconds(-5); DateTimeOffset sampleDate2 = new DateTime(sampleDate1.Year - 5, 12, 31, 14, 30, 0); + var styles = new DateTimeFormats[] { DateTimeFormats.Application, DateTimeFormats.System, DateTimeFormats.Universal }; + DateFormats = styles.Select(style => new DateTimeFormatItem(style, sampleDate1, sampleDate2)).ToList(); } private void AddSupportedAppLanguages() { var appLanguages = ApplicationLanguages.ManifestLanguages - .Append(string.Empty) // add default language id + .Append(string.Empty) // Add default language id .Select(language => new AppLanguageItem(language)) - .OrderBy(language => language.LanguagID is not "") // default language on top + .OrderBy(language => language.LanguagID is not "") // Default language on top .ThenBy(language => language.LanguageName); AppLanguages = new ObservableCollection(appLanguages); @@ -160,8 +158,8 @@ private async Task InitStartupSettingsRecentFoldersFlyout() }); await PopulateRecentItems(recentsItem); - // ensure recent folders aren't stale since we don't update them with a watcher - // then update the items source again to actually include those items + // Ensure recent folders aren't stale since we don't update them with a watcher + // Then update the items source again to actually include those items await App.RecentItemsManager.UpdateRecentFoldersAsync(); await PopulateRecentItems(recentsItem); } @@ -177,7 +175,7 @@ private Task PopulateRecentItems(MenuFlyoutSubItemViewModel menu) .Select(m => m.Text) .ToHashSet(); - // add separator if we need one and one wasn't added already + // Add separator if we need one and one wasn't added already if (recentFolders.Any() && !currentFolderMenus.Any()) menu.Items.Add(new MenuFlyoutSeparatorViewModel()); @@ -199,11 +197,13 @@ private Task PopulateRecentItems(MenuFlyoutSubItemViewModel menu) App.Logger.Info(ex, "Could not fetch recent items"); } - // update items source - AddFlyoutItemsSource = new List() { + // Update items source + AddFlyoutItemsSource = new List() + { new MenuFlyoutItemViewModel("Browse".GetLocalizedResource()) { Command = AddPageCommand }, menu, - }.AsReadOnly(); + } + .AsReadOnly(); return Task.CompletedTask; } @@ -226,6 +226,7 @@ public bool OpenNewTabPageOnStartup if (value != UserSettingsService.PreferencesSettingsService.OpenNewTabOnStartup) { UserSettingsService.PreferencesSettingsService.OpenNewTabOnStartup = value; + OnPropertyChanged(); } } @@ -239,6 +240,7 @@ public bool ContinueLastSessionOnStartUp if (value != UserSettingsService.PreferencesSettingsService.ContinueLastSessionOnStartUp) { UserSettingsService.PreferencesSettingsService.ContinueLastSessionOnStartUp = value; + OnPropertyChanged(); } } @@ -252,6 +254,7 @@ public bool OpenASpecificPageOnStartup if (value != UserSettingsService.PreferencesSettingsService.OpenSpecificPageOnStartup) { UserSettingsService.PreferencesSettingsService.OpenSpecificPageOnStartup = value; + OnPropertyChanged(); } } @@ -273,7 +276,10 @@ public bool AlwaysOpenANewInstance if (value != UserSettingsService.PreferencesSettingsService.AlwaysOpenNewInstance) { UserSettingsService.PreferencesSettingsService.AlwaysOpenNewInstance = value; - ApplicationData.Current.LocalSettings.Values["AlwaysOpenANewInstance"] = value; // Needed in Program.cs + + // Needed in Program.cs + ApplicationData.Current.LocalSettings.Values["AlwaysOpenANewInstance"] = value; + OnPropertyChanged(); } } @@ -287,6 +293,7 @@ public bool IsDualPaneEnabled if (value != UserSettingsService.PreferencesSettingsService.IsDualPaneEnabled) { UserSettingsService.PreferencesSettingsService.IsDualPaneEnabled = value; + OnPropertyChanged(); } } @@ -300,6 +307,7 @@ public bool AlwaysOpenDualPaneInNewTab if (value != UserSettingsService.PreferencesSettingsService.AlwaysOpenDualPaneInNewTab) { UserSettingsService.PreferencesSettingsService.AlwaysOpenDualPaneInNewTab = value; + OnPropertyChanged(); } } @@ -322,6 +330,7 @@ private async Task ChangePage() private FolderPicker InitializeWithWindow(FolderPicker obj) { WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle); + return obj; } @@ -331,6 +340,7 @@ private void RemovePage() if (index >= 0) { PagesOnStartupList.RemoveAt(index); + if (index > 0) SelectedPageIndex = index - 1; else if (PagesOnStartupList.Count > 0) @@ -373,7 +383,6 @@ public DateTimeFormats DateTimeFormat } private bool openInLogin; - public bool OpenInLogin { get => openInLogin; @@ -381,7 +390,6 @@ public bool OpenInLogin } private bool canOpenInLogin; - public bool CanOpenInLogin { get => canOpenInLogin; @@ -455,6 +463,7 @@ public bool SearchUnindexedItems if (value != UserSettingsService.PreferencesSettingsService.SearchUnindexedItems) { UserSettingsService.PreferencesSettingsService.SearchUnindexedItems = value; + OnPropertyChanged(); } } @@ -527,6 +536,7 @@ public bool ShowFavoritesSection if (value != UserSettingsService.PreferencesSettingsService.ShowFavoritesSection) { UserSettingsService.PreferencesSettingsService.ShowFavoritesSection = value; + OnPropertyChanged(); } } @@ -540,6 +550,7 @@ public bool ShowLibrarySection if (value != UserSettingsService.PreferencesSettingsService.ShowLibrarySection) { UserSettingsService.PreferencesSettingsService.ShowLibrarySection = value; + OnPropertyChanged(); } } @@ -553,6 +564,7 @@ public bool ShowDrivesSection if (value != UserSettingsService.PreferencesSettingsService.ShowDrivesSection) { UserSettingsService.PreferencesSettingsService.ShowDrivesSection = value; + OnPropertyChanged(); } } @@ -566,6 +578,7 @@ public bool ShowCloudDrivesSection if (value != UserSettingsService.PreferencesSettingsService.ShowCloudDrivesSection) { UserSettingsService.PreferencesSettingsService.ShowCloudDrivesSection = value; + OnPropertyChanged(); } } @@ -579,6 +592,7 @@ public bool ShowNetworkDrivesSection if (value != UserSettingsService.PreferencesSettingsService.ShowNetworkDrivesSection) { UserSettingsService.PreferencesSettingsService.ShowNetworkDrivesSection = value; + OnPropertyChanged(); } } @@ -592,6 +606,7 @@ public bool ShowWslSection if (value != UserSettingsService.PreferencesSettingsService.ShowWslSection) { UserSettingsService.PreferencesSettingsService.ShowWslSection = value; + OnPropertyChanged(); } } @@ -605,6 +620,7 @@ public bool ShowFileTagsSection if (value != UserSettingsService.PreferencesSettingsService.ShowFileTagsSection) { UserSettingsService.PreferencesSettingsService.ShowFileTagsSection = value; + OnPropertyChanged(); } } @@ -615,6 +631,7 @@ public void Dispose() if (!disposed) { disposed = true; + GC.SuppressFinalize(this); } } @@ -633,6 +650,7 @@ public string Text { if (Path == "Home") return "Home".GetLocalizedResource(); + return (Path == CommonPaths.RecycleBinPath) ? ApplicationData.Current.LocalSettings.Values.Get("RecycleBin_Title", "Recycle Bin") : Path; @@ -641,12 +659,14 @@ public string Text public string Path { get; } - internal PageOnStartupViewModel(string path) => Path = path; + internal PageOnStartupViewModel(string path) + => Path = path; } public class AppLanguageItem { public string LanguagID { get; set; } + public string LanguageName { get; set; } public AppLanguageItem(string languagID) @@ -661,6 +681,7 @@ public AppLanguageItem(string languagID) { LanguagID = string.Empty; var systemDefaultLanguageOptionStr = "SettingsPreferencesSystemDefaultLanguageOption".GetLocalizedResource(); + LanguageName = string.IsNullOrEmpty(systemDefaultLanguageOptionStr) ? "System Default" : systemDefaultLanguageOptionStr; } } @@ -674,7 +695,9 @@ public override string ToString() public class DateTimeFormatItem { public string Label { get; } + public string Sample1 { get; } + public string Sample2 { get; } public DateTimeFormatItem(DateTimeFormats style, DateTimeOffset sampleDate1, DateTimeOffset sampleDate2) @@ -688,4 +711,3 @@ public DateTimeFormatItem(DateTimeFormats style, DateTimeOffset sampleDate1, Dat } } } - diff --git a/src/Files.App/ViewModels/SidebarViewModel.cs b/src/Files.App/ViewModels/SidebarViewModel.cs index b300adf024aa..55a82db87fed 100644 --- a/src/Files.App/ViewModels/SidebarViewModel.cs +++ b/src/Files.App/ViewModels/SidebarViewModel.cs @@ -31,14 +31,14 @@ public class SidebarViewModel : ObservableObject, IDisposable public ICommand EmptyRecycleBinCommand { get; private set; } private IPaneHolder paneHolder; - public IPaneHolder PaneHolder { get => paneHolder; set => SetProperty(ref paneHolder, value); } - public IFilesystemHelpers FilesystemHelpers => PaneHolder?.FilesystemHelpers; + public IFilesystemHelpers FilesystemHelpers + => PaneHolder?.FilesystemHelpers; private DispatcherQueue dispatcherQueue; @@ -47,7 +47,6 @@ public IPaneHolder PaneHolder public static readonly GridLength CompactSidebarWidth = SidebarControl.GetSidebarCompactSize(); private NavigationViewDisplayMode sidebarDisplayMode; - public NavigationViewDisplayMode SidebarDisplayMode { get => sidebarDisplayMode; @@ -72,7 +71,8 @@ public NavigationViewDisplayMode SidebarDisplayMode SectionType.FileTag }; - public bool IsSidebarCompactSize => SidebarDisplayMode == NavigationViewDisplayMode.Compact || SidebarDisplayMode == NavigationViewDisplayMode.Minimal; + public bool IsSidebarCompactSize + => SidebarDisplayMode == NavigationViewDisplayMode.Compact || SidebarDisplayMode == NavigationViewDisplayMode.Minimal; public void NotifyInstanceRelatedPropertiesChanged(string arg) { @@ -101,6 +101,7 @@ public void UpdateSidebarSelectedItemFromArgs(string arg) item ??= sidebarItems.FirstOrDefault(x => x.Path.Equals(value + "\\", StringComparison.OrdinalIgnoreCase)); item ??= sidebarItems.FirstOrDefault(x => value.StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)); item ??= sidebarItems.FirstOrDefault(x => x.Path.Equals(Path.GetPathRoot(value), StringComparison.OrdinalIgnoreCase)); + if (item is null && value == "Home") item = sidebarItems.FirstOrDefault(x => x.Path.Equals("Home")); @@ -281,8 +282,10 @@ private async Task SyncSidebarItems(LocationItem section, Func item is not null && !item.IsEmpty && item.IsDefaultLocation; + private bool IsLibraryOnSidebar(LibraryLocationItem item) + => item is not null && !item.IsEmpty && item.IsDefaultLocation; private async Task AddElementToSection(INavigationControlItem elem, LocationItem section, int index = -1) { @@ -385,8 +392,10 @@ private async Task CreateSection(SectionType sectionType) section.Path = "Home"; section.Font = App.AppModel.SymbolFontFamily; section.Icon = new BitmapImage(new Uri(Constants.FluentIconsPaths.HomeIcon)); + break; } + case SectionType.Favorites: { if (ShowFavoritesSection == false) @@ -397,8 +406,10 @@ 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)); + break; } + case SectionType.Library: { if (ShowLibrarySection == false) @@ -407,8 +418,10 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("SidebarLibraries".GetLocalizedResource(), sectionType, new ContextMenuOptions { IsLibrariesHeader = true, ShowHideSection = true }, false); iconIdex = Constants.ImageRes.Libraries; + break; } + case SectionType.Drives: { if (ShowDrivesSection == false) @@ -417,8 +430,10 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("Drives".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); iconIdex = Constants.ImageRes.ThisPC; + break; } + case SectionType.CloudDrives: { if (ShowCloudDrivesSection == false || App.CloudDrivesManager.Drives.Any() == false) @@ -427,8 +442,10 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("SidebarCloudDrives".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); icon = new BitmapImage(new Uri(Constants.FluentIconsPaths.CloudDriveIcon)); + break; } + case SectionType.Network: { if (!ShowNetworkDrivesSection) @@ -437,8 +454,10 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("SidebarNetworkDrives".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); iconIdex = Constants.ImageRes.NetworkDrives; + break; } + case SectionType.WSL: { if (ShowWslSection == false || App.WSLDistroManager.Distros.Any() == false) @@ -447,8 +466,10 @@ private async Task CreateSection(SectionType sectionType) } section = BuildSection("WSL".GetLocalizedResource(), sectionType, new ContextMenuOptions { ShowHideSection = true }, false); icon = new BitmapImage(new Uri(Constants.WslIconsPaths.GenericIcon)); + break; } + case SectionType.FileTag: { if (!ShowFileTagsSection) @@ -457,6 +478,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)); + break; } } @@ -514,7 +536,9 @@ public async void UpdateSectionVisibility(SectionType sectionType, bool show) SectionType.Favorites => App.QuickAccessManager.Model.AddAllItemsToSidebar, _ => () => Task.CompletedTask }; + Manager_DataChanged(sectionType, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + await action(); } else diff --git a/src/Files.App/ViewModels/StatusCenterViewModel.cs b/src/Files.App/ViewModels/StatusCenterViewModel.cs index f112e9c6b48d..3c10563ad3d9 100644 --- a/src/Files.App/ViewModels/StatusCenterViewModel.cs +++ b/src/Files.App/ViewModels/StatusCenterViewModel.cs @@ -72,10 +72,10 @@ public int InfoBadgeState return (anyFailure, AnyOperationsOngoing) switch { - (false, false) => 0, // all success - (false, true) => 1, // ongoing - (true, true) => 2, // onging with failure - (true, false) => 3 // completed with failure + (false, false) => 0, // All success + (false, true) => 1, // Ongoing + (true, true) => 2, // Ongoing with failure + (true, false) => 3 // Completed with failure }; } } @@ -99,9 +99,12 @@ public PostedStatusBanner PostBanner(string title, string message, float initial { StatusBanner banner = new StatusBanner(message, title, initialProgress, status, operation); PostedStatusBanner postedBanner = new PostedStatusBanner(banner, this); + StatusBannersSource.Insert(0, banner); ProgressBannerPosted?.Invoke(this, postedBanner); + UpdateBanner(banner); + return postedBanner; } @@ -111,10 +114,14 @@ public PostedStatusBanner PostOperationBanner(string title, string message, floa { CancellationTokenSource = cancellationTokenSource, }; + PostedStatusBanner postedBanner = new PostedStatusBanner(banner, this, cancellationTokenSource); + StatusBannersSource.Insert(0, banner); ProgressBannerPosted?.Invoke(this, postedBanner); + UpdateBanner(banner); + return postedBanner; } @@ -122,9 +129,12 @@ public PostedStatusBanner PostActionBanner(string title, string message, string { StatusBanner banner = new StatusBanner(message, title, primaryButtonText, cancelButtonText, primaryAction); PostedStatusBanner postedBanner = new PostedStatusBanner(banner, this); + StatusBannersSource.Insert(0, banner); ProgressBannerPosted?.Invoke(this, postedBanner); + UpdateBanner(banner); + return postedBanner; } @@ -136,7 +146,9 @@ public bool CloseBanner(StatusBanner banner) } StatusBannersSource.Remove(banner); + UpdateBanner(banner); + return true; } @@ -174,6 +186,7 @@ public class PostedStatusBanner #region Public Members public readonly FileSystemProgress Progress; + public readonly Progress ProgressEventSource; public CancellationToken CancellationToken => cancellationTokenSource?.Token ?? default; @@ -186,6 +199,7 @@ public PostedStatusBanner(StatusBanner banner, IOngoingTasksActions OngoingTasks { Banner = banner; this.OngoingTasksActions = OngoingTasksActions; + ProgressEventSource = new Progress(ReportProgressToBanner); Progress = new(ProgressEventSource, status: FileSystemStatusCode.InProgress); } @@ -195,6 +209,7 @@ public PostedStatusBanner(StatusBanner banner, IOngoingTasksActions OngoingTasks Banner = banner; this.OngoingTasksActions = OngoingTasksActions; this.cancellationTokenSource = cancellationTokenSource; + ProgressEventSource = new Progress(ReportProgressToBanner); Progress = new(ProgressEventSource, status: FileSystemStatusCode.InProgress); } @@ -205,10 +220,9 @@ public PostedStatusBanner(StatusBanner banner, IOngoingTasksActions OngoingTasks private void ReportProgressToBanner(FileSystemProgress value) { - if (CancellationToken.IsCancellationRequested) // file operation has been cancelled, so don't update the progress text - { + // File operation has been cancelled, so don't update the progress text + if (CancellationToken.IsCancellationRequested) return; - } if (value.Status is FileSystemStatusCode status) Banner.Status = status.ToStatus(); @@ -219,7 +233,8 @@ private void ReportProgressToBanner(FileSystemProgress value) { Banner.Progress = f; Banner.FullTitle = $"{Banner.Title} ({Banner.Progress:0.00}%)"; - // TODO: show detailed progress if Size/Count information available + + // TODO: Show detailed progress if Size/Count information available } else if (value.EnumerationCompleted) { @@ -229,14 +244,17 @@ private void ReportProgressToBanner(FileSystemProgress value) Banner.Progress = value.ProcessedSize * 100f / value.TotalSize; Banner.FullTitle = $"{Banner.Title} ({value.ProcessedItemsCount} ({value.ProcessedSize.ToSizeString()}) / {value.ItemsCount} ({value.TotalSize.ToSizeString()}): {Banner.Progress:0.00}%)"; break; + case (not 0, _): Banner.Progress = value.ProcessedSize * 100f / value.TotalSize; Banner.FullTitle = $"{Banner.Title} ({value.ProcessedSize.ToSizeString()} / {value.TotalSize.ToSizeString()}: {Banner.Progress:0.00}%)"; break; + case (_, not 0): Banner.Progress = value.ProcessedItemsCount * 100f / value.ItemsCount; Banner.FullTitle = $"{Banner.Title} ({value.ProcessedItemsCount} / {value.ItemsCount}: {Banner.Progress:0.00}%)"; break; + default: Banner.FullTitle = $"{Banner.Title} (...)"; break; @@ -244,21 +262,13 @@ private void ReportProgressToBanner(FileSystemProgress value) } else { - switch (value.ProcessedSize, value.ProcessedItemsCount) + Banner.FullTitle = (value.ProcessedSize, value.ProcessedItemsCount) switch { - case (not 0, not 0): - Banner.FullTitle = $"{Banner.Title} ({value.ProcessedItemsCount} ({value.ProcessedSize.ToSizeString()}) / ...)"; - break; - case (not 0, _): - Banner.FullTitle = $"{Banner.Title} ({value.ProcessedSize.ToSizeString()} / ...)"; - break; - case (_, not 0): - Banner.FullTitle = $"{Banner.Title} ({value.ProcessedItemsCount} / ...)"; - break; - default: - Banner.FullTitle = $"{Banner.Title} (...)"; - break; - } + (not 0, not 0) => $"{Banner.Title} ({value.ProcessedItemsCount} ({value.ProcessedSize.ToSizeString()}) / ...)", + (not 0, _) => $"{Banner.Title} ({value.ProcessedSize.ToSizeString()} / ...)", + (_, not 0) => $"{Banner.Title} ({value.ProcessedItemsCount} / ...)", + _ => $"{Banner.Title} (...)", + }; } OngoingTasksActions.UpdateBanner(Banner); @@ -301,10 +311,7 @@ public class StatusBanner : ObservableObject public float Progress { get => progress; - set - { - SetProperty(ref progress, value); - } + set => SetProperty(ref progress, value); } private bool isProgressing = false; @@ -312,23 +319,16 @@ public float Progress public bool IsProgressing { get => isProgressing; - set - { - SetProperty(ref isProgressing, value); - } + set => SetProperty(ref isProgressing, value); } public string Title { get; private set; } private ReturnResult status = ReturnResult.InProgress; - public ReturnResult Status { get => status; - set - { - SetProperty(ref status, value); - } + set => SetProperty(ref status, value); } public FileOperationType Operation { get; private set; } @@ -347,7 +347,8 @@ public ReturnResult Status public bool SolutionButtonsVisible { get; } = false; - public bool CancelButtonVisible => CancellationTokenSource is not null; + public bool CancelButtonVisible + => CancellationTokenSource is not null; public CancellationTokenSource CancellationTokenSource { get; set; } @@ -409,7 +410,9 @@ public StatusBanner(string message, string title, float progress, ReturnResult s break; } } + FullTitle = $"{Title} ({initialProgress}%)"; + break; case ReturnResult.Success: @@ -438,6 +441,7 @@ public StatusBanner(string message, string title, float progress, ReturnResult s FullTitle = Title; InfoBarSeverity = InfoBarSeverity.Error; } + break; } } @@ -487,4 +491,4 @@ public void CancelOperation() } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/ToolbarViewModel.cs b/src/Files.App/ViewModels/ToolbarViewModel.cs index da8638464134..a1292c2227b0 100644 --- a/src/Files.App/ViewModels/ToolbarViewModel.cs +++ b/src/Files.App/ViewModels/ToolbarViewModel.cs @@ -42,6 +42,7 @@ namespace Files.App.ViewModels public class ToolbarViewModel : ObservableObject, IAddressToolbar, IDisposable { private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); + public IUpdateService UpdateService { get; } = Ioc.Default.GetService()!; public delegate void ToolbarPathItemInvokedEventHandler(object sender, PathNavigationEventArgs e); @@ -85,13 +86,21 @@ public class ToolbarViewModel : ObservableObject, IAddressToolbar, IDisposable public bool IsSortedAscending { get => InstanceViewModel?.FolderSettings.DirectorySortDirection == SortDirection.Ascending; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortDirection = SortDirection.Ascending; } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortDirection = SortDirection.Ascending; + } } public bool IsSortedDescending { get => InstanceViewModel?.FolderSettings.DirectorySortDirection == SortDirection.Descending; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortDirection = SortDirection.Descending; } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortDirection = SortDirection.Descending; + } } public bool IsGroupedAscending @@ -109,7 +118,7 @@ public bool IsGroupedDescending public bool AreDirectoriesSortedAlongsideFiles { get => InstanceViewModel.FolderSettings.SortDirectoriesAlongsideFiles; - set { InstanceViewModel.FolderSettings.SortDirectoriesAlongsideFiles = value; } + set => InstanceViewModel.FolderSettings.SortDirectoriesAlongsideFiles = value; } // Sort by @@ -117,55 +126,87 @@ public bool AreDirectoriesSortedAlongsideFiles public bool IsSortedByName { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.Name; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.Name; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.Name; OnPropertyChanged(); + } } public bool IsSortedByDateModified { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.DateModified; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.DateModified; OnPropertyChanged(); } + set + { + if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.DateModified; OnPropertyChanged(); + } } public bool IsSortedByDateCreated { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.DateCreated; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.DateCreated; OnPropertyChanged(); } + set + { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.DateCreated; OnPropertyChanged(); + } } public bool IsSortedBySize { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.Size; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.Size; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.Size; OnPropertyChanged(); + } } public bool IsSortedByType { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.FileType; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.FileType; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.FileType; OnPropertyChanged(); + } } public bool IsSortedBySyncStatus { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.SyncStatus; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.SyncStatus; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.SyncStatus; OnPropertyChanged(); + } } public bool IsSortedByOriginalFolder { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.OriginalFolder; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.OriginalFolder; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.OriginalFolder; OnPropertyChanged(); + } } public bool IsSortedByDateDeleted { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.DateDeleted; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.DateDeleted; OnPropertyChanged(); } + set + { + if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.DateDeleted; OnPropertyChanged(); + } } public bool IsSortedByFileTag { get => InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.FileTag; - set { if (value) InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.FileTag; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.FileTag; OnPropertyChanged(); + } } // Group by @@ -173,67 +214,111 @@ public bool IsSortedByFileTag public bool IsGroupedByNone { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.None; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.None; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.None; OnPropertyChanged(); + } } public bool IsGroupedByName { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.Name; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.Name; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.Name; OnPropertyChanged(); + } } public bool IsGroupedByDateModified { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.DateModified; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.DateModified; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.DateModified; OnPropertyChanged(); + } } public bool IsGroupedByDateCreated { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.DateCreated; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.DateCreated; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.DateCreated; OnPropertyChanged(); + } } public bool IsGroupedBySize { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.Size; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.Size; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.Size; OnPropertyChanged(); + } } public bool IsGroupedByType { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.FileType; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.FileType; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.FileType; OnPropertyChanged(); + } } public bool IsGroupedBySyncStatus { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.SyncStatus; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.SyncStatus; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.SyncStatus; OnPropertyChanged(); + } } public bool IsGroupedByOriginalFolder { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.OriginalFolder; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.OriginalFolder; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.OriginalFolder; OnPropertyChanged(); + } } public bool IsGroupedByDateDeleted { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.DateDeleted; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.DateDeleted; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.DateDeleted; OnPropertyChanged(); + } } public bool IsGroupedByFileTag { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.FileTag; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.FileTag; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.FileTag; OnPropertyChanged(); + } } public bool IsGroupedByFolderPath { get => InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.FolderPath; - set { if (value) InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.FolderPath; OnPropertyChanged(); } + set + { + if (value) + InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.FolderPath; OnPropertyChanged(); + } } public bool IsLayoutDetailsView @@ -367,6 +452,7 @@ public string? PathText set { pathText = value; + OnPropertyChanged(nameof(PathText)); } } @@ -430,7 +516,7 @@ private async void UpdateService_OnPropertyChanged(object? sender, PropertyChang IsUpdateAvailable = UpdateService.IsUpdateAvailable; IsUpdating = UpdateService.IsUpdating; - // Bad code, result is called twice when checking for release notes + // TODO: Bad code, result is called twice when checking for release notes if (UpdateService.IsReleaseNotesAvailable) await CheckForReleaseNotesAsync(); } @@ -459,7 +545,8 @@ private void UserSettingsService_OnSettingChangedEvent(object? sender, SettingCh { switch (e.SettingName) { - case nameof(UserSettingsService.PreferencesSettingsService.ShowQuickAccessWidget): // ToDo: Move this to the widget page, it doesn't belong here. + // TODO: Move this to the widget page, it doesn't belong here. + case nameof(UserSettingsService.PreferencesSettingsService.ShowQuickAccessWidget): case nameof(UserSettingsService.PreferencesSettingsService.ShowDrivesWidget): case nameof(UserSettingsService.PreferencesSettingsService.ShowBundlesWidget): case nameof(UserSettingsService.PreferencesSettingsService.ShowFileTagsWidget): @@ -477,14 +564,14 @@ private void UserSettingsService_OnSettingChangedEvent(object? sender, SettingCh private DispatcherQueueTimer dragOverTimer; private ISearchBox searchBox = new SearchBoxViewModel(); - public ISearchBox SearchBox { get => searchBox; set => SetProperty(ref searchBox, value); } - public SearchBoxViewModel SearchBoxViewModel => (SearchBoxViewModel)SearchBox; + public SearchBoxViewModel SearchBoxViewModel + => (SearchBoxViewModel)SearchBox; public bool IsSingleItemOverride { get; set; } = false; @@ -579,7 +666,8 @@ public async void PathBoxItem_Drop(object sender, DragEventArgs e) lockFlag = true; - dragOverPath = null; // Reset dragged over pathbox item + // Reset dragged over pathbox item + dragOverPath = null; if (((StackPanel)sender).DataContext is not PathBoxItem pathBoxItem || pathBoxItem.Path == "Home") @@ -590,6 +678,7 @@ public async void PathBoxItem_Drop(object sender, DragEventArgs e) var deferral = e.GetDeferral(); var signal = new AsyncManualResetEvent(); + PathBoxItemDropped?.Invoke(this, new PathBoxItemDroppedEventArgs() { AcceptedOperation = e.AcceptedOperation, @@ -597,10 +686,12 @@ public async void PathBoxItem_Drop(object sender, DragEventArgs e) Path = pathBoxItem.Path, SignalEvent = signal }); + await signal.WaitAsync(); deferral.Complete(); await Task.Yield(); + lockFlag = false; } @@ -617,6 +708,7 @@ public async void PathBoxItem_DragOver(object sender, DragEventArgs e) { dragOverPath = pathBoxItem.Path; dragOverTimer.Stop(); + if (dragOverPath != (this as IAddressToolbar).PathComponents.LastOrDefault()?.Path) { dragOverTimer.Debounce(() => @@ -634,8 +726,8 @@ public async void PathBoxItem_DragOver(object sender, DragEventArgs e) } } - if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView) - || string.IsNullOrEmpty(pathBoxItem.Path)) // In search page + // In search page + if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView) || string.IsNullOrEmpty(pathBoxItem.Path)) { e.AcceptedOperation = DataPackageOperation.None; return; @@ -654,7 +746,8 @@ public async void PathBoxItem_DragOver(object sender, DragEventArgs e) { e.AcceptedOperation = DataPackageOperation.None; } - // copy be default when dragging from zip + + // Copy be default when dragging from zip else if (storageItems.Any(x => x.Item is ZipStorageFile || x.Item is ZipStorageFolder) || ZipStorageFolder.IsZipPath(pathBoxItem.Path)) { @@ -842,7 +935,8 @@ public void SearchRegion_LostFocus(object sender, RoutedEventArgs e) CloseSearchBox(); } - private void SearchRegion_Escaped(object? sender, ISearchBox searchBox) => CloseSearchBox(); + private void SearchRegion_Escaped(object? sender, ISearchBox searchBox) + => CloseSearchBox(); public ICommand? SelectAllContentPageItemsCommand { get; set; } @@ -926,7 +1020,9 @@ public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem p //Foreground = (SolidColorBrush)Application.Current.Resources["SystemControlErrorTextForegroundBrush"], FontSize = 12 }; + flyout.Items?.Add(flyoutItem); + return; } @@ -1031,10 +1127,11 @@ public async Task CheckPathInput(string currentInput, string currentSelectedPath } else // Not a file or not accessible { - var workingDir = string.IsNullOrEmpty(shellPage.FilesystemViewModel.WorkingDirectory) - || shellPage.CurrentPageType == typeof(WidgetsPage) - ? CommonPaths.HomePath - : shellPage.FilesystemViewModel.WorkingDirectory; + var workingDir = + string.IsNullOrEmpty(shellPage.FilesystemViewModel.WorkingDirectory) || + shellPage.CurrentPageType == typeof(WidgetsPage) ? + CommonPaths.HomePath : + shellPage.FilesystemViewModel.WorkingDirectory; if (await LaunchApplicationFromPath(currentInput, workingDir)) return; @@ -1069,6 +1166,7 @@ private static async Task LaunchApplicationFromPath(string currentInput, s fileName = trimmedInput.Substring(0, positionOfBlank); arguments = currentInput.Substring(currentInput.IndexOf(' ')); } + return await LaunchHelper.LaunchAppAsync(fileName, arguments, workingDir); } @@ -1082,7 +1180,10 @@ public async void SetAddressBarSuggestions(AutoSuggestBox sender, IShellPage she var expandedPath = StorageFileExtensions.GetPathWithoutEnvironmentVariable(sender.Text); var folderPath = PathNormalization.GetParentDir(expandedPath) ?? expandedPath; StorageFolderWithPath folder = await shellpage.FilesystemViewModel.GetFolderWithPathFromPathAsync(folderPath); - if (folder is null) return false; + + if (folder is null) + return false; + var currPath = await folder.GetFoldersWithPathAsync(Path.GetFileName(expandedPath), (uint)maxSuggestions); if (currPath.Count >= maxSuggestions) { @@ -1142,6 +1243,7 @@ public async void SetAddressBarSuggestions(AutoSuggestBox sender, IShellPage she foreach (var s in suggestions.ExceptBy(NavigationBarSuggestions, x => x.ItemNameRaw).ToList()) NavigationBarSuggestions.Insert(suggestions.IndexOf(s), s); } + return true; })) { @@ -1219,7 +1321,8 @@ public List SelectedItems public bool IsInfFile => SelectedItems is not null && SelectedItems.Count == 1 && FileExtensionHelpers.IsInfFile(SelectedItems.First().FileExtension) && !InstanceViewModel.IsPageTypeRecycleBin; public bool IsFont => SelectedItems is not null && SelectedItems.Any() && SelectedItems.All(x => FileExtensionHelpers.IsFontFile(x.FileExtension)) && !InstanceViewModel.IsPageTypeRecycleBin; - public string ExtractToText => IsSelectionArchivesOnly ? SelectedItems.Count > 1 ? string.Format("ExtractToChildFolder".GetLocalizedResource(), $"*{Path.DirectorySeparatorChar}") : string.Format("ExtractToChildFolder".GetLocalizedResource() + "\\", Path.GetFileNameWithoutExtension(selectedItems.First().Name)) : "ExtractToChildFolder".GetLocalizedResource(); + public string ExtractToText + => IsSelectionArchivesOnly ? SelectedItems.Count > 1 ? string.Format("ExtractToChildFolder".GetLocalizedResource(), $"*{Path.DirectorySeparatorChar}") : string.Format("ExtractToChildFolder".GetLocalizedResource() + "\\", Path.GetFileNameWithoutExtension(selectedItems.First().Name)) : "ExtractToChildFolder".GetLocalizedResource(); public void Dispose() { diff --git a/src/Files.App/ViewModels/Widgets/Bundles/BundleContainerViewModel.cs b/src/Files.App/ViewModels/Widgets/Bundles/BundleContainerViewModel.cs index 959a62da8492..6c0226d76dde 100644 --- a/src/Files.App/ViewModels/Widgets/Bundles/BundleContainerViewModel.cs +++ b/src/Files.App/ViewModels/Widgets/Bundles/BundleContainerViewModel.cs @@ -64,7 +64,6 @@ public class BundleContainerViewModel : ObservableObject, IDisposable public ObservableCollection Contents { get; private set; } = new ObservableCollection(); private string bundleName = "DefaultBundle"; - public string BundleName { get => bundleName; @@ -72,7 +71,6 @@ public string BundleName } private bool noBundleContentsTextLoad; - public bool NoBundleContentsTextLoad { get => noBundleContentsTextLoad; @@ -80,7 +78,6 @@ public bool NoBundleContentsTextLoad } private bool isAddItemOptionEnabled = true; - public bool IsAddItemOptionEnabled { get => isAddItemOptionEnabled; @@ -116,7 +113,6 @@ public BundleContainerViewModel() internalCollectionCount = Contents.Count; Contents.CollectionChanged += Contents_CollectionChanged; - // Create commands RemoveBundleCommand = new RelayCommand(RemoveBundle); RenameBundleCommand = new AsyncRelayCommand(RenameBundle); DragOverCommand = new RelayCommand(DragOver); @@ -255,6 +251,7 @@ private async Task RenameBundle() }, DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel }); + await dialog.ShowAsync(); bool CanAddBundleSetErrorMessage() @@ -274,12 +271,15 @@ private void RenameBundleConfirm(string bundleRenameText) { if (BundlesSettingsService.SavedBundles.ContainsKey(BundleName)) { - Dictionary> allBundles = BundlesSettingsService.SavedBundles; // We need to do it this way for Set() to be called + // We need to do it this way for Set() to be called + Dictionary> allBundles = BundlesSettingsService.SavedBundles; + Dictionary> newBundles = new Dictionary>(); foreach (var item in allBundles) { - if (item.Key == BundleName) // Item matches to-rename name + // Item matches to-rename name + if (item.Key == BundleName) { newBundles.Add(bundleRenameText, item.Value); @@ -289,7 +289,8 @@ private void RenameBundleConfirm(string bundleRenameText) bundleItem.ParentBundleName = bundleRenameText; } } - else // Ignore, and add existing values + // Ignore, and add existing values + else { newBundles.Add(item.Key, item.Value); } @@ -305,7 +306,8 @@ private void DragOver(DragEventArgs e) { if (Filesystem.FilesystemHelpers.HasDraggedStorageItems(e.DataView) || e.DataView.Contains(StandardDataFormats.Text)) { - if (Contents.Count < Constants.Widgets.Bundles.MaxAmountOfItemsPerBundle) // Don't exceed the limit! + // Don't exceed the limit! + if (Contents.Count < Constants.Widgets.Bundles.MaxAmountOfItemsPerBundle) { e.AcceptedOperation = DataPackageOperation.Move; } @@ -570,4 +572,4 @@ public void Dispose() #endregion IDisposable } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Widgets/Bundles/BundleItemViewModel.cs b/src/Files.App/ViewModels/Widgets/Bundles/BundleItemViewModel.cs index dbf0c943a7bc..a0c541d3f484 100644 --- a/src/Files.App/ViewModels/Widgets/Bundles/BundleItemViewModel.cs +++ b/src/Files.App/ViewModels/Widgets/Bundles/BundleItemViewModel.cs @@ -125,7 +125,8 @@ private void OpenItemLocation() public async Task UpdateIcon() { - if (TargetType == FilesystemItemType.Directory) // OpenDirectory + // OpenDirectory + if (TargetType == FilesystemItemType.Directory) { Icon = FolderIcon; } @@ -170,4 +171,4 @@ public void Dispose() #endregion IDisposable } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Widgets/Bundles/BundlesViewModel.cs b/src/Files.App/ViewModels/Widgets/Bundles/BundlesViewModel.cs index 875c2b22456b..0eeebdcc92d0 100644 --- a/src/Files.App/ViewModels/Widgets/Bundles/BundlesViewModel.cs +++ b/src/Files.App/ViewModels/Widgets/Bundles/BundlesViewModel.cs @@ -216,6 +216,7 @@ private void AddBundle(string name) OpenPath = OpenPathHandle, OpenPathInNewPane = OpenPathInNewPaneHandle, }); + NoBundlesAddItemLoad = false; itemAddedInternally = false; @@ -236,8 +237,9 @@ private async Task ImportBundles() string data = NativeFileOperationsHelper.ReadStringFromFile(file.Path); BundlesSettingsService.ImportSettings(data); } - catch // Couldn't deserialize, data is corrupted + catch { + // Couldn't deserialize, data is corrupted } } } @@ -258,6 +260,7 @@ private async Task ExportBundles() NativeFileOperationsHelper.WriteStringToFile(file.Path, (string)BundlesSettingsService.ExportSettings()); } } + private FileSavePicker InitializeWithWindow(FileSavePicker obj) { WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle); @@ -367,13 +370,15 @@ public void Save() bundles.Add(bundle.BundleName, bundleItems); } - BundlesSettingsService.SavedBundles = bundles; // Calls Set() + // Calls Set() + BundlesSettingsService.SavedBundles = bundles; } } public async Task Load() { - if (BundlesSettingsService.SavedBundles is null) // Null, therefore no items :) + // Null, therefore no items :) + if (BundlesSettingsService.SavedBundles is null) { NoBundlesAddItemLoad = true; return; @@ -410,7 +415,8 @@ public async Task Load() NotifyBundleItemRemoved = NotifyBundleItemRemovedHandle, OpenPath = OpenPathHandle, OpenPathInNewPane = OpenPathInNewPaneHandle, - }.SetBundleItems(bundleItems)); + } + .SetBundleItems(bundleItems)); itemAddedInternally = false; } @@ -458,4 +464,4 @@ public void Dispose() #endregion IDisposable } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Widgets/ICustomWidgetItemModel.cs b/src/Files.App/ViewModels/Widgets/ICustomWidgetItemModel.cs index ed2cbad67c79..0f95cd8b2a2d 100644 --- a/src/Files.App/ViewModels/Widgets/ICustomWidgetItemModel.cs +++ b/src/Files.App/ViewModels/Widgets/ICustomWidgetItemModel.cs @@ -6,4 +6,4 @@ namespace Files.App.ViewModels.Widgets public interface ICustomWidgetItemModel : IWidgetItemModel { } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Widgets/IWidgetItemModel.cs b/src/Files.App/ViewModels/Widgets/IWidgetItemModel.cs index 748d1060cfd2..3b8d47e046c2 100644 --- a/src/Files.App/ViewModels/Widgets/IWidgetItemModel.cs +++ b/src/Files.App/ViewModels/Widgets/IWidgetItemModel.cs @@ -20,4 +20,4 @@ public interface IWidgetItemModel : IDisposable Task RefreshWidget(); } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Widgets/WidgetsListControlItemViewModel.cs b/src/Files.App/ViewModels/Widgets/WidgetsListControlItemViewModel.cs index 3721ff546af1..ea29a6d4b4e7 100644 --- a/src/Files.App/ViewModels/Widgets/WidgetsListControlItemViewModel.cs +++ b/src/Files.App/ViewModels/Widgets/WidgetsListControlItemViewModel.cs @@ -59,4 +59,4 @@ public void Dispose() (WidgetControl as IDisposable)?.Dispose(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Widgets/WidgetsListControlViewModel.cs b/src/Files.App/ViewModels/Widgets/WidgetsListControlViewModel.cs index c89d3bcfe7dc..ccd1b144f438 100644 --- a/src/Files.App/ViewModels/Widgets/WidgetsListControlViewModel.cs +++ b/src/Files.App/ViewModels/Widgets/WidgetsListControlViewModel.cs @@ -104,4 +104,4 @@ public void Dispose() Widgets.Clear(); } } -} \ No newline at end of file +}