From e7fd5072aae833ddb1b6565d5303b97f1325ec10 Mon Sep 17 00:00:00 2001 From: Steven He Date: Thu, 30 Jul 2026 23:48:54 +0900 Subject: [PATCH 01/14] Address nullable warnings --- .../Omnibar/IOmnibarTextMemberPathProvider.cs | 2 +- src/Files.App.Controls/Omnibar/Omnibar.cs | 2 +- .../Compress/BaseCompressArchiveAction.cs | 2 +- .../Decompress/BaseDecompressArchiveAction.cs | 17 +- .../Archives/Decompress/DecompressArchive.cs | 21 +- .../DecompressArchiveToChildFolderAction.cs | 24 +- .../Content/Install/InstallFontAction.cs | 6 +- .../Content/Selection/SelectAllAction.cs | 5 +- .../Actions/Content/Tags/RemoveTagsAction.cs | 4 +- .../Actions/FileSystem/BaseDeleteAction.cs | 4 +- .../Actions/FileSystem/CopyItemPathAction.cs | 6 +- .../CopyItemPathWithQuotesAction.cs | 7 +- .../Actions/FileSystem/CopyPathAction.cs | 4 +- .../FileSystem/CopyPathWithQuotesAction.cs | 4 +- .../FileSystem/OpenFileLocationAction.cs | 13 +- .../Actions/FileSystem/PasteItemAction.cs | 6 +- .../FileSystem/PasteItemAsShortcutAction.cs | 6 +- .../FileSystem/PasteItemToSelectionAction.cs | 10 +- src/Files.App/Actions/Git/GitCloneAction.cs | 4 +- src/Files.App/Actions/Global/UndoAction.cs | 2 +- .../ArrangePanesHorizontallyAction.cs | 2 +- .../ArrangePanesVerticallyAction.cs | 2 +- .../Navigation/CloseActivePaneAction.cs | 2 +- .../CloseOtherTabsSelectedAction.cs | 3 +- .../CloseTabsToTheLeftSelectedAction.cs | 3 +- .../CloseTabsToTheRightSelectedAction.cs | 3 +- .../Navigation/DuplicateSelectedTabAction.cs | 2 +- .../Actions/Navigation/FocusOtherPane.cs | 2 +- .../Actions/Navigation/NextTabAction.cs | 2 +- .../OpenInNewPane/BaseOpenInNewPaneAction.cs | 8 +- .../Actions/Navigation/PreviousTabAction.cs | 2 +- .../Navigation/SplitPaneHorizontallyAction.cs | 3 +- .../Navigation/SplitPaneVerticallyAction.cs | 3 +- src/Files.App/Actions/Open/OpenInIDEAction.cs | 5 +- .../Open/OpenTerminalFromSidebarAction.cs | 8 +- .../Shelf/DeleteItemFromShelfAction.cs | 4 +- .../Actions/Show/ToggleDualPaneAction.cs | 6 +- .../Actions/Show/ToggleFilterHeaderAction.cs | 6 +- .../Actions/Start/UnpinFromStartAction.cs | 7 +- src/Files.App/App.xaml.cs | 4 +- .../DateTimeOffsetToStringConverter.cs | 15 +- .../DoubleArrayToStringConverter.cs | 5 +- .../Converters/DoubleToStringConverter.cs | 15 +- .../StringArrayToStringConverter.cs | 2 +- .../Converters/UInt32ToStringConverter.cs | 15 +- .../Data/Behaviors/StickyHeaderBehavior.cs | 15 +- .../Contexts/MultiPanes/MultiPanesContext.cs | 4 +- .../Multitasking/IMultitaskingContext.cs | 4 +- .../Multitasking/MultitaskingContext.cs | 4 +- .../Data/Contexts/Tags/TagsContext.cs | 2 +- .../Contracts/IFileTagsSettingsService.cs | 10 +- .../Data/Contracts/IGeneralSettingsService.cs | 14 +- .../Data/Contracts/IMultiPaneInfo.cs | 2 +- src/Files.App/Data/Contracts/IShellPage.cs | 10 +- .../Data/Contracts/IVersionControlService.cs | 2 +- .../Data/Contracts/IWidgetCardItem.cs | 2 +- .../Data/Contracts/ImagingService.cs | 6 +- .../CurrentInstanceChangedEventArgs.cs | 4 +- .../PathBoxItemDroppedEventArgs.cs | 6 +- .../EventArguments/PathNavigationEventArgs.cs | 4 +- .../ToolbarQuerySubmittedEventArgs.cs | 2 +- .../ContentPageContextFlyoutFactory.cs | 12 +- .../Factories/ShellContextFlyoutHelper.cs | 25 +- src/Files.App/Data/Items/AccessMaskItem.cs | 2 +- .../Data/Items/AppThemeResourceItem.cs | 4 +- src/Files.App/Data/Items/ContextMenu.cs | 10 +- src/Files.App/Data/Items/DriveItem.cs | 36 +- src/Files.App/Data/Items/FileTagItem.cs | 14 +- src/Files.App/Data/Items/IListedItem.cs | 24 +- src/Files.App/Data/Items/ListedItem.cs | 98 +++--- src/Files.App/Data/Items/LocationItem.cs | 14 +- .../Data/Items/NavigationBarSuggestionItem.cs | 2 +- src/Files.App/Data/Items/ShellFileItem.cs | 24 +- src/Files.App/Data/Items/ShellLibraryItem.cs | 12 +- src/Files.App/Data/Items/ShellLinkItem.cs | 6 +- src/Files.App/Data/Items/ShellNewEntry.cs | 12 +- .../Data/Items/ShellOperationResult.cs | 4 +- .../Data/Items/SidebarLibraryItem.cs | 10 +- .../Data/Items/WidgetContainerItem.cs | 6 +- .../Data/Items/WidgetDriveCardItem.cs | 15 +- .../Data/Items/WidgetFileTagCardItem.cs | 4 +- src/Files.App/Data/Items/WidgetRecentItem.cs | 3 + src/Files.App/Data/Items/Win32Process.cs | 6 +- src/Files.App/Data/Items/WslDistroItem.cs | 16 +- src/Files.App/Data/Models/AppModel.cs | 2 +- .../Data/Models/CompressArchiveModel.cs | 24 +- .../Models/ContextMenuFlyoutItemViewModel.cs | 24 +- .../ContextMenuFlyoutItemViewModelBuilder.cs | 2 +- .../Data/Models/CurrentInstanceViewModel.cs | 4 +- src/Files.App/Data/Models/HashInfoItem.cs | 8 +- .../Data/Models/ItemManipulationModel.cs | 28 +- .../Data/Models/ListedTagViewModel.cs | 6 +- .../Data/Models/PinnedFoldersManager.cs | 11 +- .../SelectedItemsPropertiesViewModel.cs | 94 ++--- src/Files.App/Data/Models/SuggestionModel.cs | 2 +- src/Files.App/Data/Parameters/ColumnParam.cs | 2 +- .../PropertiesPageNavigationParameter.cs | 8 +- .../Data/Parameters/TabBarItemParameter.cs | 32 +- src/Files.App/Dialogs/AddItemDialog.xaml.cs | 5 +- src/Files.App/Dialogs/DynamicDialog.xaml.cs | 23 +- .../Dialogs/FilesystemOperationDialog.xaml.cs | 4 +- .../Extensions/ImageSourceExtensions.cs | 4 +- .../Extensions/ShellNewEntryExtensions.cs | 28 +- .../Helpers/Application/AppLifecycleHelper.cs | 3 +- src/Files.App/Helpers/BitmapHelper.cs | 4 +- .../Helpers/Dialog/DialogDisplayHelper.cs | 4 +- .../Helpers/Dialog/DynamicDialogFactory.cs | 36 +- src/Files.App/Helpers/LocationHelpers.cs | 2 +- src/Files.App/Helpers/MediaFileHelper.cs | 4 +- .../Helpers/MenuFlyout/MenuFlyoutHelper.cs | 14 +- .../Helpers/NaturalStringComparer.cs | 2 +- .../Navigation/MultitaskingTabsHelpers.cs | 5 +- .../Helpers/Navigation/NavigationHelpers.cs | 66 ++-- src/Files.App/Helpers/PathNormalization.cs | 12 +- src/Files.App/Helpers/TransferHelpers.cs | 10 +- src/Files.App/Helpers/UI/AppSystemBackdrop.cs | 16 +- src/Files.App/Helpers/UI/DragZoneHelper.cs | 7 +- .../Helpers/UI/UIFilesystemHelpers.cs | 36 +- src/Files.App/Helpers/UI/UIHelpers.cs | 10 +- .../Helpers/Win32/Win32Helper.Process.cs | 19 +- .../Helpers/Win32/Win32Helper.Shell.cs | 6 +- .../Helpers/Win32/Win32Helper.Storage.cs | 29 +- .../Helpers/Win32/Win32PInvoke.Structs.cs | 2 +- .../Helpers/Xaml/DependencyObjectHelpers.cs | 12 +- .../Helpers/Xaml/SystemTypeToXaml.cs | 7 +- src/Files.App/MainWindow.xaml.cs | 20 +- .../Services/App/AppUpdateSideloadService.cs | 22 +- .../UserDateTimeFormatter.cs | 12 +- src/Files.App/Services/Git/LibGit2Service.cs | 10 +- .../Settings/FileTagsSettingsService.cs | 36 +- .../Settings/GeneralSettingsService.cs | 14 +- .../Services/Settings/UserSettingsService.cs | 36 +- .../Services/SizeProvider/UserSizeProvider.cs | 6 +- .../Services/Storage/StorageArchiveService.cs | 13 +- .../Services/Storage/StorageDevicesService.cs | 13 +- .../Services/Storage/StorageNetworkService.cs | 9 +- .../Windows/WindowsJumpListService.cs | 2 +- .../Windows/WindowsQuickAccessService.cs | 11 + .../Services/Windows/WindowsShellService.cs | 2 +- .../UserControls/DataGridHeader.xaml.cs | 4 +- src/Files.App/UserControls/FileIcon.xaml.cs | 17 +- .../FilePreviews/CodePreview.xaml.cs | 11 +- .../FilePreviews/MediaPreview.xaml.cs | 4 +- .../UserControls/Menus/FileTagsContextMenu.cs | 2 +- .../Menus/MenuFlyoutItemWithImage.xaml.cs | 9 +- .../MenuFlyoutItemWithThemedIcon.xaml.cs | 9 +- .../MenuFlyoutSubItemCustomProperties.cs | 9 +- ...ToggleMenuFlyoutItemWithThemedIcon.xaml.cs | 9 +- .../UserControls/NavigationToolbar.xaml.cs | 100 +++--- .../Selection/RectangleSelection.cs | 15 +- .../RectangleSelection_ListViewBase.cs | 29 +- .../UserControls/StatusCenter/SpeedGraph.cs | 24 +- .../UserControls/TabBar/BaseTabBar.cs | 25 +- src/Files.App/UserControls/TabBar/ITabBar.cs | 6 +- .../UserControls/TabBar/ITabBarItem.cs | 8 +- .../UserControls/TabBar/ITabBarItemContent.cs | 2 +- .../UserControls/TabBar/TabBar.xaml.cs | 44 ++- .../UserControls/TabBar/TabBarItem.cs | 12 +- src/Files.App/UserControls/Toolbar.xaml.cs | 11 +- .../Widgets/RecentFilesWidget.xaml.cs | 4 +- .../Utils/Cloud/CloudDriveSyncStatusUI.cs | 4 +- .../Utils/Cloud/CloudDrivesDetector.cs | 24 +- .../Utils/Cloud/CloudDrivesManager.cs | 4 +- .../Utils/Cloud/Detector/BoxCloudDetector.cs | 9 +- .../Cloud/Detector/DropBoxCloudDetector.cs | 26 +- .../Detector/GoogleDriveCloudDetector.cs | 6 +- .../Detector/SynologyDriveCloudDetector.cs | 18 +- .../Utils/CommandLine/CommandLineParser.cs | 4 +- .../Utils/FileTags/FileTagsHelper.cs | 4 +- .../Utils/FileTags/FileTagsManager.cs | 4 +- src/Files.App/Utils/Git/GitHelpers.cs | 4 +- .../Utils/Global/WSLDistroManager.cs | 4 +- .../Global/WindowsStorageDeviceWatcher.cs | 14 +- src/Files.App/Utils/Library/LibraryManager.cs | 80 +++-- .../Utils/Serialization/BaseJsonSettings.cs | 2 + .../Serialization/IJsonSettingsSerializer.cs | 2 +- .../Serialization/ISettingsSerializer.cs | 2 +- .../DefaultJsonSettingsSerializer.cs | 2 +- .../DefaultSettingsSerializer.cs | 4 +- src/Files.App/Utils/Shell/ContextMenu.cs | 19 +- src/Files.App/Utils/Shell/LaunchHelper.cs | 22 +- src/Files.App/Utils/Shell/OpenWithMenu.cs | 3 +- .../Utils/Shell/ShellFileOperations2.cs | 125 +++---- .../Utils/Shell/ShellFolderExtensions.cs | 22 +- src/Files.App/Utils/Shell/ShellLibraryEx.cs | 48 +-- .../Utils/Shell/ShellLibraryFolders.cs | 9 +- .../Utils/Shell/ShellNewMenuHelper.cs | 20 +- .../Utils/Shell/ThreadWithMessageQueue.cs | 60 ++-- .../Utils/StatusCenter/StatusCenterHelper.cs | 6 +- .../Collection/BlockingListEnumerator.cs | 15 +- .../BulkConcurrentObservableCollection.cs | 4 +- .../Storage/Collection/GroupedCollection.cs | 4 +- .../Collection/GroupedHeaderViewModel.cs | 18 +- .../Storage/Collection/GroupingHelper.cs | 25 +- .../Utils/Storage/Collection/SortingHelper.cs | 2 +- .../Enumerators/UniversalStorageEnumerator.cs | 36 +- .../Enumerators/Win32StorageEnumerator.cs | 24 +- .../Utils/Storage/Helpers/DriveHelpers.cs | 11 +- .../Storage/Helpers/FileThumbnailHelper.cs | 4 +- .../Utils/Storage/Helpers/FilesystemResult.cs | 12 +- .../Utils/Storage/Helpers/FilesystemTasks.cs | 29 +- .../Utils/Storage/Helpers/FolderHelpers.cs | 2 +- .../Storage/Helpers/IStorageItemWithPath.cs | 2 +- .../Storage/Helpers/StorageFileExtensions.cs | 82 ++--- .../Storage/Helpers/StorageFileWithPath.cs | 10 +- .../Storage/Helpers/StorageFolderWithPath.cs | 10 +- .../Utils/Storage/Helpers/StorageHelpers.cs | 44 +-- .../Storage/Helpers/StorageItemIconHelpers.cs | 2 +- .../Utils/Storage/History/IStorageHistory.cs | 8 +- .../Utils/Storage/History/StorageHistory.cs | 16 +- .../Storage/History/StorageHistoryHelpers.cs | 8 +- .../History/StorageHistoryOperations.cs | 78 +++-- .../Storage/History/StorageHistoryWrapper.cs | 3 +- .../Operations/FileOperationsHelpers.cs | 101 +++--- .../Storage/Operations/FilesystemHelpers.cs | 44 ++- .../Operations/FilesystemOperations.cs | 281 ++++++++------- .../Operations/IFilesystemOperations.cs | 38 +-- .../Operations/ShellFilesystemOperations.cs | 181 ++++++---- .../Utils/Storage/Search/FolderSearch.cs | 53 +-- .../BaseBasicStorageItemExtraProperties.cs | 8 +- .../StorageBaseItems/BaseStorageFile.cs | 66 ++-- .../StorageBaseItems/BaseStorageFolder.cs | 99 ++++-- .../BaseStorageItemExtraProperties.cs | 26 +- .../StorageBaseItems/IBaseStorageFile.cs | 14 +- .../StorageBaseItems/IBaseStorageFolder.cs | 28 +- .../StorageBaseItems/ICreateFileWithStream.cs | 4 +- .../IPasswordProtectedItem.cs | 4 +- .../StorageBaseItems/StorageCredential.cs | 4 + .../Storage/StorageItems/BaseQueryResults.cs | 35 +- .../Storage/StorageItems/FtpStorageFile.cs | 51 +-- .../Storage/StorageItems/FtpStorageFolder.cs | 66 ++-- .../Storage/StorageItems/NativeStorageFile.cs | 25 +- .../Storage/StorageItems/ShellStorageFile.cs | 22 +- .../StorageItems/ShellStorageFolder.cs | 52 +-- .../StorageItems/StreamWithContentType.cs | 6 +- .../Storage/StorageItems/SystemStorageFile.cs | 52 +-- .../StorageItems/SystemStorageFolder.cs | 60 ++-- .../StorageItems/VirtualStorageFile.cs | 29 +- .../StorageItems/VirtualStorageFolder.cs | 48 +-- .../StorageItems/VirtualStorageItem.cs | 8 +- .../Storage/StorageItems/ZipStorageFile.cs | 99 +++--- .../Storage/StorageItems/ZipStorageFolder.cs | 95 +++--- .../Utils/Taskbar/SystemTrayIconWindow.cs | 4 +- .../Dialogs/CreateShortcutDialogViewModel.cs | 8 +- .../DecompressArchiveDialogViewModel.cs | 10 +- .../Dialogs/DynamicDialogViewModel.cs | 139 ++++---- .../FileSystemDialogConflictItemViewModel.cs | 16 +- .../FileSystemDialogViewModel.cs | 2 +- .../ViewModels/Layouts/BaseLayoutViewModel.cs | 35 +- src/Files.App/ViewModels/MainPageViewModel.cs | 7 +- .../Properties/BasePropertiesPage.cs | 22 +- .../Properties/CustomizationViewModel.cs | 16 +- .../ViewModels/Properties/HashesViewModel.cs | 10 +- .../Properties/Items/BaseProperties.cs | 20 +- .../Items/CombinedFileProperties.cs | 52 +-- .../Properties/Items/CombinedProperties.cs | 7 +- .../Properties/Items/DriveProperties.cs | 16 +- .../Properties/Items/FileProperties.cs | 62 ++-- .../Properties/Items/FileProperty.cs | 64 ++-- .../Properties/Items/FilePropertySection.cs | 2 +- .../Properties/Items/FolderProperties.cs | 14 +- .../Properties/Items/LibraryProperties.cs | 16 +- .../Properties/MainPropertiesViewModel.cs | 3 +- .../Properties/SecurityAdvancedViewModel.cs | 8 +- .../Properties/SecurityViewModel.cs | 9 +- .../ViewModels/Settings/ActionsViewModel.cs | 4 +- .../ViewModels/Settings/AdvancedViewModel.cs | 12 +- .../Settings/AppearanceViewModel.cs | 9 +- .../ViewModels/Settings/DevToolsViewModel.cs | 6 +- .../ViewModels/Settings/FoldersViewModel.cs | 8 +- .../ViewModels/Settings/GeneralViewModel.cs | 16 +- src/Files.App/ViewModels/ShellViewModel.cs | 195 ++++++----- .../UserControls/InfoPaneViewModel.cs | 35 +- .../NavigationToolbarViewModel.cs | 189 ++++++----- .../Previews/ArchivePreviewViewModel.cs | 13 +- .../UserControls/Previews/BasePreviewModel.cs | 55 +-- .../Previews/CodePreviewViewModel.cs | 10 +- .../Previews/FolderPreviewViewModel.cs | 25 +- .../Previews/ImagePreviewViewModel.cs | 6 +- .../Previews/MarkdownPreviewViewModel.cs | 6 +- .../Previews/MediaPreviewViewModel.cs | 13 +- .../Previews/PDFPreviewViewModel.cs | 2 +- .../Previews/RichTextPreviewViewModel.cs | 4 +- .../Previews/ShortcutPreviewViewModel.cs | 4 +- .../Previews/TextPreviewViewModel.cs | 16 +- .../UserControls/SidebarViewModel.cs | 180 +++++----- .../UserControls/StatusCenterViewModel.cs | 2 +- .../Widgets/FileTagsWidgetViewModel.cs | 2 +- .../Widgets/QuickAccessWidgetViewModel.cs | 29 +- .../Widgets/RecentFilesWidgetViewModel.cs | 28 +- src/Files.App/Views/HomePage.xaml.cs | 13 +- .../Views/Layouts/BaseGroupableLayoutPage.cs | 71 ++-- src/Files.App/Views/Layouts/BaseLayoutPage.cs | 321 ++++++++++-------- .../Views/Layouts/ColumnLayoutPage.xaml.cs | 34 +- .../Views/Layouts/ColumnsLayoutPage.xaml.cs | 124 ++++--- .../Views/Layouts/DetailsLayoutPage.xaml.cs | 118 ++++--- .../Views/Layouts/GridLayoutPage.xaml.cs | 83 +++-- src/Files.App/Views/MainPage.xaml.cs | 12 +- .../Properties/CustomizationPage.xaml.cs | 9 +- .../Views/Properties/GeneralPage.xaml.cs | 8 +- .../Views/Properties/HashesPage.xaml.cs | 4 +- .../Views/Properties/LibraryPage.xaml.cs | 6 +- .../Properties/MainPropertiesPage.xaml.cs | 54 +-- .../Properties/SecurityAdvancedPage.xaml.cs | 2 +- .../Views/Properties/SecurityPage.xaml.cs | 7 +- .../Views/Properties/SignaturesPage.xaml.cs | 4 +- src/Files.App/Views/ReleaseNotesPage.xaml.cs | 13 +- .../Views/Settings/AppearancePage.xaml.cs | 4 +- .../Views/Settings/GeneralPage.xaml.cs | 3 +- .../Views/Settings/SettingsPage.xaml.cs | 13 +- src/Files.App/Views/Settings/TagsPage.xaml.cs | 72 ++-- src/Files.App/Views/ShellPanesPage.xaml.cs | 4 +- src/Files.App/Views/Shells/BaseShellPage.cs | 163 +++++---- .../Views/Shells/ColumnShellPage.xaml.cs | 58 ++-- .../Views/Shells/ModernShellPage.xaml.cs | 41 +-- .../Extensions/GenericExtensions.cs | 11 + .../Helpers/FileExtensionHelpers.cs | 3 +- 317 files changed, 4146 insertions(+), 3024 deletions(-) diff --git a/src/Files.App.Controls/Omnibar/IOmnibarTextMemberPathProvider.cs b/src/Files.App.Controls/Omnibar/IOmnibarTextMemberPathProvider.cs index decf6588f52b..aa99018edafc 100644 --- a/src/Files.App.Controls/Omnibar/IOmnibarTextMemberPathProvider.cs +++ b/src/Files.App.Controls/Omnibar/IOmnibarTextMemberPathProvider.cs @@ -15,6 +15,6 @@ public interface IOmnibarTextMemberPathProvider /// Retrieves the path of the text member as a string. This path can be used to identify the location of the text member. /// /// Returns a string representing the path of the text member. - string GetTextMemberPath(string textMemberPath); + string? GetTextMemberPath(string textMemberPath); } } diff --git a/src/Files.App.Controls/Omnibar/Omnibar.cs b/src/Files.App.Controls/Omnibar/Omnibar.cs index 707dd90bfc58..7e7528f1e84f 100644 --- a/src/Files.App.Controls/Omnibar/Omnibar.cs +++ b/src/Files.App.Controls/Omnibar/Omnibar.cs @@ -275,7 +275,7 @@ private string GetObjectText(object obj) return obj is string text ? text : obj is IOmnibarTextMemberPathProvider textMemberPathProvider - ? textMemberPathProvider.GetTextMemberPath(CurrentSelectedMode.TextMemberPath ?? string.Empty) + ? textMemberPathProvider.GetTextMemberPath(CurrentSelectedMode.TextMemberPath ?? string.Empty) ?? string.Empty : obj.ToString() ?? string.Empty; } diff --git a/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs b/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs index 670ff2a7116d..d94ef7017f8a 100644 --- a/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs +++ b/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs @@ -44,7 +44,7 @@ protected void GetDestination(out string[] sources, out string directory, out st if (sources.Length is not 0) { // Get the current directory path - directory = context.ShellPage.ShellViewModel.WorkingDirectory.Normalize(); + directory = (context.ShellPage?.ShellViewModel?.WorkingDirectory ?? context.Folder?.ItemPath ?? string.Empty).Normalize(); // Get the library save folder if the folder is library item if (App.LibraryManager.TryGetLibrary(directory, out var library) && !library.IsEmpty) diff --git a/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs b/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs index 0611c69cbccd..af4e4b542624 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs @@ -56,15 +56,17 @@ protected async Task DecompressArchiveHereAsync(bool smart = false) return; var selectedItems = context.SelectedItems.ToList(); - var currentFolderPath = context.ShellPage?.ShellViewModel.CurrentFolder?.ItemPath ?? string.Empty; - BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(currentFolderPath); + var currentFolderPath = context.ShellPage?.ShellViewModel?.CurrentFolder?.ItemPath ?? string.Empty; + BaseStorageFolder? currentFolder = await StorageHelpers.ToStorageItem(currentFolderPath); + if (currentFolder is null) + return; foreach (var selectedItem in selectedItems) { var password = string.Empty; - BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); + BaseStorageFile? archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); - if (archive?.Path is null) + if (archive is null) return; if (await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path))) @@ -137,13 +139,16 @@ static ReadOnlySpan GetFirstMeaningfulSegment(ReadOnlySpan path) return false; }); - if (smart && currentFolder is not null && isMultipleItems) + if (smart && isMultipleItems) { destinationFolder = - await FilesystemTasks.Wrap(() => + await FilesystemTasks.WrapNullable(() => currentFolder.CreateFolderAsync( SystemIO.Path.GetFileNameWithoutExtension(archive.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); + + if (destinationFolder is null) + return; } else { diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs index f3bbf4b87eaf..69d1df369cb2 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs @@ -42,9 +42,9 @@ public override async Task ExecuteAsync(object? parameter = null) if (string.IsNullOrEmpty(archivePath)) return; - BaseStorageFile archive = await StorageHelpers.ToStorageItem(archivePath); + BaseStorageFile? archive = await StorageHelpers.ToStorageItem(archivePath); - if (archive?.Path is null) + if (archive is null) return; var isArchiveEncrypted = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path)); @@ -83,7 +83,7 @@ public override async Task ExecuteAsync(object? parameter = null) if (!StorageHelpers.Exists(archive.Path)) return; - BaseStorageFolder destinationFolder = decompressArchiveViewModel.DestinationFolder; + BaseStorageFolder? destinationFolder = decompressArchiveViewModel.DestinationFolder; string destinationFolderPath = decompressArchiveViewModel.DestinationFolderPath; // Save extraction location for future use @@ -91,13 +91,22 @@ public override async Task ExecuteAsync(object? parameter = null) if (destinationFolder is null) { - BaseStorageFolder parentFolder = await StorageHelpers.ToStorageItem(Path.GetDirectoryName(archive.Path) ?? string.Empty); - destinationFolder = await FilesystemTasks.Wrap(() => parentFolder.CreateFolderAsync(Path.GetFileName(destinationFolderPath), CreationCollisionOption.GenerateUniqueName).AsTask()); + var parentPath = Path.GetDirectoryName(archive.Path); + if (parentPath is null) + return; + + BaseStorageFolder? parentFolder = await StorageHelpers.ToStorageItem(parentPath); + if (parentFolder is null) + return; + + destinationFolder = await FilesystemTasks.WrapNullable(() => parentFolder.CreateFolderAsync(Path.GetFileName(destinationFolderPath), CreationCollisionOption.GenerateUniqueName).AsTask()); + if (destinationFolder is null) + return; } // Operate decompress var result = await FilesystemTasks.Wrap(() => - StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password, encoding)); + StorageArchiveService.DecompressAsync(archive.Path, destinationFolder.Path, password, encoding)); if (decompressArchiveViewModel.OpenDestinationFolderOnCompletion) await NavigationHelpers.OpenPath(destinationFolderPath, context.ShellPage, FilesystemItemType.Directory); diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs index d3b2d35e290e..2e0ca10c0e79 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs @@ -34,11 +34,13 @@ public override async Task ExecuteAsync(object? parameter = null) { var password = string.Empty; - BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); - BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(context.ShellPage?.ShellViewModel.CurrentFolder.ItemPath); - BaseStorageFolder destinationFolder = null; + var archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); + var currentFolderPath = context.ShellPage?.ShellViewModel?.CurrentFolder?.ItemPath; + if (archive?.Path is null || string.IsNullOrEmpty(currentFolderPath)) + return; - if (archive?.Path is null) + var currentFolder = await StorageHelpers.ToStorageItem(currentFolderPath); + if (currentFolder is null) return; if (await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path))) @@ -57,16 +59,20 @@ public override async Task ExecuteAsync(object? parameter = null) ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); if (option != ContentDialogResult.Primary) return; + if (decompressArchiveViewModel.Password is not { } passwordBytes) + return; - password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); + password = Encoding.UTF8.GetString(passwordBytes); } - if (currentFolder is not null) - destinationFolder = await FilesystemTasks.Wrap(() => currentFolder.CreateFolderAsync(SystemIO.Path.GetFileNameWithoutExtension(archive.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); + var destinationResult = await FilesystemTasks.WrapNullable(() => + currentFolder.CreateFolderAsync(SystemIO.Path.GetFileNameWithoutExtension(archive.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); + if (destinationResult.Result is not { } destinationFolder) + return; // Operate decompress - var result = await FilesystemTasks.Wrap(() => - StorageArchiveService.DecompressAsync(selectedItem.ItemPath, destinationFolder?.Path ?? string.Empty, password)); + await FilesystemTasks.Wrap(() => + StorageArchiveService.DecompressAsync(selectedItem.ItemPath, destinationFolder.Path, password)); } } diff --git a/src/Files.App/Actions/Content/Install/InstallFontAction.cs b/src/Files.App/Actions/Content/Install/InstallFontAction.cs index 31b5f4aeaed6..d38416d79570 100644 --- a/src/Files.App/Actions/Content/Install/InstallFontAction.cs +++ b/src/Files.App/Actions/Content/Install/InstallFontAction.cs @@ -38,17 +38,17 @@ public InstallFontAction() public async Task ExecuteAsync(object? parameter = null) { - if (context?.ShellPage?.ShellViewModel.WorkingDirectory is null) + if (context.ShellPage?.ShellViewModel?.WorkingDirectory is not { } workingDirectory) return; - var banner = StatusCenterHelper.AddCard_InstallFont(context.ShellPage.ShellViewModel.WorkingDirectory.CreateEnumerable(), ReturnResult.InProgress, context.SelectedItems.Count); + var banner = StatusCenterHelper.AddCard_InstallFont(workingDirectory.CreateEnumerable(), ReturnResult.InProgress, context.SelectedItems.Count); banner.IsCancelable = false; var paths = context.SelectedItems.Select(item => item.ItemPath).ToArray(); await Win32Helper.InstallFontsAsync(paths, false); StatusCenterViewModel.RemoveItem(banner); - StatusCenterHelper.AddCard_InstallFont(context.ShellPage.ShellViewModel.WorkingDirectory.CreateEnumerable(), ReturnResult.Success, context.SelectedItems.Count); + StatusCenterHelper.AddCard_InstallFont(workingDirectory.CreateEnumerable(), ReturnResult.Success, context.SelectedItems.Count); } public void Context_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) diff --git a/src/Files.App/Actions/Content/Selection/SelectAllAction.cs b/src/Files.App/Actions/Content/Selection/SelectAllAction.cs index 142d675fcb20..514429d54e4e 100644 --- a/src/Files.App/Actions/Content/Selection/SelectAllAction.cs +++ b/src/Files.App/Actions/Content/Selection/SelectAllAction.cs @@ -30,11 +30,10 @@ public bool IsExecutable if (context.PageType is ContentPageTypes.Home) return false; - var page = context.ShellPage; - if (page is null) + if (context.ShellPage is not { ShellViewModel: { } shellViewModel } page) return false; - int itemCount = page.ShellViewModel.FilesAndFolders.Count; + int itemCount = shellViewModel.FilesAndFolders.Count; int selectedItemCount = context.SelectedItems.Count; if (itemCount == selectedItemCount) return false; diff --git a/src/Files.App/Actions/Content/Tags/RemoveTagsAction.cs b/src/Files.App/Actions/Content/Tags/RemoveTagsAction.cs index 26646ddd889d..489a86288c93 100644 --- a/src/Files.App/Actions/Content/Tags/RemoveTagsAction.cs +++ b/src/Files.App/Actions/Content/Tags/RemoveTagsAction.cs @@ -33,9 +33,9 @@ public RemoveTagsAction() public async Task ExecuteAsync(object? parameter = null) { if (await FileTagsHelper.RemoveTagsAsync(context.SelectedItems) && - context.ShellPage is not null) + context.ShellPage?.ShellViewModel is { } shellViewModel) { - await context.ShellPage.ShellViewModel.RefreshTagGroups(); + await shellViewModel.RefreshTagGroups(); } } diff --git a/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs b/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs index d4d64e52ad1d..cc4d2e674c58 100644 --- a/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs +++ b/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs @@ -37,10 +37,10 @@ item.PrimaryItemAttribute is StorageItemTypes.File ? FilesystemItemType.File : FilesystemItemType.Directory)); - if (context.ShellPage is IShellPage shellPage) + if (context.ShellPage is { ShellViewModel: { } shellViewModel } shellPage) { await shellPage.FilesystemHelpers.DeleteItemsAsync(items, settings.DeleteConfirmationPolicy, permanently, true); - await shellPage.ShellViewModel.ApplyFilesAndFoldersChangesAsync(); + await shellViewModel.ApplyFilesAndFoldersChangesAsync(); } } diff --git a/src/Files.App/Actions/FileSystem/CopyItemPathAction.cs b/src/Files.App/Actions/FileSystem/CopyItemPathAction.cs index 1093989f0dcc..61a96eae2253 100644 --- a/src/Files.App/Actions/FileSystem/CopyItemPathAction.cs +++ b/src/Files.App/Actions/FileSystem/CopyItemPathAction.cs @@ -35,11 +35,9 @@ public CopyItemPathAction() public Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage?.SlimContentPage is not null) + if (context.SelectedItems.Count > 0) { - var path = context.ShellPage.SlimContentPage.SelectedItems is not null - ? context.ShellPage.SlimContentPage.SelectedItems.Select(x => x.ItemPath).Aggregate((accum, current) => accum + "\n" + current) - : context.ShellPage.ShellViewModel.WorkingDirectory; + var path = string.Join('\n', context.SelectedItems.Select(x => x.ItemPath)); if (FtpHelpers.IsFtpPath(path)) path = path.Replace('\\', '/'); diff --git a/src/Files.App/Actions/FileSystem/CopyItemPathWithQuotesAction.cs b/src/Files.App/Actions/FileSystem/CopyItemPathWithQuotesAction.cs index 2d6374e43b69..0431b9b2a433 100644 --- a/src/Files.App/Actions/FileSystem/CopyItemPathWithQuotesAction.cs +++ b/src/Files.App/Actions/FileSystem/CopyItemPathWithQuotesAction.cs @@ -35,12 +35,9 @@ public CopyItemPathWithQuotesAction() public Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage?.SlimContentPage is not null) + if (context.SelectedItems.Count > 0) { - var selectedItems = context.ShellPage.SlimContentPage.SelectedItems; - var path = selectedItems is not null - ? string.Join("\n", selectedItems.Select(item => $"\"{item.ItemPath}\"")) - : context.ShellPage.ShellViewModel.WorkingDirectory; + var path = string.Join("\n", context.SelectedItems.Select(item => $"\"{item.ItemPath}\"")); if (FtpHelpers.IsFtpPath(path)) path = path.Replace('\\', '/'); diff --git a/src/Files.App/Actions/FileSystem/CopyPathAction.cs b/src/Files.App/Actions/FileSystem/CopyPathAction.cs index c1d1cf13b256..c20378e86bde 100644 --- a/src/Files.App/Actions/FileSystem/CopyPathAction.cs +++ b/src/Files.App/Actions/FileSystem/CopyPathAction.cs @@ -35,9 +35,9 @@ public CopyPathAction() public Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage?.SlimContentPage is not null) + if (context.ShellPage?.ShellViewModel?.WorkingDirectory is { } workingDirectory) { - var path = context.ShellPage.ShellViewModel.WorkingDirectory; + var path = workingDirectory; if (FtpHelpers.IsFtpPath(path)) path = path.Replace('\\', '/'); diff --git a/src/Files.App/Actions/FileSystem/CopyPathWithQuotesAction.cs b/src/Files.App/Actions/FileSystem/CopyPathWithQuotesAction.cs index e6da45c48893..fcc679e682e8 100644 --- a/src/Files.App/Actions/FileSystem/CopyPathWithQuotesAction.cs +++ b/src/Files.App/Actions/FileSystem/CopyPathWithQuotesAction.cs @@ -35,9 +35,9 @@ public CopyPathWithQuotesAction() public Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage?.SlimContentPage is not null) + if (context.ShellPage?.ShellViewModel?.WorkingDirectory is { } workingDirectory) { - var path = "\"" + context.ShellPage.ShellViewModel.WorkingDirectory + "\""; + var path = $"\"{workingDirectory}\""; if (FtpHelpers.IsFtpPath(path)) path = path.Replace('\\', '/'); diff --git a/src/Files.App/Actions/FileSystem/OpenFileLocationAction.cs b/src/Files.App/Actions/FileSystem/OpenFileLocationAction.cs index e39ad4fc9c51..7119b193da65 100644 --- a/src/Files.App/Actions/FileSystem/OpenFileLocationAction.cs +++ b/src/Files.App/Actions/FileSystem/OpenFileLocationAction.cs @@ -36,7 +36,7 @@ public OpenFileLocationAction() public async Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage?.ShellViewModel is null) + if (context.ShellPage is not { ShellViewModel: { } shellViewModel } shellPage) return; var item = context.SelectedItem as IShortcutItem; @@ -46,15 +46,18 @@ public async Task ExecuteAsync(object? parameter = null) // Check if destination path exists var folderPath = Path.GetDirectoryName(item.TargetPath); - var destFolder = await context.ShellPage.ShellViewModel.GetFolderWithPathFromPathAsync(folderPath); + if (folderPath is null) + return; + + var destFolder = await shellViewModel.GetFolderWithPathFromPathAsync(folderPath); if (destFolder) { - context.ShellPage?.NavigateWithArguments(context.ShellPage.InstanceViewModel.FolderSettings.GetLayoutType(folderPath), new NavigationArguments() + shellPage.NavigateWithArguments(shellPage.InstanceViewModel.FolderSettings.GetLayoutType(folderPath), new NavigationArguments() { NavPathParam = folderPath, - SelectItems = [Path.GetFileName(item.TargetPath.TrimPath())], - AssociatedTabInstance = context.ShellPage + SelectItems = [Path.GetFileName(item.TargetPath.TrimPath()) ?? string.Empty], + AssociatedTabInstance = shellPage }); } else if (destFolder == FileSystemStatusCode.NotFound) diff --git a/src/Files.App/Actions/FileSystem/PasteItemAction.cs b/src/Files.App/Actions/FileSystem/PasteItemAction.cs index 5610db6994b9..fb59cd6a684a 100644 --- a/src/Files.App/Actions/FileSystem/PasteItemAction.cs +++ b/src/Files.App/Actions/FileSystem/PasteItemAction.cs @@ -42,11 +42,11 @@ public PasteItemAction() public async Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage is null) + if (context.ShellPage is not { ShellViewModel: { } shellViewModel } shellPage) return; - string path = context.ShellPage.ShellViewModel.WorkingDirectory; - await UIFilesystemHelpers.PasteItemAsync(path, context.ShellPage); + string path = shellViewModel.WorkingDirectory; + await UIFilesystemHelpers.PasteItemAsync(path, shellPage); } public bool GetIsExecutable() diff --git a/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs b/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs index 15729fe5de36..625d3efdfcf8 100644 --- a/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs +++ b/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs @@ -33,11 +33,11 @@ public PasteItemAsShortcutAction() public Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage is null) + if (context.ShellPage is not { ShellViewModel: { } shellViewModel } shellPage) return Task.CompletedTask; - string path = context.ShellPage.ShellViewModel.WorkingDirectory; - return UIFilesystemHelpers.PasteItemAsShortcutAsync(path, context.ShellPage); + string path = shellViewModel.WorkingDirectory; + return UIFilesystemHelpers.PasteItemAsShortcutAsync(path, shellPage); } public bool GetIsExecutable() diff --git a/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs b/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs index e91afce97763..7862e5e88399 100644 --- a/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs +++ b/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs @@ -39,14 +39,16 @@ public PasteItemToSelectionAction() public async Task ExecuteAsync(object? parameter = null) { - if (context.ShellPage is null) + if (context.ShellPage is not { } shellPage) return; - string path = context.SelectedItem is ListedItem selectedItem + string? path = context.SelectedItem is ListedItem selectedItem ? selectedItem.ItemPath - : context.ShellPage.ShellViewModel.WorkingDirectory; + : shellPage.ShellViewModel?.WorkingDirectory; + if (path is null) + return; - await UIFilesystemHelpers.PasteItemAsync(path, context.ShellPage); + await UIFilesystemHelpers.PasteItemAsync(path, shellPage); } public bool GetIsExecutable() diff --git a/src/Files.App/Actions/Git/GitCloneAction.cs b/src/Files.App/Actions/Git/GitCloneAction.cs index 7db6d6fae71a..cbcb70b4760b 100644 --- a/src/Files.App/Actions/Git/GitCloneAction.cs +++ b/src/Files.App/Actions/Git/GitCloneAction.cs @@ -29,11 +29,11 @@ public GitCloneAction() public Task ExecuteAsync(object? parameter = null) { - if (pageContext.ShellPage is null) + if (pageContext.ShellPage?.ShellViewModel is not { } shellViewModel) return Task.CompletedTask; var repoUrl = parameter?.ToString() ?? string.Empty; - var viewModel = new CloneRepoDialogViewModel(repoUrl, pageContext.ShellPage.ShellViewModel.WorkingDirectory); + var viewModel = new CloneRepoDialogViewModel(repoUrl, shellViewModel.WorkingDirectory); return dialogService.ShowDialogAsync(viewModel); } diff --git a/src/Files.App/Actions/Global/UndoAction.cs b/src/Files.App/Actions/Global/UndoAction.cs index f12421cf7692..5d2860dfa710 100644 --- a/src/Files.App/Actions/Global/UndoAction.cs +++ b/src/Files.App/Actions/Global/UndoAction.cs @@ -33,7 +33,7 @@ public UndoAction() public Task ExecuteAsync(object? parameter = null) { - return context.ShellPage.StorageHistoryHelpers.TryUndo(); + return context.ShellPage?.StorageHistoryHelpers.TryUndo() ?? Task.CompletedTask; } private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Files.App/Actions/Navigation/ArrangePanesHorizontallyAction.cs b/src/Files.App/Actions/Navigation/ArrangePanesHorizontallyAction.cs index 92e8eaad063d..755ac52500d0 100644 --- a/src/Files.App/Actions/Navigation/ArrangePanesHorizontallyAction.cs +++ b/src/Files.App/Actions/Navigation/ArrangePanesHorizontallyAction.cs @@ -36,7 +36,7 @@ public ArrangePanesHorizontallyAction() public Task ExecuteAsync(object? parameter = null) { - ContentPageContext.ShellPage!.PaneHolder.ArrangePanes(ShellPaneArrangement.Horizontal); + ContentPageContext.ShellPage?.PaneHolder?.ArrangePanes(ShellPaneArrangement.Horizontal); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/ArrangePanesVerticallyAction.cs b/src/Files.App/Actions/Navigation/ArrangePanesVerticallyAction.cs index 49251e87d581..1f1a8a581aa1 100644 --- a/src/Files.App/Actions/Navigation/ArrangePanesVerticallyAction.cs +++ b/src/Files.App/Actions/Navigation/ArrangePanesVerticallyAction.cs @@ -36,7 +36,7 @@ public ArrangePanesVerticallyAction() public Task ExecuteAsync(object? parameter = null) { - ContentPageContext.ShellPage!.PaneHolder.ArrangePanes(ShellPaneArrangement.Vertical); + ContentPageContext.ShellPage?.PaneHolder?.ArrangePanes(ShellPaneArrangement.Vertical); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/CloseActivePaneAction.cs b/src/Files.App/Actions/Navigation/CloseActivePaneAction.cs index 08bd7d9c5f81..10fa93fec432 100644 --- a/src/Files.App/Actions/Navigation/CloseActivePaneAction.cs +++ b/src/Files.App/Actions/Navigation/CloseActivePaneAction.cs @@ -33,7 +33,7 @@ public CloseActivePaneAction() public Task ExecuteAsync(object? parameter = null) { - ContentPageContext.ShellPage?.PaneHolder.CloseActivePane(); + ContentPageContext.ShellPage?.PaneHolder?.CloseActivePane(); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/CloseOtherTabsSelectedAction.cs b/src/Files.App/Actions/Navigation/CloseOtherTabsSelectedAction.cs index efa757d56554..4f5b607d3953 100644 --- a/src/Files.App/Actions/Navigation/CloseOtherTabsSelectedAction.cs +++ b/src/Files.App/Actions/Navigation/CloseOtherTabsSelectedAction.cs @@ -18,7 +18,8 @@ public CloseOtherTabsSelectedAction() public override Task ExecuteAsync(object? parameter = null) { - MultitaskingTabsHelpers.CloseOtherTabs(context.SelectedTabItem, context.Control!); + if (context.SelectedTabItem is { } selectedTabItem && context.Control is { } control) + MultitaskingTabsHelpers.CloseOtherTabs(selectedTabItem, control); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/CloseTabsToTheLeftSelectedAction.cs b/src/Files.App/Actions/Navigation/CloseTabsToTheLeftSelectedAction.cs index 4ca4e35a9fa4..a36d428081f0 100644 --- a/src/Files.App/Actions/Navigation/CloseTabsToTheLeftSelectedAction.cs +++ b/src/Files.App/Actions/Navigation/CloseTabsToTheLeftSelectedAction.cs @@ -18,7 +18,8 @@ public CloseTabsToTheLeftSelectedAction() public override Task ExecuteAsync(object? parameter = null) { - MultitaskingTabsHelpers.CloseTabsToTheLeft(context.SelectedTabItem, context.Control!); + if (context.SelectedTabItem is { } selectedTabItem && context.Control is { } control) + MultitaskingTabsHelpers.CloseTabsToTheLeft(selectedTabItem, control); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/CloseTabsToTheRightSelectedAction.cs b/src/Files.App/Actions/Navigation/CloseTabsToTheRightSelectedAction.cs index a49b56b93d8d..e95aee89b38d 100644 --- a/src/Files.App/Actions/Navigation/CloseTabsToTheRightSelectedAction.cs +++ b/src/Files.App/Actions/Navigation/CloseTabsToTheRightSelectedAction.cs @@ -18,7 +18,8 @@ public CloseTabsToTheRightSelectedAction() public override Task ExecuteAsync(object? parameter = null) { - MultitaskingTabsHelpers.CloseTabsToTheRight(context.SelectedTabItem, context.Control!); + if (context.SelectedTabItem is { } selectedTabItem && context.Control is { } control) + MultitaskingTabsHelpers.CloseTabsToTheRight(selectedTabItem, control); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/DuplicateSelectedTabAction.cs b/src/Files.App/Actions/Navigation/DuplicateSelectedTabAction.cs index 550e2d16e798..003d0aade99d 100644 --- a/src/Files.App/Actions/Navigation/DuplicateSelectedTabAction.cs +++ b/src/Files.App/Actions/Navigation/DuplicateSelectedTabAction.cs @@ -31,7 +31,7 @@ public DuplicateSelectedTabAction() public async Task ExecuteAsync(object? parameter = null) { - var arguments = context.SelectedTabItem.NavigationParameter; + var arguments = context.SelectedTabItem?.NavigationParameter; if (arguments is null) { diff --git a/src/Files.App/Actions/Navigation/FocusOtherPane.cs b/src/Files.App/Actions/Navigation/FocusOtherPane.cs index 1dfeebde9c9d..31d09784483b 100644 --- a/src/Files.App/Actions/Navigation/FocusOtherPane.cs +++ b/src/Files.App/Actions/Navigation/FocusOtherPane.cs @@ -32,7 +32,7 @@ public FocusOtherPaneAction() public Task ExecuteAsync(object? parameter = null) { - context.ShellPage!.PaneHolder.FocusOtherPane(); + context.ShellPage?.PaneHolder?.FocusOtherPane(); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/NextTabAction.cs b/src/Files.App/Actions/Navigation/NextTabAction.cs index 585169ff3c2b..050ea7139979 100644 --- a/src/Files.App/Actions/Navigation/NextTabAction.cs +++ b/src/Files.App/Actions/Navigation/NextTabAction.cs @@ -42,7 +42,7 @@ public async Task ExecuteAsync(object? parameter = null) await Task.Delay(500); // Focus the content of the selected tab item (needed for keyboard navigation) - contentPageContext.ShellPage!.PaneHolder.FocusActivePane(); + contentPageContext.ShellPage?.PaneHolder?.FocusActivePane(); } private void MultitaskingContext_PropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Files.App/Actions/Navigation/OpenInNewPane/BaseOpenInNewPaneAction.cs b/src/Files.App/Actions/Navigation/OpenInNewPane/BaseOpenInNewPaneAction.cs index fa92bbae1581..8f92fe5e93cb 100644 --- a/src/Files.App/Actions/Navigation/OpenInNewPane/BaseOpenInNewPaneAction.cs +++ b/src/Files.App/Actions/Navigation/OpenInNewPane/BaseOpenInNewPaneAction.cs @@ -36,9 +36,13 @@ public BaseOpenInNewPaneAction() public virtual Task ExecuteAsync(object? parameter = null) { + if (ContentPageContext.ShellPage is not { } shellPage || + ContentPageContext.SelectedItem is not { } selectedItem) + return Task.CompletedTask; + NavigationHelpers.OpenInSecondaryPane( - ContentPageContext.ShellPage, - ContentPageContext.ShellPage.SlimContentPage.SelectedItems.FirstOrDefault(), + shellPage, + selectedItem, parameter as ShellPaneArrangement? ?? ShellPaneArrangement.None); return Task.CompletedTask; diff --git a/src/Files.App/Actions/Navigation/PreviousTabAction.cs b/src/Files.App/Actions/Navigation/PreviousTabAction.cs index f0ff278c4483..f3b4c2cdbeca 100644 --- a/src/Files.App/Actions/Navigation/PreviousTabAction.cs +++ b/src/Files.App/Actions/Navigation/PreviousTabAction.cs @@ -45,7 +45,7 @@ public async Task ExecuteAsync(object? parameter = null) await Task.Delay(500); // Focus the content of the selected tab item (needed for keyboard navigation) - contentPageContext.ShellPage!.PaneHolder.FocusActivePane(); + contentPageContext.ShellPage?.PaneHolder?.FocusActivePane(); } private void MultitaskingContext_PropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Files.App/Actions/Navigation/SplitPaneHorizontallyAction.cs b/src/Files.App/Actions/Navigation/SplitPaneHorizontallyAction.cs index 5e1f39cdf295..4b52b0d408ef 100644 --- a/src/Files.App/Actions/Navigation/SplitPaneHorizontallyAction.cs +++ b/src/Files.App/Actions/Navigation/SplitPaneHorizontallyAction.cs @@ -34,7 +34,8 @@ public SplitPaneHorizontallyAction() public Task ExecuteAsync(object? parameter = null) { - ContentPageContext.ShellPage!.PaneHolder.OpenSecondaryPane(ContentPageContext.ShellPage!.ShellViewModel.WorkingDirectory, ShellPaneArrangement.Horizontal); + if (ContentPageContext.ShellPage is { PaneHolder: { } paneHolder, ShellViewModel: { } shellViewModel }) + paneHolder.OpenSecondaryPane(shellViewModel.WorkingDirectory, ShellPaneArrangement.Horizontal); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Navigation/SplitPaneVerticallyAction.cs b/src/Files.App/Actions/Navigation/SplitPaneVerticallyAction.cs index b00e481df066..25b80d80ad7d 100644 --- a/src/Files.App/Actions/Navigation/SplitPaneVerticallyAction.cs +++ b/src/Files.App/Actions/Navigation/SplitPaneVerticallyAction.cs @@ -34,7 +34,8 @@ public SplitPaneVerticallyAction() public Task ExecuteAsync(object? parameter = null) { - ContentPageContext.ShellPage!.PaneHolder.OpenSecondaryPane(ContentPageContext.ShellPage!.ShellViewModel.WorkingDirectory, ShellPaneArrangement.Vertical); + if (ContentPageContext.ShellPage is { PaneHolder: { } paneHolder, ShellViewModel: { } shellViewModel }) + paneHolder.OpenSecondaryPane(shellViewModel.WorkingDirectory, ShellPaneArrangement.Vertical); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Open/OpenInIDEAction.cs b/src/Files.App/Actions/Open/OpenInIDEAction.cs index 9c461c1ae737..7e403040272b 100644 --- a/src/Files.App/Actions/Open/OpenInIDEAction.cs +++ b/src/Files.App/Actions/Open/OpenInIDEAction.cs @@ -37,8 +37,11 @@ public OpenInIDEAction() public async Task ExecuteAsync(object? parameter = null) { + if (_context.ShellPage?.ShellViewModel?.WorkingDirectory is not { } workingDirectory) + return; + var res = await Win32Helper.RunPowershellCommandAsync( - $"& \'{_devToolsSettingsService.IDEPath}\' \'{_context.ShellPage?.ShellViewModel.WorkingDirectory}\'", + $"& \'{_devToolsSettingsService.IDEPath}\' \'{workingDirectory}\'", PowerShellExecutionOptions.Hidden ); diff --git a/src/Files.App/Actions/Open/OpenTerminalFromSidebarAction.cs b/src/Files.App/Actions/Open/OpenTerminalFromSidebarAction.cs index 1f8664c6861a..29ab10a63959 100644 --- a/src/Files.App/Actions/Open/OpenTerminalFromSidebarAction.cs +++ b/src/Files.App/Actions/Open/OpenTerminalFromSidebarAction.cs @@ -17,6 +17,7 @@ public override string Description public override bool IsExecutable => SidebarContext.IsItemRightClicked && SidebarContext.RightClickedItem is not null && + !string.IsNullOrEmpty(SidebarContext.RightClickedItem.Path) && SidebarContext.RightClickedItem.MenuOptions.ShowShellItems && !SidebarContext.RightClickedItem.MenuOptions.ShowEmptyRecycleBin; @@ -28,8 +29,11 @@ public override HotKey HotKey protected override string[] GetPaths() { - if (SidebarContext.IsItemRightClicked && SidebarContext.RightClickedItem is not null) - return [SidebarContext.RightClickedItem.Path]; + if (SidebarContext.IsItemRightClicked && + SidebarContext.RightClickedItem?.Path is { Length: > 0 } path) + { + return [path]; + } return []; } diff --git a/src/Files.App/Actions/Shelf/DeleteItemFromShelfAction.cs b/src/Files.App/Actions/Shelf/DeleteItemFromShelfAction.cs index eebb415975a1..e60073a069d4 100644 --- a/src/Files.App/Actions/Shelf/DeleteItemFromShelfAction.cs +++ b/src/Files.App/Actions/Shelf/DeleteItemFromShelfAction.cs @@ -39,7 +39,7 @@ public DeleteItemFromShelfAction() public async Task ExecuteAsync(object? parameter = null) { - if (contentPageContext.ShellPage is not { } shellPage) + if (contentPageContext.ShellPage is not { ShellViewModel: { } shellViewModel } shellPage) return; var itemsToDelete = shelfContext.SelectedItems.Select(x => StorageHelpers.FromPathAndType(x.Inner.Id, x.Inner switch @@ -50,7 +50,7 @@ public async Task ExecuteAsync(object? parameter = null) })); await shellPage.FilesystemHelpers.DeleteItemsAsync(itemsToDelete, foldersSettingsService.DeleteConfirmationPolicy, false, true); - await shellPage.ShellViewModel.ApplyFilesAndFoldersChangesAsync(); + await shellViewModel.ApplyFilesAndFoldersChangesAsync(); } private void ShelfContext_PropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Files.App/Actions/Show/ToggleDualPaneAction.cs b/src/Files.App/Actions/Show/ToggleDualPaneAction.cs index 3a327aa631ab..bad8a0fd2dd3 100644 --- a/src/Files.App/Actions/Show/ToggleDualPaneAction.cs +++ b/src/Files.App/Actions/Show/ToggleDualPaneAction.cs @@ -32,9 +32,9 @@ public ToggleDualPaneAction() public Task ExecuteAsync(object? parameter = null) { if (IsOn) - ContentPageContext.ShellPage?.PaneHolder.CloseOtherPane(); - else - ContentPageContext.ShellPage?.PaneHolder.OpenSecondaryPane(ContentPageContext.ShellPage!.ShellViewModel.WorkingDirectory, generalSettingsService.ShellPaneArrangementOption); + ContentPageContext.ShellPage?.PaneHolder?.CloseOtherPane(); + else if (ContentPageContext.ShellPage is { PaneHolder: { } paneHolder, ShellViewModel: { } shellViewModel }) + paneHolder.OpenSecondaryPane(shellViewModel.WorkingDirectory, generalSettingsService.ShellPaneArrangementOption); return Task.CompletedTask; } diff --git a/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs b/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs index ef39911eea13..79afbe800930 100644 --- a/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs +++ b/src/Files.App/Actions/Show/ToggleFilterHeaderAction.cs @@ -37,12 +37,12 @@ public Task ExecuteAsync(object? parameter = null) generalSettingsService.ShowFilterHeader = !IsOn; // Only attempt to focus if there's an active shell page - if (ContentPageContext.ShellPage is not null) + if (ContentPageContext.ShellPage is { } shellPage) { if (IsOn) - ContentPageContext.ShellPage.ShellViewModel.InvokeFocusFilterHeader(); + shellPage.ShellViewModel?.InvokeFocusFilterHeader(); else - ContentPageContext.ShellPage.PaneHolder.FocusActivePane(); + shellPage.PaneHolder?.FocusActivePane(); } return Task.CompletedTask; diff --git a/src/Files.App/Actions/Start/UnpinFromStartAction.cs b/src/Files.App/Actions/Start/UnpinFromStartAction.cs index ede4883f561e..777e21d17454 100644 --- a/src/Files.App/Actions/Start/UnpinFromStartAction.cs +++ b/src/Files.App/Actions/Start/UnpinFromStartAction.cs @@ -33,7 +33,7 @@ public async Task ExecuteAsync(object? parameter = null) { if (context.SelectedItems.Count > 0) { - foreach (ListedItem listedItem in context.ShellPage?.SlimContentPage.SelectedItems) + foreach (ListedItem listedItem in context.SelectedItems) { await SafetyExtensions.IgnoreExceptions(async () => { @@ -49,9 +49,12 @@ await SafetyExtensions.IgnoreExceptions(async () => } else { + var currentFolder = context.ShellPage?.ShellViewModel?.CurrentFolder; + if (currentFolder is null) + return; + await SafetyExtensions.IgnoreExceptions(async () => { - var currentFolder = context.ShellPage.ShellViewModel.CurrentFolder; IStorable storable = context.PageType is ContentPageTypes.ZipFolder ? await StorageService.GetFileAsync(currentFolder.ItemPath) : await StorageService.GetFolderAsync(currentFolder.ItemPath); diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index b5842a331998..eab6fad5aed0 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -175,7 +175,7 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() /// private void Window_Activated(object sender, WindowActivatedEventArgs args) { - Logger.LogInformation($"Window_Activated: State={args?.WindowActivationState.ToString()}"); + Logger.LogInformation($"Window_Activated: State={args.WindowActivationState}"); if (args.WindowActivationState != WindowActivationState.Deactivated) AppModel.IsMainWindowClosed = false; @@ -218,7 +218,7 @@ private async void Window_Closed(object sender, WindowEventArgs args) if (OutputPath is not null) { - var instance = MainPageViewModel.AppInstances.FirstOrDefault(x => x.TabItemContent.IsCurrentInstance); + var instance = MainPageViewModel.AppInstances.FirstOrDefault(x => x.TabItemContent?.IsCurrentInstance == true); if (instance is null) return; diff --git a/src/Files.App/Converters/DateTimeOffsetToStringConverter.cs b/src/Files.App/Converters/DateTimeOffsetToStringConverter.cs index f919197467e0..8d90bb2cac9a 100644 --- a/src/Files.App/Converters/DateTimeOffsetToStringConverter.cs +++ b/src/Files.App/Converters/DateTimeOffsetToStringConverter.cs @@ -7,7 +7,7 @@ namespace Files.App.Converters { internal sealed partial class DateTimeOffsetToStringConverter : IValueConverter { - private static readonly IDateTimeFormatter formatter = Ioc.Default.GetService(); + private static readonly IDateTimeFormatter formatter = Ioc.Default.GetRequiredService(); public object Convert(object value, Type targetType, object parameter, string language) { @@ -16,16 +16,11 @@ public object Convert(object value, Type targetType, object parameter, string la : formatter.ToLongLabel((DateTimeOffset)value); } - public object ConvertBack(object value, Type targetType, object parameter, string language) + public object? ConvertBack(object value, Type targetType, object parameter, string language) { - try - { - return DateTimeOffset.Parse(value as string); - } - catch (FormatException) - { - return null; - } + return value is string text && DateTimeOffset.TryParse(text, out var result) + ? result + : null; } } } diff --git a/src/Files.App/Converters/DoubleArrayToStringConverter.cs b/src/Files.App/Converters/DoubleArrayToStringConverter.cs index 31953912be9d..dc9b65b9c590 100644 --- a/src/Files.App/Converters/DoubleArrayToStringConverter.cs +++ b/src/Files.App/Converters/DoubleArrayToStringConverter.cs @@ -27,7 +27,10 @@ public object Convert(object value, Type targetType, object parameter, string la public object ConvertBack(object value, Type targetType, object parameter, string language) { - var strArray = (value as string).Split("; "); + if (value is not string text) + return Array.Empty(); + + var strArray = text.Split("; "); var array = new double[strArray.Length]; for (int i = 0; i < strArray.Length; i++) diff --git a/src/Files.App/Converters/DoubleToStringConverter.cs b/src/Files.App/Converters/DoubleToStringConverter.cs index da2646ede853..6bc95093a125 100644 --- a/src/Files.App/Converters/DoubleToStringConverter.cs +++ b/src/Files.App/Converters/DoubleToStringConverter.cs @@ -11,22 +11,17 @@ public object Convert(object value, Type targetType, object parameter, string la { if (value is not null) { - return value.ToString(); + return value.ToString() ?? string.Empty; } return ""; } - public object ConvertBack(object value, Type targetType, object parameter, string language) + public object? ConvertBack(object value, Type targetType, object parameter, string language) { - try - { - return Double.Parse(value as string); - } - catch (FormatException) - { - return null; - } + return value is string text && double.TryParse(text, out var result) + ? result + : null; } } } diff --git a/src/Files.App/Converters/StringArrayToStringConverter.cs b/src/Files.App/Converters/StringArrayToStringConverter.cs index 85d25f6e96eb..2c286e917a02 100644 --- a/src/Files.App/Converters/StringArrayToStringConverter.cs +++ b/src/Files.App/Converters/StringArrayToStringConverter.cs @@ -26,7 +26,7 @@ public object Convert(object value, Type targetType, object parameter, string la public object ConvertBack(object value, Type targetType, object parameter, string language) { - return (value as string).Split("; "); + return value is string text ? text.Split("; ") : Array.Empty(); } } } diff --git a/src/Files.App/Converters/UInt32ToStringConverter.cs b/src/Files.App/Converters/UInt32ToStringConverter.cs index 51d5e3f723b4..7707acb1e741 100644 --- a/src/Files.App/Converters/UInt32ToStringConverter.cs +++ b/src/Files.App/Converters/UInt32ToStringConverter.cs @@ -9,19 +9,14 @@ internal sealed partial class UInt32ToStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - return value is not null ? value.ToString() : string.Empty; + return value?.ToString() ?? string.Empty; } - public object ConvertBack(object value, Type targetType, object parameter, string language) + public object? ConvertBack(object value, Type targetType, object parameter, string language) { - try - { - return uint.Parse(value as string); - } - catch (FormatException) - { - return null; - } + return value is string text && uint.TryParse(text, out var result) + ? result + : null; } } } diff --git a/src/Files.App/Data/Behaviors/StickyHeaderBehavior.cs b/src/Files.App/Data/Behaviors/StickyHeaderBehavior.cs index d41f144e626b..5b72fa5de2cb 100644 --- a/src/Files.App/Data/Behaviors/StickyHeaderBehavior.cs +++ b/src/Files.App/Data/Behaviors/StickyHeaderBehavior.cs @@ -62,9 +62,9 @@ public StickyHeaderBehavior() /// /// Set this using the header of a ListView or GridView. /// - public UIElement HeaderElement + public UIElement? HeaderElement { - get => (UIElement)GetValue(HeaderElementProperty); + get => GetValue(HeaderElementProperty) as UIElement; set => SetValue(HeaderElementProperty, value); } @@ -169,17 +169,14 @@ private bool AssignAnimation() _headerVisual.StartAnimation("Offset.Y", expressionAnimation); // Mod: clip items panel below header - var itemsPanel = listView.ItemsPanelRoot; + var itemsPanel = listView?.ItemsPanelRoot; if (itemsPanel is null) return true; - if (_itemsPanelVisual is null) - { - _itemsPanelVisual = ElementCompositionPreview.GetElementVisual(itemsPanel); - _contentClip = compositor.CreateInsetClip(); - _itemsPanelVisual.Clip = _contentClip; - } + _itemsPanelVisual ??= ElementCompositionPreview.GetElementVisual(itemsPanel); + _contentClip ??= compositor.CreateInsetClip(); + _itemsPanelVisual.Clip = _contentClip; var expressionClipAnimation = ExpressionFunctions.Max(-scrollPropSet.Translation.Y, 0); _contentClip.TopInset = (float)Math.Max(-_scrollViewer.VerticalOffset, 0); diff --git a/src/Files.App/Data/Contexts/MultiPanes/MultiPanesContext.cs b/src/Files.App/Data/Contexts/MultiPanes/MultiPanesContext.cs index 2df8d741b864..de4ffb3752a6 100644 --- a/src/Files.App/Data/Contexts/MultiPanes/MultiPanesContext.cs +++ b/src/Files.App/Data/Contexts/MultiPanes/MultiPanesContext.cs @@ -65,7 +65,7 @@ private void Page_PropertyChanged(object? sender, PropertyChangedEventArgs e) UpdateContent(); break; case nameof(IShellPanesPage.ShellPaneArrangement): - _ShellPaneArrangement = ActivePane?.PaneHolder.ShellPaneArrangement ?? ShellPaneArrangement.Horizontal; + _ShellPaneArrangement = ActivePane?.PaneHolder?.ShellPaneArrangement ?? ShellPaneArrangement.Horizontal; ShellPaneArrangementChanged?.Invoke(this, EventArgs.Empty); break; } @@ -106,7 +106,7 @@ private void UpdateContent() ActivePaneChanged?.Invoke(this, EventArgs.Empty); - _ShellPaneArrangement = ActivePane?.PaneHolder.ShellPaneArrangement ?? ShellPaneArrangement.Horizontal; + _ShellPaneArrangement = ActivePane?.PaneHolder?.ShellPaneArrangement ?? ShellPaneArrangement.Horizontal; ShellPaneArrangementChanged?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Files.App/Data/Contexts/Multitasking/IMultitaskingContext.cs b/src/Files.App/Data/Contexts/Multitasking/IMultitaskingContext.cs index d68068155de2..26147674c6a7 100644 --- a/src/Files.App/Data/Contexts/Multitasking/IMultitaskingContext.cs +++ b/src/Files.App/Data/Contexts/Multitasking/IMultitaskingContext.cs @@ -9,10 +9,10 @@ public interface IMultitaskingContext : INotifyPropertyChanged ushort TabCount { get; } - TabBarItem CurrentTabItem { get; } + TabBarItem? CurrentTabItem { get; } ushort CurrentTabIndex { get; } - TabBarItem SelectedTabItem { get; } + TabBarItem? SelectedTabItem { get; } ushort SelectedTabIndex { get; } } } diff --git a/src/Files.App/Data/Contexts/Multitasking/MultitaskingContext.cs b/src/Files.App/Data/Contexts/Multitasking/MultitaskingContext.cs index 15dfcb901c87..a946528b03aa 100644 --- a/src/Files.App/Data/Contexts/Multitasking/MultitaskingContext.cs +++ b/src/Files.App/Data/Contexts/Multitasking/MultitaskingContext.cs @@ -17,8 +17,8 @@ internal sealed partial class MultitaskingContext : ObservableObject, IMultitask private ushort tabCount = 0; public ushort TabCount => tabCount; - public TabBarItem CurrentTabItem => MainPageViewModel.AppInstances.ElementAtOrDefault(currentTabIndex); - public TabBarItem SelectedTabItem => MainPageViewModel.AppInstances.ElementAtOrDefault(selectedTabIndex); + public TabBarItem? CurrentTabItem => MainPageViewModel.AppInstances.ElementAtOrDefault(currentTabIndex); + public TabBarItem? SelectedTabItem => MainPageViewModel.AppInstances.ElementAtOrDefault(selectedTabIndex); private ushort currentTabIndex = 0; public ushort CurrentTabIndex => currentTabIndex; diff --git a/src/Files.App/Data/Contexts/Tags/TagsContext.cs b/src/Files.App/Data/Contexts/Tags/TagsContext.cs index 5ae3fe925207..e5c98ef2f477 100644 --- a/src/Files.App/Data/Contexts/Tags/TagsContext.cs +++ b/src/Files.App/Data/Contexts/Tags/TagsContext.cs @@ -32,7 +32,7 @@ public TagsContext() SidebarViewModel.SelectedTagChanged += SelectedTagsChanged; } - private void SelectedTagsChanged(object _, SelectedTagChangedEventArgs e) + private void SelectedTagsChanged(object? _, SelectedTagChangedEventArgs e) { TaggedItems = e.Items; } diff --git a/src/Files.App/Data/Contracts/IFileTagsSettingsService.cs b/src/Files.App/Data/Contracts/IFileTagsSettingsService.cs index 7a886a5e0eaa..da5c65f336b9 100644 --- a/src/Files.App/Data/Contracts/IFileTagsSettingsService.cs +++ b/src/Files.App/Data/Contracts/IFileTagsSettingsService.cs @@ -5,15 +5,15 @@ namespace Files.App.Data.Contracts { public interface IFileTagsSettingsService : IBaseSettingsService { - event EventHandler OnSettingImportedEvent; + event EventHandler? OnSettingImportedEvent; - event EventHandler OnTagsUpdated; + event EventHandler? OnTagsUpdated; IList FileTagList { get; set; } - TagViewModel GetTagById(string uid); + TagViewModel? GetTagById(string uid); - IList? GetTagsByIds(string[] uids); + IList? GetTagsByIds(string[]? uids); IEnumerable GetTagsByName(string tagName); @@ -27,4 +27,4 @@ public interface IFileTagsSettingsService : IBaseSettingsService bool ImportSettings(object import); } -} \ No newline at end of file +} diff --git a/src/Files.App/Data/Contracts/IGeneralSettingsService.cs b/src/Files.App/Data/Contracts/IGeneralSettingsService.cs index bdb7c6c45f73..5a9e4c510921 100644 --- a/src/Files.App/Data/Contracts/IGeneralSettingsService.cs +++ b/src/Files.App/Data/Contracts/IGeneralSettingsService.cs @@ -33,12 +33,12 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty /// /// A list containing all paths to open at startup. /// - List TabsOnStartupList { get; set; } + List? TabsOnStartupList { get; set; } /// /// A list containing all paths to tabs closed on last session. /// - List LastSessionTabList { get; set; } + List? LastSessionTabList { get; set; } /// /// Gets or sets the index of the tab that was active when the last session ended. @@ -48,22 +48,22 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty /// /// A list containing paths of the tabs from the previous session that crashed. /// - List LastCrashedTabList { get; set; } + List? LastCrashedTabList { get; set; } /// /// A list containing paths previously entered in the path bar. /// - List PathHistoryList { get; set; } + List? PathHistoryList { get; set; } /// /// A list containing previous search queries. /// - List PreviousSearchQueriesList { get; set; } + List? PreviousSearchQueriesList { get; set; } /// /// Stores list of paths where archives have previously been extracted. /// - List PreviousArchiveExtractionLocations { get; set; } + List? PreviousArchiveExtractionLocations { get; set; } /// /// Gets or sets a value indicating which date and time format to use. @@ -323,7 +323,7 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty /// /// A dictionary to determine which hashes should be shown. /// - Dictionary ShowHashesDictionary { get; set; } + Dictionary? ShowHashesDictionary { get; set; } /// /// Gets or sets a value indicating a random user ID. diff --git a/src/Files.App/Data/Contracts/IMultiPaneInfo.cs b/src/Files.App/Data/Contracts/IMultiPaneInfo.cs index 75f0cf29fa0e..58cd72684caf 100644 --- a/src/Files.App/Data/Contracts/IMultiPaneInfo.cs +++ b/src/Files.App/Data/Contracts/IMultiPaneInfo.cs @@ -5,6 +5,6 @@ namespace Files.App.Data.Contracts { public interface IMultiPaneInfo { - public IShellPanesPage PaneHolder { get; } + public IShellPanesPage? PaneHolder { get; } } } diff --git a/src/Files.App/Data/Contracts/IShellPage.cs b/src/Files.App/Data/Contracts/IShellPage.cs index b75626883d1a..ca1552428745 100644 --- a/src/Files.App/Data/Contracts/IShellPage.cs +++ b/src/Files.App/Data/Contracts/IShellPage.cs @@ -7,7 +7,7 @@ namespace Files.App.Data.Contracts { public interface IShellPage : ITabBarItemContent, IMultiPaneInfo, IDisposable, INotifyPropertyChanged { - ShellViewModel ShellViewModel { get; } + ShellViewModel? ShellViewModel { get; } CurrentInstanceViewModel InstanceViewModel { get; } @@ -17,7 +17,7 @@ public interface IShellPage : ITabBarItemContent, IMultiPaneInfo, IDisposable, I IList BackwardStack { get; } - IBaseLayoutPage SlimContentPage { get; } + IBaseLayoutPage? SlimContentPage { get; } Type CurrentPageType { get; } @@ -50,16 +50,16 @@ public interface IShellPage : ITabBarItemContent, IMultiPaneInfo, IDisposable, I void Up_Click(); - Task UpdatePathUIToWorkingDirectoryAsync(string newWorkingDir, string singleItemOverride = null); + Task UpdatePathUIToWorkingDirectoryAsync(string? newWorkingDir, string? singleItemOverride = null); - void NavigateToPath(string navigationPath, Type sourcePageType, NavigationArguments navArgs = null); + void NavigateToPath(string? navigationPath, Type? sourcePageType, NavigationArguments? navArgs = null); /// /// Gets the layout mode for the specified path then navigates to it /// /// /// - public void NavigateToPath(string navigationPath, NavigationArguments navArgs = null); + public void NavigateToPath(string navigationPath, NavigationArguments? navArgs = null); /// /// Navigates to the home page diff --git a/src/Files.App/Data/Contracts/IVersionControlService.cs b/src/Files.App/Data/Contracts/IVersionControlService.cs index 3236b56b5c46..5b3ed96fd18d 100644 --- a/src/Files.App/Data/Contracts/IVersionControlService.cs +++ b/src/Files.App/Data/Contracts/IVersionControlService.cs @@ -26,7 +26,7 @@ public interface IVersionControlService /// This method is used for determining whether a directory is within a repository and, if so, /// which directory should be treated as the repository root. /// - string? GetGitRepositoryPath(string? path, string root); + string? GetGitRepositoryPath(string? path, string? root); /// /// Gets the repository name. diff --git a/src/Files.App/Data/Contracts/IWidgetCardItem.cs b/src/Files.App/Data/Contracts/IWidgetCardItem.cs index 7696304c8088..b895d5504da5 100644 --- a/src/Files.App/Data/Contracts/IWidgetCardItem.cs +++ b/src/Files.App/Data/Contracts/IWidgetCardItem.cs @@ -9,7 +9,7 @@ public interface IWidgetCardItem { T Item { get; } - BitmapImage Thumbnail { get; } + BitmapImage? Thumbnail { get; } Task LoadCardThumbnailAsync(); } diff --git a/src/Files.App/Data/Contracts/ImagingService.cs b/src/Files.App/Data/Contracts/ImagingService.cs index 8a19fc207012..7e8f4c095671 100644 --- a/src/Files.App/Data/Contracts/ImagingService.cs +++ b/src/Files.App/Data/Contracts/ImagingService.cs @@ -22,7 +22,11 @@ internal sealed class ImagingService : IImageService public async Task GetImageModelFromDataAsync(byte[]? rawData) { - return new BitmapImageModel(await BitmapHelper.ToBitmapAsync(rawData)); + if (rawData is null) + return null; + + var bitmapImage = await BitmapHelper.ToBitmapAsync(rawData); + return bitmapImage is null ? null : new BitmapImageModel(bitmapImage); } public async Task GetImageModelFromPathAsync(string filePath, uint thumbnailSize = 64) diff --git a/src/Files.App/Data/EventArguments/CurrentInstanceChangedEventArgs.cs b/src/Files.App/Data/EventArguments/CurrentInstanceChangedEventArgs.cs index b4a2154d6ee3..56034716f72f 100644 --- a/src/Files.App/Data/EventArguments/CurrentInstanceChangedEventArgs.cs +++ b/src/Files.App/Data/EventArguments/CurrentInstanceChangedEventArgs.cs @@ -5,8 +5,8 @@ namespace Files.App.Data.EventArguments { public sealed class CurrentInstanceChangedEventArgs : EventArgs { - public ITabBarItemContent CurrentInstance { get; set; } + public required ITabBarItemContent CurrentInstance { get; set; } - public List PageInstances { get; set; } + public required List PageInstances { get; set; } } } diff --git a/src/Files.App/Data/EventArguments/PathBoxItemDroppedEventArgs.cs b/src/Files.App/Data/EventArguments/PathBoxItemDroppedEventArgs.cs index e9b2c2c71439..d935296c4975 100644 --- a/src/Files.App/Data/EventArguments/PathBoxItemDroppedEventArgs.cs +++ b/src/Files.App/Data/EventArguments/PathBoxItemDroppedEventArgs.cs @@ -8,12 +8,12 @@ namespace Files.App.Data.EventArguments { public sealed class PathBoxItemDroppedEventArgs { - public DataPackageView Package { get; set; } + public required DataPackageView Package { get; set; } - public string Path { get; set; } + public required string Path { get; set; } public DataPackageOperation AcceptedOperation { get; set; } - public AsyncManualResetEvent SignalEvent { get; set; } + public required AsyncManualResetEvent SignalEvent { get; set; } } } diff --git a/src/Files.App/Data/EventArguments/PathNavigationEventArgs.cs b/src/Files.App/Data/EventArguments/PathNavigationEventArgs.cs index f44bd27aeb46..712a14eecb49 100644 --- a/src/Files.App/Data/EventArguments/PathNavigationEventArgs.cs +++ b/src/Files.App/Data/EventArguments/PathNavigationEventArgs.cs @@ -5,9 +5,9 @@ namespace Files.App.Data.EventArguments { public sealed class PathNavigationEventArgs { - public string ItemPath { get; set; } + public required string ItemPath { get; set; } - public string ItemName { get; set; } + public string? ItemName { get; set; } public bool IsFile { get; set; } } diff --git a/src/Files.App/Data/EventArguments/ToolbarQuerySubmittedEventArgs.cs b/src/Files.App/Data/EventArguments/ToolbarQuerySubmittedEventArgs.cs index 8e83c9b4f6f9..94d0fdc1be58 100644 --- a/src/Files.App/Data/EventArguments/ToolbarQuerySubmittedEventArgs.cs +++ b/src/Files.App/Data/EventArguments/ToolbarQuerySubmittedEventArgs.cs @@ -5,6 +5,6 @@ namespace Files.App.Data.EventArguments { public sealed class ToolbarQuerySubmittedEventArgs { - public string QueryText { get; set; } = null; + public required string QueryText { get; init; } } } diff --git a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs index 63eeac377579..b54cdeb3942b 100644 --- a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs +++ b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs @@ -31,7 +31,7 @@ public static List GetItemContextCommandsWithout return menuItemsList; } - public static Task> GetItemContextShellCommandsAsync(string workingDir, List selectedItems, bool shiftPressed, bool showOpenMenu, CancellationToken cancellationToken) + public static Task> GetItemContextShellCommandsAsync(string? workingDir, List selectedItems, bool shiftPressed, bool showOpenMenu, CancellationToken cancellationToken) { return ShellContextFlyoutFactory.GetShellContextmenuAsync(shiftPressed: shiftPressed, showOpenMenu: showOpenMenu, workingDirectory: workingDir, selectedItems: selectedItems, cancellationToken: cancellationToken); } @@ -44,20 +44,22 @@ public static List Filter(List x.ID == "ItemOverflow"); if (overflow is not null) { + var overflowMenuItems = overflow.Items ??= []; + if (!shiftPressed && UserSettingsService.GeneralSettingsService.MoveShellExtensionsToSubMenu) // items with ShowOnShift to overflow menu { var overflowItems = items.Where(x => x.ShowOnShift).ToList(); // Adds a separator between items already there and the new ones - if (overflow.Items.Count != 0 && overflowItems.Count > 0 && overflow.Items.Last().ItemType != ContextMenuFlyoutItemType.Separator) - overflow.Items.Add(new ContextMenuFlyoutItemViewModel { ItemType = ContextMenuFlyoutItemType.Separator }); + if (overflowMenuItems.Count != 0 && overflowItems.Count > 0 && overflowMenuItems.Last().ItemType != ContextMenuFlyoutItemType.Separator) + overflowMenuItems.Add(new ContextMenuFlyoutItemViewModel { ItemType = ContextMenuFlyoutItemType.Separator }); items = items.Except(overflowItems).ToList(); - overflow.Items.AddRange(overflowItems); + overflowMenuItems.AddRange(overflowItems); } // remove the overflow if it has no child items - if (overflow.Items.Count == 0 && removeOverflowMenu) + if (overflowMenuItems.Count == 0 && removeOverflowMenu) items.Remove(overflow); } diff --git a/src/Files.App/Data/Factories/ShellContextFlyoutHelper.cs b/src/Files.App/Data/Factories/ShellContextFlyoutHelper.cs index 7250378ea1e2..5e9becf38bca 100644 --- a/src/Files.App/Data/Factories/ShellContextFlyoutHelper.cs +++ b/src/Files.App/Data/Factories/ShellContextFlyoutHelper.cs @@ -72,15 +72,16 @@ private static ContextMenuFlyoutItemViewModel CreateShellMenuItem(Win32ContextMe }; } - public static async Task> GetShellContextmenuAsync(bool showOpenMenu, bool shiftPressed, string workingDirectory, List? selectedItems, CancellationToken cancellationToken) + public static async Task> GetShellContextmenuAsync(bool showOpenMenu, bool shiftPressed, string? workingDirectory, List? selectedItems, CancellationToken cancellationToken) { - bool IsItemSelected = selectedItems?.Count > 0; - var menuItemsList = new List(); - - var filePaths = IsItemSelected - ? selectedItems!.Select(x => x.ItemPath).ToArray() - : new[] { workingDirectory }; + string[] filePaths; + if (selectedItems is { Count: > 0 }) + filePaths = selectedItems.Select(x => x.ItemPath).ToArray(); + else if (!string.IsNullOrEmpty(workingDirectory)) + filePaths = [workingDirectory]; + else + return menuItemsList; Func FilterMenuItems(bool showOpenMenu) { @@ -142,12 +143,14 @@ private static void LoadMenuFlyoutItem( { Text = Strings.ShowMoreOptions.GetLocalizedResource(), Glyph = "\xE712", + Items = [], }; LoadMenuFlyoutItem(menuLayoutSubItem.Items, contextMenu, overflowItems, cancellationToken, showIcons); menuItemsListLocal.Insert(0, menuLayoutSubItem); } else { + moreItem.Items ??= []; LoadMenuFlyoutItem(moreItem.Items, contextMenu, overflowItems, cancellationToken, showIcons); } } @@ -421,7 +424,11 @@ x.Tag is Win32ContextMenuItem menuItem && var itemsWithSubMenu = shellMenuItems.Where(x => x.LoadSubMenuAction is not null); var subMenuTasks = itemsWithSubMenu.Select(async item => { - await item.LoadSubMenuAction(); + var loadSubMenuAction = item.LoadSubMenuAction; + if (loadSubMenuAction is null) + return; + + await loadSubMenuAction(); if (!UserSettingsService.GeneralSettingsService.MoveShellExtensionsToSubMenu) { AddItemsToMainMenu(itemContextMenuFlyout.SecondaryCommands, item); @@ -459,7 +466,7 @@ public static void AddItemsToOverflowMenu(AppBarButton? overflowItem, ContextMen var flyoutSubItem = flyout.Items.FirstOrDefault(x => x.Tag == viewModel.Tag) as MenuFlyoutSubItem; if (flyoutSubItem is not null) { - viewModel.Items.ForEach(i => flyoutSubItem.Items.Add(ContextFlyoutModelToElementHelper.GetMenuItem(i))); + viewModel.Items?.ForEach(i => flyoutSubItem.Items.Add(ContextFlyoutModelToElementHelper.GetMenuItem(i))); flyout.Items[flyout.Items.IndexOf(flyoutSubItem) + 1].Visibility = Visibility.Collapsed; flyoutSubItem.Visibility = Visibility.Visible; } diff --git a/src/Files.App/Data/Items/AccessMaskItem.cs b/src/Files.App/Data/Items/AccessMaskItem.cs index 8f5e1853cda6..074baa927e63 100644 --- a/src/Files.App/Data/Items/AccessMaskItem.cs +++ b/src/Files.App/Data/Items/AccessMaskItem.cs @@ -8,7 +8,7 @@ namespace Files.App.Data.Items /// public sealed partial class AccessMaskItem : ObservableObject { - public string AccessMaskName { get; set; } + public required string AccessMaskName { get; set; } public AccessMaskFlags AccessMask { get; set; } diff --git a/src/Files.App/Data/Items/AppThemeResourceItem.cs b/src/Files.App/Data/Items/AppThemeResourceItem.cs index ee9023017aa9..73454a9023dc 100644 --- a/src/Files.App/Data/Items/AppThemeResourceItem.cs +++ b/src/Files.App/Data/Items/AppThemeResourceItem.cs @@ -5,8 +5,8 @@ namespace Files.App.Data.Items { public sealed class AppThemeResourceItem { - public string? Name { get; set; } + public required string Name { get; set; } - public string? BackgroundColor { get; set; } + public required string BackgroundColor { get; set; } } } diff --git a/src/Files.App/Data/Items/ContextMenu.cs b/src/Files.App/Data/Items/ContextMenu.cs index 36532585196e..c7e556be6074 100644 --- a/src/Files.App/Data/Items/ContextMenu.cs +++ b/src/Files.App/Data/Items/ContextMenu.cs @@ -22,16 +22,16 @@ public enum HBITMAP_HMENU : long public class Win32ContextMenu { - public List Items { get; set; } + public List Items { get; set; } = []; } public class Win32ContextMenuItem { - public byte[] Icon { get; set; } + public byte[]? Icon { get; set; } public int ID { get; set; } // Valid only in current menu to invoke item - public string Label { get; set; } - public string CommandString { get; set; } + public string? Label { get; set; } + public string? CommandString { get; set; } public MENU_ITEM_TYPE Type { get; set; } - public List SubItems { get; set; } + public List? SubItems { get; set; } } } diff --git a/src/Files.App/Data/Items/DriveItem.cs b/src/Files.App/Data/Items/DriveItem.cs index 0b6b73c4041a..124cdc0ed560 100644 --- a/src/Files.App/Data/Items/DriveItem.cs +++ b/src/Files.App/Data/Items/DriveItem.cs @@ -14,8 +14,8 @@ namespace Files.App.Data.Items { public sealed partial class DriveItem : ExpandableSidebarItemBase, INavigationControlItem, IFolder, IExpandableSidebarFolder { - private BitmapImage icon; - public BitmapImage Icon + private BitmapImage? icon; + public BitmapImage? Icon { get => icon; set @@ -25,18 +25,18 @@ public BitmapImage Icon } } - public byte[] IconData { get; set; } + public byte[]? IconData { get; set; } - private string path; + private string path = string.Empty; public string Path { get => path; set => path = value; } - public string DeviceID { get; set; } + public string DeviceID { get; set; } = string.Empty; - public StorageFolder Root { get; set; } + public StorageFolder? Root { get; set; } public NavigationControlItemType ItemType { get; set; } = NavigationControlItemType.Drive; @@ -137,14 +137,14 @@ public string Filesystem set => SetProperty(ref filesystem, value); } - private string text; + private string text = string.Empty; public string Text { get => text; set => SetProperty(ref text, value); } - private string spaceText; + private string spaceText = string.Empty; public string SpaceText { get => spaceText; @@ -153,7 +153,7 @@ public string SpaceText public SectionType Section { get; set; } - public ContextMenuOptions MenuOptions { get; set; } + public ContextMenuOptions MenuOptions { get; set; } = new(); private float percentageUsed = 0.0f; public float PercentageUsed @@ -177,7 +177,7 @@ public bool ShowStorageSense } public string Id => Path; - public string Name => Root.DisplayName; + public string Name => Root?.DisplayName ?? Text; public object? Children => IsExpandableFolder ? (childItems ??= []) : null; private BulkConcurrentObservableCollection? childItems; @@ -235,7 +235,7 @@ private void ItemDecorator_Click(object sender, RoutedEventArgs e) DriveHelpers.EjectDeviceAsync(Path); } - public static async Task CreateFromPropertiesAsync(StorageFolder root, string deviceId, string label, DriveType type, IRandomAccessStream imageStream = null) + public static async Task CreateFromPropertiesAsync(StorageFolder root, string deviceId, string label, DriveType type, IRandomAccessStream? imageStream = null) { var item = new DriveItem(); @@ -266,11 +266,15 @@ public static async Task CreateFromPropertiesAsync(StorageFolder root public async Task UpdateLabelAsync() { + if (Root is not { } root) + return; + try { - var properties = await Root.Properties.RetrievePropertiesAsync(["System.ItemNameDisplay"]) + var properties = await root.Properties.RetrievePropertiesAsync(["System.ItemNameDisplay"]) .AsTask().WithTimeoutAsync(TimeSpan.FromSeconds(5)); - Text = (string)properties["System.ItemNameDisplay"]; + if (properties?.TryGetValue("System.ItemNameDisplay", out var value) == true && value is string displayName) + Text = displayName; } catch (NullReferenceException) { @@ -305,6 +309,9 @@ public async Task UpdatePropertiesAsync() catch { } } + if (Root is null) + return; + var properties = await Root.Properties.RetrievePropertiesAsync(["System.FreeSpace", "System.Capacity", "System.Volume.FileSystem"]) .AsTask().WithTimeoutAsync(TimeSpan.FromSeconds(5)); @@ -350,6 +357,9 @@ public async IAsyncEnumerable GetItemsAsync(StorableType storabl public int CompareTo(INavigationControlItem? other) { + if (other is null) + return 1; + var result = Type.CompareTo((other as DriveItem)?.Type ?? Type); return result == 0 ? Text.CompareTo(other.Text) : result; } diff --git a/src/Files.App/Data/Items/FileTagItem.cs b/src/Files.App/Data/Items/FileTagItem.cs index 3171a7e28ce4..20f80c4113b2 100644 --- a/src/Files.App/Data/Items/FileTagItem.cs +++ b/src/Files.App/Data/Items/FileTagItem.cs @@ -11,10 +11,10 @@ namespace Files.App.Data.Items { public sealed partial class FileTagItem : ObservableObject, INavigationControlItem { - public string Text { get; set; } + public required string Text { get; set; } - private string path; - public string Path + private string path = string.Empty; + public required string Path { get => path; set @@ -25,19 +25,19 @@ public string Path } } - public string ToolTipText { get; private set; } + public string ToolTipText { get; private set; } = string.Empty; public SectionType Section { get; set; } - public ContextMenuOptions MenuOptions { get; set; } + public required ContextMenuOptions MenuOptions { get; set; } public NavigationControlItemType ItemType => NavigationControlItemType.FileTag; public int CompareTo(INavigationControlItem? other) - => Text.CompareTo(other.Text); + => other is null ? 1 : Text.CompareTo(other.Text); - public TagViewModel FileTag { get; set; } + public required TagViewModel FileTag { get; set; } public object? Children => null; diff --git a/src/Files.App/Data/Items/IListedItem.cs b/src/Files.App/Data/Items/IListedItem.cs index 3864f34f5bbd..d79b18608439 100644 --- a/src/Files.App/Data/Items/IListedItem.cs +++ b/src/Files.App/Data/Items/IListedItem.cs @@ -3,31 +3,33 @@ using Files.App.ViewModels.Properties; using Microsoft.UI.Xaml.Media.Imaging; +using System.Diagnostics.CodeAnalysis; using Windows.Storage; namespace Files.App.Utils { public interface IListedItem { - GitItem AsGitItem { get; } - RecycleBinItem AsRecycleBinItem { get; } + GitItem? AsGitItem { get; } + RecycleBinItem? AsRecycleBinItem { get; } bool ContainsFilesOrFolders { get; set; } string ContextualProperty { get; set; } - BitmapImage CustomIcon { get; set; } - Uri CustomIconSource { get; set; } + BitmapImage? CustomIcon { get; set; } + Uri? CustomIconSource { get; set; } ObservableCollection FileDetails { get; set; } string FileExtension { get; set; } ulong? FileFRN { get; set; } - BitmapImage FileImage { get; set; } + BitmapImage? FileImage { get; set; } string FileSize { get; set; } long FileSizeBytes { get; set; } string FileSizeDisplay { get; } - string[] FileTags { get; set; } + [DisallowNull] + string[]? FileTags { get; set; } IList? FileTagsUI { get; } string FileVersion { get; set; } - string FolderRelativeId { get; set; } + string? FolderRelativeId { get; set; } bool HasTags { get; set; } - BitmapImage IconOverlay { get; set; } + BitmapImage? IconOverlay { get; set; } string ImageDimensions { get; set; } bool IsAlternateStream { get; } bool IsArchive { get; } @@ -51,7 +53,7 @@ public interface IListedItem DateTimeOffset ItemDateCreatedReal { get; set; } string ItemDateModified { get; } DateTimeOffset ItemDateModifiedReal { get; set; } - BaseStorageFile ItemFile { get; set; } + BaseStorageFile? ItemFile { get; set; } string ItemNameRaw { get; set; } string ItemPath { get; set; } ObservableCollection ItemProperties { get; set; } @@ -67,7 +69,7 @@ public interface IListedItem bool NeedsPlaceholderGlyph { get; set; } double Opacity { get; set; } StorageItemTypes PrimaryItemAttribute { get; set; } - BitmapImage ShieldIcon { get; set; } + BitmapImage? ShieldIcon { get; set; } bool ShowDriveStorageDetails { get; set; } ByteSizeLib.ByteSize SpaceUsed { get; set; } string SyncStatusString { get; } @@ -76,4 +78,4 @@ public interface IListedItem string ToString(); void UpdateContainsFilesFolders(); } -} \ No newline at end of file +} diff --git a/src/Files.App/Data/Items/ListedItem.cs b/src/Files.App/Data/Items/ListedItem.cs index d3a94b71d753..774781ac7820 100644 --- a/src/Files.App/Data/Items/ListedItem.cs +++ b/src/Files.App/Data/Items/ListedItem.cs @@ -6,6 +6,7 @@ using FluentFTP; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media.Imaging; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using Windows.Storage; @@ -62,7 +63,7 @@ public string ItemTooltipText } } - public string FolderRelativeId { get; set; } + public string? FolderRelativeId { get; set; } public bool ContainsFilesOrFolders { get; set; } = true; @@ -89,8 +90,8 @@ public bool LoadCustomIcon // Note: Never attempt to call this from a secondary window or another thread, create a new instance from CustomIconSource instead // TODO: eventually we should remove this b/c it's not thread safe - private BitmapImage customIcon; - public BitmapImage CustomIcon + private BitmapImage? customIcon; + public BitmapImage? CustomIcon { get => customIcon; set @@ -102,8 +103,9 @@ public BitmapImage CustomIcon public ulong? FileFRN { get; set; } - private string[] fileTags = null!; - public string[] FileTags + private string[]? fileTags; + [DisallowNull] + public string[]? FileTags { get => fileTags; set @@ -112,8 +114,6 @@ public string[] FileTags var fileTagsInitialized = fileTags is not null; if (SetProperty(ref fileTags, value)) { - Debug.Assert(value != null); - // only set the tags if the file tags have been changed if (fileTagsInitialized) { @@ -122,7 +122,7 @@ public string[] FileTags FileTagsHelper.WriteFileTag(ItemPath, value); } - HasTags = !FileTags.IsEmpty(); + HasTags = !value.IsEmpty(); OnPropertyChanged(nameof(FileTagsUI)); } } @@ -133,8 +133,8 @@ public IList? FileTagsUI get => fileTagsSettingsService.GetTagsByIds(FileTags); } - private Uri customIconSource; - public Uri CustomIconSource + private Uri? customIconSource; + public Uri? CustomIconSource { get => customIconSource; set => SetProperty(ref customIconSource, value); @@ -176,8 +176,8 @@ public string SyncStatusString get => string.IsNullOrEmpty(SyncStatusUI?.SyncStatusString) ? Strings.CloudDriveSyncStatus_Unknown.GetLocalizedResource() : SyncStatusUI.SyncStatusString; } - private BitmapImage fileImage; - public BitmapImage FileImage + private BitmapImage? fileImage; + public BitmapImage? FileImage { get => fileImage; set @@ -195,8 +195,8 @@ public BitmapImage FileImage public bool IsItemPinnedToStart => StartMenuService.IsPinned((this as IShortcutItem)?.TargetPath ?? ItemPath); - private BitmapImage iconOverlay; - public BitmapImage IconOverlay + private BitmapImage? iconOverlay; + public BitmapImage? IconOverlay { get => iconOverlay; set @@ -208,8 +208,8 @@ public BitmapImage IconOverlay } } - private BitmapImage shieldIcon; - public BitmapImage ShieldIcon + private BitmapImage? shieldIcon; + public BitmapImage? ShieldIcon { get => shieldIcon; set @@ -221,14 +221,14 @@ public BitmapImage ShieldIcon } } - private string itemPath; + private string itemPath = string.Empty; public string ItemPath { get => itemPath; set => SetProperty(ref itemPath, value); } - private string itemNameRaw; + private string itemNameRaw = string.Empty; public string ItemNameRaw { get => itemNameRaw; @@ -257,7 +257,7 @@ public virtual string Name } } - private string itemType; + private string itemType = string.Empty; public string ItemType { get => itemType; @@ -270,9 +270,9 @@ public string ItemType } } - public string FileExtension { get; set; } + public string FileExtension { get; set; } = string.Empty; - private string fileSize; + private string fileSize = string.Empty; public string FileSize { get => fileSize; @@ -315,21 +315,21 @@ public bool ShowCalculatingText public long FileSizeBytes { get; set; } - private string itemDateModified; + private string itemDateModified = string.Empty; public string ItemDateModified { get => itemDateModified; private set => SetProperty(ref itemDateModified, value); } - private string itemDateCreated; + private string itemDateCreated = string.Empty; public string ItemDateCreated { get => itemDateCreated; private set => SetProperty(ref itemDateCreated, value); } - private string itemDateAccessed; + private string itemDateAccessed = string.Empty; public string ItemDateAccessed { get => itemDateAccessed; @@ -369,7 +369,7 @@ public DateTimeOffset ItemDateAccessedReal } } - private ObservableCollection itemProperties; + private ObservableCollection itemProperties = []; public ObservableCollection ItemProperties { get => itemProperties; @@ -399,21 +399,21 @@ public ByteSize SpaceUsed } - private string imageDimensions; + private string imageDimensions = string.Empty; public string ImageDimensions { get => imageDimensions; set => SetProperty(ref imageDimensions, value); } - private string fileVersion; + private string fileVersion = string.Empty; public string FileVersion { get => fileVersion; set => SetProperty(ref fileVersion, value); } - private string mediaDuration; + private string mediaDuration = string.Empty; public string MediaDuration { get => mediaDuration; @@ -423,7 +423,7 @@ public string MediaDuration /// /// Contextual property that changes based on the item type. /// - private string contextualProperty; + private string contextualProperty = string.Empty; public string ContextualProperty { get => contextualProperty; @@ -434,12 +434,12 @@ public string ContextualProperty /// Initializes a new instance of the class. /// /// - public ListedItem(string folderRelativeId) => FolderRelativeId = folderRelativeId; + public ListedItem(string? folderRelativeId) => FolderRelativeId = folderRelativeId; // Parameterless constructor for JsonConvert public ListedItem() { } - private ObservableCollection fileDetails; + private ObservableCollection fileDetails = []; public ObservableCollection FileDetails { get => fileDetails; @@ -484,19 +484,19 @@ public override string ToString() public bool IsDriveRoot => ItemPath == PathNormalization.GetPathRoot(ItemPath); public bool IsElevationRequired { get; set; } - private BaseStorageFile itemFile; - public BaseStorageFile ItemFile + private BaseStorageFile? itemFile; + public BaseStorageFile? ItemFile { get => itemFile; set => SetProperty(ref itemFile, value); } // This is a hack used because x:Bind casting did not work properly - public RecycleBinItem AsRecycleBinItem => this as RecycleBinItem; + public RecycleBinItem? AsRecycleBinItem => this as RecycleBinItem; - public GitItem AsGitItem => this as GitItem; + public GitItem? AsGitItem => this as GitItem; - public string Key { get; set; } + public string Key { get; set; } = string.Empty; public virtual bool IsRealChanges => ItemDateAccessed != dateTimeFormatter.ToShortLabel(ItemDateAccessedReal) || ItemDateCreated != dateTimeFormatter.ToShortLabel(ItemDateCreatedReal) @@ -521,11 +521,11 @@ public void UpdateContainsFilesFolders() public sealed partial class RecycleBinItem : ListedItem { - public RecycleBinItem(string folderRelativeId) : base(folderRelativeId) + public RecycleBinItem(string? folderRelativeId) : base(folderRelativeId) { } - private string itemDateDeleted; + private string itemDateDeleted = string.Empty; public string ItemDateDeleted { get => itemDateDeleted; @@ -559,10 +559,10 @@ public override void UpdateReal() private DateTimeOffset itemDateDeletedReal; // For recycle bin elements (path + name) - public string ItemOriginalPath { get; set; } + public string ItemOriginalPath { get; set; } = string.Empty; // For recycle bin elements (path) - public string ItemOriginalFolder => Path.IsPathRooted(ItemOriginalPath) ? Path.GetDirectoryName(ItemOriginalPath) : ItemOriginalPath; + public string ItemOriginalFolder => Path.IsPathRooted(ItemOriginalPath) ? Path.GetDirectoryName(ItemOriginalPath) ?? ItemOriginalPath : ItemOriginalPath; public string ItemOriginalFolderName => Path.GetFileName(ItemOriginalFolder); } @@ -590,7 +590,7 @@ public FtpItem(FtpListItem item, string folder) : base(null) FileSizeBytes = item.Size; ContainsFilesOrFolders = !isFile; FileImage = null; - FileSize = isFile ? FileSizeBytes.ToSizeString() : null; + FileSize = isFile ? FileSizeBytes.ToSizeString() : string.Empty; Opacity = 1; IsHiddenItem = false; } @@ -605,7 +605,7 @@ public FtpItem(FtpListItem item, string folder) : base(null) public sealed partial class ShortcutItem : ListedItem, IShortcutItem { - public ShortcutItem(string folderRelativeId) : base(folderRelativeId) + public ShortcutItem(string? folderRelativeId) : base(folderRelativeId) { } @@ -614,13 +614,13 @@ public ShortcutItem() : base() { } // For shortcut elements (.lnk and .url) - public string TargetPath { get; set; } + public string TargetPath { get; set; } = string.Empty; public override string Name => IsSymLink ? base.Name : Path.GetFileNameWithoutExtension(ItemNameRaw); // Always hide extension for shortcuts - public string Arguments { get; set; } - public string WorkingDirectory { get; set; } + public string Arguments { get; set; } = string.Empty; + public string WorkingDirectory { get; set; } = string.Empty; public bool RunAsAdmin { get; set; } public SHOW_WINDOW_CMD ShowWindowCommand { get; set; } public bool IsUrl { get; set; } @@ -631,7 +631,7 @@ public override string Name public sealed partial class ZipItem : ListedItem { - public ZipItem(string folderRelativeId) : base(folderRelativeId) + public ZipItem(string? folderRelativeId) : base(folderRelativeId) { } @@ -794,13 +794,13 @@ public string? GitLastCommitFullSha } public sealed partial class GitShortcutItem : GitItem, IShortcutItem { - public string TargetPath { get; set; } + public string TargetPath { get; set; } = string.Empty; public override string Name => IsSymLink ? base.Name : Path.GetFileNameWithoutExtension(ItemNameRaw); // Always hide extension for shortcuts - public string Arguments { get; set; } - public string WorkingDirectory { get; set; } + public string Arguments { get; set; } = string.Empty; + public string WorkingDirectory { get; set; } = string.Empty; public bool RunAsAdmin { get; set; } public SHOW_WINDOW_CMD ShowWindowCommand { get; set; } public bool IsUrl { get; set; } diff --git a/src/Files.App/Data/Items/LocationItem.cs b/src/Files.App/Data/Items/LocationItem.cs index 6df43a36c31e..c3d2502a8054 100644 --- a/src/Files.App/Data/Items/LocationItem.cs +++ b/src/Files.App/Data/Items/LocationItem.cs @@ -14,8 +14,8 @@ namespace Files.App.Data.Items { public partial class LocationItem : ExpandableSidebarItemBase, INavigationControlItem, IExpandableSidebarFolder { - public BitmapImage icon; - public BitmapImage Icon + public BitmapImage? icon; + public BitmapImage? Icon { get => icon; set @@ -25,7 +25,7 @@ public BitmapImage Icon } } - public byte[] IconData { get; set; } + public byte[]? IconData { get; set; } private string text = ""; public string Text @@ -40,7 +40,7 @@ public string Text } } - private string path; + private string path = string.Empty; public string Path { get => path; @@ -222,7 +222,7 @@ internal static LocationItem CreateSubfolder(SubfolderEntry entry, BitmapImage? IsExpandableFolder = true, HasUnrealizedChildren = entry.HasSubfolders, IsHidden = entry.IsHidden, - Icon = sharedIcon!, + Icon = sharedIcon, MenuOptions = new ContextMenuOptions { IsLocationItem = true, @@ -240,7 +240,7 @@ internal static LocationItem CreateSubfolder(SubfolderEntry entry, BitmapImage? public SectionType Section { get; set; } - public ContextMenuOptions MenuOptions { get; set; } + public ContextMenuOptions MenuOptions { get; set; } = new(); public bool IsHeader { get; set; } @@ -257,7 +257,7 @@ public virtual object ToolTip public FrameworkElement? ItemDecorator => null; public int CompareTo(INavigationControlItem? other) - => Text.CompareTo(other.Text); + => other is null ? 1 : Text.CompareTo(other.Text); public static T Create() where T : LocationItem, new() { diff --git a/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs b/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs index f5eb7895a255..118946925e34 100644 --- a/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs +++ b/src/Files.App/Data/Items/NavigationBarSuggestionItem.cs @@ -98,7 +98,7 @@ private void UpdatePrimaryDisplay() } } - public string GetTextMemberPath(string textMemberPath) + public string? GetTextMemberPath(string textMemberPath) { return textMemberPath switch { diff --git a/src/Files.App/Data/Items/ShellFileItem.cs b/src/Files.App/Data/Items/ShellFileItem.cs index ead2e56294da..ca7a5f3862a4 100644 --- a/src/Files.App/Data/Items/ShellFileItem.cs +++ b/src/Files.App/Data/Items/ShellFileItem.cs @@ -7,11 +7,11 @@ public class ShellFileItem { public bool IsFolder { get; set; } - public string RecyclePath { get; set; } + public string RecyclePath { get; set; } = string.Empty; - public string FileName { get; set; } + public string FileName { get; set; } = string.Empty; - public string FilePath { get; set; } + public string FilePath { get; set; } = string.Empty; public DateTime RecycleDate { get; set; } @@ -19,13 +19,13 @@ public class ShellFileItem public DateTime CreatedDate { get; set; } - public string FileSize { get; set; } + public string FileSize { get; set; } = string.Empty; public ulong FileSizeBytes { get; set; } - public string FileType { get; set; } + public string FileType { get; set; } = string.Empty; - public byte[] PIDL { get; set; } // Low level shell item identifier + public byte[] PIDL { get; set; } = []; // Low level shell item identifier public Dictionary Properties { get; set; } @@ -34,18 +34,18 @@ public ShellFileItem() Properties = []; } - public ShellFileItem(bool isFolder, string recyclePath, string fileName, string filePath, DateTime recycleDate, DateTime modifiedDate, DateTime createdDate, string fileSize, ulong fileSizeBytes, string fileType, byte[] pidl) : this() + public ShellFileItem(bool isFolder, string? recyclePath, string? fileName, string? filePath, DateTime recycleDate, DateTime modifiedDate, DateTime createdDate, string? fileSize, ulong fileSizeBytes, string? fileType, byte[] pidl) : this() { IsFolder = isFolder; - RecyclePath = recyclePath; - FileName = fileName; - FilePath = filePath; + RecyclePath = recyclePath ?? string.Empty; + FileName = fileName ?? string.Empty; + FilePath = filePath ?? string.Empty; RecycleDate = recycleDate; ModifiedDate = modifiedDate; CreatedDate = createdDate; - FileSize = fileSize; + FileSize = fileSize ?? string.Empty; FileSizeBytes = fileSizeBytes; - FileType = fileType; + FileType = fileType ?? string.Empty; PIDL = pidl; } } diff --git a/src/Files.App/Data/Items/ShellLibraryItem.cs b/src/Files.App/Data/Items/ShellLibraryItem.cs index d6a11e5ecd98..eca5e6471637 100644 --- a/src/Files.App/Data/Items/ShellLibraryItem.cs +++ b/src/Files.App/Data/Items/ShellLibraryItem.cs @@ -15,7 +15,7 @@ public sealed class ShellLibraryItem /// C:\Users\[username]\AppData\Roaming\Microsoft\Windows\Libraries\Documents.library-ms
/// C:\Users\[username]\AppData\Roaming\Microsoft\Windows\Libraries\Custom library.library-ms ///
- public string FullPath { get; set; } + public string FullPath { get; set; } = string.Empty; /// /// ShellItemDisplayString.DesktopAbsoluteParsing
@@ -23,7 +23,7 @@ public sealed class ShellLibraryItem /// ::{031E4825-7B94-4DC3-B131-E946B44C8DD5}\Documents.library-ms
/// C:\Users\[username]\AppData\Roaming\Microsoft\Windows\Libraries\Custom library.library-ms ///
- public string AbsolutePath { get; set; } + public string AbsolutePath { get; set; } = string.Empty; /// /// ShellItemDisplayString.ParentRelativeParsing
@@ -31,7 +31,7 @@ public sealed class ShellLibraryItem /// {7B0DB17D-9CD2-4A93-9733-46CC89022E7C}
/// Custom library.library-ms ///
- public string RelativePath { get; set; } + public string RelativePath { get; set; } = string.Empty; /// /// ShellItemDisplayString.NormalDisplay
@@ -39,13 +39,13 @@ public sealed class ShellLibraryItem /// Documents (locale dependent based on desktop.ini file of the Libraries folder)
/// Custom library (locale independent) ///
- public string DisplayName { get; set; } + public string DisplayName { get; set; } = string.Empty; public bool IsPinned { get; set; } - public string DefaultSaveFolder { get; set; } + public string DefaultSaveFolder { get; set; } = string.Empty; - public string[] Folders { get; set; } + public string[] Folders { get; set; } = []; public ShellLibraryItem() { diff --git a/src/Files.App/Data/Items/ShellLinkItem.cs b/src/Files.App/Data/Items/ShellLinkItem.cs index dc0acda2b37b..6d0c1553dab2 100644 --- a/src/Files.App/Data/Items/ShellLinkItem.cs +++ b/src/Files.App/Data/Items/ShellLinkItem.cs @@ -7,11 +7,11 @@ namespace Files.App.Data.Items { public sealed class ShellLinkItem : ShellFileItem { - public string TargetPath { get; set; } + public string TargetPath { get; set; } = string.Empty; - public string Arguments { get; set; } + public string Arguments { get; set; } = string.Empty; - public string WorkingDirectory { get; set; } + public string WorkingDirectory { get; set; } = string.Empty; public bool RunAsAdmin { get; set; } diff --git a/src/Files.App/Data/Items/ShellNewEntry.cs b/src/Files.App/Data/Items/ShellNewEntry.cs index 8c753737faad..71f89841db60 100644 --- a/src/Files.App/Data/Items/ShellNewEntry.cs +++ b/src/Files.App/Data/Items/ShellNewEntry.cs @@ -5,16 +5,16 @@ namespace Files.App.Data.Items { public sealed class ShellNewEntry { - public string Extension { get; set; } + public required string Extension { get; set; } - public string Name { get; set; } + public required string Name { get; set; } - public string Command { get; set; } + public string? Command { get; set; } - public string IconBase64 { get; set; } + public string? IconBase64 { get; set; } - public byte[] Data { get; set; } + public byte[]? Data { get; set; } - public string Template { get; set; } + public string? Template { get; set; } } } diff --git a/src/Files.App/Data/Items/ShellOperationResult.cs b/src/Files.App/Data/Items/ShellOperationResult.cs index 7c93baddd4cd..436254781879 100644 --- a/src/Files.App/Data/Items/ShellOperationResult.cs +++ b/src/Files.App/Data/Items/ShellOperationResult.cs @@ -27,7 +27,7 @@ public sealed class ShellOperationItemResult { public bool Succeeded { get; set; } public int HResult { get; set; } - public string Source { get; set; } - public string Destination { get; set; } + public string? Source { get; set; } + public string? Destination { get; set; } } } diff --git a/src/Files.App/Data/Items/SidebarLibraryItem.cs b/src/Files.App/Data/Items/SidebarLibraryItem.cs index c9ab535aef32..06f8653cc1a0 100644 --- a/src/Files.App/Data/Items/SidebarLibraryItem.cs +++ b/src/Files.App/Data/Items/SidebarLibraryItem.cs @@ -9,7 +9,7 @@ public sealed partial class LibraryLocationItem : LocationItem public ReadOnlyCollection Folders { get; } - public bool IsEmpty => DefaultSaveFolder is null || Folders is null || Folders.Count is 0; + public bool IsEmpty => string.IsNullOrEmpty(DefaultSaveFolder) || Folders.Count is 0; public LibraryLocationItem(ShellLibraryItem shellLibrary) { @@ -21,10 +21,10 @@ public LibraryLocationItem(ShellLibraryItem shellLibrary) ShowShellItems = true, ShowUnpinItem = !shellLibrary.IsPinned, }; - Text = shellLibrary.DisplayName is not null ? shellLibrary.DisplayName : ""; + Text = shellLibrary.DisplayName; Path = shellLibrary.FullPath; DefaultSaveFolder = shellLibrary.DefaultSaveFolder; - Folders = shellLibrary.Folders is null ? null : new ReadOnlyCollection(shellLibrary.Folders); + Folders = new ReadOnlyCollection(shellLibrary.Folders); IsDefaultLocation = shellLibrary.IsPinned; } @@ -37,8 +37,8 @@ public async Task CheckDefaultSaveFolderAccess() if (!res) { - var item = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(DefaultSaveFolder)); - res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(DefaultSaveFolder, item)); + var item = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(DefaultSaveFolder)); + res = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(DefaultSaveFolder, item)); } return res; diff --git a/src/Files.App/Data/Items/WidgetContainerItem.cs b/src/Files.App/Data/Items/WidgetContainerItem.cs index 8184c5f2300b..18d6c0b4c072 100644 --- a/src/Files.App/Data/Items/WidgetContainerItem.cs +++ b/src/Files.App/Data/Items/WidgetContainerItem.cs @@ -30,7 +30,7 @@ public string WidgetAutomationProperties public bool ShowMenuFlyout => WidgetItemModel.ShowMenuFlyout; - public MenuFlyoutItem MenuFlyoutItem + public MenuFlyoutItem? MenuFlyoutItem => WidgetItemModel.MenuFlyoutItem; private object _WidgetControl; @@ -57,8 +57,8 @@ public WidgetContainerItem(object widgetControl, IWidgetViewModel widgetItemMode _expanderValueChangedCallback = expanderValueChangedCallback; _expanderValueRequestedCallback = expanderValueRequestedCallback; - WidgetItemModel = widgetItemModel; - WidgetControl = widgetControl; + _WidgetItemModel = widgetItemModel; + _WidgetControl = widgetControl; } // Disposer diff --git a/src/Files.App/Data/Items/WidgetDriveCardItem.cs b/src/Files.App/Data/Items/WidgetDriveCardItem.cs index c8d41ed31d4a..fd6a4e9fe5b0 100644 --- a/src/Files.App/Data/Items/WidgetDriveCardItem.cs +++ b/src/Files.App/Data/Items/WidgetDriveCardItem.cs @@ -7,12 +7,10 @@ namespace Files.App.Data.Items { public sealed partial class WidgetDriveCardItem : WidgetCardItem, IWidgetCardItem, IComparable { - private byte[] thumbnailData; - public new DriveItem Item { get; private set; } - private BitmapImage thumbnail; - public BitmapImage Thumbnail + private BitmapImage? thumbnail; + public BitmapImage? Thumbnail { get => thumbnail; set => SetProperty(ref thumbnail, value); @@ -32,15 +30,16 @@ public async Task LoadCardThumbnailAsync() true, IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale); - if (result is null) + if (result is null && Item.Root is { } root) { - using var thumbnail = await DriveHelpers.GetThumbnailAsync(Item.Root); + using var thumbnail = await DriveHelpers.GetThumbnailAsync(root); result ??= await thumbnail.ToByteArrayAsync(); } - thumbnailData = result; + if (result is null) + return; - var bitmapImage = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => thumbnailData.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); + var bitmapImage = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => result.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); if (bitmapImage is not null) Thumbnail = bitmapImage; } diff --git a/src/Files.App/Data/Items/WidgetFileTagCardItem.cs b/src/Files.App/Data/Items/WidgetFileTagCardItem.cs index b18d137ab66e..dc0603ed4f05 100644 --- a/src/Files.App/Data/Items/WidgetFileTagCardItem.cs +++ b/src/Files.App/Data/Items/WidgetFileTagCardItem.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Files.Shared.Utils; +using System.Diagnostics.CodeAnalysis; using System.Windows.Input; namespace Files.App.Data.Items @@ -36,10 +37,11 @@ public string? Name } private string _Path; + [AllowNull] public override string Path { get => _Path; - set => SetProperty(ref _Path, value); + set => SetProperty(ref _Path, value ?? string.Empty); } // Commands diff --git a/src/Files.App/Data/Items/WidgetRecentItem.cs b/src/Files.App/Data/Items/WidgetRecentItem.cs index e5ae7dee93eb..4f5a3f04e02a 100644 --- a/src/Files.App/Data/Items/WidgetRecentItem.cs +++ b/src/Files.App/Data/Items/WidgetRecentItem.cs @@ -45,6 +45,9 @@ public BitmapImage? Icon /// public async Task LoadRecentItemIconAsync() { + if (string.IsNullOrEmpty(Path)) + return; + var result = await FileThumbnailHelper.GetIconAsync(Path, Constants.ShellIconSizes.Small, false, IconOptions.UseCurrentScale); var bitmapImage = await result.ToBitmapAsync(); diff --git a/src/Files.App/Data/Items/Win32Process.cs b/src/Files.App/Data/Items/Win32Process.cs index 1f26f03e2bc1..68b8ea8b8e53 100644 --- a/src/Files.App/Data/Items/Win32Process.cs +++ b/src/Files.App/Data/Items/Win32Process.cs @@ -5,12 +5,12 @@ namespace Files.App.Data.Items { public sealed class Win32Process { - public string Name { get; set; } + public required string Name { get; set; } public int Pid { get; set; } - public string FileName { get; set; } + public string? FileName { get; set; } - public string AppName { get; set; } + public string? AppName { get; set; } } } diff --git a/src/Files.App/Data/Items/WslDistroItem.cs b/src/Files.App/Data/Items/WslDistroItem.cs index ef3603fbaedc..5f35d076a8c6 100644 --- a/src/Files.App/Data/Items/WslDistroItem.cs +++ b/src/Files.App/Data/Items/WslDistroItem.cs @@ -7,10 +7,10 @@ namespace Files.App.Data.Items { public sealed partial class WslDistroItem : ObservableObject, INavigationControlItem { - public string Text { get; set; } + public required string Text { get; set; } - private string path; - public string Path + private string path = string.Empty; + public required string Path { get => path; set @@ -23,10 +23,10 @@ public string Path public NavigationControlItemType ItemType => NavigationControlItemType.LinuxDistro; - private Uri icon; - public Uri Icon + private Uri? icon; + public required Uri Icon { - get => icon; + get => icon ?? throw new InvalidOperationException("The distribution icon has not been initialized."); set { SetProperty(ref icon, value, nameof(Icon)); @@ -36,7 +36,7 @@ public Uri Icon public SectionType Section { get; set; } - public ContextMenuOptions MenuOptions { get; set; } + public required ContextMenuOptions MenuOptions { get; set; } public object? Children => null; @@ -65,6 +65,6 @@ public IconElement? IconElement } } - public int CompareTo(INavigationControlItem? other) => Text.CompareTo(other.Text); + public int CompareTo(INavigationControlItem? other) => other is null ? 1 : Text.CompareTo(other.Text); } } diff --git a/src/Files.App/Data/Models/AppModel.cs b/src/Files.App/Data/Models/AppModel.cs index 8de93514362e..3535ae454687 100644 --- a/src/Files.App/Data/Models/AppModel.cs +++ b/src/Files.App/Data/Models/AppModel.cs @@ -17,7 +17,7 @@ public AppModel() } // TODO: Refactor this method - public void Clipboard_ContentChanged(object sender, object e) + public void Clipboard_ContentChanged(object? sender, object e) { try { diff --git a/src/Files.App/Data/Models/CompressArchiveModel.cs b/src/Files.App/Data/Models/CompressArchiveModel.cs index 07ace9724c8b..bd22d25f6530 100644 --- a/src/Files.App/Data/Models/CompressArchiveModel.cs +++ b/src/Files.App/Data/Models/CompressArchiveModel.cs @@ -15,7 +15,7 @@ public sealed class CompressArchiveModel : ICompressArchiveModel { private StatusCenterItemProgressModel _fileSystemProgress; - private FileSizeCalculator _sizeCalculator; + private FileSizeCalculator? _sizeCalculator; private string ArchiveExtension => FileFormat switch { @@ -132,6 +132,11 @@ public CompressArchiveModel( ArchiveWordSizes wordSize = ArchiveWordSizes.Auto) { _Progress = new Progress(); + _fileSystemProgress = new( + _Progress, + false, + FileSystemStatusCode.InProgress); + _fileSystemProgress.Report(0); Sources = source; Directory = directory; @@ -199,12 +204,13 @@ public async Task RunCreationAsync() var directories = sources.Where(SystemIO.Directory.Exists); var skippedItems = new List(); - _sizeCalculator = new FileSizeCalculator([.. files, .. directories]); - var sizeTask = _sizeCalculator.ComputeSizeAsync(cts.Token); + var sizeCalculator = new FileSizeCalculator([.. files, .. directories]); + _sizeCalculator = sizeCalculator; + var sizeTask = sizeCalculator.ComputeSizeAsync(cts.Token); _ = sizeTask.ContinueWith(_ => { - _fileSystemProgress.TotalSize = _sizeCalculator.Size; - _fileSystemProgress.ItemsCount = _sizeCalculator.ItemsCount; + _fileSystemProgress.TotalSize = sizeCalculator.Size; + _fileSystemProgress.ItemsCount = sizeCalculator.ItemsCount; _fileSystemProgress.EnumerationCompleted = true; _fileSystemProgress.Report(); }); @@ -289,7 +295,7 @@ static void AddEntry(IDictionary fileDictionary, string director if (directories.Length == 0) { - fileDictionary.Add(entryPrefix + directoryInfo.Name, null); + AddArchiveEntry(fileDictionary, entryPrefix + directoryInfo.Name, null); } else { @@ -298,6 +304,10 @@ static void AddEntry(IDictionary fileDictionary, string director AddEntry(fileDictionary, directoryInfo2.FullName, entryPrefix); } } + + // SevenZipSharp uses a null source path to represent an empty directory. + static void AddArchiveEntry(IDictionary entries, string name, string? sourcePath) + => entries.Add(name, sourcePath!); } compressor.CompressionMode = CompressionMode.Append; @@ -399,7 +409,7 @@ private void Compressor_FileCompressionStarted(object? sender, FileNameEventArgs return; } - _sizeCalculator.ForceComputeFileSize(e.FilePath); + _sizeCalculator?.ForceComputeFileSize(e.FilePath); _fileSystemProgress.FileName = e.FileName; _fileSystemProgress.Report(); } diff --git a/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModel.cs b/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModel.cs index c8884e2eedac..f69b4c46a138 100644 --- a/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModel.cs +++ b/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModel.cs @@ -11,27 +11,27 @@ public sealed class ContextMenuFlyoutItemViewModel { public bool ShowItem { get; set; } = true; - public ICommand Command { get; set; } + public ICommand? Command { get; set; } - public object CommandParameter { get; set; } + public object? CommandParameter { get; set; } - public string Glyph { get; set; } + public string? Glyph { get; set; } - public string GlyphFontFamilyName { get; set; } + public string? GlyphFontFamilyName { get; set; } - public string KeyboardAcceleratorTextOverride { get; set; } + public string? KeyboardAcceleratorTextOverride { get; set; } - public string Text { get; set; } + public string Text { get; set; } = string.Empty; - public object Tag { get; set; } + public object? Tag { get; set; } public ContextMenuFlyoutItemType ItemType { get; set; } - public Func LoadSubMenuAction { get; set; } + public Func? LoadSubMenuAction { get; set; } - public List Items { get; set; } + public List? Items { get; set; } - public BitmapImage BitmapIcon { get; set; } + public BitmapImage? BitmapIcon { get; set; } public bool ShowOnShift { get; set; } @@ -45,13 +45,13 @@ public sealed class ContextMenuFlyoutItemViewModel public bool ShowInZipPage { get; set; } - public KeyboardAccelerator KeyboardAccelerator { get; set; } + public KeyboardAccelerator? KeyboardAccelerator { get; set; } public bool IsChecked { get; set; } public bool IsEnabled { get; set; } = true; - public string ID { get; set; } + public string ID { get; set; } = string.Empty; public bool IsPrimary { get; set; } diff --git a/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModelBuilder.cs b/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModelBuilder.cs index 16577012d18b..0a575f0689ed 100644 --- a/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModelBuilder.cs +++ b/src/Files.App/Data/Models/ContextMenuFlyoutItemViewModelBuilder.cs @@ -28,7 +28,7 @@ public bool IsVisible public string AccessKey { get; init; } = string.Empty; - public object Tag { get; init; } + public object? Tag { get; init; } public bool ShowOnShift { get; init; } = false; diff --git a/src/Files.App/Data/Models/CurrentInstanceViewModel.cs b/src/Files.App/Data/Models/CurrentInstanceViewModel.cs index ffc6a977f291..9c154b967c7e 100644 --- a/src/Files.App/Data/Models/CurrentInstanceViewModel.cs +++ b/src/Files.App/Data/Models/CurrentInstanceViewModel.cs @@ -34,8 +34,8 @@ public bool IsPageTypeSearchResults } } - private string currentSearchQuery; - public string CurrentSearchQuery + private string? currentSearchQuery; + public string? CurrentSearchQuery { get => currentSearchQuery; set => SetProperty(ref currentSearchQuery, value); diff --git a/src/Files.App/Data/Models/HashInfoItem.cs b/src/Files.App/Data/Models/HashInfoItem.cs index 6a266be32697..5b809a4af09a 100644 --- a/src/Files.App/Data/Models/HashInfoItem.cs +++ b/src/Files.App/Data/Models/HashInfoItem.cs @@ -5,15 +5,15 @@ namespace Files.App.Data.Models { public sealed partial class HashInfoItem : ObservableObject { - private string _Algorithm; - public string Algorithm + private string _Algorithm = string.Empty; + public required string Algorithm { get => _Algorithm; set => SetProperty(ref _Algorithm, value); } - private string _HashValue; - public string HashValue + private string? _HashValue; + public string? HashValue { get => _HashValue; set => SetProperty(ref _HashValue, value); diff --git a/src/Files.App/Data/Models/ItemManipulationModel.cs b/src/Files.App/Data/Models/ItemManipulationModel.cs index 670376a761ba..70293fdaa066 100644 --- a/src/Files.App/Data/Models/ItemManipulationModel.cs +++ b/src/Files.App/Data/Models/ItemManipulationModel.cs @@ -5,33 +5,33 @@ namespace Files.App.Data.Models { public sealed class ItemManipulationModel { - public event EventHandler FocusFileListInvoked; + public event EventHandler? FocusFileListInvoked; - public event EventHandler SelectAllItemsInvoked; + public event EventHandler? SelectAllItemsInvoked; - public event EventHandler ClearSelectionInvoked; + public event EventHandler? ClearSelectionInvoked; - public event EventHandler InvertSelectionInvoked; + public event EventHandler? InvertSelectionInvoked; - public event EventHandler AddSelectedItemInvoked; + public event EventHandler? AddSelectedItemInvoked; - public event EventHandler RemoveSelectedItemInvoked; + public event EventHandler? RemoveSelectedItemInvoked; - public event EventHandler FocusSelectedItemsInvoked; + public event EventHandler? FocusSelectedItemsInvoked; - public event EventHandler StartRenameItemInvoked; + public event EventHandler? StartRenameItemInvoked; - public event EventHandler ScrollIntoViewInvoked; + public event EventHandler? ScrollIntoViewInvoked; - public event EventHandler ScrollToTopInvoked; + public event EventHandler? ScrollToTopInvoked; - public event EventHandler SetDragModeForItemsInvoked; + public event EventHandler? SetDragModeForItemsInvoked; - public event EventHandler RefreshItemsOpacityInvoked; + public event EventHandler? RefreshItemsOpacityInvoked; - public event EventHandler RefreshItemThumbnailInvoked; + public event EventHandler? RefreshItemThumbnailInvoked; - public event EventHandler RefreshItemsThumbnailInvoked; + public event EventHandler? RefreshItemsThumbnailInvoked; public void FocusFileList() { diff --git a/src/Files.App/Data/Models/ListedTagViewModel.cs b/src/Files.App/Data/Models/ListedTagViewModel.cs index e8390610c8df..0ed678f1212d 100644 --- a/src/Files.App/Data/Models/ListedTagViewModel.cs +++ b/src/Files.App/Data/Models/ListedTagViewModel.cs @@ -33,7 +33,7 @@ public bool CanCommit set => SetProperty(ref _CanCommit, value); } - private string _NewName; + private string _NewName = string.Empty; public string NewName { get => _NewName; @@ -49,8 +49,8 @@ public string NewColor public ListedTagViewModel(TagViewModel tag) { - Tag = tag; - NewColor = tag.Color; + _Tag = tag; + _NewColor = tag.Color; } } } diff --git a/src/Files.App/Data/Models/PinnedFoldersManager.cs b/src/Files.App/Data/Models/PinnedFoldersManager.cs index 58d8576a0038..5f02b7761691 100644 --- a/src/Files.App/Data/Models/PinnedFoldersManager.cs +++ b/src/Files.App/Data/Models/PinnedFoldersManager.cs @@ -75,8 +75,8 @@ public async Task CreateLocationItemFromPathAsync(string path) var isLibrary = LibraryManager.IsLibraryPath(path); var libraryDisplayName = isLibrary ? GetLibraryDisplayName(path) : null; - var item = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(path)); - var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path, item)); + var item = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(path)); + var res = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path, item)); LocationItem locationItem; if (string.Equals(path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) @@ -111,11 +111,10 @@ public async Task CreateLocationItemFromPathAsync(string path) locationItem.IsInvalid = false; await LoadIconForLocationItemAsync(locationItem, path, isFolder: false); } - else if (res) + else if (res?.Result is { } folder) { locationItem.IsInvalid = false; - if (res.Result is not null) - await LoadIconForLocationItemAsync(locationItem, res.Result.Path); + await LoadIconForLocationItemAsync(locationItem, folder.Path); } else { @@ -127,7 +126,7 @@ public async Task CreateLocationItemFromPathAsync(string path) return locationItem; } - private static string GetLibraryDisplayName(string libraryPath) + private static string? GetLibraryDisplayName(string libraryPath) { using var storable = WindowsStorable.TryParse(libraryPath); if (storable is null) diff --git a/src/Files.App/Data/Models/SelectedItemsPropertiesViewModel.cs b/src/Files.App/Data/Models/SelectedItemsPropertiesViewModel.cs index d0d2828286a2..e3f28411e0a5 100644 --- a/src/Files.App/Data/Models/SelectedItemsPropertiesViewModel.cs +++ b/src/Files.App/Data/Models/SelectedItemsPropertiesViewModel.cs @@ -34,8 +34,8 @@ public bool LoadCombinedItemsGlyph set => SetProperty(ref loadCombinedItemsGlyph, value); } - private Uri customIconSource; - public Uri CustomIconSource + private Uri? customIconSource; + public Uri? CustomIconSource { get => customIconSource; set => SetProperty(ref customIconSource, value); @@ -55,14 +55,14 @@ public bool LoadFileIcon set => SetProperty(ref loadFileIcon, value); } - private byte[] iconData; - public byte[] IconData + private byte[]? iconData; + public byte[]? IconData { get => iconData; set => SetProperty(ref iconData, value); } - private string itemName; + private string itemName = string.Empty; public string ItemName { get => itemName; @@ -73,7 +73,7 @@ public string ItemName } } - private string originalItemName; + private string originalItemName = string.Empty; public string OriginalItemName { get => originalItemName; @@ -91,7 +91,7 @@ public bool ItemNameVisibility set => SetProperty(ref itemNameVisibility, value); } - private string itemType; + private string itemType = string.Empty; public string ItemType { get => itemType; @@ -109,7 +109,7 @@ public bool ItemTypeVisibility set => SetProperty(ref itemTypeVisibility, value); } - private string driveFileSystem; + private string driveFileSystem = string.Empty; public string DriveFileSystem { get => driveFileSystem; @@ -127,7 +127,7 @@ public bool DriveFileSystemVisibility set => SetProperty(ref driveFileSystemVisibility, value); } - private string itemLocation; + private string itemLocation = string.Empty; public string ItemLocation { get => itemLocation; @@ -145,21 +145,21 @@ public bool ItemLocationVisibility set => SetProperty(ref itemLocationVisibility, value); } - private string itemSize; + private string itemSize = string.Empty; public string ItemSize { get => itemSize; set => SetProperty(ref itemSize, value); } - private string itemSizeOnDisk; + private string itemSizeOnDisk = string.Empty; public string ItemSizeOnDisk { get => itemSizeOnDisk; set => SetProperty(ref itemSizeOnDisk, value); } - private string uncompresseditemSize; + private string uncompresseditemSize = string.Empty; public string UncompressedItemSize { get => uncompresseditemSize; @@ -241,7 +241,7 @@ public int FilesCount set => SetProperty(ref filesCount, value); } - public string filesAndFoldersCountString; + public string filesAndFoldersCountString = string.Empty; public string FilesAndFoldersCountString { get => filesAndFoldersCountString; @@ -274,7 +274,7 @@ public ulong DriveUsedSpaceValue } } - private string driveUsedSpace; + private string driveUsedSpace = string.Empty; public string DriveUsedSpace { get => driveUsedSpace; @@ -285,7 +285,7 @@ public string DriveUsedSpace } } - private string driveUsedSpaceLongSize; + private string driveUsedSpaceLongSize = string.Empty; public string DriveUsedSpaceLongSize { get => driveUsedSpaceLongSize; @@ -315,7 +315,7 @@ public ulong DriveFreeSpaceValue } } - private string driveFreeSpace; + private string driveFreeSpace = string.Empty; public string DriveFreeSpace { get => driveFreeSpace; @@ -326,7 +326,7 @@ public string DriveFreeSpace } } - private string driveFreeSpaceLongSize; + private string driveFreeSpaceLongSize = string.Empty; public string DriveFreeSpaceLongSize { get => driveFreeSpaceLongSize; @@ -343,7 +343,7 @@ public bool DriveFreeSpaceVisibility set => SetProperty(ref driveFreeSpaceVisibility, value); } - public string ItemCreatedTimestamp { get; private set; } + public string ItemCreatedTimestamp { get; private set; } = string.Empty; private DateTimeOffset itemCreatedTimestampReal; public DateTimeOffset ItemCreatedTimestampReal @@ -365,7 +365,7 @@ public bool ItemCreatedTimestampVisibility set => SetProperty(ref itemCreatedTimestampVisibility, value); } - public string ItemModifiedTimestamp { get; private set; } + public string ItemModifiedTimestamp { get; private set; } = string.Empty; private DateTimeOffset itemModifiedTimestampReal; public DateTimeOffset ItemModifiedTimestampReal @@ -387,7 +387,7 @@ public bool ItemModifiedTimestampVisibility set => SetProperty(ref itemModifiedTimestampVisibility, value); } - public string ItemAccessedTimestamp { get; private set; } + public string ItemAccessedTimestamp { get; private set; } = string.Empty; public DateTimeOffset itemAccessedTimestampReal; public DateTimeOffset ItemAccessedTimestampReal @@ -429,7 +429,7 @@ public ulong DriveCapacityValue } } - private string driveCapacity; + private string driveCapacity = string.Empty; public string DriveCapacity { get => driveCapacity; @@ -440,7 +440,7 @@ public string DriveCapacity } } - private string driveCapacityLongSize; + private string driveCapacityLongSize = string.Empty; public string DriveCapacityLongSize { get => driveCapacityLongSize; @@ -469,8 +469,8 @@ public bool CleanupVisibility set => SetProperty(ref cleanupVisibility, value); } - private ICommand cleanupDriveCommand; - public ICommand CleanupDriveCommand + private ICommand? cleanupDriveCommand; + public ICommand? CleanupDriveCommand { get => cleanupDriveCommand; set => SetProperty(ref cleanupDriveCommand, value); @@ -483,22 +483,22 @@ public bool FormatVisibility set => SetProperty(ref formatVisibility, value); } - private ICommand formatDriveCommand; - public ICommand FormatDriveCommand + private ICommand? formatDriveCommand; + public ICommand? FormatDriveCommand { get => formatDriveCommand; set => SetProperty(ref formatDriveCommand, value); } - private ICommand editAlbumCoverCommand; - public ICommand EditAlbumCoverCommand + private ICommand? editAlbumCoverCommand; + public ICommand? EditAlbumCoverCommand { get => editAlbumCoverCommand; set => SetProperty(ref editAlbumCoverCommand, value); } - private ICommand removeAlbumCoverCommand; - public ICommand RemoveAlbumCoverCommand + private ICommand? removeAlbumCoverCommand; + public ICommand? RemoveAlbumCoverCommand { get => removeAlbumCoverCommand; set => SetProperty(ref removeAlbumCoverCommand, value); @@ -511,7 +511,7 @@ public bool ItemAttributesVisibility set => SetProperty(ref itemAttributesVisibility, value); } - private string selectedItemsCountString; + private string selectedItemsCountString = string.Empty; public string SelectedItemsCountString { get => selectedItemsCountString; @@ -558,14 +558,14 @@ public void CheckAllFileExtensions(List itemExtensions) IsSelectedItemShortcut = (itemExtensions.Count == 1) && (itemExtensions.TrueForAll(itemExtension => FileExtensionHelpers.IsShortcutFile(itemExtension))); } - private string shortcutItemType; + private string shortcutItemType = string.Empty; public string ShortcutItemType { get => shortcutItemType; set => SetProperty(ref shortcutItemType, value); } - private string shortcutItemPath; + private string shortcutItemPath = string.Empty; public string ShortcutItemPath { get => shortcutItemPath; @@ -576,7 +576,7 @@ public string ShortcutItemPath } } - private string shortcutItemPathEditedValue; + private string shortcutItemPathEditedValue = string.Empty; public string ShortcutItemPathEditedValue { get => shortcutItemPathEditedValue; @@ -590,7 +590,7 @@ public bool IsShortcutItemPathReadOnly set => SetProperty(ref isShortcutItemPathReadOnly, value); } - private string shortcutItemWorkingDir; + private string shortcutItemWorkingDir = string.Empty; public string ShortcutItemWorkingDir { get => shortcutItemWorkingDir; @@ -601,7 +601,7 @@ public string ShortcutItemWorkingDir } } - private string shortcutItemWorkingDirEditedValue; + private string shortcutItemWorkingDirEditedValue = string.Empty; public string ShortcutItemWorkingDirEditedValue { get => shortcutItemWorkingDirEditedValue; @@ -615,7 +615,7 @@ public bool ShortcutItemWorkingDirVisibility set => SetProperty(ref shortcutItemWorkingDirVisibility, value); } - private string shortcutItemArguments; + private string shortcutItemArguments = string.Empty; public string ShortcutItemArguments { get => shortcutItemArguments; @@ -626,7 +626,7 @@ public string ShortcutItemArguments } } - private string shortcutItemArgumentsEditedValue; + private string shortcutItemArgumentsEditedValue = string.Empty; public string ShortcutItemArgumentsEditedValue { get => shortcutItemArgumentsEditedValue; @@ -650,8 +650,8 @@ public bool ShortcutItemWindowArgsVisibility set => SetProperty(ref shortcutItemWindowArgsVisibility, value); } - private RelayCommand shortcutItemOpenLinkCommand; - public RelayCommand ShortcutItemOpenLinkCommand + private RelayCommand? shortcutItemOpenLinkCommand; + public RelayCommand? ShortcutItemOpenLinkCommand { get => shortcutItemOpenLinkCommand; set @@ -812,10 +812,14 @@ public bool RunAsAdminEnabled /// The localized string of the currently selected ShowWindowCommand. /// This value can be used for display in the UI. ///
- public string SelectedShowWindowCommand + public string? SelectedShowWindowCommand { - get => ShowWindowCommandTypes.GetValueOrDefault(ShowWindowCommandEditedValue)!; - set => ShowWindowCommandEditedValue = ShowWindowCommandTypes.First(e => e.Value == value).Key; + get => ShowWindowCommandTypes.GetValueOrDefault(ShowWindowCommandEditedValue); + set + { + if (value is not null) + ShowWindowCommandEditedValue = ShowWindowCommandTypes.First(e => e.Value == value).Key; + } } private SHOW_WINDOW_CMD showWindowCommand; @@ -881,8 +885,8 @@ public bool IsEditAlbumCoverVisible set => SetProperty(ref isEditAlbumCoverVisible, value); } - private Picture modifiedAlbumCover; - public Picture ModifiedAlbumCover + private Picture? modifiedAlbumCover; + public Picture? ModifiedAlbumCover { get => modifiedAlbumCover; set => SetProperty(ref modifiedAlbumCover, value); diff --git a/src/Files.App/Data/Models/SuggestionModel.cs b/src/Files.App/Data/Models/SuggestionModel.cs index 8536839ece6f..5c3210e8a328 100644 --- a/src/Files.App/Data/Models/SuggestionModel.cs +++ b/src/Files.App/Data/Models/SuggestionModel.cs @@ -56,7 +56,7 @@ public BitmapImage? FileImage } } - private void Img_ImageOpened(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + private void Img_ImageOpened(object sender, Microsoft.UI.Xaml.RoutedEventArgs? e) { if (sender is BitmapImage image) { diff --git a/src/Files.App/Data/Parameters/ColumnParam.cs b/src/Files.App/Data/Parameters/ColumnParam.cs index 2ce6e0e8f55d..03feba5b307b 100644 --- a/src/Files.App/Data/Parameters/ColumnParam.cs +++ b/src/Files.App/Data/Parameters/ColumnParam.cs @@ -9,7 +9,7 @@ public sealed class ColumnParam : NavigationArguments { public int Column { get; set; } - public ListView ListView { get; set; } + public ListView? ListView { get; set; } public ColumnLayoutPage? Source { get; set; } } diff --git a/src/Files.App/Data/Parameters/PropertiesPageNavigationParameter.cs b/src/Files.App/Data/Parameters/PropertiesPageNavigationParameter.cs index aff7aa5b5fbc..6dab0e9ac358 100644 --- a/src/Files.App/Data/Parameters/PropertiesPageNavigationParameter.cs +++ b/src/Files.App/Data/Parameters/PropertiesPageNavigationParameter.cs @@ -7,12 +7,12 @@ namespace Files.App.Data.Parameters { public sealed class PropertiesPageNavigationParameter { - public CancellationTokenSource CancellationTokenSource; + public CancellationTokenSource CancellationTokenSource = new(); - public object Parameter; + public required object Parameter; - public IShellPage AppInstance; + public required IShellPage AppInstance; - public Window Window; + public required Window Window; } } diff --git a/src/Files.App/Data/Parameters/TabBarItemParameter.cs b/src/Files.App/Data/Parameters/TabBarItemParameter.cs index d88ad099fd95..d65e2718ea3b 100644 --- a/src/Files.App/Data/Parameters/TabBarItemParameter.cs +++ b/src/Files.App/Data/Parameters/TabBarItemParameter.cs @@ -6,10 +6,15 @@ namespace Files.App.Data.Parameters public sealed class TabBarItemParameter { private static readonly KnownTypesConverter _typesConverter = new(); + private Type? _initialPageType; - public Type InitialPageType { get; set; } + public Type InitialPageType + { + get => _initialPageType ?? throw new InvalidOperationException("The initial page type has not been set."); + set => _initialPageType = value; + } - public object NavigationParameter { get; set; } + public object? NavigationParameter { get; set; } public string Serialize() { @@ -18,21 +23,30 @@ public string Serialize() public static TabBarItemParameter Deserialize(string obj) { - var tabArgs = new TabBarItemParameter(); - - var tempArgs = JsonSerializer.Deserialize>(obj); - tabArgs.InitialPageType = Type.GetType(tempArgs[nameof(InitialPageType)].GetString()); + var tempArgs = JsonSerializer.Deserialize>(obj) + ?? throw new JsonException("The tab data is empty."); + var typeName = tempArgs[nameof(InitialPageType)].GetString(); + var initialPageType = typeName is null ? null : Type.GetType(typeName); + if (initialPageType is null) + { + throw new JsonException("The initial page type is missing or invalid."); + } + object? navigationParameter; try { - tabArgs.NavigationParameter = JsonSerializer.Deserialize(tempArgs[nameof(NavigationParameter)].GetRawText()); + navigationParameter = JsonSerializer.Deserialize(tempArgs[nameof(NavigationParameter)].GetRawText()); } catch (JsonException) { - tabArgs.NavigationParameter = tempArgs[nameof(NavigationParameter)].GetString(); + navigationParameter = tempArgs[nameof(NavigationParameter)].GetString(); } - return tabArgs; + return new TabBarItemParameter + { + InitialPageType = initialPageType, + NavigationParameter = navigationParameter, + }; } } } diff --git a/src/Files.App/Dialogs/AddItemDialog.xaml.cs b/src/Files.App/Dialogs/AddItemDialog.xaml.cs index a68da84fed86..8795bb99927e 100644 --- a/src/Files.App/Dialogs/AddItemDialog.xaml.cs +++ b/src/Files.App/Dialogs/AddItemDialog.xaml.cs @@ -31,7 +31,10 @@ public AddItemDialog() private void ListView_ItemClick(object sender, ItemClickEventArgs e) { - ViewModel.ResultType = (e.ClickedItem as AddItemDialogListItemViewModel).ItemResult; + if (e.ClickedItem is not AddItemDialogListItemViewModel { ItemResult: { } itemResult }) + return; + + ViewModel.ResultType = itemResult; Hide(); } diff --git a/src/Files.App/Dialogs/DynamicDialog.xaml.cs b/src/Files.App/Dialogs/DynamicDialog.xaml.cs index 2df3599ad2fa..cfcb3ae65753 100644 --- a/src/Files.App/Dialogs/DynamicDialog.xaml.cs +++ b/src/Files.App/Dialogs/DynamicDialog.xaml.cs @@ -8,14 +8,13 @@ namespace Files.App.Dialogs { public sealed partial class DynamicDialog : ContentDialog, IDisposable { + private DynamicDialogViewModel? viewModel; + private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; public DynamicDialogViewModel ViewModel - { - get => (DynamicDialogViewModel)DataContext; - private set => DataContext = value; - } + => viewModel ?? throw new ObjectDisposedException(nameof(DynamicDialog)); public DynamicDialogResult DynamicResult { @@ -32,27 +31,28 @@ public DynamicDialog(DynamicDialogViewModel dynamicDialogViewModel) InitializeComponent(); dynamicDialogViewModel.HideDialog = Hide; - ViewModel = dynamicDialogViewModel; + viewModel = dynamicDialogViewModel; + DataContext = dynamicDialogViewModel; } private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) { - ViewModel.PrimaryButtonCommand.Execute(args); + ViewModel.PrimaryButtonCommand?.Execute(args); } private void ContentDialog_SecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) { - ViewModel.SecondaryButtonCommand.Execute(args); + ViewModel.SecondaryButtonCommand?.Execute(args); } private void ContentDialog_CloseButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) { - ViewModel.CloseButtonCommand.Execute(args); + ViewModel.CloseButtonCommand?.Execute(args); } private void ContentDialog_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e) { - ViewModel.KeyDownCommand.Execute(e); + ViewModel.KeyDownCommand?.Execute(e); } // Focus is moved by the dialog itself while opening, so handlers that focus the @@ -64,8 +64,9 @@ private void ContentDialog_Opened(ContentDialog sender, ContentDialogOpenedEvent public void Dispose() { - ViewModel?.Dispose(); - ViewModel = null; + viewModel?.Dispose(); + viewModel = null; + DataContext = null; } } } diff --git a/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs b/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs index 36e78ab6f6fd..189e946794a1 100644 --- a/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs +++ b/src/Files.App/Dialogs/FilesystemOperationDialog.xaml.cs @@ -174,12 +174,12 @@ private void EndRename(FileSystemDialogConflictItemViewModel conflictItem) { conflictItem.CustomName = FilesystemHelpers.FilterRestrictedCharacters(conflictItem.CustomName); - if (ViewModel.IsNameAvailableForItem(conflictItem, conflictItem.CustomName!)) + if (ViewModel.IsNameAvailableForItem(conflictItem, conflictItem.CustomName)) conflictItem.IsTextBoxVisible = false; else ViewModel.PrimaryButtonEnabled = false; - if (conflictItem.CustomName.Equals(conflictItem.DisplayName)) + if (string.Equals(conflictItem.CustomName, conflictItem.DisplayName, StringComparison.Ordinal)) { var savedName = conflictItem.DestinationDisplayName; conflictItem.DestinationDisplayName = savedName; diff --git a/src/Files.App/Extensions/ImageSourceExtensions.cs b/src/Files.App/Extensions/ImageSourceExtensions.cs index 313e1d8dd0c1..504df8e5d59d 100644 --- a/src/Files.App/Extensions/ImageSourceExtensions.cs +++ b/src/Files.App/Extensions/ImageSourceExtensions.cs @@ -10,7 +10,7 @@ namespace Files.App.Extensions { internal static class ImageSourceExtensions { - internal static async Task ToByteArrayAsync(this IInputStream stream) + internal static async Task ToByteArrayAsync(this IInputStream? stream) { if (stream is null) { @@ -22,7 +22,7 @@ internal static async Task ToByteArrayAsync(this IInputStream stream) return await readStream.ToByteArrayAsync(); } - internal static async Task ToByteArrayAsync(this StorageFile file) + internal static async Task ToByteArrayAsync(this StorageFile? file) { if (file is null) { diff --git a/src/Files.App/Extensions/ShellNewEntryExtensions.cs b/src/Files.App/Extensions/ShellNewEntryExtensions.cs index 00cedd074d9b..5a5b341ae22d 100644 --- a/src/Files.App/Extensions/ShellNewEntryExtensions.cs +++ b/src/Files.App/Extensions/ShellNewEntryExtensions.cs @@ -28,10 +28,13 @@ public static async Task> GetNewContextMenuEntries() public static async Task> Create(this ShellNewEntry shellEntry, string filePath, IShellPage associatedInstance) { - var parentFolder = await associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(filePath)); - if (parentFolder) + if (associatedInstance.ShellViewModel is not { } shellViewModel) + return new FilesystemResult(null, FileSystemStatusCode.NotFound); + + var parentFolder = await shellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(filePath)); + if (parentFolder.Result is { } folder) { - return await Create(shellEntry, parentFolder, filePath); + return await Create(shellEntry, folder, filePath); } return new FilesystemResult(null, parentFolder.ErrorCode); @@ -39,25 +42,32 @@ public static async Task> Create(this ShellNew public static async Task> Create(this ShellNewEntry shellEntry, BaseStorageFolder parentFolder, string filePath) { - FilesystemResult createdFile = null; var fileName = Path.GetFileName(filePath); + if (string.IsNullOrEmpty(fileName)) + return new FilesystemResult(null, FileSystemStatusCode.NotFound); + FilesystemResult createdFile; if (shellEntry.Template is null) { - createdFile = await FilesystemTasks.Wrap(() => parentFolder.CreateFileAsync(fileName, CreationCollisionOption.GenerateUniqueName).AsTask()); + createdFile = await FilesystemTasks.WrapNullable( + () => parentFolder.CreateFileAsync(fileName, CreationCollisionOption.GenerateUniqueName).AsTask()); } else { - createdFile = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(shellEntry.Template)) - .OnSuccess(t => t.CopyAsync(parentFolder, fileName, NameCollisionOption.GenerateUniqueName).AsTask()); + var templateFile = await FilesystemTasks.WrapNullable( + () => StorageFileExtensions.DangerousGetFileFromPathAsync(shellEntry.Template)); + createdFile = templateFile.Result is { } sourceFile + ? await FilesystemTasks.WrapNullable( + () => sourceFile.CopyAsync(parentFolder, fileName, NameCollisionOption.GenerateUniqueName).AsTask()) + : new FilesystemResult(null, templateFile.ErrorCode); } - if (createdFile && shellEntry.Data is not null) + if (createdFile is { Result: { } result } && shellEntry.Data is not null) { // Calls unsupported OpenTransactedWriteAsync //await FileIO.WriteBytesAsync(createdFile.Result, shellEntry.Data); - await createdFile.Result.WriteBytesAsync(shellEntry.Data); + await result.WriteBytesAsync(shellEntry.Data); } return createdFile; diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 55c6e4352768..e6df305e33bf 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -409,7 +409,8 @@ public static void HandleAppUnhandledException(Exception? ex, bool showToastNoti var lastSessionTabList = userSettingsService.GeneralSettingsService.LastSessionTabList; - if (userSettingsService.GeneralSettingsService.LastCrashedTabList?.SequenceEqual(lastSessionTabList) ?? false) + if (lastSessionTabList is not null && + userSettingsService.GeneralSettingsService.LastCrashedTabList?.SequenceEqual(lastSessionTabList) == true) { // Avoid infinite restart loop userSettingsService.GeneralSettingsService.LastSessionTabList = null; diff --git a/src/Files.App/Helpers/BitmapHelper.cs b/src/Files.App/Helpers/BitmapHelper.cs index a89301d1ad92..188865abf432 100644 --- a/src/Files.App/Helpers/BitmapHelper.cs +++ b/src/Files.App/Helpers/BitmapHelper.cs @@ -64,7 +64,7 @@ public static async Task RotateAsync(string filePath, BitmapRotation rotation) } var fileStreamRes = await FilesystemTasks.Wrap(() => file.OpenAsync(FileAccessMode.ReadWrite).AsTask()); - using IRandomAccessStream fileStream = fileStreamRes.Result; + using IRandomAccessStream? fileStream = fileStreamRes.Result; if (fileStream is null) { return; @@ -148,4 +148,4 @@ public static async Task SaveSoftwareBitmapToFileAsync(SoftwareBitmap softwareBi } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Helpers/Dialog/DialogDisplayHelper.cs b/src/Files.App/Helpers/Dialog/DialogDisplayHelper.cs index db195728ea5c..f7e6d11fe0f6 100644 --- a/src/Files.App/Helpers/Dialog/DialogDisplayHelper.cs +++ b/src/Files.App/Helpers/Dialog/DialogDisplayHelper.cs @@ -27,7 +27,7 @@ internal sealed class DialogDisplayHelper /// The (optional) secondary button text. /// If not set, it won't be presented to the user at all. /// - public static async Task ShowDialogAsync(string title, string message, string primaryText = "OK", string secondaryText = null) + public static async Task ShowDialogAsync(string title, string message, string primaryText = "OK", string? secondaryText = null) { var dialog = new DynamicDialog(new DynamicDialogViewModel() { @@ -58,4 +58,4 @@ public static async Task ShowDialogAsync(DynamicDialog dial return DynamicDialogResult.Cancel; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs b/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs index f8d9ec25512b..3d39852cb4c6 100644 --- a/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs +++ b/src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs @@ -116,7 +116,7 @@ void UpdateDialogState() }, PrimaryButtonAction = (vm, e) => { - vm.HideDialog(); // Rename successful + vm.HideDialog?.Invoke(); // Rename successful }, PrimaryButtonText = Strings.Create.GetLocalizedResource(), CloseButtonText = Strings.Cancel.GetLocalizedResource(), @@ -153,7 +153,7 @@ public static DynamicDialog GetFor_CannotRunFileDialog() }); } - public static DynamicDialog GetFor_FileInUseDialog(List lockingProcess = null) + public static DynamicDialog GetFor_FileInUseDialog(List? lockingProcess = null) { DynamicDialog dialog = new DynamicDialog(new DynamicDialogViewModel() { @@ -190,25 +190,29 @@ public static DynamicDialog GetFor_CredentialEntryDialog(string path) inputUsername.TextChanged += (textBox, args) => { userAndPass[0] = inputUsername.Text; - dialog.ViewModel.AdditionalData = userAndPass; + if (dialog is not null) + dialog.ViewModel.AdditionalData = userAndPass; }; inputPassword.PasswordChanged += (textBox, args) => { userAndPass[1] = inputPassword.Password; - dialog.ViewModel.AdditionalData = userAndPass; + if (dialog is not null) + dialog.ViewModel.AdditionalData = userAndPass; }; saveCreds.Checked += (textBox, args) => { userAndPass[2] = "y"; - dialog.ViewModel.AdditionalData = userAndPass; + if (dialog is not null) + dialog.ViewModel.AdditionalData = userAndPass; }; saveCreds.Unchecked += (textBox, args) => { userAndPass[2] = "n"; - dialog.ViewModel.AdditionalData = userAndPass; + if (dialog is not null) + dialog.ViewModel.AdditionalData = userAndPass; }; dialog = new DynamicDialog(new DynamicDialogViewModel() @@ -236,8 +240,8 @@ public static DynamicDialog GetFor_CredentialEntryDialog(string path) }, CloseButtonAction = (vm, e) => { - dialog.ViewModel.AdditionalData = null; - vm.HideDialog(); + vm.AdditionalData = null; + vm.HideDialog?.Invoke(); } }); @@ -283,8 +287,8 @@ public static DynamicDialog GetFor_GitCheckoutConflicts(string checkoutBranchNam AdditionalData = GitCheckoutOptions.BringChanges, CloseButtonAction = (vm, e) => { - dialog.ViewModel.AdditionalData = GitCheckoutOptions.None; - vm.HideDialog(); + vm.AdditionalData = GitCheckoutOptions.None; + vm.HideDialog?.Invoke(); } }); @@ -330,8 +334,8 @@ public static DynamicDialog GetFor_GitMergeConflicts(string checkoutBranchName, AdditionalData = GitCheckoutOptions.AbortMerge, CloseButtonAction = (vm, e) => { - dialog.ViewModel.AdditionalData = GitCheckoutOptions.None; - vm.HideDialog(); + vm.AdditionalData = GitCheckoutOptions.None; + vm.HideDialog?.Invoke(); } }); @@ -373,8 +377,8 @@ public static DynamicDialog GetFor_DeleteGitBranchConfirmation(string branchName AdditionalData = true, CloseButtonAction = (vm, e) => { - dialog.ViewModel.AdditionalData = false; - vm.HideDialog(); + vm.AdditionalData = false; + vm.HideDialog?.Invoke(); } }); @@ -393,7 +397,7 @@ public static DynamicDialog GetFor_RenameRequiresHigherPermissions(string path) SecondaryButtonAction = (vm, e) => { var context = Ioc.Default.GetRequiredService(); - var item = context.ShellPage?.ShellViewModel.FilesAndFolders.FirstOrDefault(li => li.ItemPath.Equals(path)); + var item = context.ShellPage?.ShellViewModel?.FilesAndFolders.FirstOrDefault(li => li.ItemPath.Equals(path)); if (context.ShellPage is not null && item is not null) FilePropertiesHelpers.OpenPropertiesWindow(item, context.ShellPage, PropertiesNavigationViewItemType.Security); @@ -461,7 +465,7 @@ public static DynamicDialog GetFor_CreateAlternateDataStreamDialog() }, PrimaryButtonAction = (vm, e) => { - vm.HideDialog(); + vm.HideDialog?.Invoke(); }, PrimaryButtonText = Strings.Create.GetLocalizedResource(), CloseButtonText = Strings.Cancel.GetLocalizedResource(), diff --git a/src/Files.App/Helpers/LocationHelpers.cs b/src/Files.App/Helpers/LocationHelpers.cs index fc4757a9bab3..d0342e47ada4 100644 --- a/src/Files.App/Helpers/LocationHelpers.cs +++ b/src/Files.App/Helpers/LocationHelpers.cs @@ -8,7 +8,7 @@ namespace Files.App.Helpers { public static class LocationHelpers { - public static async Task GetAddressFromCoordinatesAsync(double? Lat, double? Lon) + public static async Task GetAddressFromCoordinatesAsync(double? Lat, double? Lon) { if (!Lat.HasValue || !Lon.HasValue) return null; diff --git a/src/Files.App/Helpers/MediaFileHelper.cs b/src/Files.App/Helpers/MediaFileHelper.cs index c66850d9ab5e..1ec0a69104b9 100644 --- a/src/Files.App/Helpers/MediaFileHelper.cs +++ b/src/Files.App/Helpers/MediaFileHelper.cs @@ -9,7 +9,7 @@ internal sealed class MediaFileHelper ///
/// The file path to change the album cover. /// The album cover to use, or null to clear any embedded cover. - public static bool ChangeAlbumCover(string filePath, Picture albumCover) + public static bool ChangeAlbumCover(string filePath, Picture? albumCover) { try { @@ -27,4 +27,4 @@ public static bool ChangeAlbumCover(string filePath, Picture albumCover) } } -} \ No newline at end of file +} diff --git a/src/Files.App/Helpers/MenuFlyout/MenuFlyoutHelper.cs b/src/Files.App/Helpers/MenuFlyout/MenuFlyoutHelper.cs index 925bdc971a32..e0c9e6c61eab 100644 --- a/src/Files.App/Helpers/MenuFlyout/MenuFlyoutHelper.cs +++ b/src/Files.App/Helpers/MenuFlyout/MenuFlyoutHelper.cs @@ -18,11 +18,11 @@ public sealed class MenuFlyoutItemViewModel : IMenuFlyoutItemViewModel { public string Text { get; init; } - public ICommand Command { get; init; } + public ICommand? Command { get; init; } - public object CommandParameter { get; init; } + public object? CommandParameter { get; init; } - public string Tooltip { get; init; } + public string? Tooltip { get; init; } public bool IsEnabled { get; set; } = true; @@ -50,13 +50,17 @@ public MenuFlyoutFactoryItemViewModel(Func factoryFunc) => Build = factoryFunc; } - public static IEnumerable GetItemsSource(DependencyObject obj) => obj.GetValue(ItemsSourceProperty) as IEnumerable; + public static IEnumerable? GetItemsSource(DependencyObject obj) => obj.GetValue(ItemsSourceProperty) as IEnumerable; public static void SetItemsSource(DependencyObject obj, IEnumerable value) => obj.SetValue(ItemsSourceProperty, value); public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached("ItemsSource", typeof(IEnumerable), typeof(MenuFlyoutHelper), new PropertyMetadata(null, ItemsSourceChanged)); - private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => _ = SetupItemsAsync(d as MenuFlyout); + private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MenuFlyout menu) + _ = SetupItemsAsync(menu); + } public static bool GetIsVisible(DependencyObject d) => (bool)d.GetValue(IsVisibleProperty); diff --git a/src/Files.App/Helpers/NaturalStringComparer.cs b/src/Files.App/Helpers/NaturalStringComparer.cs index 003b8ca1f1b3..d7102e6ac07c 100644 --- a/src/Files.App/Helpers/NaturalStringComparer.cs +++ b/src/Files.App/Helpers/NaturalStringComparer.cs @@ -6,7 +6,7 @@ namespace Files.App.Helpers // Credit: https://github.com/GihanSoft/NaturalStringComparer public sealed class NaturalStringComparer { - public static IComparer GetForProcessor() + public static IComparer GetForProcessor() { return new NaturalComparer(StringComparison.CurrentCultureIgnoreCase); } diff --git a/src/Files.App/Helpers/Navigation/MultitaskingTabsHelpers.cs b/src/Files.App/Helpers/Navigation/MultitaskingTabsHelpers.cs index f24bc2d5a97f..b4f12d4396d7 100644 --- a/src/Files.App/Helpers/Navigation/MultitaskingTabsHelpers.cs +++ b/src/Files.App/Helpers/Navigation/MultitaskingTabsHelpers.cs @@ -48,7 +48,10 @@ public static void CloseAllTabs(ITabBar multitaskingControl) public static Task MoveTabToNewWindow(TabBarItem tab, ITabBar multitaskingControl) { int index = MainPageViewModel.AppInstances.IndexOf(tab); - TabBarItemParameter tabItemArguments = MainPageViewModel.AppInstances[index].NavigationParameter; + if (index < 0) + return Task.CompletedTask; + + TabBarItemParameter? tabItemArguments = MainPageViewModel.AppInstances[index].NavigationParameter; multitaskingControl?.CloseTab(MainPageViewModel.AppInstances[index]); diff --git a/src/Files.App/Helpers/Navigation/NavigationHelpers.cs b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs index 5d61c1a5bde3..f31eacdccf1b 100644 --- a/src/Files.App/Helpers/Navigation/NavigationHelpers.cs +++ b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs @@ -85,7 +85,7 @@ public static async Task AddNewTabByPathAsync(Type type, string? path, bool swit App.AppModel.TabStripSelectedIndex = index; } - public static async Task AddNewTabByParamAsync(Type type, object tabViewItemArgs, int atIndex = -1, bool switchToNewTab = true) + public static async Task AddNewTabByParamAsync(Type type, object? tabViewItemArgs, int atIndex = -1, bool switchToNewTab = true) { var tabItem = new Files.App.UserControls.TabBar.TabBarItem() { @@ -112,11 +112,11 @@ public static async Task AddNewTabByParamAsync(Type type, object tabViewItemArgs App.AppModel.TabStripSelectedIndex = index; } - private static async Task UpdateTabInfoAsync(TabBarItem tabItem, object navigationArg) + private static async Task UpdateTabInfoAsync(TabBarItem tabItem, object? navigationArg) { tabItem.AllowStorageItemDrop = true; - (string, IconSource, string) result = (null, null, null); + (string? Header, IconSource? Icon, string? ToolTip) result = default; if (navigationArg is PaneNavigationArguments paneArgs) { if (!string.IsNullOrEmpty(paneArgs.LeftPaneNavPathParam) && !string.IsNullOrEmpty(paneArgs.RightPaneNavPathParam)) @@ -145,7 +145,9 @@ private static async Task UpdateTabInfoAsync(TabBarItem tabItem, object navigati // Don't update tabItem if the contents of the tab have already changed if (result.Item1 is not null) { - var navigationParameter = tabItem.NavigationParameter.NavigationParameter; + if (tabItem.NavigationParameter?.NavigationParameter is not { } navigationParameter) + return; + var a1 = navigationParameter is PaneNavigationArguments pna1 ? pna1 : new PaneNavigationArguments() { LeftPaneNavPathParam = navigationParameter as string }; var a2 = navigationArg is PaneNavigationArguments pna2 ? pna2 : new PaneNavigationArguments() { LeftPaneNavPathParam = navigationArg as string }; @@ -163,14 +165,14 @@ internal static void RefreshTabPathHints() { foreach (var group in MainPageViewModel.AppInstances .Where(t => !string.IsNullOrEmpty(t.Description)) - .GroupBy(t => t.Description!, StringComparer.OrdinalIgnoreCase)) + .GroupBy(t => t.Description ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { var tabs = group.ToArray(); foreach (var t in tabs) t.Header = t.Description; - if (tabs.Length < 2 || tabs[0].Description!.Contains(" | ")) + if (tabs.Length < 2 || tabs[0].Description?.Contains(" | ", StringComparison.Ordinal) == true) continue; var hints = tabs.ToDictionary(t => t, t => AncestorHints(t.ToolTipText)); @@ -195,7 +197,7 @@ private static string[] AncestorHints(string? path) try { - var root = (PathNormalization.GetPathRoot(path) ?? "").TrimEnd('\\', '/'); + var root = PathNormalization.GetPathRoot(path).TrimEnd('\\', '/'); var prefix = root.Length >= 2 && root[1] == ':' ? $"{char.ToUpperInvariant(root[0])}:\\..." : root.Length > 0 ? $"{root}\\..." : "..."; @@ -216,7 +218,10 @@ private static string[] AncestorHints(string? path) { ImageSource? imageSource; if (string.IsNullOrEmpty(path) || path == "Home") - imageSource = new BitmapImage(new Uri(SidebarSectionIcons.For(SectionType.Home)!)); + { + var iconPath = SidebarSectionIcons.For(SectionType.Home); + imageSource = iconPath is null ? null : new BitmapImage(new Uri(iconPath)); + } else if (path == "ReleaseNotes") imageSource = new BitmapImage(new Uri(AppLifecycleHelper.AppIconPath)); else if (path == "Settings") @@ -248,14 +253,15 @@ private static string[] AncestorHints(string? path) public static async Task<(string tabLocationHeader, IconSource tabIcon, string toolTipText)> GetSelectedTabInfoAsync(string currentPath) { - string? tabLocationHeader; + string tabLocationHeader; IconSource iconSource = new ImageIconSource(); string toolTipText = currentPath; if (string.IsNullOrEmpty(currentPath) || currentPath == "Home") { tabLocationHeader = Strings.Home.GetLocalizedResource(); - ((ImageIconSource)iconSource).ImageSource = new BitmapImage(new Uri(SidebarSectionIcons.For(SectionType.Home)!)); + if (SidebarSectionIcons.For(SectionType.Home) is { } iconPath) + ((ImageIconSource)iconSource).ImageSource = new BitmapImage(new Uri(iconPath)); } else if (currentPath == "ReleaseNotes") { @@ -278,7 +284,7 @@ private static string[] AncestorHints(string? path) tabLocationHeader = Strings.ThisPC.GetLocalizedResource(); else if (currentPath.Equals(Constants.UserEnvironmentPaths.NetworkFolderPath, StringComparison.OrdinalIgnoreCase)) tabLocationHeader = Strings.Network.GetLocalizedResource(); - else if (App.LibraryManager.TryGetLibrary(currentPath, out LibraryLocationItem library)) + else if (App.LibraryManager.TryGetLibrary(currentPath, out var library)) { var libName = System.IO.Path.GetFileNameWithoutExtension(library.Path).GetLocalizedResource(); // If localized string is empty use the library name. @@ -308,11 +314,11 @@ private static string[] AncestorHints(string? path) { tabLocationHeader = currentPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar).Split('\\', StringSplitOptions.RemoveEmptyEntries).Last(); - FilesystemResult rootItem = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(currentPath)); - if (rootItem) + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(currentPath)); + if (rootItem.Result is { } rootFolder) { - BaseStorageFolder currentFolder = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(currentPath, rootItem)); - if (currentFolder is not null && !string.IsNullOrEmpty(currentFolder.DisplayName)) + var currentFolderResult = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(currentPath, rootFolder)); + if (currentFolderResult.Result is { } currentFolder && !string.IsNullOrEmpty(currentFolder.DisplayName)) tabLocationHeader = currentFolder.DisplayName; } } @@ -347,7 +353,7 @@ await SafetyExtensions.IgnoreExceptions(async () => windowTitle = $"{leftTabInfo.tabLocationHeader} | {rightTabInfo.tabLocationHeader}"; } else - (windowTitle, _, _) = await GetSelectedTabInfoAsync(paneArgs.LeftPaneNavPathParam); + (windowTitle, _, _) = await GetSelectedTabInfoAsync(paneArgs.LeftPaneNavPathParam ?? paneArgs.RightPaneNavPathParam ?? string.Empty); } else if (navigationArg is string pathArgs) (windowTitle, _, _) = await GetSelectedTabInfoAsync(pathArgs); @@ -518,7 +524,7 @@ public static async Task OpenPath(string path, IShellPage associatedInstan Win32Helper.GetWin32FindDataForPath(path, out var findData) && findData.dwReserved0 == Win32PInvoke.IO_REPARSE_TAG_SYMLINK) { - shortcutInfo.TargetPath = Win32Helper.ParseSymLink(path); + shortcutInfo.TargetPath = Win32Helper.ParseSymLink(path) ?? string.Empty; } itemType ??= isDirectory ? FilesystemItemType.Directory : FilesystemItemType.File; } @@ -572,7 +578,7 @@ private static async Task OpenLibrary(string path, IShellPage await OpenPath(forceOpenInNewTab, UserSettingsService.FoldersSettingsService.OpenFoldersInNewTab, path, associatedInstance); opened = (FilesystemResult)true; } - else if (App.LibraryManager.TryGetLibrary(path, out LibraryLocationItem library)) + else if (App.LibraryManager.TryGetLibrary(path, out var library)) { opened = (FilesystemResult)await library.CheckDefaultSaveFolderAccess(); if (opened) @@ -648,9 +654,9 @@ private static async Task OpenFile(string path, IShellPage ass { if (!FileExtensionHelpers.IsWebLinkFile(path) && associatedInstance.ShellViewModel is not null) { - StorageFileWithPath childFile = await associatedInstance.ShellViewModel.GetFileWithPathFromPathAsync(shortcutInfo.TargetPath); + var childFileResult = await associatedInstance.ShellViewModel.GetFileWithPathFromPathAsync(shortcutInfo.TargetPath); // Add location to Recent Items List - if (childFile?.Item is SystemStorageFile) + if (childFileResult.Result is { Item: SystemStorageFile } childFile) WindowsRecentItemsService.Add(childFile.Path); } await Win32Helper.InvokeWin32ComponentAsync(shortcutInfo.TargetPath, associatedInstance, $"{args} {shortcutInfo.Arguments}", shortcutInfo.RunAsAdmin, shortcutInfo.WorkingDirectory); @@ -676,11 +682,17 @@ private static async Task OpenFile(string path, IShellPage ass if (openViaApplicationPicker) { + if (childFile.Item is not { } storageFile) + { + await Win32Helper.InvokeWin32ComponentAsync(path, associatedInstance, args); + return; + } + LauncherOptions options = InitializeWithWindow(new LauncherOptions { DisplayApplicationPicker = true }); - if (!await Launcher.LaunchFileAsync(childFile.Item, options)) + if (!await Launcher.LaunchFileAsync(storageFile, options)) await ContextMenu.InvokeVerb("openas", path); } else @@ -704,8 +716,8 @@ private static async Task OpenFile(string path, IShellPage ass BaseStorageFileQueryResult? fileQueryResult = null; //Get folder to create a file query (to pass to apps like Photos, Movies & TV..., needed to scroll through the folder like what Windows Explorer does) - BaseStorageFolder currentFolder = await shellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(path)); - if (!launchSuccess && currentFolder is not null) + var currentFolderResult = await shellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(path)); + if (!launchSuccess && currentFolderResult.Result is { } currentFolder) { QueryOptions queryOptions = new(CommonFileQuery.DefaultQuery, null); //We can have many sort entries @@ -756,9 +768,11 @@ private static async Task OpenFile(string path, IShellPage ass options.NeighboringFilesQuery = fileQueryResult.ToStorageFileQueryResult(); } // Now launch file with options. - var storageItem = (StorageFile)await FilesystemTasks.Wrap(() => childFile.Item.ToStorageFileAsync().AsTask()); - if (storageItem is not null) + if (childFile.Item is { } item && + (await FilesystemTasks.Wrap(() => item.ToStorageFileAsync().AsTask())).Result is { } storageItem) + { launchSuccess = await Launcher.LaunchFileAsync(storageItem, options); + } } if (!launchSuccess) await Win32Helper.InvokeWin32ComponentAsync(path, associatedInstance, args); @@ -766,7 +780,7 @@ private static async Task OpenFile(string path, IShellPage ass else if (childFile.Item is ZipStorageFile zipStorageFile) { var options = InitializeWithWindow(new LauncherOptions()); - var storageItem = (StorageFile)await FilesystemTasks.Wrap(() => zipStorageFile.ToStorageFileAsync().AsTask()); + var storageItem = (await FilesystemTasks.Wrap(() => zipStorageFile.ToStorageFileAsync().AsTask())).Result; if (storageItem is null) { await Win32Helper.InvokeWin32ComponentAsync(path, associatedInstance, args); diff --git a/src/Files.App/Helpers/PathNormalization.cs b/src/Files.App/Helpers/PathNormalization.cs index de584ea07db0..2954d55afd81 100644 --- a/src/Files.App/Helpers/PathNormalization.cs +++ b/src/Files.App/Helpers/PathNormalization.cs @@ -2,13 +2,14 @@ // Licensed under the MIT License. using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace Files.App.Helpers { public static class PathNormalization { - public static string GetPathRoot(string path) + public static string GetPathRoot(string? path) { if (string.IsNullOrEmpty(path)) return string.Empty; @@ -30,7 +31,8 @@ public static string GetPathRoot(string path) return rootPath; } - public static string NormalizePath(string path) + [return: NotNullIfNotNull(nameof(path))] + public static string? NormalizePath(string? path) { if (string.IsNullOrEmpty(path)) return path; @@ -58,12 +60,12 @@ public static string NormalizePath(string path) } } - public static string? TrimPath(this string path) + public static string? TrimPath(this string? path) { return path?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } - public static string GetParentDir(string path) + public static string GetParentDir(string? path) { if (string.IsNullOrEmpty(path)) return string.Empty; @@ -89,4 +91,4 @@ public static string Combine(string folder, string name) return folder.Contains('/', StringComparison.Ordinal) ? Path.Combine(folder, name).Replace('\\', '/') : Path.Combine(folder, name); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Helpers/TransferHelpers.cs b/src/Files.App/Helpers/TransferHelpers.cs index 00d13b8920fa..5620e6f6fdc7 100644 --- a/src/Files.App/Helpers/TransferHelpers.cs +++ b/src/Files.App/Helpers/TransferHelpers.cs @@ -76,12 +76,12 @@ await itemsToTransfer.ParallelForEachAsync(async storable => public static async Task ExecuteTransferAsync(IContentPageContext context, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy) { - if (context.ShellPage?.SlimContentPage is null || - context.ShellPage.SlimContentPage.IsItemSelected is false) + if (context.ShellPage is not { SlimContentPage: { } contentPage, ShellViewModel: { } shellViewModel } || + contentPage.IsItemSelected is false) return; // Reset cut mode - context.ShellPage.SlimContentPage.ItemManipulationModel.RefreshItemsOpacity(); + contentPage.ItemManipulationModel.RefreshItemsOpacity(); ConcurrentBag items = []; var itemsCount = context.SelectedItems.Count; @@ -125,8 +125,8 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => } var result = listedItem.PrimaryItemAttribute == StorageItemTypes.File || listedItem is ZipItem - ? await context.ShellPage.ShellViewModel.GetFileFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t)) - : await context.ShellPage.ShellViewModel.GetFolderFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t)); + ? await shellViewModel.GetFileFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t)) + : await shellViewModel.GetFolderFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t)); if (!result) throw new SystemIO.IOException($"Failed to process {listedItem.ItemPath} in cutting/copying to the clipboard.", (int)result.ErrorCode); diff --git a/src/Files.App/Helpers/UI/AppSystemBackdrop.cs b/src/Files.App/Helpers/UI/AppSystemBackdrop.cs index 233fde8dc89e..875b20e4303b 100644 --- a/src/Files.App/Helpers/UI/AppSystemBackdrop.cs +++ b/src/Files.App/Helpers/UI/AppSystemBackdrop.cs @@ -2,18 +2,17 @@ using Microsoft.UI.Composition.SystemBackdrops; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; -using System.Diagnostics.CodeAnalysis; using Windows.UI; namespace Files.App.Helpers { internal sealed partial class AppSystemBackdrop : SystemBackdrop { - private bool isSecondaryWindow; - private IUserSettingsService userSettingsService; + private readonly bool isSecondaryWindow; + private readonly IUserSettingsService userSettingsService; private ISystemBackdropControllerWithTargets? controller; - private ICompositionSupportsSystemBackdrop target; - private XamlRoot root; + private ICompositionSupportsSystemBackdrop? target; + private XamlRoot? root; private SystemBackdropTheme? prevTheme = null; public AppSystemBackdrop(bool isSecondaryWindow = false) @@ -23,7 +22,6 @@ public AppSystemBackdrop(bool isSecondaryWindow = false) userSettingsService.OnSettingChangedEvent += OnSettingChanged; } - [MemberNotNull(nameof(target), nameof(root))] protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot) { if (target is not null) @@ -52,8 +50,8 @@ protected override void OnDefaultSystemBackdropConfigurationChanged(IComposition protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget) { base.OnTargetDisconnected(disconnectedTarget); - this.target = null!; - this.root = null!; + this.target = null; + this.root = null; try @@ -71,7 +69,7 @@ protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop private void OnSettingChanged(object? sender, SettingChangedEventArgs e) { - if (target is null) + if (target is null || root is null) return; switch (e.SettingName) diff --git a/src/Files.App/Helpers/UI/DragZoneHelper.cs b/src/Files.App/Helpers/UI/DragZoneHelper.cs index dbdcb3d9ae59..217ea46dff72 100644 --- a/src/Files.App/Helpers/UI/DragZoneHelper.cs +++ b/src/Files.App/Helpers/UI/DragZoneHelper.cs @@ -24,10 +24,7 @@ public static void RaiseSetTitleBarDragRegion(this Window window, SetTitleBarDra return; // UIElement.RasterizationScale is always 1 var source = InputNonClientPointerSource.GetForWindowId(window.AppWindow.Id); - var uiElement = window.Content; - var xamlRoot = uiElement?.XamlRoot; - - if (xamlRoot is null) + if (window.Content is not UIElement { XamlRoot: { } xamlRoot } uiElement) return; var scaleFactor = xamlRoot.RasterizationScale; @@ -61,4 +58,4 @@ private static RectInt32 GetScaledRect(this UIElement uiElement, RectInt32? r = } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs index 56d2083c35b7..9bbdb68f9dd0 100644 --- a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs +++ b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs @@ -18,9 +18,9 @@ public static class UIFilesystemHelpers public static async Task PasteItemAsync(string destinationPath, IShellPage associatedInstance) { FilesystemResult packageView = await FilesystemTasks.Wrap(() => Task.FromResult(Clipboard.GetContent())); - if (packageView && packageView.Result is not null) + if (packageView && packageView.Result is { } content) { - await associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(packageView.Result.RequestedOperation, packageView, destinationPath, false, true); + await associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(content.RequestedOperation, content, destinationPath, false, true); associatedInstance.SlimContentPage?.ItemManipulationModel?.RefreshItemsOpacity(); await associatedInstance.RefreshIfNoWatcherExistsAsync(); } @@ -29,9 +29,9 @@ public static async Task PasteItemAsync(string destinationPath, IShellPage assoc public static async Task PasteItemAsShortcutAsync(string destinationPath, IShellPage associatedInstance) { FilesystemResult packageView = await FilesystemTasks.Wrap(() => Task.FromResult(Clipboard.GetContent())); - if (packageView.Result.Contains(StandardDataFormats.StorageItems)) + if (packageView.Result is { } content && content.Contains(StandardDataFormats.StorageItems)) { - var items = await packageView.Result.GetStorageItemsAsync(); + var items = await content.GetStorageItemsAsync(); await Task.WhenAll(items.Select(async item => { var fileName = FilesystemHelpers.GetShortcutNamingPreference(item.Name); @@ -93,7 +93,7 @@ public static async Task CreateFileFromDialogResultTypeAsync(AddItemDialogItemTy if (associatedInstance.SlimContentPage is not null) { - currentPath = associatedInstance.ShellViewModel.WorkingDirectory; + currentPath = associatedInstance.ShellViewModel?.WorkingDirectory; if (App.LibraryManager.TryGetLibrary(currentPath, out var library) && !library.IsEmpty && library.Folders.Count == 1) // TODO: handle libraries with multiple folders @@ -101,6 +101,8 @@ public static async Task CreateFileFromDialogResultTypeAsync(AddItemDialogItemTy currentPath = library.Folders.First(); } } + if (string.IsNullOrEmpty(currentPath)) + return null; // Skip rename dialog when ShellNewEntry has a Command (e.g. ".accdb", ".gdoc") string? userInput = null; @@ -116,7 +118,7 @@ public static async Task CreateFileFromDialogResultTypeAsync(AddItemDialogItemTy } // Create file based on dialog result - (ReturnResult Status, IStorageItem Item) created = (ReturnResult.Failed, null); + (ReturnResult Status, IStorageItem? Item) created = (ReturnResult.Failed, null); switch (itemType) { case AddItemDialogItemType.Folder: @@ -156,9 +158,12 @@ public static async Task CreateFolderWithSelectionAsync(IShellPage associatedIns { try { - var items = associatedInstance.SlimContentPage.SelectedItems.ToList().Select((item) => StorageHelpers.FromPathAndType( + if (associatedInstance.SlimContentPage?.SelectedItems is not { } selectedItems) + return; + + var items = selectedItems.Select((item) => StorageHelpers.FromPathAndType( item.ItemPath, - item.PrimaryItemAttribute == StorageItemTypes.File ? FilesystemItemType.File : FilesystemItemType.Directory)); + item.PrimaryItemAttribute == StorageItemTypes.File ? FilesystemItemType.File : FilesystemItemType.Directory)).ToList(); var folder = await CreateFileFromDialogResultTypeForResult(AddItemDialogItemType.Folder, null, associatedInstance); if (folder is null) return; @@ -186,7 +191,7 @@ public static void SetHiddenAttributeItem(ListedItem item, bool isHidden, ItemMa public static async Task CreateShortcutAsync(IShellPage? associatedInstance, IReadOnlyList selectedItems) { - var currentPath = associatedInstance?.ShellViewModel.WorkingDirectory; + var currentPath = associatedInstance?.ShellViewModel?.WorkingDirectory; if (App.LibraryManager.TryGetLibrary(currentPath ?? string.Empty, out var library) && !library.IsEmpty) currentPath = library.DefaultSaveFolder; @@ -206,7 +211,10 @@ public static async Task CreateShortcutAsync(IShellPage? associatedInstance, IRe public static async Task CreateShortcutFromDialogAsync(IShellPage associatedInstance) { - var currentPath = associatedInstance.ShellViewModel.WorkingDirectory; + var currentPath = associatedInstance.ShellViewModel?.WorkingDirectory; + if (string.IsNullOrEmpty(currentPath)) + return; + if (App.LibraryManager.TryGetLibrary(currentPath, out var library) && !library.IsEmpty) { @@ -279,10 +287,12 @@ public async static Task RequestPassword(IPasswordProtectedIt if (credentialDialogViewModel.IsAnonymous) return new(); + if (credentialDialogViewModel.Password is not { } password) + throw new InvalidOperationException("A password is required for authenticated credentials."); // Can't do more than that to mitigate immutability of strings. Perhaps convert DisposableArray to SecureString immediately? - var credentials = new StorageCredential(credentialDialogViewModel.UserName, Encoding.UTF8.GetString(credentialDialogViewModel.Password)); - credentialDialogViewModel.Password?.Dispose(); + var credentials = new StorageCredential(credentialDialogViewModel.UserName, Encoding.UTF8.GetString(password)); + password.Dispose(); if (isFtp) { @@ -293,4 +303,4 @@ public async static Task RequestPassword(IPasswordProtectedIt return credentials; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Helpers/UI/UIHelpers.cs b/src/Files.App/Helpers/UI/UIHelpers.cs index 3fe0bf992316..aebec2f4e4dc 100644 --- a/src/Files.App/Helpers/UI/UIHelpers.cs +++ b/src/Files.App/Helpers/UI/UIHelpers.cs @@ -128,11 +128,11 @@ public static void CloseAllDialogs() private static IEnumerable SidebarIconResources = LoadSidebarIconResources(); - private static IconFileInfo ShieldIconResource = LoadShieldIconResource(); + private static IconFileInfo? ShieldIconResource = LoadShieldIconResource(); - private static IconFileInfo SearchIconResource = LoadSearchIconResource(); + private static IconFileInfo? SearchIconResource = LoadSearchIconResource(); - public static IconFileInfo GetSidebarIconResourceInfo(int index) + public static IconFileInfo? GetSidebarIconResourceInfo(int index) { var icons = UIHelpers.SidebarIconResources; return icons?.FirstOrDefault(x => x.Index == index); @@ -173,7 +173,7 @@ private static IEnumerable LoadSidebarIconResources() return imageResList; } - private static IconFileInfo LoadShieldIconResource() + private static IconFileInfo? LoadShieldIconResource() { string imageres = Path.Combine(Constants.UserEnvironmentPaths.SystemRootPath, "System32", "imageres.dll"); var imageResList = Win32Helper.ExtractSelectedIconsFromDLL(imageres, new List() { @@ -183,7 +183,7 @@ private static IconFileInfo LoadShieldIconResource() return imageResList.FirstOrDefault(); } - private static IconFileInfo LoadSearchIconResource() + private static IconFileInfo? LoadSearchIconResource() { string imageres = Path.Combine(Constants.UserEnvironmentPaths.SystemRootPath, "System32", "imageres.dll"); var imageResList = Win32Helper.ExtractSelectedIconsFromDLL(imageres, new List() { diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Process.cs b/src/Files.App/Helpers/Win32/Win32Helper.Process.cs index ddffe517f545..a88e50373c0d 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Process.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Process.cs @@ -12,17 +12,22 @@ namespace Files.App.Helpers /// public static partial class Win32Helper { - public static async Task InvokeWin32ComponentAsync(string applicationPath, IShellPage associatedInstance, string arguments = null, bool runAsAdmin = false, string workingDirectory = null) + public static async Task InvokeWin32ComponentAsync(string applicationPath, IShellPage associatedInstance, string? arguments = null, bool runAsAdmin = false, string? workingDirectory = null) { return await InvokeWin32ComponentsAsync(applicationPath.CreateEnumerable(), associatedInstance, arguments, runAsAdmin, workingDirectory); } - public static async Task InvokeWin32ComponentsAsync(IEnumerable applicationPaths, IShellPage associatedInstance, string arguments = null, bool runAsAdmin = false, string workingDirectory = null) + public static async Task InvokeWin32ComponentsAsync(IEnumerable applicationPaths, IShellPage associatedInstance, string? arguments = null, bool runAsAdmin = false, string? workingDirectory = null) { var application = applicationPaths.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(application)) + return false; - if (string.IsNullOrEmpty(workingDirectory) && associatedInstance?.ShellViewModel != null && !associatedInstance.ShellViewModel.IsSearchResults) - workingDirectory = associatedInstance.ShellViewModel.WorkingDirectory; + if (string.IsNullOrEmpty(workingDirectory) && + associatedInstance.ShellViewModel is { IsSearchResults: false } shellViewModel) + { + workingDirectory = shellViewModel.WorkingDirectory; + } if (runAsAdmin) { @@ -32,9 +37,9 @@ public static async Task InvokeWin32ComponentsAsync(IEnumerable ap ProcessStartInfo startInfo = new ProcessStartInfo { FileName = application, - Arguments = arguments, + Arguments = arguments ?? string.Empty, Verb = "runas", - WorkingDirectory = workingDirectory, + WorkingDirectory = workingDirectory ?? string.Empty, UseShellExecute = true }; @@ -54,7 +59,7 @@ public static async Task InvokeWin32ComponentsAsync(IEnumerable ap } else { - return await LaunchHelper.LaunchAppAsync(application, arguments, workingDirectory); + return await LaunchHelper.LaunchAppAsync(application, arguments ?? string.Empty, workingDirectory ?? string.Empty); } } diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs index ef0562e846f9..27c21897a5ec 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs @@ -16,7 +16,7 @@ namespace Files.App.Helpers /// public static partial class Win32Helper { - public static async Task<(ShellFileItem Folder, List Enumerate)> GetShellFolderAsync(string path, bool getFolder, bool getEnumerate, int from, int count, params string[] properties) + public static async Task<(ShellFileItem? Folder, List Enumerate)> GetShellFolderAsync(string path, bool getFolder, bool getEnumerate, int from, int count, params string[] properties) { if (path.StartsWith("::{", StringComparison.Ordinal)) { @@ -26,7 +26,7 @@ public static partial class Win32Helper return await STATask.Run(() => { var flc = new List(); - var folder = (ShellFileItem)null; + ShellFileItem? folder = null; try { @@ -56,6 +56,8 @@ public static partial class Win32Helper var shellFileItem = folderItem is ShellLink link ? ShellFolderExtensions.GetShellLinkItem(link) : ShellFolderExtensions.GetShellFileItem(folderItem); + if (shellFileItem is null) + continue; foreach (var prop in properties) shellFileItem.Properties[prop] = SafetyExtensions.IgnoreExceptions(() => folderItem.Properties[prop]); diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs index 65f047c1b150..b090059201e5 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs @@ -115,8 +115,10 @@ public static string ExtractStringFromDLL(string file, int number) if (!hOverlay.IsNull && !hOverlay.IsInvalid) { using var icon = hOverlay.ToIcon(); - using var image = icon.ToBitmap(); + if (icon is null) + return null; + using var image = icon.ToBitmap(); overlayData = (byte[]?)new ImageConverter().ConvertTo(image, typeof(byte[])); } } @@ -224,9 +226,12 @@ public static string ExtractStringFromDLL(string file, int number) if (!hIcon.IsNull && !hIcon.IsInvalid) { using (var icon = hIcon.ToIcon()) - using (var image = icon.ToBitmap()) { - iconData = (byte[]?)new ImageConverter().ConvertTo(image, typeof(byte[])); + if (icon is not null) + { + using var image = icon.ToBitmap(); + iconData = (byte[]?)new ImageConverter().ConvertTo(image, typeof(byte[])); + } } } } @@ -378,7 +383,7 @@ public static bool SetCustomDirectoryIcon(string? folderPath, string? iconFile, var fcs = new Shell32.SHFOLDERCUSTOMSETTINGS() { dwMask = Shell32.FOLDERCUSTOMSETTINGSMASK.FCSM_ICONFILE, - pszIconFile = iconFile, + pszIconFile = iconFile ?? string.Empty, cchIconFile = 0, iIconIndex = iconIndex, }; @@ -561,6 +566,8 @@ public static void BringToForeground(IEnumerable currentWindows) public static string? PathFromFileId(ulong frn, string volumeHint) { string? volumePath = Path.GetPathRoot(volumeHint); + if (string.IsNullOrEmpty(volumePath)) + return null; using var volumeHandle = Kernel32.CreateFile(volumePath, Kernel32.FileAccess.GENERIC_READ, FileShare.Read, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS); if (volumeHandle.IsInvalid) @@ -601,8 +608,7 @@ public static void OpenFolderInExistingShellWindow(string folderPath) { var item = shellWindows.Item(i); - var serv = (Shell32.IServiceProvider)item; - if (serv is not null) + if (item is Shell32.IServiceProvider serv) { if (serv.QueryService(Shell32.SID_STopLevelBrowser, typeof(Shell32.IShellBrowser).GUID, out var ppv).Succeeded) { @@ -612,9 +618,9 @@ public static void OpenFolderInExistingShellWindow(string folderPath) using var targetFolder = SafetyExtensions.IgnoreExceptions(() => new Vanara.Windows.Shell.ShellItem(folderPath)); if (targetFolder is not null) { - if (shellBrowser.QueryActiveShellView(out var shellView).Succeeded) + if (shellBrowser.QueryActiveShellView(out var shellView).Succeeded && + shellView is Shell32.IFolderView folderView) { - var folderView = (Shell32.IFolderView)shellView; var folder = folderView.GetFolder(); var folderPidl = new Shell32.PIDL(IntPtr.Zero); @@ -647,7 +653,8 @@ public static void OpenFolderInExistingShellWindow(string folderPath) Marshal.ReleaseComObject(serv); } - Marshal.ReleaseComObject(item); + if (item is not null) + Marshal.ReleaseComObject(item); } Marshal.ReleaseComObject(shellWindows); @@ -858,7 +865,7 @@ public static unsafe bool SetCompressionAttributeIoctl(string lpFileName, bool i return result; } - public static string ReadStringFromFile(string filePath) + public static string? ReadStringFromFile(string filePath) { IntPtr hFile = Win32PInvoke.CreateFileFromApp(filePath, (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ, @@ -994,7 +1001,7 @@ public static bool WriteBufferToFileWithProgress(string filePath, byte[] buffer, } // https://github.com/rad1oactive/BetterExplorer/blob/master/Windows%20API%20Code%20Pack%201.1/source/WindowsAPICodePack/Shell/ReparsePoint.cs - public static string ParseSymLink(string path) + public static string? ParseSymLink(string path) { using var handle = OpenFileForRead(path, false, 0x00200000); if (!handle.IsInvalid) diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs index fec7a25f37e4..1ea510d38c22 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs @@ -99,7 +99,7 @@ public sealed class WIN32_FIND_STREAM_DATA { public long StreamSize; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 296)] - public string cStreamName; + public string cStreamName = string.Empty; } [StructLayout(LayoutKind.Sequential)] diff --git a/src/Files.App/Helpers/Xaml/DependencyObjectHelpers.cs b/src/Files.App/Helpers/Xaml/DependencyObjectHelpers.cs index 938bf6917776..5c64c153ca97 100644 --- a/src/Files.App/Helpers/Xaml/DependencyObjectHelpers.cs +++ b/src/Files.App/Helpers/Xaml/DependencyObjectHelpers.cs @@ -9,7 +9,7 @@ namespace Files.App.Helpers { public static class DependencyObjectHelpers { - public static T FindChild(DependencyObject startNode) where T : DependencyObject + public static T? FindChild(DependencyObject startNode) where T : DependencyObject { int count = VisualTreeHelper.GetChildrenCount(startNode); for (int i = 0; i < count; i++) @@ -29,7 +29,7 @@ public static T FindChild(DependencyObject startNode) where T : DependencyObj return null; } - public static T FindChild(DependencyObject startNode, Func predicate) where T : DependencyObject + public static T? FindChild(DependencyObject startNode, Func predicate) where T : DependencyObject { int count = VisualTreeHelper.GetChildrenCount(startNode); for (int i = 0; i < count; i++) @@ -70,14 +70,14 @@ public static IEnumerable FindChildren(DependencyObject startNode) where T } } - public static T FindParent(DependencyObject child) where T : DependencyObject + public static T? FindParent(DependencyObject? child) where T : DependencyObject { - T parent = null; + T? parent = null; if (child is null) { return parent; } - DependencyObject CurrentParent = VisualTreeHelper.GetParent(child); + DependencyObject? CurrentParent = VisualTreeHelper.GetParent(child); while (CurrentParent is not null) { if (CurrentParent is T) @@ -90,4 +90,4 @@ public static T FindParent(DependencyObject child) where T : DependencyObject return parent; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Helpers/Xaml/SystemTypeToXaml.cs b/src/Files.App/Helpers/Xaml/SystemTypeToXaml.cs index 8ea8c858c5cb..2080e17a5594 100644 --- a/src/Files.App/Helpers/Xaml/SystemTypeToXaml.cs +++ b/src/Files.App/Helpers/Xaml/SystemTypeToXaml.cs @@ -9,7 +9,7 @@ public sealed partial class SystemTypeToXaml : MarkupExtension { #region Private Members - private object parameter; + private object? parameter; #endregion Private Members @@ -27,7 +27,8 @@ public sealed partial class SystemTypeToXaml : MarkupExtension protected override object ProvideValue() { - return parameter; + return parameter + ?? throw new InvalidOperationException("A value must be supplied to the markup extension."); } } -} \ No newline at end of file +} diff --git a/src/Files.App/MainWindow.xaml.cs b/src/Files.App/MainWindow.xaml.cs index b5bf7fd00115..02c10cb6cc23 100644 --- a/src/Files.App/MainWindow.xaml.cs +++ b/src/Files.App/MainWindow.xaml.cs @@ -50,7 +50,7 @@ public void ShowSplashScreen() rootFrame?.Navigate(typeof(SplashScreenPage)); } - public async Task InitializeApplicationAsync(object activatedEventArgs) + public async Task InitializeApplicationAsync(object? activatedEventArgs) { var rootFrame = EnsureWindowIsInitialized(); @@ -113,8 +113,8 @@ public async Task InitializeApplicationAsync(object activatedEventArgs) int.TryParse(parsedArgs[2].Split('&')[0], out var dx) && int.TryParse(parsedArgs[3], out var dy)) AppWindow?.Move(new(dx - 100, dy - 16)); - var folder = (StorageFolder)await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(unescapedValue).AsTask()); - if (folder is not null && !string.IsNullOrEmpty(folder.Path)) + var folderResult = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(unescapedValue).AsTask()); + if (folderResult.Result is { } folder && !string.IsNullOrEmpty(folder.Path)) { // Convert short name to long name (#6190) unescapedValue = folder.Path; @@ -229,7 +229,7 @@ public async Task InitializeApplicationAsync(object activatedEventArgs) private async Task EnsureContentHasKeyboardFocusAsync() { await Task.Delay(100); - Ioc.Default.GetService()?.ShellPage?.PaneHolder.FocusActivePane(); + Ioc.Default.GetService()?.ShellPage?.PaneHolder?.FocusActivePane(); } private Frame? EnsureWindowIsInitialized() @@ -262,13 +262,13 @@ private async Task EnsureContentHasKeyboardFocusAsync() private async Task InitializeFromCmdLineArgsAsync(Frame rootFrame, ParsedCommands parsedCommands, string activationPath = "") { - async Task PerformNavigationAsync(string payload, string selectItem = null) + async Task PerformNavigationAsync(string? payload, string? selectItem = null) { if (!string.IsNullOrEmpty(payload)) { - payload = Constants.UserEnvironmentPaths.ShellPlaces.Get(payload.ToUpperInvariant(), payload); - var folder = (StorageFolder)await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(payload).AsTask()); - if (folder is not null && !string.IsNullOrEmpty(folder.Path)) + payload = Constants.UserEnvironmentPaths.ShellPlaces.Get(payload.ToUpperInvariant(), payload) ?? payload; + var folderResult = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(payload).AsTask()); + if (folderResult.Result is { } folder && !string.IsNullOrEmpty(folder.Path)) payload = folder.Path; // Convert short name to long name (#6190) } @@ -303,7 +303,7 @@ async Task PerformNavigationAsync(string payload, string selectItem = null) var existingTabIndex = MainPageViewModel.AppInstances .Select((tabItem, idx) => new { tabItem, idx }) .FirstOrDefault(x => - x.tabItem.NavigationParameter.NavigationParameter is PaneNavigationArguments paneArgs && + x.tabItem.NavigationParameter?.NavigationParameter is PaneNavigationArguments paneArgs && (paneNavigationArgs.LeftPaneNavPathParam == paneArgs.LeftPaneNavPathParam || paneNavigationArgs.LeftPaneNavPathParam == paneArgs.RightPaneNavPathParam))?.idx ?? -1; @@ -333,7 +333,7 @@ x.tabItem.NavigationParameter.NavigationParameter is PaneNavigationArguments pan case ParsedCommandType.TagFiles: var tagService = Ioc.Default.GetService(); - var tag = tagService.GetTagsByName(command.Payload).FirstOrDefault(); + var tag = tagService?.GetTagsByName(command.Payload).FirstOrDefault(); foreach (var file in command.Args.Skip(1)) { var fileFRN = await FilesystemTasks.Wrap(() => StorageHelpers.ToStorageItem(file)) diff --git a/src/Files.App/Services/App/AppUpdateSideloadService.cs b/src/Files.App/Services/App/AppUpdateSideloadService.cs index 0a9b4b06f235..caacaa741e37 100644 --- a/src/Files.App/Services/App/AppUpdateSideloadService.cs +++ b/src/Files.App/Services/App/AppUpdateSideloadService.cs @@ -95,15 +95,21 @@ public async Task CheckForUpdatesAsync() ArgumentNullException.ThrowIfNull(appInstaller); + var remotePackageName = appInstaller.MainBundle?.Name; + if (string.IsNullOrWhiteSpace(remotePackageName) || string.IsNullOrWhiteSpace(appInstaller.Version)) + { + throw new InvalidDataException("The app installer manifest does not identify a package and version."); + } + var remoteVersion = new Version(appInstaller.Version); Logger?.LogInformation($"SIDELOAD: Current Package Name: {PackageName}"); - Logger?.LogInformation($"SIDELOAD: Remote Package Name: {appInstaller.MainBundle.Name}"); + Logger?.LogInformation($"SIDELOAD: Remote Package Name: {remotePackageName}"); Logger?.LogInformation($"SIDELOAD: Current Version: {PackageVersion}"); Logger?.LogInformation($"SIDELOAD: Remote Version: {remoteVersion}"); // Check details and version number - if (appInstaller.MainBundle.Name.Equals(PackageName) && remoteVersion.CompareTo(PackageVersion) > 0) + if (remotePackageName.Equals(PackageName) && remoteVersion.CompareTo(PackageVersion) > 0) { Logger?.LogInformation("SIDELOAD: Update found."); MainWindow.Instance.DispatcherQueue.TryEnqueue(() => @@ -255,24 +261,24 @@ public void Dispose() public sealed class AppInstaller { [XmlElement("MainBundle")] - public MainBundle MainBundle { get; set; } + public MainBundle? MainBundle { get; set; } [XmlAttribute("Uri")] - public string Uri { get; set; } + public string? Uri { get; set; } [XmlAttribute("Version")] - public string Version { get; set; } + public string? Version { get; set; } } public sealed class MainBundle { [XmlAttribute("Name")] - public string Name { get; set; } + public string? Name { get; set; } [XmlAttribute("Version")] - public string Version { get; set; } + public string? Version { get; set; } [XmlAttribute("Uri")] - public string Uri { get; set; } + public string? Uri { get; set; } } } diff --git a/src/Files.App/Services/DateTimeFormatter/UserDateTimeFormatter.cs b/src/Files.App/Services/DateTimeFormatter/UserDateTimeFormatter.cs index d5ce1d04e068..4deb437440ad 100644 --- a/src/Files.App/Services/DateTimeFormatter/UserDateTimeFormatter.cs +++ b/src/Files.App/Services/DateTimeFormatter/UserDateTimeFormatter.cs @@ -14,9 +14,8 @@ public string Name public UserDateTimeFormatter() { + formatter = GetFormatter(); UserSettingsService.OnSettingChangedEvent += UserSettingsService_OnSettingChangedEvent; - - Update(); } public string ToShortLabel(DateTimeOffset offset) @@ -29,14 +28,17 @@ public ITimeSpanLabel ToTimeSpanLabel(DateTimeOffset offset, GroupByDateUnit uni => formatter.ToTimeSpanLabel(offset, unit); private void Update() + => formatter = GetFormatter(); + + private IDateTimeFormatter GetFormatter() { var dateTimeFormat = UserSettingsService.GeneralSettingsService.DateTimeFormat; - var factory = Ioc.Default.GetService(); + var factory = Ioc.Default.GetRequiredService(); - formatter = factory.GetDateTimeFormatter(dateTimeFormat); + return factory.GetDateTimeFormatter(dateTimeFormat); } - private void UserSettingsService_OnSettingChangedEvent(object sender, SettingChangedEventArgs e) + private void UserSettingsService_OnSettingChangedEvent(object? sender, SettingChangedEventArgs e) { if (e.SettingName is nameof(UserSettingsService.GeneralSettingsService.DateTimeFormat)) Update(); diff --git a/src/Files.App/Services/Git/LibGit2Service.cs b/src/Files.App/Services/Git/LibGit2Service.cs index fc96980da5cc..095120bed523 100644 --- a/src/Files.App/Services/Git/LibGit2Service.cs +++ b/src/Files.App/Services/Git/LibGit2Service.cs @@ -44,7 +44,7 @@ public bool IsExecutingGitAction public event PropertyChangedEventHandler? IsExecutingGitActionChanged; public event EventHandler? GitFetchCompleted; - public string? GetGitRepositoryPath(string? path, string root) + public string? GetGitRepositoryPath(string? path, string? root) { if (string.IsNullOrEmpty(root)) return null; @@ -182,7 +182,9 @@ public async Task Checkout(string? repositoryPath, string? branch) var dialog = DynamicDialogFactory.GetFor_GitMergeConflicts(checkoutBranch.FriendlyName, repository.Head.FriendlyName); await dialog.ShowAsync(); - var resolveConflictOption = (GitCheckoutOptions)dialog.ViewModel.AdditionalData; + var resolveConflictOption = dialog.ViewModel.AdditionalData is GitCheckoutOptions option + ? option + : GitCheckoutOptions.None; switch (resolveConflictOption) { @@ -199,7 +201,9 @@ public async Task Checkout(string? repositoryPath, string? branch) var dialog = DynamicDialogFactory.GetFor_GitCheckoutConflicts(checkoutBranch.FriendlyName, repository.Head.FriendlyName); await dialog.ShowAsync(); - var resolveConflictOption = (GitCheckoutOptions)dialog.ViewModel.AdditionalData; + var resolveConflictOption = dialog.ViewModel.AdditionalData is GitCheckoutOptions option + ? option + : GitCheckoutOptions.None; switch (resolveConflictOption) { diff --git a/src/Files.App/Services/Settings/FileTagsSettingsService.cs b/src/Files.App/Services/Settings/FileTagsSettingsService.cs index 78d85f2b4e1e..707b97d33d62 100644 --- a/src/Files.App/Services/Settings/FileTagsSettingsService.cs +++ b/src/Files.App/Services/Settings/FileTagsSettingsService.cs @@ -10,9 +10,9 @@ namespace Files.App.Services.Settings { internal sealed class FileTagsSettingsService : BaseJsonSettings, IFileTagsSettingsService { - public event EventHandler OnSettingImportedEvent; + public event EventHandler? OnSettingImportedEvent; - public event EventHandler OnTagsUpdated; + public event EventHandler? OnTagsUpdated; private static readonly List DefaultFileTags = [ @@ -24,22 +24,24 @@ internal sealed class FileTagsSettingsService : BaseJsonSettings, IFileTagsSetti public FileTagsSettingsService() { - SettingsSerializer = new DefaultSettingsSerializer(); + var settingsSerializer = new DefaultSettingsSerializer(); + SettingsSerializer = settingsSerializer; Initialize(Path.Combine(ApplicationData.Current.LocalFolder.Path, Constants.LocalSettings.SettingsFolderName, Constants.LocalSettings.FileTagSettingsFileName)); - JsonSettingsSerializer = new DefaultJsonSettingsSerializer(); - JsonSettingsDatabase = new CachingJsonSettingsDatabase(SettingsSerializer, JsonSettingsSerializer); + var jsonSettingsSerializer = new DefaultJsonSettingsSerializer(); + JsonSettingsSerializer = jsonSettingsSerializer; + JsonSettingsDatabase = new CachingJsonSettingsDatabase(settingsSerializer, jsonSettingsSerializer); } public IList FileTagList { get { - var tags = Get>(DefaultFileTags); + var tags = Get>(DefaultFileTags) ?? DefaultFileTags; - foreach (var tag in tags!) + foreach (var tag in tags) tag.Color = ColorHelpers.FromHex(tag.Color).ToString(); return tags; @@ -47,11 +49,11 @@ public IList FileTagList set { Set(value); - OnTagsUpdated.Invoke(this, EventArgs.Empty); + OnTagsUpdated?.Invoke(this, EventArgs.Empty); } } - public TagViewModel GetTagById(string uid) + public TagViewModel? GetTagById(string uid) { if (FileTagList.Any(x => x.Uid is null)) { @@ -70,11 +72,11 @@ public TagViewModel GetTagById(string uid) return tag; } - public IList? GetTagsByIds(string[] uids) + public IList? GetTagsByIds(string[]? uids) { return uids is null || uids.Length == 0 ? null - : uids.Select(x => GetTagById(x)).Where(x => x is not null).ToList(); + : uids.Select(GetTagById).WhereNotNull().ToList(); } public IEnumerable GetTagsByName(string tagName) @@ -122,7 +124,7 @@ public override bool ImportSettings(object import) { if (import is string importString) { - var settings = JsonSettingsSerializer.DeserializeFromJson>>(importString); + var settings = GetJsonSettingsSerializer().DeserializeFromJson>>(importString); if (settings is not null && settings.TryGetValue("FileTagList", out var importedTags)) FileTagList = importedTags; } @@ -136,7 +138,7 @@ public override bool ImportSettings(object import) if (FileTagList is not null) { FlushSettings(); - OnSettingImportedEvent?.Invoke(this, null); + OnSettingImportedEvent?.Invoke(this, EventArgs.Empty); return true; } @@ -151,7 +153,7 @@ public override object ExportSettings() }; // Serialize settings to JSON format - return JsonSettingsSerializer.SerializeToJson(settings); + return GetJsonSettingsSerializer().SerializeToJson(settings); } private int GetTagIndex(string uid) @@ -179,5 +181,11 @@ private void UntagAllFiles(string uid) } } } + + private IJsonSettingsSerializer GetJsonSettingsSerializer() + { + return JsonSettingsSerializer + ?? throw new InvalidOperationException("The JSON settings serializer has not been initialized."); + } } } diff --git a/src/Files.App/Services/Settings/GeneralSettingsService.cs b/src/Files.App/Services/Settings/GeneralSettingsService.cs index c794a24f7169..e754b3c7705e 100644 --- a/src/Files.App/Services/Settings/GeneralSettingsService.cs +++ b/src/Files.App/Services/Settings/GeneralSettingsService.cs @@ -41,13 +41,13 @@ public bool OpenTabInExistingInstance set => Set(value); } - public List TabsOnStartupList + public List? TabsOnStartupList { get => Get>(null); set => Set(value); } - public List LastSessionTabList + public List? LastSessionTabList { get => Get>(null); set => Set(value); @@ -59,25 +59,25 @@ public int LastSessionSelectedTabIndex set => Set(value); } - public List LastCrashedTabList + public List? LastCrashedTabList { get => Get>(null); set => Set(value); } - public List PathHistoryList + public List? PathHistoryList { get => Get>(null); set => Set(value); } - public List PreviousSearchQueriesList + public List? PreviousSearchQueriesList { get => Get>(null); set => Set(value); } - public List PreviousArchiveExtractionLocations + public List? PreviousArchiveExtractionLocations { get => Get>(null); set => Set(value); @@ -393,7 +393,7 @@ public ArchiveWordSizes ArchiveWordSizesOption set => Set((long)value); } - public Dictionary ShowHashesDictionary + public Dictionary? ShowHashesDictionary { get => Get>(null); set => Set(value); diff --git a/src/Files.App/Services/Settings/UserSettingsService.cs b/src/Files.App/Services/Settings/UserSettingsService.cs index 89c79757ee01..23054b3225e6 100644 --- a/src/Files.App/Services/Settings/UserSettingsService.cs +++ b/src/Files.App/Services/Settings/UserSettingsService.cs @@ -9,43 +9,43 @@ namespace Files.App.Services.Settings { internal sealed class UserSettingsService : BaseJsonSettings, IUserSettingsService { - private IGeneralSettingsService _GeneralSettingsService; + private IGeneralSettingsService? _GeneralSettingsService; public IGeneralSettingsService GeneralSettingsService { get => GetSettingsService(ref _GeneralSettingsService); } - private IFoldersSettingsService _FoldersSettingsService; + private IFoldersSettingsService? _FoldersSettingsService; public IFoldersSettingsService FoldersSettingsService { get => GetSettingsService(ref _FoldersSettingsService); } - private IAppearanceSettingsService _AppearanceSettingsService; + private IAppearanceSettingsService? _AppearanceSettingsService; public IAppearanceSettingsService AppearanceSettingsService { get => GetSettingsService(ref _AppearanceSettingsService); } - private IInfoPaneSettingsService _InfoPaneSettingsService; + private IInfoPaneSettingsService? _InfoPaneSettingsService; public IInfoPaneSettingsService InfoPaneSettingsService { get => GetSettingsService(ref _InfoPaneSettingsService); } - private ILayoutSettingsService _LayoutSettingsService; + private ILayoutSettingsService? _LayoutSettingsService; public ILayoutSettingsService LayoutSettingsService { get => GetSettingsService(ref _LayoutSettingsService); } - private IApplicationSettingsService _ApplicationSettingsService; + private IApplicationSettingsService? _ApplicationSettingsService; public IApplicationSettingsService ApplicationSettingsService { get => GetSettingsService(ref _ApplicationSettingsService); } - private IAppSettingsService _AppSettingsService; + private IAppSettingsService? _AppSettingsService; public IAppSettingsService AppSettingsService { get => GetSettingsService(ref _AppSettingsService); @@ -53,12 +53,14 @@ public IAppSettingsService AppSettingsService public UserSettingsService() { - SettingsSerializer = new DefaultSettingsSerializer(); + var settingsSerializer = new DefaultSettingsSerializer(); + SettingsSerializer = settingsSerializer; Initialize(Path.Combine(ApplicationData.Current.LocalFolder.Path, Constants.LocalSettings.SettingsFolderName, Constants.LocalSettings.UserSettingsFileName)); - JsonSettingsSerializer = new DefaultJsonSettingsSerializer(); - JsonSettingsDatabase = new CachingJsonSettingsDatabase(SettingsSerializer, JsonSettingsSerializer); + var jsonSettingsSerializer = new DefaultJsonSettingsSerializer(); + JsonSettingsSerializer = jsonSettingsSerializer; + JsonSettingsDatabase = new CachingJsonSettingsDatabase(settingsSerializer, jsonSettingsSerializer); } public override object ExportSettings() @@ -73,14 +75,14 @@ public override object ExportSettings() export.Remove(nameof(GeneralSettingsService.PreviousSearchQueriesList)); export.Remove(nameof(GeneralSettingsService.PreviousArchiveExtractionLocations)); - return JsonSettingsSerializer.SerializeToJson(export); + return GetJsonSettingsSerializer().SerializeToJson(export); } public override bool ImportSettings(object import) { Dictionary settingsImport = import switch { - string s => JsonSettingsSerializer?.DeserializeFromJson>(s) ?? [], + string s => GetJsonSettingsSerializer().DeserializeFromJson>(s) ?? [], Dictionary d => d, _ => [], }; @@ -98,12 +100,18 @@ public override bool ImportSettings(object import) return false; } - private TSettingsService GetSettingsService(ref TSettingsService settingsServiceMember) + private static TSettingsService GetSettingsService(ref TSettingsService? settingsServiceMember) where TSettingsService : class, IBaseSettingsService { - settingsServiceMember ??= Ioc.Default.GetService()!; + settingsServiceMember ??= Ioc.Default.GetRequiredService(); return settingsServiceMember; } + + private IJsonSettingsSerializer GetJsonSettingsSerializer() + { + return JsonSettingsSerializer + ?? throw new InvalidOperationException("The JSON settings serializer has not been initialized."); + } } } diff --git a/src/Files.App/Services/SizeProvider/UserSizeProvider.cs b/src/Files.App/Services/SizeProvider/UserSizeProvider.cs index 45cd9adecd9d..3051e920615b 100644 --- a/src/Files.App/Services/SizeProvider/UserSizeProvider.cs +++ b/src/Files.App/Services/SizeProvider/UserSizeProvider.cs @@ -12,7 +12,7 @@ private readonly IFoldersSettingsService folderPreferences private ISizeProvider provider; - public event EventHandler SizeChanged; + public event EventHandler? SizeChanged; public UserSizeProvider() { @@ -43,7 +43,7 @@ public void Dispose() private ISizeProvider GetProvider() => new DrivesSizeProvider(); - private async void FolderPreferences_PropertyChanged(object sender, PropertyChangedEventArgs e) + private async void FolderPreferences_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is nameof(IFoldersSettingsService.CalculateFolderSizes)) { @@ -54,7 +54,7 @@ private async void FolderPreferences_PropertyChanged(object sender, PropertyChan } } - private void Provider_SizeChanged(object sender, SizeChangedEventArgs e) + private void Provider_SizeChanged(object? sender, SizeChangedEventArgs e) => SizeChanged?.Invoke(this, e); } } diff --git a/src/Files.App/Services/Storage/StorageArchiveService.cs b/src/Files.App/Services/Storage/StorageArchiveService.cs index bcb5bed7e06f..7dd850de314e 100644 --- a/src/Files.App/Services/Storage/StorageArchiveService.cs +++ b/src/Files.App/Services/Storage/StorageArchiveService.cs @@ -289,9 +289,9 @@ await Task.Run(() => string entryFileName = zipEntry.Name; string fullZipToPath = Path.Combine(destinationFolderPath, entryFileName); - string directoryName = Path.GetDirectoryName(fullZipToPath); + string? directoryName = Path.GetDirectoryName(fullZipToPath); - if (!Directory.Exists(directoryName)) + if (directoryName is not null && !Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } @@ -441,15 +441,18 @@ public async Task IsEncodingUndeterminedAsync(string archiveFilePath) /// public async Task GetSevenZipExtractorAsync(string archiveFilePath, string password = "") { - return await FilesystemTasks.Wrap(async () => + return await FilesystemTasks.WrapNullable(async () => { - BaseStorageFile archive = await StorageHelpers.ToStorageItem(archiveFilePath); + BaseStorageFile? archive = await StorageHelpers.ToStorageItem(archiveFilePath); + if (archive is null) + return null; + var extractor = string.IsNullOrEmpty(password) ? new SevenZipExtractor(archive.Path) : new SevenZipExtractor(archive.Path, password); // Force to load archive (1665013614u) - return extractor?.ArchiveFileData is null ? null : extractor; + return extractor.ArchiveFileData is null ? null : extractor; }); } } diff --git a/src/Files.App/Services/Storage/StorageDevicesService.cs b/src/Files.App/Services/Storage/StorageDevicesService.cs index 2d7f967a9b1b..7b5d842f1f89 100644 --- a/src/Files.App/Services/Storage/StorageDevicesService.cs +++ b/src/Files.App/Services/Storage/StorageDevicesService.cs @@ -44,10 +44,13 @@ public async IAsyncEnumerable GetDrivesAsync() continue; } - using var thumbnail = await DriveHelpers.GetThumbnailAsync(res.Result); + if (res.Result is not { } root) + continue; + + using var thumbnail = await DriveHelpers.GetThumbnailAsync(root); var type = DriveHelpers.GetDriveType(drive); var label = DriveHelpers.GetExtendedDriveLabel(drive); - var driveItem = await DriveItem.CreateFromPropertiesAsync(res.Result, drive.Name.TrimEnd('\\'), label, type, thumbnail); + var driveItem = await DriveItem.CreateFromPropertiesAsync(root, drive.Name.TrimEnd('\\'), label, type, thumbnail); App.Logger.LogInformation($"Drive added: {driveItem.Path}, {driveItem.Type}"); @@ -70,12 +73,12 @@ public async IAsyncEnumerable GetDrivesAsync() public async Task UpdateDrivePropertiesAsync(IFolder drive) { var rootModified = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(drive.Id).AsTask()); - if (rootModified && drive is DriveItem matchingDriveEjected) + if (rootModified.Result is { } root && drive is DriveItem matchingDriveEjected) { _ = MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => { - matchingDriveEjected.Root = rootModified.Result; - matchingDriveEjected.Text = rootModified.Result.DisplayName; + matchingDriveEjected.Root = root; + matchingDriveEjected.Text = root.DisplayName; return matchingDriveEjected.UpdatePropertiesAsync(); }); } diff --git a/src/Files.App/Services/Storage/StorageNetworkService.cs b/src/Files.App/Services/Storage/StorageNetworkService.cs index 1ebb9d697157..4c1181155bde 100644 --- a/src/Files.App/Services/Storage/StorageNetworkService.cs +++ b/src/Files.App/Services/Storage/StorageNetworkService.cs @@ -107,14 +107,15 @@ public async Task> GetShortcutsAsync() { if (item is ShellLink link) { - locations.Add(ShellFolderExtensions.GetShellLinkItem(link)); + if (ShellFolderExtensions.GetShellLinkItem(link) is { } linkItem) + locations.Add(linkItem); } else { - var linkPath = (string?)item?.Properties["System.Link.TargetParsingPath"]; - if (linkPath is not null) + var linkPath = item.Properties["System.Link.TargetParsingPath"] as string; + if (linkPath is not null && + ShellFolderExtensions.GetShellFileItem(item) is { } linkItem) { - var linkItem = ShellFolderExtensions.GetShellFileItem(item); locations.Add(new(linkItem) { TargetPath = linkPath }); } } diff --git a/src/Files.App/Services/Windows/WindowsJumpListService.cs b/src/Files.App/Services/Windows/WindowsJumpListService.cs index 572718c619d1..635af433cb40 100644 --- a/src/Files.App/Services/Windows/WindowsJumpListService.cs +++ b/src/Files.App/Services/Windows/WindowsJumpListService.cs @@ -159,7 +159,7 @@ private void AddFolder(string path, string group, JumpList instance) displayName = Strings.RecycleBin.GetLocalizedResource(); else if (path.Equals(Constants.UserEnvironmentPaths.MyComputerPath, StringComparison.OrdinalIgnoreCase)) displayName = Strings.ThisPC.GetLocalizedResource(); - else if (App.LibraryManager.TryGetLibrary(path, out LibraryLocationItem library)) + else if (App.LibraryManager.TryGetLibrary(path, out var library)) { var libName = Path.GetFileNameWithoutExtension(library.Path); displayName = libName switch diff --git a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs index 86cc2008cefd..1a1b7c4065b9 100644 --- a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs +++ b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs @@ -42,8 +42,16 @@ private async Task PinToSidebarAsync(string[] folderPaths, bool doUpdateQuickAcc private async Task UnpinFromSidebarAsync(string[] folderPaths, bool doUpdateQuickAccessWidget) { Type? shellAppType = Type.GetTypeFromProgID("Shell.Application"); + if (shellAppType is null) + return; + object? shell = Activator.CreateInstance(shellAppType); + if (shell is null) + return; + dynamic? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, [$"shell:{guid}"]); + if (f2 is null) + return; if (folderPaths.Length == 0) folderPaths = (await GetPinnedFoldersAsync()) @@ -52,6 +60,9 @@ private async Task UnpinFromSidebarAsync(string[] folderPaths, bool doUpdateQuic foreach (dynamic? fi in f2.Items()) { + if (fi is null) + continue; + string pathStr = (string)fi.Path; if (ShellStorageFolder.IsShellPath(pathStr)) diff --git a/src/Files.App/Services/Windows/WindowsShellService.cs b/src/Files.App/Services/Windows/WindowsShellService.cs index b768d6b792a7..d9af5e219f35 100644 --- a/src/Files.App/Services/Windows/WindowsShellService.cs +++ b/src/Files.App/Services/Windows/WindowsShellService.cs @@ -6,7 +6,7 @@ namespace Files.App.Services /// internal sealed class AddItemService : IAddItemService { - private List _cached; + private List _cached = []; public async Task InitializeAsync() { diff --git a/src/Files.App/UserControls/DataGridHeader.xaml.cs b/src/Files.App/UserControls/DataGridHeader.xaml.cs index b5f2c8569519..e13d5a6c05fd 100644 --- a/src/Files.App/UserControls/DataGridHeader.xaml.cs +++ b/src/Files.App/UserControls/DataGridHeader.xaml.cs @@ -19,8 +19,8 @@ public sealed partial class DataGridHeader : UserControl [GeneratedDependencyProperty] public partial SortDirection? ColumnSortOption { get; set; } - public ICommand Command { get; set; } - public object CommandParameter { get; set; } + public ICommand? Command { get; set; } + public object? CommandParameter { get; set; } partial void OnColumnSortOptionChanged(SortDirection? newValue) { diff --git a/src/Files.App/UserControls/FileIcon.xaml.cs b/src/Files.App/UserControls/FileIcon.xaml.cs index a516c67498c1..1624c59b7957 100644 --- a/src/Files.App/UserControls/FileIcon.xaml.cs +++ b/src/Files.App/UserControls/FileIcon.xaml.cs @@ -11,9 +11,9 @@ namespace Files.App.UserControls { public sealed partial class FileIcon : UserControl { - private SelectedItemsPropertiesViewModel viewModel; + private SelectedItemsPropertiesViewModel? viewModel; - public SelectedItemsPropertiesViewModel ViewModel + public SelectedItemsPropertiesViewModel? ViewModel { get => viewModel; set @@ -48,7 +48,7 @@ public double ItemSize public static readonly DependencyProperty FileIconImageSourceProperty = DependencyProperty.Register(nameof(FileIconImageSource), typeof(BitmapImage), typeof(FileIcon), null); - private BitmapImage FileIconImageSource + private BitmapImage? FileIconImageSource { get => GetValue(FileIconImageSourceProperty) as BitmapImage; set => SetValue(FileIconImageSourceProperty, value); @@ -56,7 +56,7 @@ private BitmapImage FileIconImageSource public static readonly DependencyProperty FileIconImageDataProperty = DependencyProperty.Register(nameof(FileIconImageData), typeof(byte[]), typeof(FileIcon), null); - public byte[] FileIconImageData + public byte[]? FileIconImageData { get => GetValue(FileIconImageDataProperty) as byte[]; set @@ -69,7 +69,7 @@ public byte[] FileIconImageData } } - private SvgImageSource CustomIconImageSource { get; set; } + private SvgImageSource? CustomIconImageSource { get; set; } public FileIcon() { @@ -80,12 +80,13 @@ public async Task UpdateImageSourceAsync() { if (FileIconImageData is not null) { - FileIconImageSource = new BitmapImage(); + var imageSource = new BitmapImage(); + FileIconImageSource = imageSource; using InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream(); await stream.WriteAsync(FileIconImageData.AsBuffer()); stream.Seek(0); - await FileIconImageSource.SetSourceAsync(stream); + await imageSource.SetSourceAsync(stream); } } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/FilePreviews/CodePreview.xaml.cs b/src/Files.App/UserControls/FilePreviews/CodePreview.xaml.cs index 187f787ba9d4..c1c3555662fa 100644 --- a/src/Files.App/UserControls/FilePreviews/CodePreview.xaml.cs +++ b/src/Files.App/UserControls/FilePreviews/CodePreview.xaml.cs @@ -9,8 +9,6 @@ namespace Files.App.UserControls.FilePreviews { public sealed partial class CodePreview : UserControl { - private RichTextBlockFormatter formatter; - private CodePreviewViewModel ViewModel { get; set; } public CodePreview(CodePreviewViewModel model) @@ -33,8 +31,11 @@ private void RenderDocument() return; } - formatter = new RichTextBlockFormatter(ActualTheme); - formatter.FormatRichTextBlock(ViewModel.TextValue, ViewModel.CodeLanguage, codeView); + if (ViewModel.CodeLanguage is not null) + { + var formatter = new RichTextBlockFormatter(ActualTheme); + formatter.FormatRichTextBlock(ViewModel.TextValue ?? string.Empty, ViewModel.CodeLanguage, codeView); + } } } @@ -54,4 +55,4 @@ private void UserControl_ActualThemeChanged(Microsoft.UI.Xaml.FrameworkElement s } } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/FilePreviews/MediaPreview.xaml.cs b/src/Files.App/UserControls/FilePreviews/MediaPreview.xaml.cs index a71c7d30baa1..4818734e15a2 100644 --- a/src/Files.App/UserControls/FilePreviews/MediaPreview.xaml.cs +++ b/src/Files.App/UserControls/FilePreviews/MediaPreview.xaml.cs @@ -64,7 +64,7 @@ private void TogglePlaybackRequestInvoked(object? sender, EventArgs e) private void TogglePlaybackAcceleratorInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) { - TogglePlaybackRequestInvoked(sender, null); + TogglePlaybackRequestInvoked(sender, EventArgs.Empty); } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/Menus/FileTagsContextMenu.cs b/src/Files.App/UserControls/Menus/FileTagsContextMenu.cs index 4245f9ec4d1a..a2649d4994d1 100644 --- a/src/Files.App/UserControls/Menus/FileTagsContextMenu.cs +++ b/src/Files.App/UserControls/Menus/FileTagsContextMenu.cs @@ -12,7 +12,7 @@ namespace Files.App.UserControls.Menus public sealed partial class FileTagsContextMenu : MenuFlyout { private IFileTagsSettingsService FileTagsSettingsService { get; } = - Ioc.Default.GetService(); + Ioc.Default.GetRequiredService(); /// /// Event fired when an item's tags are updated (added/removed). diff --git a/src/Files.App/UserControls/Menus/MenuFlyoutItemWithImage.xaml.cs b/src/Files.App/UserControls/Menus/MenuFlyoutItemWithImage.xaml.cs index 0da88fa1d4e2..a64d184e330f 100644 --- a/src/Files.App/UserControls/Menus/MenuFlyoutItemWithImage.xaml.cs +++ b/src/Files.App/UserControls/Menus/MenuFlyoutItemWithImage.xaml.cs @@ -11,9 +11,9 @@ namespace Files.App.UserControls { public sealed partial class MenuFlyoutItemWithImage : MenuFlyoutItem { - public BitmapImage BitmapIcon + public BitmapImage? BitmapIcon { - get { return (BitmapImage)GetValue(BitmapIconProperty); } + get { return GetValue(BitmapIconProperty) as BitmapImage; } set { SetValue(BitmapIconProperty, value); } } @@ -22,7 +22,8 @@ public BitmapImage BitmapIcon private static void OnBitmapIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - (d as MenuFlyoutItem).Icon = e.NewValue is not null ? new IconSourceElement() : null; + if (d is MenuFlyoutItem item) + item.Icon = e.NewValue is not null ? new IconSourceElement() : null; } public MenuFlyoutItemWithImage() @@ -30,4 +31,4 @@ public MenuFlyoutItemWithImage() InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/Menus/MenuFlyoutItemWithThemedIcon.xaml.cs b/src/Files.App/UserControls/Menus/MenuFlyoutItemWithThemedIcon.xaml.cs index 3ed25642391f..3eb9c9ddde5c 100644 --- a/src/Files.App/UserControls/Menus/MenuFlyoutItemWithThemedIcon.xaml.cs +++ b/src/Files.App/UserControls/Menus/MenuFlyoutItemWithThemedIcon.xaml.cs @@ -10,9 +10,9 @@ namespace Files.App.UserControls { public sealed partial class MenuFlyoutItemWithThemedIcon : MenuFlyoutItem { - public Style ThemedIconStyle + public Style? ThemedIconStyle { - get { return (Style)GetValue(ThemedIconStyleProperty); } + get { return GetValue(ThemedIconStyleProperty) as Style; } set { SetValue(ThemedIconStyleProperty, value); } } @@ -21,7 +21,8 @@ public Style ThemedIconStyle private static void OnThemedIconStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - (d as MenuFlyoutItem).Icon = e.NewValue is not null ? new IconSourceElement() : null; + if (d is MenuFlyoutItem item) + item.Icon = e.NewValue is not null ? new IconSourceElement() : null; } public MenuFlyoutItemWithThemedIcon() @@ -29,4 +30,4 @@ public MenuFlyoutItemWithThemedIcon() InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/Menus/MenuFlyoutSubItemCustomProperties.cs b/src/Files.App/UserControls/Menus/MenuFlyoutSubItemCustomProperties.cs index 261aa6367169..efd2a895e3c6 100644 --- a/src/Files.App/UserControls/Menus/MenuFlyoutSubItemCustomProperties.cs +++ b/src/Files.App/UserControls/Menus/MenuFlyoutSubItemCustomProperties.cs @@ -13,19 +13,20 @@ public sealed class MenuFlyoutSubItemCustomProperties : DependencyObject public static readonly DependencyProperty BitmapIconProperty = DependencyProperty.Register("BitmapIcon", typeof(BitmapImage), typeof(MenuFlyoutSubItemCustomProperties), new PropertyMetadata(null, OnBitmapIconChanged)); - public static BitmapImage GetBitmapIcon(DependencyObject obj) + public static BitmapImage? GetBitmapIcon(DependencyObject obj) { - return (BitmapImage)obj.GetValue(BitmapIconProperty); + return obj.GetValue(BitmapIconProperty) as BitmapImage; } - public static void SetBitmapIcon(DependencyObject obj, BitmapImage value) + public static void SetBitmapIcon(DependencyObject obj, BitmapImage? value) { obj.SetValue(BitmapIconProperty, value); } private static void OnBitmapIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - (d as MenuFlyoutSubItem).Icon = e.NewValue is not null ? new IconSourceElement() : null; + if (d is MenuFlyoutSubItem item) + item.Icon = e.NewValue is not null ? new IconSourceElement() : null; } } } diff --git a/src/Files.App/UserControls/Menus/ToggleMenuFlyoutItemWithThemedIcon.xaml.cs b/src/Files.App/UserControls/Menus/ToggleMenuFlyoutItemWithThemedIcon.xaml.cs index 7718b0a32dc4..91a40c649d8b 100644 --- a/src/Files.App/UserControls/Menus/ToggleMenuFlyoutItemWithThemedIcon.xaml.cs +++ b/src/Files.App/UserControls/Menus/ToggleMenuFlyoutItemWithThemedIcon.xaml.cs @@ -10,9 +10,9 @@ namespace Files.App.UserControls { public sealed partial class ToggleMenuFlyoutItemWithThemedIcon : ToggleMenuFlyoutItem { - public Style ThemedIconStyle + public Style? ThemedIconStyle { - get { return (Style)GetValue(ThemedIconStyleProperty); } + get { return GetValue(ThemedIconStyleProperty) as Style; } set { SetValue(ThemedIconStyleProperty, value); } } @@ -21,7 +21,8 @@ public Style ThemedIconStyle private static void OnThemedIconStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - (d as ToggleMenuFlyoutItem).Icon = e.NewValue is not null ? new IconSourceElement() : null; + if (d is ToggleMenuFlyoutItem item) + item.Icon = e.NewValue is not null ? new IconSourceElement() : null; } public ToggleMenuFlyoutItemWithThemedIcon() @@ -29,4 +30,4 @@ public ToggleMenuFlyoutItemWithThemedIcon() InitializeComponent(); } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml.cs b/src/Files.App/UserControls/NavigationToolbar.xaml.cs index 746d31075c4e..a31cf824f0c1 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml.cs +++ b/src/Files.App/UserControls/NavigationToolbar.xaml.cs @@ -171,13 +171,16 @@ void HistoryItemClicked(ToolbarHistoryItemModel? itemModel) private async void Omnibar_QuerySubmitted(Omnibar sender, OmnibarQuerySubmittedEventArgs args) { + if (ViewModel is not { } viewModel) + return; + var mode = Omnibar.CurrentSelectedMode; // Path mode if (mode == OmnibarPathMode) { - await ViewModel.HandleItemNavigationAsync(args.Text); - ContentPageContext.ShellPage!.PaneHolder.FocusActivePane(); + await viewModel.HandleItemNavigationAsync(args.Text); + ContentPageContext.ShellPage?.PaneHolder?.FocusActivePane(); return; } @@ -197,14 +200,14 @@ private async void Omnibar_QuerySubmitted(Omnibar sender, OmnibarQuerySubmittedE continue; await command.ExecuteAsync(); - ContentPageContext.ShellPage!.PaneHolder.FocusActivePane(); + ContentPageContext.ShellPage?.PaneHolder?.FocusActivePane(); return; } await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidCommand.GetLocalizedResource(), string.Format(Strings.InvalidCommandContent.GetLocalizedResource(), args.Text)); - ContentPageContext.ShellPage!.PaneHolder.FocusActivePane(); + ContentPageContext.ShellPage?.PaneHolder?.FocusActivePane(); return; } @@ -214,7 +217,7 @@ await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidCommand.GetLocalizedRes var shellPage = ContentPageContext.ShellPage; // Settings search: jump to the matching card, or apply the query in-page. - if (ViewModel.InstanceViewModel?.IsPageTypeSettings == true) + if (viewModel.InstanceViewModel.IsPageTypeSettings) { var settingsPage = (MainWindow.Instance.Content as FrameworkElement)?.FindDescendant(); @@ -227,9 +230,9 @@ await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidCommand.GetLocalizedRes settingsPage?.ApplySearchQuery(args.Text); } - ViewModel.OmnibarSearchModeText = string.Empty; + viewModel.OmnibarSearchModeText = string.Empty; Omnibar.IsFocused = false; - shellPage?.PaneHolder.FocusActivePane(); + shellPage?.PaneHolder?.FocusActivePane(); return; } @@ -242,53 +245,57 @@ await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidCommand.GetLocalizedRes : args.Text; shellPage?.SubmitSearch(searchQuery); // use the resolved shellPage for consistency - ViewModel.SaveSearchQueryToList(searchQuery); + viewModel.SaveSearchQueryToList(searchQuery); } - ContentPageContext.ShellPage!.PaneHolder.FocusActivePane(); + ContentPageContext.ShellPage?.PaneHolder?.FocusActivePane(); return; } } private async void Omnibar_TextChanged(Omnibar sender, OmnibarTextChangedEventArgs args) { - if (args.Reason is not OmnibarTextChangeReason.UserInput) + if (args.Reason is not OmnibarTextChangeReason.UserInput || + ViewModel is not { } viewModel) return; if (Omnibar.CurrentSelectedMode == OmnibarPathMode) { - await DispatcherQueue.EnqueueOrInvokeAsync(ViewModel.PopulateOmnibarSuggestionsForPathMode); + await DispatcherQueue.EnqueueOrInvokeAsync(viewModel.PopulateOmnibarSuggestionsForPathMode); } else if (Omnibar.CurrentSelectedMode == OmnibarCommandPaletteMode) { - await DispatcherQueue.EnqueueOrInvokeAsync(ViewModel.PopulateOmnibarSuggestionsForCommandPaletteMode); + await DispatcherQueue.EnqueueOrInvokeAsync(viewModel.PopulateOmnibarSuggestionsForCommandPaletteMode); } else if (Omnibar.CurrentSelectedMode == OmnibarSearchMode) { - await DispatcherQueue.EnqueueOrInvokeAsync(ViewModel.PopulateOmnibarSuggestionsForSearchMode); + await DispatcherQueue.EnqueueOrInvokeAsync(viewModel.PopulateOmnibarSuggestionsForSearchMode); } } private async void BreadcrumbBar_ItemClicked(Controls.BreadcrumbBar sender, Controls.BreadcrumbBarItemClickedEventArgs args) { + if (ViewModel is not { } viewModel) + return; + if (args.IsRootItem) { - await ViewModel.HandleItemNavigationAsync("Home"); + await viewModel.HandleItemNavigationAsync("Home"); return; } // Validate index before accessing the collection - if (args.Index < 0 || args.Index >= ViewModel.PathComponents.Count) + if (args.Index < 0 || args.Index >= viewModel.PathComponents.Count) return; // Navigation to the current folder should not happen - if (args.Index == ViewModel.PathComponents.Count - 1 || - ViewModel.PathComponents[args.Index].Path is not { } path) + if (args.Index == viewModel.PathComponents.Count - 1 || + viewModel.PathComponents[args.Index].Path is not { } path) return; // If user clicked the item with middle mouse button, open it in new tab var openInNewTab = args.PointerRoutedEventArgs?.GetCurrentPoint(null).Properties.PointerUpdateKind is PointerUpdateKind.MiddleButtonReleased; - await ViewModel.HandleFolderNavigationAsync(path, openInNewTab); + await viewModel.HandleFolderNavigationAsync(path, openInNewTab); } private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, BreadcrumbBarItemDropDownFlyoutEventArgs e) @@ -322,7 +329,8 @@ private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, Breadc flyoutItem.Click += (sender, args) => { // NOTE: We should not pass a path string but pass the storable object itself in the future. - contentPageContext.ShellPage!.NavigateToPath((string)flyoutItem.DataContext); + if (flyoutItem.DataContext is string path) + contentPageContext.ShellPage?.NavigateToPath(path); }; } @@ -350,14 +358,20 @@ private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, Breadc flyoutItem.Click += (sender, args) => { // NOTE: We should not pass a path string but pass the storable object itself in the future. - contentPageContext.ShellPage!.NavigateToPath((string)flyoutItem.DataContext); + if (flyoutItem.DataContext is string path) + contentPageContext.ShellPage?.NavigateToPath(path); }; } return; } - await ViewModel.SetPathBoxDropDownFlyoutAsync(e.Flyout, ViewModel.PathComponents[e.Index]); + if (ViewModel is { } viewModel && + e.Index >= 0 && + e.Index < viewModel.PathComponents.Count) + { + await viewModel.SetPathBoxDropDownFlyoutAsync(e.Flyout, viewModel.PathComponents[e.Index]); + } } private void BreadcrumbBar_ItemDropDownFlyoutClosed(object sender, BreadcrumbBarItemDropDownFlyoutEventArgs e) @@ -373,31 +387,35 @@ private void BreadcrumbBar_ItemDropDownFlyoutClosed(object sender, BreadcrumbBar /// private async void Omnibar_ModeChanged(object sender, OmnibarModeChangedEventArgs e) { + if (ViewModel is not { } viewModel) + return; + if (e.NewMode == OmnibarPathMode) { // Initialize with current working directory or fallback to home path - ViewModel.PathText = string.IsNullOrEmpty(ContentPageContext.ShellPage?.ShellViewModel?.WorkingDirectory) + var workingDirectory = ContentPageContext.ShellPage?.ShellViewModel?.WorkingDirectory; + viewModel.PathText = string.IsNullOrEmpty(workingDirectory) ? Constants.UserEnvironmentPaths.HomePath - : ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + : workingDirectory; - await DispatcherQueue.EnqueueOrInvokeAsync(ViewModel.PopulateOmnibarSuggestionsForPathMode); + await DispatcherQueue.EnqueueOrInvokeAsync(viewModel.PopulateOmnibarSuggestionsForPathMode); } else if (e.NewMode == OmnibarCommandPaletteMode) { // Clear text and load command suggestions - ViewModel.OmnibarCommandPaletteModeText = string.Empty; + viewModel.OmnibarCommandPaletteModeText = string.Empty; - await DispatcherQueue.EnqueueOrInvokeAsync(ViewModel.PopulateOmnibarSuggestionsForCommandPaletteMode); + await DispatcherQueue.EnqueueOrInvokeAsync(viewModel.PopulateOmnibarSuggestionsForCommandPaletteMode); } else if (e.NewMode == OmnibarSearchMode) { // Preserve existing search query or clear for new search - if (!ViewModel.InstanceViewModel.IsPageTypeSearchResults) - ViewModel.OmnibarSearchModeText = string.Empty; + if (!viewModel.InstanceViewModel.IsPageTypeSearchResults) + viewModel.OmnibarSearchModeText = string.Empty; else - ViewModel.OmnibarSearchModeText = ViewModel.InstanceViewModel.CurrentSearchQuery; + viewModel.OmnibarSearchModeText = viewModel.InstanceViewModel.CurrentSearchQuery; - await DispatcherQueue.EnqueueOrInvokeAsync(ViewModel.PopulateOmnibarSuggestionsForSearchMode); + await DispatcherQueue.EnqueueOrInvokeAsync(viewModel.PopulateOmnibarSuggestionsForSearchMode); } } @@ -408,23 +426,27 @@ private async void Omnibar_ModeChanged(object sender, OmnibarModeChangedEventArg /// private async void Omnibar_IsFocusedChanged(Omnibar sender, OmnibarIsFocusedChangedEventArgs args) { + if (ViewModel is not { } viewModel) + return; + if (args.IsFocused) { // Path Mode needs special handling when gaining focus since it has an unfocused state if (Omnibar.CurrentSelectedMode == OmnibarPathMode) { - ViewModel.PathText = string.IsNullOrEmpty(ContentPageContext.ShellPage?.ShellViewModel?.WorkingDirectory) + var workingDirectory = ContentPageContext.ShellPage?.ShellViewModel?.WorkingDirectory; + viewModel.PathText = string.IsNullOrEmpty(workingDirectory) ? Constants.UserEnvironmentPaths.HomePath - : ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + : workingDirectory; - await DispatcherQueue.EnqueueOrInvokeAsync(ViewModel.PopulateOmnibarSuggestionsForPathMode); + await DispatcherQueue.EnqueueOrInvokeAsync(viewModel.PopulateOmnibarSuggestionsForPathMode); } } else { if (Omnibar.CurrentSelectedMode == OmnibarSearchMode) { - ViewModel?.CancelSuggestionSearch(); + viewModel.CancelSuggestionSearch(); } // When Omnibar loses focus, revert to Path Mode to display BreadcrumbBar @@ -437,7 +459,7 @@ private async void Omnibar_PreviewKeyDown(object sender, KeyRoutedEventArgs e) if (e.Key is VirtualKey.Escape) { Omnibar.IsFocused = false; - ContentPageContext.ShellPage!.PaneHolder.FocusActivePane(); + ContentPageContext.ShellPage?.PaneHolder?.FocusActivePane(); } else if (e.Key is VirtualKey.Tab && Omnibar.IsFocused && !InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down)) { @@ -463,17 +485,19 @@ private void NavigationButtonOverflowFlyoutButton_LosingFocus(UIElement sender, private void BreadcrumbBarItem_DragLeave(object sender, DragEventArgs e) { - ViewModel.PathBoxItem_DragLeave(sender, e); + ViewModel?.PathBoxItem_DragLeave(sender, e); } private async void BreadcrumbBarItem_DragOver(object sender, DragEventArgs e) { - await ViewModel.PathBoxItem_DragOver(sender, e); + if (ViewModel is { } viewModel) + await viewModel.PathBoxItem_DragOver(sender, e); } private async void BreadcrumbBarItem_Drop(object sender, DragEventArgs e) { - await ViewModel.PathBoxItem_Drop(sender, e); + if (ViewModel is { } viewModel) + await viewModel.PathBoxItem_Drop(sender, e); } private void BreadcrumbBarItem_RightTapped(object sender, RightTappedRoutedEventArgs e) diff --git a/src/Files.App/UserControls/Selection/RectangleSelection.cs b/src/Files.App/UserControls/Selection/RectangleSelection.cs index a3dc27287475..265ba41a5a15 100644 --- a/src/Files.App/UserControls/Selection/RectangleSelection.cs +++ b/src/Files.App/UserControls/Selection/RectangleSelection.cs @@ -19,8 +19,9 @@ public class RectangleSelection protected Rectangle selectionRectangle; protected SelectionState selectionState; - protected RectangleSelection() + protected RectangleSelection(Rectangle selectionRectangle) { + this.selectionRectangle = selectionRectangle; } /// @@ -30,11 +31,11 @@ protected RectangleSelection() /// Rectangle inside a Canvas /// SelectionChanged event associated with uiElement /// - public static RectangleSelection Create(FrameworkElement uiElement, Rectangle selectionRectangle, SelectionChangedEventHandler selectionChanged = null) + public static RectangleSelection Create(FrameworkElement uiElement, Rectangle selectionRectangle, SelectionChangedEventHandler? selectionChanged = null) { - if (uiElement is ListViewBase) + if (uiElement is ListViewBase listView) { - return new RectangleSelection_ListViewBase(uiElement as ListViewBase, selectionRectangle, selectionChanged); + return new RectangleSelection_ListViewBase(listView, selectionRectangle, selectionChanged); } else { @@ -44,9 +45,9 @@ public static RectangleSelection Create(FrameworkElement uiElement, Rectangle se public delegate void SelectionStatusHandler(object sender, EventArgs e); - public event SelectionStatusHandler SelectionStarted; + public event SelectionStatusHandler? SelectionStarted; - public event SelectionStatusHandler SelectionEnded; + public event SelectionStatusHandler? SelectionEnded; protected void OnSelectionStarted() { @@ -116,4 +117,4 @@ protected bool HasMovedMinimalDelta(double originalX, double originalY, double c return deltaX > MinSelectionDelta || deltaY > MinSelectionDelta; } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/Selection/RectangleSelection_ListViewBase.cs b/src/Files.App/UserControls/Selection/RectangleSelection_ListViewBase.cs index eb64ebb415aa..44c3d3c1b821 100644 --- a/src/Files.App/UserControls/Selection/RectangleSelection_ListViewBase.cs +++ b/src/Files.App/UserControls/Selection/RectangleSelection_ListViewBase.cs @@ -16,19 +16,19 @@ namespace Files.App.UserControls.Selection public sealed class RectangleSelection_ListViewBase : RectangleSelection { private ListViewBase uiElement; - private ScrollViewer scrollViewer; - private SelectionChangedEventHandler selectionChanged; + private ScrollViewer? scrollViewer; + private SelectionChangedEventHandler? selectionChanged; private DispatcherQueueTimer timer; private Point originDragPoint; private Dictionary itemsPosition; - private List prevSelectedItems; - private List prevSelectedItemsDrag; - private ItemSelectionStrategy selectionStrategy; + private List? prevSelectedItems; + private List? prevSelectedItemsDrag; + private ItemSelectionStrategy? selectionStrategy; - public RectangleSelection_ListViewBase(ListViewBase uiElement, Rectangle selectionRectangle, SelectionChangedEventHandler selectionChanged = null) + public RectangleSelection_ListViewBase(ListViewBase uiElement, Rectangle selectionRectangle, SelectionChangedEventHandler? selectionChanged = null) + : base(selectionRectangle) { this.uiElement = uiElement; - this.selectionRectangle = selectionRectangle; this.selectionChanged = selectionChanged; itemsPosition = []; timer = DispatcherQueue.GetForCurrentThread().CreateTimer(); @@ -52,7 +52,7 @@ private void RectangleSelection_PointerMoved(object sender, PointerRoutedEventAr } // Clear selected items once if the pointer is pressed and moved - selectionStrategy.StartSelection(); + selectionStrategy?.StartSelection(); OnSelectionStarted(); selectionState = SelectionState.Active; } @@ -71,11 +71,11 @@ private void RectangleSelection_PointerMoved(object sender, PointerRoutedEventAr { if (rect.IntersectsWith(item.Value)) { - selectionStrategy.HandleIntersectionWithItem(item.Key); + selectionStrategy?.HandleIntersectionWithItem(item.Key); } else { - selectionStrategy.HandleNoIntersectionWithItem(item.Key); + selectionStrategy?.HandleNoIntersectionWithItem(item.Key); } } catch (ArgumentException) @@ -157,6 +157,9 @@ private void RectangleSelection_PointerPressed(object sender, PointerRoutedEvent private void FetchItemsPosition() { + if (scrollViewer is null) + return; + var verticalOffset = scrollViewer.VerticalOffset; foreach (var item in uiElement.Items.ToList().Except(itemsPosition.Keys)) { @@ -173,7 +176,7 @@ private void FetchItemsPosition() } } - private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e) + private void ScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) { if (!timer.IsRunning) { @@ -227,7 +230,7 @@ private void RectangleSelection_SizeChanged(object sender, object e) } } - private void InitEvents(object sender, RoutedEventArgs e) + private void InitEvents(object? sender, RoutedEventArgs? e) { if (!uiElement.IsLoaded) { @@ -249,4 +252,4 @@ private void InitEvents(object sender, RoutedEventArgs e) } } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/StatusCenter/SpeedGraph.cs b/src/Files.App/UserControls/StatusCenter/SpeedGraph.cs index 3a4dcae09190..500f1afdf0d6 100644 --- a/src/Files.App/UserControls/StatusCenter/SpeedGraph.cs +++ b/src/Files.App/UserControls/StatusCenter/SpeedGraph.cs @@ -28,19 +28,19 @@ public ObservableCollection Points Compositor compositor; - ContainerVisual rootVisual; + ContainerVisual? rootVisual; - CompositionPathGeometry graphGeometry; - InsetClip graphClip; + CompositionPathGeometry? graphGeometry; + InsetClip? graphClip; - SpriteVisual line; + SpriteVisual? line; - CompositionColorBrush backgroundBrush; - CompositionColorGradientStop graphFillBottom; - CompositionColorGradientStop graphFillTop; - CompositionColorBrush graphStrokeBrush; + CompositionColorBrush? backgroundBrush; + CompositionColorGradientStop? graphFillBottom; + CompositionColorGradientStop? graphFillTop; + CompositionColorBrush? graphStrokeBrush; - LinearEasingFunction linearEasing; + LinearEasingFunction? linearEasing; bool initialized; @@ -196,6 +196,9 @@ private void InitGraph() void UpdateGraph() { + if (graphGeometry is null || line is null || linearEasing is null || graphClip is null) + return; + var path = CreatePathFromPoints(); graphGeometry.Path = path; @@ -231,6 +234,9 @@ CompositionPath CreatePathFromPoints() void SetGraphColors() { + if (backgroundBrush is null || graphFillTop is null || graphFillBottom is null || graphStrokeBrush is null) + return; + var accentColor = themeModeService.DefaultAccentColor; var veryLightColor = accentColor with { A = 0x0f }; diff --git a/src/Files.App/UserControls/TabBar/BaseTabBar.cs b/src/Files.App/UserControls/TabBar/BaseTabBar.cs index 7196b481592c..49034b53466b 100644 --- a/src/Files.App/UserControls/TabBar/BaseTabBar.cs +++ b/src/Files.App/UserControls/TabBar/BaseTabBar.cs @@ -12,7 +12,7 @@ namespace Files.App.UserControls.TabBar /// public abstract class BaseTabBar : UserControl, ITabBar, INotifyPropertyChanged { - protected ITabBarItemContent CurrentSelectedAppInstance; + protected ITabBarItemContent? CurrentSelectedAppInstance; public static event EventHandler? OnLoaded; public static event PropertyChangedEventHandler? StaticPropertyChanged; @@ -28,7 +28,7 @@ public abstract class BaseTabBar : UserControl, ITabBar, INotifyPropertyChanged public ObservableCollection Items => MainPageViewModel.AppInstances; - public event EventHandler CurrentInstanceChanged; + public event EventHandler? CurrentInstanceChanged; private static bool _IsRestoringClosedTab; public static bool IsRestoringClosedTab @@ -46,12 +46,12 @@ public BaseTabBar() Loaded += TabView_Loaded; } - public virtual DependencyObject ContainerFromItem(ITabBarItem item) + public virtual DependencyObject? ContainerFromItem(ITabBarItem item) { return null; } - private void TabView_CurrentInstanceChanged(object sender, CurrentInstanceChangedEventArgs e) + private void TabView_CurrentInstanceChanged(object? sender, CurrentInstanceChangedEventArgs e) { foreach (ITabBarItemContent instance in e.PageInstances) { @@ -62,7 +62,7 @@ private void TabView_CurrentInstanceChanged(object sender, CurrentInstanceChange } } - protected void TabView_SelectionChanged(object sender, SelectionChangedEventArgs e) + protected void TabView_SelectionChanged(object? sender, SelectionChangedEventArgs? e) { if (App.AppModel.TabStripSelectedIndex >= 0 && App.AppModel.TabStripSelectedIndex < Items.Count) { @@ -100,7 +100,7 @@ public void TabView_Loaded(object sender, RoutedEventArgs e) OnLoaded?.Invoke(null, this); } - public ITabBarItemContent GetCurrentSelectedTabInstance() + public ITabBarItemContent? GetCurrentSelectedTabInstance() { return MainPageViewModel.AppInstances[App.AppModel.TabStripSelectedIndex].TabItemContent; } @@ -118,7 +118,7 @@ public static void PushRecentTab(TabBarItemParameter[] tab) public List GetAllTabInstances() { - return MainPageViewModel.AppInstances.Select(x => x.TabItemContent).ToList(); + return MainPageViewModel.AppInstances.Select(x => x.TabItemContent).OfType().ToList(); } public async Task ReopenClosedTabAsync() @@ -136,10 +136,11 @@ public async Task ReopenClosedTabAsync() public async void MoveTabToNewWindowAsync(object sender, RoutedEventArgs e) { - await MultitaskingTabsHelpers.MoveTabToNewWindow(((FrameworkElement)sender).DataContext as TabBarItem, this); + if (sender is FrameworkElement { DataContext: TabBarItem tabItem }) + await MultitaskingTabsHelpers.MoveTabToNewWindow(tabItem, this); } - public void CloseTab(TabBarItem tabItem) + public void CloseTab(TabBarItem? tabItem) { if (tabItem is null) return; @@ -148,10 +149,8 @@ public void CloseTab(TabBarItem tabItem) tabItem.Unload(); // Dispose and save tab arguments - PushRecentTab( - [ - tabItem.NavigationParameter, - ]); + if (tabItem.NavigationParameter is { } navigationParameter) + PushRecentTab([navigationParameter]); // Save the updated tab list AppLifecycleHelper.SaveSessionTabs(); diff --git a/src/Files.App/UserControls/TabBar/ITabBar.cs b/src/Files.App/UserControls/TabBar/ITabBar.cs index fad18875b97b..9f5260379fee 100644 --- a/src/Files.App/UserControls/TabBar/ITabBar.cs +++ b/src/Files.App/UserControls/TabBar/ITabBar.cs @@ -8,17 +8,17 @@ namespace Files.App.UserControls.TabBar /// public interface ITabBar { - public event EventHandler CurrentInstanceChanged; + public event EventHandler? CurrentInstanceChanged; public ObservableCollection Items { get; } - public ITabBarItemContent GetCurrentSelectedTabInstance(); + public ITabBarItemContent? GetCurrentSelectedTabInstance(); public List GetAllTabInstances(); public Task ReopenClosedTabAsync(); - public void CloseTab(TabBarItem tabItem); + public void CloseTab(TabBarItem? tabItem); public void SetLoadingIndicatorStatus(ITabBarItem item, bool loading); } diff --git a/src/Files.App/UserControls/TabBar/ITabBarItem.cs b/src/Files.App/UserControls/TabBar/ITabBarItem.cs index e80b49f9cefa..39756ff2b2fe 100644 --- a/src/Files.App/UserControls/TabBar/ITabBarItem.cs +++ b/src/Files.App/UserControls/TabBar/ITabBarItem.cs @@ -10,14 +10,14 @@ namespace Files.App.UserControls.TabBar /// public interface ITabBarItem { - IconSource IconSource { get; } + IconSource? IconSource { get; } - string Header { get; } + string? Header { get; } - string Description { get; } + string? Description { get; } bool AllowStorageItemDrop { get; } - public TabBarItemParameter NavigationParameter { get; } + public TabBarItemParameter? NavigationParameter { get; } } } diff --git a/src/Files.App/UserControls/TabBar/ITabBarItemContent.cs b/src/Files.App/UserControls/TabBar/ITabBarItemContent.cs index 2a9ffdad49db..45896a6ab30b 100644 --- a/src/Files.App/UserControls/TabBar/ITabBarItemContent.cs +++ b/src/Files.App/UserControls/TabBar/ITabBarItemContent.cs @@ -14,7 +14,7 @@ public interface ITabBarItemContent public TabBarItemParameter? TabBarItemParameter { get; } - public event EventHandler ContentChanged; + public event EventHandler? ContentChanged; public Task TabItemDragOver(object sender, DragEventArgs e); diff --git a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs index b2a36829693c..91d5e17b408c 100644 --- a/src/Files.App/UserControls/TabBar/TabBar.xaml.cs +++ b/src/Files.App/UserControls/TabBar/TabBar.xaml.cs @@ -81,7 +81,11 @@ public TabBar() private void TabView_TabItemsChanged(TabView sender, Windows.Foundation.Collections.IVectorChangedEventArgs args) { if (args.CollectionChange == Windows.Foundation.Collections.CollectionChange.ItemRemoved) - App.AppModel.TabStripSelectedIndex = Items.IndexOf(HorizontalTabView.SelectedItem as TabBarItem); + { + App.AppModel.TabStripSelectedIndex = HorizontalTabView.SelectedItem is TabBarItem selectedItem + ? Items.IndexOf(selectedItem) + : -1; + } if (App.AppModel.TabStripSelectedIndex >= 0 && App.AppModel.TabStripSelectedIndex < Items.Count) { @@ -102,19 +106,25 @@ private void TabView_TabItemsChanged(TabView sender, Windows.Foundation.Collecti private async void TabViewItem_Drop(object sender, DragEventArgs e) { - await ((sender as TabViewItem).DataContext as TabBarItem).TabItemContent.TabItemDrop(sender, e); + if (sender is not TabViewItem { DataContext: TabBarItem { TabItemContent: { } tabContent } }) + return; + + await tabContent.TabItemDrop(sender, e); HorizontalTabView.CanReorderTabs = true; tabHoverTimer.Stop(); } private async void TabViewItem_DragEnter(object sender, DragEventArgs e) { - await ((sender as TabViewItem).DataContext as TabBarItem).TabItemContent.TabItemDragOver(sender, e); + if (sender is not TabViewItem { DataContext: TabBarItem { TabItemContent: { } tabContent } } tabViewItem) + return; + + await tabContent.TabItemDragOver(sender, e); if (e.AcceptedOperation != DataPackageOperation.None) { HorizontalTabView.CanReorderTabs = false; tabHoverTimer.Start(); - hoveredTabViewItem = sender as TabViewItem; + hoveredTabViewItem = tabViewItem; } } @@ -125,11 +135,11 @@ private void TabViewItem_DragLeave(object sender, DragEventArgs e) } // Select tab that is hovered over for a certain duration - private void TabHoverSelected(object sender, object e) + private void TabHoverSelected(object? sender, object e) { tabHoverTimer.Stop(); - if (hoveredTabViewItem is not null) - App.AppModel.TabStripSelectedIndex = Items.IndexOf(hoveredTabViewItem.DataContext as TabBarItem); + if (hoveredTabViewItem?.DataContext is TabBarItem tabItem) + App.AppModel.TabStripSelectedIndex = Items.IndexOf(tabItem); } private void TabView_TabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args) @@ -137,7 +147,9 @@ private void TabView_TabDragStarting(TabView sender, TabViewTabDragStartingEvent // Reset value isCancelingDragOperation = false; - var tabViewItemArgs = (args.Item as TabBarItem).NavigationParameter; + if (args.Item is not TabBarItem { NavigationParameter: { } tabViewItemArgs } tabItem) + return; + args.Data.Properties.Add(TabPathIdentifier, tabViewItemArgs.Serialize()); args.Data.RequestedOperation = DataPackageOperation.Move; @@ -150,7 +162,7 @@ private void TabView_TabDragStarting(TabView sender, TabViewTabDragStartingEvent Focus(FocusState.Programmatic); PreviewKeyDown += TabDragging_PreviewKeyDown; - TabDragStarted?.Invoke(this, args.Item as TabBarItem); + TabDragStarted?.Invoke(this, tabItem); } private void TabDragging_PreviewKeyDown(object sender, KeyRoutedEventArgs e) @@ -199,6 +211,8 @@ private async void TabView_TabStripDrop(object sender, DragEventArgs e) for (int i = 0; i < tabStrip.TabItems.Count; i++) { var item = tabStrip.ContainerFromIndex(i) as TabViewItem; + if (item is null) + continue; if (e.GetPosition(item).X - item.ActualWidth < 0) { @@ -247,18 +261,20 @@ private async void TabView_TabDroppedOutside(TabView sender, TabViewTabDroppedOu return; var indexOfTabViewItem = sender.TabItems.IndexOf(args.Item); - var tabViewItemArgs = (args.Item as TabBarItem).NavigationParameter; + if (args.Item is not TabBarItem { NavigationParameter: { } tabViewItemArgs } tabItem) + return; + var selectedTabViewItemIndex = sender.SelectedIndex; - Items.Remove(args.Item as TabBarItem); + Items.Remove(tabItem); if (!await NavigationHelpers.OpenTabInNewWindowAsync(tabViewItemArgs.Serialize(), droppedPoint.X, droppedPoint.Y)) { - Items.Insert(indexOfTabViewItem, args.Item as TabBarItem); + Items.Insert(indexOfTabViewItem, tabItem); sender.SelectedIndex = selectedTabViewItemIndex; } else // Dispose tab arguments - (args.Item as TabBarItem)?.Unload(); + tabItem.Unload(); } private void TabView_PointerWheelChanged(object sender, PointerRoutedEventArgs e) @@ -378,4 +394,4 @@ private async void DragAreaRectangle_Loaded(object sender, RoutedEventArgs e) Math.Max(0, HorizontalTabView.ActualHeight))); } } -} \ No newline at end of file +} diff --git a/src/Files.App/UserControls/TabBar/TabBarItem.cs b/src/Files.App/UserControls/TabBar/TabBarItem.cs index e82897386a41..c283d9047cb0 100644 --- a/src/Files.App/UserControls/TabBar/TabBarItem.cs +++ b/src/Files.App/UserControls/TabBar/TabBarItem.cs @@ -13,10 +13,10 @@ public sealed partial class TabBarItem : ObservableObject, ITabBarItem, IDisposa { public Frame ContentFrame { get; private set; } - public event EventHandler ContentChanged; + public event EventHandler? ContentChanged; - private IconSource _IconSource; - public IconSource IconSource + private IconSource? _IconSource; + public IconSource? IconSource { get => _IconSource; set => SetProperty(ref _IconSource, value); @@ -50,8 +50,8 @@ public bool AllowStorageItemDrop set => SetProperty(ref _AllowStorageItemDrop, value); } - private TabBarItemParameter _NavigationArguments; - public TabBarItemParameter NavigationParameter + private TabBarItemParameter? _NavigationArguments; + public TabBarItemParameter? NavigationParameter { get => _NavigationArguments; set @@ -71,7 +71,7 @@ public TabBarItemParameter NavigationParameter } } - public ITabBarItemContent TabItemContent + public ITabBarItemContent? TabItemContent => ContentFrame?.Content as ITabBarItemContent; public TabBarItem() diff --git a/src/Files.App/UserControls/Toolbar.xaml.cs b/src/Files.App/UserControls/Toolbar.xaml.cs index d20dea5b9670..56d472861706 100644 --- a/src/Files.App/UserControls/Toolbar.xaml.cs +++ b/src/Files.App/UserControls/Toolbar.xaml.cs @@ -99,8 +99,8 @@ private void AppearanceSettings_PropertyChanged(object? sender, PropertyChangedE private async void EditTagsMenu_TagsChanged(object? sender, EventArgs e) { - if (PageContext.ShellPage is not null) - await PageContext.ShellPage.ShellViewModel.RefreshTagGroups(); + if (PageContext.ShellPage?.ShellViewModel is { } shellViewModel) + await shellViewModel.RefreshTagGroups(); } private void RequestToolbarRefresh(bool ignoreDebounce) @@ -200,18 +200,19 @@ and not ContentPageTypes.RecycleBin } else { - btn.Flyout = new MenuFlyout + var menuFlyout = new MenuFlyout { Placement = group is NewItemCommandGroup or OpenWithCommandGroup ? FlyoutPlacementMode.BottomEdgeAlignedLeft : FlyoutPlacementMode.Bottom }; + btn.Flyout = menuFlyout; - ((MenuFlyout)btn.Flyout).Opening += async (s, _) => + menuFlyout.Opening += async (s, _) => { try { - await PopulateGroupFlyoutAsync((MenuFlyout)s, group); + await PopulateGroupFlyoutAsync(menuFlyout, group); } catch (Exception ex) { diff --git a/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs b/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs index e9bba29b081f..b6e8fe566d1f 100644 --- a/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs +++ b/src/Files.App/UserControls/Widgets/RecentFilesWidget.xaml.cs @@ -22,10 +22,10 @@ public RecentFilesWidget() private void RecentFilesListView_ItemClick(object sender, ItemClickEventArgs e) { - if (e.ClickedItem is not RecentItem item) + if (e.ClickedItem is not RecentItem { Path: { Length: > 0 } path }) return; - ViewModel.NavigateToPath(item.Path); + ViewModel.NavigateToPath(path); } private void RecentFilesListView_RightTapped(object sender, RightTappedRoutedEventArgs e) diff --git a/src/Files.App/Utils/Cloud/CloudDriveSyncStatusUI.cs b/src/Files.App/Utils/Cloud/CloudDriveSyncStatusUI.cs index 4314f96eb293..447f98c4f136 100644 --- a/src/Files.App/Utils/Cloud/CloudDriveSyncStatusUI.cs +++ b/src/Files.App/Utils/Cloud/CloudDriveSyncStatusUI.cs @@ -7,9 +7,9 @@ namespace Files.App.Utils.Cloud { public sealed partial class CloudDriveSyncStatusUI : ObservableObject { - public string Glyph { get; } + public string? Glyph { get; } - public Style ThemedIconStyle { get; } + public Style? ThemedIconStyle { get; } public CloudDriveSyncStatus SyncStatus { get; } diff --git a/src/Files.App/Utils/Cloud/CloudDrivesDetector.cs b/src/Files.App/Utils/Cloud/CloudDrivesDetector.cs index 8e296039fa6a..db54c3c4b2ab 100644 --- a/src/Files.App/Utils/Cloud/CloudDrivesDetector.cs +++ b/src/Files.App/Utils/Cloud/CloudDrivesDetector.cs @@ -10,11 +10,11 @@ namespace Files.App.Utils.Cloud [SupportedOSPlatform("Windows10.0.10240")] public sealed class CloudDrivesDetector { - private readonly static string programFilesFolder = Environment.GetEnvironmentVariable("ProgramFiles"); + private readonly static string programFilesFolder = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); public static async Task> DetectCloudDrives() { - var tasks = new Task>[] + var tasks = new Task?>[] { SafetyExtensions.IgnoreExceptions(DetectOneDrive, App.Logger), SafetyExtensions.IgnoreExceptions(DetectSharepoint, App.Logger), @@ -26,11 +26,11 @@ public static async Task> DetectCloudDrives() SafetyExtensions.IgnoreExceptions(DetectAutodeskDrive, App.Logger), }; - await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks); - return tasks - .Where(o => o.Result is not null) - .SelectMany(o => o.Result) + return results + .WhereNotNull() + .SelectMany(providers => providers) .OrderBy(o => o.ID.ToString()) .ThenBy(o => o.Name) .Distinct(); @@ -121,7 +121,7 @@ private static Task> DetectGenericCloudDrive() CloudProviders.LucidLink => !string.IsNullOrEmpty(clsidDefaultValue) ? clsidDefaultValue : "lucidLink", CloudProviders.SyncDrive => !string.IsNullOrEmpty(clsidDefaultValue) ? clsidDefaultValue : "Sync", CloudProviders.MagentaCloud => !string.IsNullOrEmpty(clsidDefaultValue) ? clsidDefaultValue : "MagentaCLOUD", - _ => null + _ => cloudProvider.Value.ToString() }, SyncFolder = syncedFolder, IconData = GetIconData(iconPath) @@ -132,7 +132,7 @@ private static Task> DetectGenericCloudDrive() return Task.FromResult>(results); } - private static byte[]? GetIconData(string iconPath) + private static byte[]? GetIconData(string? iconPath) { if (string.IsNullOrEmpty(iconPath) || !File.Exists(iconPath)) return null; @@ -148,7 +148,7 @@ private static Task> DetectOneDrive() using var oneDriveAccountsKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\OneDrive\Accounts"); if (oneDriveAccountsKey is null) { - return Task.FromResult>(null); + return Task.FromResult>([]); } var oneDriveAccounts = new List(); @@ -165,7 +165,7 @@ private static Task> DetectOneDrive() { Name = accountName, SyncFolder = userFolder, - IconData = GetIconData($"Environment.GetEnvironmentVariable(\"ProgramFiles\")\\Microsoft OneDrive\\OneDrive.exe"), + IconData = GetIconData(Path.Combine(programFilesFolder, "Microsoft OneDrive", "OneDrive.exe")), }); } } @@ -178,7 +178,7 @@ private static Task> DetectSharepoint() using var oneDriveAccountsKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\OneDrive\Accounts"); if (oneDriveAccountsKey is null) { - return Task.FromResult>(null); + return Task.FromResult>([]); } var sharepointAccounts = new List(); @@ -296,7 +296,7 @@ private static Task> DetectSeadriveDrive() using var SeadriveKey = Registry.CurrentUser.OpenSubKey(@"Software\SeaDrive\Seafile Drive Client\Settings"); var syncFolder = (string?)SeadriveKey?.GetValue("seadriveRoot"); - if (SeadriveKey is not null) + if (!string.IsNullOrWhiteSpace(syncFolder)) { string iconPath = Path.Combine(programFilesFolder, "SeaDrive", "bin", "seadrive.exe"); var iconFile = Win32Helper.ExtractSelectedIconsFromDLL(iconPath, new List() { 101 }).FirstOrDefault(); diff --git a/src/Files.App/Utils/Cloud/CloudDrivesManager.cs b/src/Files.App/Utils/Cloud/CloudDrivesManager.cs index 8e845dd50ed8..31e839c09cea 100644 --- a/src/Files.App/Utils/Cloud/CloudDrivesManager.cs +++ b/src/Files.App/Utils/Cloud/CloudDrivesManager.cs @@ -12,7 +12,7 @@ public static class CloudDrivesManager { private static readonly ILogger _logger = Ioc.Default.GetRequiredService>(); private static readonly ICloudDetector _detector = Ioc.Default.GetRequiredService(); - public static EventHandler DataChanged; + public static event EventHandler? DataChanged; private static readonly List _Drives = []; public static IReadOnlyList Drives @@ -116,4 +116,4 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Cloud/Detector/BoxCloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/BoxCloudDetector.cs index 42b7104290ac..11d4a0411588 100644 --- a/src/Files.App/Utils/Cloud/Detector/BoxCloudDetector.cs +++ b/src/Files.App/Utils/Cloud/Detector/BoxCloudDetector.cs @@ -17,9 +17,14 @@ protected override async IAsyncEnumerable GetProviders() string configPathBoxSync = Path.Combine(UserDataPaths.GetDefault().LocalAppData, @"Box Sync\sync_root_folder.txt"); - StorageFile configFile = await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(configPathBoxDrive).AsTask()); + var configFileResult = await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(configPathBoxDrive).AsTask()); + var configFile = configFileResult.Result; - configFile ??= await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(configPathBoxSync).AsTask()); + if (configFile is null) + { + configFileResult = await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(configPathBoxSync).AsTask()); + configFile = configFileResult.Result; + } if (configFile is not null) { diff --git a/src/Files.App/Utils/Cloud/Detector/DropBoxCloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/DropBoxCloudDetector.cs index 5905b16eaec1..b1ab7485918e 100644 --- a/src/Files.App/Utils/Cloud/Detector/DropBoxCloudDetector.cs +++ b/src/Files.App/Utils/Cloud/Detector/DropBoxCloudDetector.cs @@ -74,24 +74,26 @@ private async IAsyncEnumerable ParseInfoJson(string jsonPath) if (jsonElem.TryGetProperty("personal", out JsonElement inner)) { - string dropBoxPath = inner.GetProperty("path").GetString(); - - yield return new CloudProvider(CloudProviders.DropBox) + if (inner.GetProperty("path").GetString() is { Length: > 0 } dropBoxPath) { - Name = "Dropbox", - SyncFolder = dropBoxPath, - }; + yield return new CloudProvider(CloudProviders.DropBox) + { + Name = "Dropbox", + SyncFolder = dropBoxPath, + }; + } } if (jsonElem.TryGetProperty("business", out JsonElement innerBusiness)) { - string dropBoxPath = innerBusiness.GetProperty("path").GetString(); - - yield return new CloudProvider(CloudProviders.DropBox) + if (innerBusiness.GetProperty("path").GetString() is { Length: > 0 } dropBoxPath) { - Name = "Dropbox Business", - SyncFolder = dropBoxPath, - }; + yield return new CloudProvider(CloudProviders.DropBox) + { + Name = "Dropbox Business", + SyncFolder = dropBoxPath, + }; + } } } } diff --git a/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs index 502a4c4a1fa7..b25517eccba6 100644 --- a/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs +++ b/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs @@ -64,13 +64,12 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a } var folderResult = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(path).AsTask()); - if (!folderResult) + if (folderResult.Result is not { } folder) { _logger.LogWarning($"Could not access Google Drive path as local storage: {path}"); continue; } - var folder = folderResult.Result; string title = reader["title"]?.ToString() ?? folder.Name; Debug.WriteLine("YIELD RETURNING from `GoogleDriveCloudDetector.GetProviders()` (roots): "); @@ -97,13 +96,12 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a continue; var folderResult = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(path).AsTask()); - if (!folderResult) + if (folderResult.Result is not { } folder) { _logger.LogWarning($"Could not access Google Drive path as local storage: {path}"); continue; } - var folder = folderResult.Result; string title = reader["name"]?.ToString() ?? folder.Name; Debug.WriteLine("YIELD RETURNING from `GoogleDriveCloudDetector.GetProviders` (media): "); diff --git a/src/Files.App/Utils/Cloud/Detector/SynologyDriveCloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/SynologyDriveCloudDetector.cs index df75d90a789f..ca7e4c9e20b8 100644 --- a/src/Files.App/Utils/Cloud/Detector/SynologyDriveCloudDetector.cs +++ b/src/Files.App/Utils/Cloud/Detector/SynologyDriveCloudDetector.cs @@ -43,20 +43,28 @@ protected override async IAsyncEnumerable GetProviders() var reader = cmdConnection.ExecuteReader(); while (reader.Read()) { + var connectionId = reader["id"]?.ToString(); + var connectionType = reader["conn_type"]?.ToString(); + if (string.IsNullOrEmpty(connectionId) || string.IsNullOrEmpty(connectionType)) + continue; + var connection = ( - ConnectionType: reader["conn_type"]?.ToString(), - HostName: reader["host_name"]?.ToString() + ConnectionType: connectionType, + HostName: reader["host_name"]?.ToString() ?? string.Empty ); - connections.Add(reader["id"]?.ToString(), connection); + connections.Add(connectionId, connection); } reader = cmdTable.ExecuteReader(); while (reader.Read()) { // Extract the data from the reader - if (connections[reader["conn_id"]?.ToString()].ConnectionType is "1") + var connectionId = reader["conn_id"]?.ToString(); + if (!string.IsNullOrEmpty(connectionId) && + connections.TryGetValue(connectionId, out var connection) && + connection.ConnectionType is "1") { string? path = reader["sync_folder"]?.ToString(); @@ -68,7 +76,7 @@ protected override async IAsyncEnumerable GetProviders() yield return new CloudProvider(CloudProviders.SynologyDrive) { SyncFolder = path, - Name = $"Synology Drive - {connections[reader["conn_id"]?.ToString()].HostName} ({folder.Name})", + Name = $"Synology Drive - {connection.HostName} ({folder.Name})", }; } } diff --git a/src/Files.App/Utils/CommandLine/CommandLineParser.cs b/src/Files.App/Utils/CommandLine/CommandLineParser.cs index 77164b60e829..6f96a0b25113 100644 --- a/src/Files.App/Utils/CommandLine/CommandLineParser.cs +++ b/src/Files.App/Utils/CommandLine/CommandLineParser.cs @@ -129,7 +129,7 @@ public static string[] SplitArguments(string commandLine, bool trimQuotes = fals /// /// /// - public static List> Parse(string[] args = null) + public static List> Parse(string[]? args = null) { var parsedArgs = new List>(); @@ -163,7 +163,7 @@ public static List> Parse(string[] args = null) } } - if (parsedArgs.Count == 0 && args.Length >= 2) + if (parsedArgs.Count == 0 && args is { Length: >= 2 }) parsedArgs.Add(new KeyValuePair("Cmdless", [string.Join(' ', args.Skip(1)).TrimStart()])); return parsedArgs; diff --git a/src/Files.App/Utils/FileTags/FileTagsHelper.cs b/src/Files.App/Utils/FileTags/FileTagsHelper.cs index 7e95c07c77d5..2fd834672f60 100644 --- a/src/Files.App/Utils/FileTags/FileTagsHelper.cs +++ b/src/Files.App/Utils/FileTags/FileTagsHelper.cs @@ -136,7 +136,7 @@ public static async Task RemoveTagsAsync(IEnumerable items) public static ulong? GetFileFRN(string filePath) => Win32Helper.GetFileFRN(filePath); - public static Task GetFileFRN(IStorageItem item) + public static Task GetFileFRN(IStorageItem? item) { return item switch { @@ -152,4 +152,4 @@ public static async Task RemoveTagsAsync(IEnumerable items) } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/FileTags/FileTagsManager.cs b/src/Files.App/Utils/FileTags/FileTagsManager.cs index 284dedd09e1d..60b8e2d41227 100644 --- a/src/Files.App/Utils/FileTags/FileTagsManager.cs +++ b/src/Files.App/Utils/FileTags/FileTagsManager.cs @@ -9,9 +9,9 @@ namespace Files.App.Utils.FileTags public sealed class FileTagsManager { private readonly ILogger logger = Ioc.Default.GetRequiredService>(); - private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetService(); + private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService(); - public EventHandler DataChanged; + public event EventHandler? DataChanged; private readonly List fileTags = []; public IReadOnlyList FileTags diff --git a/src/Files.App/Utils/Git/GitHelpers.cs b/src/Files.App/Utils/Git/GitHelpers.cs index f6eb2a1ca182..f642e57ffb6f 100644 --- a/src/Files.App/Utils/Git/GitHelpers.cs +++ b/src/Files.App/Utils/Git/GitHelpers.cs @@ -19,7 +19,7 @@ internal static partial class GitHelpers private static LibGit2Service _implementation = Ioc.Default.GetRequiredService(); // TODO: Replace with IVersionControl abstraction when it is complete /// - public static string? GetGitRepositoryPath(string? path, string root) => _implementation.GetGitRepositoryPath(path, root); + public static string? GetGitRepositoryPath(string? path, string? root) => _implementation.GetGitRepositoryPath(path, root); /// public static string GetOriginRepositoryName(string? path) => _implementation.GetOriginRepositoryName(path); @@ -668,4 +668,4 @@ public static async Task CloneRepoAsync(string repoUrl, string repoName, string #endregion } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Global/WSLDistroManager.cs b/src/Files.App/Utils/Global/WSLDistroManager.cs index 6c8d28332476..a0696fbabd30 100644 --- a/src/Files.App/Utils/Global/WSLDistroManager.cs +++ b/src/Files.App/Utils/Global/WSLDistroManager.cs @@ -10,7 +10,7 @@ namespace Files.App.Utils { public static class WSLDistroManager { - public static EventHandler DataChanged; + public static event EventHandler? DataChanged; private static readonly List distros = []; public static IReadOnlyList Distros @@ -98,4 +98,4 @@ static bool Contains(string displayName, string distroName) => displayName.Contains(distroName, StringComparison.OrdinalIgnoreCase); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Global/WindowsStorageDeviceWatcher.cs b/src/Files.App/Utils/Global/WindowsStorageDeviceWatcher.cs index a74720b50bc8..c8657eaa1a59 100644 --- a/src/Files.App/Utils/Global/WindowsStorageDeviceWatcher.cs +++ b/src/Files.App/Utils/Global/WindowsStorageDeviceWatcher.cs @@ -12,10 +12,10 @@ namespace Files.App.Utils { public sealed class WindowsStorageDeviceWatcher : IStorageDeviceWatcher { - public event EventHandler DeviceAdded; - public event EventHandler DeviceRemoved; - public event EventHandler EnumerationCompleted; - public event EventHandler DeviceModified; + public event EventHandler? DeviceAdded; + public event EventHandler? DeviceRemoved; + public event EventHandler? EnumerationCompleted; + public event EventHandler? DeviceModified; private DeviceWatcher watcher; @@ -55,10 +55,10 @@ private async void Win32_OnDeviceAdded(object? sender, DeviceEventArgs e) if (!driveAdded.IsReady && !IsUnauthorizedDrive(driveAdded)) return; - var rootAdded = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(e.DeviceId).AsTask()); - if (!rootAdded) + var rootResult = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(e.DeviceId).AsTask()); + if (rootResult.Result is not { } rootAdded) { - App.Logger.LogWarning($"{rootAdded.ErrorCode}: Attempting to add the device, {e.DeviceId}," + App.Logger.LogWarning($"{rootResult.ErrorCode}: Attempting to add the device, {e.DeviceId}," + " failed at the StorageFolder initialization step. This device will be ignored."); return; } diff --git a/src/Files.App/Utils/Library/LibraryManager.cs b/src/Files.App/Utils/Library/LibraryManager.cs index 3ca4461ec356..54719dddc80b 100644 --- a/src/Files.App/Utils/Library/LibraryManager.cs +++ b/src/Files.App/Utils/Library/LibraryManager.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml.Controls; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.IO; using Vanara.PInvoke; using Vanara.Windows.Shell; @@ -17,7 +18,7 @@ public sealed partial class LibraryManager : IDisposable { public EventHandler? DataChanged; - private FileSystemWatcher librariesWatcher; + private FileSystemWatcher? librariesWatcher; private readonly List libraries = []; private static readonly Lazy lazy = new(() => new LibraryManager()); @@ -76,11 +77,11 @@ public static async Task> ListUserLibraries() var libFiles = Directory.EnumerateFiles(ShellLibraryItem.LibrariesPath, "*" + ShellLibraryItem.EXTENSION); foreach (var libFile in libFiles) { - using var shellItem = new ShellLibraryEx(Shell32.ShellUtil.GetShellItemForPath(libFile), true); - if (shellItem is ShellLibraryEx library) - { - libraryItems.Add(ShellFolderExtensions.GetShellLibraryItem(library, libFile)); - } + if (Shell32.ShellUtil.GetShellItemForPath(libFile) is not { } libraryItem) + continue; + + using var library = new ShellLibraryEx(libraryItem, true); + libraryItems.Add(ShellFolderExtensions.GetShellLibraryItem(library, libFile)); } return libraryItems; } @@ -113,7 +114,7 @@ public async Task UpdateLibrariesAsync() DataChanged?.Invoke(SectionType.Library, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - public bool TryGetLibrary(string path, out LibraryLocationItem library) + public bool TryGetLibrary(string? path, [NotNullWhen(true)] out LibraryLocationItem? library) { if (string.IsNullOrWhiteSpace(path) || !path.EndsWith(ShellLibraryItem.EXTENSION, StringComparison.OrdinalIgnoreCase)) { @@ -134,7 +135,7 @@ public async Task CreateNewLibrary(string name) if (string.IsNullOrWhiteSpace(name) || !CanCreateLibrary(name).result) return false; - var newLib = new LibraryLocationItem(await STATask.Run(() => + var shellLibrary = await STATask.Run(() => { try { @@ -142,26 +143,29 @@ public async Task CreateNewLibrary(string name) library.Folders.Add(ShellItem.Open(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments))); // Add default folder so it's not empty library.Commit(); library.Reload(); - return Task.FromResult(ShellFolderExtensions.GetShellLibraryItem(library, library.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing))); + var libraryPath = library.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing); + return Task.FromResult(libraryPath is null + ? null + : ShellFolderExtensions.GetShellLibraryItem(library, libraryPath)); } catch (Exception e) { App.Logger.LogWarning(e, null); } - return Task.FromResult(null); - }, App.Logger)); + return Task.FromResult(null); + }, App.Logger); - if (newLib is not null) + if (shellLibrary is null) + return false; + + var newLib = new LibraryLocationItem(shellLibrary); + lock (libraries) { - lock (libraries) - { - libraries.Add(newLib); - } - DataChanged?.Invoke(SectionType.Library, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newLib)); - return true; + libraries.Add(newLib); } - return false; + DataChanged?.Invoke(SectionType.Library, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newLib)); + return true; } /// @@ -172,7 +176,7 @@ public async Task CreateNewLibrary(string name) /// Update the library folders or null to keep current /// Update the library pinned status or null to keep current /// The new library if successfully updated - public async Task UpdateLibrary(string libraryPath, string defaultSaveFolder = null, string[] folders = null, bool? isPinned = null) + public async Task UpdateLibrary(string libraryPath, string? defaultSaveFolder = null, string[]? folders = null, bool? isPinned = null) { if (string.IsNullOrWhiteSpace(libraryPath) || (defaultSaveFolder is null && folders is null && isPinned is null)) // Nothing to update @@ -183,7 +187,10 @@ public async Task UpdateLibrary(string libraryPath, string try { bool updated = false; - using var library = new ShellLibraryEx(Shell32.ShellUtil.GetShellItemForPath(libraryPath), false); + if (Shell32.ShellUtil.GetShellItemForPath(libraryPath) is not { } libraryItem) + return Task.FromResult(null); + + using var library = new ShellLibraryEx(libraryItem, false); if (folders is not null) { if (folders.Length > 0) @@ -222,7 +229,7 @@ public async Task UpdateLibrary(string libraryPath, string { library.Commit(); library.Reload(); // Reload folders list - return Task.FromResult(ShellFolderExtensions.GetShellLibraryItem(library, libraryPath)); + return Task.FromResult(ShellFolderExtensions.GetShellLibraryItem(library, libraryPath)); } } catch (Exception e) @@ -230,7 +237,7 @@ public async Task UpdateLibrary(string libraryPath, string App.Logger.LogWarning(e, null); } - return Task.FromResult(null); + return Task.FromResult(null); }, App.Logger); var newLib = item is not null ? new LibraryLocationItem(item) : null; @@ -285,12 +292,12 @@ public static async Task ShowRestoreDefaultLibrariesDialogAsync() await ContextMenu.InvokeVerb("restorelibraries", ShellLibraryItem.LibrariesPath); await App.LibraryManager.UpdateLibrariesAsync(); }, - CloseButtonAction = (vm, e) => vm.HideDialog(), + CloseButtonAction = (vm, e) => vm.HideDialog?.Invoke(), KeyDownAction = (vm, e) => { if (e.Key == VirtualKey.Escape) { - vm.HideDialog(); + vm.HideDialog?.Invoke(); } }, DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel @@ -345,7 +352,7 @@ public static async Task ShowCreateNewLibraryDialogAsync() }, CloseButtonAction = (vm, e) => { - vm.HideDialog(); + vm.HideDialog?.Invoke(); }, KeyDownAction = async (vm, e) => { @@ -355,7 +362,7 @@ public static async Task ShowCreateNewLibraryDialogAsync() } else if (e.Key == VirtualKey.Escape) { - vm.HideDialog(); + vm.HideDialog?.Invoke(); } }, DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel @@ -363,7 +370,7 @@ public static async Task ShowCreateNewLibraryDialogAsync() await dialog.ShowAsync(); } - private void OnLibraryChanged(WatcherChangeTypes changeType, string oldPath, string newPath) + private void OnLibraryChanged(WatcherChangeTypes changeType, string? oldPath, string? newPath) { if (newPath is not null && (!newPath.ToLowerInvariant().EndsWith(ShellLibraryItem.EXTENSION, StringComparison.Ordinal) || !File.Exists(newPath))) { @@ -375,7 +382,14 @@ private void OnLibraryChanged(WatcherChangeTypes changeType, string oldPath, str if (!changeType.HasFlag(WatcherChangeTypes.Deleted)) { - var library = SafetyExtensions.IgnoreExceptions(() => new ShellLibraryEx(Shell32.ShellUtil.GetShellItemForPath(newPath), true)); + if (newPath is null || + Shell32.ShellUtil.GetShellItemForPath(newPath) is not { } libraryItem) + { + App.Logger.LogWarning($"Failed to open library after {changeType}: {newPath}"); + return; + } + + var library = SafetyExtensions.IgnoreExceptions(() => new ShellLibraryEx(libraryItem, true)); if (library is null) { App.Logger.LogWarning($"Failed to open library after {changeType}: {newPath}"); @@ -392,7 +406,7 @@ private void OnLibraryChanged(WatcherChangeTypes changeType, string oldPath, str string? path = oldPath; if (string.IsNullOrEmpty(oldPath)) { - path = library1?.FullPath; + path = library1.FullPath; } var changedLibrary = Libraries.FirstOrDefault(l => string.Equals(l.Path, path, StringComparison.OrdinalIgnoreCase)); if (changedLibrary is not null) @@ -404,7 +418,7 @@ private void OnLibraryChanged(WatcherChangeTypes changeType, string oldPath, str DataChanged?.Invoke(SectionType.Library, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, changedLibrary)); } // library is null in case it was deleted - if (library is not null && !Libraries.Any(x => x.Path == library1?.FullPath)) + if (!Libraries.Any(x => x.Path == library1.FullPath)) { var libItem = new LibraryLocationItem(library1); lock (libraries) @@ -414,7 +428,7 @@ private void OnLibraryChanged(WatcherChangeTypes changeType, string oldPath, str DataChanged?.Invoke(SectionType.Library, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, libItem)); } - library?.Dispose(); + library.Dispose(); } } @@ -441,4 +455,4 @@ public static bool IsLibraryPath(string path) public void Dispose() => librariesWatcher?.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Serialization/BaseJsonSettings.cs b/src/Files.App/Utils/Serialization/BaseJsonSettings.cs index 215952c8f0ec..e18e3e143c13 100644 --- a/src/Files.App/Utils/Serialization/BaseJsonSettings.cs +++ b/src/Files.App/Utils/Serialization/BaseJsonSettings.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; namespace Files.App.Utils.Serialization { @@ -77,6 +78,7 @@ protected virtual void Initialize(string filePath) IsAvailable = SettingsSerializer?.CreateFile(filePath) ?? false; } + [return: NotNullIfNotNull(nameof(defaultValue))] protected virtual TValue? Get(TValue? defaultValue, [CallerMemberName] string propertyName = "") { if (string.IsNullOrEmpty(propertyName)) diff --git a/src/Files.App/Utils/Serialization/IJsonSettingsSerializer.cs b/src/Files.App/Utils/Serialization/IJsonSettingsSerializer.cs index 04d273c60559..2aeb9b0af240 100644 --- a/src/Files.App/Utils/Serialization/IJsonSettingsSerializer.cs +++ b/src/Files.App/Utils/Serialization/IJsonSettingsSerializer.cs @@ -5,7 +5,7 @@ namespace Files.App.Utils.Serialization { internal interface IJsonSettingsSerializer { - string? SerializeToJson(object? obj); + string SerializeToJson(object? obj); T? DeserializeFromJson(string json); } diff --git a/src/Files.App/Utils/Serialization/ISettingsSerializer.cs b/src/Files.App/Utils/Serialization/ISettingsSerializer.cs index 39a508353a45..ee9a27b2b6a9 100644 --- a/src/Files.App/Utils/Serialization/ISettingsSerializer.cs +++ b/src/Files.App/Utils/Serialization/ISettingsSerializer.cs @@ -9,6 +9,6 @@ internal interface ISettingsSerializer string ReadFromFile(); - bool WriteToFile(string? text); + bool WriteToFile(string text); } } diff --git a/src/Files.App/Utils/Serialization/Implementation/DefaultJsonSettingsSerializer.cs b/src/Files.App/Utils/Serialization/Implementation/DefaultJsonSettingsSerializer.cs index 7965e6cbf534..3b3282025534 100644 --- a/src/Files.App/Utils/Serialization/Implementation/DefaultJsonSettingsSerializer.cs +++ b/src/Files.App/Utils/Serialization/Implementation/DefaultJsonSettingsSerializer.cs @@ -10,7 +10,7 @@ internal sealed class DefaultJsonSettingsSerializer : IJsonSettingsSerializer WriteIndented = true }; - public string? SerializeToJson(object? obj) + public string SerializeToJson(object? obj) { return JsonSerializer.Serialize(obj, Options); } diff --git a/src/Files.App/Utils/Serialization/Implementation/DefaultSettingsSerializer.cs b/src/Files.App/Utils/Serialization/Implementation/DefaultSettingsSerializer.cs index db37b20cd7f5..8a720bcdbcf3 100644 --- a/src/Files.App/Utils/Serialization/Implementation/DefaultSettingsSerializer.cs +++ b/src/Files.App/Utils/Serialization/Implementation/DefaultSettingsSerializer.cs @@ -38,10 +38,10 @@ public string ReadFromFile() { ArgumentNullException.ThrowIfNull(_filePath); - return ReadStringFromFile(_filePath); + return ReadStringFromFile(_filePath) ?? string.Empty; } - public bool WriteToFile(string? text) + public bool WriteToFile(string text) { ArgumentNullException.ThrowIfNull(_filePath); diff --git a/src/Files.App/Utils/Shell/ContextMenu.cs b/src/Files.App/Utils/Shell/ContextMenu.cs index 54cb5eaab4a5..ea10a4ac83f6 100644 --- a/src/Files.App/Utils/Shell/ContextMenu.cs +++ b/src/Files.App/Utils/Shell/ContextMenu.cs @@ -120,7 +120,7 @@ public async Task InvokeItem(int itemID, string? workingDirectory = null) { var owningThread = new ThreadWithMessageQueue(); - return await owningThread.PostMethod(() => + return await owningThread.PostMethod(() => { var shellItems = new List(); @@ -148,7 +148,7 @@ public async Task InvokeItem(int itemID, string? workingDirectory = 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, uint flags, ThreadWithMessageQueue owningThread, Func? itemFilter = null) @@ -160,11 +160,13 @@ public async Task InvokeItem(int itemID, string? workingDirectory = null) { // NOTE: The items are all in the same folder using var sf = shellItems[0].Parent; + if (sf is null) + return null; Shell32.IContextMenu menu = sf.GetChildrenUIObjects(default, shellItems); var hMenu = User32.CreatePopupMenu(); menu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, (Shell32.CMF)flags); - var contextMenu = new ContextMenu(menu, hMenu, shellItems.Select(x => x.ParsingName), owningThread, itemFilter); + var contextMenu = new ContextMenu(menu, hMenu, shellItems.Select(x => x.ParsingName).WhereNotNull(), owningThread, itemFilter); contextMenu.EnumMenuItems(hMenu, contextMenu.Items); return contextMenu; @@ -227,7 +229,9 @@ private void EnumMenuItems(Vanara.PInvoke.HMENU hMenu, List LaunchAppAsync(string application, string arguments, string workingDirectory) + public static Task LaunchAppAsync(string application, string? arguments, string? workingDirectory) { - return HandleApplicationLaunch(application, arguments, workingDirectory); + return HandleApplicationLaunch(application, arguments ?? string.Empty, workingDirectory ?? string.Empty); } public static Task RunCompatibilityTroubleshooterAsync(string filePath) @@ -104,17 +104,21 @@ private static async Task HandleApplicationLaunch(string application, stri // Refresh env variables for the child process foreach (DictionaryEntry ent in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Machine)) { - string key = (string)ent.Key; + if (ent.Key is not string key || ent.Value is not string value) + continue; // Skip USERNAME to avoid issues where files were executed as SYSTEM user (#12139) - if (string.Equals(key, "USERNAME", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(key, "USERNAME", StringComparison.OrdinalIgnoreCase)) continue; - process.StartInfo.EnvironmentVariables[key] = (string)ent.Value; + process.StartInfo.EnvironmentVariables[key] = value; } foreach (DictionaryEntry ent in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)) - process.StartInfo.EnvironmentVariables[(string)ent.Key] = (string)ent.Value; + { + if (ent.Key is string key && ent.Value is string value) + process.StartInfo.EnvironmentVariables[key] = value; + } process.StartInfo.EnvironmentVariables["PATH"] = string.Join(';', Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine), @@ -214,7 +218,7 @@ private static async Task HandleApplicationLaunch(string application, stri var isAlternateStream = RegexHelpers.AlternateStream().IsMatch(application); if (isAlternateStream) { - var basePath = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString("n")); + var basePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); Kernel32.CreateDirectory(basePath); var tempPath = Path.Combine(basePath, new string(Path.GetFileName(application).SkipWhile(x => x != ':').Skip(1).ToArray())); @@ -268,7 +272,9 @@ private static string GetMtpPath(string executable) if (executable.StartsWith("\\\\?\\", StringComparison.Ordinal)) { using var computer = new ShellFolder(Shell32.KNOWNFOLDERID.FOLDERID_ComputerFolder); - using var device = computer.FirstOrDefault(i => executable.Replace("\\\\?\\", "", StringComparison.Ordinal).StartsWith(i.Name, StringComparison.Ordinal)); + using var device = computer.FirstOrDefault(i => + i.Name is { } name && + executable.Replace("\\\\?\\", "", StringComparison.Ordinal).StartsWith(name, StringComparison.Ordinal)); var deviceId = device?.ParsingName; var itemPath = RegexHelpers.WindowsPath().Replace(executable, ""); return deviceId is not null ? Path.Combine(deviceId, itemPath) : executable; diff --git a/src/Files.App/Utils/Shell/OpenWithMenu.cs b/src/Files.App/Utils/Shell/OpenWithMenu.cs index c4b5350c5427..e49c8c8180c8 100644 --- a/src/Files.App/Utils/Shell/OpenWithMenu.cs +++ b/src/Files.App/Utils/Shell/OpenWithMenu.cs @@ -188,7 +188,8 @@ private unsafe void EnumMenuItems(HMENU hMenu) if (bitmap is not null) { bitmap.MakeTransparent(); - menuItem.Icon = (byte[])IconConverter.ConvertTo(bitmap, typeof(byte[])); + if (IconConverter.ConvertTo(bitmap, typeof(byte[])) is byte[] icon) + menuItem.Icon = icon; } } diff --git a/src/Files.App/Utils/Shell/ShellFileOperations2.cs b/src/Files.App/Utils/Shell/ShellFileOperations2.cs index 05fba030f96a..cd24f09290e8 100644 --- a/src/Files.App/Utils/Shell/ShellFileOperations2.cs +++ b/src/Files.App/Utils/Shell/ShellFileOperations2.cs @@ -12,11 +12,12 @@ public partial class ShellFileOperations2 : IDisposable { private const OperationFlags defaultOptions = OperationFlags.AllowUndo | OperationFlags.NoConfirmMkDir; private int disposedValue = 0; - private IFileOperation op; + private IFileOperation? op; private OperationFlags opFlags = defaultOptions; private HWND owner; private readonly IFileOperationProgressSink sink; private readonly uint sinkCookie; + private IFileOperation Operation => op ?? throw new ObjectDisposedException(nameof(ShellFileOperations2)); /// Initializes a new instance of the class. /// The window that owns the modal dialog. This value can be . @@ -28,11 +29,11 @@ public ShellFileOperations2(HWND owner = default) //op = new IFileOperation(); if (owner != default) { - op.SetOwnerWindow(owner); + Operation.SetOwnerWindow(owner); } sink = new OpSink(this); - sinkCookie = op.Advise(sink); + sinkCookie = Operation.Advise(sink); } /// Initializes a new instance of the class. @@ -41,7 +42,7 @@ public ShellFileOperations2(IFileOperation operation) { op = operation; sink = new OpSink(this); - sinkCookie = op.Advise(sink); + sinkCookie = Operation.Advise(sink); } /// Finalizes an instance of the class. @@ -51,56 +52,56 @@ public ShellFileOperations2(IFileOperation operation) } /// Performs caller-implemented actions after the last operation performed by the call to IFileOperation is complete. - public event EventHandler FinishOperations; + public event EventHandler? FinishOperations; /// Performs caller-implemented actions after the copy process for each item is complete. - public event EventHandler PostCopyItem; + public event EventHandler? PostCopyItem; /// Performs caller-implemented actions after the delete process for each item is complete. - public event EventHandler PostDeleteItem; + public event EventHandler? PostDeleteItem; /// Performs caller-implemented actions after the move process for each item is complete. - public event EventHandler PostMoveItem; + public event EventHandler? PostMoveItem; /// Performs caller-implemented actions after the new item is created. - public event EventHandler PostNewItem; + public event EventHandler? PostNewItem; /// Performs caller-implemented actions after the rename process for each item is complete. - public event EventHandler PostRenameItem; + public event EventHandler? PostRenameItem; /// Performs caller-implemented actions before the copy process for each item begins. - public event EventHandler PreCopyItem; + public event EventHandler? PreCopyItem; /// Performs caller-implemented actions before the delete process for each item begins. - public event EventHandler PreDeleteItem; + public event EventHandler? PreDeleteItem; /// Performs caller-implemented actions before the move process for each item begins. - public event EventHandler PreMoveItem; + public event EventHandler? PreMoveItem; /// Performs caller-implemented actions before the process to create a new item begins. - public event EventHandler PreNewItem; + public event EventHandler? PreNewItem; /// Performs caller-implemented actions before the rename process for each item begins. - public event EventHandler PreRenameItem; + public event EventHandler? PreRenameItem; /// Performs caller-implemented actions before any specific file operations are performed. - public event EventHandler StartOperations; + public event EventHandler? StartOperations; /// Occurs when a progress update is received. - public event ProgressChangedEventHandler UpdateProgress; + public event ProgressChangedEventHandler? UpdateProgress; /// /// Gets a value that states whether any file operations initiated by a call to were stopped before they /// were complete. The operations could be stopped either by user action or silently by the system. /// /// if any file operations were aborted before they were complete; otherwise, . - public bool AnyOperationsAborted => op.GetAnyOperationsAborted(); + public bool AnyOperationsAborted => Operation.GetAnyOperationsAborted(); /// Gets or sets options that control file operations. public OperationFlags Options { get => opFlags; - set { if (value == opFlags) { return; } op.SetOperationFlags((FILEOP_FLAGS)(opFlags = value)); } + set { if (value == opFlags) { return; } Operation.SetOperationFlags((FILEOP_FLAGS)(opFlags = value)); } } /// Gets or sets the parent or owner window for progress and dialog windows. @@ -108,7 +109,7 @@ public OperationFlags Options public HWND OwnerWindow { get => owner; - set => op.SetOwnerWindow(owner = value); + set => Operation.SetOwnerWindow(owner = value); } /// Gets the number of queued operations. @@ -122,7 +123,7 @@ public HWND OwnerWindow /// of the destination item is the same as the source. /// /// Options that control file operations. - public static void Copy(string source, string dest, string newName = null, OperationFlags options = defaultOptions) + public static void Copy(string source, string dest, string? newName = null, OperationFlags options = defaultOptions) { using ShellItem shfile = new(source); using ShellFolder shfld = new(dest); @@ -137,7 +138,7 @@ public static void Copy(string source, string dest, string newName = null, Opera /// of the destination item is the same as the source. /// /// Options that control file operations. - public static void Copy(ShellItem source, ShellFolder dest, string newName = null, OperationFlags options = defaultOptions) + public static void Copy(ShellItem source, ShellFolder dest, string? newName = null, OperationFlags options = defaultOptions) { using ShellFileOperations2 sop = new(); sop.Options = options; @@ -154,7 +155,7 @@ public static void Copy(ShellItem source, ShellFolder dest, string newName = nul sop.PostCopyItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Copies a set of items to a specified destination using the Shell to provide progress and error dialogs. @@ -180,7 +181,7 @@ public static void Copy(IEnumerable sourceItems, ShellFolder dest, Op sop.PostCopyItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Deletes a single item using the Shell to provide progress and error dialogs. @@ -212,7 +213,7 @@ public static void Delete(ShellItem source, OperationFlags options = defaultOpti sop.PostDeleteItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Deletes a set of items using the Shell to provide progress and error dialogs. @@ -237,7 +238,7 @@ public static void Delete(IEnumerable sourceItems, OperationFlags opt sop.PostDeleteItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Moves a single item to a specified destination using the Shell to provide progress and error dialogs. @@ -248,7 +249,7 @@ public static void Delete(IEnumerable sourceItems, OperationFlags opt /// destination item is the same as the source. /// /// Options that control file operations. - public static void Move(string source, string dest, string newName = null, OperationFlags options = defaultOptions) + public static void Move(string source, string dest, string? newName = null, OperationFlags options = defaultOptions) { using ShellItem shfile = new(source); using ShellFolder shfld = new(dest); @@ -263,7 +264,7 @@ public static void Move(string source, string dest, string newName = null, Opera /// destination item is the same as the source. /// /// Options that control file operations. - public static void Move(ShellItem source, ShellFolder dest, string newName = null, OperationFlags options = defaultOptions) + public static void Move(ShellItem source, ShellFolder dest, string? newName = null, OperationFlags options = defaultOptions) { using ShellFileOperations2 sop = new() { Options = options }; HRESULT hr = HRESULT.S_OK; @@ -279,7 +280,7 @@ public static void Move(ShellItem source, ShellFolder dest, string newName = nul sop.PostMoveItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Moves a set of items to a specified destination using the Shell to provide progress and error dialogs. @@ -305,7 +306,7 @@ public static void Move(IEnumerable sourceItems, ShellFolder dest, Op sop.PostMoveItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Creates a new item in a specified location using the Shell to provide progress and error dialogs. @@ -332,7 +333,7 @@ public static void Move(IEnumerable sourceItems, ShellFolder dest, Op /// This parameter is normally to specify a new, blank file. /// /// Options that control file operations. - public static void NewItem(ShellFolder dest, string name, System.IO.FileAttributes attr = System.IO.FileAttributes.Normal, string template = null, OperationFlags options = defaultOptions) + public static void NewItem(ShellFolder dest, string name, System.IO.FileAttributes attr = System.IO.FileAttributes.Normal, string? template = null, OperationFlags options = defaultOptions) { using ShellFileOperations2 sop = new(); sop.Options = options; @@ -349,14 +350,14 @@ public static void NewItem(ShellFolder dest, string name, System.IO.FileAttribut sop.PostRenameItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Renames a single item to a new display name using the Shell to provide progress and error dialogs. /// A that specifies the source item. /// The new display name of the item. /// Options that control file operations. - public static void Rename(ShellItem source, string newName = null, OperationFlags options = defaultOptions) + public static void Rename(ShellItem source, string newName, OperationFlags options = defaultOptions) { using ShellFileOperations2 sop = new(); sop.Options = options; @@ -373,7 +374,7 @@ public static void Rename(ShellItem source, string newName = null, OperationFlag sop.PostRenameItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// @@ -402,7 +403,7 @@ public static void Rename(IEnumerable sourceItems, string newName, Op sop.PostRenameItem -= OnPost; } - void OnPost(object sender, ShellFileOpEventArgs e) => hr = e.Result; + void OnPost(object? sender, ShellFileOpEventArgs e) => hr = e.Result; } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -420,7 +421,7 @@ public void Dispose() /// public void PerformOperations() { - op.PerformOperations(); + Operation.PerformOperations(); QueuedOperations = 0; } @@ -431,8 +432,8 @@ public void PerformOperations() /// public void QueueApplyPropertiesOperation(ShellItem item, ShellItemPropertyUpdates props) { - op.SetProperties(props.IPropertyChangeArray); - op.ApplyPropertiesToItem(item.IShellItem); + Operation.SetProperties(props.IPropertyChangeArray); + Operation.ApplyPropertiesToItem(item.IShellItem); QueuedOperations++; } @@ -445,8 +446,8 @@ public void QueueApplyPropertiesOperation(ShellItem item, ShellItemPropertyUpdat /// public void QueueApplyPropertiesOperation(IEnumerable items, ShellItemPropertyUpdates props) { - op.SetProperties(props.IPropertyChangeArray); - op.ApplyPropertiesToItems(GetSHArray(items).IShellItemArray); + Operation.SetProperties(props.IPropertyChangeArray); + Operation.ApplyPropertiesToItems(GetRequiredShellItemArray(items)); QueuedOperations++; } @@ -457,9 +458,9 @@ public void QueueApplyPropertiesOperation(IEnumerable items, ShellIte /// An optional new name for the item after it has been copied. This can be . If , the name /// of the destination item is the same as the source. /// - public void QueueCopyOperation(ShellItem source, ShellFolder dest, string newName = null) + public void QueueCopyOperation(ShellItem source, ShellFolder dest, string? newName = null) { - op.CopyItem(source.IShellItem, dest.IShellItem, newName, null); + Operation.CopyItem(source.IShellItem, dest.IShellItem, newName, null); QueuedOperations++; } @@ -470,7 +471,7 @@ public void QueueCopyOperation(ShellItem source, ShellFolder dest, string newNam /// A that specifies the destination folder to contain the copy of the items. public void QueueCopyOperation(IEnumerable sourceItems, ShellFolder dest) { - op.CopyItems(GetSHArray(sourceItems).IShellItemArray, dest.IShellItem); + Operation.CopyItems(GetRequiredShellItemArray(sourceItems), dest.IShellItem); QueuedOperations++; } @@ -478,7 +479,7 @@ public void QueueCopyOperation(IEnumerable sourceItems, ShellFolder d /// >A that specifies the item to be deleted. public void QueueDeleteOperation(ShellItem item) { - op.DeleteItem(item.IShellItem, null); + Operation.DeleteItem(item.IShellItem, null); QueuedOperations++; } @@ -488,7 +489,7 @@ public void QueueDeleteOperation(ShellItem item) /// public void QueueDeleteOperation(IEnumerable items) { - op.DeleteItems(GetSHArray(items).IShellItemArray); + Operation.DeleteItems(GetRequiredShellItemArray(items)); QueuedOperations++; } @@ -499,9 +500,9 @@ public void QueueDeleteOperation(IEnumerable items) /// An optional new name for the item in its new location. This can be . If , the name of the /// destination item is the same as the source. /// - public void QueueMoveOperation(ShellItem source, ShellFolder dest, string newName = null) + public void QueueMoveOperation(ShellItem source, ShellFolder dest, string? newName = null) { - op.MoveItem(source.IShellItem, dest.IShellItem, newName, null); + Operation.MoveItem(source.IShellItem, dest.IShellItem, newName, null); QueuedOperations++; } @@ -512,7 +513,7 @@ public void QueueMoveOperation(ShellItem source, ShellFolder dest, string newNam /// A that specifies the destination folder to contain the moved items. public void QueueMoveOperation(IEnumerable sourceItems, ShellFolder dest) { - op.MoveItems(GetSHArray(sourceItems).IShellItemArray, dest.IShellItem); + Operation.MoveItems(GetRequiredShellItemArray(sourceItems), dest.IShellItem); QueuedOperations++; } @@ -539,9 +540,9 @@ public void QueueMoveOperation(IEnumerable sourceItems, ShellFolder d /// /// This parameter is normally to specify a new, blank file. /// - public void QueueNewItemOperation(ShellFolder dest, string name, System.IO.FileAttributes attr = System.IO.FileAttributes.Normal, string template = null) + public void QueueNewItemOperation(ShellFolder dest, string name, System.IO.FileAttributes attr = System.IO.FileAttributes.Normal, string? template = null) { - op.NewItem(dest.IShellItem, attr, name, template, null); + Operation.NewItem(dest.IShellItem, attr, name, template, null); QueuedOperations++; } @@ -550,7 +551,7 @@ public void QueueNewItemOperation(ShellFolder dest, string name, System.IO.FileA /// The new display name of the item. public void QueueRenameOperation(ShellItem source, string newName) { - op.RenameItem(source.IShellItem, newName, null); + Operation.RenameItem(source.IShellItem, newName, null); QueuedOperations++; } @@ -561,7 +562,7 @@ public void QueueRenameOperation(ShellItem source, string newName) /// The new display name of the items. public void QueueRenameOperation(IEnumerable sourceItems, string newName) { - op.RenameItems(GetSHArray(sourceItems).IShellItemArray, newName); + Operation.RenameItems(GetRequiredShellItemArray(sourceItems), newName); QueuedOperations++; } @@ -576,7 +577,7 @@ protected virtual void Dispose(bool disposing) // Dispose managed state (managed objects). } - if (sink != null && op != null) + if (op is not null) { op.Unadvise(sinkCookie); } @@ -585,7 +586,9 @@ protected virtual void Dispose(bool disposing) } } - private ShellItemArray GetSHArray(IEnumerable items) => items is ShellItemArray a ? a : new ShellItemArray(items); + private static IShellItemArray GetRequiredShellItemArray(IEnumerable items) => + (items is ShellItemArray array ? array : new ShellItemArray(items)).IShellItemArray ?? + throw new InvalidOperationException("Unable to create a shell item array."); private sealed class OpSink : IFileOperationProgressSink { @@ -651,7 +654,7 @@ private HRESULT CallChkErr(Action action) /// public sealed class ShellFileNewOpEventArgs : ShellFileOpEventArgs { - internal ShellFileNewOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem source, IShellItem folder, IShellItem dest, string name, HRESULT hr, string templ, uint attr) : + internal ShellFileNewOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem? source, IShellItem folder, IShellItem dest, string name, HRESULT hr, string? templ, uint attr) : base(flags, source, folder, dest, name, hr) { TemplateName = templ; @@ -660,7 +663,7 @@ internal ShellFileNewOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem source, /// Gets the name of the template. /// The name of the template. - public string TemplateName { get; private set; } + public string? TemplateName { get; private set; } /// Gets the file attributes. /// The file attributes. @@ -673,7 +676,7 @@ internal ShellFileNewOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem source, /// public class ShellFileOpEventArgs : EventArgs { - internal ShellFileOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem source, IShellItem folder = null, IShellItem dest = null, string name = null, HRESULT hr = default) + internal ShellFileOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem? source, IShellItem? folder = null, IShellItem? dest = null, string? name = null, HRESULT hr = default) { Flags = (TransferFlags)flags; if (source != null) try { SourceItem = ShellItem.Open(source); } catch { } @@ -685,11 +688,11 @@ internal ShellFileOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem source, IS /// Gets the destination folder. /// The destination folder. - public ShellItem DestFolder { get; protected set; } + public ShellItem? DestFolder { get; protected set; } /// Gets the destination item. /// The destination item. - public ShellItem DestItem { get; protected set; } + public ShellItem? DestItem { get; protected set; } /// Gets the tranfer flag values. /// The flags. @@ -697,7 +700,7 @@ internal ShellFileOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem source, IS /// Gets the name of the item. /// The item name. - public string Name { get; protected set; } + public string? Name { get; protected set; } /// Gets the result of the operation. /// The result. @@ -705,7 +708,7 @@ internal ShellFileOpEventArgs(TRANSFER_SOURCE_FLAGS flags, IShellItem source, IS /// Gets the source item. /// The source item. - public ShellItem SourceItem { get; protected set; } + public ShellItem? SourceItem { get; protected set; } /// Returns a that represents this instance. /// A that represents this instance. diff --git a/src/Files.App/Utils/Shell/ShellFolderExtensions.cs b/src/Files.App/Utils/Shell/ShellFolderExtensions.cs index d9c8dbdadbd8..c045fbfbb509 100644 --- a/src/Files.App/Utils/Shell/ShellFolderExtensions.cs +++ b/src/Files.App/Utils/Shell/ShellFolderExtensions.cs @@ -17,32 +17,32 @@ public static ShellLibraryItem GetShellLibraryItem(ShellLibraryEx library, strin var libraryItem = new ShellLibraryItem { FullPath = filePath, - AbsolutePath = library.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing), - RelativePath = library.GetDisplayName(ShellItemDisplayString.ParentRelativeParsing), - DisplayName = library.GetDisplayName(ShellItemDisplayString.NormalDisplay), + AbsolutePath = library.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing) ?? string.Empty, + RelativePath = library.GetDisplayName(ShellItemDisplayString.ParentRelativeParsing) ?? string.Empty, + DisplayName = library.GetDisplayName(ShellItemDisplayString.NormalDisplay) ?? string.Empty, 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(); + libraryItem.DefaultSaveFolder = SafetyExtensions.IgnoreExceptions(() => library.DefaultSaveFolder.FileSystemPath) ?? string.Empty; + libraryItem.Folders = folders.Select(f => f.FileSystemPath).OfType().ToArray(); } return libraryItem; } - private static T TryGetProperty(this ShellItemPropertyStore sip, Ole32.PROPERTYKEY key) + private static T? TryGetProperty(this ShellItemPropertyStore sip, Ole32.PROPERTYKEY key) { - T value = default; + T? value = default; SafetyExtensions.IgnoreExceptions(() => sip.TryGetValue(key, out value)); return value; } - public static ShellFileItem GetShellFileItem(ShellItem folderItem) + public static ShellFileItem? GetShellFileItem(ShellItem? folderItem) { if (folderItem is null) return null; @@ -107,13 +107,13 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem) 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; + 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(isFolder, parsingPath, fileName, filePath, recycleDate, modifiedDate, createdDate, fileSize, fileSizeBytes ?? 0, fileType, folderItem.PIDL.GetBytes()); } - public static ShellLinkItem GetShellLinkItem(ShellLink linkItem) + public static ShellLinkItem? GetShellLinkItem(ShellLink? linkItem) { if (linkItem is null) return null; @@ -135,7 +135,7 @@ public static ShellLinkItem GetShellLinkItem(ShellLink linkItem) return link; } - public static string GetParsingPath(this ShellItem item) + public static string? GetParsingPath(this ShellItem? item) { if (item is null) return null; diff --git a/src/Files.App/Utils/Shell/ShellLibraryEx.cs b/src/Files.App/Utils/Shell/ShellLibraryEx.cs index 0ef9ed8872f3..769fe29f2a0b 100644 --- a/src/Files.App/Utils/Shell/ShellLibraryEx.cs +++ b/src/Files.App/Utils/Shell/ShellLibraryEx.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Windows.Forms; +using System.Diagnostics.CodeAnalysis; using Vanara.Extensions; using Vanara.PInvoke; using Vanara.Windows.Shell; @@ -15,11 +16,14 @@ public sealed partial class ShellLibraryEx : ShellFolder { //private const string ext = ".library-ms"; - internal Shell32.IShellLibrary _lib; + internal Shell32.IShellLibrary? _lib; - private ShellLibraryFolders _folders; + private Shell32.IShellLibrary Library + => _lib ?? throw new ObjectDisposedException(nameof(ShellLibraryEx)); - private string _name; + private ShellLibraryFolders? _folders; + + private string _name = string.Empty; /// /// Initializes a new instance of the Ex class. @@ -29,7 +33,7 @@ public sealed partial class ShellLibraryEx : ShellFolder public ShellLibraryEx(Shell32.KNOWNFOLDERID knownFolderId, bool readOnly = false) { _lib = new Shell32.IShellLibrary(); - _lib.LoadLibraryFromKnownFolder(knownFolderId.Guid(), readOnly ? STGM.STGM_READ : STGM.STGM_READWRITE); + Library.LoadLibraryFromKnownFolder(knownFolderId.Guid(), readOnly ? STGM.STGM_READ : STGM.STGM_READWRITE); Init(knownFolderId.GetIShellItem()); } @@ -44,7 +48,7 @@ public ShellLibraryEx(string libraryName, Shell32.KNOWNFOLDERID kf = Shell32.KNO { _lib = new Shell32.IShellLibrary(); _name = libraryName; - var item = _lib.SaveInKnownFolder(kf.Guid(), libraryName, overwrite ? Shell32.LIBRARYSAVEFLAGS.LSF_OVERRIDEEXISTING : Shell32.LIBRARYSAVEFLAGS.LSF_FAILIFTHERE); + var item = Library.SaveInKnownFolder(kf.Guid(), libraryName, overwrite ? Shell32.LIBRARYSAVEFLAGS.LSF_OVERRIDEEXISTING : Shell32.LIBRARYSAVEFLAGS.LSF_FAILIFTHERE); Init(item); } @@ -59,7 +63,7 @@ public ShellLibraryEx(string libraryName, ShellFolder parent, bool overwrite = f { _lib = new Shell32.IShellLibrary(); _name = libraryName; - var item = _lib.Save(parent.IShellItem, libraryName, overwrite ? Shell32.LIBRARYSAVEFLAGS.LSF_OVERRIDEEXISTING : Shell32.LIBRARYSAVEFLAGS.LSF_FAILIFTHERE); + var item = Library.Save(parent.IShellItem, libraryName, overwrite ? Shell32.LIBRARYSAVEFLAGS.LSF_OVERRIDEEXISTING : Shell32.LIBRARYSAVEFLAGS.LSF_FAILIFTHERE); Init(item); } @@ -72,7 +76,7 @@ public ShellLibraryEx(string libraryName, ShellFolder parent, bool overwrite = f public ShellLibraryEx(Shell32.IShellItem libraryItem, bool readOnly = false) { _lib = new Shell32.IShellLibrary(); - _lib.LoadLibraryFromItem(libraryItem, readOnly ? STGM.STGM_READ : STGM.STGM_READWRITE); + Library.LoadLibraryFromItem(libraryItem, readOnly ? STGM.STGM_READ : STGM.STGM_READWRITE); Init(libraryItem); } @@ -83,8 +87,8 @@ public ShellLibraryEx(Shell32.IShellItem libraryItem, bool readOnly = false) /// The default save folder. public ShellItem DefaultSaveFolder { - get => Open(_lib.GetDefaultSaveFolder(Shell32.DEFAULTSAVEFOLDERTYPE.DSFT_DETECT)); - set => _lib.SetDefaultSaveFolder(Shell32.DEFAULTSAVEFOLDERTYPE.DSFT_DETECT, value.IShellItem); + get => Open(Library.GetDefaultSaveFolder(Shell32.DEFAULTSAVEFOLDERTYPE.DSFT_DETECT)); + set => Library.SetDefaultSaveFolder(Shell32.DEFAULTSAVEFOLDERTYPE.DSFT_DETECT, value.IShellItem); } /// Gets the set of child folders that are contained in the library. @@ -104,19 +108,20 @@ public IconLocation IconLocation { get { - _ = IconLocation.TryParse(_lib.GetIcon(), out var l); + _ = IconLocation.TryParse(Library.GetIcon(), out var l); return l; } - set => _lib.SetIcon(value.ToString()); + set => Library.SetIcon(value.ToString()); } /// /// Gets the name relative to the parent for the item. /// + [AllowNull] public override string Name { get => _name; - protected set => _name = value; + protected set => _name = value ?? string.Empty; } /// @@ -127,8 +132,8 @@ public override string Name /// public bool PinnedToNavigationPane { - get => _lib.GetOptions().IsFlagSet(Shell32.LIBRARYOPTIONFLAGS.LOF_PINNEDTONAVPANE); - set => _lib.SetOptions(Shell32.LIBRARYOPTIONFLAGS.LOF_PINNEDTONAVPANE, value ? Shell32.LIBRARYOPTIONFLAGS.LOF_PINNEDTONAVPANE : 0); + get => Library.GetOptions().IsFlagSet(Shell32.LIBRARYOPTIONFLAGS.LOF_PINNEDTONAVPANE); + set => Library.SetOptions(Shell32.LIBRARYOPTIONFLAGS.LOF_PINNEDTONAVPANE, value ? Shell32.LIBRARYOPTIONFLAGS.LOF_PINNEDTONAVPANE : 0); } /// @@ -155,8 +160,8 @@ public LibraryViewTemplate ViewTemplate /// public Guid ViewTemplateId { - get => _lib.GetFolderType(); - set => _lib.SetFolderType(value); + get => Library.GetFolderType(); + set => Library.SetFolderType(value); } /// @@ -172,7 +177,7 @@ public void Reload() /// public void Commit() { - _lib.Commit(); + Library.Commit(); } /// @@ -182,7 +187,10 @@ public void Commit() /// A containing the child folders. public ShellLibraryFolders GetFilteredFolders(LibraryFolderFilter filter = LibraryFolderFilter.AllItems) { - return new(_lib, _lib.GetFolders((Shell32.LIBRARYFOLDERFILTER)filter)); + var library = Library; + var folders = library.GetFolders((Shell32.LIBRARYFOLDERFILTER)filter) + ?? throw new InvalidOperationException("The shell library did not return its folders."); + return new(library, folders); } /// @@ -196,7 +204,7 @@ public ShellLibraryFolders GetFilteredFolders(LibraryFolderFilter filter = Libra /// The resulting target location. public ShellItem ResolveFolder(ShellItem item, TimeSpan timeout) { - return Open(_lib.ResolveFolder(item.IShellItem, Convert.ToUInt32(timeout.TotalMilliseconds))); + return Open(Library.ResolveFolder(item.IShellItem, Convert.ToUInt32(timeout.TotalMilliseconds))); } /// @@ -215,7 +223,7 @@ public ShellItem ResolveFolder(ShellItem item, TimeSpan timeout) /// /// If set to true do not display a warning dialog to the user in collisions that concern network locations that cannot be indexed. /// - public void ShowLibraryManagementDialog(IWin32Window parentWindow = null, string title = null, string instruction = null, bool allowUnindexableLocations = false) + public void ShowLibraryManagementDialog(IWin32Window? parentWindow = null, string? title = null, string? instruction = null, bool allowUnindexableLocations = false) { Shell32.SHShowManageLibraryUI( IShellItem, diff --git a/src/Files.App/Utils/Shell/ShellLibraryFolders.cs b/src/Files.App/Utils/Shell/ShellLibraryFolders.cs index ee33126d09c3..013e239d1c75 100644 --- a/src/Files.App/Utils/Shell/ShellLibraryFolders.cs +++ b/src/Files.App/Utils/Shell/ShellLibraryFolders.cs @@ -13,7 +13,10 @@ namespace Files.App.Utils.Shell /// public sealed partial class ShellLibraryFolders : ShellItemArray, ICollection { - private Shell32.IShellLibrary _lib; + private Shell32.IShellLibrary? _lib; + + private Shell32.IShellLibrary Library + => _lib ?? throw new ObjectDisposedException(nameof(ShellLibraryFolders)); /// /// Initializes a new instance of the class. @@ -40,7 +43,7 @@ public void Add(ShellItem location) { ArgumentNullException.ThrowIfNull(location); - _lib.AddFolder(location.IShellItem); + Library.AddFolder(location.IShellItem); } /// @@ -55,7 +58,7 @@ public bool Remove(ShellItem location) try { - _lib.RemoveFolder(location.IShellItem); + Library.RemoveFolder(location.IShellItem); return true; } diff --git a/src/Files.App/Utils/Shell/ShellNewMenuHelper.cs b/src/Files.App/Utils/Shell/ShellNewMenuHelper.cs index 0e49e2332c3f..f67e9c97c7e2 100644 --- a/src/Files.App/Utils/Shell/ShellNewMenuHelper.cs +++ b/src/Files.App/Utils/Shell/ShellNewMenuHelper.cs @@ -38,7 +38,7 @@ public static async Task> GetNewContextMenuEntries() return [.. newMenuItems.OrderBy(item => item.Name)]; } - public static async Task GetNewContextMenuEntryForType(string extension) + public static async Task GetNewContextMenuEntryForType(string extension) { if (string.IsNullOrEmpty(extension)) return null; @@ -48,7 +48,7 @@ public static async Task GetNewContextMenuEntryForType(string ext return key is not null ? await GetShellNewRegistryEntries(key, key) : null; } - private static async Task GetShellNewRegistryEntries(RegistryKey current, RegistryKey root) + private static async Task GetShellNewRegistryEntries(RegistryKey current, RegistryKey root) { try { @@ -77,7 +77,7 @@ private static async Task GetShellNewRegistryEntries(RegistryKey return null; } - private static Task ParseShellNewRegistryEntry(RegistryKey key, RegistryKey root) + private static async Task ParseShellNewRegistryEntry(RegistryKey key, RegistryKey root) { var valueNames = key.GetValueNames(); @@ -88,14 +88,14 @@ private static Task ParseShellNewRegistryEntry(RegistryKey key, R !valueNames.Contains("ItemName", StringComparer.OrdinalIgnoreCase) && !valueNames.Contains("Data", StringComparer.OrdinalIgnoreCase)) { - return Task.FromResult(null); + return null; } var extension = root.Name.Substring(root.Name.LastIndexOf('\\') + 1); - var fileName = (string)key.GetValue("FileName"); - var command = (string)key.GetValue("Command"); + var fileName = key.GetValue("FileName") as string; + var command = key.GetValue("Command") as string; - byte[] data = null; + byte[]? data = null; var dataObj = key.GetValue("Data"); if (dataObj is not null) @@ -108,7 +108,7 @@ private static Task ParseShellNewRegistryEntry(RegistryKey key, R }; } - return CreateShellNewEntry(extension, fileName, command, data); + return await CreateShellNewEntry(extension, fileName, command, data); } private static async Task CreateShellNewEntry(string extension, string? fileName, string? command, byte[]? data) @@ -119,7 +119,7 @@ private static async Task CreateShellNewEntry(string extension, s var displayType = sampleFile is not null ? sampleFile.DisplayType : $"file {extension}"; var thumbnail = sampleFile is not null ? await SafetyExtensions.IgnoreExceptions(() => sampleFile.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode.ListView, 24, Windows.Storage.FileProperties.ThumbnailOptions.UseCurrentScale).AsTask()) : null; - string iconString = null; + string? iconString = null; if (thumbnail is not null) { @@ -142,7 +142,7 @@ private static async Task CreateShellNewEntry(string extension, s return entry; } - private static RegistryKey OpenSubKeySafe(this RegistryKey root, string keyName) + private static RegistryKey? OpenSubKeySafe(this RegistryKey root, string keyName) { try { diff --git a/src/Files.App/Utils/Shell/ThreadWithMessageQueue.cs b/src/Files.App/Utils/Shell/ThreadWithMessageQueue.cs index 755fb4c3229d..a392cc6cf56f 100644 --- a/src/Files.App/Utils/Shell/ThreadWithMessageQueue.cs +++ b/src/Files.App/Utils/Shell/ThreadWithMessageQueue.cs @@ -7,7 +7,7 @@ namespace Files.App.Utils.Shell { public sealed partial class ThreadWithMessageQueue : Disposable { - private readonly BlockingCollection messageQueue; + private readonly BlockingCollection messageQueue; private readonly Thread thread; @@ -21,33 +21,30 @@ protected override void Dispose(bool disposing) } } - public async Task PostMethod(Func payload) + public Task PostMethod(Func payload) { - var message = new Internal(payload); + var message = new Internal(payload); messageQueue.TryAdd(message); - return (V)await message.tcs.Task; + return message.Task; } public Task PostMethod(Action payload) { - var message = new Internal(payload); + var message = new ActionInternal(payload); messageQueue.TryAdd(message); - return message.tcs.Task; + return message.Task; } public ThreadWithMessageQueue() { - messageQueue = new BlockingCollection(new ConcurrentQueue()); + messageQueue = new BlockingCollection(new ConcurrentQueue()); thread = new Thread(new ThreadStart(() => { foreach (var message in messageQueue.GetConsumingEnumerable()) - { - var res = message.payload(); - message.tcs.SetResult(res); - } + message.Invoke(); })); thread.SetApartmentState(ApartmentState.STA); @@ -58,22 +55,47 @@ public ThreadWithMessageQueue() thread.Start(); } - private sealed class Internal + private interface IInternal { - public Func payload; + void Invoke(); + } + + private sealed class Internal : IInternal + { + private readonly Func payload; + private readonly TaskCompletionSource taskCompletionSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + public Task Task => taskCompletionSource.Task; - public TaskCompletionSource tcs; + public Internal(Func payload) + { + this.payload = payload; + } - public Internal(Action payload) + public void Invoke() { - this.payload = () => { payload(); return default; }; - tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + taskCompletionSource.SetResult(payload()); } + } + + private sealed class ActionInternal : IInternal + { + private readonly Action payload; + private readonly TaskCompletionSource taskCompletionSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + public Task Task => taskCompletionSource.Task; - public Internal(Func payload) + public ActionInternal(Action payload) { this.payload = payload; - tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public void Invoke() + { + payload(); + taskCompletionSource.SetResult(); } } } diff --git a/src/Files.App/Utils/StatusCenter/StatusCenterHelper.cs b/src/Files.App/Utils/StatusCenter/StatusCenterHelper.cs index 8605bdd12415..85f2c063a5e1 100644 --- a/src/Files.App/Utils/StatusCenter/StatusCenterHelper.cs +++ b/src/Files.App/Utils/StatusCenter/StatusCenterHelper.cs @@ -645,7 +645,7 @@ public static void UpdateCardStrings(StatusCenterItem card, StatusCenterItemProg { case FileOperationType.Copy: { - string headerResource = card.HeaderStringResource; + string? headerResource = card.HeaderStringResource; if (card.IsDiscovering && card.TotalItemsCount > 0 && card.IsInProgress) headerResource = "StatusCenter_CopyDiscovery_Header"; @@ -664,7 +664,7 @@ public static void UpdateCardStrings(StatusCenterItem card, StatusCenterItemProg } case FileOperationType.Move: { - string headerResource = card.HeaderStringResource; + string? headerResource = card.HeaderStringResource; if (card.IsDiscovering && card.TotalItemsCount > 0 && card.IsInProgress) headerResource = "StatusCenter_CopyDiscovery_Header"; @@ -683,7 +683,7 @@ public static void UpdateCardStrings(StatusCenterItem card, StatusCenterItemProg } case FileOperationType.Delete: { - string headerResource = card.HeaderStringResource; + string? headerResource = card.HeaderStringResource; if (card.IsDiscovering && card.TotalItemsCount > 0 && card.IsInProgress) headerResource = "StatusCenter_CopyDiscovery_Header"; diff --git a/src/Files.App/Utils/Storage/Collection/BlockingListEnumerator.cs b/src/Files.App/Utils/Storage/Collection/BlockingListEnumerator.cs index 3c85449fc585..e780a576b56e 100644 --- a/src/Files.App/Utils/Storage/Collection/BlockingListEnumerator.cs +++ b/src/Files.App/Utils/Storage/Collection/BlockingListEnumerator.cs @@ -9,8 +9,6 @@ public sealed class BlockingListEnumerator : IEnumerator private readonly object m_Lock; - private T m_Current; - private int m_Pos; public T Current @@ -18,11 +16,16 @@ public T Current get { lock (m_Lock) - return m_Current; + { + if (m_Pos < 0 || m_Pos >= m_Inner.Count) + throw new InvalidOperationException(); + + return m_Inner[m_Pos]; + } } } - object IEnumerator.Current + object? IEnumerator.Current => Current; public BlockingListEnumerator(IList inner, object @lock) @@ -42,10 +45,6 @@ public bool MoveNext() { m_Pos++; var hasNext = m_Pos < m_Inner.Count; - if (hasNext) - { - m_Current = m_Inner[m_Pos]; - } return hasNext; } } diff --git a/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs b/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs index dbf52a68687c..43f832198864 100644 --- a/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs +++ b/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs @@ -81,9 +81,9 @@ public T this[int index] public event PropertyChangedEventHandler? PropertyChanged; - private Func? itemGroupKeySelector; + private Func? itemGroupKeySelector; - public Func? ItemGroupKeySelector + public Func? ItemGroupKeySelector { get => itemGroupKeySelector; set diff --git a/src/Files.App/Utils/Storage/Collection/GroupedCollection.cs b/src/Files.App/Utils/Storage/Collection/GroupedCollection.cs index 9466d24ced31..322eadd6b2b2 100644 --- a/src/Files.App/Utils/Storage/Collection/GroupedCollection.cs +++ b/src/Files.App/Utils/Storage/Collection/GroupedCollection.cs @@ -5,7 +5,7 @@ namespace Files.App.Utils.Storage { public sealed class GroupedCollection : BulkConcurrentObservableCollection, IGroupedCollectionHeader { - public GroupedHeaderViewModel Model { get; set; } + public GroupedHeaderViewModel Model { get; set; } = new(); public GroupedCollection(IEnumerable items) : base(items) { @@ -37,7 +37,7 @@ private void AddEvents() PropertyChanged += GroupedCollection_PropertyChanged; } - private void GroupedCollection_PropertyChanged(object sender, PropertyChangedEventArgs e) + private void GroupedCollection_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Count)) { diff --git a/src/Files.App/Utils/Storage/Collection/GroupedHeaderViewModel.cs b/src/Files.App/Utils/Storage/Collection/GroupedHeaderViewModel.cs index dff592522bef..c88b4f7a9654 100644 --- a/src/Files.App/Utils/Storage/Collection/GroupedHeaderViewModel.cs +++ b/src/Files.App/Utils/Storage/Collection/GroupedHeaderViewModel.cs @@ -8,19 +8,19 @@ namespace Files.App.Utils.Storage { public sealed partial class GroupedHeaderViewModel : ObservableObject { - public string Key { get; set; } + public string Key { get; set; } = string.Empty; public bool Initialized { get; set; } public int SortIndexOverride { get; set; } - private string text; + private string text = string.Empty; public string Text { - get => text ?? ""; // Text is bound to AutomationProperties.Name and can't be null + get => text; // Text is bound to AutomationProperties.Name and can't be null set => SetPropertyWithUpdateDelay(ref text, value); } - private string subtext; + private string subtext = string.Empty; public string Subtext { @@ -28,7 +28,7 @@ public string Subtext set => SetPropertyWithUpdateDelay(ref subtext, value); } - private string countText; + private string countText = string.Empty; public string CountText { @@ -44,15 +44,15 @@ public bool ShowCountTextBelow set => SetProperty(ref showCountTextBelow, value); } - private ImageSource imageSource; + private ImageSource? imageSource; - public ImageSource ImageSource + public ImageSource? ImageSource { get => imageSource; set => SetPropertyWithUpdateDelay(ref imageSource, value); } - private string icon; + private string icon = string.Empty; public string Icon { @@ -60,7 +60,7 @@ public string Icon set => SetPropertyWithUpdateDelay(ref icon, value); } - private void SetPropertyWithUpdateDelay(ref T field, T newVal, [CallerMemberName] string propName = null) + private void SetPropertyWithUpdateDelay(ref T field, T newVal, [CallerMemberName] string? propName = null) { if (propName is null) { diff --git a/src/Files.App/Utils/Storage/Collection/GroupingHelper.cs b/src/Files.App/Utils/Storage/Collection/GroupingHelper.cs index ef91971c8cf6..225c96b90f13 100644 --- a/src/Files.App/Utils/Storage/Collection/GroupingHelper.cs +++ b/src/Files.App/Utils/Storage/Collection/GroupingHelper.cs @@ -7,9 +7,9 @@ namespace Files.App.Utils.Storage { public static class GroupingHelper { - private static readonly IDateTimeFormatter dateTimeFormatter = Ioc.Default.GetService(); + private static readonly IDateTimeFormatter dateTimeFormatter = Ioc.Default.GetRequiredService(); - public static Func GetItemGroupKeySelector(GroupOption option, GroupByDateUnit unit) + public static Func? GetItemGroupKeySelector(GroupOption option, GroupByDateUnit unit) { return option switch { @@ -23,11 +23,12 @@ public static Func GetItemGroupKeySelector(GroupOption optio GroupOption.OriginalFolder => x => (x as RecycleBinItem)?.ItemOriginalFolder, GroupOption.DateDeleted => x => dateTimeFormatter.ToTimeSpanLabel((x as RecycleBinItem)?.ItemDateDeletedReal ?? DateTimeOffset.Now, unit).Text, GroupOption.FolderPath => x => PathNormalization.GetParentDir(x.ItemPath.TrimPath()), - _ => null, + GroupOption.None => null, + _ => throw new ArgumentOutOfRangeException(nameof(option)), }; } - public static (Action>, Action>) GetGroupInfoSelector(GroupOption option, GroupByDateUnit unit) + public static (Action>?, Action>?) GetGroupInfoSelector(GroupOption option, GroupByDateUnit unit) { return option switch { @@ -85,12 +86,12 @@ public static (Action>, Action (x => { - ListedItem first = x.FirstOrDefault(); + ListedItem first = x.First(); x.Model.ShowCountTextBelow = true; x.Model.Text = first.FileTagsUI?.FirstOrDefault()?.Name ?? Strings.Untagged.GetLocalizedResource(); //x.Model.Icon = first.FileTagsUI?.FirstOrDefault()?.Color; @@ -99,9 +100,9 @@ public static (Action>, Action (x => { var vals = dateTimeFormatter.ToTimeSpanLabel((x.First() as RecycleBinItem)?.ItemDateDeletedReal ?? DateTimeOffset.Now, unit); - x.Model.Subtext = vals?.Text; - x.Model.Icon = vals?.Glyph; - x.Model.SortIndexOverride = vals?.Index ?? 0; + x.Model.Subtext = vals.Text; + x.Model.Icon = vals.Glyph; + x.Model.SortIndexOverride = vals.Index; }, null), GroupOption.OriginalFolder => (x => @@ -110,8 +111,8 @@ public static (Action>, Action (x => @@ -168,8 +169,6 @@ private static readonly (long size, string text, string sizeText)[] sizeGroups = private static string GetFolderName(string path) { - if (path == null) - return null; return path.Substring(path.LastIndexOf('\\') + 1); } } diff --git a/src/Files.App/Utils/Storage/Collection/SortingHelper.cs b/src/Files.App/Utils/Storage/Collection/SortingHelper.cs index b816829c809d..bfd9be32648b 100644 --- a/src/Files.App/Utils/Storage/Collection/SortingHelper.cs +++ b/src/Files.App/Utils/Storage/Collection/SortingHelper.cs @@ -10,7 +10,7 @@ public static class SortingHelper private static object OrderByNameFunc(ListedItem item) => item.Name; - public static Func GetSortFunc(SortOption directorySortOption) + public static Func GetSortFunc(SortOption directorySortOption) { return directorySortOption switch { diff --git a/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs index d369d0adc375..265a6df7ff89 100644 --- a/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs +++ b/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs @@ -12,7 +12,7 @@ namespace Files.App.Utils.Storage { public static class UniversalStorageEnumerator { - private static readonly ISizeProvider folderSizeProvider = Ioc.Default.GetService(); + private static readonly ISizeProvider folderSizeProvider = Ioc.Default.GetRequiredService(); private static readonly IIconCacheService iconCacheService = Ioc.Default.GetRequiredService(); @@ -22,7 +22,7 @@ public static async Task> ListEntries( CancellationToken cancellationToken, int countLimit, Func, Task> intermediateAction, - Dictionary defaultIconPairs = null) + Dictionary? defaultIconPairs = null) { var sampler = new IntervalSampler(500); var tempList = new List(); @@ -87,7 +87,10 @@ ex is FileNotFoundException || { if (item.IsOfType(StorageItemTypes.Folder)) { - var folder = await AddFolderAsync(item.AsBaseStorageFolder(), currentStorageFolder, cancellationToken); + if (item.AsBaseStorageFolder() is not { } storageFolder) + continue; + + var folder = await AddFolderAsync(storageFolder, currentStorageFolder, cancellationToken); if (folder is not null) { folder.PreloadedIconData = await iconCacheService.GetIconAsync(folder.ItemPath, null, true); @@ -113,7 +116,10 @@ ex is FileNotFoundException || } else { - var fileEntry = await AddFileAsync(item.AsBaseStorageFile(), currentStorageFolder, cancellationToken); + if (item.AsBaseStorageFile() is not { } storageFile) + continue; + + var fileEntry = await AddFileAsync(storageFile, currentStorageFolder, cancellationToken); if (fileEntry is not null) { fileEntry.PreloadedIconData = await iconCacheService.GetIconAsync(fileEntry.ItemPath, fileEntry.FileExtension, false); @@ -161,7 +167,7 @@ private static async Task> EnumerateFileByFile(BaseS for (var i = startFrom; i < startFrom + itemsToIterate; i++) { - IStorageItem item; + IStorageItem? item; try { var results = await rootFolder.GetItemsAsync(i, 1); @@ -193,9 +199,9 @@ ex is FileNotFoundException || return tempList; } - public static async Task AddFolderAsync( + public static async Task AddFolderAsync( BaseStorageFolder folder, - StorageFolderWithPath currentStorageFolder, + StorageFolderWithPath? currentStorageFolder, CancellationToken cancellationToken) { var basicProperties = await folder.GetBasicPropertiesAsync(); @@ -215,7 +221,7 @@ public static async Task AddFolderAsync( ItemDateCreatedReal = folder.DateCreated, ItemType = folder.DisplayType, ItemPath = folder.Path, - FileSize = null, + FileSize = string.Empty, FileSizeBytes = 0, TargetPath = linkFolder.TargetPath, Arguments = linkFolder.Arguments, @@ -237,7 +243,7 @@ public static async Task AddFolderAsync( Opacity = 1, FileImage = null, LoadFileIcon = false, - ItemPath = string.IsNullOrEmpty(folder.Path) ? PathNormalization.Combine(currentStorageFolder.Path, folder.Name) : folder.Path, + ItemPath = string.IsNullOrEmpty(folder.Path) ? PathNormalization.Combine(currentStorageFolder?.Path ?? string.Empty, folder.Name) : folder.Path, FileSize = basicProperties.Size.ToSizeString(), FileSizeBytes = (long)basicProperties.Size, ItemDateDeletedReal = binFolder.DateDeleted, @@ -257,8 +263,8 @@ public static async Task AddFolderAsync( Opacity = 1, FileImage = null, LoadFileIcon = false, - ItemPath = string.IsNullOrEmpty(folder.Path) ? PathNormalization.Combine(currentStorageFolder.Path, folder.Name) : folder.Path, - FileSize = null, + ItemPath = string.IsNullOrEmpty(folder.Path) ? PathNormalization.Combine(currentStorageFolder?.Path ?? string.Empty, folder.Name) : folder.Path, + FileSize = string.Empty, FileSizeBytes = 0 }; } @@ -267,9 +273,9 @@ public static async Task AddFolderAsync( return null; } - public static async Task AddFileAsync( + public static async Task AddFileAsync( BaseStorageFile file, - StorageFolderWithPath currentStorageFolder, + StorageFolderWithPath? currentStorageFolder, CancellationToken cancellationToken) { var basicProperties = await file.GetBasicPropertiesAsync(); @@ -277,7 +283,7 @@ public static async Task AddFileAsync( var itemName = file.Name; var itemModifiedDate = basicProperties.DateModified; var itemCreatedDate = file.DateCreated; - var itemPath = string.IsNullOrEmpty(file.Path) ? PathNormalization.Combine(currentStorageFolder.Path, file.Name) : file.Path; + var itemPath = string.IsNullOrEmpty(file.Path) ? PathNormalization.Combine(currentStorageFolder?.Path ?? string.Empty, file.Name) : file.Path; var itemSize = basicProperties.Size.ToSizeString(); var itemSizeBytes = basicProperties.Size; var itemType = file.DisplayType; @@ -288,7 +294,7 @@ public static async Task AddFileAsync( return null; // TODO: is this needed to be handled here? - if (App.LibraryManager.TryGetLibrary(file.Path, out LibraryLocationItem library)) + if (App.LibraryManager.TryGetLibrary(file.Path, out var library)) { return new LibraryItem(library) { diff --git a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs index 1f02afcfd36a..db3169e2bc5e 100644 --- a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs +++ b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs @@ -11,7 +11,7 @@ namespace Files.App.Utils.Storage { public static class Win32StorageEnumerator { - private static readonly ISizeProvider folderSizeProvider = Ioc.Default.GetService(); + private static readonly ISizeProvider folderSizeProvider = Ioc.Default.GetRequiredService(); private static readonly IStorageCacheService fileListCache = Ioc.Default.GetRequiredService(); private static readonly string folderTypeTextLocalized = Strings.Folder.GetLocalizedResource(); @@ -120,11 +120,11 @@ private static IEnumerable EnumAdsForPath(string itemPath, ListedIte public static ListedItem GetAlternateStream((string Name, long Size) ads, ListedItem main) { string itemType = Strings.File.GetLocalizedResource(); - string itemFileExtension = null; + var itemFileExtension = string.Empty; if (ads.Name.Contains('.')) { - itemFileExtension = Path.GetExtension(ads.Name); + itemFileExtension = Path.GetExtension(ads.Name) ?? string.Empty; itemType = itemFileExtension.Trim('.') + " " + itemType; } @@ -149,7 +149,7 @@ public static ListedItem GetAlternateStream((string Name, long Size) ads, Listed }; } - public static async Task GetFolder( + public static async Task GetFolder( Win32PInvoke.WIN32_FIND_DATA findData, string pathRoot, bool isGitRepo, @@ -202,7 +202,7 @@ CancellationToken cancellationToken Opacity = opacity, LoadFileIcon = false, ItemPath = itemPath, - FileSize = null, + FileSize = string.Empty, FileSizeBytes = 0, }; } @@ -220,13 +220,13 @@ CancellationToken cancellationToken Opacity = opacity, LoadFileIcon = false, ItemPath = itemPath, - FileSize = null, + FileSize = string.Empty, FileSizeBytes = 0, }; } } - public static async Task GetFile( + public static async Task GetFile( Win32PInvoke.WIN32_FIND_DATA findData, string pathRoot, bool isGitRepo, @@ -258,11 +258,11 @@ CancellationToken cancellationToken long itemSizeBytes = findData.GetSize(); var itemSize = itemSizeBytes.ToSizeString(); string itemType = Strings.File.GetLocalizedResource(); - string itemFileExtension = null; + var itemFileExtension = string.Empty; if (findData.cFileName.Contains('.')) { - itemFileExtension = Path.GetExtension(itemPath); + itemFileExtension = Path.GetExtension(itemPath) ?? string.Empty; itemType = itemFileExtension.Trim('.') + " " + itemType; } @@ -300,7 +300,7 @@ CancellationToken cancellationToken ItemPath = itemPath, FileSize = itemSize, FileSizeBytes = itemSizeBytes, - TargetPath = targetPath, + TargetPath = targetPath ?? string.Empty, IsSymLink = true, }; } @@ -322,7 +322,7 @@ CancellationToken cancellationToken ItemPath = itemPath, FileSize = itemSize, FileSizeBytes = itemSizeBytes, - TargetPath = targetPath, + TargetPath = targetPath ?? string.Empty, IsSymLink = true }; } @@ -388,7 +388,7 @@ CancellationToken cancellationToken }; } } - else if (App.LibraryManager.TryGetLibrary(itemPath, out LibraryLocationItem library)) + else if (App.LibraryManager.TryGetLibrary(itemPath, out var library)) { return new LibraryItem(library) { diff --git a/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs b/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs index 654e15cf5c87..f84377be2c3c 100644 --- a/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs @@ -38,7 +38,7 @@ public static async Task CheckEmptyDrive(string? drivePath) return true; } - public static async Task GetRootFromPathAsync(string devicePath) + public static async Task GetRootFromPathAsync(string devicePath) { if (!SystemIO.Path.IsPathRooted(devicePath)) return null; @@ -46,10 +46,13 @@ public static async Task GetRootFromPathAsync(string devi var drivesViewModel = Ioc.Default.GetRequiredService(); var rootPath = SystemIO.Path.GetPathRoot(devicePath); + if (rootPath is null) + return null; + if (devicePath.StartsWith(@"\\?\", StringComparison.Ordinal)) // USB device { // Check among already discovered drives - StorageFolder matchingDrive = drivesViewModel.Drives.Cast().FirstOrDefault(x => + StorageFolder? matchingDrive = drivesViewModel.Drives.Cast().FirstOrDefault(x => Helpers.PathNormalization.NormalizePath(x.Path) == Helpers.PathNormalization.NormalizePath(rootPath))?.Root; if (matchingDrive is null) { @@ -175,8 +178,8 @@ public static unsafe string GetExtendedDriveLabel(SystemIO.DriveInfo drive) }) ?? ""; } - public static async Task GetThumbnailAsync(StorageFolder folder) - => (StorageItemThumbnail)await FilesystemTasks.Wrap(() + public static async Task GetThumbnailAsync(StorageFolder folder) + => await FilesystemTasks.Wrap(() => folder.GetThumbnailAsync(ThumbnailMode.SingleItem, 40, ThumbnailOptions.UseCurrentScale).AsTask() ); } diff --git a/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs b/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs index 72f04e297c70..99a93ee52076 100644 --- a/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs +++ b/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs @@ -38,7 +38,7 @@ public static class FileThumbnailHelper } } - var resolvedPath = path is not null && path.StartsWith(@"\\?\", StringComparison.Ordinal) + var resolvedPath = path.StartsWith(@"\\?\", StringComparison.Ordinal) ? MtpHelpers.ResolveMtpShellPath(path) ?? path : path; @@ -61,4 +61,4 @@ public static class FileThumbnailHelper return result; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs b/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs index ccb51f9209c8..c6a0eb1dfdc8 100644 --- a/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs +++ b/src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs @@ -14,20 +14,20 @@ public class FilesystemResult public static implicit operator FileSystemStatusCode(FilesystemResult res) => res.ErrorCode; public static implicit operator FilesystemResult(FileSystemStatusCode res) => new(res); - public static implicit operator bool(FilesystemResult res) => res?.ErrorCode is FileSystemStatusCode.Success; + public static implicit operator bool(FilesystemResult? res) => res?.ErrorCode is FileSystemStatusCode.Success; public static explicit operator FilesystemResult(bool res) => new(res ? FileSystemStatusCode.Success : FileSystemStatusCode.Generic); - public static implicit operator BOOL(FilesystemResult res) => res?.ErrorCode is FileSystemStatusCode.Success; + public static implicit operator BOOL(FilesystemResult? res) => res?.ErrorCode is FileSystemStatusCode.Success; public static explicit operator FilesystemResult(BOOL res) => new(res ? FileSystemStatusCode.Success : FileSystemStatusCode.Generic); } public sealed class FilesystemResult : FilesystemResult { - public T Result { get; } + public T? Result { get; } - public FilesystemResult(T result, FileSystemStatusCode errorCode) : base(errorCode) => Result = result; + public FilesystemResult(T? result, FileSystemStatusCode errorCode) : base(errorCode) => Result = result; - public static implicit operator T(FilesystemResult res) => res.Result; + public static implicit operator T?(FilesystemResult res) => res.Result; } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/Helpers/FilesystemTasks.cs b/src/Files.App/Utils/Storage/Helpers/FilesystemTasks.cs index 924845641754..af6898a94291 100644 --- a/src/Files.App/Utils/Storage/Helpers/FilesystemTasks.cs +++ b/src/Files.App/Utils/Storage/Helpers/FilesystemTasks.cs @@ -48,36 +48,49 @@ public async static Task> Wrap(Func> wrapped) } } + public async static Task> WrapNullable(Func> wrapped) + where T : class + { + try + { + return new FilesystemResult(await wrapped(), FileSystemStatusCode.Success); + } + catch (Exception ex) + { + return new FilesystemResult(default, GetErrorCode(ex, typeof(T))); + } + } + public async static Task OnSuccess(this Task> wrapped, Action func) { var res = await wrapped; - if (res) + if (res && res.Result is { } result) { - func(res.Result); + func(result); } return res; } public async static Task OnSuccess(this Task> wrapped, Func func) { var res = await wrapped; - if (res) + if (res && res.Result is { } result) { - return await Wrap(() => func(res.Result)); + return await Wrap(() => func(result)); } return res; } public async static Task> OnSuccess(this Task> wrapped, Func> func) { var res = await wrapped; - if (res) + if (res && res.Result is { } result) { - return await Wrap(() => func(res.Result)); + return await Wrap(() => func(result)); } return new FilesystemResult(default, res.ErrorCode); } - private static FileSystemStatusCode ToStatusCode(Type T) - => T == typeof(StorageFolderWithPath) || typeof(IStorageFolder).IsAssignableFrom(T) + private static FileSystemStatusCode ToStatusCode(Type? T) + => T == typeof(StorageFolderWithPath) || T is not null && typeof(IStorageFolder).IsAssignableFrom(T) ? FileSystemStatusCode.NotAFolder : FileSystemStatusCode.NotAFile; } diff --git a/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs b/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs index 9d7b30a095e1..017436d3d7aa 100644 --- a/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs @@ -21,7 +21,7 @@ public static bool CheckFolderAccessWithWin32(string path) return false; } - public static async Task CheckBitlockerStatusAsync(BaseStorageFolder rootFolder, string path) + public static async Task CheckBitlockerStatusAsync(BaseStorageFolder? rootFolder, string path) { if (rootFolder?.Properties is null) { diff --git a/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs b/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs index 19f080e987a9..f0372790e292 100644 --- a/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs +++ b/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs @@ -11,7 +11,7 @@ public interface IStorageItemWithPath public string Path { get; } - public IStorageItem Item { get; } + public IStorageItem? Item { get; } public FilesystemItemType ItemType { get; } } diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs index efe31e6c18d7..c804b18204ed 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs @@ -17,12 +17,12 @@ public static class StorageFileExtensions public static readonly ImmutableHashSet _ftpPaths = new HashSet() { "ftp:/", "ftps:/", "ftpes:/" }.ToImmutableHashSet(); - public static BaseStorageFile? AsBaseStorageFile(this IStorageItem item) + public static BaseStorageFile? AsBaseStorageFile(this IStorageItem? item) { if (item is null || !item.IsOfType(StorageItemTypes.File)) return null; - return item is StorageFile file ? (BaseStorageFile)file : item as BaseStorageFile; + return item is StorageFile file ? new SystemStorageFile(file) : item as BaseStorageFile; } public static async Task> ToStandardStorageItemsAsync(this IEnumerable items) @@ -37,11 +37,13 @@ public static async Task> ToStandardStorageItemsAsync(this IE } else if (item.IsOfType(StorageItemTypes.File)) { - newItems.Add(await item.AsBaseStorageFile().ToStorageFileAsync()); + if (item.AsBaseStorageFile() is { } file) + newItems.Add(await file.ToStorageFileAsync()); } else if (item.IsOfType(StorageItemTypes.Folder)) { - newItems.Add(await item.AsBaseStorageFolder().ToStorageFolderAsync()); + if (item.AsBaseStorageFolder() is { } folder) + newItems.Add(await folder.ToStorageFolderAsync()); } } catch (NotSupportedException) @@ -57,7 +59,7 @@ public static bool AreItemsInSameDrive(this IEnumerable itemsPath, strin try { var destinationRoot = Path.GetPathRoot(destinationPath); - return itemsPath.Any(itemPath => Path.GetPathRoot(itemPath).Equals(destinationRoot, StringComparison.OrdinalIgnoreCase)); + return itemsPath.Any(itemPath => string.Equals(Path.GetPathRoot(itemPath), destinationRoot, StringComparison.OrdinalIgnoreCase)); } catch { @@ -74,7 +76,7 @@ public static bool AreItemsAlreadyInFolder(this IEnumerable itemsPath, s try { var trimmedPath = destinationPath.TrimPath(); - return itemsPath.All(itemPath => Path.GetDirectoryName(itemPath).Equals(trimmedPath, StringComparison.OrdinalIgnoreCase)); + return itemsPath.All(itemPath => string.Equals(Path.GetDirectoryName(itemPath), trimmedPath, StringComparison.OrdinalIgnoreCase)); } catch { @@ -86,10 +88,10 @@ public static bool AreItemsAlreadyInFolder(this IEnumerable storag public static bool AreItemsAlreadyInFolder(this IEnumerable storageItems, string destinationPath) => storageItems.Select(x => x.Path).AreItemsAlreadyInFolder(destinationPath); - public static BaseStorageFolder? AsBaseStorageFolder(this IStorageItem item) + public static BaseStorageFolder? AsBaseStorageFolder(this IStorageItem? item) { if (item is not null && item.IsOfType(StorageItemTypes.Folder)) - return item is StorageFolder folder ? (BaseStorageFolder)folder : item as BaseStorageFolder; + return item is StorageFolder folder ? new SystemStorageFolder(folder) : item as BaseStorageFolder; return null; } @@ -144,9 +146,9 @@ public static async Task> GetDirectoryPathComponentsWithDispla item.Title = Strings.ReleaseNotes.GetLocalizedResource(); else if (item.Path == "Settings") item.Title = Strings.Settings.GetLocalizedResource(); - else + else if (item.Path is { } path) { - BaseStorageFolder folder = await FilesystemTasks.Wrap(() => DangerousGetFolderFromPathAsync(item.Path)); + BaseStorageFolder? folder = await FilesystemTasks.WrapNullable(() => DangerousGetFolderFromPathAsync(path)); if (!string.IsNullOrEmpty(folder?.DisplayName)) item.Title = folder.DisplayName; @@ -164,40 +166,40 @@ public static string GetResolvedPath(string path, bool isFtp) return ResolvePath(withoutEnvirnment, isFtp); } - public async static Task DangerousGetFileFromPathAsync - (string value, StorageFolderWithPath rootFolder = null, StorageFolderWithPath parentFolder = null) + public async static Task DangerousGetFileFromPathAsync + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) => (await DangerousGetFileWithPathFromPathAsync(value, rootFolder, parentFolder)).Item; public async static Task DangerousGetFileWithPathFromPathAsync - (string value, StorageFolderWithPath rootFolder = null, StorageFolderWithPath parentFolder = null) + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) { - if (rootFolder is not null) + if (rootFolder?.Item is { } rootItem) { var currComponents = GetDirectoryPathComponents(value); - if (parentFolder is not null && value.IsSubPathOf(parentFolder.Path)) + if (parentFolder?.Item is { } parentItem && value.IsSubPathOf(parentFolder.Path)) { - var folder = parentFolder.Item; + var folder = parentItem; var prevComponents = GetDirectoryPathComponents(parentFolder.Path); var path = parentFolder.Path; foreach (var component in currComponents.ExceptBy(prevComponents, c => c.Path).SkipLast(1)) { - folder = await folder.GetFolderAsync(component.Title); + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); path = PathNormalization.Combine(path, folder.Name); } - var file = await folder.GetFileAsync(currComponents.Last().Title); + var file = await folder.GetFileAsync(currComponents.Last().Title ?? throw new InvalidOperationException("A path component is missing its title.")); path = PathNormalization.Combine(path, file.Name); return new StorageFileWithPath(file, path); } else if (value.IsSubPathOf(rootFolder.Path)) { - var folder = rootFolder.Item; + var folder = rootItem; var path = rootFolder.Path; foreach (var component in currComponents.Skip(1).SkipLast(1)) { - folder = await folder.GetFolderAsync(component.Title); + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); path = PathNormalization.Combine(path, folder.Name); } - var file = await folder.GetFileAsync(currComponents.Last().Title); + var file = await folder.GetFileAsync(currComponents.Last().Title ?? throw new InvalidOperationException("A path component is missing its title.")); path = PathNormalization.Combine(path, file.Name); return new StorageFileWithPath(file, path); } @@ -215,18 +217,20 @@ public async static Task DangerousGetFileWithPathFromPathAs } public async static Task> GetFilesWithPathAsync (this StorageFolderWithPath parentFolder, uint maxNumberOfItems = uint.MaxValue) - => (await parentFolder.Item.GetFilesAsync(CommonFileQuery.DefaultQuery, 0, maxNumberOfItems)) - .Select(x => new StorageFileWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); + => parentFolder.Item is { } folder + ? (await folder.GetFilesAsync(CommonFileQuery.DefaultQuery, 0, maxNumberOfItems)) + .Select(x => new StorageFileWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList() + : []; - public async static Task DangerousGetFolderFromPathAsync - (string value, StorageFolderWithPath rootFolder = null, StorageFolderWithPath parentFolder = null) + public async static Task DangerousGetFolderFromPathAsync + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) => (await DangerousGetFolderWithPathFromPathAsync(value, rootFolder, parentFolder)).Item; public async static Task DangerousGetFolderWithPathFromPathAsync - (string value, StorageFolderWithPath rootFolder = null, StorageFolderWithPath parentFolder = null) + (string value, StorageFolderWithPath? rootFolder = null, StorageFolderWithPath? parentFolder = null) { // Archive paths can't be resolved by chaining WinRT GetFolderAsync from a network root/parent // (an archive is not a real subfolder of the share); resolve them directly like local archives. - if (rootFolder is not null && !ZipStorageFolder.IsZipPath(value)) + if (rootFolder?.Item is { } rootItem && !ZipStorageFolder.IsZipPath(value)) { var currComponents = GetDirectoryPathComponents(value); @@ -234,25 +238,25 @@ public async static Task DangerousGetFolderWithPathFromPa { return rootFolder; } - else if (parentFolder is not null && value.IsSubPathOf(parentFolder.Path)) + else if (parentFolder?.Item is { } parentItem && value.IsSubPathOf(parentFolder.Path)) { - var folder = parentFolder.Item; + var folder = parentItem; var prevComponents = GetDirectoryPathComponents(parentFolder.Path); var path = parentFolder.Path; foreach (var component in currComponents.ExceptBy(prevComponents, c => c.Path)) { - folder = await folder.GetFolderAsync(component.Title); + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); path = PathNormalization.Combine(path, folder.Name); } return new StorageFolderWithPath(folder, path); } else if (value.IsSubPathOf(rootFolder.Path)) { - var folder = rootFolder.Item; + var folder = rootItem; var path = rootFolder.Path; foreach (var component in currComponents.Skip(1)) { - folder = await folder.GetFolderAsync(component.Title); + folder = await folder.GetFolderAsync(component.Title ?? throw new InvalidOperationException("A path component is missing its title.")); path = PathNormalization.Combine(path, folder.Name); } return new StorageFolderWithPath(folder, path); @@ -271,19 +275,21 @@ public async static Task DangerousGetFolderWithPathFromPa } public async static Task> GetFoldersWithPathAsync (this StorageFolderWithPath parentFolder, uint maxNumberOfItems = uint.MaxValue) - => (await parentFolder.Item.GetFoldersAsync(CommonFolderQuery.DefaultQuery, 0, maxNumberOfItems)) - .Select(x => new StorageFolderWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); + => parentFolder.Item is { } folder + ? (await folder.GetFoldersAsync(CommonFolderQuery.DefaultQuery, 0, maxNumberOfItems)) + .Select(x => new StorageFolderWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList() + : []; public async static Task> GetFoldersWithPathAsync (this StorageFolderWithPath parentFolder, string nameFilter, uint maxNumberOfItems = uint.MaxValue) { - if (parentFolder is null) - return null; + if (parentFolder?.Item is not { } folder) + return []; var queryOptions = new QueryOptions { ApplicationSearchFilter = $"System.FileName:{nameFilter}*" }; - BaseStorageFolderQueryResult queryResult = parentFolder.Item.CreateFolderQueryWithOptions(queryOptions); + BaseStorageFolderQueryResult queryResult = folder.CreateFolderQueryWithOptions(queryOptions); return (await queryResult.GetFoldersAsync(0, maxNumberOfItems)) .Select(x => new StorageFolderWithPath(x, string.IsNullOrEmpty(x.Path) ? PathNormalization.Combine(parentFolder.Path, x.Name) : x.Path)).ToList(); @@ -431,7 +437,7 @@ private static void SetCurrentWorkingDirectory(StringBuilder path, char separato var subPath = path.ToString().Substring(substringIndex); path.Clear(); - path.Append(context.ShellPage?.ShellViewModel.WorkingDirectory); + path.Append(context.ShellPage?.ShellViewModel?.WorkingDirectory); path.Append(separator); path.Append(subPath); i = -1; diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs index 9fb6dccfbffe..ad793836243b 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs @@ -9,16 +9,16 @@ namespace Files.App.Utils public sealed class StorageFileWithPath : IStorageItemWithPath { public string Path { get; } - public string Name => Item?.Name ?? IO.Path.GetFileName(Path); + public string Name => Item?.Name ?? IO.Path.GetFileName(Path) ?? string.Empty; - IStorageItem IStorageItemWithPath.Item => Item; - public BaseStorageFile Item { get; } + IStorageItem? IStorageItemWithPath.Item => Item; + public BaseStorageFile? Item { get; } public FilesystemItemType ItemType => FilesystemItemType.File; public StorageFileWithPath(BaseStorageFile file) : this(file, file.Path) { } - public StorageFileWithPath(BaseStorageFile file, string path) - => (Item, Path) = (file, path); + public StorageFileWithPath(BaseStorageFile? file, string? path) + => (Item, Path) = (file, path ?? file?.Path ?? string.Empty); } } diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs b/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs index d9d5eaff4cfd..39777d19f0c5 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs @@ -9,16 +9,16 @@ namespace Files.App.Utils public sealed class StorageFolderWithPath : IStorageItemWithPath { public string Path { get; } - public string Name => Item?.Name ?? IO.Path.GetFileName(Path); + public string Name => Item?.Name ?? IO.Path.GetFileName(Path) ?? string.Empty; - IStorageItem IStorageItemWithPath.Item => Item; - public BaseStorageFolder Item { get; } + IStorageItem? IStorageItemWithPath.Item => Item; + public BaseStorageFolder? Item { get; } public FilesystemItemType ItemType => FilesystemItemType.Directory; public StorageFolderWithPath(BaseStorageFolder folder) : this(folder, folder.Path) { } - public StorageFolderWithPath(BaseStorageFolder folder, string path) - => (Item, Path) = (folder, path); + public StorageFolderWithPath(BaseStorageFolder? folder, string? path) + => (Item, Path) = (folder, path ?? folder?.Path ?? string.Empty); } } diff --git a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs index 5af8817424d7..04d0592e00bb 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs @@ -13,15 +13,15 @@ namespace Files.App.Helpers /// public static class StorageHelpers { - public static async Task ToStorageItem(this IStorageItemWithPath item) + public static async Task ToStorageItem(this IStorageItemWithPath item) { return (await item.ToStorageItemResult()).Result; } - public static async Task ToStorageItem(string path) where TRequested : IStorageItem + public static async Task ToStorageItem(string path) where TRequested : IStorageItem { - FilesystemResult file = null; - FilesystemResult folder = null; + FilesystemResult? file = null; + FilesystemResult? folder = null; if (FileExtensionHelpers.IsShortcutOrUrlFile(path)) { @@ -80,11 +80,11 @@ public static async Task ToStorageItem(string path) wher await GetFileAsync(); } - if (!file || file.Result is null) // Possibly a folder + if (file?.Result is null) // Possibly a folder { await GetFolderAsync(); - if (file is null && (!folder || folder.Result is null)) + if (file is null && folder?.Result is null) { // Try file because it wasn't checked await GetFileAsync(); @@ -93,13 +93,13 @@ public static async Task ToStorageItem(string path) wher } } - if (file is not null && file) + if (file?.Result is TRequested requestedFile) { - return (TRequested)(IStorageItem)file.Result; + return requestedFile; } - else if (folder is not null && folder) + else if (folder?.Result is TRequested requestedFolder) { - return (TRequested)(IStorageItem)folder.Result; + return requestedFolder; } return default; @@ -108,14 +108,14 @@ public static async Task ToStorageItem(string path) wher async Task GetFileAsync() { - var rootItem = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(path)); - file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path, rootItem)); + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(path)); + file = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path, rootItem.Result)); } async Task GetFolderAsync() { - var rootItem = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(path)); - folder = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path, rootItem)); + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(path)); + folder = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path, rootItem.Result)); } } @@ -128,14 +128,14 @@ public static async Task GetFileSize(this IStorageFile file) public static async Task> ToStorageItemResult(this IStorageItemWithPath item) { var returnedItem = new FilesystemResult(null, FileSystemStatusCode.Generic); - var rootItem = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(item.Path)); + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(item.Path)); if (!string.IsNullOrEmpty(item.Path)) { returnedItem = (item.ItemType == FilesystemItemType.File) ? ToType( - await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.Path, rootItem))) : + await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.Path, rootItem))) : ToType( - await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(item.Path, rootItem))); + await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(item.Path, rootItem))); } if (returnedItem.Result is null && item.Item is not null) returnedItem = new FilesystemResult(item.Item, FileSystemStatusCode.Success); @@ -144,7 +144,7 @@ await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathA return returnedItem; } - public static IStorageItemWithPath FromPathAndType(string customPath, FilesystemItemType? itemType) + public static IStorageItemWithPath FromPathAndType(string? customPath, FilesystemItemType? itemType) { return (itemType == FilesystemItemType.File) ? new StorageFileWithPath(null, customPath) : @@ -153,7 +153,7 @@ public static IStorageItemWithPath FromPathAndType(string customPath, Filesystem public static async Task GetTypeFromPath(string path) { - IStorageItem item = await ToStorageItem(path); + IStorageItem? item = await ToStorageItem(path); return item is null ? FilesystemItemType.File : (item.IsOfType(StorageItemTypes.Folder) ? FilesystemItemType.Directory : FilesystemItemType.File); } @@ -163,7 +163,7 @@ public static bool Exists(string path) return Win32PInvoke.GetFileAttributesExFromApp(path, Win32PInvoke.GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, out _); } - public static IStorageItemWithPath FromStorageItem(this IStorageItem item, string customPath = null, FilesystemItemType? itemType = null) + public static IStorageItemWithPath FromStorageItem(this IStorageItem? item, string? customPath = null, FilesystemItemType? itemType = null) { if (item is null) { @@ -177,7 +177,7 @@ public static IStorageItemWithPath FromStorageItem(this IStorageItem item, strin { return new StorageFolderWithPath(item.AsBaseStorageFolder(), string.IsNullOrEmpty(item.Path) ? customPath : item.Path); } - return null; + throw new ArgumentException("The storage item must represent a file or folder.", nameof(item)); } public static FilesystemResult ToType(FilesystemResult result) where T : class @@ -185,4 +185,4 @@ public static FilesystemResult ToType(FilesystemResult result) where return new FilesystemResult(result.Result as T, result.ErrorCode); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/Helpers/StorageItemIconHelpers.cs b/src/Files.App/Utils/Storage/Helpers/StorageItemIconHelpers.cs index ff6dbefba6ec..bc490024ceca 100644 --- a/src/Files.App/Utils/Storage/Helpers/StorageItemIconHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/StorageItemIconHelpers.cs @@ -15,7 +15,7 @@ public static class StorageItemIconHelpers /// Optionally choose not to persist icon-backing item in LocalCache /// The file type (extension) of the generic icon to retrieve. Leave empty if a directory icon is desired /// - public static async Task GetIconForItemType(uint requestedSize, IconPersistenceOptions persistenceOptions = IconPersistenceOptions.Persist, string fileExtension = null) + public static async Task GetIconForItemType(uint requestedSize, IconPersistenceOptions persistenceOptions = IconPersistenceOptions.Persist, string? fileExtension = null) { if (string.IsNullOrEmpty(fileExtension)) { diff --git a/src/Files.App/Utils/Storage/History/IStorageHistory.cs b/src/Files.App/Utils/Storage/History/IStorageHistory.cs index e1247978f08a..fa3a90781980 100644 --- a/src/Files.App/Utils/Storage/History/IStorageHistory.cs +++ b/src/Files.App/Utils/Storage/History/IStorageHistory.cs @@ -28,10 +28,10 @@ public interface IStorageHistory ///
/// May contain more that one item /// - IList Destination { get; } + IList? Destination { get; } void Modify(IStorageHistory newHistory); - void Modify(FileOperationType operationType, IStorageItemWithPath source, IStorageItemWithPath destination); - void Modify(FileOperationType operationType, IList source, IList destination); + void Modify(FileOperationType operationType, IStorageItemWithPath source, IStorageItemWithPath? destination); + void Modify(FileOperationType operationType, IList source, IList? destination); } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/History/StorageHistory.cs b/src/Files.App/Utils/Storage/History/StorageHistory.cs index ad434d36db40..263c8a5a0940 100644 --- a/src/Files.App/Utils/Storage/History/StorageHistory.cs +++ b/src/Files.App/Utils/Storage/History/StorageHistory.cs @@ -9,14 +9,14 @@ public sealed class StorageHistory : IStorageHistory public IList Source { get; private set; } - public IList Destination { get; private set; } + public IList? Destination { get; private set; } - public StorageHistory(FileOperationType operationType, IStorageItemWithPath source, IStorageItemWithPath destination) - : this(operationType, source.CreateList(), destination.CreateList()) + public StorageHistory(FileOperationType operationType, IStorageItemWithPath source, IStorageItemWithPath? destination) + : this(operationType, source.CreateList(), destination?.CreateList()) { } - public StorageHistory(FileOperationType operationType, IList source, IList destination) + public StorageHistory(FileOperationType operationType, IList source, IList? destination) { OperationType = operationType; Source = source; @@ -25,9 +25,9 @@ public StorageHistory(FileOperationType operationType, IList (OperationType, Source, Destination) = (newHistory.OperationType, newHistory.Source, newHistory.Destination); - public void Modify(FileOperationType operationType, IStorageItemWithPath source, IStorageItemWithPath destination) - => (OperationType, Source, Destination) = (operationType, source.CreateList(), destination.CreateList()); - public void Modify(FileOperationType operationType, IList source, IList destination) + public void Modify(FileOperationType operationType, IStorageItemWithPath source, IStorageItemWithPath? destination) + => (OperationType, Source, Destination) = (operationType, source.CreateList(), destination?.CreateList()); + public void Modify(FileOperationType operationType, IList source, IList? destination) => (OperationType, Source, Destination) = (operationType, source, destination); } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/History/StorageHistoryHelpers.cs b/src/Files.App/Utils/Storage/History/StorageHistoryHelpers.cs index c798d35af4bc..cb3799b34fa0 100644 --- a/src/Files.App/Utils/Storage/History/StorageHistoryHelpers.cs +++ b/src/Files.App/Utils/Storage/History/StorageHistoryHelpers.cs @@ -5,7 +5,7 @@ namespace Files.App.Utils.Storage { public sealed partial class StorageHistoryHelpers : IDisposable { - private IStorageHistoryOperations operations; + private IStorageHistoryOperations? operations; private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); @@ -14,6 +14,8 @@ public StorageHistoryHelpers(IStorageHistoryOperations storageHistoryOperations) public async Task TryUndo() { + var operations = this.operations ?? throw new ObjectDisposedException(nameof(StorageHistoryHelpers)); + if (App.HistoryWrapper.CanUndo()) { if (!await semaphore.WaitAsync(0)) @@ -41,6 +43,8 @@ public async Task TryUndo() public async Task TryRedo() { + var operations = this.operations ?? throw new ObjectDisposedException(nameof(StorageHistoryHelpers)); + if (App.HistoryWrapper.CanRedo()) { if (!await semaphore.WaitAsync(0)) @@ -67,4 +71,4 @@ public void Dispose() operations = null; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/History/StorageHistoryOperations.cs b/src/Files.App/Utils/Storage/History/StorageHistoryOperations.cs index 76a07c913a95..bec80efb1108 100644 --- a/src/Files.App/Utils/Storage/History/StorageHistoryOperations.cs +++ b/src/Files.App/Utils/Storage/History/StorageHistoryOperations.cs @@ -2,14 +2,15 @@ // Licensed under the MIT License. using System.IO; +using System.Diagnostics.CodeAnalysis; using Windows.Storage; namespace Files.App.Utils.Storage { public sealed partial class StorageHistoryOperations : IStorageHistoryOperations { - private IFilesystemHelpers helpers; - private ShellFilesystemOperations operations; + private IFilesystemHelpers? helpers; + private ShellFilesystemOperations? operations; private readonly CancellationToken cancellationToken; @@ -22,6 +23,8 @@ public StorageHistoryOperations(IShellPage associatedInstance, CancellationToken public async Task Undo(IStorageHistory history) { + var helpers = this.helpers ?? throw new ObjectDisposedException(nameof(StorageHistoryOperations)); + var operations = this.operations ?? throw new ObjectDisposedException(nameof(StorageHistoryOperations)); ReturnResult returnStatus = ReturnResult.InProgress; Progress progress = new(); @@ -37,34 +40,38 @@ public async Task Undo(IStorageHistory history) } break; case FileOperationType.CreateLink: // Opposite: Delete created items - if (!IsHistoryNull(history.Destination)) + var createdLinks = history.Destination; + if (!IsHistoryNull(createdLinks)) { // Show a dialog regardless of the setting to prevent unexpected deletion - return await helpers.DeleteItemsAsync(history.Destination, DeleteConfirmationPolicies.Always, true, false); + return await helpers.DeleteItemsAsync(createdLinks, DeleteConfirmationPolicies.Always, true, false); } break; case FileOperationType.Rename: // Opposite: Restore original item names - if (!IsHistoryNull(history)) + var renamedItems = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(renamedItems)) { NameCollisionOption collision = NameCollisionOption.GenerateUniqueName; - for (int i = 0; i < history.Destination.Count; i++) + for (int i = 0; i < renamedItems.Count; i++) { string name = Path.GetFileName(history.Source[i].Path); - await operations.RenameAsync(history.Destination[i], name, collision, progress, cancellationToken); + await operations.RenameAsync(renamedItems[i], name, collision, progress, cancellationToken); } } break; case FileOperationType.Copy: // Opposite: Delete copied items - if (!IsHistoryNull(history.Destination)) + var copiedItems = history.Destination; + if (!IsHistoryNull(copiedItems)) { // Show a dialog regardless of the setting to prevent unexpected deletion - return await helpers.DeleteItemsAsync(history.Destination, DeleteConfirmationPolicies.Always, true, false); + return await helpers.DeleteItemsAsync(copiedItems, DeleteConfirmationPolicies.Always, true, false); } break; case FileOperationType.Move: // Opposite: Move the items to original directory - if (!IsHistoryNull(history)) + var movedItems = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(movedItems)) { - return await helpers.MoveItemsAsync(history.Destination, history.Source.Select(item => item.Path), false, false); + return await helpers.MoveItemsAsync(movedItems, history.Source.Select(item => item.Path), false, false); } break; case FileOperationType.Extract: // Opposite: No opposite for archive extraction @@ -72,9 +79,10 @@ public async Task Undo(IStorageHistory history) Debugger.Break(); break; case FileOperationType.Recycle: // Opposite: Restore recycled items - if (!IsHistoryNull(history)) + var recycledItems = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(recycledItems)) { - returnStatus = await helpers.RestoreItemsFromTrashAsync(history.Destination, history.Source.Select(item => item.Path), false); + returnStatus = await helpers.RestoreItemsFromTrashAsync(recycledItems, history.Source.Select(item => item.Path), false); if (returnStatus is ReturnResult.IntegrityCheckFailed) // Not found, corrupted { App.HistoryWrapper.RemoveHistory(history, false); @@ -82,9 +90,10 @@ public async Task Undo(IStorageHistory history) } break; case FileOperationType.Restore: // Opposite: Move restored items to Recycle Bin - if (!IsHistoryNull(history.Destination)) + var restoredItems = history.Destination; + if (!IsHistoryNull(restoredItems)) { - var newHistory = await operations.DeleteItemsAsync(history.Destination, progress, false, cancellationToken); + var newHistory = await operations.DeleteItemsAsync(restoredItems, progress, false, cancellationToken); if (newHistory is null) { App.HistoryWrapper.RemoveHistory(history, false); @@ -106,6 +115,8 @@ public async Task Undo(IStorageHistory history) public async Task Redo(IStorageHistory history) { + var helpers = this.helpers ?? throw new ObjectDisposedException(nameof(StorageHistoryOperations)); + var operations = this.operations ?? throw new ObjectDisposedException(nameof(StorageHistoryOperations)); ReturnResult returnStatus = ReturnResult.InProgress; Progress progress = new(); @@ -114,7 +125,7 @@ public async Task Redo(IStorageHistory history) switch (history.OperationType) { case FileOperationType.CreateNew: - if (IsHistoryNull(history)) + if (!IsHistoryNull(history.Source)) { foreach (var source in history.Source) { @@ -123,33 +134,37 @@ public async Task Redo(IStorageHistory history) } break; case FileOperationType.CreateLink: - if (!IsHistoryNull(history)) + var linkDestinations = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(linkDestinations)) { await operations.CreateShortcutItemsAsync(history.Source, - await history.Destination.Select(item => item.Path).ToListAsync(), progress, cancellationToken); + await linkDestinations.Select(item => item.Path).ToListAsync(), progress, cancellationToken); } break; case FileOperationType.Rename: - if (!IsHistoryNull(history)) + var renameDestinations = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(renameDestinations)) { NameCollisionOption collision = NameCollisionOption.GenerateUniqueName; for (int i = 0; i < history.Source.Count; i++) { - string name = Path.GetFileName(history.Destination[i].Path); + string name = Path.GetFileName(renameDestinations[i].Path); await operations.RenameAsync(history.Source[i], name, collision, progress, cancellationToken); } } break; case FileOperationType.Copy: - if (!IsHistoryNull(history)) + var copyDestinations = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(copyDestinations)) { - return await helpers.CopyItemsAsync(history.Source, history.Destination.Select(item => item.Path), false, false); + return await helpers.CopyItemsAsync(history.Source, copyDestinations.Select(item => item.Path), false, false); } break; case FileOperationType.Move: - if (!IsHistoryNull(history)) + var moveDestinations = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(moveDestinations)) { - return await helpers.MoveItemsAsync(history.Source, history.Destination.Select(item => item.Path), false, false); + return await helpers.MoveItemsAsync(history.Source, moveDestinations.Select(item => item.Path), false, false); } break; case FileOperationType.Extract: @@ -157,7 +172,8 @@ await operations.CreateShortcutItemsAsync(history.Source, Debugger.Break(); break; case FileOperationType.Recycle: // Recycle PASS - if (!IsHistoryNull(history.Destination)) + var recycleDestinations = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(recycleDestinations)) { var newHistory = await operations.DeleteItemsAsync(history.Source, progress, false, cancellationToken); if (newHistory is null) @@ -172,9 +188,10 @@ await operations.CreateShortcutItemsAsync(history.Source, } break; case FileOperationType.Restore: - if (!IsHistoryNull(history)) + var restoreDestinations = history.Destination; + if (!IsHistoryNull(history.Source) && !IsHistoryNull(restoreDestinations)) { - await helpers.RestoreItemsFromTrashAsync(history.Source, history.Destination.Select(item => item.Path), false); + await helpers.RestoreItemsFromTrashAsync(history.Source, restoreDestinations.Select(item => item.Path), false); } break; case FileOperationType.Delete: @@ -194,10 +211,9 @@ public void Dispose() operations = null; } - private bool IsHistoryNull(IStorageHistory history) // history.Destination is null with CreateNew - => IsHistoryNull(history.Source) || (history.Destination is not null && IsHistoryNull(history.Destination)); - private bool IsHistoryNull(IEnumerable source) => !source.All(HasPath); + private static bool IsHistoryNull([NotNullWhen(false)] IEnumerable? source) + => source is null || !source.All(HasPath); private static bool HasPath(IStorageItemWithPath item) => item is not null && !string.IsNullOrWhiteSpace(item.Path); } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/History/StorageHistoryWrapper.cs b/src/Files.App/Utils/Storage/History/StorageHistoryWrapper.cs index d9d8839aa5a6..51b3bcd456a8 100644 --- a/src/Files.App/Utils/Storage/History/StorageHistoryWrapper.cs +++ b/src/Files.App/Utils/Storage/History/StorageHistoryWrapper.cs @@ -45,7 +45,8 @@ public void ModifyCurrentHistory(IStorageHistory newHistory) public void Dispose() { - histories = null; + histories.Clear(); + index = -1; } } } diff --git a/src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs b/src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs index e578783c305e..b9509741bac0 100644 --- a/src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs +++ b/src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs @@ -36,7 +36,7 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera }, App.Logger); } - public static Task<(bool, ShellOperationResult)> CreateItemAsync(string filePath, string fileOp, long ownerHwnd, bool asAdmin, string template = "", byte[]? dataBytes = null) + public static Task<(bool, ShellOperationResult)> CreateItemAsync(string filePath, string fileOp, long ownerHwnd, bool asAdmin, string? template = null, byte[]? dataBytes = null) { return STATask.Run(async () => { @@ -54,10 +54,11 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera op.OwnerWindow = (IntPtr)ownerHwnd; var shellOperationResult = new ShellOperationResult(); + var parentPath = Path.GetDirectoryName(filePath); - if (!SafetyExtensions.IgnoreExceptions(() => + if (parentPath is null || !SafetyExtensions.IgnoreExceptions(() => { - using var shd = new ShellFolder(Path.GetDirectoryName(filePath)); + using var shd = new ShellFolder(parentPath); op.QueueNewItemOperation(shd, Path.GetFileName(filePath), fileOp == "CreateFolder" ? FileAttributes.Directory : FileAttributes.Normal, template); })) @@ -91,11 +92,12 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera createTcs.TrySetResult(false); } - if (dataBytes is not null && (shellOperationResult.Items.SingleOrDefault()?.Succeeded ?? false)) + if (dataBytes is not null && + shellOperationResult.Items.SingleOrDefault() is { Succeeded: true, Destination: { } destination }) { SafetyExtensions.IgnoreExceptions(() => { - using var fs = new FileStream(shellOperationResult.Items.Single().Destination, FileMode.Open); + using var fs = new FileStream(destination, FileMode.Open); fs.Write(dataBytes, 0, dataBytes.Length); fs.Flush(); }, App.Logger); @@ -193,7 +195,7 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera }, App.Logger); } - public static Task<(bool, ShellOperationResult)> DeleteItemAsync(string[] fileToDeletePath, bool permanently, long ownerHwnd, bool asAdmin, IProgress progress, string operationID = "") + public static Task<(bool, ShellOperationResult)> DeleteItemAsync(string[] fileToDeletePath, bool permanently, long ownerHwnd, bool asAdmin, IProgress? progress, string operationID = "") { operationID = string.IsNullOrEmpty(operationID) ? Guid.NewGuid().ToString() : operationID; @@ -277,25 +279,31 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera // Right before deleting item op.PreDeleteItem += (s, e) => { - sizeCalculator.ForceComputeFileSize(e.SourceItem.GetParsingPath()); - fsProgress.FileName = e.SourceItem.Name; + if (e.SourceItem is not { } sourceItem) + return; + + if (sourceItem.GetParsingPath() is { } sourcePath) + sizeCalculator.ForceComputeFileSize(sourcePath); + fsProgress.FileName = sourceItem.Name ?? string.Empty; fsProgress.Report(); }; // Right after deleted item op.PostDeleteItem += (s, e) => { - if (!e.SourceItem.IsFolder) + var sourceItem = e.SourceItem; + var sourcePath = sourceItem?.GetParsingPath(); + if (sourceItem is { IsFolder: false } && sourcePath is not null) { - if (sizeCalculator.TryGetComputedFileSize(e.SourceItem.GetParsingPath(), out _)) + if (sizeCalculator.TryGetComputedFileSize(sourcePath, out _)) fsProgress.AddProcessedItemsCount(1); } shellOperationResult.Items.Add(new ShellOperationItemResult() { Succeeded = e.Result.Succeeded, - Source = e.SourceItem.GetParsingPath(), - Destination = e.DestItem.GetParsingPath(), + Source = sourcePath, + Destination = e.DestItem?.GetParsingPath(), HResult = (int)e.Result }); @@ -373,11 +381,13 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera var renameTcs = new TaskCompletionSource(); op.PostRenameItem += (s, e) => { + var sourcePath = e.SourceItem.GetParsingPath(); + var sourceFolderPath = sourcePath is null ? null : Path.GetDirectoryName(sourcePath); shellOperationResult.Items.Add(new ShellOperationItemResult() { Succeeded = e.Result.Succeeded, - Source = e.SourceItem.GetParsingPath(), - Destination = !string.IsNullOrEmpty(e.Name) ? Path.Combine(Path.GetDirectoryName(e.SourceItem.GetParsingPath()), e.Name) : null, + Source = sourcePath, + Destination = sourceFolderPath is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(sourceFolderPath, e.Name) : null, HResult = (int)e.Result }); }; @@ -459,7 +469,9 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera if (!SafetyExtensions.IgnoreExceptions(() => { using ShellItem shi = new(fileToMovePath[i]); - using ShellFolder shd = new(Path.GetDirectoryName(moveDestination[i])); + var destinationFolderPath = Path.GetDirectoryName(moveDestination[i]) + ?? throw new ArgumentException("The move destination must include a parent folder.", nameof(moveDestination)); + using ShellFolder shd = new(destinationFolderPath); op.QueueMoveOperation(shi, shd, Path.GetFileName(moveDestination[i])); })) @@ -482,24 +494,31 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera op.PreMoveItem += (s, e) => { - sizeCalculator.ForceComputeFileSize(e.SourceItem.GetParsingPath()); - fsProgress.FileName = e.SourceItem.Name; + if (e.SourceItem is not { } sourceItem) + return; + + if (sourceItem.GetParsingPath() is { } sourcePath) + sizeCalculator.ForceComputeFileSize(sourcePath); + fsProgress.FileName = sourceItem.Name ?? string.Empty; fsProgress.Report(); }; op.PostMoveItem += (s, e) => { - if (!e.SourceItem.IsFolder) + var sourceItem = e.SourceItem; + var sourcePath = sourceItem?.GetParsingPath(); + if (sourceItem is { IsFolder: false } && sourcePath is not null) { - if (sizeCalculator.TryGetComputedFileSize(e.SourceItem.GetParsingPath(), out _)) + if (sizeCalculator.TryGetComputedFileSize(sourcePath, out _)) fsProgress.AddProcessedItemsCount(1); } + var destinationFolderPath = e.DestFolder?.GetParsingPath(); shellOperationResult.Items.Add(new ShellOperationItemResult() { Succeeded = e.Result.Succeeded, - Source = e.SourceItem.GetParsingPath(), - Destination = e.DestFolder.GetParsingPath() is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(e.DestFolder.GetParsingPath(), e.Name) : null, + Source = sourcePath, + Destination = destinationFolderPath is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(destinationFolderPath, e.Name) : null, HResult = (int)e.Result }); @@ -597,7 +616,9 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera if (!SafetyExtensions.IgnoreExceptions(() => { using ShellItem shi = new(fileToCopyPath[i]); - using ShellFolder shd = new(Path.GetDirectoryName(copyDestination[i])); + var destinationFolderPath = Path.GetDirectoryName(copyDestination[i]) + ?? throw new ArgumentException("The copy destination must include a parent folder.", nameof(copyDestination)); + using ShellFolder shd = new(destinationFolderPath); var fileName = GetIncrementalName(overwriteOnCopy, copyDestination[i], fileToCopyPath[i]); // Perform a copy operation @@ -622,24 +643,31 @@ public static Task SetClipboard(string[] filesToCopy, DataPackageOperation opera op.PreCopyItem += (s, e) => { - sizeCalculator.ForceComputeFileSize(e.SourceItem.GetParsingPath()); - fsProgress.FileName = e.SourceItem.Name; + if (e.SourceItem is not { } sourceItem) + return; + + if (sourceItem.GetParsingPath() is { } sourcePath) + sizeCalculator.ForceComputeFileSize(sourcePath); + fsProgress.FileName = sourceItem.Name ?? string.Empty; fsProgress.Report(); }; op.PostCopyItem += (s, e) => { - if (!e.SourceItem.IsFolder) + var sourceItem = e.SourceItem; + var sourcePath = sourceItem?.GetParsingPath(); + if (sourceItem is { IsFolder: false } && sourcePath is not null) { - if (sizeCalculator.TryGetComputedFileSize(e.SourceItem.GetParsingPath(), out _)) + if (sizeCalculator.TryGetComputedFileSize(sourcePath, out _)) fsProgress.AddProcessedItemsCount(1); } + var destinationFolderPath = e.DestFolder?.GetParsingPath(); shellOperationResult.Items.Add(new ShellOperationItemResult() { Succeeded = e.Result.Succeeded, - Source = e.SourceItem.GetParsingPath(), - Destination = e.DestFolder.GetParsingPath() is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(e.DestFolder.GetParsingPath(), e.Name) : null, + Source = sourcePath, + Destination = destinationFolderPath is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(destinationFolderPath, e.Name) : null, HResult = (int)e.Result }); @@ -722,9 +750,9 @@ public static void TryCancelOperation(string operationId) targetPath = await STATask.Run(() => { var ipf = new Url.IUniformResourceLocator(); - (ipf as System.Runtime.InteropServices.ComTypes.IPersistFile).Load(linkPath, 0); + ((System.Runtime.InteropServices.ComTypes.IPersistFile)ipf).Load(linkPath, 0); ipf.GetUrl(out var retVal); - return retVal; + return retVal ?? string.Empty; }, App.Logger); return string.IsNullOrEmpty(targetPath) ? new ShellLinkItem @@ -778,7 +806,7 @@ public static Task CreateOrUpdateLinkAsync(string linkSavePath, string tar { var ipf = new Url.IUniformResourceLocator(); ipf.SetUrl(targetPath, Url.IURL_SETURL_FLAGS.IURL_SETURL_FL_GUESS_PROTOCOL); - (ipf as System.Runtime.InteropServices.ComTypes.IPersistFile).Save(linkSavePath, false); // Overwrite if exists + ((System.Runtime.InteropServices.ComTypes.IPersistFile)ipf).Save(linkSavePath, false); // Overwrite if exists return true; }, App.Logger); } @@ -968,14 +996,14 @@ private static bool TrySetLnkShortcutIcon(string filePath, string iconFile, int private static void UpdateFileTagsDb(ShellFileOperations2.ShellFileOpEventArgs e, string operationType) { var dbInstance = FileTagsHelper.GetDbInstance(); - if (e.Result.Succeeded) + if (e.Result.Succeeded && e.SourceItem.GetParsingPath() is { } sourcePath) { - var sourcePath = e.SourceItem.GetParsingPath(); var destPath = e.DestFolder.GetParsingPath(); + var sourceFolderPath = Path.GetDirectoryName(sourcePath); var destination = operationType switch { "delete" => e.DestItem.GetParsingPath(), - "rename" => !string.IsNullOrEmpty(e.Name) ? Path.Combine(Path.GetDirectoryName(sourcePath), e.Name) : null, + "rename" => sourceFolderPath is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(sourceFolderPath, e.Name) : null, "copy" => destPath is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(destPath, e.Name) : null, _ => destPath is not null && !string.IsNullOrEmpty(e.Name) ? Path.Combine(destPath, e.Name) : null }; @@ -1122,11 +1150,8 @@ protected override void Dispose(bool disposing) } } - private static string GetIncrementalName(bool overWriteOnCopy, string? filePathToCheck, string? filePathToCopy) + private static string GetIncrementalName(bool overWriteOnCopy, string filePathToCheck, string filePathToCopy) { - if (filePathToCheck == null) - return null; - if ((!Path.Exists(filePathToCheck)) || overWriteOnCopy || filePathToCheck == filePathToCopy) return Path.GetFileName(filePathToCheck); diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs index e930860a5155..97efe0a2e92b 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs @@ -154,11 +154,11 @@ showDialog is DeleteConfirmationPolicies.PermanentOnly && var sw = new Stopwatch(); sw.Start(); - IStorageHistory history = await filesystemOperations.DeleteItemsAsync((IList)source, banner.ProgressEventSource, permanently, token); + IStorageHistory? history = await filesystemOperations.DeleteItemsAsync((IList)source, banner.ProgressEventSource, permanently, token); banner.Progress.ReportStatus(FileSystemStatusCode.Success); await Task.Yield(); - if (!permanently && registerHistory) + if (!permanently && registerHistory && history is not null) App.HistoryWrapper.AddHistory(history); // Execute removal tasks concurrently in background @@ -210,10 +210,10 @@ public async Task RestoreItemsFromTrashAsync(IEnumerable)source, (IList)destination, progress, cancellationToken); + IStorageHistory? history = await filesystemOperations.RestoreItemsFromTrashAsync((IList)source, (IList)destination, progress, cancellationToken); await Task.Yield(); - if (registerHistory && source.Any((item) => !string.IsNullOrWhiteSpace(item.Path))) + if (registerHistory && history is not null && source.Any((item) => !string.IsNullOrWhiteSpace(item.Path))) { App.HistoryWrapper.AddHistory(history); } @@ -313,21 +313,24 @@ public async Task CopyItemsAsync(IEnumerable itemManipulationModel?.ClearSelection(); - IStorageHistory history = await filesystemOperations.CopyItemsAsync((IList)source, (IList)destination, collisions, banner.ProgressEventSource, token); + IStorageHistory? history = await filesystemOperations.CopyItemsAsync((IList)source, (IList)destination, collisions, banner.ProgressEventSource, token); if (returnStatus == ReturnResult.InProgress || returnStatus == ReturnResult.Success) banner.Progress.ReportStatus(FileSystemStatusCode.Success); - if (registerHistory && history is not null && source.Any((item) => !string.IsNullOrWhiteSpace(item.Path))) + if (registerHistory && + history?.Destination is { } historyDestinations && + source.Any((item) => !string.IsNullOrWhiteSpace(item.Path))) { - foreach (var (histSrcItem, histDestItem) in history.Source.Zip(history.Destination)) + foreach (var (histSrcItem, histDestItem) in history.Source.Zip(historyDestinations)) { foreach (var conflictItem in itemsResult) { if (!string.IsNullOrEmpty(conflictItem.CustomName) && conflictItem.SourcePath == histSrcItem.Path && Path.GetFileName(conflictItem.SourcePath) != conflictItem.CustomName) { var renameHistory = await filesystemOperations.RenameAsync(histDestItem, conflictItem.CustomName, NameCollisionOption.FailIfExists, banner.ProgressEventSource, token); - history.Destination[history.Source.IndexOf(histSrcItem)] = renameHistory.Destination[0]; + if (renameHistory?.Destination is { Count: > 0 } renameDestinations) + historyDestinations[history.Source.IndexOf(histSrcItem)] = renameDestinations[0]; } } } @@ -391,6 +394,9 @@ public async Task CopyItemsFromClipboard(DataPackageView packageVi var imgSource = await packageView.GetBitmapAsync(); using var imageStream = await imgSource.OpenReadAsync(); var folder = await StorageFileExtensions.DangerousGetFolderFromPathAsync(destination); + if (folder is null) + return ReturnResult.Failed; + // Set the name of the file to be the current time and date var file = await folder.CreateFileAsync($"{DateTime.Now:MM-dd-yy-HHmmss}.png", CreationCollisionOption.GenerateUniqueName); @@ -452,23 +458,26 @@ public async Task MoveItemsAsync(IEnumerable itemManipulationModel?.ClearSelection(); - IStorageHistory history = await filesystemOperations.MoveItemsAsync((IList)source, (IList)destination, collisions, banner.ProgressEventSource, token); + IStorageHistory? history = await filesystemOperations.MoveItemsAsync((IList)source, (IList)destination, collisions, banner.ProgressEventSource, token); if (returnStatus == ReturnResult.InProgress || returnStatus == ReturnResult.Success) banner.Progress.ReportStatus(FileSystemStatusCode.Success); await Task.Yield(); - if (registerHistory && history is not null && source.Any((item) => !string.IsNullOrWhiteSpace(item.Path))) + if (registerHistory && + history?.Destination is { } historyDestinations && + source.Any((item) => !string.IsNullOrWhiteSpace(item.Path))) { - foreach (var (histSrcItem, histDestItem) in history.Source.Zip(history.Destination)) + foreach (var (histSrcItem, histDestItem) in history.Source.Zip(historyDestinations)) { foreach (var conflictItem in itemsResult) { if (!string.IsNullOrEmpty(conflictItem.CustomName) && conflictItem.SourcePath == histSrcItem.Path) { var renameHistory = await filesystemOperations.RenameAsync(histDestItem, conflictItem.CustomName, NameCollisionOption.FailIfExists, banner.ProgressEventSource, token); - history.Destination[history.Source.IndexOf(histSrcItem)] = renameHistory.Destination[0]; + if (renameHistory?.Destination is { Count: > 0 } renameDestinations) + historyDestinations[history.Source.IndexOf(histSrcItem)] = renameDestinations[0]; } } } @@ -653,7 +662,7 @@ public static bool IsValidForFilename(string name) foreach (var (src, dest) in source.Zip(destination)) { - string itemPathOrName = string.IsNullOrEmpty(src.Path) ? src.Item.Name : src.Path; + string itemPathOrName = string.IsNullOrEmpty(src.Path) ? src.Name : src.Path; var incomingItem = new FileSystemDialogConflictItemViewModel { ConflictResolveOption = FileNameConflictResolveOptionType.None, @@ -666,7 +675,7 @@ public static bool IsValidForFilename(string name) { // Something strange happened, log App.Logger.LogWarning($"Duplicate key when resolving conflicts: {itemPathOrName}, {src.Name}\n" + - $"Source: {string.Join(", ", source.Select(x => string.IsNullOrEmpty(x.Path) ? x.Item.Name : x.Path))}"); + $"Source: {string.Join(", ", source.Select(x => string.IsNullOrEmpty(x.Path) ? x.Name : x.Path))}"); } // Assume GenerateNewName when source and destination are the same @@ -713,7 +722,8 @@ await Ioc.Default.GetRequiredService().TryGetFileAsync(dest) collisions.Clear(); foreach (var item in itemsResult) { - collisions.TryAdd(item.SourcePath!, item.ConflictResolveOption); + if (!string.IsNullOrEmpty(item.SourcePath)) + collisions.TryAdd(item.SourcePath, item.ConflictResolveOption); } } @@ -722,7 +732,7 @@ await Ioc.Default.GetRequiredService().TryGetFileAsync(dest) foreach (var src in source) { - var itemPathOrName = string.IsNullOrEmpty(src.Path) ? src.Item.Name : src.Path; + var itemPathOrName = string.IsNullOrEmpty(src.Path) ? src.Name : src.Path; bool found = collisions.TryGetValue(itemPathOrName, out var match); var fileNameConflictResolveOptionType = found ? match : FileNameConflictResolveOptionType.Skip; newCollisions.Add(fileNameConflictResolveOptionType); @@ -842,7 +852,7 @@ public static async Task> GetDraggedStorageIte } } - itemsList = itemsList.DistinctBy(x => string.IsNullOrEmpty(x.Path) ? x.Item.Name : x.Path).ToList(); + itemsList = itemsList.DistinctBy(x => string.IsNullOrEmpty(x.Path) ? x.Name : x.Path).ToList(); return itemsList; } diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs index 080397e1b89a..8ab6a5b9bbd5 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemOperations.cs @@ -18,16 +18,18 @@ public sealed partial class FilesystemOperations : IFilesystemOperations { private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); - private IShellPage _associatedInstance; + private readonly IShellPage _associatedInstance; + private ShellViewModel ShellViewModel + => _associatedInstance.ShellViewModel ?? throw new InvalidOperationException("File operations require an initialized shell."); public FilesystemOperations(IShellPage associatedInstance) { _associatedInstance = associatedInstance; } - public async Task<(IStorageHistory, IStorageItem)> CreateAsync(IStorageItemWithPath source, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) + public async Task<(IStorageHistory?, IStorageItem?)> CreateAsync(IStorageItemWithPath source, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) { - IStorageItemWithPath item = null; + IStorageItemWithPath? item = null; FilesystemResult fsResult = (FilesystemResult)false; StatusCenterItemProgressModel fsProgress = new(progress, true, FileSystemStatusCode.InProgress, 1); fsProgress.Report(); @@ -41,13 +43,15 @@ public FilesystemOperations(IShellPage associatedInstance) var newEntryInfo = await ShellNewEntryExtensions.GetNewContextMenuEntryForType(Path.GetExtension(source.Path)); if (newEntryInfo is null) { - var fsFolderResult = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(source.Path)); + var fsFolderResult = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(source.Path)); fsResult = fsFolderResult; - if (fsResult) + var fileName = Path.GetFileName(source.Path); + if (fsResult && fsFolderResult.Result is { } folder && !string.IsNullOrEmpty(fileName)) { - var fsCreateResult = await FilesystemTasks.Wrap(() => fsFolderResult.Result.CreateFileAsync(Path.GetFileName(source.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); + var fsCreateResult = await FilesystemTasks.Wrap(() => folder.CreateFileAsync(fileName, CreationCollisionOption.GenerateUniqueName).AsTask()); fsResult = fsCreateResult; - item = fsCreateResult.Result.FromStorageItem(); + if (fsCreateResult.Result is { } createdFile) + item = createdFile.FromStorageItem(); } if (fsResult == FileSystemStatusCode.Unauthorized) { @@ -58,9 +62,9 @@ public FilesystemOperations(IShellPage associatedInstance) { var fsCreateResult = await newEntryInfo.Create(source.Path, _associatedInstance); fsResult = fsCreateResult; - if (fsResult) + if (fsResult && fsCreateResult.Result is { } createdFile) { - item = fsCreateResult.Result.FromStorageItem(); + item = createdFile.FromStorageItem(); } if (fsResult == FileSystemStatusCode.Unauthorized) { @@ -73,13 +77,15 @@ public FilesystemOperations(IShellPage associatedInstance) case FilesystemItemType.Directory: { - var fsFolderResult = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(source.Path)); + var fsFolderResult = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(source.Path)); fsResult = fsFolderResult; - if (fsResult) + var folderName = Path.GetFileName(source.Path); + if (fsResult && fsFolderResult.Result is { } folder && !string.IsNullOrEmpty(folderName)) { - var fsCreateResult = await FilesystemTasks.Wrap(() => fsFolderResult.Result.CreateFolderAsync(Path.GetFileName(source.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); + var fsCreateResult = await FilesystemTasks.Wrap(() => folder.CreateFolderAsync(folderName, CreationCollisionOption.GenerateUniqueName).AsTask()); fsResult = fsCreateResult; - item = fsCreateResult.Result.FromStorageItem(); + if (fsCreateResult.Result is { } createdFolder) + item = createdFolder.FromStorageItem(); } if (fsResult == FileSystemStatusCode.Unauthorized) { @@ -106,12 +112,12 @@ public FilesystemOperations(IShellPage associatedInstance) } } - public Task CopyAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task CopyAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress? progress, CancellationToken cancellationToken) { return CopyAsync(source.FromStorageItem(), destination, collision, progress, cancellationToken); } - public async Task CopyAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public async Task CopyAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress? progress, CancellationToken cancellationToken) { StatusCenterItemProgressModel fsProgress = new( progress, @@ -132,7 +138,7 @@ await DialogDisplayHelper.ShowDialogAsync( return null; } - IStorageItem copiedItem = null; + IStorageItem? copiedItem = null; if (source.ItemType == FilesystemItemType.Directory) { @@ -166,17 +172,19 @@ await DialogDisplayHelper.ShowDialogAsync( { // CopyFileFromApp only works on file not directories var fsSourceFolder = await source.ToStorageItemResult(); - var fsDestinationFolder = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); + var fsDestinationFolder = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); var fsResult = (FilesystemResult)(fsSourceFolder.ErrorCode | fsDestinationFolder.ErrorCode); - if (fsResult) + if (fsResult && + fsSourceFolder.Result is BaseStorageFolder sourceFolder && + fsDestinationFolder.Result is BaseStorageFolder destinationFolder) { - if (fsSourceFolder.Result is IPasswordProtectedItem ppis) + if (sourceFolder is IPasswordProtectedItem ppis) ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; - var fsCopyResult = await FilesystemTasks.Wrap(() => CloneDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert())); + var fsCopyResult = await FilesystemTasks.Wrap(() => CloneDirectoryAsync(sourceFolder, destinationFolder, sourceFolder.Name, collision.Convert())); - if (fsSourceFolder.Result is IPasswordProtectedItem ppiu) + if (sourceFolder is IPasswordProtectedItem ppiu) ppiu.PasswordRequestedCallback = null; if (fsCopyResult == FileSystemStatusCode.AlreadyExists) @@ -186,15 +194,15 @@ await DialogDisplayHelper.ShowDialogAsync( return null; } - if (fsCopyResult) + if (fsCopyResult.Result is { } copiedFolder) { if (Win32Helper.HasFileAttribute(source.Path, SystemIO.FileAttributes.Hidden)) { // The source folder was hidden, apply hidden attribute to destination - Win32Helper.SetFileAttribute(fsCopyResult.Result.Path, SystemIO.FileAttributes.Hidden); + Win32Helper.SetFileAttribute(copiedFolder.Path, SystemIO.FileAttributes.Hidden); } - copiedItem = (BaseStorageFolder)fsCopyResult; + copiedItem = copiedFolder; } fsResult = fsCopyResult; @@ -219,16 +227,17 @@ await DialogDisplayHelper.ShowDialogAsync( { Debug.WriteLine(System.Runtime.InteropServices.Marshal.GetLastWin32Error()); - FilesystemResult destinationResult = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); + FilesystemResult destinationResult = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); var sourceResult = await source.ToStorageItemResult(); fsResult = sourceResult.ErrorCode | destinationResult.ErrorCode; - if (fsResult) + if (fsResult && + sourceResult.Result is BaseStorageFile file && + destinationResult.Result is { } destinationFolder) { - if (sourceResult.Result is IPasswordProtectedItem ppis) + if (file is IPasswordProtectedItem ppis) ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; - var file = (BaseStorageFile)sourceResult; var fsResultCopy = new FilesystemResult(null, FileSystemStatusCode.Generic); if (string.IsNullOrEmpty(file.Path) && collision == NameCollisionOption.GenerateUniqueName) { @@ -242,16 +251,16 @@ await DialogDisplayHelper.ShowDialogAsync( do { - fsResultCopy = await FilesystemTasks.Wrap(() => file.CopyAsync(destinationResult.Result, desiredNewName, NameCollisionOption.FailIfExists).AsTask()); + fsResultCopy = await FilesystemTasks.WrapNullable(() => file.CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists).AsTask()); desiredNewName = $"{nameWithoutExt} ({attempt}){extension}"; } while (fsResultCopy.ErrorCode == FileSystemStatusCode.AlreadyExists && ++attempt < 1024); } else { - fsResultCopy = await FilesystemTasks.Wrap(() => file.CopyAsync(destinationResult.Result, Path.GetFileName(file.Name), collision).AsTask()); + fsResultCopy = await FilesystemTasks.WrapNullable(() => file.CopyAsync(destinationFolder, Path.GetFileName(file.Name) ?? file.Name, collision).AsTask()); } - if (sourceResult.Result is IPasswordProtectedItem ppiu) + if (file is IPasswordProtectedItem ppiu) ppiu.PasswordRequestedCallback = null; if (fsResultCopy == FileSystemStatusCode.AlreadyExists) @@ -261,8 +270,8 @@ await DialogDisplayHelper.ShowDialogAsync( return null; } - if (fsResultCopy) - copiedItem = fsResultCopy.Result; + if (fsResultCopy.Result is { } copiedFile) + copiedItem = copiedFile; fsResult = fsResultCopy; } @@ -292,12 +301,12 @@ await DialogDisplayHelper.ShowDialogAsync( return new StorageHistory(FileOperationType.Copy, source, pathWithType); } - public Task MoveAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task MoveAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress? progress, CancellationToken cancellationToken) { return MoveAsync(source.FromStorageItem(), destination, collision, progress, cancellationToken); } - public async Task MoveAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public async Task MoveAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress? progress, CancellationToken cancellationToken) { StatusCenterItemProgressModel fsProgress = new( progress, @@ -333,7 +342,7 @@ await DialogDisplayHelper.ShowDialogAsync( return null; } - IStorageItem movedItem = null; + IStorageItem? movedItem = null; if (source.ItemType == FilesystemItemType.Directory) { @@ -373,16 +382,17 @@ await DialogDisplayHelper.ShowDialogAsync( Debug.WriteLine(System.Runtime.InteropServices.Marshal.GetLastWin32Error()); var fsSourceFolder = await source.ToStorageItemResult(); - var fsDestinationFolder = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); + var fsDestinationFolder = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); fsResult = fsSourceFolder.ErrorCode | fsDestinationFolder.ErrorCode; - if (fsResult) + if (fsResult && + fsSourceFolder.Result is BaseStorageFolder sourceFolder && + fsDestinationFolder.Result is BaseStorageFolder destinationFolder) { - if (fsSourceFolder.Result is IPasswordProtectedItem ppis) + if (sourceFolder is IPasswordProtectedItem ppis) ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; - var srcFolder = (BaseStorageFolder)fsSourceFolder; - var fsResultMove = await FilesystemTasks.Wrap(() => srcFolder.MoveAsync(fsDestinationFolder.Result, collision).AsTask()); + var fsResultMove = await FilesystemTasks.WrapNullable(() => sourceFolder.MoveAsync(destinationFolder, collision).AsTask()); if (!fsResultMove) // Use generic move folder operation (move folder items one by one) { @@ -390,10 +400,10 @@ await DialogDisplayHelper.ShowDialogAsync( //var fsResultMove = await FilesystemTasks.Wrap(() => MoveDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert(), true)); if (await DialogDisplayHelper.ShowDialogAsync(Strings.ErrorDialogThisActionCannotBeDone.GetLocalizedResource(), Strings.ErrorDialogUnsupportedMoveOperation.GetLocalizedResource(), "OK", Strings.Cancel.GetLocalizedResource())) - fsResultMove = await FilesystemTasks.Wrap(() => CloneDirectoryAsync((BaseStorageFolder)fsSourceFolder, (BaseStorageFolder)fsDestinationFolder, fsSourceFolder.Result.Name, collision.Convert())); + fsResultMove = await FilesystemTasks.Wrap(() => CloneDirectoryAsync(sourceFolder, destinationFolder, sourceFolder.Name, collision.Convert())); } - if (fsSourceFolder.Result is IPasswordProtectedItem ppiu) + if (sourceFolder is IPasswordProtectedItem ppiu) ppiu.PasswordRequestedCallback = null; if (fsResultMove == FileSystemStatusCode.AlreadyExists) @@ -403,15 +413,15 @@ await DialogDisplayHelper.ShowDialogAsync( return null; } - if (fsResultMove) + if (fsResultMove.Result is { } movedFolder) { if (Win32Helper.HasFileAttribute(source.Path, SystemIO.FileAttributes.Hidden)) { // The source folder was hidden, apply hidden attribute to destination - Win32Helper.SetFileAttribute(fsResultMove.Result.Path, SystemIO.FileAttributes.Hidden); + Win32Helper.SetFileAttribute(movedFolder.Path, SystemIO.FileAttributes.Hidden); } - movedItem = (BaseStorageFolder)fsResultMove; + movedItem = movedFolder; } fsResult = fsResultMove; } @@ -432,19 +442,20 @@ await DialogDisplayHelper.ShowDialogAsync( { Debug.WriteLine(System.Runtime.InteropServices.Marshal.GetLastWin32Error()); - FilesystemResult destinationResult = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); + FilesystemResult destinationResult = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); var sourceResult = await source.ToStorageItemResult(); fsResult = sourceResult.ErrorCode | destinationResult.ErrorCode; - if (fsResult) + if (fsResult && + sourceResult.Result is BaseStorageFile file && + destinationResult.Result is { } destinationFolder) { - if (sourceResult.Result is IPasswordProtectedItem ppis) + if (file is IPasswordProtectedItem ppis) ppis.PasswordRequestedCallback = UIFilesystemHelpers.RequestPassword; - var file = (BaseStorageFile)sourceResult; - var fsResultMove = await FilesystemTasks.Wrap(() => file.MoveAsync(destinationResult.Result, Path.GetFileName(file.Name), collision).AsTask()); + var fsResultMove = await FilesystemTasks.Wrap(() => file.MoveAsync(destinationFolder, Path.GetFileName(file.Name) ?? file.Name, collision).AsTask()); - if (sourceResult.Result is IPasswordProtectedItem ppiu) + if (file is IPasswordProtectedItem ppiu) ppiu.PasswordRequestedCallback = null; if (fsResultMove == FileSystemStatusCode.AlreadyExists) @@ -473,12 +484,12 @@ await DialogDisplayHelper.ShowDialogAsync( return null; } - bool sourceInCurrentFolder = PathNormalization.TrimPath(_associatedInstance.ShellViewModel.CurrentFolder.ItemPath) == - PathNormalization.GetParentDir(source.Path); + bool sourceInCurrentFolder = ShellViewModel.CurrentFolder is { } currentFolder && + PathNormalization.TrimPath(currentFolder.ItemPath) == PathNormalization.GetParentDir(source.Path); if (fsProgress.Status == FileSystemStatusCode.Success && sourceInCurrentFolder) { - await _associatedInstance.ShellViewModel.RemoveFileOrFolderAsync(source.Path); - await _associatedInstance.ShellViewModel.ApplyFilesAndFoldersChangesAsync(); + await ShellViewModel.RemoveFileOrFolderAsync(source.Path); + await ShellViewModel.ApplyFilesAndFoldersChangesAsync(); } var pathWithType = movedItem.FromStorageItem(destination, source.ItemType); @@ -486,12 +497,12 @@ await DialogDisplayHelper.ShowDialogAsync( return new StorageHistory(FileOperationType.Move, source, pathWithType); } - public Task DeleteAsync(IStorageItem source, IProgress progress, bool permanently, CancellationToken cancellationToken) + public Task DeleteAsync(IStorageItem source, IProgress? progress, bool permanently, CancellationToken cancellationToken) { return DeleteAsync(source.FromStorageItem(), progress, permanently, cancellationToken); } - public async Task DeleteAsync(IStorageItemWithPath source, IProgress progress, bool permanently, CancellationToken cancellationToken) + public async Task DeleteAsync(IStorageItemWithPath source, IProgress? progress, bool permanently, CancellationToken cancellationToken) { StatusCenterItemProgressModel fsProgress = new( progress, @@ -512,12 +523,12 @@ public async Task DeleteAsync(IStorageItemWithPath source, IPro { if (source.ItemType == FilesystemItemType.File) { - fsResult = await _associatedInstance.ShellViewModel.GetFileFromPathAsync(source.Path, cancellationToken) + fsResult = await ShellViewModel.GetFileFromPathAsync(source.Path, cancellationToken) .OnSuccess((t) => t.DeleteAsync(permanently ? StorageDeleteOption.PermanentDelete : StorageDeleteOption.Default).AsTask()); } else if (source.ItemType == FilesystemItemType.Directory) { - fsResult = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(source.Path, cancellationToken) + fsResult = await ShellViewModel.GetFolderFromPathAsync(source.Path, cancellationToken) .OnSuccess((t) => t.DeleteAsync(permanently ? StorageDeleteOption.PermanentDelete : StorageDeleteOption.Default).AsTask()); } } @@ -537,16 +548,20 @@ public async Task DeleteAsync(IStorageItemWithPath source, IPro if (deleteFromRecycleBin) { // Recycle bin also stores a file starting with $I for each item - string iFilePath = Path.Combine(Path.GetDirectoryName(source.Path), Path.GetFileName(source.Path).Replace("$R", "$I", StringComparison.Ordinal)); - - await _associatedInstance.ShellViewModel.GetFileFromPathAsync(iFilePath, cancellationToken) - .OnSuccess(iFile => iFile.DeleteAsync(StorageDeleteOption.PermanentDelete).AsTask()); + var parentPath = Path.GetDirectoryName(source.Path); + var fileName = Path.GetFileName(source.Path); + if (parentPath is not null && fileName is not null) + { + string iFilePath = Path.Combine(parentPath, fileName.Replace("$R", "$I", StringComparison.Ordinal)); + await ShellViewModel.GetFileFromPathAsync(iFilePath, cancellationToken) + .OnSuccess(iFile => iFile.DeleteAsync(StorageDeleteOption.PermanentDelete).AsTask()); + } } fsProgress.ReportStatus(fsResult); if (fsResult) { - await _associatedInstance.ShellViewModel.RemoveFileOrFolderAsync(source.Path); + await ShellViewModel.RemoveFileOrFolderAsync(source.Path); if (!permanently) { @@ -555,14 +570,22 @@ await _associatedInstance.ShellViewModel.GetFileFromPathAsync(iFilePath, cancell // Get name matching files if (FileExtensionHelpers.IsShortcutOrUrlFile(source.Path)) // We need to check if it is a shortcut file - nameMatchItems = items.Where((item) => item.FilePath == Path.Combine(Path.GetDirectoryName(source.Path), Path.GetFileNameWithoutExtension(source.Path))); + { + var parentPath = Path.GetDirectoryName(source.Path); + var fileName = Path.GetFileNameWithoutExtension(source.Path); + nameMatchItems = parentPath is not null && fileName is not null + ? items.Where((item) => item.FilePath == Path.Combine(parentPath, fileName)) + : []; + } else nameMatchItems = items.Where((item) => item.FilePath == source.Path); // Get newest file - ShellFileItem item = nameMatchItems.OrderBy((item) => item.RecycleDate).FirstOrDefault(); + ShellFileItem? item = nameMatchItems.OrderBy((item) => item.RecycleDate).FirstOrDefault(); + if (item is null) + return null; - return new StorageHistory(FileOperationType.Recycle, source, StorageHelpers.FromPathAndType(item?.RecyclePath, source.ItemType)); + return new StorageHistory(FileOperationType.Recycle, source, StorageHelpers.FromPathAndType(item.RecyclePath, source.ItemType)); } return new StorageHistory(FileOperationType.Delete, source, null); @@ -574,12 +597,12 @@ await _associatedInstance.ShellViewModel.GetFileFromPathAsync(iFilePath, cancell } } - public Task RenameAsync(IStorageItem source, string newName, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task RenameAsync(IStorageItem source, string newName, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) { return RenameAsync(StorageHelpers.FromStorageItem(source), newName, collision, progress, cancellationToken); } - public async Task RenameAsync( + public async Task RenameAsync( IStorageItemWithPath source, string newName, NameCollisionOption collision, @@ -613,16 +636,20 @@ public async Task RenameAsync( return t; }); - if (renamed) + if (renamed.Result is { } renamedItem) { fsProgress.ReportStatus(FileSystemStatusCode.Success); - return new StorageHistory(FileOperationType.Rename, source, renamed.Result.FromStorageItem()); + return new StorageHistory(FileOperationType.Rename, source, renamedItem.FromStorageItem()); } else if (renamed == FileSystemStatusCode.Unauthorized) { // Try again with MoveFileFromApp - var destination = Path.Combine(Path.GetDirectoryName(source.Path), newName); + var parentPath = Path.GetDirectoryName(source.Path); + if (parentPath is null) + return null; + + var destination = Path.Combine(parentPath, newName); if (PInvoke.MoveFileFromApp(source.Path, destination)) { fsProgress.ReportStatus(FileSystemStatusCode.Success); @@ -680,12 +707,12 @@ public async Task RenameAsync( return null; } - public async Task RestoreItemsFromTrashAsync(IList source, IList destination, IProgress progress, CancellationToken cancellationToken) + public async Task RestoreItemsFromTrashAsync(IList source, IList destination, IProgress progress, CancellationToken cancellationToken) { return await RestoreItemsFromTrashAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), destination, progress, cancellationToken); } - public async Task RestoreItemsFromTrashAsync( + public async Task RestoreItemsFromTrashAsync( IList source, IList destination, IProgress progress, @@ -695,7 +722,7 @@ public async Task RestoreItemsFromTrashAsync( StatusCenterItemProgressModel fsProgress = new(progress, true, FileSystemStatusCode.InProgress, source.Count); fsProgress.Report(); - var rawStorageHistory = new List(); + var rawStorageHistory = new List(); for (int i = 0; i < source.Count; i++) { @@ -709,23 +736,24 @@ public async Task RestoreItemsFromTrashAsync( } - if (rawStorageHistory.Any() && rawStorageHistory.All((item) => item is not null)) + if (rawStorageHistory.Count > 0 && rawStorageHistory.All(item => item is not null)) { + var storageHistory = rawStorageHistory.WhereNotNull().ToList(); return new StorageHistory( - rawStorageHistory[0].OperationType, - await rawStorageHistory.SelectMany((item) => item.Source).ToListAsync(), - await rawStorageHistory.SelectMany((item) => item.Destination).ToListAsync()); + storageHistory[0].OperationType, + await storageHistory.SelectMany(item => item.Source).ToListAsync(), + await storageHistory.SelectMany(item => item.Destination ?? []).ToListAsync()); } return null; } - public Task RestoreFromTrashAsync(IStorageItem source, string destination, IProgress progress, CancellationToken cancellationToken) + public Task RestoreFromTrashAsync(IStorageItem source, string destination, IProgress? progress, CancellationToken cancellationToken) { return RestoreFromTrashAsync(source.FromStorageItem(), destination, progress, cancellationToken); } - public async Task RestoreFromTrashAsync(IStorageItemWithPath source, string destination, IProgress progress, CancellationToken cancellationToken) + public async Task RestoreFromTrashAsync(IStorageItemWithPath source, string destination, IProgress? progress, CancellationToken cancellationToken) { StatusCenterItemProgressModel fsProgress = new(progress, true, FileSystemStatusCode.InProgress); fsProgress.Report(); @@ -738,19 +766,25 @@ public async Task RestoreFromTrashAsync(IStorageItemWithPath so { if (source.ItemType == FilesystemItemType.Directory) { - FilesystemResult sourceFolder = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(source.Path, cancellationToken); - FilesystemResult destinationFolder = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); + FilesystemResult sourceFolder = await ShellViewModel.GetFolderFromPathAsync(source.Path, cancellationToken); + FilesystemResult destinationFolder = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); fsResult = sourceFolder.ErrorCode | destinationFolder.ErrorCode; fsProgress.ReportStatus(fsResult); - if (fsResult) + if (fsResult && + sourceFolder.Result is { } sourceFolderItem && + destinationFolder.Result is { } destinationFolderItem) { // Moving folders using Storage API can result in data loss, copy instead //fsResult = await FilesystemTasks.Wrap(() => MoveDirectoryAsync(sourceFolder.Result, destinationFolder.Result, Path.GetFileName(destination), CreationCollisionOption.FailIfExists, true)); - if (await DialogDisplayHelper.ShowDialogAsync(Strings.ErrorDialogThisActionCannotBeDone.GetLocalizedResource(), Strings.ErrorDialogUnsupportedMoveOperation.GetLocalizedResource(), "OK", Strings.Cancel.GetLocalizedResource())) - fsResult = await FilesystemTasks.Wrap(() => CloneDirectoryAsync(sourceFolder.Result, destinationFolder.Result, Path.GetFileName(destination), CreationCollisionOption.FailIfExists)); + var destinationName = Path.GetFileName(destination); + if (!string.IsNullOrEmpty(destinationName) && + await DialogDisplayHelper.ShowDialogAsync(Strings.ErrorDialogThisActionCannotBeDone.GetLocalizedResource(), Strings.ErrorDialogUnsupportedMoveOperation.GetLocalizedResource(), "OK", Strings.Cancel.GetLocalizedResource())) + { + fsResult = await FilesystemTasks.Wrap(() => CloneDirectoryAsync(sourceFolderItem, destinationFolderItem, destinationName, CreationCollisionOption.FailIfExists)); + } // TODO: we could use here FilesystemHelpers with registerHistory false? } @@ -759,14 +793,20 @@ public async Task RestoreFromTrashAsync(IStorageItemWithPath so } else { - FilesystemResult sourceFile = await _associatedInstance.ShellViewModel.GetFileFromPathAsync(source.Path, cancellationToken); - FilesystemResult destinationFolder = await _associatedInstance.ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); + FilesystemResult sourceFile = await ShellViewModel.GetFileFromPathAsync(source.Path, cancellationToken); + FilesystemResult destinationFolder = await ShellViewModel.GetFolderFromPathAsync(PathNormalization.GetParentDir(destination), cancellationToken); fsResult = sourceFile.ErrorCode | destinationFolder.ErrorCode; fsProgress.ReportStatus(fsResult); - if (fsResult) - fsResult = await FilesystemTasks.Wrap(() => sourceFile.Result.MoveAsync(destinationFolder.Result, Path.GetFileName(destination), NameCollisionOption.GenerateUniqueName).AsTask()); + var destinationName = Path.GetFileName(destination); + if (fsResult && + sourceFile.Result is { } sourceFileItem && + destinationFolder.Result is { } destinationFolderItem && + !string.IsNullOrEmpty(destinationName)) + { + fsResult = await FilesystemTasks.Wrap(() => sourceFileItem.MoveAsync(destinationFolderItem, destinationName, NameCollisionOption.GenerateUniqueName).AsTask()); + } fsProgress.ReportStatus(fsResult); } @@ -780,10 +820,14 @@ public async Task RestoreFromTrashAsync(IStorageItemWithPath so if (fsResult) { // Recycle bin also stores a file starting with $I for each item - string iFilePath = Path.Combine(Path.GetDirectoryName(source.Path), Path.GetFileName(source.Path).Replace("$R", "$I", StringComparison.Ordinal)); - - await _associatedInstance.ShellViewModel.GetFileFromPathAsync(iFilePath, cancellationToken) - .OnSuccess(iFile => iFile.DeleteAsync(StorageDeleteOption.PermanentDelete).AsTask()); + var parentPath = Path.GetDirectoryName(source.Path); + var fileName = Path.GetFileName(source.Path); + if (parentPath is not null && fileName is not null) + { + string iFilePath = Path.Combine(parentPath, fileName.Replace("$R", "$I", StringComparison.Ordinal)); + await ShellViewModel.GetFileFromPathAsync(iFilePath, cancellationToken) + .OnSuccess(iFile => iFile.DeleteAsync(StorageDeleteOption.PermanentDelete).AsTask()); + } } fsProgress.ReportStatus(fsResult); @@ -831,17 +875,17 @@ private async static Task CloneDirectoryAsync(BaseStorageFold return createdRoot; } - public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) + public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) { return await CopyItemsAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), destination, collisions, progress, cancellationToken); } - public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken token, bool asAdmin = false) + public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken token, bool asAdmin = false) { StatusCenterItemProgressModel fsProgress = new(progress, true, FileSystemStatusCode.InProgress, source.Count); fsProgress.Report(); - var rawStorageHistory = new List(); + var rawStorageHistory = new List(); for (int i = 0; i < source.Count; i++) { @@ -865,27 +909,28 @@ public async Task CopyItemsAsync(IList so } - if (rawStorageHistory.Any() && rawStorageHistory.All((item) => item is not null)) + if (rawStorageHistory.Count > 0 && rawStorageHistory.All(item => item is not null)) { + var storageHistory = rawStorageHistory.WhereNotNull().ToList(); return new StorageHistory( - rawStorageHistory[0].OperationType, - await rawStorageHistory.SelectMany((item) => item.Source).ToListAsync(), - await rawStorageHistory.SelectMany((item) => item.Destination).ToListAsync()); + storageHistory[0].OperationType, + await storageHistory.SelectMany(item => item.Source).ToListAsync(), + await storageHistory.SelectMany(item => item.Destination ?? []).ToListAsync()); } return null; } - public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) + public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) { return await MoveItemsAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), destination, collisions, progress, cancellationToken); } - public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken token, bool asAdmin = false) + public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken token, bool asAdmin = false) { StatusCenterItemProgressModel fsProgress = new(progress, true, FileSystemStatusCode.InProgress, source.Count); fsProgress.Report(); - var rawStorageHistory = new List(); + var rawStorageHistory = new List(); for (int i = 0; i < source.Count; i++) { @@ -908,28 +953,29 @@ public async Task MoveItemsAsync(IList so fsProgress.Report(); } - if (rawStorageHistory.Any() && rawStorageHistory.All((item) => item is not null)) + if (rawStorageHistory.Count > 0 && rawStorageHistory.All(item => item is not null)) { + var storageHistory = rawStorageHistory.WhereNotNull().ToList(); return new StorageHistory( - rawStorageHistory[0].OperationType, - await rawStorageHistory.SelectMany((item) => item.Source).ToListAsync(), - await rawStorageHistory.SelectMany((item) => item.Destination).ToListAsync()); + storageHistory[0].OperationType, + await storageHistory.SelectMany(item => item.Source).ToListAsync(), + await storageHistory.SelectMany(item => item.Destination ?? []).ToListAsync()); } return null; } - public async Task DeleteItemsAsync(IList source, IProgress progress, bool permanently, CancellationToken cancellationToken) + public async Task DeleteItemsAsync(IList source, IProgress? progress, bool permanently, CancellationToken cancellationToken) { return await DeleteItemsAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), progress, permanently, cancellationToken); } - public async Task DeleteItemsAsync(IList source, IProgress progress, bool permanently, CancellationToken token, bool asAdmin = false) + public async Task DeleteItemsAsync(IList source, IProgress? progress, bool permanently, CancellationToken token, bool asAdmin = false) { StatusCenterItemProgressModel fsProgress = new(progress, true, FileSystemStatusCode.InProgress, source.Count); fsProgress.Report(); bool originalPermanently = permanently; - var rawStorageHistory = new List(); + var rawStorageHistory = new List(); for (int i = 0; i < source.Count; i++) { @@ -943,12 +989,14 @@ public async Task DeleteItemsAsync(IList fsProgress.Report(); } - if (rawStorageHistory.Any() && rawStorageHistory.All((item) => item is not null)) + if (rawStorageHistory.Count > 0 && rawStorageHistory.All(item => item is not null)) { + var storageHistory = rawStorageHistory.WhereNotNull().ToList(); + var destinations = await storageHistory.SelectMany(item => item.Destination ?? []).ToListAsync(); return new StorageHistory( - rawStorageHistory[0].OperationType, - await rawStorageHistory.SelectMany((item) => item.Source).ToListAsync(), - await rawStorageHistory.SelectMany((item) => item.Destination).ToListAsync()); + storageHistory[0].OperationType, + await storageHistory.SelectMany(item => item.Source).ToListAsync(), + destinations.Count > 0 ? destinations : null); } return null; } @@ -960,7 +1008,6 @@ public Task CreateShortcutItemsAsync(IList /// Destination: null /// - Task<(IStorageHistory, IStorageItem)> CreateAsync( + Task<(IStorageHistory?, IStorageItem?)> CreateAsync( IStorageItemWithPath source, IProgress process, CancellationToken cancellationToken, @@ -51,7 +51,7 @@ Task CreateShortcutItemsAsync( ///
/// Destination: The item fullPath (as ) the was copied /// - Task CopyAsync( + Task CopyAsync( IStorageItem source, string destination, NameCollisionOption collision, @@ -72,7 +72,7 @@ Task CopyAsync( ///
/// Destination: The item fullPath (as ) the was copied /// - Task CopyAsync( + Task CopyAsync( IStorageItemWithPath source, string destination, NameCollisionOption collision, @@ -82,7 +82,7 @@ Task CopyAsync( /// /// Copies to fullPath /// - Task CopyItemsAsync( + Task CopyItemsAsync( IList source, IList destination, IList collisions, @@ -92,7 +92,7 @@ Task CopyItemsAsync( /// /// Copies to fullPath /// - Task CopyItemsAsync( + Task CopyItemsAsync( IList source, IList destination, IList collisions, @@ -114,7 +114,7 @@ Task CopyItemsAsync( ///
/// Destination: The item fullPath (as ) the was moved /// - Task MoveAsync( + Task MoveAsync( IStorageItem source, string destination, NameCollisionOption collision, @@ -135,7 +135,7 @@ Task MoveAsync( ///
/// Destination: The item fullPath (as ) the was moved /// - Task MoveAsync( + Task MoveAsync( IStorageItemWithPath source, string destination, NameCollisionOption collision, @@ -145,7 +145,7 @@ Task MoveAsync( /// /// Moves to fullPath /// - Task MoveItemsAsync( + Task MoveItemsAsync( IList source, IList destination, IList collisions, @@ -155,7 +155,7 @@ Task MoveItemsAsync( /// /// Moves to fullPath /// - Task MoveItemsAsync( + Task MoveItemsAsync( IList source, IList destination, IList collisions, @@ -180,7 +180,7 @@ Task MoveItemsAsync( ///
/// If was false, returns path to recycled item /// - Task DeleteAsync( + Task DeleteAsync( IStorageItem source, IProgress progress, bool permanently, @@ -203,7 +203,7 @@ Task DeleteAsync( ///
/// If was false, returns path to recycled item /// - Task DeleteAsync( + Task DeleteAsync( IStorageItemWithPath source, IProgress progress, bool permanently, @@ -212,7 +212,7 @@ Task DeleteAsync( /// /// Deletes provided /// - Task DeleteItemsAsync( + Task DeleteItemsAsync( IList source, IProgress progress, bool permanently, @@ -221,7 +221,7 @@ Task DeleteItemsAsync( /// /// Deletes provided /// - Task DeleteItemsAsync( + Task DeleteItemsAsync( IList source, IProgress progress, bool permanently, @@ -241,7 +241,7 @@ Task DeleteItemsAsync( ///
/// Destination: The renamed item fullPath (as ) /// - Task RenameAsync( + Task RenameAsync( IStorageItem source, string newName, NameCollisionOption collision, @@ -261,7 +261,7 @@ Task RenameAsync( ///
/// Destination: The renamed item fullPath (as ) /// - Task RenameAsync( + Task RenameAsync( IStorageItemWithPath source, string newName, NameCollisionOption collision, @@ -282,7 +282,7 @@ Task RenameAsync( ///
/// Destination: The item fullPath (as ) the has been restored /// - Task RestoreItemsFromTrashAsync( + Task RestoreItemsFromTrashAsync( IList source, IList destination, IProgress progress, @@ -301,7 +301,7 @@ Task RestoreItemsFromTrashAsync( ///
/// Destination: The item fullPath (as ) the has been restored /// - Task RestoreFromTrashAsync( + Task RestoreFromTrashAsync( IStorageItem source, string destination, IProgress progress, @@ -320,7 +320,7 @@ Task RestoreFromTrashAsync( ///
/// Destination: The item fullPath (as ) the has been restored /// - Task RestoreItemsFromTrashAsync( + Task RestoreItemsFromTrashAsync( IList source, IList destination, IProgress progress, @@ -340,7 +340,7 @@ Task RestoreItemsFromTrashAsync( ///
/// Destination: The item fullPath (as ) the has been restored /// - Task RestoreFromTrashAsync( + Task RestoreFromTrashAsync( IStorageItemWithPath source, string destination, IProgress progress, diff --git a/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs b/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs index 8c9e60ff49d4..9bdfca3f7776 100644 --- a/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs +++ b/src/Files.App/Utils/Storage/Operations/ShellFilesystemOperations.cs @@ -13,9 +13,12 @@ public sealed partial class ShellFilesystemOperations : IFilesystemOperations { private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService(); - private IShellPage _associatedInstance; + private readonly IShellPage _associatedInstance; - private FilesystemOperations _filesystemOperations; + private readonly FilesystemOperations _filesystemOperations; + + private ShellViewModel ShellViewModel => + _associatedInstance.ShellViewModel ?? throw new InvalidOperationException("The shell page is not initialized."); public ShellFilesystemOperations(IShellPage associatedInstance) { @@ -23,22 +26,22 @@ public ShellFilesystemOperations(IShellPage associatedInstance) _filesystemOperations = new FilesystemOperations(associatedInstance); } - public Task CopyAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task CopyAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) { return CopyAsync(source.FromStorageItem(), destination, collision, progress, cancellationToken); } - public Task CopyAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task CopyAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) { return CopyItemsAsync(source.CreateList(), destination.CreateList(), collision.ConvertBack().CreateList(), progress, cancellationToken); } - public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) + public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) { return await CopyItemsAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), destination, collisions, progress, cancellationToken); } - public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) + public async Task CopyItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) { if (source.Any(x => string.IsNullOrWhiteSpace(x.Path) || x.Path.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(x.Path) || ZipStorageFolder.IsZipPath(x.Path, false)) || destination.Any(x => string.IsNullOrWhiteSpace(x) || x.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(x) || ZipStorageFolder.IsZipPath(x, false))) @@ -94,11 +97,11 @@ public async Task CopyItemsAsync(IList so if (result) { fsProgress.ReportStatus(FileSystemStatusCode.Success); - var copiedSources = copyResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination); + var copiedSources = GetSuccessfulResults(copyResult.Items); if (copiedSources.Any()) { var sourceMatch = await copiedSources.Select(x => sourceRename - .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return new StorageHistory( FileOperationType.Copy, @@ -120,14 +123,14 @@ public async Task CopyItemsAsync(IList so else if (copyResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse)) { var failedSources = copyResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse); - var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source); + var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source).WhereNotNull(); var lockingProcess = WhoIsLocking(filePath); switch (await GetFileInUseDialog(filePath, lockingProcess)) { case DialogResult.Primary: var copyZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await CopyItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -140,7 +143,7 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), // Retry with the StorageFile API var failedSources = copyResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong); var copyZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await _filesystemOperations.CopyItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -158,13 +161,13 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), else if (copyResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.PropertyLoss)) { var failedSources = copyResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.PropertyLoss); - var filePath = failedSources.Select(x => x.Source); + var filePath = failedSources.Select(x => x.Source).WhereNotNull(); switch (await GetFileListDialog(filePath, Strings.FilePropertiesCannotBeCopied.GetLocalizedResource(), Strings.CopyFileWithoutProperties.GetLocalizedResource(), Strings.OK.GetLocalizedResource(), Strings.Cancel.GetLocalizedResource())) { case DialogResult.Primary: var copyZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await CopyItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -178,7 +181,8 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), { var failingItems = copyResult.Items .Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.FileTooLarge) - .Select(item => item.Source); + .Select(item => item.Source) + .WhereNotNull(); await Ioc.Default.GetRequiredService().ShowDialogAsync(new FileTooLargeDialogViewModel(failingItems)); } @@ -188,7 +192,7 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), // Retry with the StorageFile API var failedSources = copyResult.Items.Where(x => !x.Succeeded); var copyZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await _filesystemOperations.CopyItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -202,7 +206,7 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), } } - public async Task<(IStorageHistory, IStorageItem)> CreateAsync(IStorageItemWithPath source, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) + public async Task<(IStorageHistory?, IStorageItem?)> CreateAsync(IStorageItemWithPath source, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) { if (string.IsNullOrWhiteSpace(source.Path) || source.Path.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(source.Path) || ZipStorageFolder.IsZipPath(source.Path, false)) { @@ -259,10 +263,12 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), { fsProgress.ReportStatus(FileSystemStatusCode.Success); - var createdSources = createResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination); - if (createdSources.Any()) + var createdDestination = createResult.Items + .SingleOrDefault(x => x is { Succeeded: true, Destination: not null } && x.Source != x.Destination) + ?.Destination; + if (createdDestination is not null) { - var item = StorageHelpers.FromPathAndType(createdSources.Single().Destination, source.ItemType); + var item = StorageHelpers.FromPathAndType(createdDestination, source.ItemType); var storageItem = await item.ToStorageItem(); return (new StorageHistory(FileOperationType.CreateNew, item.CreateList(), null), storageItem); @@ -328,22 +334,22 @@ public async Task CreateShortcutItemsAsync(IList DeleteAsync(IStorageItem source, IProgress progress, bool permanently, CancellationToken cancellationToken) + public Task DeleteAsync(IStorageItem source, IProgress progress, bool permanently, CancellationToken cancellationToken) { return DeleteAsync(source.FromStorageItem(), progress, permanently, cancellationToken); } - public Task DeleteAsync(IStorageItemWithPath source, IProgress progress, bool permanently, CancellationToken cancellationToken) + public Task DeleteAsync(IStorageItemWithPath source, IProgress progress, bool permanently, CancellationToken cancellationToken) { return DeleteItemsAsync(source.CreateList(), progress, permanently, cancellationToken); } - public async Task DeleteItemsAsync(IList source, IProgress progress, bool permanently, CancellationToken cancellationToken) + public async Task DeleteItemsAsync(IList source, IProgress? progress, bool permanently, CancellationToken cancellationToken) { return await DeleteItemsAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), progress, permanently, cancellationToken); } - public async Task DeleteItemsAsync(IList source, IProgress progress, bool permanently, CancellationToken cancellationToken, bool asAdmin = false) + public async Task DeleteItemsAsync(IList source, IProgress? progress, bool permanently, CancellationToken cancellationToken, bool asAdmin = false) { if (source.Any(x => string.IsNullOrWhiteSpace(x.Path) || x.Path.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(x.Path))) { @@ -367,7 +373,7 @@ public async Task DeleteItemsAsync(IList if (deleteFromRecycleBin) { // Recycle bin also stores a file starting with $I for each item - deleteFilePaths = deleteFilePaths.Concat(source.Select(x => Path.Combine(Path.GetDirectoryName(x.Path), Path.GetFileName(x.Path).Replace("$R", "$I", StringComparison.Ordinal)))).Distinct(); + deleteFilePaths = deleteFilePaths.Concat(source.Select(x => GetRecycleMetadataPath(x.Path)).WhereNotNull()).Distinct(); } var operationID = Guid.NewGuid().ToString(); @@ -387,14 +393,15 @@ public async Task DeleteItemsAsync(IList foreach (var item in deleteResult.Items) { - await _associatedInstance.ShellViewModel.RemoveFileOrFolderAsync(item.Source); + if (item.Source is { } itemPath) + await ShellViewModel.RemoveFileOrFolderAsync(itemPath); } - var recycledSources = deleteResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination); + var recycledSources = GetSuccessfulResults(deleteResult.Items); if (recycledSources.Any()) { var sourceMatch = await recycledSources.Select(x => source.DistinctBy(x => x.Path) - .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return new StorageHistory( FileOperationType.Recycle, @@ -419,28 +426,31 @@ public async Task DeleteItemsAsync(IList var filesToCheck = new List(); foreach (var failedSource in failedSources) { - if (Directory.Exists(failedSource.Source)) + if (failedSource.Source is not { } failedPath) + continue; + + if (Directory.Exists(failedPath)) { try { - var files = Directory.EnumerateFiles(failedSource.Source, "*", SearchOption.AllDirectories); + var files = Directory.EnumerateFiles(failedPath, "*", SearchOption.AllDirectories); filesToCheck.AddRange(files); } catch { } } - else if (File.Exists(failedSource.Source)) + else if (File.Exists(failedPath)) { - filesToCheck.Add(failedSource.Source); + filesToCheck.Add(failedPath); } } - var filePath = filesToCheck.Any() ? filesToCheck : failedSources.Select(x => x.Source); + var filePath = filesToCheck.Any() ? filesToCheck : failedSources.Select(x => x.Source).WhereNotNull(); var lockingProcess = WhoIsLocking(filePath); switch (await GetFileInUseDialog(filePath, lockingProcess)) { case DialogResult.Primary: - return await DeleteItemsAsync(await failedSources.Select(x => source.DistinctBy(x => x.Path).SingleOrDefault(s => s.Path == x.Source)).Where(x => x is not null).ToListAsync(), progress, permanently, cancellationToken); + return await DeleteItemsAsync(await failedSources.Select(x => source.DistinctBy(x => x.Path).SingleOrDefault(s => s.Path == x.Source)).WhereNotNull().ToListAsync(), progress, permanently, cancellationToken); } } else if (deleteResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong)) @@ -455,7 +465,7 @@ public async Task DeleteItemsAsync(IList { // Retry with StorageFile API var failedSources = deleteResult.Items.Where(x => !x.Succeeded); - var sourceMatch = await failedSources.Select(x => source.DistinctBy(x => x.Path).SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => source.DistinctBy(x => x.Path).SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await _filesystemOperations.DeleteItemsAsync(sourceMatch, progress, permanently, cancellationToken); } @@ -466,22 +476,22 @@ public async Task DeleteItemsAsync(IList } } - public Task MoveAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task MoveAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) { return MoveAsync(source.FromStorageItem(), destination, collision, progress, cancellationToken); } - public Task MoveAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task MoveAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) { return MoveItemsAsync(source.CreateList(), destination.CreateList(), collision.ConvertBack().CreateList(), progress, cancellationToken); } - public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) + public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken) { return await MoveItemsAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), destination, collisions, progress, cancellationToken); } - public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) + public async Task MoveItemsAsync(IList source, IList destination, IList collisions, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) { if (source.Any(x => string.IsNullOrWhiteSpace(x.Path) || x.Path.StartsWith(@"\\?\", StringComparison.Ordinal)) || destination.Any(x => string.IsNullOrWhiteSpace(x) || x.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(x) || ZipStorageFolder.IsZipPath(x, false))) { @@ -533,11 +543,11 @@ public async Task MoveItemsAsync(IList so { fsProgress.ReportStatus(FileSystemStatusCode.Success); - var movedSources = moveResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination); + var movedSources = GetSuccessfulResults(moveResult.Items); if (movedSources.Any()) { var sourceMatch = await movedSources.Select(x => sourceRename - .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return new StorageHistory(FileOperationType.Move, sourceMatch, @@ -566,14 +576,14 @@ public async Task MoveItemsAsync(IList so else if (moveResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse)) { var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse); - var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source); + var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source).WhereNotNull(); var lockingProcess = WhoIsLocking(filePath); switch (await GetFileInUseDialog(filePath, lockingProcess)) { case DialogResult.Primary: var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await MoveItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -586,7 +596,7 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), // Retry with the StorageFile API var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong); var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await _filesystemOperations.MoveItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -604,12 +614,12 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), else if (moveResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.PropertyLoss)) { var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.PropertyLoss); - var filePath = failedSources.Select(x => x.Source); + var filePath = failedSources.Select(x => x.Source).WhereNotNull(); switch (await GetFileListDialog(filePath, Strings.FilePropertiesCannotBeMoved.GetLocalizedResource(), Strings.MoveFileWithoutProperties.GetLocalizedResource(), Strings.OK.GetLocalizedResource(), Strings.Cancel.GetLocalizedResource())) { case DialogResult.Primary: var copyZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await CopyItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), await sourceMatch.Select(x => x.dest).ToListAsync(), @@ -623,7 +633,7 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), // Retry with the StorageFile API var failedSources = moveResult.Items.Where(x => !x.Succeeded); var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll }); - var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await _filesystemOperations.MoveItemsAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -635,12 +645,12 @@ await sourceMatch.Select(x => x.dest).ToListAsync(), } } - public Task RenameAsync(IStorageItem source, string newName, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) + public Task RenameAsync(IStorageItem source, string newName, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken) { return RenameAsync(StorageHelpers.FromStorageItem(source), newName, collision, progress, cancellationToken); } - public async Task RenameAsync(IStorageItemWithPath source, string newName, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) + public async Task RenameAsync(IStorageItemWithPath source, string newName, NameCollisionOption collision, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) { if (string.IsNullOrWhiteSpace(source.Path) || source.Path.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(source.Path) || ZipStorageFolder.IsZipPath(source.Path, false)) { @@ -663,12 +673,15 @@ public async Task RenameAsync(IStorageItemWithPath source, stri { fsProgress.ReportStatus(FileSystemStatusCode.Success); - var renamedSources = renameResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination) - .Where(x => x.Source == source.Path); - if (renamedSources.Any()) + var renamedDestination = renameResult.Items + .SingleOrDefault(x => x is { Succeeded: true, Destination: not null } && + x.Source == source.Path && + x.Source != x.Destination) + ?.Destination; + if (renamedDestination is not null) { return new StorageHistory(FileOperationType.Rename, source, - StorageHelpers.FromPathAndType(renamedSources.Single().Destination, source.ItemType)); + StorageHelpers.FromPathAndType(renamedDestination, source.ItemType)); } // Cannot undo overwrite operation @@ -694,7 +707,7 @@ await DynamicDialogFactory else if (renameResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse)) { var failedSources = renameResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse); - var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source); + var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source).WhereNotNull(); var lockingProcess = WhoIsLocking(filePath); switch (await GetFileInUseDialog(filePath, lockingProcess)) @@ -729,22 +742,22 @@ await DynamicDialogFactory } } - public Task RestoreFromTrashAsync(IStorageItem source, string destination, IProgress progress, CancellationToken cancellationToken) + public Task RestoreFromTrashAsync(IStorageItem source, string destination, IProgress progress, CancellationToken cancellationToken) { return RestoreFromTrashAsync(source.FromStorageItem(), destination, progress, cancellationToken); } - public Task RestoreFromTrashAsync(IStorageItemWithPath source, string destination, IProgress progress, CancellationToken cancellationToken) + public Task RestoreFromTrashAsync(IStorageItemWithPath source, string destination, IProgress progress, CancellationToken cancellationToken) { return RestoreItemsFromTrashAsync(source.CreateList(), destination.CreateList(), progress, cancellationToken); } - public async Task RestoreItemsFromTrashAsync(IList source, IList destination, IProgress progress, CancellationToken cancellationToken) + public async Task RestoreItemsFromTrashAsync(IList source, IList destination, IProgress progress, CancellationToken cancellationToken) { return await RestoreItemsFromTrashAsync(await source.Select((item) => item.FromStorageItem()).ToListAsync(), destination, progress, cancellationToken); } - public async Task RestoreItemsFromTrashAsync(IList source, IList destination, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) + public async Task RestoreItemsFromTrashAsync(IList source, IList destination, IProgress progress, CancellationToken cancellationToken, bool asAdmin = false) { if (source.Any(x => string.IsNullOrWhiteSpace(x.Path) || x.Path.StartsWith(@"\\?\", StringComparison.Ordinal)) || destination.Any(x => string.IsNullOrWhiteSpace(x) || x.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(x) || ZipStorageFolder.IsZipPath(x, false))) { @@ -769,16 +782,16 @@ public async Task RestoreItemsFromTrashAsync(IList x.Succeeded && x.Destination is not null && x.Source != x.Destination); + var movedSources = GetSuccessfulResults(moveResult.Items); if (movedSources.Any()) { var sourceMatch = await movedSources.Select(x => source - .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + .SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); // Recycle bin also stores a file starting with $I for each item await DeleteItemsAsync(await movedSources.Zip(sourceMatch, (rSrc, oSrc) => new { rSrc, oSrc }) .Select(src => StorageHelpers.FromPathAndType( - Path.Combine(Path.GetDirectoryName(src.rSrc.Source), Path.GetFileName(src.rSrc.Source).Replace("$R", "$I", StringComparison.Ordinal)), + GetRecycleMetadataPath(src.rSrc.Source) ?? throw new InvalidOperationException("The recycle bin metadata path is invalid."), src.oSrc.ItemType)).ToListAsync(), null, true, cancellationToken); return new StorageHistory(FileOperationType.Restore, @@ -799,14 +812,14 @@ public async Task RestoreItemsFromTrashAsync(IList CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse)) { var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse); - var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source); + var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_DEST ? x.Destination : x.Source).WhereNotNull(); var lockingProcess = WhoIsLocking(filePath); switch (await GetFileInUseDialog(filePath, lockingProcess)) { case DialogResult.Primary: var moveZip = source.Zip(destination, (src, dest) => new { src, dest }); - var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await RestoreItemsFromTrashAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -818,7 +831,7 @@ await sourceMatch.Select(x => x.src).ToListAsync(), // Retry with StorageFile API var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong); var moveZip = source.Zip(destination, (src, dest) => new { src, dest }); - var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync(); + var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).WhereNotNull().ToListAsync(); return await _filesystemOperations.RestoreItemsFromTrashAsync( await sourceMatch.Select(x => x.src).ToListAsync(), @@ -839,9 +852,10 @@ await sourceMatch.Select(x => x.src).ToListAsync(), } } - private void CancelOperation(object operationID) + private void CancelOperation(object? operationID) { - FileOperationsHelpers.TryCancelOperation((string)operationID); + if (operationID is string id) + FileOperationsHelpers.TryCancelOperation(id); } private async Task RequestAdminOperation() @@ -850,8 +864,10 @@ private async Task RequestAdminOperation() return await dialogService.ShowDialogAsync(new ElevateConfirmDialogViewModel()) == DialogResult.Primary; } - private Task GetFileInUseDialog(IEnumerable source, IEnumerable lockingProcess = null) + private Task GetFileInUseDialog(IEnumerable source, IEnumerable? lockingProcess = null) { + lockingProcess ??= []; + var titleText = Strings.FileInUseDialogTitle.GetLocalizedResource(); var subtitleText = lockingProcess.IsEmpty() ? Strings.FileInUseDialogText.GetLocalizedResource() @@ -867,10 +883,10 @@ private Task GetFileInUseDialog(IEnumerable source, IEnume sourceCount > 1 ? Strings.Skip.GetLocalizedResource() : Strings.Cancel.GetLocalizedResource()); } - private async Task GetFileListDialog(IEnumerable source, string titleText, string descriptionText = null, string primaryButtonText = null, string secondaryButtonText = null, string closeButtonText = null) + private async Task GetFileListDialog(IEnumerable source, string titleText, string? descriptionText = null, string? primaryButtonText = null, string? secondaryButtonText = null, string? closeButtonText = null) { var incomingItems = new List(); - List binItems = null; + List? binItems = null; foreach (var src in source) { if (StorageTrashBinService.IsUnderTrashBin(src)) @@ -902,15 +918,34 @@ private async Task GetFileListDialog(IEnumerable source, s private IEnumerable WhoIsLocking(IEnumerable filesToCheck) { - return FileOperationsHelpers.CheckFileInUse(filesToCheck.ToArray()); + return FileOperationsHelpers.CheckFileInUse(filesToCheck.ToArray()) ?? []; } - public void Dispose() + private static IEnumerable<(ShellOperationItemResult Result, string Source, string Destination)> GetSuccessfulResults(IEnumerable results) { - _filesystemOperations?.Dispose(); + foreach (var result in results) + { + if (result is { Succeeded: true, Source: { } source, Destination: { } destination } && + source != destination) + { + yield return (result, source, destination); + } + } + } + + private static string? GetRecycleMetadataPath(string recyclePath) + { + var parentPath = Path.GetDirectoryName(recyclePath); + var fileName = Path.GetFileName(recyclePath); - _filesystemOperations = null; - _associatedInstance = null; + return parentPath is not null && !string.IsNullOrEmpty(fileName) + ? Path.Combine(parentPath, fileName.Replace("$R", "$I", StringComparison.Ordinal)) + : null; + } + + public void Dispose() + { + _filesystemOperations.Dispose(); } } } diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index dcb61192fffd..a16c5d1ef4ba 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -25,9 +25,9 @@ public sealed class FolderSearch private const uint defaultStepSize = 500; - public string? Query { get; set; } + public required string Query { get; init; } - public string? Folder { get; set; } + public required string Folder { get; init; } public uint MaxItemCount { get; set; } = 0; // 0: no limit @@ -164,7 +164,10 @@ private async Task SearchAsync(BaseStorageFolder folder, IList resul try { if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles) - results.Add(await GetListedItemAsync(item)); + { + if (await GetListedItemAsync(item) is { } listedItem) + results.Add(listedItem); + } } catch (Exception ex) { @@ -342,10 +345,14 @@ private async Task SearchTagsAsync(string folder, IList results, Can { try { - IStorageItem item = (BaseStorageFile)await GetStorageFileAsync(match.FilePath); - item ??= (BaseStorageFolder)await GetStorageFolderAsync(match.FilePath); - if (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles) - results.Add(await GetListedItemAsync(item)); + IStorageItem? item = (await GetStorageFileAsync(match.FilePath)).Result; + item ??= (await GetStorageFolderAsync(match.FilePath)).Result; + if (item is not null && + (!item.Name.StartsWith('.') || UserSettingsService.FoldersSettingsService.ShowDotFiles) && + await GetListedItemAsync(item) is { } listedItem) + { + results.Add(listedItem); + } } catch (Exception ex) { @@ -374,9 +381,9 @@ private async Task AddItemsAsync(string folder, IList results, Cance var workingFolder = await GetStorageFolderAsync(folder); var hiddenOnlyFromWin32 = false; - if (workingFolder) + if (workingFolder.Result is { } storageFolder) { - await SearchAsync(workingFolder, results, token); + await SearchAsync(storageFolder, results, token); hiddenOnlyFromWin32 = (results.Count != 0); } @@ -467,7 +474,7 @@ await Task.Run(() => Win32PInvoke.FileTimeToSystemTime(ref shortcutFindData.ftLastWriteTime, out Win32PInvoke.SYSTEMTIME modifiedTime); Win32PInvoke.FileTimeToSystemTime(ref shortcutFindData.ftCreationTime, out Win32PInvoke.SYSTEMTIME createdTime); var fileSize = Win32FindDataExtensions.GetSize(shortcutFindData); - var itemFileExtension = shortcutFindData.cFileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath) : null; + var itemFileExtension = shortcutFindData.cFileName.Contains('.', StringComparison.Ordinal) ? Path.GetExtension(itemPath) ?? string.Empty : string.Empty; var shortcutItem = new ShortcutItem(null) { @@ -572,9 +579,9 @@ await Task.Run(() => } } - private ListedItem GetListedItemAsync(string itemPath, WIN32_FIND_DATA findData) + private ListedItem? GetListedItemAsync(string itemPath, WIN32_FIND_DATA findData) { - ListedItem listedItem = null; + ListedItem? listedItem = null; var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden; var isFolder = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Directory) == FileAttributes.Directory; Win32PInvoke.FileTimeToSystemTime(ref findData.ftLastWriteTime, out Win32PInvoke.SYSTEMTIME systemModifiedTimeOutput); @@ -582,8 +589,8 @@ private ListedItem GetListedItemAsync(string itemPath, WIN32_FIND_DATA findData) if (!isFolder) { - string itemFileExtension = null; - string itemType = null; + var itemFileExtension = string.Empty; + var itemType = string.Empty; long fileSize = Win32FindDataExtensions.GetSize(findData); if (findData.cFileName.Contains('.', StringComparison.Ordinal)) { @@ -650,12 +657,15 @@ private ListedItem GetListedItemAsync(string itemPath, WIN32_FIND_DATA findData) return listedItem; } - private async Task GetListedItemAsync(IStorageItem item) + private async Task GetListedItemAsync(IStorageItem item) { - ListedItem listedItem = null; + ListedItem? listedItem = null; if (item.IsOfType(StorageItemTypes.Folder)) { var folder = item.AsBaseStorageFolder(); + if (folder is null) + return null; + var props = await folder.GetBasicPropertiesAsync(); if (folder is BinStorageFolder binFolder) { @@ -693,9 +703,12 @@ private async Task GetListedItemAsync(IStorageItem item) else if (item.IsOfType(StorageItemTypes.File)) { var file = item.AsBaseStorageFile(); + if (file is null) + return null; + var props = await file.GetBasicPropertiesAsync(); - string itemFileExtension = null; - string itemType = null; + var itemFileExtension = string.Empty; + var itemType = string.Empty; if (file.Name.Contains('.', StringComparison.Ordinal)) { itemFileExtension = Path.GetExtension(file.Path); @@ -820,9 +833,9 @@ private QueryOptions ToQueryOptions() } private static Task> GetStorageFolderAsync(string path) - => FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); + => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); private static Task> GetStorageFileAsync(string path) - => FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path)); + => FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(path)); } } diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseBasicStorageItemExtraProperties.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseBasicStorageItemExtraProperties.cs index 03134b3f8125..f8edc874035b 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseBasicStorageItemExtraProperties.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseBasicStorageItemExtraProperties.cs @@ -22,16 +22,16 @@ public override IAsyncOperation> RetrievePropertiesA { var props = new Dictionary(); - propertiesToRetrieve.ForEach(x => props[x] = null); + propertiesToRetrieve.ForEach(x => props.SetNullablePropertyValue(x, null)); // Fill out common properties var ret = _item.AsBaseStorageFile()?.GetBasicPropertiesAsync() ?? _item.AsBaseStorageFolder()?.GetBasicPropertiesAsync(); var basicProps = ret is not null ? await ret : null; - props["System.ParsingPath"] = _item?.Path; - props["System.DateCreated"] = basicProps?.DateCreated; - props["System.DateModified"] = basicProps?.DateModified; + props.SetNullablePropertyValue("System.ParsingPath", _item.Path); + props.SetNullablePropertyValue("System.DateCreated", basicProps?.DateCreated); + props.SetNullablePropertyValue("System.DateModified", basicProps?.DateModified); return props; }); diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs index 11f4eb53e8cd..fc2f897e5b69 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFile.cs @@ -39,9 +39,11 @@ public StorageProvider? Provider public abstract IStorageItemExtraProperties Properties { get; } StorageItemContentProperties IStorageItemProperties.Properties - => this is SystemStorageFile file ? file.File.Properties : null; + => this is SystemStorageFile file + ? file.File.Properties + : throw new NotSupportedException("Content properties are only available for system storage files."); - public static implicit operator BaseStorageFile(StorageFile value) + public static implicit operator BaseStorageFile?(StorageFile? value) { return value is not null ? new SystemStorageFile(value) : null; } @@ -52,13 +54,16 @@ public static implicit operator BaseStorageFile(StorageFile value) public abstract bool IsOfType(StorageItemTypes type); - public abstract IAsyncOperation GetParentAsync(); + public abstract IAsyncOperation GetParentAsync(); - IAsyncOperation IStorageItem2.GetParentAsync() + IAsyncOperation IStorageItem2.GetParentAsync() { return - AsyncInfo.Run(async (cancellationToken) - => await (await GetParentAsync()).ToStorageFolderAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var parent = await GetParentAsync(); + return parent is not null ? await parent.ToStorageFolderAsync() : null; + }); } public abstract IAsyncOperation GetBasicPropertiesAsync(); @@ -82,31 +87,40 @@ IAsyncOperation IStorageItem.GetBasicPropertiesAsync() public abstract IAsyncOperation OpenTransactedWriteAsync(StorageOpenOptions options); - public abstract IAsyncOperation CopyAsync(IStorageFolder destinationFolder); + public abstract IAsyncOperation CopyAsync(IStorageFolder destinationFolder); IAsyncOperation IStorageFile.CopyAsync(IStorageFolder destinationFolder) { return - AsyncInfo.Run(async (cancellationToken) - => await (await CopyAsync(destinationFolder)).ToStorageFileAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var file = await CopyAsync(destinationFolder) ?? throw new IOException("The file could not be copied."); + return await file.ToStorageFileAsync(); + }); } - public abstract IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName); + public abstract IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName); IAsyncOperation IStorageFile.CopyAsync(IStorageFolder destinationFolder, string desiredNewName) { return - AsyncInfo.Run(async (cancellationToken) - => await (await CopyAsync(destinationFolder, desiredNewName)).ToStorageFileAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var file = await CopyAsync(destinationFolder, desiredNewName) ?? throw new IOException("The file could not be copied."); + return await file.ToStorageFileAsync(); + }); } - public abstract IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option); + public abstract IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option); IAsyncOperation IStorageFile.CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { return - AsyncInfo.Run(async (cancellationToken) - => await (await CopyAsync(destinationFolder, desiredNewName, option)).ToStorageFileAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var file = await CopyAsync(destinationFolder, desiredNewName, option) ?? throw new IOException("The file could not be copied."); + return await file.ToStorageFileAsync(); + }); } public abstract IAsyncAction MoveAsync(IStorageFolder destinationFolder); @@ -127,30 +141,30 @@ IAsyncOperation IStorageFile.CopyAsync(IStorageFolder destinationFo public abstract IAsyncAction DeleteAsync(StorageDeleteOption option); - public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode); + public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode); - public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize); + public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize); - public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options); + public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options); - public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode) + public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } - public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize) + public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } - public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } - public static IAsyncOperation GetFileFromPathAsync(string path) + public static IAsyncOperation GetFileFromPathAsync(string path) { - return AsyncInfo.Run(async (cancellationToken) + return AsyncInfo.Run(async (cancellationToken) => await ZipStorageFile.FromPathAsync(path) ?? await FtpStorageFile.FromPathAsync(path) ?? await ShellStorageFile.FromPathAsync(path) ?? await NativeStorageFile.FromPathAsync(path) ?? await SystemStorageFile.FromPathAsync(path) ); } diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs index 5517798b8727..b49d7c350ad8 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageFolder.cs @@ -31,9 +31,11 @@ public StorageProvider? Provider public abstract IStorageItemExtraProperties Properties { get; } StorageItemContentProperties IStorageItemProperties.Properties - => this is SystemStorageFolder folder ? folder.Folder.Properties : null; + => this is SystemStorageFolder folder + ? folder.Folder.Properties + : throw new NotSupportedException("Content properties are only available for system storage folders."); - public static implicit operator BaseStorageFolder(StorageFolder value) + public static implicit operator BaseStorageFolder?(StorageFolder? value) { return value is not null @@ -49,11 +51,15 @@ value is not null public abstract IAsyncOperation GetIndexedStateAsync(); - public abstract IAsyncOperation GetParentAsync(); + public abstract IAsyncOperation GetParentAsync(); - IAsyncOperation IStorageItem2.GetParentAsync() + IAsyncOperation IStorageItem2.GetParentAsync() { - return AsyncInfo.Run(async (cancellationToken) => await (await GetParentAsync()).ToStorageFolderAsync()); + return AsyncInfo.Run(async (cancellationToken) => + { + var parent = await GetParentAsync(); + return parent is not null ? await parent.ToStorageFolderAsync() : null; + }); } public abstract IAsyncOperation GetBasicPropertiesAsync(); @@ -63,19 +69,23 @@ IAsyncOperation IStorageItem.GetBasicPropertiesAsync() return AsyncInfo.Run(async (cancellationToken) => await (await ToStorageFolderAsync()).GetBasicPropertiesAsync()); } - public abstract IAsyncOperation GetItemAsync(string name); + public abstract IAsyncOperation GetItemAsync(string name); - public abstract IAsyncOperation TryGetItemAsync(string name); + public abstract IAsyncOperation TryGetItemAsync(string name); public abstract IAsyncOperation> GetItemsAsync(); public abstract IAsyncOperation> GetItemsAsync(uint startIndex, uint maxItemsToRetrieve); - public abstract IAsyncOperation GetFileAsync(string name); + public abstract IAsyncOperation GetFileAsync(string name); IAsyncOperation IStorageFolder.GetFileAsync(string name) { - return AsyncInfo.Run(async (cancellationToken) => await (await GetFileAsync(name)).ToStorageFileAsync()); + return AsyncInfo.Run(async (cancellationToken) => + { + var file = await GetFileAsync(name) ?? throw new System.IO.FileNotFoundException($"The file '{name}' was not found."); + return await file.ToStorageFileAsync(); + }); } public abstract IAsyncOperation> GetFilesAsync(); @@ -105,19 +115,22 @@ IAsyncOperation> IStorageFolderQueryOperations.GetFil => await Task.WhenAll((await GetFilesAsync(query, startIndex, maxItemsToRetrieve)).Select(x => x.ToStorageFileAsync().AsTask()))); } - public abstract IAsyncOperation GetFolderAsync(string name); + public abstract IAsyncOperation GetFolderAsync(string name); IAsyncOperation IStorageFolder.GetFolderAsync(string name) { return - AsyncInfo.Run(async (cancellationToken) - => await (await GetFolderAsync(name)).ToStorageFolderAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var folder = await GetFolderAsync(name) ?? throw new System.IO.DirectoryNotFoundException($"The folder '{name}' was not found."); + return await folder.ToStorageFolderAsync(); + }); } - public static IAsyncOperation GetFolderFromPathAsync(string path) + public static IAsyncOperation GetFolderFromPathAsync(string path) { return - AsyncInfo.Run(async (cancellationToken) + AsyncInfo.Run(async (cancellationToken) => await ZipStorageFolder.FromPathAsync(path) ?? await FtpStorageFolder.FromPathAsync(path) ?? await ShellStorageFolder.FromPathAsync(path) ?? await SystemStorageFolder.FromPathAsync(path)); } @@ -148,45 +161,57 @@ IAsyncOperation> IStorageFolderQueryOperations.GetF => await Task.WhenAll((await GetFoldersAsync(query, startIndex, maxItemsToRetrieve)).Select(x => x.ToStorageFolderAsync().AsTask()))); } - public abstract IAsyncOperation CreateFileAsync(string desiredName); + public abstract IAsyncOperation CreateFileAsync(string desiredName); IAsyncOperation IStorageFolder.CreateFileAsync(string desiredName) { return - AsyncInfo.Run(async (cancellationToken) - => await (await CreateFileAsync(desiredName)).ToStorageFileAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var file = await CreateFileAsync(desiredName) ?? throw new System.IO.IOException($"The file '{desiredName}' could not be created."); + return await file.ToStorageFileAsync(); + }); } - public abstract IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options); + public abstract IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options); IAsyncOperation IStorageFolder.CreateFileAsync(string desiredName, CreationCollisionOption options) { return - AsyncInfo.Run(async (cancellationToken) - => await (await CreateFileAsync(desiredName, options)).ToStorageFileAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var file = await CreateFileAsync(desiredName, options) ?? throw new System.IO.IOException($"The file '{desiredName}' could not be created."); + return await file.ToStorageFileAsync(); + }); } - public abstract IAsyncOperation CreateFolderAsync(string desiredName); + public abstract IAsyncOperation CreateFolderAsync(string desiredName); IAsyncOperation IStorageFolder.CreateFolderAsync(string desiredName) { return - AsyncInfo.Run(async (cancellationToken) - => await (await CreateFolderAsync(desiredName)).ToStorageFolderAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var folder = await CreateFolderAsync(desiredName) ?? throw new System.IO.IOException($"The folder '{desiredName}' could not be created."); + return await folder.ToStorageFolderAsync(); + }); } - public abstract IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options); + public abstract IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options); IAsyncOperation IStorageFolder.CreateFolderAsync(string desiredName, CreationCollisionOption options) { return - AsyncInfo.Run(async (cancellationToken) - => await (await CreateFolderAsync(desiredName, options)).ToStorageFolderAsync()); + AsyncInfo.Run(async (cancellationToken) => + { + var folder = await CreateFolderAsync(desiredName, options) ?? throw new System.IO.IOException($"The folder '{desiredName}' could not be created."); + return await folder.ToStorageFolderAsync(); + }); } - public abstract IAsyncOperation MoveAsync(IStorageFolder destinationFolder); + public abstract IAsyncOperation MoveAsync(IStorageFolder destinationFolder); - public abstract IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option); + public abstract IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option); public abstract IAsyncAction RenameAsync(string desiredName); @@ -196,23 +221,23 @@ IAsyncOperation IStorageFolder.CreateFolderAsync(string desiredNa public abstract IAsyncAction DeleteAsync(StorageDeleteOption option); - public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode); + public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode); - public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize); + public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize); - public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options); + public abstract IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options); - public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode) + public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } - public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize) - => Task.FromResult(null).AsAsyncOperation(); + public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize) + => Task.FromResult(null).AsAsyncOperation(); - public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public IAsyncOperation GetScaledImageAsThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } public abstract bool AreQueryOptionsSupported(QueryOptions queryOptions); diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageItemExtraProperties.cs b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageItemExtraProperties.cs index 1c2f532e3498..616190633039 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageItemExtraProperties.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/BaseStorageItemExtraProperties.cs @@ -16,7 +16,7 @@ public virtual IAsyncOperation> RetrievePropertiesAs AsyncInfo.Run((cancellationToken) => { var props = new Dictionary(); - propertiesToRetrieve.ForEach(x => props[x] = null); + propertiesToRetrieve.ForEach(x => props.SetNullablePropertyValue(x, null)); return Task.FromResult>(props); }); @@ -31,5 +31,29 @@ public virtual IAsyncAction SavePropertiesAsync([HasVariant] IEnumerable> propertiesToSave) + { + return properties.SavePropertiesAsync(propertiesToSave.Select( + static property => new KeyValuePair(property.Key, ToWinRtPropertyValue(property.Value)))); + } + + internal static void SetNullablePropertyValue( + this IDictionary properties, + string name, + object? value) + { + properties[name] = ToWinRtPropertyValue(value); + } + + // WinRT property bags use null for unavailable values despite the projected object contract. + private static object ToWinRtPropertyValue(object? value) + => value!; } } diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFile.cs b/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFile.cs index c8e9d201cce1..85055765180a 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFile.cs @@ -13,14 +13,20 @@ public interface IBaseStorageFile : IStorageItem2, IStorageFile, IStorageFile2, IAsyncOperation ToStorageFileAsync(); - new IAsyncOperation GetParentAsync(); + new IAsyncOperation GetParentAsync(); new IAsyncOperation GetBasicPropertiesAsync(); - new IAsyncOperation CopyAsync(IStorageFolder destinationFolder); + new IAsyncOperation CopyAsync(IStorageFolder destinationFolder); - new IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName); + new IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName); - new IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option); + new IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option); + + new IAsyncOperation GetThumbnailAsync(ThumbnailMode mode); + + new IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize); + + new IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options); } } diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFolder.cs b/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFolder.cs index 44827c86732b..f8fadb853ae7 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/IBaseStorageFolder.cs @@ -14,15 +14,17 @@ public interface IBaseStorageFolder : IStorageItem2, IStorageFolder, IStorageFol IAsyncOperation ToStorageFolderAsync(); - new IAsyncOperation GetParentAsync(); + new IAsyncOperation GetParentAsync(); new IAsyncOperation GetBasicPropertiesAsync(); - new IAsyncOperation GetItemAsync(string name); + new IAsyncOperation GetItemAsync(string name); + + new IAsyncOperation TryGetItemAsync(string name); new IAsyncOperation> GetItemsAsync(); - new IAsyncOperation GetFileAsync(string name); + new IAsyncOperation GetFileAsync(string name); new IAsyncOperation> GetFilesAsync(); @@ -30,7 +32,7 @@ public interface IBaseStorageFolder : IStorageItem2, IStorageFolder, IStorageFol new IAsyncOperation> GetFilesAsync(CommonFileQuery query, uint startIndex, uint maxItemsToRetrieve); - new IAsyncOperation GetFolderAsync(string name); + new IAsyncOperation GetFolderAsync(string name); new IAsyncOperation> GetFoldersAsync(); @@ -38,16 +40,22 @@ public interface IBaseStorageFolder : IStorageItem2, IStorageFolder, IStorageFol new IAsyncOperation> GetFoldersAsync(CommonFolderQuery query, uint startIndex, uint maxItemsToRetrieve); - new IAsyncOperation CreateFileAsync(string desiredName); + new IAsyncOperation CreateFileAsync(string desiredName); + + new IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options); + + new IAsyncOperation CreateFolderAsync(string desiredName); + + new IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options); - new IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options); + IAsyncOperation MoveAsync(IStorageFolder destinationFolder); + IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option); - new IAsyncOperation CreateFolderAsync(string desiredName); + new IAsyncOperation GetThumbnailAsync(ThumbnailMode mode); - new IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options); + new IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize); - IAsyncOperation MoveAsync(IStorageFolder destinationFolder); - IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option); + new IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options); new BaseStorageItemQueryResult CreateItemQueryWithOptions(QueryOptions queryOptions); diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/ICreateFileWithStream.cs b/src/Files.App/Utils/Storage/StorageBaseItems/ICreateFileWithStream.cs index 8eed297359fb..2c7fded647de 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/ICreateFileWithStream.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/ICreateFileWithStream.cs @@ -9,8 +9,8 @@ namespace Files.App.Utils.Storage { public interface ICreateFileWithStream { - IAsyncOperation CreateFileAsync(Stream contents, string desiredName); + IAsyncOperation CreateFileAsync(Stream contents, string desiredName); - IAsyncOperation CreateFileAsync(Stream contents, string desiredName, CreationCollisionOption options); + IAsyncOperation CreateFileAsync(Stream contents, string desiredName, CreationCollisionOption options); } } diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs b/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs index 7278d390cd27..dacdbdfd2af8 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs @@ -8,9 +8,9 @@ namespace Files.App.Utils.Storage { public interface IPasswordProtectedItem { - StorageCredential Credentials { get; set; } + StorageCredential? Credentials { get; set; } - Func> PasswordRequestedCallback { get; set; } + Func>? PasswordRequestedCallback { get; set; } async Task RetryWithCredentialsAsync(Func> func, Exception exception) { diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/StorageCredential.cs b/src/Files.App/Utils/Storage/StorageBaseItems/StorageCredential.cs index 8687d7fa078b..7175021fc739 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/StorageCredential.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/StorageCredential.cs @@ -1,6 +1,7 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Security; @@ -10,6 +11,7 @@ namespace Files.App.Utils.Storage public sealed class StorageCredential { private string _userName = string.Empty; + [AllowNull] public string UserName { get => _userName; @@ -17,6 +19,7 @@ public string UserName } private object? _password; + [AllowNull] public string Password { get @@ -36,6 +39,7 @@ public string Password } } + [AllowNull] public SecureString SecurePassword { get diff --git a/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs b/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs index 3a5a55e4460d..3b037da25103 100644 --- a/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs +++ b/src/Files.App/Utils/Storage/StorageItems/BaseQueryResults.cs @@ -58,7 +58,7 @@ public virtual IAsyncOperation> GetItemsAsync() }); } - public virtual StorageItemQueryResult ToStorageItemQueryResult() => null; + public virtual StorageItemQueryResult? ToStorageItemQueryResult() => null; } public class BaseStorageFileQueryResult @@ -110,7 +110,7 @@ public virtual IAsyncOperation> GetFilesAsync() }); } - public virtual StorageFileQueryResult ToStorageFileQueryResult() => null; + public virtual StorageFileQueryResult? ToStorageFileQueryResult() => null; } public class BaseStorageFolderQueryResult @@ -162,14 +162,17 @@ public virtual IAsyncOperation> GetFoldersAsync }); } - public virtual StorageFolderQueryResult ToStorageFolderQueryResult() => null; + public virtual StorageFolderQueryResult? ToStorageFolderQueryResult() => null; } public sealed class SystemStorageItemQueryResult : BaseStorageItemQueryResult { private StorageItemQueryResult StorageItemQueryResult { get; } - public SystemStorageItemQueryResult(StorageItemQueryResult sfqr) : base(sfqr.Folder, sfqr.GetCurrentQueryOptions()) + public SystemStorageItemQueryResult(StorageItemQueryResult sfqr) + : base( + sfqr.Folder is { } folder ? new SystemStorageFolder(folder) : throw new ArgumentException("The query result has no source folder.", nameof(sfqr)), + sfqr.GetCurrentQueryOptions()) { StorageItemQueryResult = sfqr; } @@ -179,7 +182,12 @@ public override IAsyncOperation> GetItemsAsync(uint return AsyncInfo.Run>(async (cancellationToken) => { var items = await StorageItemQueryResult.GetItemsAsync(startIndex, maxNumberOfItems); - return items.Select(x => x is StorageFolder ? (IStorageItem)new SystemStorageFolder(x as StorageFolder) : new SystemStorageFile(x as StorageFile)).ToList(); + return items.Select(x => x switch + { + StorageFolder folder => new SystemStorageFolder(folder), + StorageFile file => new SystemStorageFile(file), + _ => throw new InvalidOperationException("The query returned an unsupported storage item."), + }).ToList(); }); } @@ -188,7 +196,12 @@ public override IAsyncOperation> GetItemsAsync() return AsyncInfo.Run>(async (cancellationToken) => { var items = await StorageItemQueryResult.GetItemsAsync(); - return items.Select(x => x is StorageFolder ? (IStorageItem)new SystemStorageFolder(x as StorageFolder) : new SystemStorageFile(x as StorageFile)).ToList(); + return items.Select(x => x switch + { + StorageFolder folder => new SystemStorageFolder(folder), + StorageFile file => new SystemStorageFile(file), + _ => throw new InvalidOperationException("The query returned an unsupported storage item."), + }).ToList(); }); } @@ -199,7 +212,10 @@ public sealed class SystemStorageFileQueryResult : BaseStorageFileQueryResult { private StorageFileQueryResult StorageFileQueryResult { get; } - public SystemStorageFileQueryResult(StorageFileQueryResult sfqr) : base(sfqr.Folder, sfqr.GetCurrentQueryOptions()) + public SystemStorageFileQueryResult(StorageFileQueryResult sfqr) + : base( + sfqr.Folder is { } folder ? new SystemStorageFolder(folder) : throw new ArgumentException("The query result has no source folder.", nameof(sfqr)), + sfqr.GetCurrentQueryOptions()) { StorageFileQueryResult = sfqr; } @@ -229,7 +245,10 @@ public sealed class SystemStorageFolderQueryResult : BaseStorageFolderQueryResul { private StorageFolderQueryResult StorageFolderQueryResult { get; } - public SystemStorageFolderQueryResult(StorageFolderQueryResult sfqr) : base(sfqr.Folder, sfqr.GetCurrentQueryOptions()) + public SystemStorageFolderQueryResult(StorageFolderQueryResult sfqr) + : base( + sfqr.Folder is { } folder ? new SystemStorageFolder(folder) : throw new ArgumentException("The query result has no source folder.", nameof(sfqr)), + sfqr.GetCurrentQueryOptions()) { StorageFolderQueryResult = sfqr; } diff --git a/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs index ea2b0dc0b711..5df23fc3915e 100644 --- a/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/FtpStorageFile.cs @@ -40,9 +40,9 @@ public override string DisplayType public override Windows.Storage.FileAttributes Attributes { get; } = Windows.Storage.FileAttributes.Normal; public override IStorageItemExtraProperties Properties => new BaseBasicStorageItemExtraProperties(this); - public StorageCredential Credentials { get; set; } + public StorageCredential? Credentials { get; set; } - public Func> PasswordRequestedCallback { get; set; } + public Func>? PasswordRequestedCallback { get; set; } public FtpStorageFile(string path, string name, DateTimeOffset dateCreated) { @@ -65,10 +65,10 @@ public FtpStorageFile(IStorageItemWithPath item) FtpPath = FtpHelpers.GetFtpPath(item.Path); } - public static IAsyncOperation FromPathAsync(string path) + public static IAsyncOperation FromPathAsync(string path) => FtpHelpers.IsFtpPath(path) && FtpHelpers.VerifyFtpPath(path) - ? Task.FromResult(new FtpStorageFile(new StorageFileWithPath(null, path))).AsAsyncOperation() - : Task.FromResult(null).AsAsyncOperation(); + ? Task.FromResult(new FtpStorageFile(new StorageFileWithPath(null, path))).AsAsyncOperation() + : Task.FromResult(null).AsAsyncOperation(); public override IAsyncOperation ToStorageFileAsync() => StorageFile.CreateStreamedFileAsync(Name, FtpDataStreamingHandlerAsync, null); @@ -76,7 +76,7 @@ public override IAsyncOperation ToStorageFileAsync() public override bool IsEqual(IStorageItem item) => item?.Path == Path; public override bool IsOfType(StorageItemTypes type) => type is StorageItemTypes.File; - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); public override IAsyncOperation GetBasicPropertiesAsync() { @@ -100,7 +100,7 @@ public override IAsyncOperation OpenAsync(FileAccessMode ac var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) { - return null; + throw new IOException("Failed to connect to the FTP server."); } if (accessMode is FileAccessMode.Read) @@ -126,7 +126,7 @@ public override IAsyncOperation OpenReadAsyn var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) { - return null; + throw new IOException("Failed to connect to the FTP server."); } var inStream = await ftpClient.OpenRead(FtpPath, token: cancellationToken); @@ -141,7 +141,7 @@ public override IAsyncOperation OpenSequentialReadAsync() var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) { - return null; + throw new IOException("Failed to connect to the FTP server."); } var inStream = await ftpClient.OpenRead(FtpPath, token: cancellationToken); @@ -152,13 +152,13 @@ public override IAsyncOperation OpenSequentialReadAsync() public override IAsyncOperation OpenTransactedWriteAsync() => throw new NotSupportedException(); public override IAsyncOperation OpenTransactedWriteAsync(StorageOpenOptions options) => throw new NotSupportedException(); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) => CopyAsync(destinationFolder, Name, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -166,7 +166,9 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin return null; } - BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder(); + var destFolder = destinationFolder.AsBaseStorageFolder(); + if (destFolder is null) + return null; if (destFolder is ICreateFileWithStream cwsf) { @@ -175,7 +177,10 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin } else { - BaseStorageFile file = await destFolder.CreateFileAsync(desiredNewName, option.Convert()); + var file = await destFolder.CreateFileAsync(desiredNewName, option.Convert()); + if (file is null) + return null; + await using var stream = await file.OpenStreamForWriteAsync(); return await ftpClient.DownloadStream(stream, FtpPath, token: cancellationToken) ? file : null; } @@ -194,7 +199,7 @@ public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string if (!await ftpClient.EnsureConnectedAsync()) throw new IOException($"Failed to connect to FTP server."); - BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder(); + var destFolder = destinationFolder.AsBaseStorageFolder(); if (destFolder is FtpStorageFolder ftpFolder) { @@ -249,12 +254,12 @@ public override IAsyncAction DeleteAsync() } public override IAsyncAction DeleteAsync(StorageDeleteOption option) => DeleteAsync(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) - => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + => Task.FromResult(null).AsAsyncOperation(); private AsyncFtpClient GetFtpClient() { @@ -313,4 +318,4 @@ public FtpFileBasicProperties(FtpListItem item) } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/StorageItems/FtpStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/FtpStorageFolder.cs index 48ddd3d47394..1630a83fdc08 100644 --- a/src/Files.App/Utils/Storage/StorageItems/FtpStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/FtpStorageFolder.cs @@ -25,9 +25,9 @@ public sealed partial class FtpStorageFolder : BaseStorageFolder, IPasswordProte public override Windows.Storage.FileAttributes Attributes { get; } = Windows.Storage.FileAttributes.Directory; public override IStorageItemExtraProperties Properties => new BaseBasicStorageItemExtraProperties(this); - public StorageCredential Credentials { get; set; } + public StorageCredential? Credentials { get; set; } - public Func> PasswordRequestedCallback { get; set; } + public Func>? PasswordRequestedCallback { get; set; } public FtpStorageFolder(string path, string name, DateTimeOffset dateCreated) { @@ -50,10 +50,10 @@ public FtpStorageFolder(IStorageItemWithPath item) FtpPath = FtpHelpers.GetFtpPath(item.Path); } - public static IAsyncOperation FromPathAsync(string path) + public static IAsyncOperation FromPathAsync(string path) => FtpHelpers.IsFtpPath(path) && FtpHelpers.VerifyFtpPath(path) - ? Task.FromResult(new FtpStorageFolder(new StorageFolderWithPath(null, path))).AsAsyncOperation() - : Task.FromResult(null).AsAsyncOperation(); + ? Task.FromResult(new FtpStorageFolder(new StorageFolderWithPath(null, path))).AsAsyncOperation() + : Task.FromResult(null).AsAsyncOperation(); public override IAsyncOperation ToStorageFolderAsync() => throw new NotSupportedException(); @@ -64,7 +64,7 @@ public static IAsyncOperation FromPathAsync(string path) public override IAsyncOperation GetIndexedStateAsync() => Task.FromResult(IndexedState.NotIndexed).AsAsyncOperation(); - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); public override IAsyncOperation GetBasicPropertiesAsync() { @@ -81,9 +81,9 @@ public override IAsyncOperation GetBasicPropertiesAsync() }, (_, _) => Task.FromResult(new BaseBasicProperties()))); } - public override IAsyncOperation GetItemAsync(string name) + public override IAsyncOperation GetItemAsync(string name) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -110,7 +110,7 @@ public override IAsyncOperation GetItemAsync(string name) return null; }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } - public override IAsyncOperation TryGetItemAsync(string name) + public override IAsyncOperation TryGetItemAsync(string name) { return AsyncInfo.Run(async (cancellationToken) => { @@ -131,7 +131,7 @@ public override IAsyncOperation> GetItemsAsync() using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) { - return null; + return []; } var items = new List(); @@ -158,31 +158,31 @@ public override IAsyncOperation> GetItemsAsync(uint => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList()); - public override IAsyncOperation GetFileAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFile); + public override IAsyncOperation GetFileAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFile); public override IAsyncOperation> GetFilesAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFilesAsync()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query, uint startIndex, uint maxItemsToRetrieve) => AsyncInfo.Run>(async (cancellationToken) => (await GetFilesAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList()); - public override IAsyncOperation GetFolderAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFolder); + public override IAsyncOperation GetFolderAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFolder); public override IAsyncOperation> GetFoldersAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFoldersAsync()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query, uint startIndex, uint maxItemsToRetrieve) => AsyncInfo.Run>(async (cancellationToken) => (await GetFoldersAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList()); - public override IAsyncOperation CreateFileAsync(string desiredName) + public override IAsyncOperation CreateFileAsync(string desiredName) => CreateFileAsync(desiredName, CreationCollisionOption.FailIfExists); - public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -229,11 +229,11 @@ public override IAsyncOperation CreateFileAsync(string desiredN }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } - public override IAsyncOperation CreateFolderAsync(string desiredName) + public override IAsyncOperation CreateFolderAsync(string desiredName) => CreateFolderAsync(desiredName, CreationCollisionOption.FailIfExists); - public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) @@ -262,17 +262,17 @@ public override IAsyncOperation CreateFolderAsync(string desi }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => MoveAsync(destinationFolder, NameCollisionOption.FailIfExists); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using var ftpClient = GetFtpClient(); if (!await ftpClient.EnsureConnectedAsync()) throw new IOException($"Failed to connect to FTP server."); - BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder(); + var destFolder = destinationFolder.AsBaseStorageFolder(); if (destFolder is FtpStorageFolder ftpFolder) { @@ -342,12 +342,12 @@ public override IAsyncAction DeleteAsync() public override StorageFolderQueryResult CreateFolderQuery(CommonFolderQuery query) => throw new NotSupportedException(); public override BaseStorageFolderQueryResult CreateFolderQueryWithOptions(QueryOptions queryOptions) => new(this, queryOptions); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) - => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + => Task.FromResult(null).AsAsyncOperation(); private AsyncFtpClient GetFtpClient() { @@ -376,4 +376,4 @@ public FtpFolderBasicProperties(FtpListItem item) } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.cs index 83c5e8f829d6..4669e0291394 100644 --- a/src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/NativeStorageFile.cs @@ -96,15 +96,15 @@ public NativeStorageFile(string path, string name, DateTimeOffset dateCreated) public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) => throw new NotSupportedException(); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) => CopyAsync(destinationFolder, Name, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run(async (cancellationToken) => { if (string.IsNullOrEmpty(destinationFolder.Path)) { @@ -169,26 +169,26 @@ public override IAsyncOperation GetBasicPropertiesAsync() return Task.FromResult(new BaseBasicProperties()).AsAsyncOperation(); } - public override IAsyncOperation GetParentAsync() + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) => throw new NotSupportedException(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) => throw new NotSupportedException(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) => throw new NotSupportedException(); - public static IAsyncOperation FromPathAsync(string path) + public static IAsyncOperation FromPathAsync(string path) { if (IsNativePath(path) && CheckAccess(path)) { var name = IO.Path.GetFileName(path); - return Task.FromResult((BaseStorageFile)new NativeStorageFile(path, name[(name.LastIndexOf(':') + 1)..], DateTime.Now)).AsAsyncOperation(); + return Task.FromResult(new NativeStorageFile(path, name[(name.LastIndexOf(':') + 1)..], DateTime.Now)).AsAsyncOperation(); } - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } private static bool CheckAccess(string path) @@ -294,7 +294,8 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption { return AsyncInfo.Run(async (cancellationToken) => { - string destination = IO.Path.Combine(IO.Path.GetDirectoryName(Path), desiredName); + var parentPath = IO.Path.GetDirectoryName(Path) ?? throw new InvalidOperationException("The file path has no parent."); + string destination = IO.Path.Combine(parentPath, desiredName); var destFile = new NativeStorageFile(destination, desiredName, DateTime.Now); if (!IsAlternateStream) { diff --git a/src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs index 1ef3e7343109..05b0b3e0a834 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs @@ -56,7 +56,7 @@ public partial class ShellStorageFile : BaseStorageFile public override FileAttributes Attributes => FileAttributes.Normal | FileAttributes.ReadOnly; - private IStorageItemExtraProperties properties; + private IStorageItemExtraProperties? properties; public override IStorageItemExtraProperties Properties => properties ??= new BaseBasicStorageItemExtraProperties(this); public ShellStorageFile(ShellFileItem item) @@ -80,11 +80,11 @@ public static ShellStorageFile FromShellItem(ShellFileItem item) return new ShellStorageFile(item); } - public static IAsyncOperation FromPathAsync(string path) + public static IAsyncOperation FromPathAsync(string path) { if (ShellStorageFolder.IsShellPath(path) && GetFile(path) is ShellFileItem file) - return Task.FromResult(FromShellItem(file)).AsAsyncOperation(); - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(FromShellItem(file)).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } private static ShellFileItem? GetFile(string path) @@ -104,18 +104,18 @@ public static IAsyncOperation FromPathAsync(string path) public override bool IsEqual(IStorageItem item) => item?.Path == Path; public override bool IsOfType(StorageItemTypes type) => type is StorageItemTypes.File; - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); public override IAsyncOperation GetBasicPropertiesAsync() => GetBasicProperties().AsAsyncOperation(); public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) => throw new NotSupportedException(); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) => throw new NotSupportedException(); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) => throw new NotSupportedException(); + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) => throw new NotSupportedException(); + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) => throw new NotSupportedException(); public override IAsyncAction DeleteAsync() => throw new NotSupportedException(); public override IAsyncAction DeleteAsync(StorageDeleteOption option) => throw new NotSupportedException(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) { return AsyncInfo.Run(async (cancellationToken) => { @@ -127,7 +127,7 @@ public override IAsyncOperation GetThumbnailAsync(Thumbnai return await zipFile.GetThumbnailAsync(mode); }); } - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) { return AsyncInfo.Run(async (cancellationToken) => { @@ -139,7 +139,7 @@ public override IAsyncOperation GetThumbnailAsync(Thumbnai return await zipFile.GetThumbnailAsync(mode, requestedSize); }); } - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) { return AsyncInfo.Run(async (cancellationToken) => { diff --git a/src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs index 4189cdac8474..bf9f0cf8b861 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs @@ -74,12 +74,12 @@ public ShellStorageFolder(ShellFileItem item) DisplayType = item.FileType; } - public static bool IsShellPath(string path) + public static bool IsShellPath(string? path) { return path is not null && - path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) || + (path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) || path.StartsWith("::{", StringComparison.Ordinal) || - path.StartsWith(@"\\SHELL\", StringComparison.Ordinal); + path.StartsWith(@"\\SHELL\", StringComparison.Ordinal)); } public static ShellStorageFolder FromShellItem(ShellFileItem item) @@ -93,9 +93,9 @@ public static ShellStorageFolder FromShellItem(ShellFileItem item) return new ShellStorageFolder(item); } - public static IAsyncOperation FromPathAsync(string path) + public static IAsyncOperation FromPathAsync(string path) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run(async (cancellationToken) => { if (IsShellPath(path)) { @@ -109,7 +109,7 @@ public static IAsyncOperation FromPathAsync(string path) }); } - protected static async Task<(ShellFileItem Folder, List Items)> GetFolderAndItems(string path, bool enumerate, int startIndex = 0, int maxItemsToRetrieve = int.MaxValue) + protected static async Task<(ShellFileItem? Folder, List? Items)> GetFolderAndItems(string path, bool enumerate, int startIndex = 0, int maxItemsToRetrieve = int.MaxValue) { return await Win32Helper.GetShellFolderAsync(path, !enumerate, enumerate, startIndex, maxItemsToRetrieve); } @@ -121,7 +121,7 @@ public static IAsyncOperation FromPathAsync(string path) public override IAsyncOperation GetIndexedStateAsync() => Task.FromResult(IndexedState.NotIndexed).AsAsyncOperation(); - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); public override IAsyncOperation GetBasicPropertiesAsync() { @@ -132,9 +132,9 @@ public override IAsyncOperation GetBasicPropertiesAsync() }); } - public override IAsyncOperation GetItemAsync(string name) + public override IAsyncOperation GetItemAsync(string name) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run(async (cancellationToken) => { var res = await GetFolderAndItems(Path, true); if (res.Items is null) @@ -156,7 +156,7 @@ public override IAsyncOperation GetItemAsync(string name) return ShellStorageFile.FromShellItem(entry); }); } - public override IAsyncOperation TryGetItemAsync(string name) + public override IAsyncOperation TryGetItemAsync(string name) { return AsyncInfo.Run(async (cancellationToken) => { @@ -181,7 +181,7 @@ public override IAsyncOperation> GetItemsAsync(uint var res = await GetFolderAndItems(Path, true, (int)startIndex, (int)maxItemsToRetrieve); if (res.Items is null) { - return null; + return []; } var items = new List(); @@ -200,10 +200,10 @@ public override IAsyncOperation> GetItemsAsync(uint }); } - public override IAsyncOperation GetFileAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ShellStorageFile); + public override IAsyncOperation GetFileAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ShellStorageFile); public override IAsyncOperation> GetFilesAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFilesAsync()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query, uint startIndex, uint maxItemsToRetrieve) @@ -211,10 +211,10 @@ public override IAsyncOperation> GetFilesAsync(Co => (await GetFilesAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList() ); - public override IAsyncOperation GetFolderAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ShellStorageFolder); + public override IAsyncOperation GetFolderAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ShellStorageFolder); public override IAsyncOperation> GetFoldersAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFoldersAsync()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query, uint startIndex, uint maxItemsToRetrieve) @@ -226,16 +226,16 @@ public override IAsyncOperation> GetFoldersAsyn }); } - public override IAsyncOperation CreateFileAsync(string desiredName) => throw new NotSupportedException(); - public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFileAsync(string desiredName) => throw new NotSupportedException(); + public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) => throw new NotSupportedException(); - public override IAsyncOperation CreateFolderAsync(string desiredName) => throw new NotSupportedException(); - public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFolderAsync(string desiredName) => throw new NotSupportedException(); + public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) => throw new NotSupportedException(); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); public override IAsyncAction RenameAsync(string desiredName) => throw new NotSupportedException(); public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) => throw new NotSupportedException(); @@ -258,7 +258,7 @@ public override IAsyncOperation CreateFolderAsync(string desi public override StorageFolderQueryResult CreateFolderQuery(CommonFolderQuery query) => throw new NotSupportedException(); public override BaseStorageFolderQueryResult CreateFolderQueryWithOptions(QueryOptions queryOptions) => new(this, queryOptions); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) { return AsyncInfo.Run(async (cancellationToken) => { @@ -270,7 +270,7 @@ public override IAsyncOperation GetThumbnailAsync(Thumbnai return await zipFolder.GetThumbnailAsync(mode); }); } - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) { return AsyncInfo.Run(async (cancellationToken) => { @@ -282,7 +282,7 @@ public override IAsyncOperation GetThumbnailAsync(Thumbnai return await zipFolder.GetThumbnailAsync(mode, requestedSize); }); } - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) { return AsyncInfo.Run(async (cancellationToken) => { diff --git a/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs b/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs index e12d5f143a25..c7f6261487af 100644 --- a/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs +++ b/src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs @@ -14,7 +14,7 @@ public sealed partial class InputStreamWithDisposeCallback : IInputStream { private Stream stream; private IInputStream iStream; - public Action DisposeCallback { get; set; } + public Action? DisposeCallback { get; set; } public InputStreamWithDisposeCallback(Stream stream) { @@ -43,7 +43,7 @@ public sealed partial class NonSeekableRandomAccessStreamForWrite : IRandomAcces private ulong byteSize; private bool isWritten; - public Action DisposeCallback { get; set; } + public Action? DisposeCallback { get; set; } public NonSeekableRandomAccessStreamForWrite(Stream stream) { @@ -139,7 +139,7 @@ public sealed partial class NonSeekableRandomAccessStreamForRead : IRandomAccess private ulong readToByte; private ulong byteSize; - public Action DisposeCallback { get; set; } + public Action? DisposeCallback { get; set; } public NonSeekableRandomAccessStreamForRead(Stream baseStream, ulong size) { diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs index c0eb809d0c39..1bbff8e3ad43 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFile.cs @@ -16,22 +16,26 @@ public sealed partial class SystemStorageFile : BaseStorageFile { public StorageFile File { get; } - public override string Path => File?.Path; - public override string Name => File?.Name; - public override string DisplayName => File?.DisplayName; + public override string Path => File.Path; + public override string Name => File.Name; + public override string DisplayName => File.DisplayName; public override string ContentType => File.ContentType; - public override string DisplayType => File?.DisplayType; + public override string DisplayType => File.DisplayType; public override string FileType => File.FileType; - public override string FolderRelativeId => File?.FolderRelativeId; + public override string FolderRelativeId => File.FolderRelativeId; public override DateTimeOffset DateCreated => File.DateCreated; public override Windows.Storage.FileAttributes Attributes => File.Attributes; - public override IStorageItemExtraProperties Properties => File?.Properties; + public override IStorageItemExtraProperties Properties => File.Properties; - public SystemStorageFile(StorageFile file) => File = file; + public SystemStorageFile(StorageFile file) + { + ArgumentNullException.ThrowIfNull(file); + File = file; + } - public static IAsyncOperation FromPathAsync(string path) - => AsyncInfo.Run(async (cancellationToken) => + public static IAsyncOperation FromPathAsync(string path) + => AsyncInfo.Run(async (cancellationToken) => { try { @@ -54,25 +58,28 @@ public override IAsyncOperation ToStorageFileAsync() public override bool IsEqual(IStorageItem item) => File.IsEqual(item); public override bool IsOfType(StorageItemTypes type) => File.IsOfType(type); - public override IAsyncOperation GetParentAsync() - => AsyncInfo.Run(async (cancellationToken) - => new SystemStorageFolder(await File.GetParentAsync()) - ); + public override IAsyncOperation GetParentAsync() + => AsyncInfo.Run(async (cancellationToken) => + { + var parent = await File.GetParentAsync(); + return parent is not null ? new SystemStorageFolder(parent) : null; + }); public override IAsyncOperation GetBasicPropertiesAsync() => AsyncInfo.Run(async (cancellationToken) => new SystemFileBasicProperties(await File.GetBasicPropertiesAsync(), DateCreated) ); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) => CopyAsync(destinationFolder, Name, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run(async (cancellationToken) => { - var destFolder = destinationFolder.AsBaseStorageFolder(); // Avoid calling IStorageFolder method + var destFolder = destinationFolder.AsBaseStorageFolder() ?? + throw new NotSupportedException("The destination folder type is not supported."); // Avoid calling IStorageFolder method try { if (destFolder is SystemStorageFolder sysFolder) @@ -88,6 +95,9 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin else { var destFile = await destFolder.CreateFileAsync(desiredNewName, option.Convert()); + if (destFile is null) + return null; + await using (var inStream = await this.OpenStreamForReadAsync()) await using (var outStream = await destFile.OpenStreamForWriteAsync()) { @@ -179,11 +189,11 @@ public override IAsyncAction MoveAndReplaceAsync(IStorageFile fileToReplace) public override IAsyncAction DeleteAsync() => File.DeleteAsync(); public override IAsyncAction DeleteAsync(StorageDeleteOption option) => File.DeleteAsync(option); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) => File.GetThumbnailAsync(mode); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) => File.GetThumbnailAsync(mode, requestedSize); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) => File.GetThumbnailAsync(mode, requestedSize, options); private sealed partial class SystemFileBasicProperties : BaseBasicProperties diff --git a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs index 9d7e6f2fa194..492749cef034 100644 --- a/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/SystemStorageFolder.cs @@ -26,7 +26,11 @@ public sealed partial class SystemStorageFolder : BaseStorageFolder public override FileAttributes Attributes => Folder.Attributes; public override IStorageItemExtraProperties Properties => Folder.Properties; - public SystemStorageFolder(StorageFolder folder) => Folder = folder; + public SystemStorageFolder(StorageFolder folder) + { + ArgumentNullException.ThrowIfNull(folder); + Folder = folder; + } public static IAsyncOperation FromPathAsync(string path) { @@ -34,7 +38,11 @@ public static IAsyncOperation FromPathAsync(string path) { try { - using var shellItem = new ShellLibraryEx(Shell32.ShellUtil.GetShellItemForPath(path), true); + var libraryShellItem = Shell32.ShellUtil.GetShellItemForPath(path); + if (libraryShellItem is null) + throw new System.IO.FileNotFoundException($"The library '{path}' was not found."); + + using var shellItem = new ShellLibraryEx(libraryShellItem, true); if (shellItem is ShellLibraryEx library) { var libraryItem = ShellFolderExtensions.GetShellLibraryItem(library, path); @@ -60,24 +68,28 @@ public static IAsyncOperation FromPathAsync(string path) public override bool IsEqual(IStorageItem item) => Folder.IsEqual(item); public override bool IsOfType(StorageItemTypes type) => Folder.IsOfType(type); - public override IAsyncOperation GetParentAsync() - => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await Folder.GetParentAsync())); + public override IAsyncOperation GetParentAsync() + => AsyncInfo.Run(async (cancellationToken) => + { + var parent = await Folder.GetParentAsync(); + return parent is not null ? new SystemStorageFolder(parent) : null; + }); public override IAsyncOperation GetBasicPropertiesAsync() => AsyncInfo.Run(async (cancellationToken) => new SystemFolderBasicProperties(await Folder.GetBasicPropertiesAsync(), DateCreated)); public override IAsyncOperation GetIndexedStateAsync() => Folder.GetIndexedStateAsync(); - public override IAsyncOperation GetItemAsync(string name) - => Folder.GetItemAsync(name); - public override IAsyncOperation TryGetItemAsync(string name) + public override IAsyncOperation GetItemAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await Folder.GetItemAsync(name)); + public override IAsyncOperation TryGetItemAsync(string name) => Folder.TryGetItemAsync(name); public override IAsyncOperation> GetItemsAsync() => Folder.GetItemsAsync(); public override IAsyncOperation> GetItemsAsync(uint startIndex, uint maxItemsToRetrieve) => Folder.GetItemsAsync(startIndex, maxItemsToRetrieve); - public override IAsyncOperation GetFileAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFile(await Folder.GetFileAsync(name))); + public override IAsyncOperation GetFileAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFile(await Folder.GetFileAsync(name))); public override IAsyncOperation> GetFilesAsync() => AsyncInfo.Run>(async (cancellationToken) => (await Folder.GetFilesAsync()).Select(item => new SystemStorageFile(item)).ToList() @@ -89,8 +101,8 @@ public override IAsyncOperation> GetFilesAsync(Co => AsyncInfo.Run>(async (cancellationToken) => (await Folder.GetFilesAsync(query, startIndex, maxItemsToRetrieve)).Select(x => new SystemStorageFile(x)).ToList()); - public override IAsyncOperation GetFolderAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await Folder.GetFolderAsync(name))); + public override IAsyncOperation GetFolderAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await Folder.GetFolderAsync(name))); public override IAsyncOperation> GetFoldersAsync() => AsyncInfo.Run>(async (cancellationToken) => (await Folder.GetFoldersAsync()).Select(item => new SystemStorageFolder(item)).ToList() @@ -102,18 +114,18 @@ public override IAsyncOperation> GetFoldersAsyn => AsyncInfo.Run>(async (cancellationToken) => (await Folder.GetFoldersAsync(query, startIndex, maxItemsToRetrieve)).Select(x => new SystemStorageFolder(x)).ToList()); - public override IAsyncOperation CreateFileAsync(string desiredName) - => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFile(await Folder.CreateFileAsync(desiredName))); - public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) - => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFile(await Folder.CreateFileAsync(desiredName, options))); + public override IAsyncOperation CreateFileAsync(string desiredName) + => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFile(await Folder.CreateFileAsync(desiredName))); + public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) + => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFile(await Folder.CreateFileAsync(desiredName, options))); - public override IAsyncOperation CreateFolderAsync(string desiredName) - => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await Folder.CreateFolderAsync(desiredName))); - public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) - => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await Folder.CreateFolderAsync(desiredName, options))); + public override IAsyncOperation CreateFolderAsync(string desiredName) + => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await Folder.CreateFolderAsync(desiredName))); + public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) + => AsyncInfo.Run(async (cancellationToken) => new SystemStorageFolder(await Folder.CreateFolderAsync(desiredName, options))); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); public override IAsyncAction RenameAsync(string desiredName) => Folder.RenameAsync(desiredName); public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) => Folder.RenameAsync(desiredName, option); @@ -144,11 +156,11 @@ public override StorageFolderQueryResult CreateFolderQuery(CommonFolderQuery que public override BaseStorageFolderQueryResult CreateFolderQueryWithOptions(QueryOptions queryOptions) => new SystemStorageFolderQueryResult(Folder.CreateFolderQueryWithOptions(queryOptions)); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) => Folder.GetThumbnailAsync(mode); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) => Folder.GetThumbnailAsync(mode, requestedSize); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) => Folder.GetThumbnailAsync(mode, requestedSize, options); private sealed partial class SystemFolderBasicProperties : BaseBasicProperties diff --git a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.cs index a9602339917b..fe8484a3ce7c 100644 --- a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFile.cs @@ -83,7 +83,7 @@ public override IAsyncOperation ToStorageFileAsync() public override bool IsEqual(IStorageItem item) => item?.Path == Path; - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); public override IAsyncOperation OpenAsync(FileAccessMode accessMode) { @@ -106,16 +106,18 @@ public override IAsyncOperation OpenSequentialReadAsync() public override IAsyncOperation OpenTransactedWriteAsync() => throw new NotSupportedException(); public override IAsyncOperation OpenTransactedWriteAsync(StorageOpenOptions options) => throw new NotSupportedException(); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) => CopyAsync(destinationFolder, Name, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { - return AsyncInfo.Run(async (cancellationToken) => + return AsyncInfo.Run(async (cancellationToken) => { - BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder(); + var destFolder = destinationFolder.AsBaseStorageFolder(); + if (destFolder is null) + return null; if (destFolder is ICreateFileWithStream cwsf) { @@ -125,6 +127,9 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin else { var destFile = await destFolder.CreateFileAsync(desiredNewName, option.Convert()); + if (destFile is null) + return null; + await using (var inStream = await this.OpenStreamForReadAsync()) await using (var outStream = await destFile.OpenStreamForWriteAsync()) { @@ -153,11 +158,11 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption public override IAsyncAction DeleteAsync(StorageDeleteOption option) => throw new NotSupportedException(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) - => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + => Task.FromResult(null).AsAsyncOperation(); } } diff --git a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.cs index d2c1b1cbc088..8744859a5d3e 100644 --- a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageFolder.cs @@ -34,20 +34,20 @@ public VirtualStorageFolder(string cFileName) public override IAsyncOperation GetIndexedStateAsync() => Task.FromResult(IndexedState.NotIndexed).AsAsyncOperation(); - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); public override IAsyncOperation GetBasicPropertiesAsync() { return Task.FromResult(new BaseBasicProperties()).AsAsyncOperation(); } - public override IAsyncOperation GetItemAsync(string name) + public override IAsyncOperation GetItemAsync(string name) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } - public override IAsyncOperation TryGetItemAsync(string name) + public override IAsyncOperation TryGetItemAsync(string name) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } public override IAsyncOperation> GetItemsAsync() { @@ -58,37 +58,37 @@ public override IAsyncOperation> GetItemsAsync(uint => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList()); - public override IAsyncOperation GetFileAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFile); + public override IAsyncOperation GetFileAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFile); public override IAsyncOperation> GetFilesAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFilesAsync()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query, uint startIndex, uint maxItemsToRetrieve) => AsyncInfo.Run>(async (cancellationToken) => (await GetFilesAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList()); - public override IAsyncOperation GetFolderAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFolder); + public override IAsyncOperation GetFolderAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as BaseStorageFolder); public override IAsyncOperation> GetFoldersAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFoldersAsync()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query, uint startIndex, uint maxItemsToRetrieve) => AsyncInfo.Run>(async (cancellationToken) => (await GetFoldersAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList()); - public override IAsyncOperation CreateFileAsync(string desiredName) + public override IAsyncOperation CreateFileAsync(string desiredName) => CreateFileAsync(desiredName, CreationCollisionOption.FailIfExists); - public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) => throw new NotSupportedException(); - public override IAsyncOperation CreateFolderAsync(string desiredName) + public override IAsyncOperation CreateFolderAsync(string desiredName) => CreateFolderAsync(desiredName, CreationCollisionOption.FailIfExists); - public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) => throw new NotSupportedException(); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); public override IAsyncAction RenameAsync(string desiredName) => RenameAsync(desiredName, NameCollisionOption.FailIfExists); @@ -114,11 +114,11 @@ public override IAsyncAction DeleteAsync() public override StorageFolderQueryResult CreateFolderQuery(CommonFolderQuery query) => throw new NotSupportedException(); public override BaseStorageFolderQueryResult CreateFolderQueryWithOptions(QueryOptions queryOptions) => new(this, queryOptions); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) - => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + => Task.FromResult(null).AsAsyncOperation(); } -} \ No newline at end of file +} diff --git a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs index 4efdfcc6699e..d378096c9242 100644 --- a/src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs +++ b/src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs @@ -16,15 +16,15 @@ namespace Files.App.Utils.Storage /// public sealed partial class VirtualStorageItem : IStorageItem { - private static BasicProperties props; + private static BasicProperties? props; public Windows.Storage.FileAttributes Attributes { get; init; } public DateTimeOffset DateCreated { get; init; } - public string Name { get; init; } + public required string Name { get; init; } - public string Path { get; init; } + public required string Path { get; init; } private VirtualStorageItem() { } @@ -39,7 +39,7 @@ public static VirtualStorageItem FromListedItem(ListedItem item) }; } - public static VirtualStorageItem FromPath(string path) + public static VirtualStorageItem? FromPath(string path) { Win32PInvoke.FINDEX_INFO_LEVELS findInfoLevel = Win32PInvoke.FINDEX_INFO_LEVELS.FindExInfoBasic; int additionalFlags = Win32PInvoke.FIND_FIRST_EX_LARGE_FETCH; diff --git a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs index 373adc2e62c6..c3f73f5cc55b 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs @@ -3,6 +3,7 @@ using Files.Shared.Helpers; using SevenZip; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Foundation; @@ -17,7 +18,7 @@ namespace Files.App.Utils.Storage public sealed partial class ZipStorageFile : BaseStorageFile, IPasswordProtectedItem { private readonly string containerPath; - private readonly BaseStorageFile backingFile; + private readonly BaseStorageFile? backingFile; public override string Path { get; } public override string Name { get; } @@ -42,12 +43,19 @@ public override string DisplayType public override DateTimeOffset DateCreated { get; } public override Windows.Storage.FileAttributes Attributes => Windows.Storage.FileAttributes.Normal | Windows.Storage.FileAttributes.ReadOnly; - private IStorageItemExtraProperties properties; + private IStorageItemExtraProperties? properties; public override IStorageItemExtraProperties Properties => properties ??= new BaseBasicStorageItemExtraProperties(this); - public StorageCredential Credentials { get; set; } = new(); + private StorageCredential credentials = new(); - public Func> PasswordRequestedCallback { get; set; } + [AllowNull] + public StorageCredential Credentials + { + get => credentials; + set => credentials = value ?? new(); + } + + public Func>? PasswordRequestedCallback { get; set; } public ZipStorageFile(string path, string containerPath) { @@ -55,21 +63,21 @@ public ZipStorageFile(string path, string containerPath) Path = path; this.containerPath = containerPath; } - public ZipStorageFile(string path, string containerPath, BaseStorageFile backingFile) : this(path, containerPath) + public ZipStorageFile(string path, string containerPath, BaseStorageFile? backingFile) : this(path, containerPath) => this.backingFile = backingFile; public ZipStorageFile(string path, string containerPath, ArchiveFileInfo entry) : this(path, containerPath) => DateCreated = entry.CreationTime == DateTime.MinValue ? DateTimeOffset.MinValue : entry.CreationTime; - public ZipStorageFile(string path, string containerPath, ArchiveFileInfo entry, BaseStorageFile backingFile) : this(path, containerPath, entry) + public ZipStorageFile(string path, string containerPath, ArchiveFileInfo entry, BaseStorageFile? backingFile) : this(path, containerPath, entry) => this.backingFile = backingFile; public override IAsyncOperation ToStorageFileAsync() => StorageFile.CreateStreamedFileAsync(Name, ZipDataStreamingHandler(Path), null); - public static IAsyncOperation FromPathAsync(string path) + public static IAsyncOperation FromPathAsync(string path) { if (!FileExtensionHelpers.IsBrowsableZipFile(path, out var ext)) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } var marker = path.IndexOf(ext, StringComparison.OrdinalIgnoreCase); if (marker is not -1) @@ -77,20 +85,20 @@ public static IAsyncOperation FromPathAsync(string path) var containerPath = path.Substring(0, marker + ext.Length); if (path == containerPath) { - return Task.FromResult(null).AsAsyncOperation(); // Root + return Task.FromResult(null).AsAsyncOperation(); // Root } if (CheckAccess(containerPath)) { - return Task.FromResult(new ZipStorageFile(path, containerPath)).AsAsyncOperation(); + return Task.FromResult(new ZipStorageFile(path, containerPath)).AsAsyncOperation(); } } - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } public override bool IsEqual(IStorageItem item) => item?.Path == Path; public override bool IsOfType(StorageItemTypes type) => type is StorageItemTypes.File; - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); public override IAsyncOperation GetBasicPropertiesAsync() => GetBasicProperties().AsAsyncOperation(); public override IAsyncOperation OpenAsync(FileAccessMode accessMode) @@ -106,7 +114,10 @@ public override IAsyncOperation OpenAsync(FileAccessMode ac } var file = Win32Helper.OpenFileForRead(containerPath, rw); - return file.IsInvalid ? null : new FileStream(file, rw ? FileAccess.ReadWrite : FileAccess.Read).AsRandomAccessStream(); + if (file.IsInvalid) + throw new IOException($"The archive '{containerPath}' could not be opened."); + + return new FileStream(file, rw ? FileAccess.ReadWrite : FileAccess.Read).AsRandomAccessStream(); } if (!rw) @@ -114,7 +125,7 @@ public override IAsyncOperation OpenAsync(FileAccessMode ac SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) { - return null; + throw new InvalidDataException($"The archive '{containerPath}' could not be read."); } //zipFile.IsStreamOwner = true; @@ -130,7 +141,7 @@ public override IAsyncOperation OpenAsync(FileAccessMode ac DisposeCallback = () => zipFile.Dispose() }; } - return null; + throw new FileNotFoundException($"The archive entry '{Path}' was not found."); } throw new NotSupportedException("Can't open zip file as RW"); @@ -151,20 +162,23 @@ public override IAsyncOperation OpenReadAsyn } var hFile = Win32Helper.OpenFileForRead(containerPath); - return hFile.IsInvalid ? null : new StreamWithContentType(new FileStream(hFile, FileAccess.Read).AsRandomAccessStream()); + if (hFile.IsInvalid) + throw new IOException($"The archive '{containerPath}' could not be opened."); + + return new StreamWithContentType(new FileStream(hFile, FileAccess.Read).AsRandomAccessStream()); } SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) { - return null; + throw new InvalidDataException($"The archive '{containerPath}' could not be read."); } //zipFile.IsStreamOwner = true; var entry = zipFile.GetArchiveFileData(containerPath).FirstOrDefault(x => System.IO.Path.Combine(containerPath, x.FileName) == Path); if (entry.FileName is null) { - return null; + throw new FileNotFoundException($"The archive entry '{Path}' was not found."); } var ms = new MemoryStream(); @@ -190,19 +204,22 @@ public override IAsyncOperation OpenSequentialReadAsync() } var hFile = Win32Helper.OpenFileForRead(containerPath); - return hFile.IsInvalid ? null : new FileStream(hFile, FileAccess.Read).AsInputStream(); + if (hFile.IsInvalid) + throw new IOException($"The archive '{containerPath}' could not be opened."); + + return new FileStream(hFile, FileAccess.Read).AsInputStream(); } SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) { - return null; + throw new InvalidDataException($"The archive '{containerPath}' could not be read."); } //zipFile.IsStreamOwner = true; var entry = zipFile.GetArchiveFileData(containerPath).FirstOrDefault(x => System.IO.Path.Combine(containerPath, x.FileName) == Path); if (entry.FileName is null) { - return null; + throw new FileNotFoundException($"The archive entry '{Path}' was not found."); } var ms = new MemoryStream(); @@ -220,13 +237,13 @@ public override IAsyncOperation OpenTransactedWriteAsy public override IAsyncOperation OpenTransactedWriteAsync(StorageOpenOptions options) => throw new NotSupportedException(); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder) => CopyAsync(destinationFolder, Name, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName) => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); - public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) + public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) @@ -241,7 +258,8 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin return null; } - var destFolder = destinationFolder.AsBaseStorageFolder(); + if (destinationFolder.AsBaseStorageFolder() is not { } destFolder) + return null; if (destFolder is ICreateFileWithStream cwsf) { @@ -254,6 +272,9 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin else { var destFile = await destFolder.CreateFileAsync(desiredNewName, option.Convert()); + if (destFile is null) + return null; + await using var outStream = await destFile.OpenStreamForWriteAsync(); await SafetyExtensions.WrapAsync(() => zipFile.ExtractFileAsync(entry.Index, outStream), async (_, exception) => { @@ -310,7 +331,8 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption } else { - var fileName = IO.Path.Combine(IO.Path.GetDirectoryName(Path), desiredName); + var parentPath = IO.Path.GetDirectoryName(Path) ?? throw new InvalidOperationException("The archive path has no parent."); + var fileName = IO.Path.Combine(parentPath, desiredName); PInvoke.MoveFileFromApp(Path, fileName); } } @@ -328,7 +350,8 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); - var fileName = IO.Path.GetRelativePath(containerPath, IO.Path.Combine(IO.Path.GetDirectoryName(Path), desiredName)); + var parentPath = IO.Path.GetDirectoryName(Path) ?? throw new InvalidOperationException("The archive entry has no parent."); + var fileName = IO.Path.GetRelativePath(containerPath, IO.Path.Combine(parentPath, desiredName)); await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, fileName } }, Credentials.Password, ms); } @@ -378,7 +401,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); - await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, null } }, Credentials.Password, ms); + await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, null } }, Credentials.Password, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -392,12 +415,12 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) - => Task.FromResult(null).AsAsyncOperation(); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) - => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + => Task.FromResult(null).AsAsyncOperation(); + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + => Task.FromResult(null).AsAsyncOperation(); private static bool CheckAccess(string path) { @@ -447,7 +470,7 @@ private async Task GetBasicProperties() using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) { - return null; + return new BaseBasicProperties(); } //zipFile.IsStreamOwner = true; @@ -463,7 +486,7 @@ private IAsyncOperation OpenZipFileAsync() return AsyncInfo.Run(async (cancellationToken) => { var zipFile = await OpenZipFileAsync(FileAccessMode.Read); - return zipFile is not null ? new SevenZipExtractor(zipFile, Credentials.Password) : null; + return new SevenZipExtractor(zipFile, Credentials.Password); }); } @@ -481,7 +504,7 @@ private IAsyncOperation OpenZipFileAsync(FileAccessMode accessMode) var hFile = Win32Helper.OpenFileForRead(containerPath, readWrite); if (hFile.IsInvalid) { - return null; + throw new IOException($"The archive '{containerPath}' could not be opened."); } return new FileStream(hFile, readWrite ? FileAccess.ReadWrite : FileAccess.Read); } diff --git a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs index e393e758787c..46c044fb548e 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs @@ -4,6 +4,7 @@ using Files.Shared.Helpers; using SevenZip; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices.WindowsRuntime; using Windows.ApplicationModel; @@ -19,7 +20,7 @@ namespace Files.App.Utils.Storage public sealed partial class ZipStorageFolder : BaseStorageFolder, ICreateFileWithStream, IPasswordProtectedItem { private readonly string containerPath; - private BaseStorageFile backingFile; + private BaseStorageFile? backingFile; public override string Path { get; } public override string Name { get; } @@ -31,9 +32,16 @@ public sealed partial class ZipStorageFolder : BaseStorageFolder, ICreateFileWit public override Windows.Storage.FileAttributes Attributes => Windows.Storage.FileAttributes.Directory; public override IStorageItemExtraProperties Properties => new BaseBasicStorageItemExtraProperties(this); - public StorageCredential Credentials { get; set; } = new(); + private StorageCredential credentials = new(); - public Func> PasswordRequestedCallback { get; set; } + [AllowNull] + public StorageCredential Credentials + { + get => credentials; + set => credentials = value ?? new(); + } + + public Func>? PasswordRequestedCallback { get; set; } public ZipStorageFolder(string path, string containerPath) { @@ -41,7 +49,7 @@ public ZipStorageFolder(string path, string containerPath) Path = path; this.containerPath = containerPath; } - public ZipStorageFolder(string path, string containerPath, BaseStorageFile backingFile) : this(path, containerPath) + public ZipStorageFolder(string path, string containerPath, BaseStorageFile? backingFile) : this(path, containerPath) => this.backingFile = backingFile; public ZipStorageFolder(string path, string containerPath, ArchiveFileInfo entry) : this(path, containerPath) => DateCreated = entry.CreationTime == DateTime.MinValue ? DateTimeOffset.MinValue : entry.CreationTime; @@ -53,7 +61,7 @@ public ZipStorageFolder(BaseStorageFile backingFile) this.containerPath = backingFile.Path; this.backingFile = backingFile; } - public ZipStorageFolder(string path, string containerPath, ArchiveFileInfo entry, BaseStorageFile backingFile) : this(path, containerPath, entry) + public ZipStorageFolder(string path, string containerPath, ArchiveFileInfo entry, BaseStorageFile? backingFile) : this(path, containerPath, entry) => this.backingFile = backingFile; public static bool IsZipPath(string path, bool includeRoot = true) @@ -76,7 +84,7 @@ public static bool IsZipPath(string path, bool includeRoot = true) public async Task GetUncompressedSize() { long uncompressedSize = 0; - using SevenZipExtractor zipFile = await FilesystemTasks.Wrap(async () => + using SevenZipExtractor? zipFile = await FilesystemTasks.WrapNullable(async () => { var arch = await OpenZipFileAsync(); return arch?.ArchiveFileData is null ? null : arch; // Force load archive (1665013614u) @@ -112,11 +120,11 @@ public static async Task CheckDefaultZipApp(string filePath) return await defaultAppDict.GetAsync(ext ?? "", queryFileAssoc); } - public static IAsyncOperation FromPathAsync(string path) + public static IAsyncOperation FromPathAsync(string path) { if (!FileExtensionHelpers.IsBrowsableZipFile(path, out var ext)) { - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } var marker = path.IndexOf(ext, StringComparison.OrdinalIgnoreCase); if (marker is not -1) @@ -124,14 +132,14 @@ public static IAsyncOperation FromPathAsync(string path) var containerPath = path.Substring(0, marker + ext.Length); if (CheckAccess(containerPath)) { - return Task.FromResult((BaseStorageFolder)new ZipStorageFolder(path, containerPath)).AsAsyncOperation(); + return Task.FromResult(new ZipStorageFolder(path, containerPath)).AsAsyncOperation(); } } - return Task.FromResult(null).AsAsyncOperation(); + return Task.FromResult(null).AsAsyncOperation(); } - public static IAsyncOperation FromStorageFileAsync(BaseStorageFile file) - => AsyncInfo.Run(async (cancellationToken) => await CheckAccess(file) ? new ZipStorageFolder(file) : null); + public static IAsyncOperation FromStorageFileAsync(BaseStorageFile file) + => AsyncInfo.Run(async (cancellationToken) => await CheckAccess(file) ? new ZipStorageFolder(file) : null); public override IAsyncOperation ToStorageFolderAsync() => throw new NotSupportedException(); @@ -140,7 +148,7 @@ public static IAsyncOperation FromStorageFileAsync(BaseStorag public override IAsyncOperation GetIndexedStateAsync() => Task.FromResult(IndexedState.NotIndexed).AsAsyncOperation(); - public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); + public override IAsyncOperation GetParentAsync() => throw new NotSupportedException(); private async Task GetBasicProperties() { @@ -168,9 +176,9 @@ public override IAsyncOperation GetBasicPropertiesAsync() }); } - public override IAsyncOperation GetItemAsync(string name) + public override IAsyncOperation GetItemAsync(string name) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) @@ -200,7 +208,7 @@ public override IAsyncOperation GetItemAsync(string name) }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } - public override IAsyncOperation TryGetItemAsync(string name) + public override IAsyncOperation TryGetItemAsync(string name) { return AsyncInfo.Run(async (cancellationToken) => { @@ -221,7 +229,7 @@ public override IAsyncOperation> GetItemsAsync() using SevenZipExtractor zipFile = await OpenZipFileAsync(); if (zipFile is null || zipFile.ArchiveFileData is null) { - return null; + return []; } //zipFile.IsStreamOwner = true; var items = new List(); @@ -260,10 +268,10 @@ public override IAsyncOperation> GetItemsAsync(uint => (await GetItemsAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList() ); - public override IAsyncOperation GetFileAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ZipStorageFile); + public override IAsyncOperation GetFileAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ZipStorageFile); public override IAsyncOperation> GetFilesAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFilesAsync()); public override IAsyncOperation> GetFilesAsync(CommonFileQuery query, uint startIndex, uint maxItemsToRetrieve) @@ -271,10 +279,10 @@ public override IAsyncOperation> GetFilesAsync(Co => (await GetFilesAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList() ); - public override IAsyncOperation GetFolderAsync(string name) - => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ZipStorageFolder); + public override IAsyncOperation GetFolderAsync(string name) + => AsyncInfo.Run(async (cancellationToken) => await GetItemAsync(name) as ZipStorageFolder); public override IAsyncOperation> GetFoldersAsync() - => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync())?.OfType().ToList()); + => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).OfType().ToList()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query) => AsyncInfo.Run(async (cancellationToken) => await GetFoldersAsync()); public override IAsyncOperation> GetFoldersAsync(CommonFolderQuery query, uint startIndex, uint maxItemsToRetrieve) @@ -286,16 +294,16 @@ public override IAsyncOperation> GetFoldersAsyn }); } - public override IAsyncOperation CreateFileAsync(string desiredName) + public override IAsyncOperation CreateFileAsync(string desiredName) => CreateFileAsync(desiredName, CreationCollisionOption.FailIfExists); - public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFileAsync(string desiredName, CreationCollisionOption options) => CreateFileAsync(new MemoryStream(), desiredName, options); - public override IAsyncOperation CreateFolderAsync(string desiredName) + public override IAsyncOperation CreateFolderAsync(string desiredName) => CreateFolderAsync(desiredName, CreationCollisionOption.FailIfExists); - public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) + public override IAsyncOperation CreateFolderAsync(string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { var zipDesiredName = System.IO.Path.Combine(Path, desiredName); var item = await GetItemAsync(desiredName); @@ -316,7 +324,7 @@ public override IAsyncOperation CreateFolderAsync(string desi compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); var fileName = IO.Path.GetRelativePath(containerPath, zipDesiredName); - await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, null } }, Credentials.Password, ms); + await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, null } }, Credentials.Password, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -333,8 +341,8 @@ public override IAsyncOperation CreateFolderAsync(string desi }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); - public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); + public override IAsyncOperation MoveAsync(IStorageFolder destinationFolder, NameCollisionOption option) => throw new NotSupportedException(); public override IAsyncAction RenameAsync(string desiredName) => RenameAsync(desiredName, NameCollisionOption.FailIfExists); public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option) @@ -349,7 +357,8 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption } else { - var fileName = IO.Path.Combine(IO.Path.GetDirectoryName(Path), desiredName); + var parentPath = IO.Path.GetDirectoryName(Path) ?? throw new InvalidOperationException("The archive path has no parent."); + var fileName = IO.Path.Combine(parentPath, desiredName); PInvoke.MoveFileFromApp(Path, fileName); } } @@ -368,7 +377,7 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); var folderKey = IO.Path.GetRelativePath(containerPath, Path); - var folderDes = IO.Path.Combine(IO.Path.GetDirectoryName(folderKey), desiredName); + var folderDes = IO.Path.Combine(IO.Path.GetDirectoryName(folderKey) ?? string.Empty, desiredName); var entriesMap = new Dictionary(index.Select(x => new KeyValuePair(x.Index, IO.Path.Combine(folderDes, IO.Path.GetRelativePath(folderKey, x.Key))))); await compressor.ModifyArchiveAsync(archiveStream, entriesMap, Credentials.Password, ms); @@ -419,7 +428,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); - var entriesMap = new Dictionary(index.Select(x => new KeyValuePair(x.Index, null))); + var entriesMap = new Dictionary(index.Select(x => new KeyValuePair(x.Index, null))); await compressor.ModifyArchiveAsync(archiveStream, entriesMap, Credentials.Password, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) @@ -449,7 +458,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) public override StorageFolderQueryResult CreateFolderQuery(CommonFolderQuery query) => throw new NotSupportedException(); public override BaseStorageFolderQueryResult CreateFolderQueryWithOptions(QueryOptions queryOptions) => new(this, queryOptions); - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode) { return AsyncInfo.Run(async (cancellationToken) => { @@ -461,7 +470,7 @@ public override IAsyncOperation GetThumbnailAsync(Thumbnai return await zipFile.GetThumbnailAsync(mode); }); } - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize) { return AsyncInfo.Run(async (cancellationToken) => { @@ -473,7 +482,7 @@ public override IAsyncOperation GetThumbnailAsync(Thumbnai return await zipFile.GetThumbnailAsync(mode, requestedSize); }); } - public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) + public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) { return AsyncInfo.Run(async (cancellationToken) => { @@ -581,7 +590,7 @@ private IAsyncOperation OpenZipFileAsync() return AsyncInfo.Run(async (cancellationToken) => { var zipFile = await OpenZipFileAsync(FileAccessMode.Read); - return zipFile is not null ? new SevenZipExtractor(zipFile, Credentials.Password) : null; + return new SevenZipExtractor(zipFile, Credentials.Password); }); } @@ -599,7 +608,7 @@ private IAsyncOperation OpenZipFileAsync(FileAccessMode accessMode) var hFile = Win32Helper.OpenFileForRead(containerPath, readWrite); if (hFile.IsInvalid) { - return null; + throw new IOException($"The archive '{containerPath}' could not be opened."); } return new FileStream(hFile, readWrite ? FileAccess.ReadWrite : FileAccess.Read); } @@ -612,19 +621,19 @@ private IAsyncOperation OpenZipFileAsync(FileAccessMode accessMode) { if (zipFile is null || zipFile.ArchiveFileData is null) { - return null; + return []; } //zipFile.IsStreamOwner = true; return zipFile.GetArchiveFileData(containerPath).Where(x => System.IO.Path.Combine(containerPath, x.FileName).IsSubPathOf(Path)).Select(e => (e.Index, e.FileName)); } } - public IAsyncOperation CreateFileAsync(Stream contents, string desiredName) + public IAsyncOperation CreateFileAsync(Stream contents, string desiredName) => CreateFileAsync(new MemoryStream(), desiredName, CreationCollisionOption.FailIfExists); - public IAsyncOperation CreateFileAsync(Stream contents, string desiredName, CreationCollisionOption options) + public IAsyncOperation CreateFileAsync(Stream contents, string desiredName, CreationCollisionOption options) { - return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { var zipDesiredName = System.IO.Path.Combine(Path, desiredName); var item = await GetItemAsync(desiredName); diff --git a/src/Files.App/Utils/Taskbar/SystemTrayIconWindow.cs b/src/Files.App/Utils/Taskbar/SystemTrayIconWindow.cs index a8256c52dacd..b169af134a0f 100644 --- a/src/Files.App/Utils/Taskbar/SystemTrayIconWindow.cs +++ b/src/Files.App/Utils/Taskbar/SystemTrayIconWindow.cs @@ -16,7 +16,7 @@ namespace Files.App.Utils.Taskbar /// public sealed partial class SystemTrayIconWindow : IDisposable { - private SystemTrayIcon _trayIcon; + private SystemTrayIcon? _trayIcon; private readonly WNDPROC _windowProcedure; @@ -69,7 +69,7 @@ public unsafe SystemTrayIconWindow(SystemTrayIcon icon) private LRESULT WindowProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) { - return _trayIcon.WindowProc(hWnd, uMsg, wParam, lParam); + return _trayIcon?.WindowProc(hWnd, uMsg, wParam, lParam) ?? default; } public void Dispose() diff --git a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs index 472378bf74dc..bfa1e0d8006c 100644 --- a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs @@ -28,15 +28,15 @@ public sealed partial class CreateShortcutDialogViewModel : ObservableObject public string ShortcutCompleteName { get; private set; } = string.Empty; // Full path of the destination item - public string FullPath { get; private set; } + public string FullPath { get; private set; } = string.Empty; // Arguments to be passed to the destination item if it's an executable - public string Arguments { get; private set; } + public string Arguments { get; private set; } = string.Empty; // Previous path of the destination item - private string _previousShortcutTargetPath; + private string _previousShortcutTargetPath = string.Empty; - private string _shortcutName; + private string _shortcutName = string.Empty; public string ShortcutName { get => _shortcutName; diff --git a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs index 9be6bc53f979..44db93a866a4 100644 --- a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs @@ -18,7 +18,7 @@ public sealed partial class DecompressArchiveDialogViewModel : ObservableObject private readonly IStorageFile archive; // Properties - public BaseStorageFolder DestinationFolder { get; private set; } + public BaseStorageFolder? DestinationFolder { get; private set; } private string destinationFolderPath; public string DestinationFolderPath @@ -42,7 +42,7 @@ public bool IsDestinationPathValid if (string.IsNullOrWhiteSpace(DestinationFolderPath)) return false; - string parentDir = Path.GetDirectoryName(DestinationFolderPath); + string? parentDir = Path.GetDirectoryName(DestinationFolderPath); string finalSegment = Path.GetFileName(DestinationFolderPath); // Check parent directory exists @@ -113,7 +113,7 @@ public bool ShowPathSelection // Commands public IRelayCommand PrimaryButtonClickCommand { get; private set; } public ICommand SelectDestinationCommand { get; private set; } - public ICommand QuerySubmittedCommand { get; private set; } + public ICommand? QuerySubmittedCommand { get; private set; } // Constructor public DecompressArchiveDialogViewModel(IStorageFile archive) @@ -160,7 +160,7 @@ private void RefreshEncodingOptions() { EncodingOptions = EncodingItem.Defaults; } - SelectedEncoding = EncodingOptions.FirstOrDefault(); + SelectedEncoding = EncodingOptions[0]; } // Public Methods @@ -183,4 +183,4 @@ public void UpdateSuggestions(string query) } } } -} \ 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 bd9ff746ffd1..3e33ad9ed09e 100644 --- a/src/Files.App/ViewModels/Dialogs/DynamicDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/DynamicDialogViewModel.cs @@ -16,14 +16,14 @@ public sealed partial class DynamicDialogViewModel : ObservableObject, IDisposab /// /// Stores any additional data that could be written to, read from. /// - public object AdditionalData { get; set; } + public object? AdditionalData { get; set; } - public object displayControl; + public object? displayControl; /// /// The control that is dynamically displayed. /// - public object DisplayControl + public object? DisplayControl { get => displayControl; set @@ -96,7 +96,7 @@ public DynamicDialogButtons DynamicButtons } } - private string titleText; + private string titleText = string.Empty; /// /// The Title text of the dialog. @@ -107,14 +107,14 @@ public string TitleText set => SetProperty(ref titleText, value); } - private string subtitleText; + private string? subtitleText; /// /// The subtitle of the dialog. ///
/// (Can be null or empty) ///
- public string SubtitleText + public string? SubtitleText { get => subtitleText; set @@ -139,12 +139,12 @@ public bool SubtitleLoad #region Primary Button - private string primaryButtonText; + private string? primaryButtonText; /// /// The text content of the primary button. /// - public string PrimaryButtonText + public string? PrimaryButtonText { get => primaryButtonText; set => SetProperty(ref primaryButtonText, value); @@ -165,12 +165,12 @@ public bool IsPrimaryButtonEnabled #region Secondary Button - private string secondaryButtonText; + private string? secondaryButtonText; /// /// The text of the secondary button. /// - public string SecondaryButtonText + public string? SecondaryButtonText { get => secondaryButtonText; set => SetProperty(ref secondaryButtonText, value); @@ -191,12 +191,12 @@ public bool IsSecondaryButtonEnabled #region Close Button - private string closeButtonText; + private string? closeButtonText; /// /// The text of the close button. /// - public string CloseButtonText + public string? CloseButtonText { get => closeButtonText; set => SetProperty(ref closeButtonText, value); @@ -259,72 +259,87 @@ public DynamicDialogButtons DynamicButtonsEnabled ///
/// This action is assigned by default. ///
- public Action HideDialog { get; set; } + public Action? HideDialog { get; set; } - private Action primaryButtonAction; + private Action? primaryButtonAction; /// /// OnPrimary action. /// - public Action PrimaryButtonAction + public Action? PrimaryButtonAction { get => primaryButtonAction; set { if (SetProperty(ref primaryButtonAction, value)) { - PrimaryButtonCommand = new RelayCommand((e) => - { - DynamicResult = DynamicDialogResult.Primary; - PrimaryButtonAction(this, e); - }); + PrimaryButtonCommand = value is null + ? null + : new RelayCommand((e) => + { + if (e is null) + return; + + DynamicResult = DynamicDialogResult.Primary; + value(this, e); + }); } } } - private Action secondaryButtonAction; + private Action? secondaryButtonAction; /// /// OnSecondary action. /// - public Action SecondaryButtonAction + public Action? SecondaryButtonAction { get => secondaryButtonAction; set { if (SetProperty(ref secondaryButtonAction, value)) { - SecondaryButtonCommand = new RelayCommand((e) => - { - DynamicResult = DynamicDialogResult.Secondary; - SecondaryButtonAction(this, e); - }); + SecondaryButtonCommand = value is null + ? null + : new RelayCommand((e) => + { + if (e is null) + return; + + DynamicResult = DynamicDialogResult.Secondary; + value(this, e); + }); } } } - private Action closeButtonAction; + private Action? closeButtonAction; /// /// OnClose action. /// - public Action CloseButtonAction + public Action? CloseButtonAction { get => closeButtonAction; set { if (SetProperty(ref closeButtonAction, value)) { - CloseButtonCommand = new RelayCommand((e) => - { - DynamicResult = DynamicDialogResult.Cancel; - CloseButtonAction(this, e); - }); + CloseButtonCommand = value is null + ? null + : new RelayCommand((e) => + { + if (e is null) + return; + + DynamicResult = DynamicDialogResult.Cancel; + value(this, e); + }); } } } - private Action keyDownAction; + private Action? keyDownAction; /// /// The keydown action on the dialog. @@ -334,35 +349,43 @@ public Action CloseBu ///
/// This action is assigned by default. ///
- public Action KeyDownAction + public Action? KeyDownAction { get => keyDownAction; set { if (SetProperty(ref keyDownAction, value)) { - KeyDownCommand = new RelayCommand((e) => - { - DynamicResult = DynamicDialogResult.Cancel; - KeyDownAction(this, e); - }); + KeyDownCommand = value is null + ? null + : new RelayCommand((e) => + { + if (e is null) + return; + + DynamicResult = DynamicDialogResult.Cancel; + value(this, e); + }); } } } - private Action displayControlOnLoaded; + private Action? displayControlOnLoaded; - public Action DisplayControlOnLoaded + public Action? DisplayControlOnLoaded { get => displayControlOnLoaded; set { if (SetProperty(ref displayControlOnLoaded, value)) { - DisplayControlOnLoadedCommand = new RelayCommand((e) => - { - DisplayControlOnLoaded(this, e); - }); + DisplayControlOnLoadedCommand = value is null + ? null + : new RelayCommand((e) => + { + if (e is not null) + value(this, e); + }); } } } @@ -371,15 +394,15 @@ public Action DisplayControlOnLoaded #region Commands - public ICommand PrimaryButtonCommand { get; private set; } + public ICommand? PrimaryButtonCommand { get; private set; } - public ICommand SecondaryButtonCommand { get; private set; } + public ICommand? SecondaryButtonCommand { get; private set; } - public ICommand CloseButtonCommand { get; private set; } + public ICommand? CloseButtonCommand { get; private set; } - public ICommand KeyDownCommand { get; private set; } + public ICommand? KeyDownCommand { get; private set; } - public ICommand DisplayControlOnLoadedCommand { get; private set; } + public ICommand? DisplayControlOnLoadedCommand { get; private set; } #endregion Commands @@ -390,14 +413,14 @@ public DynamicDialogViewModel() // Create default implementation TitleText = "DynamicDialog"; PrimaryButtonText = "Ok"; - PrimaryButtonAction = (vm, e) => HideDialog(); - SecondaryButtonAction = (vm, e) => HideDialog(); - CloseButtonAction = (vm, e) => HideDialog(); + PrimaryButtonAction = (vm, e) => HideDialog?.Invoke(); + SecondaryButtonAction = (vm, e) => HideDialog?.Invoke(); + CloseButtonAction = (vm, e) => HideDialog?.Invoke(); KeyDownAction = (vm, e) => { if (e.Key == VirtualKey.Escape) { - HideDialog(); + HideDialog?.Invoke(); } }; DisplayControlOnLoaded = (vm, e) => @@ -406,7 +429,7 @@ public DynamicDialogViewModel() if (vm.DisplayControl is not DependencyObject displayControl) return; - Control control = (displayControl as Control) ?? DependencyObjectHelpers.FindChild(displayControl); + Control? control = (displayControl as Control) ?? DependencyObjectHelpers.FindChild(displayControl); control?.Focus(FocusState.Programmatic); }; @@ -424,7 +447,7 @@ public void Dispose() AdditionalData = null; displayControl = null; - titleText = null; + titleText = string.Empty; subtitleText = null; primaryButtonText = null; secondaryButtonText = null; diff --git a/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogConflictItemViewModel.cs b/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogConflictItemViewModel.cs index a73f44352f73..729a376defef 100644 --- a/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogConflictItemViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogConflictItemViewModel.cs @@ -7,15 +7,15 @@ namespace Files.App.ViewModels.Dialogs.FileSystemDialog { public sealed partial class FileSystemDialogConflictItemViewModel : BaseFileSystemDialogItemViewModel, IFileSystemDialogConflictItemViewModel { - private string? _DestinationDisplayName; - public string? DestinationDisplayName + private string _DestinationDisplayName = string.Empty; + public required string DestinationDisplayName { get => _DestinationDisplayName; set => SetProperty(ref _DestinationDisplayName, value); } - private string? _CustomName; - public string? CustomName + private string _CustomName = string.Empty; + public string CustomName { get => _CustomName; set @@ -23,13 +23,13 @@ public string? CustomName if (SetProperty(ref _CustomName, value)) { DestinationDisplayName = value; - _DestinationPath = Path.Combine(Path.GetDirectoryName(DestinationPath), value); + _DestinationPath = Path.Combine(Path.GetDirectoryName(DestinationPath) ?? string.Empty, value); } } } - private string? _DestinationPath; - public string? DestinationPath + private string _DestinationPath = string.Empty; + public required string DestinationPath { get => _DestinationPath; set @@ -47,7 +47,7 @@ public bool IsTextBoxVisible } public string DestinationDirectoryDisplayName - => Path.GetFileName(Path.GetDirectoryName(DestinationPath)); + => Path.GetFileName(Path.GetDirectoryName(DestinationPath)) ?? string.Empty; public bool IsConflict => ConflictResolveOption != FileNameConflictResolveOptionType.None; diff --git a/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogViewModel.cs index da7e88e3d0de..cf4f81ebaa76 100644 --- a/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/FileSystemDialog/FileSystemDialogViewModel.cs @@ -216,7 +216,7 @@ public static FileSystemDialogViewModel GetDialogViewModel(FileSystemDialogMode return viewModel; } - public static FileSystemDialogViewModel GetDialogViewModel(List nonConflictingItems, string titleText, string descriptionText, string primaryButtonText, string secondaryButtonText, string closeButtonText = null) + public static FileSystemDialogViewModel GetDialogViewModel(List nonConflictingItems, string titleText, string? descriptionText, string? primaryButtonText, string? secondaryButtonText, string? closeButtonText = null) { var viewModel = new FileSystemDialogViewModel( new() diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs index ceab728663dd..dee13c56f9e3 100644 --- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs +++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs @@ -49,20 +49,29 @@ public BaseLayoutViewModel(IShellPage associatedInstance, ItemManipulationModel DropCommand = new AsyncRelayCommand(DropAsync); } - private void CreateNewFile(ShellNewEntry f) + private void CreateNewFile(ShellNewEntry? f) { + if (f is null) + return; + _ = UIFilesystemHelpers.CreateFileFromDialogResultTypeAsync(AddItemDialogItemType.File, f, _associatedInstance); } - private async Task ItemPointerPressedAsync(PointerRoutedEventArgs e) + private async Task ItemPointerPressedAsync(PointerRoutedEventArgs? e) { + if (e is null) + return; + // If a folder item was clicked, disable middle mouse click to scroll to cancel the mouse scrolling state and re-enable it if (e.GetCurrentPoint(null).Properties.IsMiddleButtonPressed && e.OriginalSource is FrameworkElement { DataContext: ListedItem Item } && Item.PrimaryItemAttribute == StorageItemTypes.Folder) { - _associatedInstance.SlimContentPage.IsMiddleClickToScrollEnabled = false; - _associatedInstance.SlimContentPage.IsMiddleClickToScrollEnabled = true; + if (_associatedInstance.SlimContentPage is { } contentPage) + { + contentPage.IsMiddleClickToScrollEnabled = false; + contentPage.IsMiddleClickToScrollEnabled = true; + } if (Item.IsShortcut) await NavigationHelpers.OpenPathInNewTab(((e.OriginalSource as FrameworkElement)?.DataContext as IShortcutItem)?.TargetPath ?? Item.ItemPath); @@ -71,8 +80,11 @@ private async Task ItemPointerPressedAsync(PointerRoutedEventArgs e) } } - private void PointerWheelChanged(PointerRoutedEventArgs e) + private void PointerWheelChanged(PointerRoutedEventArgs? e) { + if (e is null) + return; + if (e.KeyModifiers is VirtualKeyModifiers.Control && _associatedInstance.IsCurrentInstance) { @@ -89,8 +101,11 @@ private void PointerWheelChanged(PointerRoutedEventArgs e) } } - public async Task DragOverAsync(DragEventArgs e) + public async Task DragOverAsync(DragEventArgs? e) { + if (e is null) + return; + var deferral = e.GetDeferral(); if (_associatedInstance.InstanceViewModel.IsPageTypeSearchResults) @@ -149,7 +164,7 @@ public async Task DragOverAsync(DragEventArgs e) e.DragUIOverride.Caption = string.Format(Strings.LinkToFolderCaptionText.GetLocalizedResource(), folderName); e.AcceptedOperation = DataPackageOperation.Link; } - else if (workingDirectory.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal)) + else if (workingDirectory?.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal) == true) { e.DragUIOverride.Caption = string.Format(Strings.MoveToFolderCaptionText.GetLocalizedResource(), folderName); // Some applications such as Edge can't raise the drop event by the Move flag (#14008), so we set the Copy flag as well. @@ -174,7 +189,7 @@ public async Task DragOverAsync(DragEventArgs e) else if (draggedItems.Any(x => x.Item is ZipStorageFile || x.Item is ZipStorageFolder) || - ZipStorageFolder.IsZipPath(workingDirectory)) + workingDirectory is not null && ZipStorageFolder.IsZipPath(workingDirectory)) { e.DragUIOverride.Caption = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), folderName); e.AcceptedOperation = DataPackageOperation.Copy; @@ -203,7 +218,7 @@ x.Item is ZipStorageFile || deferral.Complete(); } - public async Task DropAsync(DragEventArgs e) + public async Task DropAsync(DragEventArgs? e) { if (e is null) return; @@ -275,4 +290,4 @@ public void Dispose() { } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/MainPageViewModel.cs b/src/Files.App/ViewModels/MainPageViewModel.cs index 5fddd8d7d141..cb558243816d 100644 --- a/src/Files.App/ViewModels/MainPageViewModel.cs +++ b/src/Files.App/ViewModels/MainPageViewModel.cs @@ -389,7 +389,10 @@ private async Task ExecuteOpenNetworkSharingSettingsCommand() private async void ExecuteNavigateToNumberedTabKeyboardAcceleratorCommand(KeyboardAcceleratorInvokedEventArgs? e) { - var indexToSelect = e!.KeyboardAccelerator.Key switch + if (e is null) + return; + + var indexToSelect = e.KeyboardAccelerator.Key switch { VirtualKey.Number1 => 0, VirtualKey.Number2 => 1, @@ -412,7 +415,7 @@ private async void ExecuteNavigateToNumberedTabKeyboardAcceleratorCommand(Keyboa await Task.Delay(500); // Focus the content of the selected tab item (needed for keyboard navigation) - context.ShellPage!.PaneHolder.FocusActivePane(); + context.ShellPage?.PaneHolder?.FocusActivePane(); } e.Handled = true; diff --git a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs index 784912b38e30..6e953d9470fc 100644 --- a/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs +++ b/src/Files.App/ViewModels/Properties/BasePropertiesPage.cs @@ -14,9 +14,11 @@ public abstract class BasePropertiesPage : Page, IDisposable { private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); - public IShellPage AppInstance = null; + private IShellPage? appInstance; + public IShellPage AppInstance + => appInstance ?? throw new InvalidOperationException("The properties page has not been initialized."); - public BaseProperties BaseProperties { get; set; } + public BaseProperties? BaseProperties { get; set; } public SelectedItemsPropertiesViewModel ViewModel { get; set; } = new(); @@ -28,15 +30,15 @@ protected virtual void Properties_Loaded(object sender, RoutedEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e) { var np = (PropertiesPageNavigationParameter)e.Parameter; - AppInstance = np.AppInstance; + appInstance = np.AppInstance; // Library if (np.Parameter is LibraryItem library) - BaseProperties = new LibraryProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, library, AppInstance); + BaseProperties = new LibraryProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, library, np.AppInstance); // Drive else if (np.Parameter is DriveItem drive) { - var props = new DriveProperties(ViewModel, drive, AppInstance); + var props = new DriveProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, drive, np.AppInstance); BaseProperties = props; ViewModel.CleanupVisibility = props.Drive.Type != DriveType.Network && props.Drive.Type != DriveType.CloudDrive; @@ -59,7 +61,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) // Selection only contains files if (items.All(item => item.PrimaryItemAttribute == StorageItemTypes.File || item.IsArchive)) { - BaseProperties = new CombinedFileProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance); + BaseProperties = new CombinedFileProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, np.AppInstance); ViewModel.IsEditAlbumCoverVisible = items.All(item => item.FileExtension is not ".avi") && ( @@ -68,17 +70,17 @@ protected override void OnNavigatedTo(NavigationEventArgs e) } // Selection includes folders else - BaseProperties = new CombinedProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance); + BaseProperties = new CombinedProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, np.AppInstance); } // A storage object else if (np.Parameter is ListedItem item) { // File or Archive if (item.PrimaryItemAttribute == StorageItemTypes.File || item.IsArchive) - BaseProperties = new FileProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, item, AppInstance); + BaseProperties = new FileProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, item, np.AppInstance); // Folder else if (item.PrimaryItemAttribute == StorageItemTypes.Folder) - BaseProperties = new FolderProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, item, AppInstance); + BaseProperties = new FolderProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, item, np.AppInstance); } ViewModel.EditAlbumCoverCommand = new RelayCommand(async () => @@ -114,7 +116,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) ViewModel.IsAblumCoverModified = true; ViewModel.ModifiedAlbumCover = null; - string mediaPath = np.Parameter switch + string? mediaPath = np.Parameter switch { ListedItem singleItem => singleItem.ItemPath, List items => items.FirstOrDefault()?.ItemPath, diff --git a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs index b6869ec059a5..43e6615a455d 100644 --- a/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs +++ b/src/Files.App/ViewModels/Properties/CustomizationViewModel.cs @@ -23,7 +23,7 @@ private static string DefaultIconDllFilePath public ObservableCollection DllIcons { get; } = []; - private string _IconResourceItemPath; + private string _IconResourceItemPath = string.Empty; public string IconResourceItemPath { get => _IconResourceItemPath; @@ -62,14 +62,12 @@ public IconFileInfo? SelectedDllIcon public CustomizationViewModel(IShellPage appInstance, BaseProperties baseProperties, AppWindow appWindow) { - ListedItem item; - - if (baseProperties is FileProperties fileProperties) - item = fileProperties.Item; - else if (baseProperties is FolderProperties folderProperties) - item = folderProperties.Item; - else - return; + ListedItem item = baseProperties switch + { + FileProperties fileProperties => fileProperties.Item, + FolderProperties folderProperties => folderProperties.Item, + _ => throw new ArgumentException("The properties must represent a file or folder.", nameof(baseProperties)), + }; _appInstance = appInstance; _appWindow = appWindow; diff --git a/src/Files.App/ViewModels/Properties/HashesViewModel.cs b/src/Files.App/ViewModels/Properties/HashesViewModel.cs index f23631094043..ecd555a58573 100644 --- a/src/Files.App/ViewModels/Properties/HashesViewModel.cs +++ b/src/Files.App/ViewModels/Properties/HashesViewModel.cs @@ -13,12 +13,12 @@ namespace Files.App.ViewModels.Properties public sealed partial class HashesViewModel : ObservableObject, IDisposable { private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService(); - private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetService()!; + private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); private readonly AppWindow _appWindow; - private HashInfoItem _selectedItem; - public HashInfoItem SelectedItem + private HashInfoItem? _selectedItem; + public HashInfoItem? SelectedItem { get => _selectedItem; set => SetProperty(ref _selectedItem, value); @@ -35,7 +35,7 @@ public HashInfoItem SelectedItem private CancellationTokenSource _cancellationTokenSource; - private string _hashInput; + private string _hashInput = string.Empty; public string HashInput { get => _hashInput; @@ -55,7 +55,7 @@ public InfoBarSeverity InfoBarSeverity set => SetProperty(ref _infoBarSeverity, value); } - private string _infoBarTitle; + private string _infoBarTitle = string.Empty; public string InfoBarTitle { get => _infoBarTitle; diff --git a/src/Files.App/ViewModels/Properties/Items/BaseProperties.cs b/src/Files.App/ViewModels/Properties/Items/BaseProperties.cs index c204b74af287..abd10d94b9be 100644 --- a/src/Files.App/ViewModels/Properties/Items/BaseProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/BaseProperties.cs @@ -10,13 +10,25 @@ namespace Files.App.ViewModels.Properties { public abstract class BaseProperties { - public IShellPage AppInstance { get; set; } = null; + public IShellPage AppInstance { get; } - public SelectedItemsPropertiesViewModel ViewModel { get; set; } + public SelectedItemsPropertiesViewModel ViewModel { get; } - public CancellationTokenSource TokenSource { get; set; } + public CancellationTokenSource TokenSource { get; } - public DispatcherQueue Dispatcher { get; set; } + public DispatcherQueue Dispatcher { get; } + + protected BaseProperties( + SelectedItemsPropertiesViewModel viewModel, + CancellationTokenSource tokenSource, + DispatcherQueue dispatcher, + IShellPage appInstance) + { + ViewModel = viewModel; + TokenSource = tokenSource; + Dispatcher = dispatcher; + AppInstance = appInstance; + } public abstract void GetBaseProperties(); diff --git a/src/Files.App/ViewModels/Properties/Items/CombinedFileProperties.cs b/src/Files.App/ViewModels/Properties/Items/CombinedFileProperties.cs index 5f1855943311..2f88a6583c9e 100644 --- a/src/Files.App/ViewModels/Properties/Items/CombinedFileProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/CombinedFileProperties.cs @@ -19,8 +19,8 @@ public async Task GetSystemFilePropertiesAsync() { var queries = await Task.WhenAll(List.AsParallel().Select(async item => { - BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath)); - if (file is null) + var fileResult = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath)); + if (fileResult.Result is not { } file) { // Could not access file, can't show any other property return null; @@ -38,7 +38,9 @@ public async Task GetSystemFilePropertiesAsync() return list .Where(fileProp => !(fileProp.Value is null && fileProp.IsReadOnly)) - .GroupBy(fileProp => fileProp.SectionResource) + .Select(fileProp => (Property: fileProp, Section: fileProp.SectionResource ?? string.Empty)) + .Where(property => property.Section.Length > 0) + .GroupBy(property => property.Section, property => property.Property) .Select(group => new FilePropertySection(group) { Key = group.Key }) .Where(section => !section.All(fileProp => fileProp.Value is null) || FileProperty.IsSectionApplicableForEmpty(section.Key, item.FileExtension)); @@ -47,19 +49,27 @@ public async Task GetSystemFilePropertiesAsync() if (queries.Any(query => query is null)) return; + var validQueries = queries.WhereNotNull().ToArray(); + if (validQueries.Length == 0) + return; + // Display only the sections that all files have - var keys = queries.Select(query => query!.Select(section => section.Key)).Aggregate((x, y) => x.Intersect(y)); - var sections = queries[0]!.Where(section => keys.Contains(section.Key)).OrderBy(group => group.Priority).ToArray(); + var keys = validQueries.Select(query => query.Select(section => section.Key)).Aggregate((x, y) => x.Intersect(y)); + var sections = validQueries[0].Where(section => keys.Contains(section.Key)).OrderBy(group => group.Priority).ToArray(); foreach (var group in sections) { - var props = queries.SelectMany(query => query!.First(section => section.Key == group.Key)); + var props = validQueries.SelectMany(query => query.First(section => section.Key == group.Key)); foreach (FileProperty prop in group) { if (prop.Property == "System.Media.Duration") { ulong totalDuration = 0; - props.Where(x => x.Property == prop.Property).ForEach(x => totalDuration += (ulong)x.Value); + props + .Where(x => x.Property == prop.Property) + .Select(x => x.Value) + .OfType() + .ForEach(value => totalDuration += value); prop.Value = totalDuration; } else if (props.Where(x => x.Property == prop.Property).Any(x => !Equals(x.Value, prop.Value))) @@ -79,10 +89,9 @@ public async Task SyncPropertyChangesAsync() var files = new List(); foreach (var item in List) { - BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath)); - // Couldn't access the file to save properties - if (file is null) + var fileResult = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath)); + if (fileResult.Result is not { } file) return; files.Add(file); @@ -94,11 +103,12 @@ public async Task SyncPropertyChangesAsync() { foreach (FileProperty prop in group) { - if (!prop.IsReadOnly && prop.Modified) + var propertyName = prop.Property; + if (!prop.IsReadOnly && prop.Modified && !string.IsNullOrEmpty(propertyName)) { - var newDict = new Dictionary + var newDict = new Dictionary { - { prop.Property, prop.Value } + { propertyName, prop.Value } }; foreach (var file in files) @@ -107,7 +117,7 @@ public async Task SyncPropertyChangesAsync() { if (file.Properties is not null) { - await file.Properties.SavePropertiesAsync(newDict); + await file.Properties.SaveNullablePropertiesAsync(newDict); } } catch @@ -131,9 +141,8 @@ public async Task ClearPropertiesAsync() var files = new List(); foreach (var item in List) { - BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath)); - - if (file is null) + var fileResult = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath)); + if (fileResult.Result is not { } file) return; files.Add(file); @@ -143,11 +152,12 @@ public async Task ClearPropertiesAsync() { foreach (FileProperty prop in group) { - if (!prop.IsReadOnly) + var propertyName = prop.Property; + if (!prop.IsReadOnly && !string.IsNullOrEmpty(propertyName)) { - var newDict = new Dictionary + var newDict = new Dictionary { - { prop.Property, null } + { propertyName, null } }; foreach (var file in files) @@ -156,7 +166,7 @@ public async Task ClearPropertiesAsync() { if (file.Properties is not null) { - await file.Properties.SavePropertiesAsync(newDict); + await file.Properties.SaveNullablePropertiesAsync(newDict); } } catch diff --git a/src/Files.App/ViewModels/Properties/Items/CombinedProperties.cs b/src/Files.App/ViewModels/Properties/Items/CombinedProperties.cs index da6721fcb94d..fc65043702cf 100644 --- a/src/Files.App/ViewModels/Properties/Items/CombinedProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/CombinedProperties.cs @@ -15,12 +15,9 @@ public CombinedProperties( DispatcherQueue coreDispatcher, List listedItems, IShellPage instance) + : base(viewModel, tokenSource, coreDispatcher, instance) { - ViewModel = viewModel; - TokenSource = tokenSource; - Dispatcher = coreDispatcher; List = listedItems; - AppInstance = instance; GetBaseProperties(); ViewModel.PropertyChanged += ViewModel_PropertyChanged; } @@ -144,7 +141,7 @@ CloudDriveSyncStatus.FolderOnline or SetItemsCountString(); } - private async void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + private async void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { diff --git a/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs b/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs index 719a9212afce..4a809b39ef6b 100644 --- a/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs @@ -6,11 +6,15 @@ internal sealed class DriveProperties : BaseProperties { public DriveItem Drive { get; } - public DriveProperties(SelectedItemsPropertiesViewModel viewModel, DriveItem driveItem, IShellPage instance) + public DriveProperties( + SelectedItemsPropertiesViewModel viewModel, + CancellationTokenSource tokenSource, + Microsoft.UI.Dispatching.DispatcherQueue dispatcher, + DriveItem driveItem, + IShellPage instance) + : base(viewModel, tokenSource, dispatcher, instance) { - ViewModel = viewModel; Drive = driveItem; - AppInstance = instance; GetBaseProperties(); } @@ -39,8 +43,10 @@ public async override Task GetSpecialPropertiesAsync() { ViewModel.ItemAttributesVisibility = false; - var item = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(Drive.Path)); - BaseStorageFolder diskRoot = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(Drive.Path, item)); + var rootResult = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(Drive.Path)); + var diskRootResult = await FilesystemTasks.WrapNullable( + () => StorageFileExtensions.DangerousGetFolderFromPathAsync(Drive.Path, rootResult.Result)); + var diskRoot = diskRootResult.Result; if (ViewModel.LoadFileIcon) { diff --git a/src/Files.App/ViewModels/Properties/Items/FileProperties.cs b/src/Files.App/ViewModels/Properties/Items/FileProperties.cs index 2aa9d02e59c6..748554de8bf3 100644 --- a/src/Files.App/ViewModels/Properties/Items/FileProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/FileProperties.cs @@ -17,12 +17,9 @@ public FileProperties( DispatcherQueue coreDispatcher, ListedItem item, IShellPage instance) + : base(viewModel, tokenSource, coreDispatcher, instance) { - ViewModel = viewModel; - TokenSource = tokenSource; - Dispatcher = coreDispatcher; Item = item; - AppInstance = instance; GetBaseProperties(); @@ -38,7 +35,7 @@ public override void GetBaseProperties() ViewModel.OriginalItemName = Item.Name; ViewModel.ItemType = Item.ItemType; ViewModel.ItemLocation = (Item as RecycleBinItem)?.ItemOriginalFolder ?? - (Path.IsPathRooted(Item.ItemPath) ? Path.GetDirectoryName(Item.ItemPath) : Item.ItemPath); + (Path.IsPathRooted(Item.ItemPath) ? Path.GetDirectoryName(Item.ItemPath) ?? string.Empty : Item.ItemPath); ViewModel.ItemModifiedTimestampReal = Item.ItemDateModifiedReal; ViewModel.ItemCreatedTimestampReal = Item.ItemDateCreatedReal; ViewModel.LoadCustomIcon = Item.LoadCustomIcon; @@ -142,10 +139,12 @@ public override async Task GetSpecialPropertiesAsync() // Get file for further processing string filePath = (Item as IShortcutItem)?.TargetPath ?? Item.ItemPath; - BaseStorageFile file = await AppInstance.ShellViewModel.GetFileFromPathAsync(filePath); + if (AppInstance.ShellViewModel is not { } shellViewModel) + return; // Couldn't access the file and can't load any other properties - if (file is null) + var fileResult = await shellViewModel.GetFileFromPathAsync(filePath); + if (fileResult.Result is not { } file) return; // Can't load any other properties @@ -170,8 +169,8 @@ public override async Task GetSpecialPropertiesAsync() public async Task GetSystemFilePropertiesAsync() { - BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath)); - if (file is null) + var fileResult = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath)); + if (fileResult.Result is not { } file) { // Could not access file, can't show any other property return; @@ -179,14 +178,19 @@ public async Task GetSystemFilePropertiesAsync() var list = await FileProperty.RetrieveAndInitializePropertiesAsync(file); - list.Find(x => x.ID == "address").Value = - await LocationHelpers.GetAddressFromCoordinatesAsync((double?)list.Find( - x => x.Property == "System.GPS.LatitudeDecimal").Value, - (double?)list.Find(x => x.Property == "System.GPS.LongitudeDecimal").Value); + var addressProperty = list.Find(x => x.ID == "address"); + if (addressProperty is not null) + { + var latitude = list.Find(x => x.Property == "System.GPS.LatitudeDecimal")?.Value as double?; + var longitude = list.Find(x => x.Property == "System.GPS.LongitudeDecimal")?.Value as double?; + addressProperty.Value = await LocationHelpers.GetAddressFromCoordinatesAsync(latitude, longitude); + } var query = list .Where(fileProp => !(fileProp.Value is null && fileProp.IsReadOnly)) - .GroupBy(fileProp => fileProp.SectionResource) + .Select(fileProp => (Property: fileProp, Section: fileProp.SectionResource ?? string.Empty)) + .Where(item => item.Section.Length > 0) + .GroupBy(item => item.Section, item => item.Property) .Select(group => new FilePropertySection(group) { Key = group.Key }) .Where(section => !section.All(fileProp => fileProp.Value is null) || FileProperty.IsSectionApplicableForEmpty(section.Key, Item.FileExtension)) @@ -198,10 +202,9 @@ await LocationHelpers.GetAddressFromCoordinatesAsync((double?)list.Find( public async Task SyncPropertyChangesAsync() { - BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath)); - // Couldn't access the file to save properties - if (file is null) + var fileResult = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath)); + if (fileResult.Result is not { } file) return; var failedProperties = ""; @@ -210,18 +213,19 @@ public async Task SyncPropertyChangesAsync() { foreach (FileProperty prop in group) { - if (!prop.IsReadOnly && prop.Modified) + var propertyName = prop.Property; + if (!prop.IsReadOnly && prop.Modified && !string.IsNullOrEmpty(propertyName)) { - var newDict = new Dictionary + var newDict = new Dictionary { - { prop.Property, prop.Value } + { propertyName, prop.Value } }; try { if (file.Properties is not null) { - await file.Properties.SavePropertiesAsync(newDict); + await file.Properties.SaveNullablePropertiesAsync(newDict); } } catch @@ -241,27 +245,27 @@ public async Task SyncPropertyChangesAsync() public async Task ClearPropertiesAsync() { var failedProperties = new List(); - BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath)); - - if (file is null) + var fileResult = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath)); + if (fileResult.Result is not { } file) return; foreach (var group in ViewModel.PropertySections) { foreach (FileProperty prop in group) { - if (!prop.IsReadOnly) + var propertyName = prop.Property; + if (!prop.IsReadOnly && !string.IsNullOrEmpty(propertyName)) { - var newDict = new Dictionary + var newDict = new Dictionary { - { prop.Property, null } + { propertyName, null } }; try { if (file.Properties is not null) { - await file.Properties.SavePropertiesAsync(newDict); + await file.Properties.SaveNullablePropertiesAsync(newDict); } } catch @@ -275,7 +279,7 @@ public async Task ClearPropertiesAsync() _ = GetSystemFilePropertiesAsync(); } - private async void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + private async void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { diff --git a/src/Files.App/ViewModels/Properties/Items/FileProperty.cs b/src/Files.App/ViewModels/Properties/Items/FileProperty.cs index 1018d958cc32..632add679ce5 100644 --- a/src/Files.App/ViewModels/Properties/Items/FileProperty.cs +++ b/src/Files.App/ViewModels/Properties/Items/FileProperty.cs @@ -18,40 +18,40 @@ public sealed partial class FileProperty : ObservableObject /// /// The name to display /// - public string Name => LocalizedName ?? NameResource.GetLocalizedResource(); + public string Name => LocalizedName ?? NameResource?.GetLocalizedResource() ?? string.Empty; /// /// The name of the string resource for the property name /// - public string NameResource { get; set; } + public string? NameResource { get; set; } - public string LocalizedName { get; set; } + public string? LocalizedName { get; set; } /// /// The name of the section to display /// - public string Section + public string? Section => SectionResource?.GetLocalizedResource(); /// /// The name of the string resource for the section name /// - public string SectionResource { get; set; } + public string? SectionResource { get; set; } /// /// The identifier of the property to get, eg System.Media.Duration /// - public string Property { get; set; } + public string? Property { get; set; } /// /// The value of the property /// - public object Value { get; set; } + public object? Value { get; set; } /// /// The string value of the property's value /// - public string ValueText + public string? ValueText { get => ConvertToString(); set @@ -77,31 +77,31 @@ public string ValueText } } - public string PlaceholderText { get; set; } + public string? PlaceholderText { get; set; } /// /// This function is run on the value of the property before displaying it. /// Also serves as an alternative to the Converter property /// Note: should only be used on read only properties /// - public Func DisplayFunction { get; set; } + public Func? DisplayFunction { get; set; } /// /// The name of the display function to get from the display dictionary /// - public string DisplayFunctionName { get; set; } + public string? DisplayFunctionName { get; set; } /// /// The converter used to convert the property to a string, and vice versa if needed /// - public IValueConverter Converter => GetConverter(); + public IValueConverter? Converter => GetConverter(); public bool IsReadOnly { get; set; } = true; /// /// Should be used in instances where a property does not have a "Property" value, but needs to be idenitfiable in a list of properties /// - public string ID { get; set; } + public string? ID { get; set; } /// /// True if the property accepts multiple lines of text (e.g., System.Comment) @@ -121,7 +121,7 @@ public string ValueText /// If a property has an enumerated list of strings to display, add a dictionary in the JSON file that has the number as it's key /// and the string resource as its value /// - public Dictionary EnumeratedList { get; set; } + public Dictionary? EnumeratedList { get; set; } public FileProperty() { @@ -150,8 +150,7 @@ public FileProperty(string property, string nameResource, string sectionResource /// public void InitializeProperty() { - Func displayFunction; - if (!string.IsNullOrEmpty(DisplayFunctionName) && DisplayFuncs.TryGetValue(DisplayFunctionName, out displayFunction)) + if (!string.IsNullOrEmpty(DisplayFunctionName) && DisplayFuncs.TryGetValue(DisplayFunctionName, out var displayFunction)) { DisplayFunction = displayFunction; } @@ -164,17 +163,17 @@ public void InitializeProperty() /// public Task SaveValueToFile(BaseStorageFile file) { - if (!string.IsNullOrEmpty(Property) || file.Properties is null) + if (string.IsNullOrEmpty(Property) || file.Properties is null) { return Task.CompletedTask; } - var propsToSave = new Dictionary + var propsToSave = new Dictionary { - { Property, Converter.ConvertBack(Value, null, null, null) } + { Property, Converter?.ConvertBack(Value, null, null, null) ?? Value } }; - return file.Properties.SavePropertiesAsync(propsToSave).AsTask(); + return file.Properties.SaveNullablePropertiesAsync(propsToSave).AsTask(); } /// @@ -189,7 +188,7 @@ public void UpdateValueText() /// Call this function just after getting properties to set the property's converter based on the value type. /// For some reason, this does not work for arrays. In the case of arrays, override the converter /// - private IValueConverter GetConverter() + private IValueConverter? GetConverter() { if (Value is uint) { @@ -208,12 +207,12 @@ private IValueConverter GetConverter() if (Value is not null && Value.GetType().IsArray) { - if (Value.GetType().GetElementType().Equals(typeof(string))) + if (Value.GetType().GetElementType() == typeof(string)) { return new StringArrayToStringConverter(); } - if (Value.GetType().GetElementType().Equals(typeof(double))) + if (Value.GetType().GetElementType() == typeof(double)) { return new DoubleArrayToStringConverter(); } @@ -271,7 +270,7 @@ private IValueConverter GetConverter() /// /// The input string /// - private object ConvertBack(string value) + private object? ConvertBack(string? value) { if (Converter is not null && value is not null) { @@ -297,15 +296,14 @@ private object ConvertBack(string value) public async static Task> RetrieveAndInitializePropertiesAsync(BaseStorageFile file, string path = Constants.ResourceFilePaths.DetailsPagePropertiesJsonPath) { // cache the contents of the file to avoid repeatedly reading the file - string text; - if (!cachedPropertiesListFiles.TryGetValue(path, out text)) + if (!cachedPropertiesListFiles.TryGetValue(path, out var text)) { var propertiesJsonFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(path)); text = await FileIO.ReadTextAsync(propertiesJsonFile); cachedPropertiesListFiles[path] = text; } - List list = JsonSerializer.Deserialize>(text); + List list = JsonSerializer.Deserialize>(text) ?? []; var propsToGet = new List(); @@ -319,10 +317,10 @@ public async static Task> RetrieveAndInitializePropertiesAsyn #if DEBUG // This makes it much easier to debug issues with the property list - var keyValuePairs = new Dictionary(); + var keyValuePairs = new Dictionary(); foreach (var prop in propsToGet) { - object val = null; + object? val = null; try { if (file.Properties is not null) @@ -337,10 +335,11 @@ public async static Task> RetrieveAndInitializePropertiesAsyn keyValuePairs.Add(prop, val); } #else - IDictionary keyValuePairs = new Dictionary(); + IDictionary keyValuePairs = new Dictionary(); if (file.Properties is not null) { - keyValuePairs = await file.Properties.RetrievePropertiesAsync(propsToGet); + keyValuePairs = (await file.Properties.RetrievePropertiesAsync(propsToGet)) + .ToDictionary(pair => pair.Key, pair => (object?)pair.Value); } #endif @@ -348,7 +347,8 @@ public async static Task> RetrieveAndInitializePropertiesAsyn { if (!string.IsNullOrEmpty(prop.Property)) { - prop.Value = keyValuePairs[prop.Property]; + keyValuePairs.TryGetValue(prop.Property, out var value); + prop.Value = value; } prop.InitializeProperty(); diff --git a/src/Files.App/ViewModels/Properties/Items/FilePropertySection.cs b/src/Files.App/ViewModels/Properties/Items/FilePropertySection.cs index 2bcd674810f4..4abc9b70d89e 100644 --- a/src/Files.App/ViewModels/Properties/Items/FilePropertySection.cs +++ b/src/Files.App/ViewModels/Properties/Items/FilePropertySection.cs @@ -14,7 +14,7 @@ public FilePropertySection(IEnumerable items) public Visibility Visibility { get; set; } - public string Key { get; set; } + public required string Key { get; set; } public string Title => Key.GetLocalizedResource(); diff --git a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs index dcabf9d1e989..d5dd29c34a5b 100644 --- a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs @@ -20,12 +20,9 @@ public FolderProperties( DispatcherQueue coreDispatcher, ListedItem item, IShellPage instance) + : base(viewModel, tokenSource, coreDispatcher, instance) { - ViewModel = viewModel; - TokenSource = tokenSource; - Dispatcher = coreDispatcher; Item = item; - AppInstance = instance; GetBaseProperties(); @@ -40,7 +37,7 @@ public override void GetBaseProperties() ViewModel.OriginalItemName = Item.Name; ViewModel.ItemType = Item.ItemType; ViewModel.ItemLocation = (Item as RecycleBinItem)?.ItemOriginalFolder ?? - (Path.IsPathRooted(Item.ItemPath) ? Path.GetDirectoryName(Item.ItemPath) : Item.ItemPath); + (Path.IsPathRooted(Item.ItemPath) ? Path.GetDirectoryName(Item.ItemPath) ?? string.Empty : Item.ItemPath); ViewModel.ItemModifiedTimestampReal = Item.ItemDateModifiedReal; ViewModel.ItemCreatedTimestampReal = Item.ItemDateCreatedReal; ViewModel.LoadCustomIcon = Item.LoadCustomIcon; @@ -112,7 +109,10 @@ public async override Task GetSpecialPropertiesAsync() } string folderPath = (Item as IShortcutItem)?.TargetPath ?? Item.ItemPath; - BaseStorageFolder storageFolder = await AppInstance.ShellViewModel.GetFolderFromPathAsync(folderPath); + if (AppInstance.ShellViewModel is not { } shellViewModel) + return; + + var storageFolder = (await shellViewModel.GetFolderFromPathAsync(folderPath)).Result; if (storageFolder is not null) { @@ -200,7 +200,7 @@ private async Task GetFolderSizeAsync(string folderPath, CancellationToken token SetItemsCountString(); } - private async void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + private async void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { diff --git a/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs b/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs index e21f5b30b833..d24576069dce 100644 --- a/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs @@ -12,12 +12,9 @@ internal sealed class LibraryProperties : BaseProperties public LibraryProperties(SelectedItemsPropertiesViewModel viewModel, CancellationTokenSource tokenSource, DispatcherQueue coreDispatcher, LibraryItem item, IShellPage instance) + : base(viewModel, tokenSource, coreDispatcher, instance) { - ViewModel = viewModel; - TokenSource = tokenSource; - Dispatcher = coreDispatcher; Library = item; - AppInstance = instance; GetBaseProperties(); ViewModel.PropertyChanged += ViewModel_PropertyChanged; @@ -64,7 +61,10 @@ public async override Task GetSpecialPropertiesAsync() ViewModel.LoadFileIcon = true; } - BaseStorageFile libraryFile = await AppInstance.ShellViewModel.GetFileFromPathAsync(Library.ItemPath); + if (AppInstance.ShellViewModel is not { } shellViewModel) + return; + + BaseStorageFile? libraryFile = (await shellViewModel.GetFileFromPathAsync(Library.ItemPath)).Result; if (libraryFile is not null) { ViewModel.ItemCreatedTimestampReal = libraryFile.DateCreated; @@ -81,8 +81,8 @@ public async override Task GetSpecialPropertiesAsync() { foreach (var path in Library.Folders) { - BaseStorageFolder folder = await AppInstance.ShellViewModel.GetFolderFromPathAsync(path); - if (!string.IsNullOrEmpty(folder.Path)) + BaseStorageFolder? folder = (await AppInstance.ShellViewModel.GetFolderFromPathAsync(path)).Result; + if (folder is not null && !string.IsNullOrEmpty(folder.Path)) { storageFolders.Add(folder); } @@ -138,7 +138,7 @@ private async Task GetLibrarySizeAsync(List storageFolders, C SetItemsCountString(); } - private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { diff --git a/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs b/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs index c5a33a860005..76577ad4a67e 100644 --- a/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs +++ b/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs @@ -59,7 +59,8 @@ public MainPropertiesViewModel(Window window, Frame mainFrame, BaseProperties ba foreach (var navItem in NavigationItems) FlatNavigationItems.Add(new FlatSidebarItem(navItem, 0)); - SelectedNavigationItem = NavigationItems.First(x => x.ItemType == PropertiesNavigationViewItemType.General); + _SelectedNavigationItem = NavigationItems.First(x => x.ItemType == PropertiesNavigationViewItemType.General); + NavigateToPage(_SelectedNavigationItem); } private void NavigateToPage(PropertiesNavigationItem item) diff --git a/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs b/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs index 367e8b4aaee5..e6883265a832 100644 --- a/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs @@ -50,7 +50,7 @@ public string DisableInheritanceOption } } - private AccessControlList _AccessControlList; + private AccessControlList _AccessControlList = new(); public AccessControlList AccessControlList { get => _AccessControlList; @@ -146,7 +146,7 @@ public SecurityAdvancedViewModel(PropertiesPageNavigationParameter parameter) } ; - LoadShieldIconResource(); + ShieldIconFileInfo = LoadShieldIconResource(); LoadAccessControlEntry(); @@ -155,7 +155,7 @@ public SecurityAdvancedViewModel(PropertiesPageNavigationParameter parameter) RemoveAccessControlEntryCommand = new AsyncRelayCommand(ExecuteRemoveAccessControlEntryCommandAsync); } - private void LoadShieldIconResource() + private IconFileInfo LoadShieldIconResource() { string imageres = System.IO.Path.Combine(Constants.UserEnvironmentPaths.SystemRootPath, "System32", "imageres.dll"); @@ -164,7 +164,7 @@ private void LoadShieldIconResource() new List() { Constants.ImageRes.ShieldIcon }, 16); - ShieldIconFileInfo = imageResList.First(); + return imageResList.First(); } private void LoadAccessControlEntry() diff --git a/src/Files.App/ViewModels/Properties/SecurityViewModel.cs b/src/Files.App/ViewModels/Properties/SecurityViewModel.cs index 30da282c7d7d..abb794e18635 100644 --- a/src/Files.App/ViewModels/Properties/SecurityViewModel.cs +++ b/src/Files.App/ViewModels/Properties/SecurityViewModel.cs @@ -132,16 +132,21 @@ await MainWindow.Instance.DispatcherQueue.EnqueueAsync(() => private async Task ExecuteRemoveAccessControlEntryCommandAsync() { + if (SelectedAccessControlEntry is not { } selectedEntry) + return; + await MainWindow.Instance.DispatcherQueue.EnqueueAsync(() => { // Get index of the ACE - var index = AccessControlList.AccessControlEntries.IndexOf(SelectedAccessControlEntry); + var index = AccessControlList.AccessControlEntries.IndexOf(selectedEntry); + if (index < 0) + return; // Run Win32API var win32Result = StorageSecurityService.DeleteAce(_path, (uint)index); // Remove the ACE - AccessControlList.AccessControlEntries.Remove(SelectedAccessControlEntry); + AccessControlList.AccessControlEntries.Remove(selectedEntry); if (AccessControlList.AccessControlEntries.Count == 0) return; diff --git a/src/Files.App/ViewModels/Settings/ActionsViewModel.cs b/src/Files.App/ViewModels/Settings/ActionsViewModel.cs index 6967086783ea..34e2c38036da 100644 --- a/src/Files.App/ViewModels/Settings/ActionsViewModel.cs +++ b/src/Files.App/ViewModels/Settings/ActionsViewModel.cs @@ -20,7 +20,7 @@ public sealed partial class ActionsViewModel : ObservableObject public ObservableCollection ValidActionItems { get; } = []; public ObservableCollection AllActionItems { get; } = []; - private ObservableCollection _FilteredActionItems; + private ObservableCollection _FilteredActionItems = []; public ObservableCollection FilteredActionItems { get { return _FilteredActionItems; } @@ -469,4 +469,4 @@ public void FilterItems(string query) } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs index f4af5fa50be1..f89d98bd6916 100644 --- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs @@ -160,9 +160,10 @@ private async Task ImportSettingsAsync() try { var file = await StorageHelpers.ToStorageItem(filePath); + if (file is null) + return; - var zipFolder = await ZipStorageFolder.FromStorageFileAsync(file); - if (zipFolder is null) + if (await ZipStorageFolder.FromStorageFileAsync(file) is not ZipStorageFolder zipFolder) return; var localFolderPath = ApplicationData.Current.LocalFolder.Path; @@ -220,11 +221,12 @@ private async Task ExportSettingsAsync() Win32PInvoke.CloseHandle(handle); var file = await StorageHelpers.ToStorageItem(filePath); + if (file is null) + return; await ZipStorageFolder.InitArchive(file, OutArchiveFormat.Zip); - var zipFolder = (ZipStorageFolder)await ZipStorageFolder.FromStorageFileAsync(file); - if (zipFolder is null) + if (await ZipStorageFolder.FromStorageFileAsync(file) is not ZipStorageFolder zipFolder) return; var localFolderPath = ApplicationData.Current.LocalFolder.Path; @@ -483,4 +485,4 @@ public async Task ReadState() } } } -} \ No newline at end of file +} diff --git a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs index ba9a7d986dae..bfd9ddac5a8d 100644 --- a/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AppearanceViewModel.cs @@ -61,6 +61,7 @@ public AppearanceViewModel(IUserSettingsService userSettingsService, IResourcesS selectedBackdropMaterial = BackdropMaterialTypes[UserSettingsService.AppearanceSettingsService.AppThemeBackdropMaterial]; AppThemeResources = AppThemeResourceFactory.AppThemeResources; + selectedAppThemeResources = AppThemeResources[0]; // Background image fit options @@ -296,7 +297,7 @@ public string AppThemeBackgroundImageSource } } - private string selectedImageStretchType; + private string selectedImageStretchType = string.Empty; public string SelectedImageStretchType { get => selectedImageStretchType; @@ -323,7 +324,7 @@ public float AppThemeBackgroundImageOpacity } } - private string selectedImageVerticalAlignmentType; + private string selectedImageVerticalAlignmentType = string.Empty; public string SelectedImageVerticalAlignmentType { get => selectedImageVerticalAlignmentType; @@ -336,7 +337,7 @@ public string SelectedImageVerticalAlignmentType } } - private string selectedImageHorizontalAlignmentType; + private string selectedImageHorizontalAlignmentType = string.Empty; public string SelectedImageHorizontalAlignmentType { get => selectedImageHorizontalAlignmentType; @@ -405,7 +406,7 @@ public bool ShowShelfPaneToggleButton } } - private string selectedStatusCenterVisibilityOption; + private string selectedStatusCenterVisibilityOption = string.Empty; public string SelectedStatusCenterVisibilityOption { get => selectedStatusCenterVisibilityOption; diff --git a/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs b/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs index 75fda57a8da9..80cd5b599c54 100644 --- a/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs +++ b/src/Files.App/ViewModels/Settings/DevToolsViewModel.cs @@ -53,7 +53,7 @@ public bool IsIDENameValid set => SetProperty(ref _IsIDENameValid, value); } - private string _IDEPath; + private string _IDEPath = string.Empty; public string IDEPath { get => _IDEPath; @@ -72,7 +72,7 @@ public string IDEPath } } - private string _IDEName; + private string _IDEName = string.Empty; public string IDEName { get => _IDEName; @@ -109,7 +109,7 @@ public DevToolsViewModel() TestIDECommand = new RelayCommand(DoTestIDE); } - private string selectedOpenInIDEOption; + private string selectedOpenInIDEOption = string.Empty; public string SelectedOpenInIDEOption { get => selectedOpenInIDEOption; diff --git a/src/Files.App/ViewModels/Settings/FoldersViewModel.cs b/src/Files.App/ViewModels/Settings/FoldersViewModel.cs index 22838e7157c1..d17ef396de83 100644 --- a/src/Files.App/ViewModels/Settings/FoldersViewModel.cs +++ b/src/Files.App/ViewModels/Settings/FoldersViewModel.cs @@ -103,7 +103,7 @@ public bool ShowDotFiles } } - private string selectedOpenFilesWithSingleClickOption; + private string selectedOpenFilesWithSingleClickOption = string.Empty; public string SelectedOpenFilesWithSingleClickOption { get => selectedOpenFilesWithSingleClickOption; @@ -116,7 +116,7 @@ public string SelectedOpenFilesWithSingleClickOption } } - private string selectedOpenFoldersWithSingleClickOption; + private string selectedOpenFoldersWithSingleClickOption = string.Empty; public string SelectedOpenFoldersWithSingleClickOption { get => selectedOpenFoldersWithSingleClickOption; @@ -129,7 +129,7 @@ public string SelectedOpenFoldersWithSingleClickOption } } - private string selectedOpenFoldersInColumnsViewWithSingleClickOption; + private string selectedOpenFoldersInColumnsViewWithSingleClickOption = string.Empty; public string SelectedOpenFoldersInColumnsViewWithSingleClickOption { get => selectedOpenFoldersInColumnsViewWithSingleClickOption; @@ -282,7 +282,7 @@ public bool ShowCheckboxesWhenSelectingItems } } - private string sizeUnitFormat; + private string sizeUnitFormat = string.Empty; public string SizeUnitFormat { get => sizeUnitFormat; diff --git a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs index db6160d96f93..bfb59cf7abad 100644 --- a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs +++ b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs @@ -16,7 +16,7 @@ public sealed partial class GeneralViewModel : ObservableObject, IDisposable private bool disposed; - private ReadOnlyCollection addFlyoutItemsSource; + private ReadOnlyCollection addFlyoutItemsSource = []; public RelayCommand ChangePageCommand { get; } public RelayCommand RemovePageCommand { get; } @@ -78,7 +78,7 @@ public int SelectedAppLanguageIndex } } - public List DateFormats { get; set; } + public List DateFormats { get; set; } = []; public ObservableCollection AppLanguages => AppLanguageHelper.SupportedLanguages; @@ -154,9 +154,10 @@ private void InitStartupSettingsRecentFoldersFlyout() Tooltip = Strings.Home.GetLocalizedResource() }); recentsItem.Items.Add(new MenuFlyoutItemViewModel(Strings.Browse.GetLocalizedResource()) { Command = AddPageCommand }); + AddFlyoutItemsSource = new List { recentsItem }.AsReadOnly(); } - private void PagesOnStartupList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + private void PagesOnStartupList_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (PagesOnStartupList.Count > 0) UserSettingsService.GeneralSettingsService.TabsOnStartupList = PagesOnStartupList.Select((p) => p.Path).ToList(); @@ -353,12 +354,13 @@ private void ChangePageAsync() PagesOnStartupList[SelectedPageIndex] = new PageOnStartupViewModel(filePath); } - private void RemovePage(PageOnStartupViewModel page) + private void RemovePage(PageOnStartupViewModel? page) { - PagesOnStartupList.Remove(page); + if (page is not null) + PagesOnStartupList.Remove(page); } - private async Task AddPageAsync(string path = null) + private async Task AddPageAsync(string? path = null) { if (string.IsNullOrWhiteSpace(path)) { @@ -575,7 +577,7 @@ public bool ShowOpenTerminal } } - private string selectedShellPaneArrangementType; + private string selectedShellPaneArrangementType = string.Empty; public string SelectedShellPaneArrangementType { get => selectedShellPaneArrangementType; diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index 47cbfc458de9..bc1fae4ea2a7 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -67,7 +67,7 @@ public sealed partial class ShellViewModel : ObservableObject, IDisposable // Only used for Binding and ApplyFilesAndFoldersChangesAsync, don't manipulate on this! public BulkConcurrentObservableCollection FilesAndFolders { get; } - private LayoutPreferencesManager folderSettings = null; + private readonly LayoutPreferencesManager folderSettings; private ListedItem? currentFolder; public ListedItem? CurrentFolder @@ -160,51 +160,51 @@ public GitProperties EnabledGitProperties } } - public CollectionViewSource viewSource; + public CollectionViewSource viewSource = new(); - private FileSystemWatcher watcher; + private FileSystemWatcher? watcher; - private static BitmapImage shieldIcon; + private static BitmapImage? shieldIcon; private CancellationTokenSource addFilesCTS; private CancellationTokenSource semaphoreCTS; private CancellationTokenSource loadPropsCTS; private CancellationTokenSource watcherCTS; - private CancellationTokenSource searchCTS; - private CancellationTokenSource updateTagGroupCTS; + private CancellationTokenSource? searchCTS; + private CancellationTokenSource? updateTagGroupCTS; private CancellationTokenSource? filterDebounceCS; private CancellationTokenSource? networkAvailabilityCTS; - public event EventHandler FocusFilterHeader; + public event EventHandler? FocusFilterHeader; - public event EventHandler DirectoryInfoUpdated; + public event EventHandler? DirectoryInfoUpdated; - public event EventHandler GitDirectoryUpdated; + public event EventHandler? GitDirectoryUpdated; - public event EventHandler> OnSelectionRequestedEvent; + public event EventHandler>? OnSelectionRequestedEvent; - public string WorkingDirectory { get; private set; } + public string WorkingDirectory { get; private set; } = string.Empty; public string? GitDirectory { get; private set; } public bool IsValidGitDirectory { get; private set; } - public List DesktopIni { get; private set; } + public List DesktopIni { get; private set; } = []; private StorageFolderWithPath? currentStorageFolder; - private StorageFolderWithPath workingRoot; + private StorageFolderWithPath? workingRoot; - public delegate void WorkingDirectoryModifiedEventHandler(object sender, WorkingDirectoryModifiedEventArgs e); + public delegate void WorkingDirectoryModifiedEventHandler(object? sender, WorkingDirectoryModifiedEventArgs e); - public event WorkingDirectoryModifiedEventHandler WorkingDirectoryModified; + public event WorkingDirectoryModifiedEventHandler? WorkingDirectoryModified; - public delegate void PageTypeUpdatedEventHandler(object sender, PageTypeUpdatedEventArgs e); + public delegate void PageTypeUpdatedEventHandler(object? sender, PageTypeUpdatedEventArgs e); - public event PageTypeUpdatedEventHandler PageTypeUpdated; + public event PageTypeUpdatedEventHandler? PageTypeUpdated; - public delegate void ItemLoadStatusChangedEventHandler(object sender, ItemLoadStatusChangedEventArgs e); + public delegate void ItemLoadStatusChangedEventHandler(object? sender, ItemLoadStatusChangedEventArgs e); - public event ItemLoadStatusChangedEventHandler ItemLoadStatusChanged; + public event ItemLoadStatusChangedEventHandler? ItemLoadStatusChanged; public void InvokeFocusFilterHeader() { @@ -218,7 +218,7 @@ public async Task SetWorkingDirectoryAsync(string? value) var isLibrary = false; string? name = null; - if (App.LibraryManager.TryGetLibrary(value, out LibraryLocationItem library)) + if (App.LibraryManager.TryGetLibrary(value, out var library)) { isLibrary = true; name = library.Text; @@ -229,7 +229,7 @@ public async Task SetWorkingDirectoryAsync(string? value) if (isLibrary || !Path.IsPathRooted(value)) workingRoot = currentStorageFolder = null; else if (!Path.IsPathRooted(WorkingDirectory) || Path.GetPathRoot(WorkingDirectory) != Path.GetPathRoot(value)) - workingRoot = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(value)); + workingRoot = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(value)); if (value == "Home" || value == "ReleaseNotes" || value == "Settings") currentStorageFolder = null; @@ -258,7 +258,7 @@ public async Task> GetFolderFromPathAsync(st await getFileOrFolderSemaphore.WaitAsync(cancellationToken); try { - return await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(value, workingRoot, currentStorageFolder)); + return await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(value, workingRoot, currentStorageFolder)); } finally { @@ -271,7 +271,7 @@ public async Task> GetFileFromPathAsync(string await getFileOrFolderSemaphore.WaitAsync(cancellationToken); try { - return await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(value, workingRoot, currentStorageFolder)); + return await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(value, workingRoot, currentStorageFolder)); } finally { @@ -743,7 +743,7 @@ private async void LayoutModeChangeRequested(object? sender, LayoutModeEventArgs await dispatcherQueue.EnqueueOrInvokeAsync(CheckForBackgroundImage, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); } - private async void RecycleBinRefreshRequestedAsync(object sender, FileSystemEventArgs e) + private async void RecycleBinRefreshRequestedAsync(object? sender, FileSystemEventArgs e) { if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; @@ -754,7 +754,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => }); } - private async void RecycleBinItemDeletedAsync(object sender, FileSystemEventArgs e) + private async void RecycleBinItemDeletedAsync(object? sender, FileSystemEventArgs e) { if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; @@ -765,7 +765,7 @@ private async void RecycleBinItemDeletedAsync(object sender, FileSystemEventArgs await ApplyFilesAndFoldersChangesAsync(); } - private async void RecycleBinItemCreatedAsync(object sender, FileSystemEventArgs e) + private async void RecycleBinItemCreatedAsync(object? sender, FileSystemEventArgs e) { if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase)) return; @@ -775,6 +775,8 @@ private async void RecycleBinItemCreatedAsync(object sender, FileSystemEventArgs return; var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem); + if (shellFileItem is null) + return; var newListedItem = await AddFileOrFolderFromShellFile(shellFileItem); if (newListedItem is null) @@ -1126,7 +1128,8 @@ private Task RequestSelectionAsync(List itemsToSelect) { // Don't notify if shell page is not the active pane (eg. Dual Pane) // https://github.com/files-community/Files/issues/17427 - if (WorkingDirectory != ContentPageContext.ShellPage!.ShellViewModel.WorkingDirectory) + var activeShellViewModel = ContentPageContext.ShellPage?.ShellViewModel; + if (activeShellViewModel is null || WorkingDirectory != activeShellViewModel.WorkingDirectory) return Task.CompletedTask; // Don't notify if there weren't listed items @@ -1274,7 +1277,10 @@ public Task ReloadItemGroupHeaderImagesAsync() { foreach (var gp in FilesAndFolders.GroupedCollection.ToList()) { - var img = await GetItemTypeGroupIcon(gp.FirstOrDefault()); + if (gp.FirstOrDefault() is not { } item) + continue; + + var img = await GetItemTypeGroupIcon(item); await dispatcherQueue.EnqueueOrInvokeAsync(() => { gp.Model.ImageSource = img; @@ -1298,7 +1304,7 @@ public bool IsLoadingItems set => isLoadingItems = value; } - private async Task GetShieldIcon() + private async Task GetShieldIcon() { shieldIcon ??= await UIHelpers.GetShieldIconResource(); @@ -1526,9 +1532,9 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => item.FileFRN = fileFRN; item.FileTags = fileTag; item.IsElevationRequired = CheckElevationRights(item); - item.ImageDimensions = extraProperties?.Result["System.Image.Dimensions"]?.ToString() ?? string.Empty; - item.FileVersion = extraProperties?.Result["System.FileVersion"]?.ToString() ?? string.Empty; - item.MediaDuration = ulong.TryParse(extraProperties?.Result["System.Media.Duration"]?.ToString(), out ulong duration) + item.ImageDimensions = extraProperties?.Result?["System.Image.Dimensions"]?.ToString() ?? string.Empty; + item.FileVersion = extraProperties?.Result?["System.FileVersion"]?.ToString() ?? string.Empty; + item.MediaDuration = ulong.TryParse(extraProperties?.Result?["System.Media.Duration"]?.ToString(), out ulong duration) ? TimeSpan.FromTicks((long)duration).ToString(@"hh\:mm\:ss") : string.Empty; @@ -1559,7 +1565,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => { if (!item.IsShortcut && !item.IsHiddenItem && !FtpHelpers.IsFtpPath(item.ItemPath)) { - BaseStorageFolder matchingStorageFolder = await GetFolderFromPathAsync(item.ItemPath, cts.Token); + BaseStorageFolder? matchingStorageFolder = await GetFolderFromPathAsync(item.ItemPath, cts.Token); if (matchingStorageFolder is not null) { if (matchingStorageFolder.DisplayName != item.Name && !matchingStorageFolder.DisplayName.StartsWith("$R", StringComparison.Ordinal)) @@ -1594,12 +1600,12 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => item.FileFRN = fileFRN; item.FileTags = fileTag; - if (extraProperties is not null) + if (extraProperties?.Result is { } properties) { // Drive Storage Details - if (extraProperties.Result["System.SFGAOFlags"] is uint attributesRaw && - extraProperties.Result["System.Capacity"] is ulong capacityRaw && - extraProperties.Result["System.FreeSpace"] is ulong freeSpaceRaw && + if (properties["System.SFGAOFlags"] is uint attributesRaw && + properties["System.Capacity"] is ulong capacityRaw && + properties["System.FreeSpace"] is ulong freeSpaceRaw && ((SFGAO_FLAGS)attributesRaw).HasFlag(SFGAO_FLAGS.SFGAO_REMOVABLE) && !((SFGAO_FLAGS)attributesRaw).HasFlag(SFGAO_FLAGS.SFGAO_FILESYSTEM)) { @@ -1667,7 +1673,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => } } - if (loadGroupHeaderInfo) + if (loadGroupHeaderInfo && gp is not null) { cts.Token.ThrowIfCancellationRequested(); await SafetyExtensions.IgnoreExceptions(() => @@ -1817,7 +1823,7 @@ await SafetyExtensions.IgnoreExceptions(() => if (matchingStorageItem is not null) { - using StorageItemThumbnail headerThumbnail = await FilesystemTasks.Wrap(() => matchingStorageItem.GetThumbnailAsync(ThumbnailMode.DocumentsView, 36, ThumbnailOptions.UseCurrentScale).AsTask()); + using StorageItemThumbnail? headerThumbnail = await FilesystemTasks.Wrap(() => matchingStorageItem.GetThumbnailAsync(ThumbnailMode.DocumentsView, 36, ThumbnailOptions.UseCurrentScale).AsTask()); if (headerThumbnail is not null) { await dispatcherQueue.EnqueueOrInvokeAsync(async () => @@ -1844,12 +1850,12 @@ await dispatcherQueue.EnqueueOrInvokeAsync(async () => return groupImage; } - public void RefreshItems(string? previousDir, Action postLoadCallback = null) + public void RefreshItems(string? previousDir, Action? postLoadCallback = null) { _ = RapidAddItemsToCollectionAsync(WorkingDirectory, previousDir, postLoadCallback); } - private async Task RapidAddItemsToCollectionAsync(string path, string? previousDir, Action postLoadCallback) + private async Task RapidAddItemsToCollectionAsync(string path, string? previousDir, Action? postLoadCallback) { IsSearchResults = false; HasNoWatcher = false; @@ -1890,7 +1896,7 @@ private async Task RapidAddItemsToCollectionAsync(string path, string? previousD if (path.ToLowerInvariant().EndsWith(ShellLibraryItem.EXTENSION, StringComparison.Ordinal)) { - if (App.LibraryManager.TryGetLibrary(path, out LibraryLocationItem library) && !library.IsEmpty) + if (App.LibraryManager.TryGetLibrary(path, out var library) && !library.IsEmpty) { var libItem = new LibraryItem(library); foreach (var folder in library.Folders) @@ -2044,17 +2050,20 @@ private async Task EnumerateItemsFromStandardFolderAsync(string path, Cance if (!res) return -1; - currentStorageFolder = res.Result; - rootFolder = currentStorageFolder.Item; + if (res.Result is not { } storageFolder) + return -1; + + currentStorageFolder = storageFolder; + rootFolder = storageFolder.Item; enumFromStorageFolder = true; } else { var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder)); - if (res) + if (res.Result is { } storageFolder) { - currentStorageFolder = res.Result; - rootFolder = currentStorageFolder.Item; + currentStorageFolder = storageFolder; + rootFolder = storageFolder.Item; } else if (res == FileSystemStatusCode.Unauthorized) { @@ -2079,7 +2088,7 @@ private async Task EnumerateItemsFromStandardFolderAsync(string path, Cance var pathRoot = Path.GetPathRoot(path); if (Path.IsPathRooted(path) && pathRoot == path) { - rootFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); + rootFolder ??= await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); if (await FolderHelpers.CheckBitlockerStatusAsync(rootFolder, WorkingDirectory)) await ContextMenu.InvokeVerb("unlock-bde", pathRoot); } @@ -2099,7 +2108,7 @@ private async Task EnumerateItemsFromStandardFolderAsync(string path, Cance FileImage = null, LoadFileIcon = false, ItemPath = string.IsNullOrEmpty(rootFolder?.Path) ? currentStorageFolder?.Path ?? string.Empty : rootFolder.Path, - FileSize = null, + FileSize = string.Empty, FileSizeBytes = 0, }; @@ -2162,7 +2171,7 @@ private async Task EnumerateItemsFromStandardFolderAsync(string path, Cance Opacity = opacity, LoadFileIcon = false, ItemPath = path, - FileSize = null, + FileSize = string.Empty, FileSizeBytes = 0, }; @@ -2215,7 +2224,7 @@ await Task.Run(async () => Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); }); - rootFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); + rootFolder ??= await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path)); if (rootFolder is not null) { if (rootFolder.DisplayName is not null) @@ -2233,11 +2242,13 @@ await Task.Run(async () => } } - private async Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath currentStorageFolder, CancellationToken cancellationToken) + private async Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath? currentStorageFolder, CancellationToken cancellationToken) { if (rootFolder is null) return; + currentStorageFolder ??= new StorageFolderWithPath(rootFolder, path); + if (rootFolder is IPasswordProtectedItem ppis) ppis.PasswordRequestedCallback = async (item) => { @@ -2361,24 +2372,24 @@ public void CheckForBackgroundImage() FolderBackgroundImageHorizontalAlignment = HorizontalAlignment.Center; } - public async Task CheckCloudDriveSyncStatusAsync(IStorageItem item) + public async Task CheckCloudDriveSyncStatusAsync(IStorageItem? item) { int? syncStatus = null; if (item is BaseStorageFile file && file.Properties is not null) { var extraProperties = await FilesystemTasks.Wrap(() => file.Properties.RetrievePropertiesAsync(["System.FilePlaceholderStatus"]).AsTask()); - if (extraProperties) - syncStatus = (int?)(uint?)extraProperties.Result["System.FilePlaceholderStatus"]; + if (extraProperties.Result is { } properties) + syncStatus = (int?)(uint?)properties["System.FilePlaceholderStatus"]; } else if (item is BaseStorageFolder folder && folder.Properties is not null) { var extraProperties = await FilesystemTasks.Wrap(() => folder.Properties.RetrievePropertiesAsync(["System.FilePlaceholderStatus", "System.FileOfflineAvailabilityStatus"]).AsTask()); - if (extraProperties) + if (extraProperties.Result is { } properties) { - syncStatus = (int?)(uint?)extraProperties.Result["System.FileOfflineAvailabilityStatus"]; + syncStatus = (int?)(uint?)properties["System.FileOfflineAvailabilityStatus"]; // If no FileOfflineAvailabilityStatus, check FilePlaceholderStatus - syncStatus ??= (int?)(uint?)extraProperties.Result["System.FilePlaceholderStatus"]; + syncStatus ??= (int?)(uint?)properties["System.FilePlaceholderStatus"]; } } @@ -2418,6 +2429,9 @@ await Task.Factory.StartNew(() => if (rootFolder.AreQueryOptionsSupported(options)) { var itemQueryResult = rootFolder.CreateItemQueryWithOptions(options).ToStorageItemQueryResult(); + if (itemQueryResult is null) + return; + itemQueryResult.ContentsChanged += ItemQueryResult_ContentsChanged; // Just get one item to start getting notifications @@ -2509,9 +2523,9 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat if (hasSyncStatus) notifyFilters |= FILE_NOTIFY_CHANGE_ATTRIBUTES; - var overlapped = new OVERLAPPED(); + var overlapped = new NativeOverlapped(); using var eventHandle = PInvoke.CreateEvent(null, false, false, null); - overlapped.hEvent = eventHandle.DangerousGetHandle(); + overlapped.EventHandle = eventHandle.DangerousGetHandle(); const uint INFINITE = 0xFFFFFFFF; while (x.Status != AsyncStatus.Canceled) @@ -2523,10 +2537,15 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat ref var notifyInformation = ref Unsafe.As(ref buff[0]); if (x.Status != AsyncStatus.Canceled) { - ReadDirectoryChangesW(hWatchDir, pBuff, - 4096, false, - notifyFilters, null, - ref overlapped, null); + PInvoke.ReadDirectoryChanges( + new Windows.Win32.Foundation.HANDLE(hWatchDir), + pBuff, + 4096, + false, + (Windows.Win32.Storage.FileSystem.FILE_NOTIFY_CHANGE)notifyFilters, + null, + &overlapped, + null); } else { @@ -2537,7 +2556,7 @@ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStat if (x.Status == AsyncStatus.Canceled) break; - var rc = WaitForSingleObjectEx(overlapped.hEvent, INFINITE, true); + var rc = WaitForSingleObjectEx(overlapped.EventHandle, INFINITE, true); Debug.WriteLine("wait done: {0}", rand); uint offset = 0; @@ -2620,9 +2639,9 @@ private void WatchForGitChanges() var rand = Guid.NewGuid(); var notifyFilters = FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_CREATION; - var overlapped = new OVERLAPPED(); + var overlapped = new NativeOverlapped(); using var eventHandle = PInvoke.CreateEvent(null, false, false, null); - overlapped.hEvent = eventHandle.DangerousGetHandle(); + overlapped.EventHandle = eventHandle.DangerousGetHandle(); const uint INFINITE = 0xFFFFFFFF; while (x.Status != AsyncStatus.Canceled) @@ -2635,15 +2654,20 @@ private void WatchForGitChanges() if (x.Status == AsyncStatus.Canceled) break; - ReadDirectoryChangesW(hWatchDir, pBuff, - 4096, true, - notifyFilters, null, - ref overlapped, null); + PInvoke.ReadDirectoryChanges( + new Windows.Win32.Foundation.HANDLE(hWatchDir), + pBuff, + 4096, + true, + (Windows.Win32.Storage.FileSystem.FILE_NOTIFY_CHANGE)notifyFilters, + null, + &overlapped, + null); if (x.Status == AsyncStatus.Canceled) break; - var rc = WaitForSingleObjectEx(overlapped.hEvent, INFINITE, true); + var rc = WaitForSingleObjectEx(overlapped.EventHandle, INFINITE, true); uint offset = 0; ref var notifyInfo = ref Unsafe.As(ref buff[offset]); @@ -2842,7 +2866,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => Debug.WriteLine("aProcessQueueAction done: {0}", rand); } - public Task AddFileOrFolderFromShellFile(ShellFileItem item) + public Task AddFileOrFolderFromShellFile(ShellFileItem item) { return item.IsFolder ? @@ -2909,13 +2933,20 @@ private async Task AddFileOrFolderAsync(ListedItem? item) return null; } - ListedItem listedItem; + var parentPath = Directory.GetParent(fileOrFolderPath)?.FullName; + if (parentPath is null) + return null; + + ListedItem? listedItem; // FILE_ATTRIBUTE_DIRECTORY if ((findData.dwFileAttributes & 0x10) > 0) - listedItem = await Win32StorageEnumerator.GetFolder(findData, Directory.GetParent(fileOrFolderPath).FullName, IsValidGitDirectory, addFilesCTS.Token); + listedItem = await Win32StorageEnumerator.GetFolder(findData, parentPath, IsValidGitDirectory, addFilesCTS.Token); else - listedItem = await Win32StorageEnumerator.GetFile(findData, Directory.GetParent(fileOrFolderPath).FullName, IsValidGitDirectory, addFilesCTS.Token); + listedItem = await Win32StorageEnumerator.GetFile(findData, parentPath, IsValidGitDirectory, addFilesCTS.Token); + + if (listedItem is null) + return null; await AddFileOrFolderAsync(listedItem); @@ -2938,14 +2969,20 @@ private async Task AddFileOrFolderAsync(ListedItem? item) if (storageItem.IsOfType(StorageItemTypes.File)) { - var properties = await storageItem.AsBaseStorageFile().GetBasicPropertiesAsync(); + if (storageItem.AsBaseStorageFile() is not { } file) + return null; + + var properties = await file.GetBasicPropertiesAsync(); size = (long)properties.Size; modified = properties.DateModified; created = properties.DateCreated; } else if (storageItem.IsOfType(StorageItemTypes.Folder)) { - var properties = await storageItem.AsBaseStorageFolder().GetBasicPropertiesAsync(); + if (storageItem.AsBaseStorageFolder() is not { } folder) + return null; + + var properties = await folder.GetBasicPropertiesAsync(); size = item.IsArchive ? (long)properties.Size : null; modified = properties.DateModified; created = properties.DateCreated; @@ -3143,7 +3180,7 @@ public void CancelSearch() public void UpdateDateDisplay() { - App.Logger.LogDebug($"UpdateDateDisplay: itemCount={filesAndFolders?.Count}"); + App.Logger.LogDebug($"UpdateDateDisplay: itemCount={filesAndFolders.Count}"); filesAndFolders.ToList().AsParallel().ForAll(async item => { diff --git a/src/Files.App/ViewModels/UserControls/InfoPaneViewModel.cs b/src/Files.App/ViewModels/UserControls/InfoPaneViewModel.cs index d90f086d44cd..f388fda66193 100644 --- a/src/Files.App/ViewModels/UserControls/InfoPaneViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/InfoPaneViewModel.cs @@ -19,7 +19,7 @@ public sealed partial class InfoPaneViewModel : ObservableObject, IDisposable private IContentPageContext contentPageContext { get; } = Ioc.Default.GetRequiredService(); private DrivesViewModel drivesViewModel { get; } = Ioc.Default.GetRequiredService(); - private CancellationTokenSource loadCancellationTokenSource; + private CancellationTokenSource? loadCancellationTokenSource; private Files.App.UserControls.Menus.FileTagsContextMenu? cachedTagsContextMenu; /// @@ -121,8 +121,8 @@ public bool ShowCloudItemButton set => SetProperty(ref showCloudItemButton, value); } - private UIElement previewPaneContent; - public UIElement PreviewPaneContent + private UIElement? previewPaneContent; + public UIElement? PreviewPaneContent { get => previewPaneContent; set @@ -208,7 +208,11 @@ private async void ContentPageContext_PropertyChanged(object? sender, PropertyCh private async Task LoadPreviewControlAsync(CancellationToken token, bool downloadItem) { - if (SelectedItem.IsHiddenItem && !SelectedItem.ItemPath.EndsWith('\\')) + var item = SelectedItem; + if (item is null) + return; + + if (item.IsHiddenItem && !item.ItemPath.EndsWith('\\')) { PreviewPaneState = PreviewPaneStates.NoPreviewOrDetailsAvailable; @@ -216,7 +220,7 @@ private async Task LoadPreviewControlAsync(CancellationToken token, bool downloa return; } - var control = await GetBuiltInPreviewControlAsync(SelectedItem, downloadItem); + var control = await GetBuiltInPreviewControlAsync(item, downloadItem); if (token.IsCancellationRequested) return; @@ -228,7 +232,7 @@ private async Task LoadPreviewControlAsync(CancellationToken token, bool downloa return; } - var basicModel = new BasicPreviewViewModel(SelectedItem); + var basicModel = new BasicPreviewViewModel(item); await basicModel.LoadAsync(); control = new BasicPreview(basicModel); @@ -240,7 +244,7 @@ private async Task LoadPreviewControlAsync(CancellationToken token, bool downloa PreviewPaneState = SelectedDriveItem is not null ? PreviewPaneStates.DriveStorageDetailsAvailable : PreviewPaneStates.PreviewAndDetailsAvailable; } - private async Task GetBuiltInPreviewControlAsync(ListedItem item, bool downloadItem) + private async Task GetBuiltInPreviewControlAsync(ListedItem item, bool downloadItem) { ShowCloudItemButton = false; @@ -498,7 +502,10 @@ private async Task LoadBasicPreviewAsync() { try { - var basicModel = new BasicPreviewViewModel(SelectedItem); + if (SelectedItem is not { } selectedItem) + return; + + var basicModel = new BasicPreviewViewModel(selectedItem); await basicModel.LoadAsync(); PreviewPaneContent = new BasicPreview(basicModel); @@ -523,23 +530,25 @@ private void UpdateTagsItems() try { Items.Clear(); + if (SelectedItem is not { } selectedItem) + return; - SelectedItem?.FileTagsUI?.ForEach(tag => Items.Add(new TagItem(tag))); + selectedItem.FileTagsUI?.ForEach(tag => Items.Add(new TagItem(tag))); // Create menu once and reuse it for subsequent selections if (cachedTagsContextMenu is null) { - cachedTagsContextMenu = new Files.App.UserControls.Menus.FileTagsContextMenu(new List() { SelectedItem }); + cachedTagsContextMenu = new Files.App.UserControls.Menus.FileTagsContextMenu([selectedItem]); cachedTagsContextMenu.TagsChanged += async (s, e) => { - if (contentPageContext.ShellPage is not null) - await contentPageContext.ShellPage.ShellViewModel.RefreshTagGroups(); + if (contentPageContext.ShellPage?.ShellViewModel is { } shellViewModel) + await shellViewModel.RefreshTagGroups(); }; } else { // Reset menu for new selection - cachedTagsContextMenu.ResetForItems(new List() { SelectedItem }); + cachedTagsContextMenu.ResetForItems([selectedItem]); } Items.Add(new FlyoutItem(cachedTagsContextMenu)); diff --git a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs index 7e2f8d7f5c71..db1bf4515110 100644 --- a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs @@ -100,11 +100,11 @@ public sealed partial class NavigationToolbarViewModel : ObservableObject, IAddr public bool CanExtract => Commands.DecompressArchive.CanExecute(null) || Commands.DecompressArchiveHere.CanExecute(null) || Commands.DecompressArchiveHereSmart.CanExecute(null) || Commands.DecompressArchiveToChildFolder.CanExecute(null); - public bool IsCardsLayout => _InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.CardsView; - public bool IsColumnLayout => _InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.ColumnView; - public bool IsGridLayout => _InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.GridView; - public bool IsDetailsLayout => _InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.DetailsView; - public bool IsListLayout => _InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.ListView; + public bool IsCardsLayout => InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.CardsView; + public bool IsColumnLayout => InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.ColumnView; + public bool IsGridLayout => InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.GridView; + public bool IsDetailsLayout => InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.DetailsView; + public bool IsListLayout => InstanceViewModel.FolderSettings.LayoutMode is FolderLayoutModes.ListView; public bool IsLayoutSizeCompact => (IsDetailsLayout && UserSettingsService.LayoutSettingsService.DetailsViewSize == DetailsViewSizeKind.Compact) || @@ -181,15 +181,15 @@ public int UpdateProgress private bool _CanRefresh; public bool CanRefresh { get => _CanRefresh; set => SetProperty(ref _CanRefresh, value); } - private string _PathControlDisplayText; + private string _PathControlDisplayText = string.Empty; [Obsolete("Superseded by Omnibar.")] public string PathControlDisplayText { get => _PathControlDisplayText; set => SetProperty(ref _PathControlDisplayText, value); } private bool _HasItem = false; public bool HasItem { get => _HasItem; set => SetProperty(ref _HasItem, value); } - private Style _LayoutThemedIcon; - public Style LayoutThemedIcon { get => _LayoutThemedIcon; set => SetProperty(ref _LayoutThemedIcon, value); } + private Style? _LayoutThemedIcon; + public Style? LayoutThemedIcon { get => _LayoutThemedIcon; set => SetProperty(ref _LayoutThemedIcon, value); } // SetProperty doesn't seem to properly notify the binding in path bar private string? _PathText; @@ -213,7 +213,7 @@ public string? PathText private string? _OmnibarSearchModeText; public string? OmnibarSearchModeText { get => _OmnibarSearchModeText; set => SetProperty(ref _OmnibarSearchModeText, value); } - public string OmnibarSearchModePlaceholder => InstanceViewModel?.IsPageTypeSettings == true + public string OmnibarSearchModePlaceholder => _InstanceViewModel?.IsPageTypeSettings == true ? Strings.SearchSettings.GetLocalizedResource() : Strings.OmnibarSearchModeTextPlaceholder.GetLocalizedResource(); @@ -222,23 +222,24 @@ public string? PathText private string _OmnibarCurrentSelectedModeName = OmnibarPathModeName; public string OmnibarCurrentSelectedModeName { get => _OmnibarCurrentSelectedModeName; set => SetProperty(ref _OmnibarCurrentSelectedModeName, value); } - private CurrentInstanceViewModel _InstanceViewModel; + private CurrentInstanceViewModel? _InstanceViewModel; public CurrentInstanceViewModel InstanceViewModel { - get => _InstanceViewModel; + get => _InstanceViewModel + ?? throw new InvalidOperationException("The navigation toolbar has not been initialized."); set { - if (_InstanceViewModel?.FolderSettings is not null) - _InstanceViewModel.FolderSettings.PropertyChanged -= FolderSettings_PropertyChanged; - - if (_InstanceViewModel is not null) - _InstanceViewModel.PropertyChanged -= InstanceViewModel_PropertyChanged; + if (_InstanceViewModel is { } previousViewModel) + { + previousViewModel.FolderSettings.PropertyChanged -= FolderSettings_PropertyChanged; + previousViewModel.PropertyChanged -= InstanceViewModel_PropertyChanged; + } - if (SetProperty(ref _InstanceViewModel, value) && _InstanceViewModel?.FolderSettings is not null) + if (SetProperty(ref _InstanceViewModel, value)) { FolderSettings_PropertyChanged(this, new PropertyChangedEventArgs(nameof(LayoutPreferencesManager.LayoutMode))); - _InstanceViewModel.FolderSettings.PropertyChanged += FolderSettings_PropertyChanged; - _InstanceViewModel.PropertyChanged += InstanceViewModel_PropertyChanged; + value.FolderSettings.PropertyChanged += FolderSettings_PropertyChanged; + value.PropertyChanged += InstanceViewModel_PropertyChanged; OnPropertyChanged(nameof(OmnibarSearchModePlaceholder)); } } @@ -378,6 +379,7 @@ private void UserSettingsService_OnSettingChangedEvent(object? sender, SettingCh public void PathBoxItem_DragLeave(object sender, DragEventArgs e) { if (((FrameworkElement)sender).DataContext is not PathBoxItem pathBoxItem || + pathBoxItem.Path is null || pathBoxItem.Path == "Home" || pathBoxItem.Path == "ReleaseNotes" || pathBoxItem.Path == "Settings") @@ -401,10 +403,10 @@ public async Task PathBoxItem_Drop(object sender, DragEventArgs e) // Reset dragged over pathbox item _dragOverPath = null; - if (((FrameworkElement)sender).DataContext is not PathBoxItem pathBoxItem || - pathBoxItem.Path == "Home" || - pathBoxItem.Path == "ReleaseNotes" || - pathBoxItem.Path == "Settings") + if (((FrameworkElement)sender).DataContext is not PathBoxItem { Path: { } path } || + path == "Home" || + path == "ReleaseNotes" || + path == "Settings") { return; } @@ -417,7 +419,7 @@ public async Task PathBoxItem_Drop(object sender, DragEventArgs e) { AcceptedOperation = e.AcceptedOperation, Package = e.DataView, - Path = pathBoxItem.Path, + Path = path, SignalEvent = signal }); @@ -535,7 +537,7 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync( public async Task HandleItemNavigationAsync(string path) { - if (ContentPageContext.ShellPage is null) + if (ContentPageContext.ShellPage is not { ShellViewModel: { } shellViewModel } shellPage) return; var currentPath = PathComponents.LastOrDefault()?.Path; @@ -545,28 +547,28 @@ public async Task HandleItemNavigationAsync(string path) string.IsNullOrWhiteSpace(normalizedInput)) return; - if (normalizedInput.Equals(ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory) && - ContentPageContext.ShellPage.CurrentPageType != typeof(HomePage) && - !ContentPageContext.ShellPage.ShellViewModel.IsSearchResults) + if (normalizedInput.Equals(shellViewModel.WorkingDirectory) && + shellPage.CurrentPageType != typeof(HomePage) && + !shellViewModel.IsSearchResults) return; if (normalizedInput.Equals("Home", StringComparison.OrdinalIgnoreCase) || normalizedInput.Equals(Strings.Home.GetLocalizedResource(), StringComparison.OrdinalIgnoreCase)) { SavePathToHistory("Home"); - ContentPageContext.ShellPage.NavigateHome(); + shellPage.NavigateHome(); } else if (normalizedInput.Equals("ReleaseNotes", StringComparison.OrdinalIgnoreCase) || normalizedInput.Equals(Strings.ReleaseNotes.GetLocalizedResource(), StringComparison.OrdinalIgnoreCase)) { SavePathToHistory("ReleaseNotes"); - ContentPageContext.ShellPage.NavigateToReleaseNotes(); + shellPage.NavigateToReleaseNotes(); } else if (normalizedInput.Equals("Settings", StringComparison.OrdinalIgnoreCase) || normalizedInput.Equals(Strings.Settings.GetLocalizedResource(), StringComparison.OrdinalIgnoreCase)) { SavePathToHistory("Settings"); - ContentPageContext.ShellPage.NavigateToSettings(); + shellPage.NavigateToSettings(); } else { @@ -574,7 +576,7 @@ public async Task HandleItemNavigationAsync(string path) if (currentPath is not null && currentPath.Equals(normalizedInput, StringComparison.OrdinalIgnoreCase)) return; - var item = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(normalizedInput)); + var item = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(normalizedInput)); var resFolder = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(normalizedInput, item)); if (resFolder || FolderHelpers.CheckFolderAccessWithWin32(normalizedInput)) @@ -590,35 +592,34 @@ public async Task HandleItemNavigationAsync(string path) var pathToNavigate = resFolder.Result?.Path ?? normalizedInput; SavePathToHistory(pathToNavigate); - ContentPageContext.ShellPage.NavigateToPath(pathToNavigate); + shellPage.NavigateToPath(pathToNavigate); } else if (isFtp) { SavePathToHistory(normalizedInput); - ContentPageContext.ShellPage.NavigateToPath(normalizedInput); + shellPage.NavigateToPath(normalizedInput); } else // Not a folder or inaccessible { var resFile = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileWithPathFromPathAsync(normalizedInput, item)); - if (resFile) + if (resFile.Result is { } storageFile) { - var pathToInvoke = resFile.Result.Path; - await Win32Helper.InvokeWin32ComponentAsync(pathToInvoke, ContentPageContext.ShellPage); + await Win32Helper.InvokeWin32ComponentAsync(storageFile.Path, shellPage); } else // Not a file or not accessible { var workingDir = - string.IsNullOrEmpty(ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory) || - ContentPageContext.ShellPage.CurrentPageType == typeof(HomePage) + string.IsNullOrEmpty(shellViewModel.WorkingDirectory) || + shellPage.CurrentPageType == typeof(HomePage) ? Constants.UserEnvironmentPaths.HomePath - : ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + : shellViewModel.WorkingDirectory; - if (await LaunchApplicationFromPath(PathText, workingDir)) + if (await LaunchApplicationFromPath(path, workingDir)) return; try { - if (!await Windows.System.Launcher.LaunchUriAsync(new Uri(PathText))) + if (!await Windows.System.Launcher.LaunchUriAsync(new Uri(path))) await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidItemDialogTitle.GetLocalizedResource(), string.Format(Strings.InvalidItemDialogContent.GetLocalizedResource(), Environment.NewLine, resFolder.ErrorCode.ToString())); } @@ -631,7 +632,7 @@ await DialogDisplayHelper.ShowDialogAsync(Strings.InvalidItemDialogTitle.GetLoca } } - PathControlDisplayText = ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + PathControlDisplayText = shellViewModel.WorkingDirectory; } public void SwitchToCommandPaletteMode() @@ -659,8 +660,11 @@ public async Task SwitchToPathMode() await Task.Delay(100); OmnibarCurrentSelectedModeName = OmnibarPathModeName; - omnibar?.Focus(FocusState.Programmatic); - omnibar.IsFocused = true; + if (omnibar is not null) + { + omnibar.Focus(FocusState.Programmatic); + omnibar.IsFocused = true; + } } public void UpdateAdditionalActions() @@ -670,16 +674,24 @@ public void UpdateAdditionalActions() public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem pathItem) { - var childFolders = GetSubfolders(pathItem.Path); + if (pathItem.Path is not { } path) + return; + + var childFolders = GetSubfolders(path); // Fall back to StorageFolder API for non-filesystem paths (e.g. FTP) if (childFolders is null) { - StorageFolderWithPath folder = await ContentPageContext.ShellPage.ShellViewModel.GetFolderWithPathFromPathAsync(pathItem.Path); - if (folder is not null) + if (ContentPageContext.ShellPage?.ShellViewModel is { } shellViewModel) { - var result = (await FilesystemTasks.Wrap(() => folder.GetFoldersWithPathAsync(string.Empty))).Result; - childFolders = result?.Select(f => (f.Item.Name, f.Path, false)).ToList(); + var folderResult = await shellViewModel.GetFolderWithPathFromPathAsync(path); + if (folderResult.Result is { } folder) + { + var result = (await FilesystemTasks.Wrap(() => folder.GetFoldersWithPathAsync(string.Empty))).Result; + childFolders = result? + .Select(f => (f.Item?.Name ?? Path.GetFileName(f.Path), f.Path, false)) + .ToList(); + } } } @@ -702,7 +714,7 @@ public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem p var workingPath = PathComponents[PathComponents.Count - 1].Path?.TrimEnd(Path.DirectorySeparatorChar); - foreach (var (name, path, isHidden) in childFolders) + foreach (var (name, childPath, isHidden) in childFolders) { var flyoutItem = new MenuFlyoutItem { @@ -711,19 +723,19 @@ public async Task SetPathBoxDropDownFlyoutAsync(MenuFlyout flyout, PathBoxItem p Opacity = isHidden ? Constants.UI.DimItemOpacity : 1.0, }; - if (workingPath != path) + if (workingPath != childPath) { flyoutItem.Click += (sender, args) => { // Navigate to the directory - ContentPageContext.ShellPage.NavigateToPath(path); + ContentPageContext.ShellPage?.NavigateToPath(childPath); }; } flyout.Items?.Add(flyoutItem); // Start loading the thumbnail in the background - _ = LoadFlyoutItemIconAsync(flyoutItem, path); + _ = LoadFlyoutItemIconAsync(flyoutItem, childPath); } } @@ -798,8 +810,11 @@ private static string NormalizePathInput(string currentInput, bool isFtp) } [Obsolete("Superseded by Omnibar.")] - public async Task CheckPathInputAsync(string currentInput, string currentSelectedPath, IShellPage shellPage) + public async Task CheckPathInputAsync(string currentInput, string? currentSelectedPath, IShellPage shellPage) { + if (shellPage.ShellViewModel is not { } shellViewModel) + return; + if (currentInput.StartsWith('>')) { var code = currentInput.Substring(1).Trim(); @@ -824,7 +839,7 @@ await DialogDisplayHelper.ShowDialogAsync(Strings.CommandNotExecutable.GetLocali if (currentSelectedPath == normalizedInput || string.IsNullOrWhiteSpace(normalizedInput)) return; - if (normalizedInput != shellPage.ShellViewModel.WorkingDirectory || shellPage.CurrentPageType == typeof(HomePage)) + if (normalizedInput != shellViewModel.WorkingDirectory || shellPage.CurrentPageType == typeof(HomePage)) { if (normalizedInput.Equals("Home", StringComparison.OrdinalIgnoreCase) || normalizedInput.Equals(Strings.Home.GetLocalizedResource(), StringComparison.OrdinalIgnoreCase)) { @@ -847,7 +862,7 @@ await DialogDisplayHelper.ShowDialogAsync(Strings.CommandNotExecutable.GetLocali if (currentSelectedPath == normalizedInput) return; - var item = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(normalizedInput)); + var item = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(normalizedInput)); var resFolder = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(normalizedInput, item)); if (resFolder || FolderHelpers.CheckFolderAccessWithWin32(normalizedInput)) @@ -872,18 +887,17 @@ await DialogDisplayHelper.ShowDialogAsync(Strings.CommandNotExecutable.GetLocali else // Not a folder or inaccessible { var resFile = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileWithPathFromPathAsync(normalizedInput, item)); - if (resFile) + if (resFile.Result is { } storageFile) { - var pathToInvoke = resFile.Result.Path; - await Win32Helper.InvokeWin32ComponentAsync(pathToInvoke, shellPage); + await Win32Helper.InvokeWin32ComponentAsync(storageFile.Path, shellPage); } else // Not a file or not accessible { var workingDir = - string.IsNullOrEmpty(shellPage.ShellViewModel.WorkingDirectory) || + string.IsNullOrEmpty(shellViewModel.WorkingDirectory) || shellPage.CurrentPageType == typeof(HomePage) ? Constants.UserEnvironmentPaths.HomePath : - shellPage.ShellViewModel.WorkingDirectory; + shellViewModel.WorkingDirectory; if (await LaunchApplicationFromPath(currentInput, workingDir)) return; @@ -943,11 +957,11 @@ public async Task PopulateOmnibarSuggestionsForPathMode() { var result = await SafetyExtensions.IgnoreExceptions((Func>)(async () => { - List? newSuggestions = []; + List newSuggestions = []; var pathText = this.PathText; // If the current input is special, populate navigation history instead. - if (string.IsNullOrWhiteSpace((string)pathText) || + if (string.IsNullOrWhiteSpace(pathText) || pathText is "Home" or "ReleaseNotes" or "Settings") { // Load previously entered path @@ -958,24 +972,31 @@ public async Task PopulateOmnibarSuggestionsForPathMode() } else { - var isFtp = FtpHelpers.IsFtpPath((string)pathText); - pathText = NormalizePathInput((string)pathText, isFtp); - var expandedPath = StorageFileExtensions.GetResolvedPath((string)pathText, isFtp); + var isFtp = FtpHelpers.IsFtpPath(pathText); + pathText = NormalizePathInput(pathText, isFtp); + var expandedPath = StorageFileExtensions.GetResolvedPath(pathText, isFtp); var folderPath = PathNormalization.GetParentDir(expandedPath) ?? expandedPath; - StorageFolderWithPath folder = await ContentPageContext.ShellPage.ShellViewModel.GetFolderWithPathFromPathAsync(folderPath); - if (folder is null) + if (ContentPageContext.ShellPage?.ShellViewModel is not { } shellViewModel) + return false; + + var folderResult = await shellViewModel.GetFolderWithPathFromPathAsync(folderPath); + if (folderResult.Result is not { } folder) return false; var currPath = await folder.GetFoldersWithPathAsync(Path.GetFileName(expandedPath), MaxSuggestionsCount); if (currPath.Count >= MaxSuggestionsCount) { - newSuggestions.AddRange(currPath.Select(x => new OmnibarPathModeSuggestionModel(x.Path, x.Item.DisplayName))); + newSuggestions.AddRange(currPath.Select(CreateSuggestion)); } else if (currPath.Any()) { - var subPath = await currPath.First().GetFoldersWithPathAsync((uint)(MaxSuggestionsCount - currPath.Count)); - newSuggestions.AddRange(currPath.Select(x => new OmnibarPathModeSuggestionModel(x.Path, x.Item.DisplayName))); - newSuggestions.AddRange(subPath.Select(x => new OmnibarPathModeSuggestionModel(x.Path, PathNormalization.Combine(currPath.First().Item.DisplayName, x.Item.DisplayName)))); + var firstPath = currPath.First(); + var subPath = await firstPath.GetFoldersWithPathAsync((uint)(MaxSuggestionsCount - currPath.Count)); + var firstDisplayName = firstPath.Item?.DisplayName ?? Path.GetFileName(firstPath.Path); + newSuggestions.AddRange(currPath.Select(CreateSuggestion)); + newSuggestions.AddRange(subPath.Select(x => new OmnibarPathModeSuggestionModel( + x.Path, + PathNormalization.Combine(firstDisplayName, x.Item?.DisplayName ?? Path.GetFileName(x.Path))))); } } @@ -1021,6 +1042,9 @@ public async Task PopulateOmnibarSuggestionsForPathMode() } return true; + + static OmnibarPathModeSuggestionModel CreateSuggestion(StorageFolderWithPath folder) + => new(folder.Path, folder.Item?.DisplayName ?? Path.GetFileName(folder.Path)); })); if (!result) @@ -1033,9 +1057,9 @@ void AddNoResultsItem() PathModeSuggestionItems.Clear(); // Use null-safe access to avoid NullReferenceException during app lifecycle transitions - var workingDirectory = string.IsNullOrEmpty(ContentPageContext.ShellPage?.ShellViewModel?.WorkingDirectory) - ? Constants.UserEnvironmentPaths.HomePath - : ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + var workingDirectory = ContentPageContext.ShellPage?.ShellViewModel?.WorkingDirectory; + if (string.IsNullOrEmpty(workingDirectory)) + workingDirectory = Constants.UserEnvironmentPaths.HomePath; PathModeSuggestionItems.Add(new( workingDirectory, @@ -1045,14 +1069,15 @@ void AddNoResultsItem() public async Task PopulateOmnibarSuggestionsForCommandPaletteMode() { + var commandPaletteText = OmnibarCommandPaletteModeText ?? string.Empty; var (suggestionsToProcess, commandsToProcess) = await Task.Run(() => { var suggestions = new List(); var commandsData = Commands .Where(command => command.IsAccessibleGlobally - && (command.Description.Contains(OmnibarCommandPaletteModeText, StringComparison.OrdinalIgnoreCase) - || command.Code.ToString().Contains(OmnibarCommandPaletteModeText, StringComparison.OrdinalIgnoreCase))) + && (command.Description.Contains(commandPaletteText, StringComparison.OrdinalIgnoreCase) + || command.Code.ToString().Contains(commandPaletteText, StringComparison.OrdinalIgnoreCase))) .Where(command => command.Description != Commands.OpenCommandPalette.Description.ToString()) .ToList(); @@ -1145,11 +1170,13 @@ public async Task PopulateOmnibarSuggestionsForSearchMode() try { await Task.Delay(200, token); + if (ContentPageContext.ShellPage?.ShellViewModel is not { } shellViewModel) + return; var search = new FolderSearch { - Query = OmnibarSearchModeText, - Folder = ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory, + Query = OmnibarSearchModeText ?? string.Empty, + Folder = shellViewModel.WorkingDirectory, MaxItemCount = 10, }; @@ -1207,7 +1234,7 @@ private void FolderSettings_PropertyChanged(object? sender, PropertyChangedEvent switch (e.PropertyName) { case nameof(LayoutPreferencesManager.LayoutMode): - LayoutThemedIcon = _InstanceViewModel.FolderSettings.LayoutMode switch + LayoutThemedIcon = InstanceViewModel.FolderSettings.LayoutMode switch { FolderLayoutModes.ListView => Commands.LayoutList.ThemedIconStyle!, FolderLayoutModes.CardsView => Commands.LayoutCards.ThemedIconStyle!, diff --git a/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs index bd4268930d40..1b105810bba6 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs @@ -18,13 +18,20 @@ public override async Task> LoadPreviewAndDetailsAsync() { var details = new List(); - using SevenZipExtractor zipFile = await FilesystemTasks.Wrap(async () => + var zipResult = await FilesystemTasks.WrapNullable(async () => { - var arch = new SevenZipExtractor(await Item.ItemFile.OpenStreamForReadAsync()); + var arch = new SevenZipExtractor(await PreviewFile.OpenStreamForReadAsync()); // Force load archive (1665013614u) - return arch?.ArchiveFileData is null ? null : arch; + if (arch.ArchiveFileData is null) + { + arch.Dispose(); + return null; + } + + return arch; }); + using var zipFile = zipResult.Result; if (zipFile is null) { diff --git a/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs index 63839ade64d8..087f32c9a3be 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs @@ -13,14 +13,17 @@ public abstract partial class BasePreviewModel : ObservableObject public ListedItem Item { get; } - private BitmapImage fileImage; - public BitmapImage FileImage + protected BaseStorageFile PreviewFile + => Item.ItemFile ?? throw new InvalidOperationException("The preview file has not been loaded."); + + private BitmapImage? fileImage; + public BitmapImage? FileImage { get => fileImage; protected set => SetProperty(ref fileImage, value); } - public List DetailsFromPreview { get; set; } + public List DetailsFromPreview { get; set; } = []; /// /// This is cancelled when the user has selected another file or closed the pane. @@ -30,11 +33,11 @@ public BitmapImage FileImage public BasePreviewModel(ListedItem item) : base() => Item = item; - public delegate void LoadedEventHandler(object sender, EventArgs e); + public delegate void LoadedEventHandler(object? sender, EventArgs e); - public static Task LoadDetailsOnlyAsync(ListedItem item, List details = null) + public static Task LoadDetailsOnlyAsync(ListedItem item, List? details = null) { - var temp = new DetailsOnlyPreviewModel(item) { DetailsFromPreview = details }; + var temp = new DetailsOnlyPreviewModel(item) { DetailsFromPreview = details ?? [] }; return temp.LoadAsync(); } @@ -52,22 +55,22 @@ public virtual async Task LoadAsync() if (Item.ItemFile is null) { - var rootItem = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(Item.ItemPath)); + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(Item.ItemPath)); Item.ItemFile = await StorageFileExtensions.DangerousGetFileFromPathAsync(Item.ItemPath, rootItem); } + if (Item.ItemFile is null) + return; + await Task.Run(async () => { DetailsFromPreview = await LoadPreviewAndDetailsAsync(); if (userSettingsService.InfoPaneSettingsService.SelectedTab == InfoPaneTabs.Details) { // Add the details from the preview function, then the system file properties - DetailsFromPreview?.ForEach(i => detailsFull.Add(i)); + DetailsFromPreview.ForEach(i => detailsFull.Add(i)); List props = await GetSystemFilePropertiesAsync(); - if (props is not null) - { - detailsFull.AddRange(props); - } + detailsFull.AddRange(props); } }); @@ -102,28 +105,36 @@ public async virtual Task> LoadPreviewAndDetailsAsync() /// /// /// - public virtual void PreviewControlBase_Unloaded(object sender, RoutedEventArgs e) + public virtual void PreviewControlBase_Unloaded(object? sender, RoutedEventArgs e) => LoadCancelledTokenSource.Cancel(); - protected static FileProperty GetFileProperty(string nameResource, object value) + protected static FileProperty GetFileProperty(string nameResource, object? value) => new() { NameResource = nameResource, Value = value }; private async Task> GetSystemFilePropertiesAsync() { - if (Item.IsShortcut) - return null; + if (Item.IsShortcut || Item.ItemFile is null) + return []; var list = await FileProperty.RetrieveAndInitializePropertiesAsync(Item.ItemFile, Constants.ResourceFilePaths.PreviewPaneDetailsPropertiesJsonPath); - list.Find(x => x.ID is "address").Value = await LocationHelpers.GetAddressFromCoordinatesAsync( - (double?)list.Find(x => x.Property is "System.GPS.LatitudeDecimal").Value, - (double?)list.Find(x => x.Property is "System.GPS.LongitudeDecimal").Value - ); + var address = list.Find(x => x.ID is "address"); + if (address is not null) + { + address.Value = await LocationHelpers.GetAddressFromCoordinatesAsync( + (double?)list.Find(x => x.Property is "System.GPS.LatitudeDecimal")?.Value, + (double?)list.Find(x => x.Property is "System.GPS.LongitudeDecimal")?.Value); + } // 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; + var fileTag = list.FirstOrDefault(x => x.ID is "filetag"); + if (fileTag is not null) + { + fileTag.Value = Item.FileTagsUI is not null + ? string.Join(',', Item.FileTagsUI.Select(x => x.Name)) + : null; + } return list.Where(i => i.ValueText is not null).ToList(); } diff --git a/src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.cs index 68ac5b647141..7325976d849d 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.cs @@ -11,15 +11,15 @@ public sealed partial class CodePreviewViewModel : BasePreviewModel { private static readonly FrozenDictionary extensions = GetDictionary(); - private string textValue; - public string TextValue + private string? textValue; + public string? TextValue { get => textValue; private set => SetProperty(ref textValue, value); } - private ILanguage codeLanguage; - public ILanguage CodeLanguage + private ILanguage? codeLanguage; + public ILanguage? CodeLanguage { get => codeLanguage; private set => SetProperty(ref codeLanguage, value); @@ -39,7 +39,7 @@ public async override Task> LoadPreviewAndDetailsAsync() try { - var text = TextValue ?? await ReadFileAsTextAsync(Item.ItemFile); + var text = TextValue ?? await ReadFileAsTextAsync(PreviewFile); details.Add(GetFileProperty("PropertyLineCount", text.Split('\n').Length)); CodeLanguage = extensions[Item.FileExtension.ToLowerInvariant()]; diff --git a/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs index fd2f759d306a..a1bfec22d7d2 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs @@ -14,7 +14,7 @@ public sealed class FolderPreviewViewModel public BitmapImage Thumbnail { get; set; } = new(); - private BaseStorageFolder Folder { get; set; } + private BaseStorageFolder? Folder { get; set; } public FolderPreviewViewModel(ListedItem item) => Item = item; @@ -24,9 +24,13 @@ public Task LoadAsync() private async Task LoadPreviewAndDetailsAsync() { - var rootItem = await FilesystemTasks.Wrap(() => DriveHelpers.GetRootFromPathAsync(Item.ItemPath)); - Folder = await StorageFileExtensions.DangerousGetFolderFromPathAsync(Item.ItemPath, rootItem); - var items = await Folder.GetItemsAsync(); + var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(Item.ItemPath)); + var folder = await StorageFileExtensions.DangerousGetFolderFromPathAsync(Item.ItemPath, rootItem.Result); + if (folder is null) + return; + + Folder = folder; + var items = await folder.GetItemsAsync(); var result = await FileThumbnailHelper.GetIconAsync( Item.ItemPath, @@ -35,7 +39,10 @@ private async Task LoadPreviewAndDetailsAsync() IconOptions.None); if (result is not null) - Thumbnail = await result.ToBitmapAsync(); + { + if (await result.ToBitmapAsync() is { } thumbnail) + Thumbnail = thumbnail; + } // If the selected item is the root of a drive (e.g. "C:\") or a cloud drive, // we do not need to load the properties below, since they will not be shown. @@ -43,20 +50,20 @@ private async Task LoadPreviewAndDetailsAsync() if (Item.IsDriveRoot || infoPaneViewModel?.SelectedDriveItem is not null) return; - var info = await Folder.GetBasicPropertiesAsync(); + var info = await folder.GetBasicPropertiesAsync(); Item.FileDetails = [ GetFileProperty("PropertyItemCount", infoPaneViewModel?.DirectoryItemCount), GetFileProperty("PropertyDateModified", info.DateModified), GetFileProperty("PropertyDateCreated", info.DateCreated), - GetFileProperty("PropertyParsingPath", Folder.Path), + GetFileProperty("PropertyParsingPath", folder.Path), ]; if (GitHelpers.IsRepositoryEx(Item.ItemPath, out var repoPath) && !string.IsNullOrEmpty(repoPath)) { - var gitDirectory = GitHelpers.GetGitRepositoryPath(Folder.Path, Path.GetPathRoot(Folder.Path)); + var gitDirectory = GitHelpers.GetGitRepositoryPath(folder.Path, Path.GetPathRoot(folder.Path)); var headName = (await GitHelpers.GetRepositoryHead(gitDirectory))?.Name ?? string.Empty; var repositoryName = GitHelpers.GetOriginRepositoryName(gitDirectory); @@ -68,7 +75,7 @@ private async Task LoadPreviewAndDetailsAsync() } } - private static FileProperty GetFileProperty(string nameResource, object value) + private static FileProperty GetFileProperty(string nameResource, object? value) => new() { NameResource = nameResource, Value = value }; } } diff --git a/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs index dbc9f9c2ad6f..2a4800be0e1c 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs @@ -11,8 +11,8 @@ namespace Files.App.ViewModels.Previews { public sealed partial class ImagePreviewViewModel : BasePreviewModel { - private ImageSource imageSource; - public ImageSource ImageSource + private ImageSource? imageSource; + public ImageSource? ImageSource { get => imageSource; private set => SetProperty(ref imageSource, value); @@ -25,7 +25,7 @@ public ImagePreviewViewModel(ListedItem item) public override async Task> LoadPreviewAndDetailsAsync() { - using IRandomAccessStream stream = await Item.ItemFile.OpenAsync(FileAccessMode.Read); + using IRandomAccessStream stream = await PreviewFile.OpenAsync(FileAccessMode.Read); await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => { diff --git a/src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.cs index 264cfbcaea58..cc53068f87c8 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.cs @@ -8,8 +8,8 @@ namespace Files.App.ViewModels.Previews { public sealed partial class MarkdownPreviewViewModel : BasePreviewModel { - private string textValue; - public string TextValue + private string? textValue; + public string? TextValue { get => textValue; private set => SetProperty(ref textValue, value); @@ -22,7 +22,7 @@ public MarkdownPreviewViewModel(ListedItem item) public override async Task> LoadPreviewAndDetailsAsync() { - var text = await ReadFileAsTextAsync(Item.ItemFile); + var text = await ReadFileAsTextAsync(PreviewFile); TextValue = EscapeRawHtml(text.Left(Constants.PreviewPane.TextCharacterLimit)); return []; diff --git a/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs index 7221dbc7d5a6..0c2f91551b89 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs @@ -9,10 +9,10 @@ namespace Files.App.ViewModels.Previews { public sealed partial class MediaPreviewViewModel : BasePreviewModel { - public event EventHandler TogglePlaybackRequested; + public event EventHandler? TogglePlaybackRequested; - private MediaSource source; - public MediaSource Source + private MediaSource? source; + public MediaSource? Source { get => source; private set => SetProperty(ref source, value); @@ -21,16 +21,17 @@ public MediaSource Source public MediaPreviewViewModel(ListedItem item) : base(item) { } public void TogglePlayback() - => TogglePlaybackRequested?.Invoke(this, null); + => TogglePlaybackRequested?.Invoke(this, EventArgs.Empty); public override Task> LoadPreviewAndDetailsAsync() { - Source = MediaSource.CreateFromStorageFile(Item.ItemFile); + if (Item.ItemFile is not null) + Source = MediaSource.CreateFromStorageFile(Item.ItemFile); return Task.FromResult(new List()); } - public override void PreviewControlBase_Unloaded(object sender, RoutedEventArgs e) + public override void PreviewControlBase_Unloaded(object? sender, RoutedEventArgs e) { Source = null; diff --git a/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs index 7e039bb0c259..4cbd9131e681 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs @@ -36,7 +36,7 @@ public PDFPreviewViewModel(ListedItem item) public async override Task> LoadPreviewAndDetailsAsync() { - var fileStream = await Item.ItemFile.OpenReadAsync(); + var fileStream = await PreviewFile.OpenReadAsync(); var pdf = await PdfDocument.LoadFromStreamAsync(fileStream); _ = TryLoadPagesAsync(pdf, fileStream); diff --git a/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs index 18d55829a526..f3905b176d74 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs @@ -8,13 +8,13 @@ namespace Files.App.ViewModels.Previews { public sealed partial class RichTextPreviewViewModel : BasePreviewModel { - public IRandomAccessStream Stream { get; set; } + public IRandomAccessStream? Stream { get; set; } public RichTextPreviewViewModel(ListedItem item) : base(item) { } public async override Task> LoadPreviewAndDetailsAsync() { - Stream = await Item.ItemFile.OpenReadAsync(); + Stream = await PreviewFile.OpenReadAsync(); return []; } diff --git a/src/Files.App/ViewModels/UserControls/Previews/ShortcutPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ShortcutPreviewViewModel.cs index ee0d5126488d..cc58b890eacf 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ShortcutPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ShortcutPreviewViewModel.cs @@ -11,7 +11,9 @@ public ShortcutPreviewViewModel(ListedItem item) : base(item) { } public async override Task> LoadPreviewAndDetailsAsync() { - var item = Item as IShortcutItem; + if (Item is not IShortcutItem item) + return []; + var details = new List { GetFileProperty("PropertyParsingPath", item.ItemPath), diff --git a/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs index 0fa858db2077..7a0f93023a1c 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs @@ -8,8 +8,8 @@ namespace Files.App.ViewModels.Previews { public sealed partial class TextPreviewViewModel : BasePreviewModel { - private string textValue; - public string TextValue + private string? textValue; + public string? TextValue { get => textValue; private set => SetProperty(ref textValue, value); @@ -26,7 +26,7 @@ public async override Task> LoadPreviewAndDetailsAsync() try { - var text = TextValue ?? await ReadFileAsTextAsync(Item.ItemFile); + var text = TextValue ?? await ReadFileAsTextAsync(PreviewFile); details.Add(GetFileProperty("PropertyLineCount", text.Split('\n').Length)); details.Add(GetFileProperty("PropertyWordCount", text.Split(new[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries).Length)); @@ -41,17 +41,19 @@ public async override Task> LoadPreviewAndDetailsAsync() return details; } - public static async Task TryLoadAsTextAsync(ListedItem item) + public static async Task TryLoadAsTextAsync(ListedItem item) { - string extension = item.FileExtension?.ToLowerInvariant(); + string extension = item.FileExtension.ToLowerInvariant(); if (ExcludedExtensions(extension) || item.FileSizeBytes is 0 or > Constants.PreviewPane.TryLoadAsTextSizeLimit) return null; try { item.ItemFile = await StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath); + if (item.ItemFile is not { } itemFile) + return null; - var text = await ReadFileAsTextAsync(item.ItemFile); + var text = await ReadFileAsTextAsync(itemFile); bool isBinaryFile = text.Contains("\0\0\0\0", StringComparison.Ordinal); if (isBinaryFile) @@ -68,7 +70,7 @@ public static async Task TryLoadAsTextAsync(ListedItem item) } } - private static bool ExcludedExtensions(string extension) + private static bool ExcludedExtensions(string? extension) => extension is ".iso"; } } diff --git a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs index 8f7b4e101058..74d2bef80841 100644 --- a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs @@ -29,20 +29,20 @@ public sealed partial class SidebarViewModel : ObservableObject, IDisposable private readonly DrivesViewModel drivesViewModel = Ioc.Default.GetRequiredService(); private readonly IFileTagsService fileTagsService; - private IShellPanesPage paneHolder; - public IShellPanesPage PaneHolder + private IShellPanesPage? paneHolder; + public IShellPanesPage? PaneHolder { get => paneHolder; set => SetProperty(ref paneHolder, value); } - public MenuFlyout PaneFlyout; + public MenuFlyout? PaneFlyout; - public IFilesystemHelpers FilesystemHelpers + public IFilesystemHelpers? FilesystemHelpers => PaneHolder?.FilesystemHelpers; - private Microsoft.UI.Dispatching.DispatcherQueue dispatcherQueue; - private INavigationControlItem rightClickedItem; + private readonly Microsoft.UI.Dispatching.DispatcherQueue? dispatcherQueue; + private INavigationControlItem? rightClickedItem; private readonly UISettings uiSettings = new(); public object SidebarItems => sidebarItems; @@ -69,7 +69,7 @@ public SidebarDisplayMode SidebarDisplayMode } } - public delegate void SelectedTagChangedEventHandler(object sender, SelectedTagChangedEventArgs e); + public delegate void SelectedTagChangedEventHandler(object? sender, SelectedTagChangedEventArgs e); public static event SelectedTagChangedEventHandler? SelectedTagChanged; public static event EventHandler? RightClickedItemChanged; @@ -282,9 +282,9 @@ public bool ShowFileTagsSection } } - private INavigationControlItem selectedSidebarItem; + private INavigationControlItem? selectedSidebarItem; - public INavigationControlItem SidebarSelectedItem + public INavigationControlItem? SidebarSelectedItem { get => selectedSidebarItem; set => SetProperty(ref selectedSidebarItem, value); @@ -334,7 +334,7 @@ public SidebarViewModel() ReorderItemsCommand = new AsyncRelayCommand(ReorderItemsAsync); } - private Task CreateItemHomeAsync() + private Task CreateItemHomeAsync() { return CreateSectionAsync(SectionType.Home); } @@ -363,15 +363,17 @@ private void RefreshSectionIcons() } } - private async void Manager_DataChanged(object sender, NotifyCollectionChangedEventArgs e) + private async void Manager_DataChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (dispatcherQueue is null) + if (dispatcherQueue is null || sender is not SectionType sectionType) return; await dispatcherQueue.EnqueueOrInvokeAsync(async () => { - var sectionType = (SectionType)sender; var section = await GetOrCreateSectionAsync(sectionType); + if (section is null) + return; + Func> getElements = () => sectionType switch { SectionType.Pinned => App.QuickAccessManager.Model.PinnedFolderItems, @@ -381,7 +383,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(async () => SectionType.WSL => WSLDistroManager.Distros, SectionType.Library => App.LibraryManager.Libraries, SectionType.FileTag => App.FileTagsManager.FileTags, - _ => null + _ => [] }; await SyncSidebarItemsAsync(section, getElements, e); }); @@ -393,19 +395,22 @@ await dispatcherQueue.EnqueueOrInvokeAsync(async () => private async Task SyncSidebarItemsAsync(LocationItem section, Func> getElements, NotifyCollectionChangedEventArgs e) { - if (section is null) - { - return; - } + var childItems = section.ChildItems ??= []; switch (e.Action) { case NotifyCollectionChangedAction.Add: { - for (int i = 0; i < e.NewItems.Count; i++) + if (e.NewItems is not { } newItems) + break; + + for (int i = 0; i < newItems.Count; i++) { + if (newItems[i] is not INavigationControlItem item) + continue; + var index = e.NewStartingIndex < 0 ? -1 : i + e.NewStartingIndex; - await AddElementToSectionAsync((INavigationControlItem)e.NewItems[i], section, index); + await AddElementToSectionAsync(item, section, index); } break; @@ -415,15 +420,18 @@ private async Task SyncSidebarItemsAsync(LocationItem section, Func()) { - var match = section.ChildItems.FirstOrDefault(x => x.Path == elem.Path); + var match = childItems.FirstOrDefault(x => x.Path == elem.Path); if (match is null) continue; // Tear down the matched item's watcher + descendants before removing it; without this the orphan keeps a live FileSystemWatcher and would fire resyncs into a detached ChildItems collection. if (match is ExpandableSidebarItemBase expandable) expandable.StopWatchingSubfoldersAndDescendants(); - section.ChildItems.Remove(match); + childItems.Remove(match); } if (e.Action != NotifyCollectionChangedAction.Remove) { @@ -439,13 +447,13 @@ private async Task SyncSidebarItemsAsync(LocationItem section, Func x.Path == elem.Path)) { if (elem is ExpandableSidebarItemBase expandable) expandable.StopWatchingSubfoldersAndDescendants(); - section.ChildItems.Remove(elem); + childItems.Remove(elem); } } @@ -459,13 +467,15 @@ private bool IsLibraryOnSidebar(LibraryLocationItem item) private async Task AddElementToSectionAsync(INavigationControlItem elem, LocationItem section, int index = -1) { + var childItems = section.ChildItems ??= []; + if (elem is LibraryLocationItem lib) { if (IsLibraryOnSidebar(lib) && await lib.CheckDefaultSaveFolderAccess() && - !section.ChildItems.Any(x => x.Path == lib.Path)) + !childItems.Any(x => x.Path == lib.Path)) { - section.ChildItems.AddSorted(elem); + childItems.AddSorted(elem); await lib.LoadLibraryIconAsync(); } } @@ -474,32 +484,33 @@ await lib.CheckDefaultSaveFolderAccess() && if (section.Section is SectionType.Network or SectionType.CloudDrives) { // Already sorted - if (!section.ChildItems.Any(x => x.Path == drive.Path)) + if (!childItems.Any(x => x.Path == drive.Path)) { - section.ChildItems.Insert(index < 0 ? section.ChildItems.Count : Math.Min(index, section.ChildItems.Count), drive); + childItems.Insert(index < 0 ? childItems.Count : Math.Min(index, childItems.Count), drive); await drive.LoadThumbnailAsync(); } } else { string drivePath = drive.Path; - var paths = section.ChildItems.Select(item => item.Path).ToList(); + var paths = childItems.Select(item => item.Path).ToList(); if (!paths.Contains(drivePath)) { - paths.AddSorted(drivePath); - int position = paths.IndexOf(drivePath); + int position = paths.BinarySearch(drivePath, Comparer.Default); + if (position < 0) + position = ~position; - section.ChildItems.Insert(position, drive); + childItems.Insert(position, drive); await drive.LoadThumbnailAsync(); } } } else { - if (!section.ChildItems.Any(x => x.Path == elem.Path)) + if (!childItems.Any(x => x.Path == elem.Path)) { - section.ChildItems.Insert(index < 0 ? section.ChildItems.Count : Math.Min(index, section.ChildItems.Count), elem); + childItems.Insert(index < 0 ? childItems.Count : Math.Min(index, childItems.Count), elem); } } @@ -525,10 +536,9 @@ private static async Task ApplyTreeViewExpandabilityAsync(INavigationControlItem expandable.HasUnrealizedChildren = hasSubfolders; } - private async Task GetOrCreateSectionAsync(SectionType sectionType) + private async Task GetOrCreateSectionAsync(SectionType sectionType) { - LocationItem? section = GetSection(sectionType) ?? await CreateSectionAsync(sectionType); - return section; + return GetSection(sectionType) ?? await CreateSectionAsync(sectionType); } private LocationItem? GetSection(SectionType sectionType) @@ -536,9 +546,9 @@ private async Task GetOrCreateSectionAsync(SectionType sectionType return sidebarItems.FirstOrDefault(x => x.Section == sectionType) as LocationItem; } - private Task CreateSectionAsync(SectionType sectionType) + private Task CreateSectionAsync(SectionType sectionType) { - LocationItem section = null; + LocationItem? section = null; switch (sectionType) { @@ -656,7 +666,7 @@ public async Task UpdateSectionVisibilityAsync(SectionType sectionType, bool sho } } - private async void UserSettingsService_OnSettingChangedEvent(object sender, SettingChangedEventArgs e) + private async void UserSettingsService_OnSettingChangedEvent(object? sender, SettingChangedEventArgs e) { switch (e.SettingName) { @@ -746,7 +756,6 @@ public void Dispose() uiSettings.ColorValuesChanged -= UiSettings_ColorValuesChanged; TeardownTabExpansionTracking(); - dispatcherQueue = null; } public void UpdateTabControlMargin() @@ -767,7 +776,7 @@ public async void HandleItemContextInvokedAsync(object sender, ItemContextInvoke if (args.Item is not INavigationControlItem item) { // We are in the pane context requested path - PaneFlyout.ShowAt(sender as FrameworkElement, args.Position); + PaneFlyout?.ShowAt(sidebarItem, args.Position); return; } @@ -841,11 +850,12 @@ void AddCloseHandlerRecursive(IEnumerable menuFlyoutItems) private async void ItemContextMenuFlyout_Opened(object? sender, object e) { - if (sender is not CommandBarFlyout itemContextMenuFlyout) + if (sender is not CommandBarFlyout itemContextMenuFlyout || + rightClickedItem is not { Path: { } itemPath } item) return; itemContextMenuFlyout.Opened -= ItemContextMenuFlyout_Opened; - await ShellContextFlyoutFactory.LoadShellMenuItemsAsync(rightClickedItem.Path, itemContextMenuFlyout, rightClickedItem.MenuOptions); + await ShellContextFlyoutFactory.LoadShellMenuItemsAsync(itemPath, itemContextMenuFlyout, item.MenuOptions); } public async void HandleItemInvokedAsync(object item, PointerUpdateKind pointerUpdateKind) @@ -907,7 +917,7 @@ public async void HandleItemInvokedAsync(object item, PointerUpdateKind pointerU case NavigationControlItemType.FileTag: var tagPath = navigationControlItem.Path; // Get the path of the invoked item - if (PaneHolder?.ActivePane is IShellPage shp) + if (tagPath is not null && PaneHolder?.ActivePane is IShellPage shp) { shp.NavigateToPath(tagPath, new NavigationArguments() { @@ -927,7 +937,7 @@ public async void HandleItemInvokedAsync(object item, PointerUpdateKind pointerU } } - if (PaneHolder?.ActivePane is IShellPage shellPage) + if (navigationPath is not null && PaneHolder?.ActivePane is IShellPage shellPage) shellPage.NavigateToPath(navigationPath, sourcePageType); } @@ -949,18 +959,25 @@ public async void HandleItemInvokedAsync(object item, PointerUpdateKind pointerU private void PinItem() { - if (rightClickedItem is DriveItem) - _ = QuickAccessService.PinToSidebarAsync(new[] { rightClickedItem.Path }); + if (rightClickedItem is DriveItem { Path: { } path }) + _ = QuickAccessService.PinToSidebarAsync([path]); } + private void UnpinItem() { - if (rightClickedItem.Section == SectionType.Pinned || rightClickedItem is DriveItem) - _ = QuickAccessService.UnpinFromSidebarAsync(rightClickedItem.Path); + if (rightClickedItem is { Path: { } path } item && + (item.Section == SectionType.Pinned || item is DriveItem)) + { + _ = QuickAccessService.UnpinFromSidebarAsync(path); + } } private void HideSection() { - switch (rightClickedItem.Section) + if (rightClickedItem is not { } item) + return; + + switch (item.Section) { case SectionType.Pinned: UserSettingsService.GeneralSettingsService.ShowPinnedSection = false; @@ -993,19 +1010,25 @@ private async Task ReorderItemsAsync() var result = await dialogService.ShowDialogAsync(dialog); } - private void OpenProperties(CommandBarFlyout menu) + private void OpenProperties(CommandBarFlyout? menu) { - EventHandler flyoutClosed = null!; - flyoutClosed = async (s, e) => + if (menu is null || + rightClickedItem is not { } item || + PaneHolder?.ActivePane is not { } activePane) + return; + + menu.Closed += FlyoutClosed; + + async void FlyoutClosed(object? sender, object e) { - menu.Closed -= flyoutClosed; - if (rightClickedItem is DriveItem) - FilePropertiesHelpers.OpenPropertiesWindow(rightClickedItem, PaneHolder.ActivePane); - else if (rightClickedItem is LibraryLocationItem library) - FilePropertiesHelpers.OpenPropertiesWindow(new LibraryItem(library), PaneHolder.ActivePane); - else if (rightClickedItem is LocationItem locationItem) + menu.Closed -= FlyoutClosed; + if (item is DriveItem) + FilePropertiesHelpers.OpenPropertiesWindow(item, activePane); + else if (item is LibraryLocationItem library) + FilePropertiesHelpers.OpenPropertiesWindow(new LibraryItem(library), activePane); + else if (item is LocationItem locationItem) { - var listedItem = new ListedItem(null!) + var listedItem = new ListedItem(null) { ItemPath = locationItem.Path, ItemNameRaw = locationItem.Text, @@ -1013,25 +1036,27 @@ private void OpenProperties(CommandBarFlyout menu) ItemType = Strings.Folder.GetLocalizedResource(), }; - if (!string.Equals(locationItem.Path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(locationItem.Path) && + !string.Equals(locationItem.Path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase) && + activePane.ShellViewModel is { } shellViewModel) { - BaseStorageFolder matchingStorageFolder = await PaneHolder.ActivePane.ShellViewModel.GetFolderFromPathAsync(locationItem.Path); - if (matchingStorageFolder is not null) + var matchingStorageFolder = await shellViewModel.GetFolderFromPathAsync(locationItem.Path); + if (matchingStorageFolder.Result is { } folder) { - var syncStatus = await PaneHolder.ActivePane.ShellViewModel.CheckCloudDriveSyncStatusAsync(matchingStorageFolder); + var syncStatus = await shellViewModel.CheckCloudDriveSyncStatusAsync(folder); listedItem.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); } } - FilePropertiesHelpers.OpenPropertiesWindow(listedItem, PaneHolder.ActivePane); + FilePropertiesHelpers.OpenPropertiesWindow(listedItem, activePane); } - }; - menu.Closed += flyoutClosed; + } } private void EjectDevice() { - DriveHelpers.EjectDeviceAsync(rightClickedItem.Path); + if (rightClickedItem is { Path: { } path }) + DriveHelpers.EjectDeviceAsync(path); } private List GetLocationItemMenuItems(INavigationControlItem item, CommandBarFlyout menu) @@ -1159,7 +1184,7 @@ private List GetLocationItemMenuItems(INavigatio }, new ContextMenuFlyoutItemViewModel() { - Text = string.Format(Strings.SideBarHideSectionFromSideBarText.GetLocalizedResource(), rightClickedItem.Text), + Text = string.Format(Strings.SideBarHideSectionFromSideBarText.GetLocalizedResource(), item.Text), Glyph = "\uE77A", Command = HideSectionCommand, ShowItem = options.ShowHideSection @@ -1406,14 +1431,17 @@ private async Task HandleLocationItemDroppedAsync(LocationItem locationItem, Ite } else { - await FilesystemHelpers.PerformOperationTypeAsync(args.RawEvent.AcceptedOperation, args.DroppedItem, locationItem.Path, false, true); + if (FilesystemHelpers is { } filesystemHelpers && locationItem.Path is { } path) + await filesystemHelpers.PerformOperationTypeAsync(args.RawEvent.AcceptedOperation, args.DroppedItem, path, false, true); } } } private Task HandleDriveItemDroppedAsync(DriveItem driveItem, ItemDroppedEventArgs args) { - return FilesystemHelpers.PerformOperationTypeAsync(args.RawEvent.AcceptedOperation, args.RawEvent.DataView, driveItem.Path, false, true); + return FilesystemHelpers is { } filesystemHelpers + ? filesystemHelpers.PerformOperationTypeAsync(args.RawEvent.AcceptedOperation, args.RawEvent.DataView, driveItem.Path, false, true) + : Task.FromResult(ReturnResult.NullException); } private async Task HandleTagItemDroppedAsync(FileTagItem fileTagItem, ItemDroppedEventArgs args) @@ -1434,10 +1462,10 @@ private async Task HandleTagItemDroppedAsync(FileTagItem fileTagItem, ItemDroppe } } - if (paneHolder.ActivePane is not null) + if (PaneHolder?.ActivePane?.ShellViewModel is { } shellViewModel) { - await paneHolder.ActivePane.ShellViewModel.UpdateItemsTags(pathToTags); - await paneHolder.ActivePane.ShellViewModel.RefreshTagGroups(); + await shellViewModel.UpdateItemsTags(pathToTags); + await shellViewModel.RefreshTagGroups(); } } diff --git a/src/Files.App/ViewModels/UserControls/StatusCenterViewModel.cs b/src/Files.App/ViewModels/UserControls/StatusCenterViewModel.cs index fc8a2e8570a4..483c1a9da803 100644 --- a/src/Files.App/ViewModels/UserControls/StatusCenterViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/StatusCenterViewModel.cs @@ -98,7 +98,7 @@ public StatusCenterItem AddItem( bool canProvideProgress = true, long itemsCount = 0, long totalSize = 0, - CancellationTokenSource cancellationTokenSource = null) + CancellationTokenSource? cancellationTokenSource = null) { var banner = new StatusCenterItem( headerResource, diff --git a/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs index 52cdb1a21e54..49d9a7d52480 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs @@ -12,7 +12,7 @@ namespace Files.App.ViewModels.UserControls.Widgets /// public sealed partial class FileTagsWidgetViewModel : BaseWidgetViewModel, IWidgetViewModel { - private CancellationTokenSource _updateCTS; + private CancellationTokenSource? _updateCTS; private bool _isInitialized; // Properties diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs index bf91920ce406..e1a48fc9ad78 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs @@ -280,38 +280,41 @@ public override async Task ExecuteUnpinFromSidebarCommand(WidgetCardItem? item) private void ExecuteOpenPropertiesCommand(WidgetFolderCardItem? item) { - if (!HomePageContext.IsAnyItemRightClicked || item is null || item.Item is null) - return; - var flyout = HomePageContext.ItemContextFlyoutMenu; - EventHandler flyoutClosed = null!; + var shellPage = ContentPageContext.ShellPage; + if (!HomePageContext.IsAnyItemRightClicked || + item is null || + string.IsNullOrEmpty(item.Path) || + flyout is null || + shellPage?.ShellViewModel is not { } shellViewModel) + return; - flyoutClosed = async (s, e) => + async void FlyoutClosed(object? sender, object args) { - flyout!.Closed -= flyoutClosed; + flyout.Closed -= FlyoutClosed; - ListedItem listedItem = new(null!) + ListedItem listedItem = new(null) { ItemPath = item.Path, - ItemNameRaw = item.Text, + ItemNameRaw = item.Text ?? System.IO.Path.GetFileName(item.Path), PrimaryItemAttribute = StorageItemTypes.Folder, ItemType = Strings.Folder.GetLocalizedResource(), }; if (!string.Equals(item.Path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) { - BaseStorageFolder matchingStorageFolder = await ContentPageContext.ShellPage!.ShellViewModel.GetFolderFromPathAsync(item.Path); + BaseStorageFolder? matchingStorageFolder = (await shellViewModel.GetFolderFromPathAsync(item.Path)).Result; if (matchingStorageFolder is not null) { - var syncStatus = await ContentPageContext.ShellPage!.ShellViewModel.CheckCloudDriveSyncStatusAsync(matchingStorageFolder); + var syncStatus = await shellViewModel.CheckCloudDriveSyncStatusAsync(matchingStorageFolder); listedItem.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); } } - FilePropertiesHelpers.OpenPropertiesWindow(listedItem, ContentPageContext.ShellPage!); - }; + FilePropertiesHelpers.OpenPropertiesWindow(listedItem, shellPage); + } - flyout!.Closed += flyoutClosed; + flyout.Closed += FlyoutClosed; } // Disposer diff --git a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs index 2b619b2e1abd..9e98f777f507 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs @@ -305,35 +305,36 @@ private async Task ExecuteClearRecentItemsCommand() private void ExecuteOpenFileLocationCommand(RecentItem? item) { - if (item is null) + var shellPage = ContentPageContext.ShellPage; + if (item is null || string.IsNullOrEmpty(item.Path) || shellPage is null) return; var itemPath = Directory.GetParent(item.Path)?.FullName ?? string.Empty; var itemName = Path.GetFileName(item.Path); - ContentPageContext.ShellPage!.NavigateWithArguments( - ContentPageContext.ShellPage!.InstanceViewModel.FolderSettings.GetLayoutType(itemPath), + shellPage.NavigateWithArguments( + shellPage.InstanceViewModel.FolderSettings.GetLayoutType(itemPath), new NavigationArguments() { NavPathParam = itemPath, SelectItems = new[] { itemName }, - AssociatedTabInstance = ContentPageContext.ShellPage! + AssociatedTabInstance = shellPage }); } private void ExecuteOpenPropertiesCommand(RecentItem? item) { var flyout = HomePageContext.ItemContextFlyoutMenu; + var shellPage = ContentPageContext.ShellPage; - if (item is null || flyout is null) + if (item is null || string.IsNullOrEmpty(item.Path) || flyout is null || shellPage is null) return; - EventHandler flyoutClosed = null!; - flyoutClosed = async (s, e) => + async void FlyoutClosed(object? sender, object args) { - flyout!.Closed -= flyoutClosed; + flyout.Closed -= FlyoutClosed; - BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.Path)); + BaseStorageFile? file = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.Path)); if (file is null) { ContentDialog dialog = new() @@ -350,12 +351,13 @@ private void ExecuteOpenPropertiesCommand(RecentItem? item) } else { - var listedItem = await UniversalStorageEnumerator.AddFileAsync(file, null!, default); - FilePropertiesHelpers.OpenPropertiesWindow(listedItem, ContentPageContext.ShellPage!); + var listedItem = await UniversalStorageEnumerator.AddFileAsync(file, null, default); + if (listedItem is not null) + FilePropertiesHelpers.OpenPropertiesWindow(listedItem, shellPage); } - }; + } - flyout!.Closed += flyoutClosed; + flyout.Closed += FlyoutClosed; } // Disposer diff --git a/src/Files.App/Views/HomePage.xaml.cs b/src/Files.App/Views/HomePage.xaml.cs index 80da0462deb0..cb1d694f1e7a 100644 --- a/src/Files.App/Views/HomePage.xaml.cs +++ b/src/Files.App/Views/HomePage.xaml.cs @@ -17,7 +17,9 @@ public sealed partial class HomePage : Page, IDisposable // Properties - private IShellPage AppInstance { get; set; } = null!; + private IShellPage? appInstance; + private IShellPage AppInstance + => appInstance ?? throw new InvalidOperationException("The home page has not been initialized."); // Constructor @@ -30,10 +32,11 @@ public HomePage() protected override async void OnNavigatedTo(NavigationEventArgs e) { - if (e.Parameter is not NavigationArguments parameters) + if (e.Parameter is not NavigationArguments { AssociatedTabInstance: { } associatedInstance } parameters || + associatedInstance.ShellViewModel is not { } shellViewModel) return; - AppInstance = parameters.AssociatedTabInstance!; + appInstance = associatedInstance; AppInstance.InstanceViewModel.IsPageTypeNotHome = false; AppInstance.InstanceViewModel.IsPageTypeSearchResults = false; @@ -53,8 +56,8 @@ protected override async void OnNavigatedTo(NavigationEventArgs e) AppInstance.ToolbarViewModel.CanNavigateToParent = false; // Set path of working directory empty - await AppInstance.ShellViewModel.SetWorkingDirectoryAsync("Home"); - AppInstance.ShellViewModel.CheckForBackgroundImage(); + await shellViewModel.SetWorkingDirectoryAsync("Home"); + shellViewModel.CheckForBackgroundImage(); AppInstance.SlimContentPage?.StatusBarViewModel.UpdateGitInfo(false, string.Empty, null); diff --git a/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs b/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs index 5baf57f29ebd..01cc8c39bf95 100644 --- a/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseGroupableLayoutPage.cs @@ -55,7 +55,10 @@ public BaseGroupableLayoutPage() : base() protected override void InitializeCommandsViewModel() { - CommandsViewModel = new BaseLayoutViewModel(ParentShellPageInstance, ItemManipulationModel); + var parentShellPage = ParentShellPageInstance + ?? throw new InvalidOperationException("The layout page must be associated with a shell page before its commands are initialized."); + + CommandsViewModel = new BaseLayoutViewModel(parentShellPage, ItemManipulationModel); } protected override void HookEvents() @@ -129,40 +132,46 @@ protected virtual async void ItemManipulationModel_RefreshItemThumbnail(object? protected virtual async Task ReloadSelectedItemIconAsync() { - if (ParentShellPageInstance?.SlimContentPage?.SelectedItem is null) + var parentShellPage = ParentShellPageInstance; + var selectedItem = parentShellPage?.SlimContentPage?.SelectedItem; + var shellViewModel = parentShellPage?.ShellViewModel; + if (selectedItem is null || shellViewModel is null) return; - ParentShellPageInstance.ShellViewModel.CancelExtendedPropertiesLoading(); - ParentShellPageInstance.SlimContentPage.SelectedItem.ItemPropertiesInitialized = false; + shellViewModel.CancelExtendedPropertiesLoading(); + selectedItem.ItemPropertiesInitialized = false; - await ParentShellPageInstance.ShellViewModel.LoadExtendedItemPropertiesAsync(ParentShellPageInstance.SlimContentPage.SelectedItem); + await shellViewModel.LoadExtendedItemPropertiesAsync(selectedItem); - if (ParentShellPageInstance.ShellViewModel.EnabledGitProperties is not GitProperties.None && - ParentShellPageInstance.SlimContentPage.SelectedItem is IGitItem gitItem) + if (shellViewModel.EnabledGitProperties is not GitProperties.None && + selectedItem is IGitItem gitItem) { - await ParentShellPageInstance.ShellViewModel.LoadGitPropertiesAsync(gitItem); + await shellViewModel.LoadGitPropertiesAsync(gitItem); } } protected virtual async Task ReloadSelectedItemsIconAsync() { - if (ParentShellPageInstance?.SlimContentPage?.SelectedItems is null) + var parentShellPage = ParentShellPageInstance; + var selectedItems = parentShellPage?.SlimContentPage?.SelectedItems; + var shellViewModel = parentShellPage?.ShellViewModel; + if (selectedItems is null || shellViewModel is null) return; - ParentShellPageInstance.ShellViewModel.CancelExtendedPropertiesLoading(); + shellViewModel.CancelExtendedPropertiesLoading(); - foreach (var selectedItem in ParentShellPageInstance.SlimContentPage.SelectedItems) + foreach (var selectedItem in selectedItems) { selectedItem.ItemPropertiesInitialized = false; - await ParentShellPageInstance.ShellViewModel.LoadExtendedItemPropertiesAsync(selectedItem); + await shellViewModel.LoadExtendedItemPropertiesAsync(selectedItem); } - if (ParentShellPageInstance.ShellViewModel.EnabledGitProperties is not GitProperties.None) + if (shellViewModel.EnabledGitProperties is not GitProperties.None) { - await Task.WhenAll(ParentShellPageInstance.SlimContentPage.SelectedItems.Select(item => + await Task.WhenAll(selectedItems.Select(item => { if (item is IGitItem gitItem) - return ParentShellPageInstance.ShellViewModel.LoadGitPropertiesAsync(gitItem); + return shellViewModel.LoadGitPropertiesAsync(gitItem); return Task.CompletedTask; })); @@ -267,32 +276,35 @@ protected static bool ShouldShowExtensionInRename(ListedItem item) => protected virtual void StartRenameItem(string itemNameTextBox) { RenamingItem = SelectedItem; - if (RenamingItem is null) + var renamingItem = RenamingItem; + if (renamingItem is null) return; - int extensionLength = RenamingItem.FileExtension?.Length ?? 0; + int extensionLength = renamingItem.FileExtension?.Length ?? 0; - ListViewItem? listViewItem = ListViewBase.ContainerFromItem(RenamingItem) as ListViewItem; + ListViewItem? listViewItem = ListViewBase.ContainerFromItem(renamingItem) as ListViewItem; if (listViewItem is null) return; - TextBox? textBox = null; TextBlock? textBlock = listViewItem.FindDescendant("ItemName") as TextBlock; - textBox = listViewItem.FindDescendant(itemNameTextBox) as TextBox; - string editText = ShouldShowExtensionInRename(RenamingItem) ? RenamingItem.ItemNameRaw : textBlock!.Text; - textBox!.Text = editText; + TextBox? textBox = listViewItem.FindDescendant(itemNameTextBox) as TextBox; + if (textBlock is null || textBox is null) + return; + + string editText = ShouldShowExtensionInRename(renamingItem) ? renamingItem.ItemNameRaw : textBlock.Text; + textBox.Text = editText; OldItemName = editText; - textBlock!.Visibility = Visibility.Collapsed; + textBlock.Visibility = Visibility.Collapsed; textBox.Visibility = Visibility.Visible; - if (textBox.FindParent() is null) + if (textBox.FindParent() is not { } parentGrid) { textBlock.Visibility = Visibility.Visible; textBox.Visibility = Visibility.Collapsed; return; } - Grid.SetColumnSpan(textBox.FindParent(), 8); + Grid.SetColumnSpan(parentGrid, 8); textBox.Focus(FocusState.Pointer); textBox.LostFocus += RenameTextBox_LostFocus; @@ -300,7 +312,7 @@ protected virtual void StartRenameItem(string itemNameTextBox) int selectedTextLength = editText.Length; - if (!SelectedItem.IsShortcut && (ShouldShowExtensionInRename(SelectedItem) || UserSettingsService.FoldersSettingsService.ShowFileExtensions)) + if (!renamingItem.IsShortcut && (ShouldShowExtensionInRename(renamingItem) || UserSettingsService.FoldersSettingsService.ShowFileExtensions)) selectedTextLength -= extensionLength; textBox.Select(0, selectedTextLength); @@ -309,10 +321,15 @@ protected virtual void StartRenameItem(string itemNameTextBox) protected virtual async Task CommitRenameAsync(TextBox textBox) { + var renamingItem = RenamingItem; + var parentShellPage = ParentShellPageInstance; EndRename(textBox); + if (renamingItem is null || parentShellPage is null) + return; + string newItemName = textBox.Text.Trim().TrimEnd('.'); - await UIFilesystemHelpers.RenameFileItemAsync(RenamingItem, newItemName, ParentShellPageInstance, nameIsComplete: ShouldShowExtensionInRename(RenamingItem)); + await UIFilesystemHelpers.RenameFileItemAsync(renamingItem, newItemName, parentShellPage, nameIsComplete: ShouldShowExtensionInRename(renamingItem)); } protected virtual async void RenameTextBox_LostFocus(object sender, RoutedEventArgs e) diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index 5623035799c9..3bff4eff38c4 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -183,7 +183,8 @@ public string JumpString // search for next file that starts with "a" (a.k.a. _jumpString = "a") if (jumpString.Length == 1 && value == jumpString + jumpString) value = jumpString; - if (value != string.Empty) + if (value != string.Empty && + ParentShellPageInstance?.ShellViewModel is { } shellViewModel) { ListedItem? jumpedToItem = null; ListedItem? previouslySelectedItem = IsItemSelected ? SelectedItem : null; @@ -192,7 +193,7 @@ public string JumpString if (previouslySelectedItem is not null) { // Use FilesAndFolders because only displayed entries should be jumped to - IEnumerable candidateItems = ParentShellPageInstance!.ShellViewModel.FilesAndFolders.ToList() + IEnumerable candidateItems = shellViewModel.FilesAndFolders.ToList() .SkipWhile(x => x != previouslySelectedItem) .Skip(value.Length == 1 ? 1 : 0) // User is trying to cycle through items starting with the same letter .Where(f => f.Name.Length >= value.Length && string.Equals(f.Name.Substring(0, value.Length), value, StringComparison.OrdinalIgnoreCase)); @@ -202,7 +203,7 @@ public string JumpString if (jumpedToItem is null) { // Use FilesAndFolders because only displayed entries should be jumped to - IEnumerable candidateItems = ParentShellPageInstance!.ShellViewModel.FilesAndFolders.ToList() + IEnumerable candidateItems = shellViewModel.FilesAndFolders.ToList() .Where(f => f.Name.Length >= value.Length && string.Equals(f.Name.Substring(0, value.Length), value, StringComparison.OrdinalIgnoreCase)); jumpedToItem = candidateItems.FirstOrDefault(); } @@ -223,19 +224,19 @@ public string JumpString } private bool isSelectedItemsSorted = false; - private List? selectedItems = []; - public List? SelectedItems + private List selectedItems = []; + public List SelectedItems { get { - if (!isSelectedItemsSorted) + if (!isSelectedItemsSorted && FolderSettings is { } folderSettings) { - var orderedItems = SortingHelper.OrderFileList(selectedItems, FolderSettings.DirectorySortOption, FolderSettings.DirectorySortDirection, FolderSettings.SortDirectoriesAlongsideFiles, FolderSettings.SortFilesFirst).ToList(); + var orderedItems = SortingHelper.OrderFileList(selectedItems, folderSettings.DirectorySortOption, folderSettings.DirectorySortDirection, folderSettings.SortDirectoriesAlongsideFiles, folderSettings.SortFilesFirst).ToList(); selectedItems = orderedItems; isSelectedItemsSorted = true; } - return SelectedItem is null || !selectedItems!.Contains(SelectedItem) + return SelectedItem is null || !selectedItems.Contains(SelectedItem) ? selectedItems : selectedItems .SkipWhile(x => x != SelectedItem) @@ -249,7 +250,7 @@ internal set isSelectedItemsSorted = false; selectedItems = value; - if (selectedItems?.Count == 0 || selectedItems?[0] is null) + if (selectedItems.Count == 0) { IsItemSelected = false; SelectedItem = null; @@ -258,7 +259,7 @@ internal set ResetRenameDoubleClick(); UpdateSelectionSize(); } - else if (selectedItems is not null) + else { IsItemSelected = true; SelectedItem = selectedItems.First(); @@ -267,7 +268,7 @@ internal set UpdateSelectionSize(); SelectedItemsPropertiesViewModel.SelectedItemsCount = selectedItems.Count; - SelectedItemsPropertiesViewModel.SelectedItemsCountString = Strings.SelectedItems.GetLocalizedFormatResource(selectedItems!.Count); + SelectedItemsPropertiesViewModel.SelectedItemsCountString = Strings.SelectedItems.GetLocalizedFormatResource(selectedItems.Count); if (selectedItems.Count == 1) { @@ -285,7 +286,7 @@ internal set NotifyPropertyChanged(nameof(SelectedItems)); } if (!isDraggingSelectionRectangle) - ParentShellPageInstance!.ToolbarViewModel.SelectedItems = value; + ParentShellPageInstance?.ToolbarViewModel.SelectedItems = value; } } @@ -343,7 +344,7 @@ private void JumpTimer_Tick(object sender, object e) jumpTimer.Stop(); } - protected IEnumerable? GetAllItems() + protected IEnumerable GetAllItems() { var items = CollectionViewSource.IsSourceGrouped ? (CollectionViewSource.Source as BulkConcurrentObservableCollection>)?.SelectMany(g => g) // add all items from each group to the new list @@ -375,31 +376,33 @@ public virtual void ResetItemOpacity() protected virtual void BaseFolderSettings_LayoutModeChangeRequested(object? sender, LayoutModeEventArgs e) { - if (ParentShellPageInstance?.SlimContentPage is not null) + if (ParentShellPageInstance is { SlimContentPage: not null, ShellViewModel: { } shellViewModel } parentShellPage && + navigationArguments is { } args) { - var layoutType = FolderSettings!.GetLayoutType(ParentShellPageInstance.ShellViewModel.WorkingDirectory); + var folderSettings = parentShellPage.InstanceViewModel.FolderSettings; + var layoutType = folderSettings.GetLayoutType(shellViewModel.WorkingDirectory); - if (layoutType != ParentShellPageInstance.CurrentPageType) + if (layoutType != parentShellPage.CurrentPageType) { - FolderSettings!.PendingLayoutSwitchSelection = SelectedItems?.Select(item => item.ItemNameRaw).ToList(); + folderSettings.PendingLayoutSwitchSelection = SelectedItems.Select(item => item.ItemNameRaw).ToList(); - ParentShellPageInstance.NavigateWithArguments(layoutType, new NavigationArguments() + parentShellPage.NavigateWithArguments(layoutType, new NavigationArguments() { - NavPathParam = navigationArguments!.NavPathParam, - IsSearchResultPage = navigationArguments.IsSearchResultPage, - SearchPathParam = navigationArguments.SearchPathParam, - SearchQuery = navigationArguments.SearchQuery, + NavPathParam = args.NavPathParam, + IsSearchResultPage = args.IsSearchResultPage, + SearchPathParam = args.SearchPathParam, + SearchQuery = args.SearchQuery, IsLayoutSwitch = true, - AssociatedTabInstance = ParentShellPageInstance + AssociatedTabInstance = parentShellPage }); // Remove old layout from back stack - ParentShellPageInstance.RemoveLastPageFromBackStack(); - ParentShellPageInstance.ResetNavigationStackLayoutMode(); + parentShellPage.RemoveLastPageFromBackStack(); + parentShellPage.ResetNavigationStackLayoutMode(); } - ParentShellPageInstance.ShellViewModel.UpdateEmptyTextType(); - ParentShellPageInstance.ShellViewModel.UpdateNetworkAvailabilityInfoBar(); + shellViewModel.UpdateEmptyTextType(); + shellViewModel.UpdateNetworkAvailabilityInfoBar(); // Focus on the active pane in case it was lost during the layout switch. // Allthough the focus is also set from SetSelectedItemsOnNavigation, @@ -408,7 +411,7 @@ protected virtual void BaseFolderSettings_LayoutModeChangeRequested(object? send // Adaptive layout fires this handler on folder-load completion - skip the focus // restore so an in-progress omnibar query isn't lost. if (!UIHelpers.IsTextInputFocused(XamlRoot)) - ParentShellPageInstance!.PaneHolder.FocusActivePane(); + parentShellPage.PaneHolder?.FocusActivePane(); } } @@ -424,102 +427,111 @@ protected override async void OnNavigatedTo(NavigationEventArgs e) // Add item jumping handler CharacterReceived += Page_CharacterReceived; - navigationArguments = (NavigationArguments)e.Parameter; - ParentShellPageInstance = navigationArguments.AssociatedTabInstance; + if (e.Parameter is not NavigationArguments { AssociatedTabInstance: { } parentShellPage } args || + parentShellPage.ShellViewModel is not { } shellViewModel) + { + throw new InvalidOperationException("Layout navigation requires an initialized shell page."); + } + + navigationArguments = args; + ParentShellPageInstance = parentShellPage; + var folderSettings = parentShellPage.InstanceViewModel.FolderSettings; // Git properties are not loaded by default - ParentShellPageInstance.ShellViewModel.EnabledGitProperties = GitProperties.None; + shellViewModel.EnabledGitProperties = GitProperties.None; InitializeCommandsViewModel(); IsItemSelected = false; - FolderSettings!.LayoutModeChangeRequested += BaseFolderSettings_LayoutModeChangeRequested; - FolderSettings.GroupOptionPreferenceUpdated += FolderSettings_GroupOptionPreferenceUpdated; - FolderSettings.GroupDirectionPreferenceUpdated += FolderSettings_GroupDirectionPreferenceUpdated; - FolderSettings.GroupByDateUnitPreferenceUpdated += FolderSettings_GroupByDateUnitPreferenceUpdated; + folderSettings.LayoutModeChangeRequested += BaseFolderSettings_LayoutModeChangeRequested; + folderSettings.GroupOptionPreferenceUpdated += FolderSettings_GroupOptionPreferenceUpdated; + folderSettings.GroupDirectionPreferenceUpdated += FolderSettings_GroupDirectionPreferenceUpdated; + folderSettings.GroupByDateUnitPreferenceUpdated += FolderSettings_GroupByDateUnitPreferenceUpdated; - ParentShellPageInstance.ShellViewModel.EmptyTextType = EmptyTextType.None; - ParentShellPageInstance.ToolbarViewModel.CanRefresh = true; + shellViewModel.EmptyTextType = EmptyTextType.None; + parentShellPage.ToolbarViewModel.CanRefresh = true; - if (!navigationArguments.IsSearchResultPage) + if (!args.IsSearchResultPage) { - var previousDir = ParentShellPageInstance.ShellViewModel.WorkingDirectory; - await ParentShellPageInstance.ShellViewModel.SetWorkingDirectoryAsync(navigationArguments.NavPathParam); + var navigationPath = args.NavPathParam ?? string.Empty; + var previousDir = shellViewModel.WorkingDirectory; + await shellViewModel.SetWorkingDirectoryAsync(navigationPath); // pathRoot will be empty on recycle bin path - var workingDir = ParentShellPageInstance.ShellViewModel.WorkingDirectory ?? string.Empty; + var workingDir = shellViewModel.WorkingDirectory ?? string.Empty; var pathRoot = GetPathRoot(workingDir); var isRecycleBin = workingDir.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal); - ParentShellPageInstance.InstanceViewModel.IsPageTypeRecycleBin = isRecycleBin; + parentShellPage.InstanceViewModel.IsPageTypeRecycleBin = isRecycleBin; // Can't go up from recycle bin - ParentShellPageInstance.ToolbarViewModel.CanNavigateToParent = !(string.IsNullOrEmpty(pathRoot) || isRecycleBin); - - ParentShellPageInstance.InstanceViewModel.IsPageTypeMtpDevice = workingDir.StartsWith("\\\\?\\", StringComparison.Ordinal); - ParentShellPageInstance.InstanceViewModel.IsPageTypeFtp = FtpHelpers.IsFtpPath(workingDir); - ParentShellPageInstance.InstanceViewModel.IsPageTypeZipFolder = ZipStorageFolder.IsZipPath(workingDir); - ParentShellPageInstance.InstanceViewModel.IsPageTypeLibrary = LibraryManager.IsLibraryPath(workingDir); - ParentShellPageInstance.InstanceViewModel.IsPageTypeSearchResults = false; - ParentShellPageInstance.InstanceViewModel.IsPageTypeReleaseNotes = false; - ParentShellPageInstance.InstanceViewModel.IsPageTypeSettings = false; - ParentShellPageInstance.ToolbarViewModel.PathControlDisplayText = navigationArguments.NavPathParam; - - if (ParentShellPageInstance.InstanceViewModel.FolderSettings.DirectorySortOption == SortOption.Path) - ParentShellPageInstance.InstanceViewModel.FolderSettings.DirectorySortOption = SortOption.Name; - - if (ParentShellPageInstance.InstanceViewModel.FolderSettings.DirectoryGroupOption == GroupOption.FolderPath && - !ParentShellPageInstance.InstanceViewModel.IsPageTypeLibrary) - ParentShellPageInstance.InstanceViewModel.FolderSettings.DirectoryGroupOption = GroupOption.None; - - if (!navigationArguments.IsLayoutSwitch || previousDir != workingDir) - ParentShellPageInstance.ShellViewModel.RefreshItems(previousDir, SetSelectedItemsOnNavigation); + parentShellPage.ToolbarViewModel.CanNavigateToParent = !(string.IsNullOrEmpty(pathRoot) || isRecycleBin); + + parentShellPage.InstanceViewModel.IsPageTypeMtpDevice = workingDir.StartsWith("\\\\?\\", StringComparison.Ordinal); + parentShellPage.InstanceViewModel.IsPageTypeFtp = FtpHelpers.IsFtpPath(workingDir); + parentShellPage.InstanceViewModel.IsPageTypeZipFolder = ZipStorageFolder.IsZipPath(workingDir); + parentShellPage.InstanceViewModel.IsPageTypeLibrary = LibraryManager.IsLibraryPath(workingDir); + parentShellPage.InstanceViewModel.IsPageTypeSearchResults = false; + parentShellPage.InstanceViewModel.IsPageTypeReleaseNotes = false; + parentShellPage.InstanceViewModel.IsPageTypeSettings = false; + parentShellPage.ToolbarViewModel.PathControlDisplayText = navigationPath; + + if (folderSettings.DirectorySortOption == SortOption.Path) + folderSettings.DirectorySortOption = SortOption.Name; + + if (folderSettings.DirectoryGroupOption == GroupOption.FolderPath && + !parentShellPage.InstanceViewModel.IsPageTypeLibrary) + folderSettings.DirectoryGroupOption = GroupOption.None; + + if (!args.IsLayoutSwitch || previousDir != workingDir) + shellViewModel.RefreshItems(previousDir, SetSelectedItemsOnNavigation); else - ParentShellPageInstance.ToolbarViewModel.CanGoForward = false; + parentShellPage.ToolbarViewModel.CanGoForward = false; } else { - await ParentShellPageInstance.ShellViewModel.SetWorkingDirectoryAsync(navigationArguments.SearchPathParam); + var searchPath = args.SearchPathParam ?? string.Empty; + await shellViewModel.SetWorkingDirectoryAsync(searchPath); - ParentShellPageInstance.ToolbarViewModel.CanGoForward = false; + parentShellPage.ToolbarViewModel.CanGoForward = false; // Impose no artificial restrictions on back navigation. Even in a search results page. - ParentShellPageInstance.ToolbarViewModel.CanGoBack = true; + parentShellPage.ToolbarViewModel.CanGoBack = true; - ParentShellPageInstance.ToolbarViewModel.CanNavigateToParent = false; + parentShellPage.ToolbarViewModel.CanNavigateToParent = false; - var workingDir = ParentShellPageInstance.ShellViewModel.WorkingDirectory ?? string.Empty; + var workingDir = shellViewModel.WorkingDirectory ?? string.Empty; - ParentShellPageInstance.InstanceViewModel.IsPageTypeRecycleBin = workingDir.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal); - ParentShellPageInstance.InstanceViewModel.IsPageTypeMtpDevice = workingDir.StartsWith("\\\\?\\", StringComparison.Ordinal); - ParentShellPageInstance.InstanceViewModel.IsPageTypeFtp = FtpHelpers.IsFtpPath(workingDir); - ParentShellPageInstance.InstanceViewModel.IsPageTypeZipFolder = ZipStorageFolder.IsZipPath(workingDir); - ParentShellPageInstance.InstanceViewModel.IsPageTypeLibrary = LibraryManager.IsLibraryPath(workingDir); - ParentShellPageInstance.InstanceViewModel.IsPageTypeSearchResults = true; - ParentShellPageInstance.InstanceViewModel.IsPageTypeReleaseNotes = false; - ParentShellPageInstance.InstanceViewModel.IsPageTypeSettings = false; + parentShellPage.InstanceViewModel.IsPageTypeRecycleBin = workingDir.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal); + parentShellPage.InstanceViewModel.IsPageTypeMtpDevice = workingDir.StartsWith("\\\\?\\", StringComparison.Ordinal); + parentShellPage.InstanceViewModel.IsPageTypeFtp = FtpHelpers.IsFtpPath(workingDir); + parentShellPage.InstanceViewModel.IsPageTypeZipFolder = ZipStorageFolder.IsZipPath(workingDir); + parentShellPage.InstanceViewModel.IsPageTypeLibrary = LibraryManager.IsLibraryPath(workingDir); + parentShellPage.InstanceViewModel.IsPageTypeSearchResults = true; + parentShellPage.InstanceViewModel.IsPageTypeReleaseNotes = false; + parentShellPage.InstanceViewModel.IsPageTypeSettings = false; - if (!navigationArguments.IsLayoutSwitch) + if (!args.IsLayoutSwitch) { - var displayName = App.LibraryManager.TryGetLibrary(navigationArguments.SearchPathParam, out var lib) ? lib.Text : navigationArguments.SearchPathParam; - await ParentShellPageInstance.UpdatePathUIToWorkingDirectoryAsync(null, string.Format(Strings.SearchPagePathBoxOverrideText.GetLocalizedResource(), navigationArguments.SearchQuery, displayName)); + var displayName = App.LibraryManager.TryGetLibrary(searchPath, out var lib) ? lib.Text : searchPath; + await parentShellPage.UpdatePathUIToWorkingDirectoryAsync(null, string.Format(Strings.SearchPagePathBoxOverrideText.GetLocalizedResource(), args.SearchQuery, displayName)); var searchInstance = new Utils.Storage.FolderSearch { - Query = navigationArguments.SearchQuery, - Folder = navigationArguments.SearchPathParam, + Query = args.SearchQuery ?? string.Empty, + Folder = searchPath, }; - _ = ParentShellPageInstance.ShellViewModel.SearchAsync(searchInstance); + _ = shellViewModel.SearchAsync(searchInstance); } } // Show controls that were hidden on the home page - ParentShellPageInstance.InstanceViewModel.IsPageTypeNotHome = true; - ParentShellPageInstance.ShellViewModel.UpdateGroupOptions(); + parentShellPage.InstanceViewModel.IsPageTypeNotHome = true; + shellViewModel.UpdateGroupOptions(); UpdateCollectionViewSource(); - FolderSettings.IsLayoutModeChanging = false; + folderSettings.IsLayoutModeChanging = false; SetSelectedItemsOnNavigation(); @@ -549,9 +561,12 @@ public async void SetSelectedItemsOnNavigation() itemsToSelect is not null && itemsToSelect.Any()) { + if (ParentShellPageInstance?.ShellViewModel is not { } shellViewModel) + return; + List listedItemsToSelect = [ - .. ParentShellPageInstance!.ShellViewModel.FilesAndFolders.ToList().Where((li) => itemsToSelect.Contains(li.ItemNameRaw)), + .. shellViewModel.FilesAndFolders.ToList().Where((li) => itemsToSelect.Contains(li.ItemNameRaw)), ]; ItemManipulationModel.SetSelectedItems(listedItemsToSelect); @@ -563,10 +578,12 @@ itemsToSelect is not null && if (!UIHelpers.IsTextInputFocused(XamlRoot)) ItemManipulationModel.FocusSelectedItems(); } - else if (navigationArguments is not null && ParentShellPageInstance!.InstanceViewModel.FolderSettings.LayoutMode is not FolderLayoutModes.ColumnView) + else if (navigationArguments is not null && + ParentShellPageInstance is { } parentShellPage && + parentShellPage.InstanceViewModel.FolderSettings.LayoutMode is not FolderLayoutModes.ColumnView) { if (!UIHelpers.IsTextInputFocused(XamlRoot)) - ParentShellPageInstance!.PaneHolder.FocusActivePane(); + parentShellPage.PaneHolder?.FocusActivePane(); } } catch (Exception) { } @@ -589,16 +606,19 @@ private async void FolderSettings_GroupByDateUnitPreferenceUpdated(object? sende private async Task GroupPreferenceUpdatedAsync() { + if (ParentShellPageInstance?.ShellViewModel is not { } shellViewModel) + return; + // Two or more of these running at the same time will cause a crash, so cancel the previous one before beginning groupingCancellationToken?.Cancel(); groupingCancellationToken = new CancellationTokenSource(); var token = groupingCancellationToken.Token; - await ParentShellPageInstance!.ShellViewModel.GroupOptionsUpdatedAsync(token); + await shellViewModel.GroupOptionsUpdatedAsync(token); UpdateCollectionViewSource(); - await ParentShellPageInstance.ShellViewModel.ReloadItemGroupHeaderImagesAsync(); + await shellViewModel.ReloadItemGroupHeaderImagesAsync(); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) @@ -607,16 +627,19 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) // Remove item jumping handler CharacterReceived -= Page_CharacterReceived; - FolderSettings!.LayoutModeChangeRequested -= BaseFolderSettings_LayoutModeChangeRequested; - FolderSettings.GroupOptionPreferenceUpdated -= FolderSettings_GroupOptionPreferenceUpdated; - FolderSettings.GroupDirectionPreferenceUpdated -= FolderSettings_GroupDirectionPreferenceUpdated; - FolderSettings.GroupByDateUnitPreferenceUpdated -= FolderSettings_GroupByDateUnitPreferenceUpdated; + if (FolderSettings is { } folderSettings) + { + folderSettings.LayoutModeChangeRequested -= BaseFolderSettings_LayoutModeChangeRequested; + folderSettings.GroupOptionPreferenceUpdated -= FolderSettings_GroupOptionPreferenceUpdated; + folderSettings.GroupDirectionPreferenceUpdated -= FolderSettings_GroupDirectionPreferenceUpdated; + folderSettings.GroupByDateUnitPreferenceUpdated -= FolderSettings_GroupByDateUnitPreferenceUpdated; + } ItemContextMenuFlyout.Opening -= ItemContextFlyout_Opening; BaseContextMenuFlyout.Opening -= BaseContextFlyout_Opening; var parameter = e.Parameter as NavigationArguments; if (parameter is not null && !parameter.IsLayoutSwitch) - ParentShellPageInstance!.ShellViewModel.CancelLoadAndClearFiles(); + ParentShellPageInstance?.ShellViewModel?.CancelLoadAndClearFiles(); } private async void ItemContextFlyout_Opening(object? sender, object e) @@ -625,10 +648,15 @@ private async void ItemContextFlyout_Opening(object? sender, object e) try { - if (!ParentShellPageInstance!.IsCurrentInstance || !ParentShellPageInstance.IsCurrentPane) + if (ParentShellPageInstance is not { ShellViewModel: { } shellViewModel } parentShellPage || + CommandsViewModel is not { } commandsViewModel) + return; + + var instanceViewModel = parentShellPage.InstanceViewModel; + if (!parentShellPage.IsCurrentInstance || !parentShellPage.IsCurrentPane) { // Wait until the pane and column become current - await Task.WhenAny(ParentShellPageInstance.WhenIsCurrent(), Task.Delay(500)); + await Task.WhenAny(parentShellPage.WhenIsCurrent(), Task.Delay(500)); // Wait a little longer to ensure the page context is updated await Task.Delay(10); } @@ -645,10 +673,10 @@ private async void ItemContextFlyout_Opening(object? sender, object e) shellContextMenuItemCancellationToken?.Cancel(); shellContextMenuItemCancellationToken = new CancellationTokenSource(); - SelectedItemsPropertiesViewModel.CheckAllFileExtensions(SelectedItems!.Select(selectedItem => selectedItem?.FileExtension).ToList()!); + SelectedItemsPropertiesViewModel.CheckAllFileExtensions(SelectedItems.Select(selectedItem => selectedItem.FileExtension).ToList()); shiftPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down); - var items = ContentPageContextFlyoutFactory.GetItemContextCommandsWithoutShellItems(currentInstanceViewModel: InstanceViewModel!, selectedItems: SelectedItems!, selectedItemsPropertiesViewModel: SelectedItemsPropertiesViewModel, commandsViewModel: CommandsViewModel!, shiftPressed: shiftPressed, itemViewModel: null); + var items = ContentPageContextFlyoutFactory.GetItemContextCommandsWithoutShellItems(currentInstanceViewModel: instanceViewModel, selectedItems: SelectedItems, selectedItemsPropertiesViewModel: SelectedItemsPropertiesViewModel, commandsViewModel: commandsViewModel, shiftPressed: shiftPressed, itemViewModel: null); ItemContextMenuFlyout.PrimaryCommands.Clear(); ItemContextMenuFlyout.SecondaryCommands.Clear(); @@ -659,12 +687,12 @@ private async void ItemContextFlyout_Opening(object? sender, object e) secondaryElements.OfType().ForEach(i => i.MinWidth = Constants.UI.ContextMenuItemsMaxWidth); // Set menu min width secondaryElements.ForEach(ItemContextMenuFlyout.SecondaryCommands.Add); - if (InstanceViewModel!.CanTagFilesInPage) + if (instanceViewModel.CanTagFilesInPage) AddNewFileTagsToMenu(ItemContextMenuFlyout); - if (!InstanceViewModel.IsPageTypeZipFolder && !InstanceViewModel.IsPageTypeFtp) + if (!instanceViewModel.IsPageTypeZipFolder && !instanceViewModel.IsPageTypeFtp) { - var shellMenuItems = await ContentPageContextFlyoutFactory.GetItemContextShellCommandsAsync(workingDir: ParentShellPageInstance.ShellViewModel.WorkingDirectory, selectedItems: SelectedItems!, shiftPressed: shiftPressed, showOpenMenu: false, shellContextMenuItemCancellationToken.Token); + var shellMenuItems = await ContentPageContextFlyoutFactory.GetItemContextShellCommandsAsync(workingDir: shellViewModel.WorkingDirectory, selectedItems: SelectedItems, shiftPressed: shiftPressed, showOpenMenu: false, shellContextMenuItemCancellationToken.Token); if (shellMenuItems.Any()) await AddShellMenuItemsAsync(shellMenuItems, ItemContextMenuFlyout, shiftPressed); else @@ -688,10 +716,15 @@ private async void BaseContextFlyout_Opening(object? sender, object e) try { - if (!ParentShellPageInstance!.IsCurrentInstance || !ParentShellPageInstance.IsCurrentPane) + if (ParentShellPageInstance is not { ShellViewModel: { } shellViewModel } parentShellPage || + CommandsViewModel is not { } commandsViewModel) + return; + + var instanceViewModel = parentShellPage.InstanceViewModel; + if (!parentShellPage.IsCurrentInstance || !parentShellPage.IsCurrentPane) { // Wait until the pane and column become current - await Task.WhenAny(ParentShellPageInstance.WhenIsCurrent(), Task.Delay(500)); + await Task.WhenAny(parentShellPage.WhenIsCurrent(), Task.Delay(500)); // Wait a little longer to ensure the page context is updated await Task.Delay(10); } @@ -706,7 +739,8 @@ private async void BaseContextFlyout_Opening(object? sender, object e) shellContextMenuItemCancellationToken = new CancellationTokenSource(); shiftPressed = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down); - var items = ContentPageContextFlyoutFactory.GetItemContextCommandsWithoutShellItems(currentInstanceViewModel: InstanceViewModel!, selectedItems: [ParentShellPageInstance!.ShellViewModel.CurrentFolder], commandsViewModel: CommandsViewModel!, shiftPressed: shiftPressed, itemViewModel: ParentShellPageInstance!.ShellViewModel, selectedItemsPropertiesViewModel: null); + List contextItems = shellViewModel.CurrentFolder is { } currentFolder ? [currentFolder] : []; + var items = ContentPageContextFlyoutFactory.GetItemContextCommandsWithoutShellItems(currentInstanceViewModel: instanceViewModel, selectedItems: contextItems, commandsViewModel: commandsViewModel, shiftPressed: shiftPressed, itemViewModel: shellViewModel, selectedItemsPropertiesViewModel: null); BaseContextMenuFlyout.PrimaryCommands.Clear(); BaseContextMenuFlyout.SecondaryCommands.Clear(); @@ -721,9 +755,9 @@ private async void BaseContextFlyout_Opening(object? sender, object e) secondaryElements.OfType().ForEach(i => i.MinWidth = Constants.UI.ContextMenuItemsMaxWidth); secondaryElements.ForEach(i => BaseContextMenuFlyout.SecondaryCommands.Add(i)); - if (!InstanceViewModel!.IsPageTypeSearchResults && !InstanceViewModel.IsPageTypeZipFolder && !InstanceViewModel.IsPageTypeFtp) + if (!instanceViewModel.IsPageTypeSearchResults && !instanceViewModel.IsPageTypeZipFolder && !instanceViewModel.IsPageTypeFtp) { - var shellMenuItems = await ContentPageContextFlyoutFactory.GetItemContextShellCommandsAsync(workingDir: ParentShellPageInstance.ShellViewModel.WorkingDirectory, selectedItems: [], shiftPressed: shiftPressed, showOpenMenu: false, shellContextMenuItemCancellationToken.Token); + var shellMenuItems = await ContentPageContextFlyoutFactory.GetItemContextShellCommandsAsync(workingDir: shellViewModel.WorkingDirectory, selectedItems: [], shiftPressed: shiftPressed, showOpenMenu: false, shellContextMenuItemCancellationToken.Token); if (shellMenuItems.Any()) await AddShellMenuItemsAsync(shellMenuItems, BaseContextMenuFlyout, shiftPressed); else @@ -815,8 +849,8 @@ private void AddNewFileTagsToMenu(CommandBarFlyout contextMenu) async void RequireTagGroupsUpdate(object? sender, EventArgs e) { - if (ParentShellPageInstance is not null) - await ParentShellPageInstance.ShellViewModel.RefreshTagGroups(); + if (ParentShellPageInstance?.ShellViewModel is { } shellViewModel) + await shellViewModel.RefreshTagGroups(); } void HandleClosed(object? sender, object e) @@ -841,11 +875,12 @@ private async Task AddShellMenuItemsAsync(List s || x.ItemType != ContextMenuFlyoutItemType.Separator).ToList(); var subMenuLoadTasks = mainShellMenuItems.Concat(overflowShellMenuItems) - .Where(x => x.LoadSubMenuAction is not null) - .Select(x => x.LoadSubMenuAction()); + .Select(x => x.LoadSubMenuAction) + .WhereNotNull() + .Select(loadSubMenu => loadSubMenu()); await Task.WhenAll(subMenuLoadTasks); - var overflowItems = ContextFlyoutModelToElementHelper.GetMenuFlyoutItemsFromModel(overflowShellMenuItems); + var overflowItems = ContextFlyoutModelToElementHelper.GetMenuFlyoutItemsFromModel(overflowShellMenuItems) ?? []; var mainItems = ContextFlyoutModelToElementHelper.GetAppBarButtonsFromModelIgnorePrimary(mainShellMenuItems); var openedPopups = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetOpenPopups(MainWindow.Instance); @@ -989,7 +1024,10 @@ private async Task AddShellMenuItemsAsync(List s var mainSubMenuTasks = mainItemsWithSubMenu.Select(async item => { - await item.LoadSubMenuAction(); + if (item.LoadSubMenuAction is not { } loadSubMenu) + return; + + await loadSubMenu(); ShellContextFlyoutFactory.AddItemsToMainMenu(mainItems, item); }); @@ -998,7 +1036,10 @@ private async Task AddShellMenuItemsAsync(List s var overflowSubMenuTasks = overflowItemsWithSubMenu.Select(async item => { - await item.LoadSubMenuAction(); + if (item.LoadSubMenuAction is not { } loadSubMenu) + return; + + await loadSubMenu(); ShellContextFlyoutFactory.AddItemsToOverflowMenu(overflowItem, item); }); @@ -1011,8 +1052,9 @@ private async Task AddShellMenuItemsAsync(List s // Close main menu when clicking on subitems (#5508) if ((item as AppBarButton)?.Flyout as MenuFlyout is MenuFlyout flyout) { - Action> clickAction = null!; - clickAction = (items) => + AddClickHandlers(flyout.Items); + + void AddClickHandlers(IList items) { items.OfType().ForEach(i => { @@ -1020,11 +1062,9 @@ private async Task AddShellMenuItemsAsync(List s }); items.OfType().ForEach(i => { - clickAction(i.Items); + AddClickHandlers(i.Items); }); - }; - - clickAction(flyout.Items); + } } }); @@ -1044,7 +1084,7 @@ private void RemoveOverflow(CommandBarFlyout contextMenuFlyout) protected virtual void Page_CharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs args) { - if (ParentShellPageInstance!.IsCurrentInstance) + if (ParentShellPageInstance?.IsCurrentInstance ?? false) { char letter = args.Character; JumpString += letter.ToString().ToLowerInvariant(); @@ -1055,19 +1095,29 @@ protected virtual void FileList_DragItemsStarting(object sender, DragItemsStarti { try { + if (FolderSettings is not { } folderSettings || + InstanceViewModel is not { } instanceViewModel) + { + e.Cancel = true; + return; + } + var itemList = e.Items.OfType().ToList(); var firstItem = itemList.FirstOrDefault(); - var sortedItems = SortingHelper.OrderFileList(itemList, FolderSettings.DirectorySortOption, FolderSettings.DirectorySortDirection, FolderSettings.SortDirectoriesAlongsideFiles, FolderSettings.SortFilesFirst).ToList(); + var sortedItems = SortingHelper.OrderFileList(itemList, folderSettings.DirectorySortOption, folderSettings.DirectorySortDirection, folderSettings.SortDirectoriesAlongsideFiles, folderSettings.SortFilesFirst).ToList(); var orderedItems = sortedItems.SkipWhile(x => x != firstItem).Concat(sortedItems.TakeWhile(x => x != firstItem)).ToList(); var shellItemList = SafetyExtensions.IgnoreExceptions(() => orderedItems.Select(x => new VanaraWindowsShell.ShellItem(x.ItemPath)).ToArray()); - if (shellItemList?[0].FileSystemPath is not null && !InstanceViewModel.IsPageTypeSearchResults) + if (shellItemList is { Length: > 0 } && + shellItemList[0].FileSystemPath is not null && + shellItemList[0].Parent is { } parentShellItem && + !instanceViewModel.IsPageTypeSearchResults) { - var iddo = shellItemList[0].Parent.GetChildrenUIObjects(HWND.NULL, shellItemList); + var iddo = parentShellItem.GetChildrenUIObjects(HWND.NULL, shellItemList); shellItemList.ForEach(x => x.Dispose()); var format = System.Windows.Forms.DataFormats.GetFormat("Shell IDList Array"); - if (iddo.TryGetData((uint)format.Id, out var data)) + if (iddo.TryGetData((uint)format.Id, out var data) && data is not null) { var mem = new MemoryStream(data).AsRandomAccessStream(); e.Data.SetData(format.Name, mem); @@ -1274,7 +1324,7 @@ private void RefreshItem(SelectorItem container, object item, bool inRecycleQueu if (inRecycleQueue) { UpdateItemToolTip(container, null); - ParentShellPageInstance!.ShellViewModel.CancelExtendedPropertiesLoadingForItem(listedItem); + ParentShellPageInstance?.ShellViewModel?.CancelExtendedPropertiesLoadingForItem(listedItem); } else { @@ -1289,9 +1339,12 @@ private void RefreshItem(SelectorItem container, object item, bool inRecycleQueu uint callbackPhase = 3; args.RegisterUpdateCallback(callbackPhase, async (s, c) => { - await ParentShellPageInstance!.ShellViewModel.LoadExtendedItemPropertiesAsync(listedItem); - if (ParentShellPageInstance.ShellViewModel.EnabledGitProperties is not GitProperties.None && listedItem is IGitItem gitItem) - await ParentShellPageInstance.ShellViewModel.LoadGitPropertiesAsync(gitItem); + if (ParentShellPageInstance?.ShellViewModel is not { } shellViewModel) + return; + + await shellViewModel.LoadExtendedItemPropertiesAsync(listedItem); + if (shellViewModel.EnabledGitProperties is not GitProperties.None && listedItem is IGitItem gitItem) + await shellViewModel.LoadGitPropertiesAsync(gitItem); }); } } @@ -1493,15 +1546,15 @@ protected virtual void ItemsLayout_Drop(object sender, DragEventArgs e) private void UpdateCollectionViewSource() { - if (ParentShellPageInstance is null) + if (ParentShellPageInstance?.ShellViewModel is not { } shellViewModel) return; - if (ParentShellPageInstance.ShellViewModel.FilesAndFolders.IsGrouped) + if (shellViewModel.FilesAndFolders.IsGrouped) { var newSource = new CollectionViewSource() { IsSourceGrouped = true, - Source = ParentShellPageInstance.ShellViewModel.FilesAndFolders.GroupedCollection + Source = shellViewModel.FilesAndFolders.GroupedCollection }; CollectionViewSource = newSource; } @@ -1512,7 +1565,7 @@ private void UpdateCollectionViewSource() var newSource = new CollectionViewSource() { IsSourceGrouped = false, - Source = ParentShellPageInstance.ShellViewModel.FilesAndFolders + Source = shellViewModel.FilesAndFolders }; CollectionViewSource = newSource; } @@ -1579,7 +1632,7 @@ virtual public void StartRenameItem() { } - public void CheckRenameDoubleClick(object clickedItem) + public void CheckRenameDoubleClick(object? clickedItem) { if (clickedItem is ListedItem item) { diff --git a/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs b/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs index 223f04aa11f1..c06623f2f928 100644 --- a/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs +++ b/src/Files.App/Views/Layouts/ColumnLayoutPage.xaml.cs @@ -102,7 +102,7 @@ private static void SetRowStyle(ListViewItem? lvi, Style? style) // Methods - private void OnItemLoadStatusChanged(object sender, ItemLoadStatusChangedEventArgs args) + private void OnItemLoadStatusChanged(object? sender, ItemLoadStatusChangedEventArgs args) { if (args.Status is ItemLoadStatusChangedEventArgs.ItemLoadStatus.Complete) { @@ -115,7 +115,8 @@ private void OnItemLoadStatusChanged(object sender, ItemLoadStatusChangedEventAr private void FileList_Loaded(object sender, RoutedEventArgs e) { ContentScroller = FileList.FindDescendant(x => x.Name == "ScrollViewer"); - ParentShellPageInstance.ShellViewModel.ItemLoadStatusChanged += OnItemLoadStatusChanged; + if (ParentShellPageInstance?.ShellViewModel is { } shellViewModel) + shellViewModel.ItemLoadStatusChanged += OnItemLoadStatusChanged; } private void ColumnViewBase_GotFocus(object sender, RoutedEventArgs e) @@ -208,7 +209,8 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { base.OnNavigatingFrom(e); UserSettingsService.LayoutSettingsService.PropertyChanged -= LayoutSettingsService_PropertyChanged; - ParentShellPageInstance.ShellViewModel.ItemLoadStatusChanged -= OnItemLoadStatusChanged; + if (ParentShellPageInstance?.ShellViewModel is { } shellViewModel) + shellViewModel.ItemLoadStatusChanged -= OnItemLoadStatusChanged; } private void LayoutSettingsService_PropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -239,16 +241,16 @@ private void LayoutSettingsService_PropertyChanged(object? sender, PropertyChang private async Task ReloadItemIconsAsync() { - if (ParentShellPageInstance is null) + if (ParentShellPageInstance?.ShellViewModel is not { } shellViewModel) return; - ParentShellPageInstance.ShellViewModel.CancelExtendedPropertiesLoading(); - var filesAndFolders = ParentShellPageInstance.ShellViewModel.FilesAndFolders.ToList(); + shellViewModel.CancelExtendedPropertiesLoading(); + var filesAndFolders = shellViewModel.FilesAndFolders.ToList(); foreach (ListedItem listedItem in filesAndFolders) { listedItem.ItemPropertiesInitialized = false; if (FileList.ContainerFromItem(listedItem) is not null) - await ParentShellPageInstance.ShellViewModel.LoadExtendedItemPropertiesAsync(listedItem); + await shellViewModel.LoadExtendedItemPropertiesAsync(listedItem); } } @@ -336,6 +338,8 @@ public override void Dispose() protected override void OnSelectionChanged(SelectionChangedEventArgs e) { + var shellViewModel = ParentShellPageInstance?.ShellViewModel; + if (e.AddedItems.Count > 0) columnsOwner?.HandleSelectionChange(this); @@ -345,8 +349,8 @@ protected override void OnSelectionChanged(SelectionChangedEventArgs e) } else if (SelectedItems?.Count > 1 || SelectedItem?.PrimaryItemAttribute is StorageItemTypes.File - || openedFolderPresenter != null && ParentShellPageInstance != null - && !ParentShellPageInstance.ShellViewModel.FilesAndFolders.ToList().Contains(FileList.ItemFromContainer(openedFolderPresenter)) + || openedFolderPresenter != null && shellViewModel is not null + && !shellViewModel.FilesAndFolders.ToList().Contains(FileList.ItemFromContainer(openedFolderPresenter)) && !isDraggingSelectionRectangle) // Skip closing if dragging since nothing should be open { CloseFolder(); @@ -410,7 +414,9 @@ private void StorageArchiveService_CompressionCompleted(object? sender, string a private void CloseFolder() { - var currentBladeIndex = (ParentShellPageInstance is ColumnShellPage associatedColumnShellPage) ? associatedColumnShellPage.ColumnParams.Column : 0; + var currentBladeIndex = ParentShellPageInstance is ColumnShellPage associatedColumnShellPage + ? associatedColumnShellPage.ColumnParams?.Column ?? 0 + : 0; this.FindAscendant()?.DismissOtherBlades(currentBladeIndex); ClearOpenedFolderSelectionIndicator(); } @@ -492,7 +498,9 @@ ParentShellPageInstance is null || } else if (e.Key == VirtualKey.Left) // Left arrow: select parent folder (previous column) { - var currentBladeIndex = (ParentShellPageInstance is ColumnShellPage associatedColumnShellPage) ? associatedColumnShellPage.ColumnParams.Column : 0; + var currentBladeIndex = ParentShellPageInstance is ColumnShellPage associatedColumnShellPage + ? associatedColumnShellPage.ColumnParams?.Column ?? 0 + : 0; this.FindAscendant()?.MoveFocusToPreviousBlade(currentBladeIndex); FileList.SelectedItem = null; ClearOpenedFolderSelectionIndicator(); @@ -500,7 +508,9 @@ ParentShellPageInstance is null || } else if (e.Key == VirtualKey.Right) // Right arrow: switch focus to next column { - var currentBladeIndex = (ParentShellPageInstance is ColumnShellPage associatedColumnShellPage) ? associatedColumnShellPage.ColumnParams.Column : 0; + var currentBladeIndex = ParentShellPageInstance is ColumnShellPage associatedColumnShellPage + ? associatedColumnShellPage.ColumnParams?.Column ?? 0 + : 0; this.FindAscendant()?.MoveFocusToNextBlade(currentBladeIndex + 1); e.Handled = true; } diff --git a/src/Files.App/Views/Layouts/ColumnsLayoutPage.xaml.cs b/src/Files.App/Views/Layouts/ColumnsLayoutPage.xaml.cs index 9cdc5a9f813f..1db4ee533232 100644 --- a/src/Files.App/Views/Layouts/ColumnsLayoutPage.xaml.cs +++ b/src/Files.App/Views/Layouts/ColumnsLayoutPage.xaml.cs @@ -57,17 +57,21 @@ protected override bool CanGetItemFromElement(object element) private void ColumnViewBase_ItemInvoked(object? sender, EventArgs e) { - var column = sender as ColumnParam; - if (column?.ListView.FindAscendant() != this) + if (sender is not ColumnParam { ListView: { } listView } column || + listView.FindAscendant() != this) return; - var nextBladeIndex = ColumnHost.ActiveBlades.IndexOf(column.ListView.FindAscendant()) + 1; + var currentBlade = listView.FindAscendant(); + if (currentBlade is null) + return; + + var nextBladeIndex = ColumnHost.ActiveBlades.IndexOf(currentBlade) + 1; var nextBlade = ColumnHost.ActiveBlades.ToList().ElementAtOrDefault(nextBladeIndex); var arePathsDifferent = ((nextBlade?.Content as Frame)?.Content as IShellPage)?.ShellViewModel?.WorkingDirectory != column.NavPathParam; if (nextBlade is null || arePathsDifferent) { - DismissOtherBlades(column.ListView); + DismissOtherBlades(listView); var (frame, newblade) = CreateAndAddNewBlade(); @@ -76,8 +80,10 @@ private void ColumnViewBase_ItemInvoked(object? sender, EventArgs e) Column = ColumnHost.ActiveBlades.IndexOf(newblade), NavPathParam = column.NavPathParam }); - navigationArguments.NavPathParam = column.NavPathParam; - ParentShellPageInstance.TabBarItemParameter.NavigationParameter = column.NavPathParam; + if (navigationArguments is { } args) + args.NavPathParam = column.NavPathParam; + if (ParentShellPageInstance?.TabBarItemParameter is { } tabArguments) + tabArguments.NavigationParameter = column.NavPathParam; } } @@ -93,16 +99,23 @@ public void SetWidth(int index) ColumnHost.ScrollToEnd(); } - private void ContentChanged(IShellPage p) + private void ContentChanged(IShellPage? shellPage) { - (ParentShellPageInstance as ModernShellPage)?.RaiseContentChanged(p, p.TabBarItemParameter); + if (ParentShellPageInstance is ModernShellPage parent && + shellPage?.TabBarItemParameter is { } tabArguments) + { + parent.RaiseContentChanged(shellPage, tabArguments); + } } protected override void OnNavigatedTo(NavigationEventArgs eventArgs) { base.OnNavigatedTo(eventArgs); - var path = navigationArguments.NavPathParam; + if (navigationArguments is not { NavPathParam: { } initialPath } args) + return; + + var path = initialPath; var pathRoot = GetPathRoot(path); var pathStack = new Stack(); @@ -120,18 +133,18 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs) } } - OwnerPath = navigationArguments.NavPathParam; + OwnerPath = args.NavPathParam; FocusIndex = pathStack.Count; MainPageFrame.Navigated += Frame_Navigated; MainPageFrame.Navigate(typeof(ColumnShellPage), new ColumnParam { Column = 0, - IsSearchResultPage = navigationArguments.IsSearchResultPage, - SearchQuery = navigationArguments.SearchQuery, - SearchPathParam = navigationArguments.SearchPathParam, + IsSearchResultPage = args.IsSearchResultPage, + SearchQuery = args.SearchQuery, + SearchPathParam = args.SearchPathParam, NavPathParam = path, - SelectItems = path == navigationArguments.NavPathParam ? navigationArguments.SelectItems : null + SelectItems = path == args.NavPathParam ? args.SelectItems : null }); var index = 0; @@ -140,17 +153,19 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs) var (frame, _) = CreateAndAddNewBlade(); frame.Navigate(typeof(ColumnShellPage), new ColumnParam - { - Column = ++index, - NavPathParam = path, - SelectItems = path == navigationArguments.NavPathParam ? navigationArguments.SelectItems : null - }); + { + Column = ++index, + NavPathParam = path, + SelectItems = path == args.NavPathParam ? args.SelectItems : null + }); } } protected override void InitializeCommandsViewModel() { - CommandsViewModel = new BaseLayoutViewModel(ParentShellPageInstance, ItemManipulationModel); + var parentShellPage = ParentShellPageInstance + ?? throw new InvalidOperationException("The parent shell page must be initialized before commands."); + CommandsViewModel = new BaseLayoutViewModel(parentShellPage, ItemManipulationModel); } protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) @@ -196,9 +211,10 @@ private void DismissOtherBlades(ListView listView) DismissOtherBlades(listView.FindAscendant()); } - private void DismissOtherBlades(BladeItem blade) + private void DismissOtherBlades(BladeItem? blade) { - DismissOtherBlades(ColumnHost.ActiveBlades.IndexOf(blade)); + if (blade is not null) + DismissOtherBlades(ColumnHost.ActiveBlades.IndexOf(blade)); } public void DismissOtherBlades(int index) @@ -219,8 +235,10 @@ public void DismissOtherBlades(int index) columnLayout.KeyUp -= ColumnViewBase_KeyUp; } - (frame?.Content as UIElement).GotFocus -= ColumnViewBrowser_GotFocus; - (frame?.Content as ColumnShellPage).ContentChanged -= ColumnViewBrowser_ContentChanged; + if (frame?.Content is UIElement element) + element.GotFocus -= ColumnViewBrowser_GotFocus; + if (frame?.Content is ColumnShellPage shellPage) + shellPage.ContentChanged -= ColumnViewBrowser_ContentChanged; // Dispose content AFTER unsubscribing event handlers if (frame?.Content is IDisposable disposableContent) @@ -230,10 +248,12 @@ public void DismissOtherBlades(int index) ColumnHost.ActiveBlades.RemoveAt(index + 1); } - if ((ColumnHost.ActiveBlades[index].Content as Frame)?.Content is ColumnShellPage s) + if ((ColumnHost.ActiveBlades[index].Content as Frame)?.Content is ColumnShellPage { ShellViewModel: { } shellViewModel }) { - navigationArguments.NavPathParam = s.ShellViewModel.WorkingDirectory; - ParentShellPageInstance.TabBarItemParameter.NavigationParameter = s.ShellViewModel.WorkingDirectory; + if (navigationArguments is { } args) + args.NavPathParam = shellViewModel.WorkingDirectory; + if (ParentShellPageInstance?.TabBarItemParameter is { } tabArguments) + tabArguments.NavigationParameter = shellViewModel.WorkingDirectory; } }); } @@ -243,10 +263,14 @@ public void DismissOtherBlades(int index) private void Frame_Navigated(object sender, NavigationEventArgs e) { - var f = sender as Frame; - f.Navigated -= Frame_Navigated; - (f.Content as IShellPage).ContentChanged += ColumnViewBrowser_ContentChanged; - (f.Content as UIElement).GotFocus += ColumnViewBrowser_GotFocus; + if (sender is not Frame frame) + return; + + frame.Navigated -= Frame_Navigated; + if (frame.Content is IShellPage shellPage) + shellPage.ContentChanged += ColumnViewBrowser_ContentChanged; + if (frame.Content is UIElement element) + element.GotFocus += ColumnViewBrowser_GotFocus; } private void ColumnViewBrowser_GotFocus(object sender, RoutedEventArgs e) @@ -261,7 +285,8 @@ private void ColumnViewBrowser_GotFocus(object sender, RoutedEventArgs e) ColumnHost.ActiveBlades.ToList().ForEach(x => { var shellPage = (x.Content as Frame)?.Content as ColumnShellPage; - shellPage.IsCurrentInstance = false; + if (shellPage is not null) + shellPage.IsCurrentInstance = false; }); } @@ -269,7 +294,7 @@ private void ColumnViewBrowser_GotFocus(object sender, RoutedEventArgs e) ContentChanged(shPage); } - private void ColumnViewBrowser_ContentChanged(object sender, TabBarItemParameter e) + private void ColumnViewBrowser_ContentChanged(object? sender, TabBarItemParameter e) { var c = sender as IShellPage; var columnView = c?.SlimContentPage as ColumnLayoutPage; @@ -309,7 +334,7 @@ public void NavigateUp() DismissOtherBlades(ColumnHost.ActiveBlades[ColumnHost.ActiveBlades.Count - 2]); else { - var workingDirectory = ((ColumnHost.ActiveBlades?.ToList().FirstOrDefault()?.Content as Frame)?.Content as ColumnShellPage)?.ShellViewModel.WorkingDirectory; + var workingDirectory = ((ColumnHost.ActiveBlades?.ToList().FirstOrDefault()?.Content as Frame)?.Content as ColumnShellPage)?.ShellViewModel?.WorkingDirectory; if (workingDirectory is null || string.Equals(workingDirectory, GetPathRoot(workingDirectory), StringComparison.OrdinalIgnoreCase)) ParentShellPageInstance?.NavigateHome(); else @@ -383,7 +408,7 @@ public void MoveFocusToNextBlade(int currentBladeIndex) return activeBladePage.SlimContentPage as ColumnLayoutPage; } - public void SetSelectedPathOrNavigate(string navigationPath, Type sourcePageType, NavigationArguments navArgs = null) + public void SetSelectedPathOrNavigate(string navigationPath, Type sourcePageType, NavigationArguments? navArgs = null) { if (navArgs is not null && navArgs.IsSearchResultPage) { @@ -393,8 +418,8 @@ public void SetSelectedPathOrNavigate(string navigationPath, Type sourcePageType } var destPath = navArgs is not null ? navArgs.NavPathParam : navigationPath; - var columnPath = ((ColumnHost.ActiveBlades.ToList().LastOrDefault()?.Content as Frame)?.Content as ColumnShellPage)?.ShellViewModel.WorkingDirectory; - var columnFirstPath = ((ColumnHost.ActiveBlades.ToList().FirstOrDefault()?.Content as Frame)?.Content as ColumnShellPage)?.ShellViewModel.WorkingDirectory; + var columnPath = ((ColumnHost.ActiveBlades.ToList().LastOrDefault()?.Content as Frame)?.Content as ColumnShellPage)?.ShellViewModel?.WorkingDirectory; + var columnFirstPath = ((ColumnHost.ActiveBlades.ToList().FirstOrDefault()?.Content as Frame)?.Content as ColumnShellPage)?.ShellViewModel?.WorkingDirectory; if (string.IsNullOrEmpty(destPath) || string.IsNullOrEmpty(columnPath) || string.IsNullOrEmpty(columnFirstPath)) { @@ -440,8 +465,8 @@ public void SetSelectedPathOrNavigate(PathNavigationEventArgs e) { foreach (var item in ColumnHost.ActiveBlades.ToList()) { - if ((item.Content as Frame)?.Content is ColumnShellPage s && - NormalizePath(s.ShellViewModel?.WorkingDirectory) == NormalizePath(e.ItemPath)) + if ((item.Content as Frame)?.Content is ColumnShellPage { ShellViewModel.WorkingDirectory: { } workingDirectory } && + NormalizePath(workingDirectory) == NormalizePath(e.ItemPath)) { DismissOtherBlades(item); return; @@ -452,21 +477,25 @@ public void SetSelectedPathOrNavigate(PathNavigationEventArgs e) if (ParentShellPageInstance is null) return; - if (NormalizePath(ParentShellPageInstance.ShellViewModel?.WorkingDirectory) != NormalizePath(e.ItemPath)) + if (ParentShellPageInstance.ShellViewModel?.WorkingDirectory is not { } parentWorkingDirectory || + NormalizePath(parentWorkingDirectory) != NormalizePath(e.ItemPath)) ParentShellPageInstance.NavigateToPath(e.ItemPath); else DismissOtherBlades(0); } - public IShellPage ActiveColumnShellPage + public IShellPage? ActiveColumnShellPage { get { if (ColumnHost.ActiveBlades?.Count > 0) { - var shellPages = ColumnHost.ActiveBlades.ToList().Select(x => (x.Content as Frame).Content as IShellPage); + var shellPages = ColumnHost.ActiveBlades + .Select(x => (x.Content as Frame)?.Content) + .OfType() + .ToList(); var activeInstance = shellPages.SingleOrDefault(x => x.IsCurrentInstance); - return activeInstance ?? shellPages.Last(); + return activeInstance ?? shellPages.LastOrDefault() ?? ParentShellPageInstance; } return ParentShellPageInstance; @@ -475,16 +504,17 @@ public IShellPage ActiveColumnShellPage private void ColumnViewBase_ItemTapped(object? sender, EventArgs e) { - var column = sender as ColumnParam; - if (column?.ListView.FindAscendant() != this || string.IsNullOrEmpty(column.NavPathParam)) + if (sender is not ColumnParam { ListView: { } listView } column || + listView.FindAscendant() != this || + string.IsNullOrEmpty(column.NavPathParam)) return; CloseUnnecessaryColumns(column); } - private void CloseUnnecessaryColumns(ColumnParam column) + private void CloseUnnecessaryColumns(ColumnParam? column) { - if (string.IsNullOrEmpty(column.NavPathParam)) + if (string.IsNullOrEmpty(column?.NavPathParam)) return; var relativeIndex = column.Column is not 0 ? column.Column : -1; diff --git a/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs b/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs index 2c136d4e4c27..72e8154729f2 100644 --- a/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs +++ b/src/Files.App/Views/Layouts/DetailsLayoutPage.xaml.cs @@ -87,16 +87,17 @@ public DetailsLayoutPage() : base() UpdateSortOptionsCommand = new RelayCommand(x => { - if (!Enum.TryParse(x, out var val)) + if (FolderSettings is not { } folderSettings || + !Enum.TryParse(x, out var val)) return; - if (FolderSettings.DirectorySortOption == val) + if (folderSettings.DirectorySortOption == val) { - FolderSettings.DirectorySortDirection = (SortDirection)(((int)FolderSettings.DirectorySortDirection + 1) % 2); + folderSettings.DirectorySortDirection = (SortDirection)(((int)folderSettings.DirectorySortDirection + 1) % 2); } else { - FolderSettings.DirectorySortOption = val; - FolderSettings.DirectorySortDirection = SortDirection.Ascending; + folderSettings.DirectorySortOption = val; + folderSettings.DirectorySortDirection = SortDirection.Ascending; } }); } @@ -148,6 +149,13 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs) base.OnNavigatedTo(eventArgs); + var parentShellPage = ParentShellPageInstance + ?? throw new InvalidOperationException("The details layout must be associated with a shell page."); + var shellViewModel = parentShellPage.ShellViewModel + ?? throw new InvalidOperationException("The details layout requires an initialized shell view model."); + var folderSettings = FolderSettings + ?? throw new InvalidOperationException("The details layout requires folder settings."); + currentIconSize = LayoutSizeKindHelper.GetIconSize(FolderLayoutModes.DetailsView); if (FolderSettings?.ColumnsViewModel is not null) @@ -174,13 +182,13 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs) ColumnsViewModel.GitLastCommitShaColumn.Update(FolderSettings.ColumnsViewModel.GitLastCommitShaColumn); } - ParentShellPageInstance.ShellViewModel.EnabledGitProperties = GetEnabledGitProperties(ColumnsViewModel); + shellViewModel.EnabledGitProperties = GetEnabledGitProperties(ColumnsViewModel); - FolderSettings.LayoutModeChangeRequested += FolderSettings_LayoutModeChangeRequested; - FolderSettings.SortDirectionPreferenceUpdated += FolderSettings_SortDirectionPreferenceUpdated; - FolderSettings.SortOptionPreferenceUpdated += FolderSettings_SortOptionPreferenceUpdated; - ParentShellPageInstance.ShellViewModel.PageTypeUpdated += FilesystemViewModel_PageTypeUpdated; - ParentShellPageInstance.ShellViewModel.ItemLoadStatusChanged += ShellViewModel_ItemLoadStatusChanged; + folderSettings.LayoutModeChangeRequested += FolderSettings_LayoutModeChangeRequested; + folderSettings.SortDirectionPreferenceUpdated += FolderSettings_SortDirectionPreferenceUpdated; + folderSettings.SortOptionPreferenceUpdated += FolderSettings_SortOptionPreferenceUpdated; + shellViewModel.PageTypeUpdated += FilesystemViewModel_PageTypeUpdated; + shellViewModel.ItemLoadStatusChanged += ShellViewModel_ItemLoadStatusChanged; UserSettingsService.LayoutSettingsService.PropertyChanged += LayoutSettingsService_PropertyChanged; FileList.Items.VectorChanged += FileListItems_VectorChanged; @@ -204,11 +212,17 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs) protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { base.OnNavigatingFrom(e); - FolderSettings.LayoutModeChangeRequested -= FolderSettings_LayoutModeChangeRequested; - FolderSettings.SortDirectionPreferenceUpdated -= FolderSettings_SortDirectionPreferenceUpdated; - FolderSettings.SortOptionPreferenceUpdated -= FolderSettings_SortOptionPreferenceUpdated; - ParentShellPageInstance.ShellViewModel.PageTypeUpdated -= FilesystemViewModel_PageTypeUpdated; - ParentShellPageInstance.ShellViewModel.ItemLoadStatusChanged -= ShellViewModel_ItemLoadStatusChanged; + if (FolderSettings is { } folderSettings) + { + folderSettings.LayoutModeChangeRequested -= FolderSettings_LayoutModeChangeRequested; + folderSettings.SortDirectionPreferenceUpdated -= FolderSettings_SortDirectionPreferenceUpdated; + folderSettings.SortOptionPreferenceUpdated -= FolderSettings_SortOptionPreferenceUpdated; + } + if (ParentShellPageInstance?.ShellViewModel is { } shellViewModel) + { + shellViewModel.PageTypeUpdated -= FilesystemViewModel_PageTypeUpdated; + shellViewModel.ItemLoadStatusChanged -= ShellViewModel_ItemLoadStatusChanged; + } UserSettingsService.LayoutSettingsService.PropertyChanged -= LayoutSettingsService_PropertyChanged; FileList.Items.VectorChanged -= FileListItems_VectorChanged; _autoFitColumnsTimer?.Stop(); @@ -240,7 +254,7 @@ private void LayoutSettingsService_PropertyChanged(object? sender, PropertyChang else { var settings = sender as ILayoutSettingsService; - var isDefaultPath = FolderSettings?.IsPathUsingDefaultLayout(ParentShellPageInstance?.ShellViewModel.CurrentFolder?.ItemPath); + var isDefaultPath = FolderSettings?.IsPathUsingDefaultLayout(ParentShellPageInstance?.ShellViewModel?.CurrentFolder?.ItemPath); if (settings is not null && (isDefaultPath ?? true)) { switch (e.PropertyName) @@ -315,16 +329,19 @@ private void FolderSettings_SortDirectionPreferenceUpdated(object? sender, SortD private void UpdateSortIndicator() { - NameHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.Name ? FolderSettings.DirectorySortDirection : null; - TagHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.FileTag ? FolderSettings.DirectorySortDirection : null; - PathHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.Path ? FolderSettings.DirectorySortDirection : null; - OriginalPathHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.OriginalFolder ? FolderSettings.DirectorySortDirection : null; - DateDeletedHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.DateDeleted ? FolderSettings.DirectorySortDirection : null; - DateModifiedHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.DateModified ? FolderSettings.DirectorySortDirection : null; - DateCreatedHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.DateCreated ? FolderSettings.DirectorySortDirection : null; - FileTypeHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.FileType ? FolderSettings.DirectorySortDirection : null; - ItemSizeHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.Size ? FolderSettings.DirectorySortDirection : null; - SyncStatusHeader.ColumnSortOption = FolderSettings.DirectorySortOption == SortOption.SyncStatus ? FolderSettings.DirectorySortDirection : null; + if (FolderSettings is not { } folderSettings) + return; + + NameHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.Name ? folderSettings.DirectorySortDirection : null; + TagHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.FileTag ? folderSettings.DirectorySortDirection : null; + PathHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.Path ? folderSettings.DirectorySortDirection : null; + OriginalPathHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.OriginalFolder ? folderSettings.DirectorySortDirection : null; + DateDeletedHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.DateDeleted ? folderSettings.DirectorySortDirection : null; + DateModifiedHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.DateModified ? folderSettings.DirectorySortDirection : null; + DateCreatedHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.DateCreated ? folderSettings.DirectorySortDirection : null; + FileTypeHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.FileType ? folderSettings.DirectorySortDirection : null; + ItemSizeHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.Size ? folderSettings.DirectorySortDirection : null; + SyncStatusHeader.ColumnSortOption = folderSettings.DirectorySortOption == SortOption.SyncStatus ? folderSettings.DirectorySortDirection : null; } private void FilesystemViewModel_PageTypeUpdated(object? sender, PageTypeUpdatedEventArgs e) @@ -576,27 +593,27 @@ protected override bool CanGetItemFromElement(object element) private async Task ReloadItemIconsAsync() { - if (ParentShellPageInstance is null) + if (ParentShellPageInstance?.ShellViewModel is not { } shellViewModel) return; - ParentShellPageInstance.ShellViewModel.CancelExtendedPropertiesLoading(); - var filesAndFolders = ParentShellPageInstance.ShellViewModel.FilesAndFolders.ToList(); + shellViewModel.CancelExtendedPropertiesLoading(); + var filesAndFolders = shellViewModel.FilesAndFolders.ToList(); await Task.WhenAll(filesAndFolders.Select(listedItem => { listedItem.ItemPropertiesInitialized = false; if (FileList.ContainerFromItem(listedItem) is not null) - return ParentShellPageInstance.ShellViewModel.LoadExtendedItemPropertiesAsync(listedItem); + return shellViewModel.LoadExtendedItemPropertiesAsync(listedItem); else return Task.CompletedTask; })); - if (ParentShellPageInstance.ShellViewModel.EnabledGitProperties is not GitProperties.None) + if (shellViewModel.EnabledGitProperties is not GitProperties.None) { await Task.WhenAll(filesAndFolders.Select(item => { if (item is IGitItem gitItem) - return ParentShellPageInstance.ShellViewModel.LoadGitPropertiesAsync(gitItem); + return shellViewModel.LoadGitPropertiesAsync(gitItem); return Task.CompletedTask; })); @@ -674,8 +691,11 @@ private async Task OpenItem(ListedItem item) if (!Commands.OpenItem.IsExecutable) { // Fallback if the command is not executable. It occurs only when search is performed from the columns view. + if (ParentShellPageInstance is not { } parentShellPage) + return; + var itemType = item.PrimaryItemAttribute == StorageItemTypes.Folder ? FilesystemItemType.Directory : FilesystemItemType.File; - await NavigationHelpers.OpenPath(item.ItemPath, ParentShellPageInstance, itemType); + await NavigationHelpers.OpenPath(item.ItemPath, parentShellPage, itemType); } else { @@ -731,7 +751,8 @@ private void GridSplitter_PreviewKeyUp(object sender, KeyRoutedEventArgs e) if (e.Key == VirtualKey.Left || e.Key == VirtualKey.Right) { UpdateColumnLayout(); - FolderSettings.ColumnsViewModel = ColumnsViewModel; + if (FolderSettings is { } folderSettings) + folderSettings.ColumnsViewModel = ColumnsViewModel; } } @@ -770,7 +791,8 @@ private void GridSplitter_ManipulationStarted(object sender, ManipulationStarted private void GridSplitter_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) { - FolderSettings.ColumnsViewModel = ColumnsViewModel; + if (FolderSettings is { } folderSettings) + folderSettings.ColumnsViewModel = ColumnsViewModel; this.ChangeCursor(InputSystemCursor.Create(InputSystemCursorShape.Arrow)); } @@ -781,8 +803,10 @@ private void GridSplitter_Loaded(object sender, RoutedEventArgs e) private void ToggleMenuFlyoutItem_Click(object sender, RoutedEventArgs e) { - FolderSettings.ColumnsViewModel = ColumnsViewModel; - ParentShellPageInstance.ShellViewModel.EnabledGitProperties = GetEnabledGitProperties(ColumnsViewModel); + if (FolderSettings is { } folderSettings) + folderSettings.ColumnsViewModel = ColumnsViewModel; + if (ParentShellPageInstance?.ShellViewModel is { } shellViewModel) + shellViewModel.EnabledGitProperties = GetEnabledGitProperties(ColumnsViewModel); } private void GridSplitter_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) @@ -821,7 +845,7 @@ private void AutoFitColumnsIfEnabled() TimeSpan.FromMilliseconds(250)); } - private void ShellViewModel_ItemLoadStatusChanged(object sender, ItemLoadStatusChangedEventArgs e) + private void ShellViewModel_ItemLoadStatusChanged(object? sender, ItemLoadStatusChangedEventArgs e) { if (e.Status == ItemLoadStatusChangedEventArgs.ItemLoadStatus.Complete) AutoFitColumnsIfEnabled(); @@ -899,7 +923,8 @@ private void ResizeColumnToFit(int columnToResize) column.UserLength = new GridLength(maxFitLength, GridUnitType.Pixel); } - FolderSettings.ColumnsViewModel = ColumnsViewModel; + if (FolderSettings is { } folderSettings) + folderSettings.ColumnsViewModel = ColumnsViewModel; } private double MeasureColumnEstimate(int columnIndex, int measureItemsCount, int maxItemLength) @@ -999,8 +1024,7 @@ private void FileList_Loaded(object sender, RoutedEventArgs e) RootGridZoom.ViewChangeStarted += (_, args) => { - var scroller = ContentScroller; - if (args.IsSourceZoomedInView || scroller is null) + if (args.IsSourceZoomedInView || ContentScroller is not { } scroller) return; void OnZoomScrolled(object? s, ScrollViewerViewChangedEventArgs ve) { @@ -1104,14 +1128,14 @@ private async void RemoveTagIcon_Tapped(object sender, TappedRoutedEventArgs e) var tagId = FileTagsSettingsService.GetTagsByName(tagName).FirstOrDefault()?.Uid; - if (tagId is not null) + if (tagId is not null && item.FileTags is { } fileTags) { - item.FileTags = item.FileTags + item.FileTags = fileTags .Except([tagId]) .ToArray(); - if (ParentShellPageInstance is not null) - await ParentShellPageInstance.ShellViewModel.RefreshTagGroups(); + if (ParentShellPageInstance?.ShellViewModel is { } shellViewModel) + await shellViewModel.RefreshTagGroups(); } e.Handled = true; @@ -1215,4 +1239,4 @@ private static GitProperties GetEnabledGitProperties(ColumnsViewModel columnsVie }; } } -} \ No newline at end of file +} diff --git a/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs b/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs index f1098b2f54c4..8a23e7f75526 100644 --- a/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs +++ b/src/Files.App/Views/Layouts/GridLayoutPage.xaml.cs @@ -199,16 +199,23 @@ protected override void OnNavigatedTo(NavigationEventArgs eventArgs) base.OnNavigatedTo(eventArgs); - currentIconSize = LayoutSizeKindHelper.GetIconSize(FolderSettings.LayoutMode); + var parentShellPage = ParentShellPageInstance + ?? throw new InvalidOperationException("The grid layout must be associated with a shell page."); + var shellViewModel = parentShellPage.ShellViewModel + ?? throw new InvalidOperationException("The grid layout requires an initialized shell view model."); + var folderSettings = FolderSettings + ?? throw new InvalidOperationException("The grid layout requires folder settings."); - FolderSettings.LayoutModeChangeRequested -= FolderSettings_LayoutModeChangeRequested; - FolderSettings.LayoutModeChangeRequested += FolderSettings_LayoutModeChangeRequested; + currentIconSize = LayoutSizeKindHelper.GetIconSize(folderSettings.LayoutMode); + + folderSettings.LayoutModeChangeRequested -= FolderSettings_LayoutModeChangeRequested; + folderSettings.LayoutModeChangeRequested += FolderSettings_LayoutModeChangeRequested; UserSettingsService.LayoutSettingsService.PropertyChanged += LayoutSettingsService_PropertyChanged; // Set ItemTemplate SetItemTemplate(); SetItemContainerStyle(); - FileList.ItemsSource ??= ParentShellPageInstance.ShellViewModel.FilesAndFolders; + FileList.ItemsSource ??= shellViewModel.FilesAndFolders; var parameters = (NavigationArguments)eventArgs.Parameter; if (parameters.IsLayoutSwitch) @@ -259,9 +266,12 @@ private void LayoutSettingsService_PropertyChanged(object? sender, PropertyChang private void FolderSettings_LayoutModeChangeRequested(object? sender, LayoutModeEventArgs e) { - if (FolderSettings.LayoutMode == FolderLayoutModes.ListView - || FolderSettings.LayoutMode == FolderLayoutModes.CardsView - || FolderSettings.LayoutMode == FolderLayoutModes.GridView) + if (FolderSettings is not { } folderSettings) + return; + + if (folderSettings.LayoutMode == FolderLayoutModes.ListView + || folderSettings.LayoutMode == FolderLayoutModes.CardsView + || folderSettings.LayoutMode == FolderLayoutModes.GridView) { // SetItemTemplate clears FileList.ItemsSource on style swap, which drops the selection var preservedSelection = SelectedItems?.ToList(); @@ -286,7 +296,10 @@ private void FolderSettings_LayoutModeChangeRequested(object? sender, LayoutMode private void SetItemTemplate() { - var newFileListStyle = FolderSettings.LayoutMode switch + if (FolderSettings is not { } folderSettings) + return; + + var newFileListStyle = folderSettings.LayoutMode switch { FolderLayoutModes.ListView => (Style)Resources["VerticalLayoutGridView"], FolderLayoutModes.CardsView => (Style)Resources["HorizontalLayoutGridView"], @@ -303,7 +316,7 @@ private void SetItemTemplate() shouldSetVerticalScrollMode = true; - switch (FolderSettings.LayoutMode) + switch (folderSettings.LayoutMode) { case FolderLayoutModes.ListView: FileList.ItemTemplate = ListViewBrowserTemplate; @@ -458,12 +471,13 @@ private void ItemNameTextBox_BeforeTextChanging(TextBox textBox, TextBoxBeforeTe protected override void EndRename(TextBox textBox) { GridViewItem? gridViewItem = FileList.ContainerFromItem(RenamingItem) as GridViewItem; + var layoutMode = FolderSettings?.LayoutMode; if (textBox is null || gridViewItem is null) { // NOTE: Navigating away, do nothing } - else if (FolderSettings.LayoutMode == FolderLayoutModes.GridView) + else if (layoutMode == FolderLayoutModes.GridView) { Popup? popup = gridViewItem.FindDescendant("EditPopup") as Popup; TextBlock? textBlock = gridViewItem.FindDescendant("ItemName") as TextBlock; @@ -471,10 +485,10 @@ protected override void EndRename(TextBox textBox) if (popup is not null) popup.IsOpen = false; - if (textBlock is not null) - textBlock.Opacity = (textBlock.DataContext as ListedItem)!.Opacity; + if (textBlock?.DataContext is ListedItem item) + textBlock.Opacity = item.Opacity; } - else if (FolderSettings.LayoutMode == FolderLayoutModes.CardsView || FolderSettings.LayoutMode == FolderLayoutModes.ListView) + else if (layoutMode is FolderLayoutModes.CardsView or FolderLayoutModes.ListView) { TextBlock? textBlock = gridViewItem.FindDescendant("ItemName") as TextBlock; @@ -523,16 +537,22 @@ protected override async void FileList_PreviewKeyDown(object sender, KeyRoutedEv if (ctrlPressed && !shiftPressed) { - var folders = ParentShellPageInstance?.SlimContentPage.SelectedItems?.Where(file => file.PrimaryItemAttribute == StorageItemTypes.Folder); - foreach (ListedItem? folder in folders) + var selectedItems = ParentShellPageInstance?.SlimContentPage?.SelectedItems; + if (selectedItems is null) + return; + + foreach (var folder in selectedItems.Where(file => file.PrimaryItemAttribute == StorageItemTypes.Folder)) { - if (folder is not null) - await NavigationHelpers.OpenPathInNewTab(folder.ItemPath); + await NavigationHelpers.OpenPathInNewTab(folder.ItemPath); } } else if (ctrlPressed && shiftPressed) { - NavigationHelpers.OpenInSecondaryPane(ParentShellPageInstance, SelectedItems.FirstOrDefault(item => item.PrimaryItemAttribute == StorageItemTypes.Folder)); + if (ParentShellPageInstance is { } parentShellPage && + SelectedItems.FirstOrDefault(item => item.PrimaryItemAttribute == StorageItemTypes.Folder) is { } folder) + { + NavigationHelpers.OpenInSecondaryPane(parentShellPage, folder); + } } } else if (e.Key == VirtualKey.Enter && e.KeyStatus.IsMenuKeyDown) @@ -570,8 +590,11 @@ protected override bool CanGetItemFromElement(object element) private void FolderSettings_IconSizeChanged() { + if (FolderSettings is not { } folderSettings) + return; + // Check if icons need to be reloaded - var newIconSize = LayoutSizeKindHelper.GetIconSize(FolderSettings.LayoutMode); + var newIconSize = LayoutSizeKindHelper.GetIconSize(folderSettings.LayoutMode); if (newIconSize != currentIconSize) { currentIconSize = newIconSize; @@ -581,24 +604,24 @@ private void FolderSettings_IconSizeChanged() private async Task ReloadItemIconsAsync() { - if (ParentShellPageInstance is null) + if (ParentShellPageInstance?.ShellViewModel is not { } shellViewModel) return; - ParentShellPageInstance.ShellViewModel.CancelExtendedPropertiesLoading(); - var filesAndFolders = ParentShellPageInstance.ShellViewModel.FilesAndFolders.ToList(); + shellViewModel.CancelExtendedPropertiesLoading(); + var filesAndFolders = shellViewModel.FilesAndFolders.ToList(); foreach (ListedItem listedItem in filesAndFolders) { listedItem.ItemPropertiesInitialized = false; if (FileList.ContainerFromItem(listedItem) is not null) - await ParentShellPageInstance.ShellViewModel.LoadExtendedItemPropertiesAsync(listedItem); + await shellViewModel.LoadExtendedItemPropertiesAsync(listedItem); } - if (ParentShellPageInstance.ShellViewModel.EnabledGitProperties is not GitProperties.None) + if (shellViewModel.EnabledGitProperties is not GitProperties.None) { await Task.WhenAll(filesAndFolders.Select(item => { if (item is IGitItem gitItem) - return ParentShellPageInstance.ShellViewModel.LoadGitPropertiesAsync(gitItem); + return shellViewModel.LoadGitPropertiesAsync(gitItem); return Task.CompletedTask; })); @@ -642,15 +665,15 @@ private async void FileList_ItemTapped(object sender, TappedRoutedEventArgs e) { if (clickedItem is TextBlock textBlock && textBlock.Name == "ItemName") { - CheckRenameDoubleClick(clickedItem?.DataContext); + CheckRenameDoubleClick(textBlock.DataContext); } else if (IsRenamingItem) { if (FileList.ContainerFromItem(RenamingItem) is GridViewItem gridViewItem) { - if (FolderSettings.LayoutMode == FolderLayoutModes.GridView) + if (FolderSettings?.LayoutMode == FolderLayoutModes.GridView) { - Popup popup = gridViewItem.FindDescendant("EditPopup") as Popup; + Popup? popup = gridViewItem.FindDescendant("EditPopup") as Popup; var textBox = popup?.Child as TextBox; if (textBox is not null) @@ -785,7 +808,7 @@ private void SelectionCheckbox_PointerCanceled(object sender, PointerRoutedEvent // To avoid crashes, disable scrolling when drag-and-drop if grouped. (#14484) private bool ShouldDisableScrollingWhenDragAndDrop => FolderSettings?.LayoutMode is FolderLayoutModes.GridView or FolderLayoutModes.CardsView && - (ParentShellPageInstance?.ShellViewModel.FilesAndFolders.IsGrouped ?? false); + (ParentShellPageInstance?.ShellViewModel is { } shellViewModel && shellViewModel.FilesAndFolders.IsGrouped); protected override void FileList_DragItemsStarting(object sender, DragItemsStartingEventArgs e) { @@ -841,4 +864,4 @@ private void UpdateCheckboxVisibility(object sender, bool isPointerOver) } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Views/MainPage.xaml.cs b/src/Files.App/Views/MainPage.xaml.cs index 6930515eda5d..80c7ac7996ec 100644 --- a/src/Files.App/Views/MainPage.xaml.cs +++ b/src/Files.App/Views/MainPage.xaml.cs @@ -183,7 +183,7 @@ public async void MultitaskingControl_CurrentInstanceChanged(object? sender, Cur private void PaneHolder_PropertyChanged(object? sender, PropertyChangedEventArgs e) { - SidebarAdaptiveViewModel.NotifyInstanceRelatedPropertiesChanged(SidebarAdaptiveViewModel.PaneHolder.ActivePane?.TabBarItemParameter?.NavigationParameter?.ToString()); + SidebarAdaptiveViewModel.NotifyInstanceRelatedPropertiesChanged(SidebarAdaptiveViewModel.PaneHolder?.ActivePane?.TabBarItemParameter?.NavigationParameter?.ToString()); UpdateStatusBarProperties(); UpdateNavToolbarProperties(); LoadPaneChanged(); @@ -199,18 +199,18 @@ private void UpdateStatusBarProperties() { if (StatusBar is not null) { - StatusBar.StatusBarViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn.SlimContentPage?.StatusBarViewModel; - StatusBar.SelectedItemsPropertiesViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn.SlimContentPage?.SelectedItemsPropertiesViewModel; + StatusBar.StatusBarViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn?.SlimContentPage?.StatusBarViewModel; + StatusBar.SelectedItemsPropertiesViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn?.SlimContentPage?.SelectedItemsPropertiesViewModel; } } private void UpdateNavToolbarProperties() { if (NavToolbar is not null) - NavToolbar.ViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn.ToolbarViewModel; + NavToolbar.ViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn?.ToolbarViewModel; if (InnerNavigationToolbar is not null) - InnerNavigationToolbar.ViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn.ToolbarViewModel; + InnerNavigationToolbar.ViewModel = SidebarAdaptiveViewModel.PaneHolder?.ActivePaneOrColumn?.ToolbarViewModel; } protected override void OnNavigatedTo(NavigationEventArgs e) @@ -503,7 +503,7 @@ private void Page_PointerReleased(object sender, PointerRoutedEventArgs e) { // Workaround for issue where clicking an empty area in the window (toolbar, title bar etc) prevents keyboard // shortcuts from working properly, see https://github.com/microsoft/microsoft-ui-xaml/issues/6467 - DispatcherQueue.TryEnqueue(() => ContentPageContext.ShellPage?.PaneHolder.FocusActivePane()); + DispatcherQueue.TryEnqueue(() => ContentPageContext.ShellPage?.PaneHolder?.FocusActivePane()); } private void SidebarControl_ItemContextInvoked(object sender, ItemContextInvokedArgs e) diff --git a/src/Files.App/Views/Properties/CustomizationPage.xaml.cs b/src/Files.App/Views/Properties/CustomizationPage.xaml.cs index 084970c73253..34b86d210016 100644 --- a/src/Files.App/Views/Properties/CustomizationPage.xaml.cs +++ b/src/Files.App/Views/Properties/CustomizationPage.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Views.Properties { public sealed partial class CustomizationPage : BasePropertiesPage { - private CustomizationViewModel CustomizationViewModel { get; set; } + private CustomizationViewModel? CustomizationViewModel { get; set; } public CustomizationPage() { @@ -21,11 +21,14 @@ protected override void OnNavigatedTo(NavigationEventArgs e) base.OnNavigatedTo(e); - CustomizationViewModel = new(AppInstance, BaseProperties, parameter.Window.AppWindow); + CustomizationViewModel = new( + AppInstance, + BaseProperties ?? throw new InvalidOperationException("The properties model has not been initialized."), + parameter.Window.AppWindow); } public override async Task SaveChangesAsync() - => await CustomizationViewModel.UpdateIcon(); + => CustomizationViewModel is { } viewModel && await viewModel.UpdateIcon(); public override void Dispose() { diff --git a/src/Files.App/Views/Properties/GeneralPage.xaml.cs b/src/Files.App/Views/Properties/GeneralPage.xaml.cs index d100014c2b6c..7bdbc715c9e4 100644 --- a/src/Files.App/Views/Properties/GeneralPage.xaml.cs +++ b/src/Files.App/Views/Properties/GeneralPage.xaml.cs @@ -108,7 +108,7 @@ async Task SaveLibraryAsync(LibraryItem library) newName = $"{newName}{ShellLibraryItem.EXTENSION}"; var file = new StorageFileWithPath(null, library.ItemPath); - var renamed = await AppInstance!.FilesystemHelpers.RenameAsync(file, newName, NameCollisionOption.FailIfExists, false, false); + var renamed = await AppInstance.FilesystemHelpers.RenameAsync(file, newName, NameCollisionOption.FailIfExists, false, false); if (renamed is ReturnResult.Success) { var newPath = Path.Combine(Path.GetDirectoryName(library.ItemPath)!, newName); @@ -188,8 +188,12 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => if (!GetNewName(out var newName)) return true; + var appInstance = AppInstance; + if (appInstance is null) + return true; + return await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => - UIFilesystemHelpers.RenameFileItemAsync(item, ViewModel.ItemName, AppInstance, false) + UIFilesystemHelpers.RenameFileItemAsync(item, ViewModel.ItemName, appInstance, false) ); } } diff --git a/src/Files.App/Views/Properties/HashesPage.xaml.cs b/src/Files.App/Views/Properties/HashesPage.xaml.cs index ff1f7c722e3d..b788de77485a 100644 --- a/src/Files.App/Views/Properties/HashesPage.xaml.cs +++ b/src/Files.App/Views/Properties/HashesPage.xaml.cs @@ -11,7 +11,7 @@ namespace Files.App.Views.Properties { public sealed partial class HashesPage : BasePropertiesPage { - private HashesViewModel HashesViewModel { get; set; } + private HashesViewModel? HashesViewModel { get; set; } private bool _cancel; @@ -60,7 +60,7 @@ public async override Task SaveChangesAsync() public override void Dispose() { - HashesViewModel.Dispose(); + HashesViewModel?.Dispose(); } } } diff --git a/src/Files.App/Views/Properties/LibraryPage.xaml.cs b/src/Files.App/Views/Properties/LibraryPage.xaml.cs index fe69ae35cd2e..fa0b3b009955 100644 --- a/src/Files.App/Views/Properties/LibraryPage.xaml.cs +++ b/src/Files.App/Views/Properties/LibraryPage.xaml.cs @@ -137,7 +137,7 @@ private void RemoveLocation() } } - private bool IsChanged(LibraryItem lib, out string newDefaultSaveFolder, out string[] newFolders, out bool? newIsPinned) + private bool IsChanged(LibraryItem lib, out string? newDefaultSaveFolder, out string[]? newFolders, out bool? newIsPinned) { bool isChanged = false; @@ -176,7 +176,7 @@ public override async Task SaveChangesAsync() return true; // Skip updates and close dialog when nothing changed - if (!IsChanged(props.Library, out string newDefaultSaveFolder, out string[] newFolders, out bool? newIsPinned)) + if (!IsChanged(props.Library, out var newDefaultSaveFolder, out var newFolders, out var newIsPinned)) return true; while (true) @@ -219,7 +219,7 @@ public override void Dispose() public sealed partial class LibraryFolder : ObservableObject { - public string Path { get; set; } + public string Path { get; set; } = string.Empty; private bool isDefault; public bool IsDefault diff --git a/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs b/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs index b9410247fe3f..5f47d0fa5209 100644 --- a/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs +++ b/src/Files.App/Views/Properties/MainPropertiesPage.xaml.cs @@ -17,11 +17,11 @@ public sealed partial class MainPropertiesPage : BasePropertiesPage { private IAppThemeModeService AppThemeModeService { get; } = Ioc.Default.GetRequiredService(); - private AppWindow AppWindow => Window.AppWindow; + private AppWindow? AppWindow => Window?.AppWindow; - private Window Window; + private Window? Window; - private MainPropertiesViewModel MainPropertiesViewModel { get; set; } + private MainPropertiesViewModel? MainPropertiesViewModel { get; set; } public MainPropertiesPage() { @@ -35,11 +35,15 @@ public MainPropertiesPage() // Navigates to specified properties page public bool TryNavigateToPage(PropertiesNavigationViewItemType pageType) { - var page = MainPropertiesViewModel.NavigationItems.FirstOrDefault(item => item.ItemType == pageType); + var viewModel = MainPropertiesViewModel; + if (viewModel is null) + return false; + + var page = viewModel.NavigationItems.FirstOrDefault(item => item.ItemType == pageType); if (page is null) return false; - MainPropertiesViewModel.SelectedNavigationItem = page; + viewModel.SelectedNavigationItem = page; return true; } @@ -48,7 +52,8 @@ private void PropertiesSidebar_ItemInvoked(object sender, ItemInvokedEventArgs e if (sender is not SidebarItem { Item: PropertiesNavigationItem navItem }) return; - MainPropertiesViewModel.SelectedNavigationItem = navItem; + if (MainPropertiesViewModel is { } viewModel) + viewModel.SelectedNavigationItem = navItem; } protected override void OnNavigatedTo(NavigationEventArgs e) @@ -59,19 +64,26 @@ protected override void OnNavigatedTo(NavigationEventArgs e) base.OnNavigatedTo(e); - MainPropertiesViewModel = new(Window, MainContentFrame, BaseProperties, parameter); + MainPropertiesViewModel = new( + Window, + MainContentFrame, + BaseProperties ?? throw new InvalidOperationException("The properties model has not been initialized."), + parameter); } private void Page_Loaded(object sender, RoutedEventArgs e) { + if (Window is not { } window || MainPropertiesViewModel is not { } viewModel) + return; + AppThemeModeService.AppThemeModeChanged += AppThemeModeService_AppThemeModeChanged; - Window.Closed += Window_Closed; + window.Closed += Window_Closed; AppThemeModeService.ApplyResources(); UpdatePageLayout(this.Width); - PropertiesSidebar.SelectedItem = MainPropertiesViewModel.SelectedNavigationItem; - Window.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion); - Window.AppWindow.Changed += AppWindow_Changed; + PropertiesSidebar.SelectedItem = viewModel.SelectedNavigationItem; + window.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion); + window.AppWindow.Changed += AppWindow_Changed; } private int SetTitleBarDragRegion(InputNonClientPointerSource source, SizeInt32 size, double scaleFactor, Func getScaledRect) @@ -86,7 +98,7 @@ private void Page_SizeChanged(object sender, SizeChangedEventArgs e) private void Page_KeyDown(object sender, KeyRoutedEventArgs e) { if (e.Key.Equals(VirtualKey.Escape)) - Window.Close(); + Window?.Close(); } private void UpdatePageLayout(double pageWidth) @@ -96,31 +108,33 @@ private void UpdatePageLayout(double pageWidth) private async void AppThemeModeService_AppThemeModeChanged(object? sender, EventArgs e) { - if (Parent is null) + if (Parent is null || Window is not { } window) return; await DispatcherQueue.EnqueueOrInvokeAsync(() => { - AppThemeModeService.SetAppThemeMode(Window, Window.AppWindow.TitleBar, AppThemeModeService.AppThemeMode, false); + AppThemeModeService.SetAppThemeMode(window, window.AppWindow.TitleBar, AppThemeModeService.AppThemeMode, false); }); } private void Window_Closed(object sender, WindowEventArgs args) { + if (Window is not { } window) + return; + AppThemeModeService.AppThemeModeChanged -= AppThemeModeService_AppThemeModeChanged; - Window.Closed -= Window_Closed; - Window.AppWindow.Changed -= AppWindow_Changed; + window.Closed -= Window_Closed; + window.AppWindow.Changed -= AppWindow_Changed; - if (MainPropertiesViewModel.ChangedPropertiesCancellationTokenSource is not null && - !MainPropertiesViewModel.ChangedPropertiesCancellationTokenSource.IsCancellationRequested) + if (MainPropertiesViewModel?.ChangedPropertiesCancellationTokenSource is { IsCancellationRequested: false } cancellation) { - MainPropertiesViewModel.ChangedPropertiesCancellationTokenSource.Cancel(); + cancellation.Cancel(); } } private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs e) { - Window.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion); + Window?.RaiseSetTitleBarDragRegion(SetTitleBarDragRegion); } public override async Task SaveChangesAsync() diff --git a/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml.cs b/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml.cs index f2aa0fd60bbb..d03a922ab966 100644 --- a/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml.cs +++ b/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Views.Properties { public sealed partial class SecurityAdvancedPage : BasePropertiesPage { - private SecurityAdvancedViewModel SecurityAdvancedViewModel { get; set; } + private SecurityAdvancedViewModel? SecurityAdvancedViewModel { get; set; } public SecurityAdvancedPage() { diff --git a/src/Files.App/Views/Properties/SecurityPage.xaml.cs b/src/Files.App/Views/Properties/SecurityPage.xaml.cs index b04b1b43ca3c..eddb62ba8b91 100644 --- a/src/Files.App/Views/Properties/SecurityPage.xaml.cs +++ b/src/Files.App/Views/Properties/SecurityPage.xaml.cs @@ -9,9 +9,9 @@ namespace Files.App.Views.Properties { public sealed partial class SecurityPage : BasePropertiesPage { - private SecurityViewModel SecurityViewModel { get; set; } + private SecurityViewModel? SecurityViewModel { get; set; } - private object _parameter; + private PropertiesPageNavigationParameter? _parameter; public SecurityPage() { @@ -30,7 +30,8 @@ protected override void OnNavigatedTo(NavigationEventArgs e) private void OpenSecurityAdvancedPageButton_Click(object sender, RoutedEventArgs e) { - Frame?.Navigate(typeof(SecurityAdvancedPage), _parameter); + if (_parameter is not null) + Frame?.Navigate(typeof(SecurityAdvancedPage), _parameter); } public async override Task SaveChangesAsync() diff --git a/src/Files.App/Views/Properties/SignaturesPage.xaml.cs b/src/Files.App/Views/Properties/SignaturesPage.xaml.cs index 74e30800dcd2..edae3916058d 100644 --- a/src/Files.App/Views/Properties/SignaturesPage.xaml.cs +++ b/src/Files.App/Views/Properties/SignaturesPage.xaml.cs @@ -8,7 +8,7 @@ namespace Files.App.Views.Properties { public sealed partial class SignaturesPage : BasePropertiesPage { - private SignaturesViewModel SignaturesViewModel { get; set; } + private SignaturesViewModel? SignaturesViewModel { get; set; } public SignaturesPage() { @@ -34,7 +34,7 @@ public override Task SaveChangesAsync() public override void Dispose() { - SignaturesViewModel.Dispose(); + SignaturesViewModel?.Dispose(); } } } diff --git a/src/Files.App/Views/ReleaseNotesPage.xaml.cs b/src/Files.App/Views/ReleaseNotesPage.xaml.cs index a20e5f13098e..aea2bc700c9a 100644 --- a/src/Files.App/Views/ReleaseNotesPage.xaml.cs +++ b/src/Files.App/Views/ReleaseNotesPage.xaml.cs @@ -20,7 +20,9 @@ public sealed partial class ReleaseNotesPage : Page, IDisposable private FrameworkElement RootAppElement => (FrameworkElement)MainWindow.Instance.Content; - private IShellPage AppInstance { get; set; } = null!; + private IShellPage? appInstance; + private IShellPage AppInstance + => appInstance ?? throw new InvalidOperationException("The release notes page has not been initialized."); public ReleaseNotesPage() { @@ -29,10 +31,11 @@ public ReleaseNotesPage() protected override async void OnNavigatedTo(NavigationEventArgs e) { - if (e.Parameter is not NavigationArguments parameters) + if (e.Parameter is not NavigationArguments { AssociatedTabInstance: { } associatedInstance } parameters || + associatedInstance.ShellViewModel is not { } shellViewModel) return; - AppInstance = parameters.AssociatedTabInstance!; + appInstance = associatedInstance; AppInstance.InstanceViewModel.IsPageTypeNotHome = true; AppInstance.InstanceViewModel.IsPageTypeSearchResults = false; @@ -51,8 +54,8 @@ protected override async void OnNavigatedTo(NavigationEventArgs e) AppInstance.ToolbarViewModel.CanNavigateToParent = false; // Set path of working directory empty - await AppInstance.ShellViewModel.SetWorkingDirectoryAsync("ReleaseNotes"); - AppInstance.ShellViewModel.CheckForBackgroundImage(); + await shellViewModel.SetWorkingDirectoryAsync("ReleaseNotes"); + shellViewModel.CheckForBackgroundImage(); AppInstance.SlimContentPage?.StatusBarViewModel.UpdateGitInfo(false, string.Empty, null); diff --git a/src/Files.App/Views/Settings/AppearancePage.xaml.cs b/src/Files.App/Views/Settings/AppearancePage.xaml.cs index 90d59c8cc40c..b29f9b4ac7a5 100644 --- a/src/Files.App/Views/Settings/AppearancePage.xaml.cs +++ b/src/Files.App/Views/Settings/AppearancePage.xaml.cs @@ -8,7 +8,9 @@ namespace Files.App.Views.Settings { public sealed partial class AppearancePage : Page { - private AppearanceViewModel ViewModel => DataContext as AppearanceViewModel; + private AppearanceViewModel ViewModel + => DataContext as AppearanceViewModel + ?? throw new InvalidOperationException("The appearance view model has not been initialized."); public AppearancePage() { diff --git a/src/Files.App/Views/Settings/GeneralPage.xaml.cs b/src/Files.App/Views/Settings/GeneralPage.xaml.cs index 8113c7d24b02..bad5ee120d9d 100644 --- a/src/Files.App/Views/Settings/GeneralPage.xaml.cs +++ b/src/Files.App/Views/Settings/GeneralPage.xaml.cs @@ -17,7 +17,8 @@ public GeneralPage() private void RemoveStartupPage(object sender, RoutedEventArgs e) { - ViewModel.RemovePageCommand.Execute((sender as FrameworkElement).DataContext as PageOnStartupViewModel); + if (sender is FrameworkElement { DataContext: PageOnStartupViewModel page }) + ViewModel.RemovePageCommand.Execute(page); } } } diff --git a/src/Files.App/Views/Settings/SettingsPage.xaml.cs b/src/Files.App/Views/Settings/SettingsPage.xaml.cs index ddaf6fbe9b5c..5f2267daa22b 100644 --- a/src/Files.App/Views/Settings/SettingsPage.xaml.cs +++ b/src/Files.App/Views/Settings/SettingsPage.xaml.cs @@ -19,7 +19,9 @@ public sealed partial class SettingsPage : Page public SettingsPageViewModel ViewModel { get; } = new(); - private IShellPage AppInstance { get; set; } = null!; + private IShellPage? appInstance; + private IShellPage AppInstance + => appInstance ?? throw new InvalidOperationException("The settings page has not been initialized."); public SettingsPage() { @@ -33,10 +35,11 @@ private void SettingsPage_Loaded(object sender, RoutedEventArgs e) protected override async void OnNavigatedTo(NavigationEventArgs e) { - if (e.Parameter is not NavigationArguments navArgs) + if (e.Parameter is not NavigationArguments { AssociatedTabInstance: { } associatedInstance } navArgs || + associatedInstance.ShellViewModel is not { } shellViewModel) return; - AppInstance = navArgs.AssociatedTabInstance!; + appInstance = associatedInstance; AppInstance.InstanceViewModel.IsPageTypeNotHome = true; AppInstance.InstanceViewModel.IsPageTypeSearchResults = false; @@ -55,8 +58,8 @@ protected override async void OnNavigatedTo(NavigationEventArgs e) AppInstance.ToolbarViewModel.CanGoForward = AppInstance.CanNavigateForward; AppInstance.ToolbarViewModel.CanNavigateToParent = false; - await AppInstance.ShellViewModel.SetWorkingDirectoryAsync("Settings"); - AppInstance.ShellViewModel.CheckForBackgroundImage(); + await shellViewModel.SetWorkingDirectoryAsync("Settings"); + shellViewModel.CheckForBackgroundImage(); AppInstance.SlimContentPage?.StatusBarViewModel.UpdateGitInfo(false, string.Empty, null); AppInstance.SlimContentPage?.InfoPaneViewModel.UpdateSelectedItemPreviewAsync(); diff --git a/src/Files.App/Views/Settings/TagsPage.xaml.cs b/src/Files.App/Views/Settings/TagsPage.xaml.cs index 43e5ae345d40..4d9016d40383 100644 --- a/src/Files.App/Views/Settings/TagsPage.xaml.cs +++ b/src/Files.App/Views/Settings/TagsPage.xaml.cs @@ -35,7 +35,7 @@ private void RenameTextBox_KeyDown(object sender, KeyRoutedEventArgs e) switch (e.Key) { case VirtualKey.Enter: - if (!editingTag!.CanCommit) + if (editingTag?.CanCommit != true) return; CommitChanges(textBox); @@ -53,26 +53,35 @@ private void EditTag_Click(object sender, RoutedEventArgs e) editingTag.NewColor = editingTag.Tag.Color; } - editingTag = (ListedTagViewModel)((Button)sender).DataContext; + if (sender is not Button { DataContext: ListedTagViewModel tag }) + return; + + editingTag = tag; editingTag.NewColor = editingTag.Tag.Color; editingTag.NewName = editingTag.Tag.Name; editingTag.IsEditing = true; - var item = (ListViewItem)TagsList.ContainerFromItem(editingTag); - var textBlock = item.FindDescendant("TagName") as TextBlock; - var textBox = item.FindDescendant("TagNameTextBox") as TextBox; + if (TagsList.ContainerFromItem(editingTag) is not ListViewItem item || + item.FindDescendant("TagName") is not TextBlock textBlock || + item.FindDescendant("TagNameTextBox") is not TextBox textBox) + { + return; + } - textBox!.TextChanged += RenameTextBox_TextChanged; + textBox.TextChanged += RenameTextBox_TextChanged; - textBox!.Text = textBlock!.Text; + textBox.Text = textBlock.Text; oldTagName = textBlock.Text; } private void CommitRenameTag_Click(object sender, RoutedEventArgs e) { - var item = (ListViewItem)TagsList.ContainerFromItem(editingTag); - - CommitChanges(item.FindDescendant("TagNameTextBox") as TextBox); + if (editingTag is not null && + TagsList.ContainerFromItem(editingTag) is ListViewItem item && + item.FindDescendant("TagNameTextBox") is TextBox textBox) + { + CommitChanges(textBox); + } } private void CancelRenameTag_Click(object sender, RoutedEventArgs e) @@ -97,12 +106,15 @@ private void RemoveTag_Click(object sender, RoutedEventArgs e) private void RenameTextBox_TextChanged(object sender, TextChangedEventArgs e) { + if (editingTag is not { } tag) + return; + var text = ((TextBox)sender).Text; var isNullOrEmpty = string.IsNullOrEmpty(text); - editingTag!.IsNameValid = isNullOrEmpty || (IsNameValid(text) && !ViewModel.Tags.Any(tag => tag.Tag.Name == text && editingTag!.Tag.Name != text)); - editingTag!.CanCommit = !isNullOrEmpty && editingTag!.IsNameValid && ( - text != editingTag!.Tag.Name || - editingTag!.NewColor != editingTag!.Tag.Color + tag.IsNameValid = isNullOrEmpty || (IsNameValid(text) && !ViewModel.Tags.Any(item => item.Tag.Name == text && tag.Tag.Name != text)); + tag.CanCommit = !isNullOrEmpty && tag.IsNameValid && ( + text != tag.Tag.Name || + tag.NewColor != tag.Tag.Color ); } @@ -111,9 +123,9 @@ private void EditColorPicker_ColorChanged(ColorPicker sender, ColorChangedEventA if (editingTag is null) return; - editingTag!.CanCommit = editingTag!.IsNameValid && ( - editingTag!.NewName != editingTag!.Tag.Name || - CommunityToolkit.WinUI.Helpers.ColorHelper.ToHex(sender.Color) != editingTag!.Tag.Color + editingTag.CanCommit = editingTag.IsNameValid && ( + editingTag.NewName != editingTag.Tag.Name || + CommunityToolkit.WinUI.Helpers.ColorHelper.ToHex(sender.Color) != editingTag.Tag.Color ); } @@ -135,26 +147,36 @@ private void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcc private void CommitChanges(TextBox textBox) { + if (editingTag is not { } tag) + return; + EndEditing(textBox); string newTagName = textBox.Text.Trim().TrimEnd('.'); - if (newTagName != oldTagName || editingTag!.NewColor != editingTag.Tag.Color) - ViewModel.EditExistingTag(editingTag!, newTagName, editingTag!.NewColor); + if (newTagName != oldTagName || tag.NewColor != tag.Tag.Color) + ViewModel.EditExistingTag(tag, newTagName, tag.NewColor); } private void EndEditing(TextBox textBox) { textBox.TextChanged -= RenameTextBox_TextChanged; - editingTag!.IsEditing = false; + if (editingTag is { } tag) + tag.IsEditing = false; } private void CloseEdit() { - var item = (ListViewItem)TagsList.ContainerFromItem(editingTag); - editingTag!.NewColor = editingTag.Tag.Color; - editingTag!.IsNameValid = true; - editingTag!.CanCommit = false; + if (editingTag is not { } tag || + TagsList.ContainerFromItem(tag) is not ListViewItem item || + item.FindDescendant("TagNameTextBox") is not TextBox textBox) + { + return; + } - EndEditing(item.FindDescendant("TagNameTextBox") as TextBox); + tag.NewColor = tag.Tag.Color; + tag.IsNameValid = true; + tag.CanCommit = false; + + EndEditing(textBox); } private bool IsNameValid(string name) diff --git a/src/Files.App/Views/ShellPanesPage.xaml.cs b/src/Files.App/Views/ShellPanesPage.xaml.cs index 73e8486b1a7b..24192cb55c3c 100644 --- a/src/Files.App/Views/ShellPanesPage.xaml.cs +++ b/src/Files.App/Views/ShellPanesPage.xaml.cs @@ -72,7 +72,7 @@ public bool IsMultiPaneAvailable } } - public IShellPage ActivePaneOrColumn + public IShellPage? ActivePaneOrColumn { get { @@ -80,7 +80,7 @@ public IShellPage ActivePaneOrColumn if (ActivePane is not null && ActivePane.IsColumnView && ActivePane.SlimContentPage is ColumnsLayoutPage columnLayoutPage) return columnLayoutPage.ActiveColumnShellPage; - return ActivePane ?? GetPane(0)!; + return ActivePane ?? GetPane(0); } } diff --git a/src/Files.App/Views/Shells/BaseShellPage.cs b/src/Files.App/Views/Shells/BaseShellPage.cs index 9b5c9063df83..f12978b4ab54 100644 --- a/src/Files.App/Views/Shells/BaseShellPage.cs +++ b/src/Files.App/Views/Shells/BaseShellPage.cs @@ -48,7 +48,7 @@ public abstract class BaseShellPage : Page, IShellPage, INotifyPropertyChanged public NavigationToolbarViewModel ToolbarViewModel { get; } = new NavigationToolbarViewModel(); - public IBaseLayoutPage SlimContentPage => ContentPage; + public IBaseLayoutPage? SlimContentPage => ContentPage; public IFilesystemHelpers FilesystemHelpers { get; protected set; } @@ -70,12 +70,12 @@ public abstract class BaseShellPage : Page, IShellPage, INotifyPropertyChanged public virtual IList BackwardStack => ItemDisplay.BackStack; - public ShellViewModel ShellViewModel { get; protected set; } + public ShellViewModel? ShellViewModel { get; protected set; } public CurrentInstanceViewModel InstanceViewModel { get; } - protected BaseLayoutPage _ContentPage; - public BaseLayoutPage ContentPage + protected BaseLayoutPage? _ContentPage; + public BaseLayoutPage? ContentPage { get => _ContentPage; set @@ -90,13 +90,13 @@ public BaseLayoutPage ContentPage NotifyPropertyChanged(nameof(ContentPage)); NotifyPropertyChanged(nameof(SlimContentPage)); if (value is not null) - _ContentPage.StatusBarViewModel.CheckoutRequested += GitCheckout_Required; + value.StatusBarViewModel.CheckoutRequested += GitCheckout_Required; } } } - protected IShellPanesPage _PaneHolder; - public IShellPanesPage PaneHolder + protected IShellPanesPage? _PaneHolder; + public IShellPanesPage? PaneHolder { get => _PaneHolder; set @@ -110,8 +110,8 @@ public IShellPanesPage PaneHolder } } - protected TabBarItemParameter _TabItemArguments; - public TabBarItemParameter TabBarItemParameter + protected TabBarItemParameter? _TabItemArguments; + public TabBarItemParameter? TabBarItemParameter { get => _TabItemArguments; set @@ -120,7 +120,8 @@ public TabBarItemParameter TabBarItemParameter { _TabItemArguments = value; - ContentChanged?.Invoke(this, value); + if (value is not null) + ContentChanged?.Invoke(this, value); } } } @@ -156,7 +157,7 @@ public bool IsCurrentInstance public event PropertyChangedEventHandler? PropertyChanged; - public event EventHandler ContentChanged; + public event EventHandler? ContentChanged; public BaseShellPage(CurrentInstanceViewModel instanceViewModel) { @@ -206,36 +207,40 @@ protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "" PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - protected void FilesystemViewModel_PageTypeUpdated(object sender, PageTypeUpdatedEventArgs e) + protected void FilesystemViewModel_PageTypeUpdated(object? sender, PageTypeUpdatedEventArgs e) { InstanceViewModel.IsPageTypeCloudDrive = e.IsTypeCloudDrive; } - protected void FilesystemViewModel_OnSelectionRequestedEvent(object sender, List e) + protected void FilesystemViewModel_OnSelectionRequestedEvent(object? sender, List e) { + if (ContentPage is not { } contentPage) + return; + // Raised by the directory watcher, which can fire while the user is typing in the // omnibar - don't yank focus in that case (the omnibar's TryCancel doesn't help here // because FocusFileList -> ListViewItem.Focus uses FocusState.Keyboard). if (!UIHelpers.IsTextInputFocused(XamlRoot)) - ContentPage.ItemManipulationModel.FocusFileList(); - ContentPage.ItemManipulationModel.SetSelectedItems(e); + contentPage.ItemManipulationModel.FocusFileList(); + contentPage.ItemManipulationModel.SetSelectedItems(e); } - protected async void FilesystemViewModel_DirectoryInfoUpdated(object sender, EventArgs e) + protected async void FilesystemViewModel_DirectoryInfoUpdated(object? sender, EventArgs e) { - if (ContentPage is null) + if (ContentPage is not { } contentPage || + ShellViewModel is not { } shellViewModel) return; - var directoryItemCountLocalization = Strings.Items.GetLocalizedFormatResource(ShellViewModel.FilesAndFolders.Count); + var directoryItemCountLocalization = Strings.Items.GetLocalizedFormatResource(shellViewModel.FilesAndFolders.Count); - BranchItem? headBranch = headBranch = InstanceViewModel.IsGitRepository + BranchItem? headBranch = InstanceViewModel.IsGitRepository ? await GitHelpers.GetRepositoryHead(InstanceViewModel.GitRepositoryPath) : null; - if (InstanceViewModel.GitRepositoryPath != ShellViewModel.GitDirectory) + if (InstanceViewModel.GitRepositoryPath != shellViewModel.GitDirectory) { - InstanceViewModel.GitRepositoryPath = ShellViewModel.GitDirectory; - InstanceViewModel.IsGitRepository = ShellViewModel.IsValidGitDirectory; + InstanceViewModel.GitRepositoryPath = shellViewModel.GitDirectory; + InstanceViewModel.IsGitRepository = shellViewModel.IsValidGitDirectory; InstanceViewModel.GitBranchName = headBranch is not null ? headBranch.Name @@ -256,24 +261,20 @@ protected async void FilesystemViewModel_DirectoryInfoUpdated(object sender, Eve } } - var contentPage = ContentPage; - if (contentPage is null) - return; - if (!GitHelpers.IsExecutingGitAction) { contentPage.StatusBarViewModel.UpdateGitInfo( InstanceViewModel.IsGitRepository, InstanceViewModel.GitRepositoryPath, - headBranch); + headBranch); } - contentPage.StatusBarViewModel.DirectoryItemCount = $"{ShellViewModel.FilesAndFolders.Count} {directoryItemCountLocalization}"; - contentPage.InfoPaneViewModel.DirectoryItemCount = $"{ShellViewModel.FilesAndFolders.Count} {directoryItemCountLocalization}"; + contentPage.StatusBarViewModel.DirectoryItemCount = $"{shellViewModel.FilesAndFolders.Count} {directoryItemCountLocalization}"; + contentPage.InfoPaneViewModel.DirectoryItemCount = $"{shellViewModel.FilesAndFolders.Count} {directoryItemCountLocalization}"; contentPage.UpdateSelectionSize(); } - protected async void FilesystemViewModel_GitDirectoryUpdated(object sender, EventArgs e) + protected async void FilesystemViewModel_GitDirectoryUpdated(object? sender, EventArgs e) { if (GitHelpers.IsExecutingGitAction) return; @@ -294,14 +295,18 @@ protected async void FilesystemViewModel_GitDirectoryUpdated(object sender, Even protected async void GitCheckout_Required(object? sender, string branchName) { - if (!await GitHelpers.Checkout(ShellViewModel.GitDirectory, branchName)) + if (ShellViewModel is not { } shellViewModel || + ContentPage is not { } contentPage) + return; + + if (!await GitHelpers.Checkout(shellViewModel.GitDirectory, branchName)) { - _ContentPage.StatusBarViewModel.ShowLocals = true; - _ContentPage.StatusBarViewModel.SelectedBranchIndex = StatusBarViewModel.ACTIVE_BRANCH_INDEX; + contentPage.StatusBarViewModel.ShowLocals = true; + contentPage.StatusBarViewModel.SelectedBranchIndex = StatusBarViewModel.ACTIVE_BRANCH_INDEX; } else { - ContentPage.StatusBarViewModel.UpdateGitInfo( + contentPage.StatusBarViewModel.UpdateGitInfo( InstanceViewModel.IsGitRepository, InstanceViewModel.GitRepositoryPath, await GitHelpers.GetRepositoryHead(InstanceViewModel.GitRepositoryPath)); @@ -340,22 +345,22 @@ protected void ShellPage_PreviewKeyDown(object sender, KeyRoutedEventArgs args) } } - protected void AppSettings_SortDirectionPreferenceUpdated(object sender, SortDirection e) + protected void AppSettings_SortDirectionPreferenceUpdated(object? sender, SortDirection e) { ShellViewModel?.UpdateSortDirectionStatusAsync(); } - protected void AppSettings_SortOptionPreferenceUpdated(object sender, SortOption e) + protected void AppSettings_SortOptionPreferenceUpdated(object? sender, SortOption e) { ShellViewModel?.UpdateSortOptionStatusAsync(); } - protected void AppSettings_SortDirectoriesAlongsideFilesPreferenceUpdated(object sender, bool e) + protected void AppSettings_SortDirectoriesAlongsideFilesPreferenceUpdated(object? sender, bool e) { ShellViewModel?.UpdateSortDirectoriesAlongsideFilesAsync(); } - protected void AppSettings_SortFilesFirstPreferenceUpdated(object sender, bool e) + protected void AppSettings_SortFilesFirstPreferenceUpdated(object? sender, bool e) { ShellViewModel?.UpdateSortFilesFirstAsync(); } @@ -382,7 +387,7 @@ protected async void NavigationToolbar_QuerySubmitted(object sender, ToolbarQuer await ToolbarViewModel.CheckPathInputAsync(e.QueryText, ToolbarViewModel.PathComponents.LastOrDefault()?.Path, this); } - protected async void DrivesManager_PropertyChanged(object sender, PropertyChangedEventArgs e) + protected async void DrivesManager_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == "ShowUserConsentOnInit") await DisplayFilesystemConsentDialogAsync(); @@ -393,13 +398,13 @@ protected async void DrivesManager_PropertyChanged(object sender, PropertyChange // Ensure that the path bar gets updated for user interaction // whenever the path changes.We will get the individual directories from // the updated, most-current path and add them to the UI. - public async Task UpdatePathUIToWorkingDirectoryAsync(string newWorkingDir, string singleItemOverride = null) + public async Task UpdatePathUIToWorkingDirectoryAsync(string? newWorkingDir, string? singleItemOverride = null) { - if (ToolbarViewModel?.PathComponents is null) - return; - if (string.IsNullOrWhiteSpace(singleItemOverride)) { + if (string.IsNullOrWhiteSpace(newWorkingDir)) + return; + cts = new CancellationTokenSource(); var components = await StorageFileExtensions.GetDirectoryPathComponentsWithDisplayNameAsync(newWorkingDir); @@ -445,23 +450,26 @@ public async Task UpdatePathUIToWorkingDirectoryAsync(string newWorkingDir, stri public void SubmitSearch(string query) { - ShellViewModel.CancelSearch(); + if (ShellViewModel is not { } shellViewModel) + return; + + shellViewModel.CancelSearch(); InstanceViewModel.CurrentSearchQuery = query; var args = new NavigationArguments() { AssociatedTabInstance = this, IsSearchResultPage = true, - SearchPathParam = ShellViewModel.WorkingDirectory, + SearchPathParam = shellViewModel.WorkingDirectory, SearchQuery = query, }; - var layout = InstanceViewModel.FolderSettings.GetLayoutType(ShellViewModel.WorkingDirectory); + var layout = InstanceViewModel.FolderSettings.GetLayoutType(shellViewModel.WorkingDirectory); if (layout == typeof(ColumnsLayoutPage)) - NavigateToPath(ShellViewModel.WorkingDirectory, typeof(DetailsLayoutPage), args); + NavigateToPath(shellViewModel.WorkingDirectory, typeof(DetailsLayoutPage), args); else - NavigateToPath(ShellViewModel.WorkingDirectory, layout, args); + NavigateToPath(shellViewModel.WorkingDirectory, layout, args); } public void NavigateWithArguments(Type sourcePageType, NavigationArguments navArgs) @@ -482,17 +490,17 @@ public void NavigateToPath(string navigationPath, NavigationArguments? navArgs = public Task TabItemDragOver(object sender, DragEventArgs e) { - return SlimContentPage?.CommandsViewModel.DragOverAsync(e); + return SlimContentPage?.CommandsViewModel?.DragOverAsync(e) ?? Task.CompletedTask; } public Task TabItemDrop(object sender, DragEventArgs e) { - return SlimContentPage?.CommandsViewModel.DropAsync(e); + return SlimContentPage?.CommandsViewModel?.DropAsync(e) ?? Task.CompletedTask; } public async Task RefreshIfNoWatcherExistsAsync() { - if (ShellViewModel.HasNoWatcher) + if (ShellViewModel is { HasNoWatcher: true }) await Refresh_Click(); } @@ -500,14 +508,17 @@ public async Task Refresh_Click() { if (InstanceViewModel.IsPageTypeSearchResults) { + if (ShellViewModel is not { } shellViewModel) + return; + ToolbarViewModel.CanRefresh = false; var searchInstance = new FolderSearch { - Query = InstanceViewModel.CurrentSearchQuery ?? (string)TabBarItemParameter.NavigationParameter, - Folder = ShellViewModel.WorkingDirectory, + Query = InstanceViewModel.CurrentSearchQuery ?? TabBarItemParameter?.NavigationParameter as string ?? string.Empty, + Folder = shellViewModel.WorkingDirectory, }; - await ShellViewModel.SearchAsync(searchInstance); + await shellViewModel.SearchAsync(searchInstance); } else if (CurrentPageType != typeof(HomePage)) { @@ -589,7 +600,7 @@ public void RaiseContentChanged(IShellPage instance, TabBarItemParameter args) ContentChanged?.Invoke(instance, args); } - protected void FilesystemViewModel_ItemLoadStatusChanged(object sender, ItemLoadStatusChangedEventArgs e) + protected void FilesystemViewModel_ItemLoadStatusChanged(object? sender, ItemLoadStatusChangedEventArgs e) { switch (e.Status) { @@ -618,14 +629,16 @@ protected void FilesystemViewModel_ItemLoadStatusChanged(object sender, ItemLoad ToolbarViewModel.CanRefresh = true; // Select previous directory if (!string.IsNullOrWhiteSpace(e.PreviousDirectory) && + !string.IsNullOrWhiteSpace(e.Path) && e.PreviousDirectory.Contains(e.Path, StringComparison.Ordinal) && !e.PreviousDirectory.Contains(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal)) { + var path = e.Path; // Remove the WorkingDir from previous dir - e.PreviousDirectory = e.PreviousDirectory.Replace(e.Path, string.Empty, StringComparison.Ordinal); + e.PreviousDirectory = e.PreviousDirectory.Replace(path, string.Empty, StringComparison.Ordinal); - var isNetwork = e.Path.StartsWith("\\\\", StringComparison.Ordinal); - var isFtp = FtpHelpers.IsFtpPath(e.Path); + var isNetwork = path.StartsWith("\\\\", StringComparison.Ordinal); + var isFtp = FtpHelpers.IsFtpPath(path); var separator = isFtp ? "/" : "\\"; // Get previous dir name @@ -635,7 +648,7 @@ protected void FilesystemViewModel_ItemLoadStatusChanged(object sender, ItemLoad e.PreviousDirectory = e.PreviousDirectory.Split(separator)[0]; // Get the first folder and combine it with WorkingDir - string folderToSelect = e.Path + separator + e.PreviousDirectory; + string folderToSelect = path + separator + e.PreviousDirectory; // Make sure we don't get double separators in the e.Path folderToSelect = folderToSelect.Replace(separator + separator, separator, StringComparison.Ordinal); @@ -648,7 +661,7 @@ protected void FilesystemViewModel_ItemLoadStatusChanged(object sender, ItemLoad if (folderToSelect.EndsWith(separator)) folderToSelect = folderToSelect.Remove(folderToSelect.Length - 1, 1); - var itemToSelect = ShellViewModel.FilesAndFolders.ToList().FirstOrDefault((item) => item.ItemPath == folderToSelect); + var itemToSelect = ShellViewModel?.FilesAndFolders.ToList().FirstOrDefault((item) => item.ItemPath == folderToSelect); if (itemToSelect is not null && ContentPage is not null && userSettingsService.FoldersSettingsService.ScrollToPreviousFolderWhenNavigatingUp) { @@ -660,7 +673,7 @@ protected void FilesystemViewModel_ItemLoadStatusChanged(object sender, ItemLoad } } - private void FolderSettings_LayoutPreferencesUpdateRequired(object sender, LayoutPreferenceEventArgs e) + private void FolderSettings_LayoutPreferencesUpdateRequired(object? sender, LayoutPreferenceEventArgs e) { if (ShellViewModel is null) return; @@ -670,7 +683,7 @@ private void FolderSettings_LayoutPreferencesUpdateRequired(object sender, Layou AdaptiveLayoutHelpers.ApplyAdaptativeLayout(InstanceViewModel.FolderSettings, ShellViewModel.FilesAndFolders.ToList()); } - protected virtual void ViewModel_WorkingDirectoryModified(object sender, WorkingDirectoryModifiedEventArgs e) + protected virtual void ViewModel_WorkingDirectoryModified(object? sender, WorkingDirectoryModifiedEventArgs e) { } @@ -689,7 +702,7 @@ protected void InitToolbarCommands() ToolbarViewModel.UpdateCommand = new AsyncRelayCommand(async () => await updateSettingsService.DownloadUpdatesAsync()); } - protected async Task GetContentOrNullAsync() + protected async Task GetContentOrNullAsync() { // WINUI3: Make sure not to run this synchronously, do not use EnqueueAsync var tcs = new TaskCompletionSource(); @@ -718,9 +731,9 @@ await DispatcherQueue.EnqueueOrInvokeAsync(async () => } } - protected void SelectSidebarItemFromPath(Type incomingSourcePageType = null) + protected void SelectSidebarItemFromPath(Type? incomingSourcePageType = null) { - if (incomingSourcePageType == typeof(HomePage) && incomingSourcePageType is not null) + if (incomingSourcePageType == typeof(HomePage)) ToolbarViewModel.PathControlDisplayText = Strings.Home.GetLocalizedResource(); } @@ -728,10 +741,14 @@ protected void SetLoadingIndicatorForTabs(bool isLoading) { try { - var multitaskingControls = ((MainWindow.Instance.Content as Frame).Content as MainPage).ViewModel.MultitaskingControls; + if (MainWindow.Instance.Content is not Frame { Content: MainPage mainPage }) + return; - foreach (var x in multitaskingControls) - x.SetLoadingIndicatorStatus(x.Items.FirstOrDefault(x => x.TabItemContent == PaneHolder), isLoading); + foreach (var tabBar in mainPage.ViewModel.MultitaskingControls) + { + if (tabBar.Items.FirstOrDefault(item => item.TabItemContent == PaneHolder) is { } tabItem) + tabBar.SetLoadingIndicatorStatus(tabItem, isLoading); + } } catch (COMException) { @@ -754,8 +771,14 @@ private void HandleBackForwardRequest(PageStackEntry pageContent) incomingPageNavPath.IsLayoutSwitch = false; // Update layout type - if (pageContent.SourcePageType != typeof(HomePage)) - InstanceViewModel.FolderSettings.GetLayoutType(incomingPageNavPath.IsSearchResultPage ? incomingPageNavPath.SearchPathParam : incomingPageNavPath.NavPathParam); + if (pageContent.SourcePageType != typeof(HomePage) && incomingPageNavPath is not null) + { + var path = incomingPageNavPath.IsSearchResultPage + ? incomingPageNavPath.SearchPathParam + : incomingPageNavPath.NavPathParam; + if (path is not null) + InstanceViewModel.FolderSettings.GetLayoutType(path); + } SelectSidebarItemFromPath(pageContent.SourcePageType); } diff --git a/src/Files.App/Views/Shells/ColumnShellPage.xaml.cs b/src/Files.App/Views/Shells/ColumnShellPage.xaml.cs index 0904d2ee477b..c98baeb07bce 100644 --- a/src/Files.App/Views/Shells/ColumnShellPage.xaml.cs +++ b/src/Files.App/Views/Shells/ColumnShellPage.xaml.cs @@ -23,8 +23,8 @@ public override bool CanNavigateForward protected override Frame ItemDisplay => ItemDisplayFrame; - private ColumnParam _ColumnParams; - public ColumnParam ColumnParams + private ColumnParam? _ColumnParams; + public ColumnParam? ColumnParams { get => _ColumnParams; set @@ -66,38 +66,32 @@ protected override void OnNavigationParamsChanged() if (ColumnParams.NavPathParam is not null) // This method call is required to load the sorting preferences. - InstanceViewModel?.FolderSettings?.GetLayoutType(ColumnParams.NavPathParam); + InstanceViewModel.FolderSettings.GetLayoutType(ColumnParams.NavPathParam); // Add null check for ItemDisplayFrame - if (ItemDisplayFrame != null) - { - ItemDisplayFrame.Navigate( - typeof(ColumnLayoutPage), - new NavigationArguments() - { - IsSearchResultPage = ColumnParams.IsSearchResultPage, - SearchQuery = ColumnParams.SearchQuery, - NavPathParam = ColumnParams.NavPathParam, - SearchPathParam = ColumnParams.SearchPathParam, - AssociatedTabInstance = this, - SelectItems = ColumnParams.SelectItems - }); - } + ItemDisplayFrame.Navigate( + typeof(ColumnLayoutPage), + new NavigationArguments() + { + IsSearchResultPage = ColumnParams.IsSearchResultPage, + SearchQuery = ColumnParams.SearchQuery, + NavPathParam = ColumnParams.NavPathParam, + SearchPathParam = ColumnParams.SearchPathParam, + AssociatedTabInstance = this, + SelectItems = ColumnParams.SelectItems + }); } protected override void Page_Loaded(object sender, RoutedEventArgs e) { // Add null check for InstanceViewModel - if (InstanceViewModel?.FolderSettings != null) - { - ShellViewModel = new ShellViewModel(InstanceViewModel.FolderSettings); - ShellViewModel.WorkingDirectoryModified += ViewModel_WorkingDirectoryModified; - ShellViewModel.ItemLoadStatusChanged += FilesystemViewModel_ItemLoadStatusChanged; - ShellViewModel.DirectoryInfoUpdated += FilesystemViewModel_DirectoryInfoUpdated; - ShellViewModel.PageTypeUpdated += FilesystemViewModel_PageTypeUpdated; - ShellViewModel.OnSelectionRequestedEvent += FilesystemViewModel_OnSelectionRequestedEvent; - ShellViewModel.GitDirectoryUpdated += FilesystemViewModel_GitDirectoryUpdated; - } + ShellViewModel = new ShellViewModel(InstanceViewModel.FolderSettings); + ShellViewModel.WorkingDirectoryModified += ViewModel_WorkingDirectoryModified; + ShellViewModel.ItemLoadStatusChanged += FilesystemViewModel_ItemLoadStatusChanged; + ShellViewModel.DirectoryInfoUpdated += FilesystemViewModel_DirectoryInfoUpdated; + ShellViewModel.PageTypeUpdated += FilesystemViewModel_PageTypeUpdated; + ShellViewModel.OnSelectionRequestedEvent += FilesystemViewModel_OnSelectionRequestedEvent; + ShellViewModel.GitDirectoryUpdated += FilesystemViewModel_GitDirectoryUpdated; PaneHolder = this.FindAscendant()?.ParentShellPageInstance?.PaneHolder; @@ -120,12 +114,12 @@ private void Page_Unloaded(object sender, RoutedEventArgs e) } } - protected override async void ViewModel_WorkingDirectoryModified(object sender, WorkingDirectoryModifiedEventArgs e) + protected override async void ViewModel_WorkingDirectoryModified(object? sender, WorkingDirectoryModifiedEventArgs e) { // Add null check for e and e.Path if (e == null) return; - string value = e.Path; + string? value = e.Path; if (!string.IsNullOrWhiteSpace(value)) await UpdatePathUIToWorkingDirectoryAsync(value); } @@ -181,11 +175,13 @@ public override void Up_Click() this.FindAscendant()?.NavigateUp(); } - public override void NavigateToPath(string navigationPath, Type sourcePageType, NavigationArguments navArgs = null) + public override void NavigateToPath(string? navigationPath, Type? sourcePageType, NavigationArguments? navArgs = null) { if (string.IsNullOrEmpty(navigationPath)) return; + sourcePageType ??= InstanceViewModel.FolderSettings.GetLayoutType(navigationPath); + var columnsLayoutPage = this.FindAscendant(); if (columnsLayoutPage != null) { @@ -240,4 +236,4 @@ public override Task WhenIsCurrent() return Task.WhenAll(_IsCurrentInstanceTCS.Task, parentTask ?? Task.CompletedTask); } } -} \ No newline at end of file +} diff --git a/src/Files.App/Views/Shells/ModernShellPage.xaml.cs b/src/Files.App/Views/Shells/ModernShellPage.xaml.cs index 5aaa27a10b33..2e8d65bb6656 100644 --- a/src/Files.App/Views/Shells/ModernShellPage.xaml.cs +++ b/src/Files.App/Views/Shells/ModernShellPage.xaml.cs @@ -18,8 +18,8 @@ protected override Frame ItemDisplay private NavigationInteractionTracker _navigationInteractionTracker; - private NavigationParams _NavParams; - public NavigationParams NavParams + private NavigationParams? _NavParams; + public NavigationParams? NavParams { get => _NavParams; set @@ -54,7 +54,7 @@ public ModernShellPage() : base(new CurrentInstanceViewModel()) _navigationInteractionTracker.NavigationRequested += OverscrollNavigationRequested; } - private async void ShellViewModel_FocusFilterHeader(object sender, EventArgs e) + private async void ShellViewModel_FocusFilterHeader(object? sender, EventArgs e) { // Delay to ensure the UI is ready for focus await Task.Delay(100); @@ -62,7 +62,7 @@ private async void ShellViewModel_FocusFilterHeader(object sender, EventArgs e) FilterTextBox.Focus(FocusState.Programmatic); } - private void ModernShellPage_RefreshWidgetsRequested(object sender, EventArgs e) + private void ModernShellPage_RefreshWidgetsRequested(object? sender, EventArgs e) { if (ItemDisplayFrame?.Content is HomePage currentPage) currentPage.ViewModel.RefreshWidgetList(); @@ -90,37 +90,38 @@ protected override void ShellPage_NavigationRequested(object sender, PathNavigat protected override void OnNavigationParamsChanged() { - if (string.IsNullOrEmpty(NavParams?.NavPath) || NavParams.NavPath == "Home") + var navParams = NavParams; + if (string.IsNullOrEmpty(navParams?.NavPath) || navParams.NavPath == "Home") { NavigateHome(); } - else if (NavParams.NavPath == "ReleaseNotes") + else if (navParams.NavPath == "ReleaseNotes") { NavigateToReleaseNotes(); } - else if (NavParams.NavPath == "Settings") + else if (navParams.NavPath == "Settings") { - NavigateToSettings(NavParams?.SelectItem); + NavigateToSettings(navParams.SelectItem); } else { - var isTagSearch = NavParams.NavPath.StartsWith("tag:"); + var isTagSearch = navParams.NavPath.StartsWith("tag:"); ItemDisplayFrame.Navigate( - InstanceViewModel.FolderSettings.GetLayoutType(NavParams.NavPath), + InstanceViewModel.FolderSettings.GetLayoutType(navParams.NavPath), new NavigationArguments() { - NavPathParam = NavParams.NavPath, - SelectItems = !string.IsNullOrWhiteSpace(NavParams?.SelectItem) ? new[] { NavParams.SelectItem } : null, + NavPathParam = navParams.NavPath, + SelectItems = !string.IsNullOrWhiteSpace(navParams.SelectItem) ? [navParams.SelectItem] : null, IsSearchResultPage = isTagSearch, SearchPathParam = isTagSearch ? "Home" : null, - SearchQuery = isTagSearch ? NavParams.NavPath : null, + SearchQuery = isTagSearch ? navParams.NavPath : null, AssociatedTabInstance = this }); } } - protected override async void ViewModel_WorkingDirectoryModified(object sender, WorkingDirectoryModifiedEventArgs e) + protected override async void ViewModel_WorkingDirectoryModified(object? sender, WorkingDirectoryModifiedEventArgs e) { if (e is null || string.IsNullOrWhiteSpace(e.Path)) return; @@ -134,6 +135,8 @@ protected override async void ViewModel_WorkingDirectoryModified(object sender, private async void ItemDisplayFrame_Navigated(object sender, NavigationEventArgs e) { ContentPage = await GetContentOrNullAsync(); + if (ContentPage is null || e.Parameter is not NavigationArguments parameters) + return; ToolbarViewModel.UpdateAdditionalActions(); if (ItemDisplayFrame.CurrentSourcePageType == (typeof(DetailsLayoutPage)) @@ -143,7 +146,6 @@ private async void ItemDisplayFrame_Navigated(object sender, NavigationEventArgs ContentPage.ResetItemOpacity(); } - var parameters = e.Parameter as NavigationArguments; var isTagSearch = parameters.NavPathParam is not null && parameters.NavPathParam.StartsWith("tag:"); TabBarItemParameter = new() { @@ -156,8 +158,8 @@ private async void ItemDisplayFrame_Navigated(object sender, NavigationEventArgs // Update the ShellViewModel with the current working directory // Fixes https://github.com/files-community/Files/issues/17469 - if (parameters.IsSearchResultPage == false) - ShellViewModel.IsSearchResults = false; + if (parameters.IsSearchResultPage == false && ShellViewModel is { } shellViewModel) + shellViewModel.IsSearchResults = false; _navigationInteractionTracker.CanNavigateBackward = CanNavigateBackward; _navigationInteractionTracker.CanNavigateForward = CanNavigateForward; @@ -288,7 +290,8 @@ public override void NavigateToSettings(string? selectItem = null) public override void NavigateToPath(string? navigationPath, Type? sourcePageType, NavigationArguments? navArgs = null) { - ShellViewModel.FilesAndFoldersFilter = null; + if (ShellViewModel is { } shellViewModel) + shellViewModel.FilesAndFoldersFilter = null; if (sourcePageType is null && !string.IsNullOrEmpty(navigationPath)) sourcePageType = InstanceViewModel.FolderSettings.GetLayoutType(navigationPath); @@ -330,7 +333,7 @@ public override void NavigateToPath(string? navigationPath, Type? sourcePageType new SuppressNavigationTransitionInfo()); } - ToolbarViewModel.PathControlDisplayText = ShellViewModel.WorkingDirectory; + ToolbarViewModel.PathControlDisplayText = ShellViewModel?.WorkingDirectory ?? string.Empty; } private void FilterTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e) diff --git a/src/Files.Shared/Extensions/GenericExtensions.cs b/src/Files.Shared/Extensions/GenericExtensions.cs index f251dd4d6e23..30b4ae7f0c74 100644 --- a/src/Files.Shared/Extensions/GenericExtensions.cs +++ b/src/Files.Shared/Extensions/GenericExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Files.Shared.Extensions @@ -13,5 +14,15 @@ public static class GenericExtensions return defaultValue is not null ? defaultValue() : default; } + + public static IEnumerable WhereNotNull(this IEnumerable source) + where T : class + { + foreach (var item in source) + { + if (item is not null) + yield return item; + } + } } } diff --git a/src/Files.Shared/Helpers/FileExtensionHelpers.cs b/src/Files.Shared/Helpers/FileExtensionHelpers.cs index 528b5e14dd9a..2d3bc5664344 100644 --- a/src/Files.Shared/Helpers/FileExtensionHelpers.cs +++ b/src/Files.Shared/Helpers/FileExtensionHelpers.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -120,7 +121,7 @@ public static bool IsZipFile(string? fileExtensionToCheck) return HasExtension(fileExtensionToCheck, ".zip", ".msix", ".appx", ".msixbundle", ".appxbundle", ".7z", ".rar", ".tar", ".mcpack", ".mcworld", ".mrpack", ".jar", ".gz", ".lzh"); } - public static bool IsBrowsableZipFile(string? filePath, out string? ext) + public static bool IsBrowsableZipFile(string? filePath, [NotNullWhen(true)] out string? ext) { if (string.IsNullOrWhiteSpace(filePath)) { From 4850693b84cb095b79b2e5214893eab0ab951557 Mon Sep 17 00:00:00 2001 From: Steven He Date: Thu, 30 Jul 2026 23:51:28 +0900 Subject: [PATCH 02/14] Fix msbuild warnings --- src/Satori.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Satori.targets b/src/Satori.targets index f0e6deec9ae4..3dc08c0991c0 100644 --- a/src/Satori.targets +++ b/src/Satori.targets @@ -39,8 +39,8 @@ + Condition=" (('%(RuntimePackAsset.Filename)' == 'coreclr' Or '%(RuntimePackAsset.Filename)' == 'clrjit') And '$(UseSatoriNativeRuntimeOverride)' == 'true') + Or (('%(RuntimePackAsset.Filename)' == 'System.Private.CoreLib' And '$(UseSatoriCoreLibOverride)' == 'true')) " /> runtime From 2e5be9638d13ea8b51c40faff2f999a68bc7ffe2 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 31 Jul 2026 00:00:46 +0900 Subject: [PATCH 03/14] Address various additional warnings --- src/Files.App/Actions/FileSystem/DeleteItemAction.cs | 2 +- .../Actions/FileSystem/DeleteItemPermanentlyAction.cs | 2 +- src/Files.App/Data/Contracts/INavigationControlItem.cs | 4 +--- src/Files.App/Helpers/Win32/Win32Helper.Shell.cs | 2 +- .../Utils/Storage/Enumerators/Win32StorageEnumerator.cs | 1 - .../ViewModels/Dialogs/CreateShortcutDialogViewModel.cs | 2 +- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Files.App/Actions/FileSystem/DeleteItemAction.cs b/src/Files.App/Actions/FileSystem/DeleteItemAction.cs index a97514bd4a77..6e5071601bcf 100644 --- a/src/Files.App/Actions/FileSystem/DeleteItemAction.cs +++ b/src/Files.App/Actions/FileSystem/DeleteItemAction.cs @@ -12,7 +12,7 @@ public string Label public string Description => Strings.DeleteItemDescription.GetLocalizedFormatResource(context.SelectedItems.Count); - public ActionCategory Category + public override ActionCategory Category => ActionCategory.FileSystem; public RichGlyph Glyph diff --git a/src/Files.App/Actions/FileSystem/DeleteItemPermanentlyAction.cs b/src/Files.App/Actions/FileSystem/DeleteItemPermanentlyAction.cs index a7d1845da3d0..9ddd3185f320 100644 --- a/src/Files.App/Actions/FileSystem/DeleteItemPermanentlyAction.cs +++ b/src/Files.App/Actions/FileSystem/DeleteItemPermanentlyAction.cs @@ -12,7 +12,7 @@ public string Label public string Description => Strings.DeleteItemPermanentlyDescription.GetLocalizedFormatResource(context.SelectedItems.Count); - public ActionCategory Category + public override ActionCategory Category => ActionCategory.FileSystem; public string AccessKey diff --git a/src/Files.App/Data/Contracts/INavigationControlItem.cs b/src/Files.App/Data/Contracts/INavigationControlItem.cs index da7f64839e41..e4f6b4146426 100644 --- a/src/Files.App/Data/Contracts/INavigationControlItem.cs +++ b/src/Files.App/Data/Contracts/INavigationControlItem.cs @@ -7,9 +7,7 @@ namespace Files.App.Data.Contracts { public interface INavigationControlItem : IComparable, INotifyPropertyChanged, ISidebarItemModel { - public new string Text { get; } - - public string Path { get; } + public string Text { get; } public SectionType Section { get; } diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs index 27c21897a5ec..f64955b12e87 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Shell.cs @@ -86,7 +86,7 @@ public static partial class Win32Helper public static unsafe string GetFolderFromKnownFolderGUID(Guid guid) { PWSTR pszPath; - PInvoke.SHGetKnownFolderPath(ref guid, (KNOWN_FOLDER_FLAG)0, null, out pszPath); + PInvoke.SHGetKnownFolderPath(in guid, (KNOWN_FOLDER_FLAG)0, null, out pszPath); string path = pszPath.ToString(); Marshal.FreeCoTaskMem((nint)pszPath.Value); diff --git a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs index db3169e2bc5e..b220ad6a04b4 100644 --- a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs +++ b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs @@ -267,7 +267,6 @@ CancellationToken cancellationToken } bool itemThumbnailImgVis = false; - bool itemEmptyImgVis = true; if (cancellationToken.IsCancellationRequested) return null; diff --git a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs index bfa1e0d8006c..98b932857e3d 100644 --- a/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/CreateShortcutDialogViewModel.cs @@ -242,7 +242,7 @@ private unsafe Task SelectDestination() { bi.pszDisplayName = displayNameBuffer; bi.lpszTitle = title; - var pidl = PInvoke.SHBrowseForFolder(ref bi); + var pidl = PInvoke.SHBrowseForFolder(in bi); if (pidl is not null) { Span path = stackalloc char[260]; From 55a4e33f31143d6688013e175f29d51ff010153d Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 31 Jul 2026 00:03:21 +0900 Subject: [PATCH 04/14] Bump toolset to v145 --- src/Files.App.Launcher/Files.App.Launcher.vcxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App.Launcher/Files.App.Launcher.vcxproj b/src/Files.App.Launcher/Files.App.Launcher.vcxproj index 93862737d1f1..65d562e1a2ca 100644 --- a/src/Files.App.Launcher/Files.App.Launcher.vcxproj +++ b/src/Files.App.Launcher/Files.App.Launcher.vcxproj @@ -45,7 +45,7 @@ Application false - v143 + v145 true Unicode false From 4a043015db3574d7ccfff1b59dac599b9267cb31 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 31 Jul 2026 00:40:56 +0900 Subject: [PATCH 05/14] Fix XAML warnings --- .../Sidebar/ISidebarItemModel.cs | 10 ++++ .../Sidebar/SidebarItem.Properties.cs | 16 ++++- src/Files.App.Controls/Sidebar/SidebarItem.cs | 58 +++++++++++++++++++ .../Sidebar/SidebarStyles.xaml | 22 +------ .../Sidebar/SidebarView.xaml | 15 +++++ .../Data/Contracts/INavigationControlItem.cs | 2 - src/Files.App/Data/Items/DriveItem.cs | 2 + src/Files.App/Data/Items/FileTagItem.cs | 5 ++ src/Files.App/Data/Items/LocationItem.cs | 2 + src/Files.App/Data/Items/WslDistroItem.cs | 5 ++ .../UserControls/NavigationToolbar.xaml | 10 ++-- src/Files.App/UserControls/Pane/InfoPane.xaml | 6 +- .../Properties/MainPropertiesViewModel.cs | 2 + .../Settings/SettingsPageViewModel.cs | 4 +- .../Views/Layouts/ColumnLayoutPage.xaml | 2 +- .../Views/Layouts/DetailsLayoutPage.xaml | 10 ++-- .../Views/Layouts/DetailsLayoutPage.xaml.cs | 8 +-- .../Views/Layouts/GridLayoutPage.xaml | 6 +- .../Views/Properties/CustomizationPage.xaml | 1 - .../Views/Settings/AppearancePage.xaml | 8 +-- src/Files.App/Views/Settings/TagsPage.xaml | 2 +- 21 files changed, 144 insertions(+), 52 deletions(-) diff --git a/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs b/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs index eedd91e8f4d1..ba415a14a905 100644 --- a/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs +++ b/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs @@ -1,10 +1,20 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using Microsoft.UI.Xaml; + namespace Files.App.Controls { public interface ISidebarItemModel : INotifyPropertyChanged { + string Text { get; } + + object? ToolTip { get; } + + FrameworkElement? IconElement { get; } + + FrameworkElement? ItemDecorator { get; } + /// /// The children of this item that will be rendered as child elements of the SidebarItem /// diff --git a/src/Files.App.Controls/Sidebar/SidebarItem.Properties.cs b/src/Files.App.Controls/Sidebar/SidebarItem.Properties.cs index 6d6565235a6a..1ea83c56edc9 100644 --- a/src/Files.App.Controls/Sidebar/SidebarItem.Properties.cs +++ b/src/Files.App.Controls/Sidebar/SidebarItem.Properties.cs @@ -83,7 +83,15 @@ public ISidebarItemModel? Item set { SetValue(ItemProperty, value); } } public static readonly DependencyProperty ItemProperty = - DependencyProperty.Register(nameof(Item), typeof(ISidebarItemModel), typeof(SidebarItem), new PropertyMetadata(null)); + DependencyProperty.Register(nameof(Item), typeof(ISidebarItemModel), typeof(SidebarItem), new PropertyMetadata(null, OnPropertyChanged)); + + public bool UseItemPresentation + { + get { return (bool)GetValue(UseItemPresentationProperty); } + set { SetValue(UseItemPresentationProperty, value); } + } + public static readonly DependencyProperty UseItemPresentationProperty = + DependencyProperty.Register(nameof(UseItemPresentation), typeof(bool), typeof(SidebarItem), new PropertyMetadata(false, OnPropertyChanged)); public bool UseReorderDrop { @@ -136,12 +144,18 @@ public static void OnPropertyChanged(DependencyObject sender, DependencyProperty } else if (e.Property == IsExpandedProperty) { + if (item.UseItemPresentation && item.Item is { } model && model.IsExpanded != item.IsExpanded) + model.IsExpanded = item.IsExpanded; item.UpdateExpansionState(); } else if (e.Property == ItemProperty) { item.HandleItemChange(); } + else if (e.Property == UseItemPresentationProperty && item.UseItemPresentation) + { + item.UpdateItemPresentation(); + } else { Debug.Write(e.Property.ToString()); diff --git a/src/Files.App.Controls/Sidebar/SidebarItem.cs b/src/Files.App.Controls/Sidebar/SidebarItem.cs index 11ceb259ad43..cd7f51a32188 100644 --- a/src/Files.App.Controls/Sidebar/SidebarItem.cs +++ b/src/Files.App.Controls/Sidebar/SidebarItem.cs @@ -90,7 +90,10 @@ protected override void OnApplyTemplate() if (GetTemplateChild("ChevronContainer") is Border chevronContainer) chevronContainer.PointerPressed += ChevronContainer_PointerPressed; if (GetTemplateChild("FlyoutChildrenPresenter") is ItemsRepeater flyoutRepeater) + { flyoutRepeater.ElementPrepared += FlyoutChildrenPresenter_ElementPrepared; + flyoutRepeater.ItemsSource = Item?.Children; + } } if (Owner is null) @@ -124,11 +127,30 @@ private void SidebarItem_Loaded(object sender, RoutedEventArgs e) public void HandleItemChange() { HookupItemChangeListener(null, Item); + if (UseItemPresentation) + UpdateItemPresentation(); + UpdateFlyoutChildrenSource(); UpdateExpansionState(); ReevaluateSelection(); CanDrag = Item?.Path is string path && Path.IsPathRooted(path); } + private void UpdateItemPresentation() + { + AutomationProperties.SetAutomationId(this, Item?.Text ?? string.Empty); + Text = Item?.Text; + ToolTip = Item?.ToolTip; + Icon = Item?.IconElement; + Decorator = Item?.ItemDecorator; + IsExpanded = Item?.IsExpanded ?? true; + } + + private void UpdateFlyoutChildrenSource() + { + if (GetTemplateChild("FlyoutChildrenPresenter") is ItemsRepeater flyoutRepeater) + flyoutRepeater.ItemsSource = Item?.Children; + } + private void HookupOwners() { // Owner is pushed in by the hosting SidebarView's MenuItemsHost_ElementPrepared (top-level rows) or the parent SidebarItem's FlyoutChildrenPresenter_ElementPrepared (flyout children) before Loaded fires. Static SidebarItems declared directly in XAML (MainPage's SettingsButton in SidebarView.Footer) aren't realized through either path, so resolve Owner via a visual-tree walk for them. OwnerExpansionSupport state is applied by OnOwnerChanged. @@ -181,9 +203,45 @@ private void Item_PropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { + case null: + case "": + if (UseItemPresentation) + UpdateItemPresentation(); + UpdateFlyoutChildrenSource(); + UpdateExpansionState(); + ReevaluateSelection(); + break; + case nameof(ISidebarItemModel.Text): + if (UseItemPresentation) + { + Text = Item?.Text; + AutomationProperties.SetAutomationId(this, Item?.Text ?? string.Empty); + } + break; + case nameof(ISidebarItemModel.ToolTip): + if (UseItemPresentation) + ToolTip = Item?.ToolTip; + break; + case nameof(ISidebarItemModel.IconElement): + if (UseItemPresentation) + Icon = Item?.IconElement; + break; + case nameof(ISidebarItemModel.ItemDecorator): + if (UseItemPresentation) + Decorator = Item?.ItemDecorator; + break; + case nameof(ISidebarItemModel.IsExpanded): + if (UseItemPresentation) + IsExpanded = Item?.IsExpanded ?? true; + UpdateExpansionState(); + break; case nameof(ISidebarItemModel.HasUnrealizedChildren): case nameof(ISidebarItemModel.IsLeafWithChildren): + UpdateExpansionState(); + ReevaluateSelection(); + break; case nameof(ISidebarItemModel.Children): + UpdateFlyoutChildrenSource(); UpdateExpansionState(); ReevaluateSelection(); break; diff --git a/src/Files.App.Controls/Sidebar/SidebarStyles.xaml b/src/Files.App.Controls/Sidebar/SidebarStyles.xaml index 004f6683cf04..b3ab9b44b1e8 100644 --- a/src/Files.App.Controls/Sidebar/SidebarStyles.xaml +++ b/src/Files.App.Controls/Sidebar/SidebarStyles.xaml @@ -36,20 +36,6 @@ - - - -