From 7c9fd596e2245f4e99390e6be0d936955d092f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 20:59:37 +0300 Subject: [PATCH 1/4] WIP P4-G: previews and leaf storage users on storables --- src/Files.App/Data/Items/IListedItem.cs | 4 + .../Data/Items/ListedItem.Windows.cs | 24 ++ src/Files.App/Data/Items/ListedItem.cs | 23 +- .../Helpers/ShareItemHelpers.Windows.cs | 105 +++++++++ src/Files.App/Helpers/ShareItemHelpers.cs | 96 +------- .../Helpers/TransferHelpers.Windows.cs | 212 ++++++++++++++++++ src/Files.App/Helpers/TransferHelpers.cs | 212 ++---------------- .../Helpers/UI/UIFilesystemHelpers.cs | 5 +- src/Files.App/Services/Git/LibGit2Service.cs | 4 +- .../UserControls/Pane/ShelfPane.xaml.cs | 14 +- .../Previews/BasePreviewModel.Windows.cs | 41 ++++ .../UserControls/Previews/BasePreviewModel.cs | 71 +++--- ...del.cs => CodePreviewViewModel.Windows.cs} | 0 .../Previews/FolderPreviewViewModel.cs | 28 ++- .../Previews/ImagePreviewViewModel.cs | 2 +- ...cs => MarkdownPreviewViewModel.Windows.cs} | 0 .../Previews/MediaMetadataPreviewViewModel.cs | 2 +- ...el.cs => MediaPreviewViewModel.Windows.cs} | 0 ...odel.cs => PDFPreviewViewModel.Windows.cs} | 0 ...cs => RichTextPreviewViewModel.Windows.cs} | 0 ...el.cs => ShellPreviewViewModel.Windows.cs} | 0 .../Previews/TextPreviewViewModel.Windows.cs | 77 +++++++ .../Previews/TextPreviewViewModel.cs | 88 +------- 23 files changed, 573 insertions(+), 435 deletions(-) create mode 100644 src/Files.App/Data/Items/ListedItem.Windows.cs create mode 100644 src/Files.App/Helpers/ShareItemHelpers.Windows.cs create mode 100644 src/Files.App/Helpers/TransferHelpers.Windows.cs create mode 100644 src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.Windows.cs rename src/Files.App/ViewModels/UserControls/Previews/{CodePreviewViewModel.cs => CodePreviewViewModel.Windows.cs} (100%) rename src/Files.App/ViewModels/UserControls/Previews/{MarkdownPreviewViewModel.cs => MarkdownPreviewViewModel.Windows.cs} (100%) rename src/Files.App/ViewModels/UserControls/Previews/{MediaPreviewViewModel.cs => MediaPreviewViewModel.Windows.cs} (100%) rename src/Files.App/ViewModels/UserControls/Previews/{PDFPreviewViewModel.cs => PDFPreviewViewModel.Windows.cs} (100%) rename src/Files.App/ViewModels/UserControls/Previews/{RichTextPreviewViewModel.cs => RichTextPreviewViewModel.Windows.cs} (100%) rename src/Files.App/ViewModels/UserControls/Previews/{ShellPreviewViewModel.cs => ShellPreviewViewModel.Windows.cs} (100%) create mode 100644 src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.Windows.cs diff --git a/src/Files.App/Data/Items/IListedItem.cs b/src/Files.App/Data/Items/IListedItem.cs index b7a17f7672fd..5cd90b468ac7 100644 --- a/src/Files.App/Data/Items/IListedItem.cs +++ b/src/Files.App/Data/Items/IListedItem.cs @@ -53,7 +53,11 @@ public interface IListedItem DateTimeOffset ItemDateCreatedReal { get; set; } string? ItemDateModified { get; } DateTimeOffset ItemDateModifiedReal { get; set; } +#if WINDOWS BaseStorageFile? ItemFile { get; set; } +#else + IFile? ItemFile { get; set; } +#endif string? ItemNameRaw { get; set; } string? ItemPath { get; set; } ObservableCollection? ItemProperties { get; set; } diff --git a/src/Files.App/Data/Items/ListedItem.Windows.cs b/src/Files.App/Data/Items/ListedItem.Windows.cs new file mode 100644 index 000000000000..431125a7d774 --- /dev/null +++ b/src/Files.App/Data/Items/ListedItem.Windows.cs @@ -0,0 +1,24 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.IO; +using Windows.Storage; + +namespace Files.App.Utils +{ + public sealed partial class FtpItem + { + public async Task ToStorageItem() + { + var path = this.GetRequiredPath(); + var name = ItemNameRaw ?? throw new InvalidOperationException("The FTP item does not have a name."); + + return PrimaryItemAttribute switch + { + StorageItemTypes.File => await new Utils.Storage.FtpStorageFile(path, name, ItemDateCreatedReal).ToStorageFileAsync(), + StorageItemTypes.Folder => new Utils.Storage.FtpStorageFolder(path, name, ItemDateCreatedReal), + _ => throw new InvalidDataException("The FTP item has an unsupported storage type."), + }; + } + } +} diff --git a/src/Files.App/Data/Items/ListedItem.cs b/src/Files.App/Data/Items/ListedItem.cs index 4ee7941ebec5..8ca68abc565a 100644 --- a/src/Files.App/Data/Items/ListedItem.cs +++ b/src/Files.App/Data/Items/ListedItem.cs @@ -10,7 +10,6 @@ using System.IO; using System.Text; using Windows.Storage; -using Windows.Win32.UI.WindowsAndMessaging; using ByteSize = ByteSizeLib.ByteSize; #pragma warning disable CS0618 // Type or member is obsolete @@ -496,8 +495,13 @@ public override string ToString() public bool IsDriveRoot => ItemPath == PathNormalization.GetPathRoot(ItemPath); public bool IsElevationRequired { get; set; } +#if WINDOWS private BaseStorageFile? itemFile; public BaseStorageFile? ItemFile +#else + private IFile? itemFile; + public IFile? ItemFile +#endif { get => itemFile; set => SetProperty(ref itemFile, value); @@ -607,18 +611,7 @@ public FtpItem(FtpListItem item, string folder) : base(null) IsHiddenItem = false; } - public async Task ToStorageItem() - { - var path = this.GetRequiredPath(); - var name = ItemNameRaw ?? throw new InvalidOperationException("The FTP item does not have a name."); - return PrimaryItemAttribute switch - { - StorageItemTypes.File => await new Utils.Storage.FtpStorageFile(path, name, ItemDateCreatedReal).ToStorageFileAsync(), - StorageItemTypes.Folder => new Utils.Storage.FtpStorageFolder(path, name, ItemDateCreatedReal), - _ => throw new InvalidDataException("The FTP item has an unsupported storage type."), - }; - } } public sealed partial class ShortcutItem : ListedItem, IShortcutItem @@ -640,7 +633,7 @@ public override string? Name public string? Arguments { get; set; } public string? WorkingDirectory { get; set; } public bool RunAsAdmin { get; set; } - public SHOW_WINDOW_CMD ShowWindowCommand { get; set; } + public ShowWindowCommand ShowWindowCommand { get; set; } public bool IsUrl { get; set; } public bool IsSymLink { get; set; } public override bool IsScriptFile => FileExtensionHelpers.IsScriptFile(TargetPath); @@ -842,7 +835,7 @@ public override string Name public string? Arguments { get; set; } public string? WorkingDirectory { get; set; } public bool RunAsAdmin { get; set; } - public SHOW_WINDOW_CMD ShowWindowCommand { get; set; } + public ShowWindowCommand ShowWindowCommand { get; set; } public bool IsUrl { get; set; } public bool IsSymLink { get; set; } public override bool IsScriptFile => FileExtensionHelpers.IsScriptFile(TargetPath); @@ -875,7 +868,7 @@ public interface IShortcutItem : IListedItem public string? Arguments { get; set; } public string? WorkingDirectory { get; set; } public bool RunAsAdmin { get; set; } - public SHOW_WINDOW_CMD ShowWindowCommand { get; set; } + public ShowWindowCommand ShowWindowCommand { get; set; } public bool IsUrl { get; set; } public bool IsSymLink { get; set; } } diff --git a/src/Files.App/Helpers/ShareItemHelpers.Windows.cs b/src/Files.App/Helpers/ShareItemHelpers.Windows.cs new file mode 100644 index 000000000000..5d4ef13f04c3 --- /dev/null +++ b/src/Files.App/Helpers/ShareItemHelpers.Windows.cs @@ -0,0 +1,105 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation.Metadata; +using Windows.Storage; + +namespace Files.App.Helpers +{ + public static partial class ShareItemHelpers + { + public static async Task ShareItemsAsync(IEnumerable itemsToShare) + { + if (itemsToShare is null) + return; + + var interop = DataTransferManager.As(); + IntPtr result = interop.GetForWindow(MainWindow.Instance.WindowHandle, Win32PInvoke.DataTransferManagerInteropIID); + + var manager = WinRT.MarshalInterface.FromAbi(result); + manager.DataRequested += Manager_DataRequested; + + try + { + interop.ShowShareUIForWindow(MainWindow.Instance.WindowHandle); + } + catch (Exception ex) + { + manager.DataRequested -= Manager_DataRequested; + + var errorDialog = new ContentDialog() + { + Title = Strings.FaildToShareItems.GetLocalizedResource(), + Content = ex.Message, + PrimaryButtonText = Strings.OK.GetLocalizedResource(), + }; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + errorDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + await errorDialog.TryShowAsync(); + } + + async void Manager_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) + { + sender.DataRequested -= Manager_DataRequested; + + DataRequestDeferral dataRequestDeferral = args.Request.GetDeferral(); + List items = []; + DataRequest dataRequest = args.Request; + + foreach (ListedItem item in itemsToShare) + { + if (item is IShortcutItem shItem) + { + if (shItem.IsLinkItem && !string.IsNullOrEmpty(shItem.TargetPath)) + { + dataRequest.Data.Properties.Title = string.Format(Strings.ShareDialogTitle.GetLocalizedResource(), item.Name); + dataRequest.Data.Properties.Description = Strings.ShareDialogSingleItemDescription.GetLocalizedResource(); + dataRequest.Data.SetWebLink(new Uri(shItem.TargetPath)); + dataRequestDeferral.Complete(); + + return; + } + } + else if (item.PrimaryItemAttribute == StorageItemTypes.Folder && !item.IsArchive) + { + if (await StorageHelpers.ToStorageItem(item.ItemPath!) is BaseStorageFolder folder) + items.Add(folder); + } + else + { + if (await StorageHelpers.ToStorageItem(item.ItemPath!) is BaseStorageFile file) + items.Add(file); + } + } + + if (items.Count == 1) + { + dataRequest.Data.Properties.Title = string.Format(Strings.ShareDialogTitle.GetLocalizedResource(), items.First().Name); + dataRequest.Data.Properties.Description = Strings.ShareDialogSingleItemDescription.GetLocalizedResource(); + } + else if (items.Count == 0) + { + dataRequest.FailWithDisplayText(Strings.ShareDialogFailMessage.GetLocalizedResource()); + dataRequestDeferral.Complete(); + + return; + } + else + { + dataRequest.Data.Properties.Title = string.Format( + Strings.ShareDialogTitleMultipleItems.GetLocalizedResource(), + items.Count, + "ItemsCount.Text".GetLocalizedResource()); + dataRequest.Data.Properties.Description = Strings.ShareDialogMultipleItemsDescription.GetLocalizedResource(); + } + + dataRequest.Data.SetStorageItems(items, false); + dataRequestDeferral.Complete(); + } + } + } +} diff --git a/src/Files.App/Helpers/ShareItemHelpers.cs b/src/Files.App/Helpers/ShareItemHelpers.cs index ce8b859dbf11..2cb46e8ff38b 100644 --- a/src/Files.App/Helpers/ShareItemHelpers.cs +++ b/src/Files.App/Helpers/ShareItemHelpers.cs @@ -1,4 +1,4 @@ -// Copyright (c) Files Community +// Copyright (c) Files Community // Licensed under the MIT License. using Microsoft.UI.Xaml.Controls; @@ -8,7 +8,7 @@ namespace Files.App.Helpers { - public static class ShareItemHelpers + public static partial class ShareItemHelpers { public static bool IsItemShareable(ListedItem item) => !item.IsHiddenItem && @@ -25,98 +25,6 @@ public static bool IsSupported() #if !WINDOWS public static Task ShareItemsAsync(IEnumerable itemsToShare) => Task.CompletedTask; // LINUX-TODO(share): see IsSupported -#else - public static async Task ShareItemsAsync(IEnumerable itemsToShare) - { - if (itemsToShare is null) - return; - - var interop = DataTransferManager.As(); - IntPtr result = interop.GetForWindow(MainWindow.Instance.WindowHandle, Win32PInvoke.DataTransferManagerInteropIID); - - var manager = WinRT.MarshalInterface.FromAbi(result); - manager.DataRequested += Manager_DataRequested; - - try - { - interop.ShowShareUIForWindow(MainWindow.Instance.WindowHandle); - } - catch (Exception ex) - { - manager.DataRequested -= Manager_DataRequested; - - var errorDialog = new ContentDialog() - { - Title = Strings.FaildToShareItems.GetLocalizedResource(), - Content = ex.Message, - PrimaryButtonText = Strings.OK.GetLocalizedResource(), - }; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - errorDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - await errorDialog.TryShowAsync(); - } - - async void Manager_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) - { - sender.DataRequested -= Manager_DataRequested; - - DataRequestDeferral dataRequestDeferral = args.Request.GetDeferral(); - List items = []; - DataRequest dataRequest = args.Request; - - foreach (ListedItem item in itemsToShare) - { - if (item is IShortcutItem shItem) - { - if (shItem.IsLinkItem && !string.IsNullOrEmpty(shItem.TargetPath)) - { - dataRequest.Data.Properties.Title = string.Format(Strings.ShareDialogTitle.GetLocalizedResource(), item.Name); - dataRequest.Data.Properties.Description = Strings.ShareDialogSingleItemDescription.GetLocalizedResource(); - dataRequest.Data.SetWebLink(new Uri(shItem.TargetPath)); - dataRequestDeferral.Complete(); - - return; - } - } - else if (item.PrimaryItemAttribute == StorageItemTypes.Folder && !item.IsArchive) - { - if (await StorageHelpers.ToStorageItem(item.ItemPath!) is BaseStorageFolder folder) - items.Add(folder); - } - else - { - if (await StorageHelpers.ToStorageItem(item.ItemPath!) is BaseStorageFile file) - items.Add(file); - } - } - - if (items.Count == 1) - { - dataRequest.Data.Properties.Title = string.Format(Strings.ShareDialogTitle.GetLocalizedResource(), items.First().Name); - dataRequest.Data.Properties.Description = Strings.ShareDialogSingleItemDescription.GetLocalizedResource(); - } - else if (items.Count == 0) - { - dataRequest.FailWithDisplayText(Strings.ShareDialogFailMessage.GetLocalizedResource()); - dataRequestDeferral.Complete(); - - return; - } - else - { - dataRequest.Data.Properties.Title = string.Format( - Strings.ShareDialogTitleMultipleItems.GetLocalizedResource(), - items.Count, - "ItemsCount.Text".GetLocalizedResource()); - dataRequest.Data.Properties.Description = Strings.ShareDialogMultipleItemsDescription.GetLocalizedResource(); - } - - dataRequest.Data.SetStorageItems(items, false); - dataRequestDeferral.Complete(); - } - } #endif } } diff --git a/src/Files.App/Helpers/TransferHelpers.Windows.cs b/src/Files.App/Helpers/TransferHelpers.Windows.cs new file mode 100644 index 000000000000..0c0e1753fbd9 --- /dev/null +++ b/src/Files.App/Helpers/TransferHelpers.Windows.cs @@ -0,0 +1,212 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage; +using WinRT; + +namespace Files.App.Helpers +{ + public static partial class TransferHelpers + { + [DynamicWindowsRuntimeCast(typeof(StorageFile))] + [DynamicWindowsRuntimeCast(typeof(StorageFolder))] + public static async Task ExecuteTransferAsync(IReadOnlyList itemsToTransfer, ShellViewModel shellViewModel, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy) + { + if (OperatingSystem.IsLinux()) + { + // LINUX-TODO(clipboard): W-CLIP replaces the in-app FileClipboard with the system clipboard + FileClipboard.Set(itemsToTransfer.Select(x => x.Id), type); + return; + } + + ConcurrentBag items = []; + var itemsCount = itemsToTransfer.Count; + var statusCenterItem = itemsCount > 50 ? StatusCenterHelper.AddCard_Prepare() : null; + var dataPackage = new DataPackage() { RequestedOperation = type }; + + try + { + // Update the status to in-progress + if (statusCenterItem is not null) + { + statusCenterItem.Progress.EnumerationCompleted = true; + statusCenterItem.Progress.ItemsCount = items.Count; + statusCenterItem.Progress.ReportStatus(FileSystemStatusCode.InProgress); + } + + await itemsToTransfer.ParallelForEachAsync(async storable => + { + // Update the status to increase processed count by one + if (statusCenterItem is not null) + { + statusCenterItem.Progress.AddProcessedItemsCount(1); + statusCenterItem.Progress.Report(); + } + + var result = storable switch + { + IFile => await shellViewModel.GetFileFromPathAsync(storable.Id).OnSuccess(x => items.Add(x + ?? throw new InvalidOperationException($"The file '{storable.Id}' could not be opened."))), + IFolder => await shellViewModel.GetFolderFromPathAsync(storable.Id).OnSuccess(x => items.Add(x + ?? throw new InvalidOperationException($"The folder '{storable.Id}' could not be opened."))), + _ => throw new ArgumentOutOfRangeException(nameof(storable)), + }; + + if (!result) + throw new SystemIO.IOException($"Failed to process {storable.Id} in cutting/copying to the clipboard.", (int)result.ErrorCode); + }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None); + + var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder); + if (standardObjectsOnly) + items = new(await items.ToStandardStorageItemsAsync()); + + if (items.IsEmpty) + return; + + dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName; + dataPackage.SetStorageItems(items, false); + + Clipboard.SetContent(dataPackage); +#if !WINDOWS + await Files.App.Services.Desktop.DesktopFileDragHelper.PublishFilesAsync(itemsToTransfer.Select(x => x.Id), type); +#endif + } + catch (Exception ex) + { + if (ex is not SystemIO.IOException) + App.Logger.LogWarning(ex, "Failed to process cutting/copying due to an unknown error."); + + if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized) + { + var filePaths = itemsToTransfer.Select(x => x.Id).ToArray(); + await FileOperationsHelpers.SetClipboard(filePaths, type); + } + } + finally + { + if (statusCenterItem is not null) + statusViewModel.RemoveItem(statusCenterItem); + } + } + + [DynamicWindowsRuntimeCast(typeof(StorageFile))] + [DynamicWindowsRuntimeCast(typeof(StorageFolder))] + public static async Task ExecuteTransferAsync(IContentPageContext context, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy) + { + if (context.ShellPage?.SlimContentPage is not { } contentPage || + contentPage.IsItemSelected is false) + return; + + // Reset cut mode + contentPage.ItemManipulationModel.RefreshItemsOpacity(); + + if (OperatingSystem.IsLinux()) + { + // LINUX-TODO(clipboard): W-CLIP replaces the in-app FileClipboard with the system clipboard + var selected = context.SelectedItems.ToList(); + FileClipboard.Set(selected.Select(x => x.ItemPath), type); + if (type is DataPackageOperation.Move) + { + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => + { + foreach (var item in selected) + item.Opacity = Constants.UI.DimItemOpacity; + }); + } + + return; + } + + ConcurrentBag items = []; + var itemsCount = context.SelectedItems.Count; + var statusCenterItem = itemsCount > 50 ? StatusCenterHelper.AddCard_Prepare() : null; + var dataPackage = new DataPackage() { RequestedOperation = type }; + + try + { + // Update the status to in-progress + if (statusCenterItem is not null) + { + statusCenterItem.Progress.EnumerationCompleted = true; + statusCenterItem.Progress.ItemsCount = items.Count; + statusCenterItem.Progress.ReportStatus(FileSystemStatusCode.InProgress); + } + + await context.SelectedItems.ToList().ParallelForEachAsync(async listedItem => + { + // Update the status to increase processed count by one + if (statusCenterItem is not null) + { + statusCenterItem.Progress.AddProcessedItemsCount(1); + statusCenterItem.Progress.Report(); + } + + if (listedItem is FtpItem ftpItem) + { + // Don't dim selected items here since FTP doesn't support cut + if (ftpItem.PrimaryItemAttribute is StorageItemTypes.File or StorageItemTypes.Folder) + items.Add(await ftpItem.ToStorageItem()); + } + else + { + if (type is DataPackageOperation.Move) + { + // Dim opacities accordingly + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => + { + listedItem.Opacity = Constants.UI.DimItemOpacity; + }); + } + + var shellViewModel = context.ShellPage.GetRequiredShellViewModel(); + var itemPath = listedItem.GetRequiredPath(); + var result = listedItem.PrimaryItemAttribute == StorageItemTypes.File || listedItem is ZipItem + ? await shellViewModel.GetFileFromPathAsync(itemPath).OnSuccess(t => items.Add(t + ?? throw new InvalidOperationException($"The file '{itemPath}' could not be opened."))) + : await shellViewModel.GetFolderFromPathAsync(itemPath).OnSuccess(t => items.Add(t + ?? throw new InvalidOperationException($"The folder '{itemPath}' could not be opened."))); + + if (!result) + throw new SystemIO.IOException($"Failed to process {listedItem.ItemPath} in cutting/copying to the clipboard.", (int)result.ErrorCode); + } + }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None); + + var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder); + if (standardObjectsOnly) + items = new(await items.ToStandardStorageItemsAsync()); + + if (items.IsEmpty) + return; + + dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName; + dataPackage.SetStorageItems(items, false); + + Clipboard.SetContent(dataPackage); +#if !WINDOWS + await Files.App.Services.Desktop.DesktopFileDragHelper.PublishFilesAsync(context.SelectedItems.Select(x => x.ItemPath), type); +#endif + } + catch (Exception ex) + { + if (ex is not SystemIO.IOException) + App.Logger.LogWarning(ex, "Failed to process cutting/copying due to an unknown error."); + + if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized) + { + var filePaths = context.SelectedItems.Select(x => x.ItemPath!).ToArray(); + await FileOperationsHelpers.SetClipboard(filePaths, type); + + return; + } + + // Reset cut mode + context.ShellPage.SlimContentPage.ItemManipulationModel.RefreshItemsOpacity(); + } + finally + { + if (statusCenterItem is not null) + statusViewModel.RemoveItem(statusCenterItem); + } + } + } +} diff --git a/src/Files.App/Helpers/TransferHelpers.cs b/src/Files.App/Helpers/TransferHelpers.cs index 0906260bc28c..ff704376961a 100644 --- a/src/Files.App/Helpers/TransferHelpers.cs +++ b/src/Files.App/Helpers/TransferHelpers.cs @@ -1,212 +1,40 @@ -using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; +// Copyright (c) Files Community +// Licensed under the MIT License. + +#if !WINDOWS using Windows.ApplicationModel.DataTransfer; -using Windows.Storage; -using WinRT; +using Files.App.Services.Desktop; namespace Files.App.Helpers { - public static class TransferHelpers + public static partial class TransferHelpers { - [DynamicWindowsRuntimeCast(typeof(StorageFile))] - [DynamicWindowsRuntimeCast(typeof(StorageFolder))] public static async Task ExecuteTransferAsync(IReadOnlyList itemsToTransfer, ShellViewModel shellViewModel, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy) { - if (OperatingSystem.IsLinux()) - { - // LINUX-TODO(clipboard): W-CLIP replaces the in-app FileClipboard with the system clipboard - FileClipboard.Set(itemsToTransfer.Select(x => x.Id), type); - return; - } - - ConcurrentBag items = []; - var itemsCount = itemsToTransfer.Count; - var statusCenterItem = itemsCount > 50 ? StatusCenterHelper.AddCard_Prepare() : null; - var dataPackage = new DataPackage() { RequestedOperation = type }; - - try - { - // Update the status to in-progress - if (statusCenterItem is not null) - { - statusCenterItem.Progress.EnumerationCompleted = true; - statusCenterItem.Progress.ItemsCount = items.Count; - statusCenterItem.Progress.ReportStatus(FileSystemStatusCode.InProgress); - } - - await itemsToTransfer.ParallelForEachAsync(async storable => - { - // Update the status to increase processed count by one - if (statusCenterItem is not null) - { - statusCenterItem.Progress.AddProcessedItemsCount(1); - statusCenterItem.Progress.Report(); - } - - var result = storable switch - { - IFile => await shellViewModel.GetFileFromPathAsync(storable.Id).OnSuccess(x => items.Add(x - ?? throw new InvalidOperationException($"The file '{storable.Id}' could not be opened."))), - IFolder => await shellViewModel.GetFolderFromPathAsync(storable.Id).OnSuccess(x => items.Add(x - ?? throw new InvalidOperationException($"The folder '{storable.Id}' could not be opened."))), - _ => throw new ArgumentOutOfRangeException(nameof(storable)), - }; - - if (!result) - throw new SystemIO.IOException($"Failed to process {storable.Id} in cutting/copying to the clipboard.", (int)result.ErrorCode); - }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None); - - var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder); - if (standardObjectsOnly) - items = new(await items.ToStandardStorageItemsAsync()); - - if (items.IsEmpty) - return; - - dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName; - dataPackage.SetStorageItems(items, false); - - Clipboard.SetContent(dataPackage); -#if !WINDOWS - await Files.App.Services.Desktop.DesktopFileDragHelper.PublishFilesAsync(itemsToTransfer.Select(x => x.Id), type); -#endif - } - catch (Exception ex) - { - if (ex is not SystemIO.IOException) - App.Logger.LogWarning(ex, "Failed to process cutting/copying due to an unknown error."); - - if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized) - { - var filePaths = itemsToTransfer.Select(x => x.Id).ToArray(); - await FileOperationsHelpers.SetClipboard(filePaths, type); - } - } - finally - { - if (statusCenterItem is not null) - statusViewModel.RemoveItem(statusCenterItem); - } + var paths = itemsToTransfer.Select(x => x.Id).ToArray(); + FileClipboard.Set(paths, type); + await DesktopFileDragHelper.PublishFilesAsync(paths, type); } - [DynamicWindowsRuntimeCast(typeof(StorageFile))] - [DynamicWindowsRuntimeCast(typeof(StorageFolder))] public static async Task ExecuteTransferAsync(IContentPageContext context, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy) { - if (context.ShellPage?.SlimContentPage is not { } contentPage || - contentPage.IsItemSelected is false) + if (context.ShellPage?.SlimContentPage is not { } contentPage || !contentPage.IsItemSelected) return; - // Reset cut mode contentPage.ItemManipulationModel.RefreshItemsOpacity(); - - if (OperatingSystem.IsLinux()) + var selected = context.SelectedItems.ToList(); + var paths = selected.Select(x => x.ItemPath).ToArray(); + FileClipboard.Set(paths, type); + await DesktopFileDragHelper.PublishFilesAsync(paths, type); + if (type is DataPackageOperation.Move) { - // LINUX-TODO(clipboard): W-CLIP replaces the in-app FileClipboard with the system clipboard - var selected = context.SelectedItems.ToList(); - FileClipboard.Set(selected.Select(x => x.ItemPath), type); - if (type is DataPackageOperation.Move) + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => { - await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => - { - foreach (var item in selected) - item.Opacity = Constants.UI.DimItemOpacity; - }); - } - - return; - } - - ConcurrentBag items = []; - var itemsCount = context.SelectedItems.Count; - var statusCenterItem = itemsCount > 50 ? StatusCenterHelper.AddCard_Prepare() : null; - var dataPackage = new DataPackage() { RequestedOperation = type }; - - try - { - // Update the status to in-progress - if (statusCenterItem is not null) - { - statusCenterItem.Progress.EnumerationCompleted = true; - statusCenterItem.Progress.ItemsCount = items.Count; - statusCenterItem.Progress.ReportStatus(FileSystemStatusCode.InProgress); - } - - await context.SelectedItems.ToList().ParallelForEachAsync(async listedItem => - { - // Update the status to increase processed count by one - if (statusCenterItem is not null) - { - statusCenterItem.Progress.AddProcessedItemsCount(1); - statusCenterItem.Progress.Report(); - } - - if (listedItem is FtpItem ftpItem) - { - // Don't dim selected items here since FTP doesn't support cut - if (ftpItem.PrimaryItemAttribute is StorageItemTypes.File or StorageItemTypes.Folder) - items.Add(await ftpItem.ToStorageItem()); - } - else - { - if (type is DataPackageOperation.Move) - { - // Dim opacities accordingly - await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => - { - listedItem.Opacity = Constants.UI.DimItemOpacity; - }); - } - - var shellViewModel = context.ShellPage.GetRequiredShellViewModel(); - var itemPath = listedItem.GetRequiredPath(); - var result = listedItem.PrimaryItemAttribute == StorageItemTypes.File || listedItem is ZipItem - ? await shellViewModel.GetFileFromPathAsync(itemPath).OnSuccess(t => items.Add(t - ?? throw new InvalidOperationException($"The file '{itemPath}' could not be opened."))) - : await shellViewModel.GetFolderFromPathAsync(itemPath).OnSuccess(t => items.Add(t - ?? throw new InvalidOperationException($"The folder '{itemPath}' could not be opened."))); - - if (!result) - throw new SystemIO.IOException($"Failed to process {listedItem.ItemPath} in cutting/copying to the clipboard.", (int)result.ErrorCode); - } - }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None); - - var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder); - if (standardObjectsOnly) - items = new(await items.ToStandardStorageItemsAsync()); - - if (items.IsEmpty) - return; - - dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName; - dataPackage.SetStorageItems(items, false); - - Clipboard.SetContent(dataPackage); -#if !WINDOWS - await Files.App.Services.Desktop.DesktopFileDragHelper.PublishFilesAsync(context.SelectedItems.Select(x => x.ItemPath), type); -#endif - } - catch (Exception ex) - { - if (ex is not SystemIO.IOException) - App.Logger.LogWarning(ex, "Failed to process cutting/copying due to an unknown error."); - - if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized) - { - var filePaths = context.SelectedItems.Select(x => x.ItemPath!).ToArray(); - await FileOperationsHelpers.SetClipboard(filePaths, type); - - return; - } - - // Reset cut mode - context.ShellPage.SlimContentPage.ItemManipulationModel.RefreshItemsOpacity(); - } - finally - { - if (statusCenterItem is not null) - statusViewModel.RemoveItem(statusCenterItem); + foreach (var item in selected) + item.Opacity = Constants.UI.DimItemOpacity; + }); } } } } +#endif diff --git a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs index 4daa9f5780e3..3b694dacc8a3 100644 --- a/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs +++ b/src/Files.App/Helpers/UI/UIFilesystemHelpers.cs @@ -8,7 +8,6 @@ using System.Text; using Windows.ApplicationModel.DataTransfer; using Windows.Storage; -using Windows.Win32.UI.WindowsAndMessaging; namespace Files.App.Helpers { @@ -330,7 +329,7 @@ public static async Task HandleShortcutCannotBeCreated(string shortcutName /// /// Updates ListedItem properties for a shortcut /// - public static void UpdateShortcutItemProperties(IShortcutItem item, string? targetPath, string? arguments, string? workingDir, bool runAsAdmin, SHOW_WINDOW_CMD showWindowCommand) + public static void UpdateShortcutItemProperties(IShortcutItem item, string? targetPath, string? arguments, string? workingDir, bool runAsAdmin, ShowWindowCommand showWindowCommand) { ArgumentNullException.ThrowIfNull(targetPath); item.TargetPath = Environment.ExpandEnvironmentVariables(targetPath); @@ -347,6 +346,7 @@ public async static Task RequestPassword(IPasswordProtectedIt var credentialDialogViewModel = new CredentialDialogViewModel() { CanBeAnonymous = isFtp, PasswordOnly = !isFtp }; +#if WINDOWS if (sender is ZipStorageFolder zipFolder) { credentialDialogViewModel.PasswordValidator = async (password) => @@ -356,6 +356,7 @@ public async static Task RequestPassword(IPasswordProtectedIt }; } +#endif IDialogService dialogService = Ioc.Default.GetRequiredService(); var dialogResult = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => dialogService.ShowDialogAsync(credentialDialogViewModel)); diff --git a/src/Files.App/Services/Git/LibGit2Service.cs b/src/Files.App/Services/Git/LibGit2Service.cs index 37670435a13a..5a5c7a5937ab 100644 --- a/src/Files.App/Services/Git/LibGit2Service.cs +++ b/src/Files.App/Services/Git/LibGit2Service.cs @@ -55,7 +55,9 @@ public bool IsExecutingGitAction if (string.IsNullOrWhiteSpace(path) || path.Equals(root, StringComparison.OrdinalIgnoreCase) || path.Equals("Home", StringComparison.OrdinalIgnoreCase) || - ShellStorageFolder.IsShellPath(path)) + path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("::{", StringComparison.Ordinal) || + path.StartsWith(@"\\SHELL\", StringComparison.Ordinal)) { return null; } diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 7896d528d635..22a39a7ae3ed 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -7,8 +7,10 @@ using System.Runtime.InteropServices; using System.Windows.Input; using Windows.ApplicationModel.DataTransfer; +#if WINDOWS using Windows.Win32.System.Com; using Windows.Win32.UI.Shell; +#endif using WinRT; using DragEventArgs = Microsoft.UI.Xaml.DragEventArgs; using Visibility = Microsoft.UI.Xaml.Visibility; @@ -41,7 +43,7 @@ private async void Shelf_Drop(object sender, DragEventArgs e) return; // Get items - var storageService = Ioc.Default.GetRequiredService(); + var resolver = Ioc.Default.GetRequiredService(); var storageItems = (await FilesystemHelpers.GetDraggedStorageItems(e.DataView)).ToArray(); // Add to list @@ -51,12 +53,8 @@ private async void Shelf_Drop(object sender, DragEventArgs e) if (ItemsSource.Any(x => x.Inner.Id == item.Path)) continue; - var storable = item switch - { - StorageFileWithPath => (IStorableChild?)await storageService.TryGetFileAsync(item.Path), - StorageFolderWithPath => (IStorableChild?)await storageService.TryGetFolderAsync(item.Path), - _ => null - }; + var resolved = await resolver.TryGetAsync(item.Path); + var storable = resolved.Item as IStorableChild; if (storable is null) continue; @@ -93,8 +91,8 @@ private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArg item.Dispose(); } #else - // LINUX-TODO(dnd): shell data object drag from the shelf; use Uno DataPackage storage items / text/uri-list instead e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; + Files.App.Services.Desktop.DesktopFileDragHelper.StartExternalDrag(paths); #endif } diff --git a/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.Windows.cs b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.Windows.cs new file mode 100644 index 000000000000..67d4fd4c7776 --- /dev/null +++ b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.Windows.cs @@ -0,0 +1,41 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.ViewModels.Properties; +using System.IO; + +namespace Files.App.ViewModels.Previews +{ + public abstract partial class BasePreviewModel + { + private async Task?> GetSystemFilePropertiesAsync() + { + if (Item.IsShortcut) + return null; + if (Item.ItemFile is null) + throw new InvalidOperationException("The preview item could not be opened as a file."); + + var list = await FileProperty.RetrieveAndInitializePropertiesAsync(Item.ItemFile, + Constants.ResourceFilePaths.PreviewPaneDetailsPropertiesJsonPath); + + var address = list.Find(x => x.ID is "address") + ?? throw new InvalidDataException("The preview property definition is missing the address field."); + var latitude = list.Find(x => x.Property is "System.GPS.LatitudeDecimal") + ?? throw new InvalidDataException("The preview property definition is missing the latitude field."); + var longitude = list.Find(x => x.Property is "System.GPS.LongitudeDecimal") + ?? throw new InvalidDataException("The preview property definition is missing the longitude field."); + address.Value = await LocationHelpers.GetAddressFromCoordinatesAsync( + (double?)latitude.Value, + (double?)longitude.Value); + + // Adds the value for the file tag + var fileTag = list.FirstOrDefault(x => x.ID is "filetag") + ?? throw new InvalidDataException("The preview property definition is missing the file tag field."); + 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/BasePreviewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs index cdacce3ff691..668fb65a300d 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs @@ -2,6 +2,10 @@ // Licensed under the MIT License. using System.IO; +using Files.Core.Storage.Contracts; +#if !WINDOWS +using OwlCore.Storage.System.IO; +#endif using Files.App.ViewModels.Properties; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media.Imaging; @@ -14,7 +18,11 @@ public abstract partial class BasePreviewModel : ObservableObject public ListedItem Item { get; } +#if WINDOWS protected BaseStorageFile PreviewFile +#else + protected IFile PreviewFile +#endif => Item.ItemFile ?? throw new InvalidOperationException("The preview file has not been loaded."); private BitmapImage? fileImage; @@ -42,8 +50,37 @@ public static Task LoadDetailsOnlyAsync(ListedItem item, List? det return temp.LoadAsync(); } +#if WINDOWS public static Task ReadFileAsTextAsync(BaseStorageFile file, int maxLength = 10 * 1024 * 1024) => file.ReadTextAsync(maxLength); +#else + protected static async Task ResolvePreviewFileAsync(ListedItem item, CancellationToken cancellationToken) + { + if (item.ItemFile is null) + { + var result = await Ioc.Default.GetRequiredService().TryGetAsync(item.GetRequiredPath(), cancellationToken); + item.ItemFile = result.Item as IFile; + } + + return item.ItemFile; + } + + protected static Task OpenPreviewReadAsync(IFile file, CancellationToken cancellationToken) + { + // Local streams must pin and stat the inode before opening it, including symlink targets. + if (file is SystemFile) + return Task.FromResult(Files.Platform.Linux.Previews.PreviewFile.OpenRead(file.Id, cancellationToken)); + + return file.OpenStreamAsync(FileAccess.Read, cancellationToken); + } + + protected async Task OpenPreviewReadAsync(CancellationToken cancellationToken) + { + var file = await ResolvePreviewFileAsync(Item, cancellationToken) + ?? throw new IOException("The preview file could not be resolved."); + return await OpenPreviewReadAsync(file, cancellationToken); + } +#endif /// /// Call this function when you are ready to load the preview and details. @@ -56,9 +93,13 @@ public virtual async Task LoadAsync() if (Item.ItemFile is null) { +#if WINDOWS var itemPath = Item.ItemPath!; var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(itemPath)); Item.ItemFile = await StorageFileExtensions.DangerousGetFileFromPathAsync(itemPath, rootItem.Result); +#else + await ResolvePreviewFileAsync(Item, LoadCancelledTokenSource.Token); +#endif } await Task.Run(async () => @@ -117,35 +158,7 @@ public virtual void PreviewControlBase_Unloaded(object? sender, RoutedEventArgs protected static FileProperty GetFileProperty(string nameResource, object? value) => new() { NameResource = nameResource, Value = value }; - private async Task?> GetSystemFilePropertiesAsync() - { - if (Item.IsShortcut) - return null; - if (Item.ItemFile is null) - throw new InvalidOperationException("The preview item could not be opened as a file."); - - var list = await FileProperty.RetrieveAndInitializePropertiesAsync(Item.ItemFile, - Constants.ResourceFilePaths.PreviewPaneDetailsPropertiesJsonPath); - - var address = list.Find(x => x.ID is "address") - ?? throw new InvalidDataException("The preview property definition is missing the address field."); - var latitude = list.Find(x => x.Property is "System.GPS.LatitudeDecimal") - ?? throw new InvalidDataException("The preview property definition is missing the latitude field."); - var longitude = list.Find(x => x.Property is "System.GPS.LongitudeDecimal") - ?? throw new InvalidDataException("The preview property definition is missing the longitude field."); - address.Value = await LocationHelpers.GetAddressFromCoordinatesAsync( - (double?)latitude.Value, - (double?)longitude.Value); - - // Adds the value for the file tag - var fileTag = list.FirstOrDefault(x => x.ID is "filetag") - ?? throw new InvalidDataException("The preview property definition is missing the file tag field."); - 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(); - } + #if DESKTOP private List GetBasicFileDetails() diff --git a/src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.Windows.cs similarity index 100% rename from src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.cs rename to src/Files.App/ViewModels/UserControls/Previews/CodePreviewViewModel.Windows.cs diff --git a/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs index f3823f216523..97555232dbed 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs @@ -4,6 +4,8 @@ using Files.App.ViewModels.Properties; using Microsoft.UI.Xaml.Media.Imaging; using System.IO; +using Files.Core.Storage.Contracts; +using OwlCore.Storage.System.IO; namespace Files.App.ViewModels.Previews { @@ -14,8 +16,6 @@ public sealed class FolderPreviewViewModel public BitmapImage? Thumbnail { get; set; } = new(); - private BaseStorageFolder? Folder { get; set; } - public FolderPreviewViewModel(ListedItem item) => Item = item; @@ -25,11 +25,15 @@ public Task LoadAsync() private async Task LoadPreviewAndDetailsAsync() { var itemPath = Item.ItemPath!; +#if WINDOWS var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(itemPath)); var folder = await StorageFileExtensions.DangerousGetFolderFromPathAsync(itemPath, rootItem.Result) ?? throw new InvalidOperationException("The preview folder could not be opened."); - - Folder = folder; +#else + var resolved = await Ioc.Default.GetRequiredService().TryGetAsync(itemPath); + if (resolved.Item is not IFolder) + throw new IOException("The preview folder could not be resolved."); +#endif var result = await FileThumbnailHelper.GetIconAsync( Item.ItemPath, @@ -46,20 +50,28 @@ private async Task LoadPreviewAndDetailsAsync() if (Item.IsDriveRoot || infoPaneViewModel?.SelectedDriveItem is not null) return; +#if WINDOWS var info = await folder.GetBasicPropertiesAsync(); + var dateModified = info.DateModified; + var dateCreated = info.DateCreated; +#else + var localInfo = resolved.Item is SystemFolder ? new DirectoryInfo(itemPath) : null; + var dateModified = localInfo is null ? Item.ItemDateModifiedReal : new DateTimeOffset(localInfo.LastWriteTime); + var dateCreated = localInfo is null ? Item.ItemDateCreatedReal : new DateTimeOffset(localInfo.CreationTime); +#endif Item.FileDetails = [ GetFileProperty("PropertyItemCount", infoPaneViewModel?.DirectoryItemCount), - GetFileProperty("PropertyDateModified", info.DateModified), - GetFileProperty("PropertyDateCreated", info.DateCreated), - GetFileProperty("PropertyParsingPath", folder.Path), + GetFileProperty("PropertyDateModified", dateModified), + GetFileProperty("PropertyDateCreated", dateCreated), + GetFileProperty("PropertyParsingPath", itemPath), ]; 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(itemPath, Path.GetPathRoot(itemPath)); var headName = (await GitHelpers.GetRepositoryHead(gitDirectory))?.Name ?? string.Empty; var repositoryName = GitHelpers.GetOriginRepositoryName(gitDirectory); diff --git a/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs index ee94aa41ceeb..782cff2aaa3c 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ImagePreviewViewModel.cs @@ -36,7 +36,7 @@ public override async Task> LoadPreviewAndDetailsAsync() try { var path = Item.ItemPath!; - using var source = Files.Platform.Linux.Previews.PreviewFile.OpenRead(path, LoadCancelledTokenSource.Token); + using var source = await OpenPreviewReadAsync(LoadCancelledTokenSource.Token); if (string.Equals(Item.FileExtension, ".pdf", StringComparison.OrdinalIgnoreCase)) { // LINUX-TODO(preview): PDF shows the first page; multipage navigation needs a document renderer. diff --git a/src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.Windows.cs similarity index 100% rename from src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.cs rename to src/Files.App/ViewModels/UserControls/Previews/MarkdownPreviewViewModel.Windows.cs diff --git a/src/Files.App/ViewModels/UserControls/Previews/MediaMetadataPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/MediaMetadataPreviewViewModel.cs index 10f797be23e8..3a165cd26127 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/MediaMetadataPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/MediaMetadataPreviewViewModel.cs @@ -31,7 +31,7 @@ public override async Task> LoadPreviewAndDetailsAsync() { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(LoadCancelledTokenSource.Token); timeout.CancelAfter(TimeSpan.FromSeconds(5)); - using var source = Files.Platform.Linux.Previews.PreviewFile.OpenRead(Item.ItemPath!, timeout.Token); + using var source = await OpenPreviewReadAsync(timeout.Token); using var snapshot = await MediaPreviewInput.ReadAsync(source, Item.FileExtension?.ToLowerInvariant(), timeout.Token); using var input = new PreviewReadStream(snapshot, MediaPreviewInput.MaxBytes, timeout.Token); using var file = OpenMedia(new ReadOnlyMediaFile(Item.ItemPath!, input), Item.FileExtension?.ToLowerInvariant()); diff --git a/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.Windows.cs similarity index 100% rename from src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs rename to src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.Windows.cs diff --git a/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.Windows.cs similarity index 100% rename from src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs rename to src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.Windows.cs diff --git a/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.Windows.cs similarity index 100% rename from src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs rename to src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.Windows.cs diff --git a/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.Windows.cs similarity index 100% rename from src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs rename to src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.Windows.cs diff --git a/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.Windows.cs b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.Windows.cs new file mode 100644 index 000000000000..4b1047c88d01 --- /dev/null +++ b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.Windows.cs @@ -0,0 +1,77 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.UserControls.FilePreviews; +using Files.App.ViewModels.Properties; + +namespace Files.App.ViewModels.Previews +{ + public sealed partial class TextPreviewViewModel : BasePreviewModel + { + private string? textValue; + public string? TextValue + { + get => textValue; + private set => SetProperty(ref textValue, value); + } + + public TextPreviewViewModel(ListedItem item) + : base(item) + { + } + + public async override Task> LoadPreviewAndDetailsAsync() + { + var details = new List(); + + try + { + 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)); + + TextValue = text.Left(Constants.PreviewPane.TextCharacterLimit); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + return details; + } + + public static async Task TryLoadAsTextAsync(ListedItem item) + { + 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(itemFile); + bool isBinaryFile = text.Contains("\0\0\0\0", StringComparison.Ordinal); + + if (isBinaryFile) + return null; + + var model = new TextPreviewViewModel(item) { TextValue = text }; + await model.LoadAsync(); + + return new TextPreview(model); + } + catch + { + return null; + } + } + + private static bool ExcludedExtensions(string? extension) + => extension is ".iso"; + } +} + diff --git a/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs index f1389dedd806..e9468de5030e 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs @@ -1,82 +1,4 @@ -#if WINDOWS -// Copyright (c) Files Community -// Licensed under the MIT License. - -using Files.App.UserControls.FilePreviews; -using Files.App.ViewModels.Properties; - -namespace Files.App.ViewModels.Previews -{ - public sealed partial class TextPreviewViewModel : BasePreviewModel - { - private string? textValue; - public string? TextValue - { - get => textValue; - private set => SetProperty(ref textValue, value); - } - - public TextPreviewViewModel(ListedItem item) - : base(item) - { - } - - public async override Task> LoadPreviewAndDetailsAsync() - { - var details = new List(); - - try - { - 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)); - - TextValue = text.Left(Constants.PreviewPane.TextCharacterLimit); - } - catch (Exception e) - { - Debug.WriteLine(e); - } - - return details; - } - - public static async Task TryLoadAsTextAsync(ListedItem item) - { - 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(itemFile); - bool isBinaryFile = text.Contains("\0\0\0\0", StringComparison.Ordinal); - - if (isBinaryFile) - return null; - - var model = new TextPreviewViewModel(item) { TextValue = text }; - await model.LoadAsync(); - - return new TextPreview(model); - } - catch - { - return null; - } - } - - private static bool ExcludedExtensions(string? extension) - => extension is ".iso"; - } -} - -#else +#if !WINDOWS // Copyright (c) Files Community // Licensed under the MIT License. @@ -136,7 +58,7 @@ public async override Task> LoadPreviewAndDetailsAsync() { if (Item.FileSizeBytes is 0) return details; - using var stream = Files.Platform.Linux.Previews.PreviewFile.OpenRead(Item.ItemPath!, LoadCancelledTokenSource.Token); + using var stream = await OpenPreviewReadAsync(LoadCancelledTokenSource.Token); if (stream.Length == 0) return details; Apply(await PreviewTextReader.ReadAsync(stream, LoadCancelledTokenSource.Token)); @@ -201,11 +123,10 @@ public static async Task CreateFromTextAsync(ListedItem item, strin try { cancellationToken.ThrowIfCancellationRequested(); - item.ItemFile ??= await StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath!); - if (item.ItemFile is not { } itemFile) + if (await ResolvePreviewFileAsync(item, cancellationToken) is not { } itemFile) return null; - using var stream = Files.Platform.Linux.Previews.PreviewFile.OpenRead(item.ItemPath!, cancellationToken); + using var stream = await OpenPreviewReadAsync(itemFile, cancellationToken); if (stream.Length == 0) return null; var result = await PreviewTextReader.ReadAsync(stream, cancellationToken); @@ -240,5 +161,4 @@ private static bool ExcludedExtensions(string? extension) => extension is ".iso" or ".pdf"; } } - #endif From d0a46dad42af6bda6441ed8aa53f3ea1600465b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 21:12:14 +0300 Subject: [PATCH 2/4] Previews and leaf storage users on storables (P4-G) ListedItem.ItemFile is IFile? on desktop, previews resolve files through IStorableResolver and keep the O_PATH/stat-before-open PreviewFile.OpenRead hardening for local files. Shortcut items use ShowWindowCommand; Windows-only preview and helper code moved to *.Windows.cs. Compat-off baseline ratcheted. Co-Authored-By: GPT-6.1-sol (OpenAI Codex) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- docs/linux-port/compat-off-baseline.txt | 16 ++----- .../ShowWindowCommandExtensions.Windows.cs | 43 ------------------- .../Extensions/ShowWindowCommandExtensions.cs | 41 ++++++++++++++++++ .../Enumerators/UniversalStorageEnumerator.cs | 4 +- .../Enumerators/Win32StorageEnumerator.cs | 4 +- .../Utils/Storage/Search/FolderSearch.cs | 2 +- .../Previews/ArchivePreviewViewModel.cs | 4 ++ ...el.Windows.cs => MediaPreviewViewModel.cs} | 5 +++ ...odel.Windows.cs => PDFPreviewViewModel.cs} | 6 +++ ...Windows.cs => RichTextPreviewViewModel.cs} | 6 +++ .../Views/Properties/ShortcutPage.xaml.cs | 2 +- 11 files changed, 71 insertions(+), 62 deletions(-) delete mode 100644 src/Files.App/Extensions/ShowWindowCommandExtensions.Windows.cs create mode 100644 src/Files.App/Extensions/ShowWindowCommandExtensions.cs rename src/Files.App/ViewModels/UserControls/Previews/{MediaPreviewViewModel.Windows.cs => MediaPreviewViewModel.cs} (87%) rename src/Files.App/ViewModels/UserControls/Previews/{PDFPreviewViewModel.Windows.cs => PDFPreviewViewModel.cs} (93%) rename src/Files.App/ViewModels/UserControls/Previews/{RichTextPreviewViewModel.Windows.cs => RichTextPreviewViewModel.cs} (78%) diff --git a/docs/linux-port/compat-off-baseline.txt b/docs/linux-port/compat-off-baseline.txt index d584a54da734..6ff30651d833 100644 --- a/docs/linux-port/compat-off-baseline.txt +++ b/docs/linux-port/compat-off-baseline.txt @@ -2,13 +2,12 @@ # One "file: error CODE: message" per line. Regenerate with scripts/linux/compat-off-check.sh --update. src/Files.App/Data/Factories/ShellContextFlyoutHelper.cs: error CS0246: The type or namespace name 'ContextMenu' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/ContextMenu.cs: error CS0246: The type or namespace name 'MENU_ITEM_TYPE' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Data/Items/ListedItem.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/ShellLinkItem.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) +src/Files.App/Data/Items/WidgetFolderCardItem.cs: error CS0234: The type or namespace name 'WinRT' does not exist in the namespace 'Windows.Win32.System' (are you missing an assembly reference?) src/Files.App/Data/Items/WidgetFolderCardItem.cs: error CS0246: The type or namespace name 'IWindowsStorable' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/WidgetRecentItem.cs: error CS0246: The type or namespace name 'IShellItem' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) -src/Files.App/Helpers/UI/UIFilesystemHelpers.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/UserControls/Toolbar.xaml.cs: error CS0246: The type or namespace name 'OpenWithMenu' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs: error CS0246: The type or namespace name 'Win32PInvoke' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/Helpers/FileTypesHelper.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) @@ -16,18 +15,8 @@ src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs: error CS0122: 'BOOL' is src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Utils/Storage/Helpers/SyncRootHelpers.cs: error CS0234: The type or namespace name 'WinRT' does not exist in the namespace 'Windows.Win32.System' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0103: The name 'SHOW_WINDOW_CMD' does not exist in the current context -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0122: 'HRESULT' is inaccessible due to its protection level -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0122: 'HWND' is inaccessible due to its protection level -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0234: The type or namespace name 'FILEOPERATION_FLAGS' does not exist in the namespace 'Windows.Win32.UI.Shell' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0234: The type or namespace name 'PROPERTYKEY' does not exist in the namespace 'Windows.Win32.Foundation' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0234: The type or namespace name 'SLR_FLAGS' does not exist in the namespace 'Windows.Win32.UI.Shell' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'Disposable' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'ShellFileOperations2' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'ShellItem' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) +src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0246: The type or namespace name 'FindCloseSafeHandle' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0246: The type or namespace name 'WIN32_FIND_DATAW' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) @@ -38,3 +27,4 @@ src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs: error CS0234: Th src/Files.App/ViewModels/ShellViewModel.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs: error CS0234: The type or namespace name 'WinRT' does not exist in the namespace 'Windows.Win32.System' (are you missing an assembly reference?) +src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs: error CS0246: The type or namespace name 'IAgileReference' could not be found (are you missing a using directive or an assembly reference?) diff --git a/src/Files.App/Extensions/ShowWindowCommandExtensions.Windows.cs b/src/Files.App/Extensions/ShowWindowCommandExtensions.Windows.cs deleted file mode 100644 index 351f32a7c4cc..000000000000 --- a/src/Files.App/Extensions/ShowWindowCommandExtensions.Windows.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -using Windows.Win32.UI.WindowsAndMessaging; - -namespace Files.App.Extensions -{ - /// - /// Converts between and the Win32 . - /// - public static class ShowWindowCommandExtensions - { - /// - /// Converts a Win32 show command to a ; unknown values map to . - /// - public static ShowWindowCommand ToShowWindowCommand(this SHOW_WINDOW_CMD command) - { - return command switch - { - SHOW_WINDOW_CMD.SW_MAXIMIZE => ShowWindowCommand.Maximized, - SHOW_WINDOW_CMD.SW_SHOWMINIMIZED or - SHOW_WINDOW_CMD.SW_MINIMIZE or - SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE or - SHOW_WINDOW_CMD.SW_FORCEMINIMIZE => ShowWindowCommand.Minimized, - _ => ShowWindowCommand.Normal, - }; - } - - /// - /// Converts a to the Win32 show command stored in shortcuts; - /// maps to . - /// - public static SHOW_WINDOW_CMD ToWin32(this ShowWindowCommand command) - { - return command switch - { - ShowWindowCommand.Maximized => SHOW_WINDOW_CMD.SW_MAXIMIZE, - ShowWindowCommand.Minimized => SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE, - _ => SHOW_WINDOW_CMD.SW_NORMAL, - }; - } - } -} diff --git a/src/Files.App/Extensions/ShowWindowCommandExtensions.cs b/src/Files.App/Extensions/ShowWindowCommandExtensions.cs new file mode 100644 index 000000000000..67fe7c4941a0 --- /dev/null +++ b/src/Files.App/Extensions/ShowWindowCommandExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Extensions +{ + /// + /// Converts between and the Win32 SHOW_WINDOW_CMD values (as ints, so no Win32 types are needed). + /// + public static class ShowWindowCommandExtensions + { + /// + /// Converts a Win32 show command (SHOW_WINDOW_CMD) to a ; unknown values map to . + /// + public static ShowWindowCommand ToShowWindowCommand(this int command) + { + return command switch + { + 3 => ShowWindowCommand.Maximized, + 2 or + 6 or + 7 or + 11 => ShowWindowCommand.Minimized, + _ => ShowWindowCommand.Normal, + }; + } + + /// + /// Converts a to the Win32 show command value stored in shortcuts; + /// maps to SW_NORMAL (1). + /// + public static int ToWin32(this ShowWindowCommand command) + { + return command switch + { + ShowWindowCommand.Maximized => 3, + ShowWindowCommand.Minimized => 7, + _ => 1, + }; + } + } +} diff --git a/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs index 32ba5d4b8eae..22d7d28779e1 100644 --- a/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs +++ b/src/Files.App/Utils/Storage/Enumerators/UniversalStorageEnumerator.cs @@ -224,7 +224,7 @@ ex is FileNotFoundException || Arguments = linkFolder.Arguments, WorkingDirectory = linkFolder.WorkingDirectory, RunAsAdmin = linkFolder.RunAsAdmin, - ShowWindowCommand = linkFolder.ShowWindowCommand + ShowWindowCommand = ((int)linkFolder.ShowWindowCommand).ToShowWindowCommand() }; } else if (folder is BinStorageFolder binFolder) @@ -324,7 +324,7 @@ ex is FileNotFoundException || Arguments = linkFile.Arguments, WorkingDirectory = linkFile.WorkingDirectory, RunAsAdmin = linkFile.RunAsAdmin, - ShowWindowCommand = linkFile.ShowWindowCommand, + ShowWindowCommand = ((int)linkFile.ShowWindowCommand).ToShowWindowCommand(), IsUrl = isUrl, }; } diff --git a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs index 4cea4acfa92d..9ffbeac5944a 100644 --- a/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs +++ b/src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs @@ -385,7 +385,7 @@ CancellationToken cancellationToken Arguments = shInfo.Arguments, WorkingDirectory = shInfo.WorkingDirectory, RunAsAdmin = shInfo.RunAsAdmin, - ShowWindowCommand = shInfo.ShowWindowCommand, + ShowWindowCommand = ((int)shInfo.ShowWindowCommand).ToShowWindowCommand(), IsUrl = isUrl, }; } @@ -411,7 +411,7 @@ CancellationToken cancellationToken Arguments = shInfo.Arguments, WorkingDirectory = shInfo.WorkingDirectory, RunAsAdmin = shInfo.RunAsAdmin, - ShowWindowCommand = shInfo.ShowWindowCommand, + ShowWindowCommand = ((int)shInfo.ShowWindowCommand).ToShowWindowCommand(), IsUrl = isUrl, }; } diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index a16769298368..177598c6f511 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -276,7 +276,7 @@ await DispatcherQueue.EnqueueOrInvokeAsync(() => shortcutItem.Arguments = link.Arguments; shortcutItem.WorkingDirectory = link.WorkingDirectory; shortcutItem.RunAsAdmin = link.RunAsAdmin; - shortcutItem.ShowWindowCommand = link.ShowWindowCommand; + shortcutItem.ShowWindowCommand = ((int)link.ShowWindowCommand).ToShowWindowCommand(); shortcutItem.PrimaryItemAttribute = link.IsFolder ? StorageItemTypes.Folder : StorageItemTypes.File; } }); diff --git a/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs index 1b105810bba6..628228f37056 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs @@ -20,7 +20,11 @@ public override async Task> LoadPreviewAndDetailsAsync() var zipResult = await FilesystemTasks.WrapNullable(async () => { + #if WINDOWS var arch = new SevenZipExtractor(await PreviewFile.OpenStreamForReadAsync()); +#else + var arch = new SevenZipExtractor(await OpenPreviewReadAsync(LoadCancelledTokenSource.Token)); +#endif // Force load archive (1665013614u) if (arch.ArchiveFileData is null) diff --git a/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.Windows.cs b/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs similarity index 87% rename from src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.Windows.cs rename to src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs index 49ec032accf7..65886767dd1c 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.Windows.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/MediaPreviewViewModel.cs @@ -25,9 +25,14 @@ public void TogglePlayback() public override Task> LoadPreviewAndDetailsAsync() { +#if WINDOWS var itemFile = Item.ItemFile ?? throw new InvalidOperationException("The media preview item does not have a storage file."); Source = MediaSource.CreateFromStorageFile(itemFile); +#else + // LINUX-TODO(media): desktop shows metadata only (MediaMetadataPreviewViewModel); playback needs libvlc + throw new NotSupportedException(); +#endif return Task.FromResult(new List()); } diff --git a/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.Windows.cs b/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs similarity index 93% rename from src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.Windows.cs rename to src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs index d92d1e8efced..7e7476874efe 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.Windows.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/PDFPreviewViewModel.cs @@ -35,6 +35,11 @@ public PDFPreviewViewModel(ListedItem item) public async override Task> LoadPreviewAndDetailsAsync() { +#if !WINDOWS + // LINUX-TODO(preview): desktop renders PDFs through ImagePreviewViewModel + await Task.CompletedTask; + throw new NotSupportedException(); +#else var fileStream = await PreviewFile.OpenReadAsync(); var pdf = await PdfDocument.LoadFromStreamAsync(fileStream); _ = TryLoadPagesAsync(pdf, fileStream); @@ -46,6 +51,7 @@ public async override Task> LoadPreviewAndDetailsAsync() }; return details; +#endif } public async Task TryLoadPagesAsync(PdfDocument pdf, IRandomAccessStream fileStream) diff --git a/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.Windows.cs b/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs similarity index 78% rename from src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.Windows.cs rename to src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs index f3905b176d74..dbc207d826d2 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.Windows.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/RichTextPreviewViewModel.cs @@ -14,9 +14,15 @@ public RichTextPreviewViewModel(ListedItem item) : base(item) { } public async override Task> LoadPreviewAndDetailsAsync() { +#if WINDOWS Stream = await PreviewFile.OpenReadAsync(); return []; +#else + // LINUX-TODO(preview): no RTF renderer on desktop + await Task.CompletedTask; + throw new NotSupportedException(); +#endif } } } diff --git a/src/Files.App/Views/Properties/ShortcutPage.xaml.cs b/src/Files.App/Views/Properties/ShortcutPage.xaml.cs index 3dbcea02214b..b7abd72b569c 100644 --- a/src/Files.App/Views/Properties/ShortcutPage.xaml.cs +++ b/src/Files.App/Views/Properties/ShortcutPage.xaml.cs @@ -40,7 +40,7 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => ViewModel.ShortcutItemArguments, ViewModel.ShortcutItemWorkingDir, ViewModel.RunAsAdmin, - (Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD)(int)ViewModel.ShowWindowCommand) + ViewModel.ShowWindowCommand) ); return true; From 1a985d52e2b2b6d4ba7c5406a687221f5df8535a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 22:20:00 +0300 Subject: [PATCH 3/4] Guard clipboard publish, keep Windows shelf path, tidy preprocessor blocks Co-Authored-By: GPT-6.1-sol (OpenAI Codex) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- .../Helpers/TransferHelpers.Windows.cs | 6 ------ src/Files.App/Helpers/TransferHelpers.cs | 17 +++++++++++++++-- .../UserControls/Pane/ShelfPane.xaml.cs | 13 +++++++++++++ .../Previews/ArchivePreviewViewModel.cs | 2 +- .../Previews/TextPreviewViewModel.cs | 2 +- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Files.App/Helpers/TransferHelpers.Windows.cs b/src/Files.App/Helpers/TransferHelpers.Windows.cs index 0c0e1753fbd9..e3550da46afa 100644 --- a/src/Files.App/Helpers/TransferHelpers.Windows.cs +++ b/src/Files.App/Helpers/TransferHelpers.Windows.cs @@ -67,9 +67,6 @@ await itemsToTransfer.ParallelForEachAsync(async storable => dataPackage.SetStorageItems(items, false); Clipboard.SetContent(dataPackage); -#if !WINDOWS - await Files.App.Services.Desktop.DesktopFileDragHelper.PublishFilesAsync(itemsToTransfer.Select(x => x.Id), type); -#endif } catch (Exception ex) { @@ -182,9 +179,6 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => dataPackage.SetStorageItems(items, false); Clipboard.SetContent(dataPackage); -#if !WINDOWS - await Files.App.Services.Desktop.DesktopFileDragHelper.PublishFilesAsync(context.SelectedItems.Select(x => x.ItemPath), type); -#endif } catch (Exception ex) { diff --git a/src/Files.App/Helpers/TransferHelpers.cs b/src/Files.App/Helpers/TransferHelpers.cs index ff704376961a..31a2c7147ffb 100644 --- a/src/Files.App/Helpers/TransferHelpers.cs +++ b/src/Files.App/Helpers/TransferHelpers.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. #if !WINDOWS +using Microsoft.Extensions.Logging; using Windows.ApplicationModel.DataTransfer; using Files.App.Services.Desktop; @@ -13,7 +14,7 @@ public static async Task ExecuteTransferAsync(IReadOnlyList itemsToTr { var paths = itemsToTransfer.Select(x => x.Id).ToArray(); FileClipboard.Set(paths, type); - await DesktopFileDragHelper.PublishFilesAsync(paths, type); + await PublishToSystemClipboardAsync(paths, type); } public static async Task ExecuteTransferAsync(IContentPageContext context, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy) @@ -25,7 +26,7 @@ public static async Task ExecuteTransferAsync(IContentPageContext context, Statu var selected = context.SelectedItems.ToList(); var paths = selected.Select(x => x.ItemPath).ToArray(); FileClipboard.Set(paths, type); - await DesktopFileDragHelper.PublishFilesAsync(paths, type); + await PublishToSystemClipboardAsync(paths, type); if (type is DataPackageOperation.Move) { await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => @@ -35,6 +36,18 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => }); } } + + private static async Task PublishToSystemClipboardAsync(string[] paths, DataPackageOperation type) + { + try + { + await DesktopFileDragHelper.PublishFilesAsync(paths, type); + } + catch (Exception ex) + { + App.Logger.LogWarning(ex, "Failed to publish the copied files to the system clipboard"); + } + } } } #endif diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 22a39a7ae3ed..52d52a0b702c 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -43,7 +43,11 @@ private async void Shelf_Drop(object sender, DragEventArgs e) return; // Get items +#if WINDOWS + var storageService = Ioc.Default.GetRequiredService(); +#else var resolver = Ioc.Default.GetRequiredService(); +#endif var storageItems = (await FilesystemHelpers.GetDraggedStorageItems(e.DataView)).ToArray(); // Add to list @@ -53,8 +57,17 @@ private async void Shelf_Drop(object sender, DragEventArgs e) if (ItemsSource.Any(x => x.Inner.Id == item.Path)) continue; +#if WINDOWS + var storable = item switch + { + StorageFileWithPath => (IStorableChild?)await storageService.TryGetFileAsync(item.Path), + StorageFolderWithPath => (IStorableChild?)await storageService.TryGetFolderAsync(item.Path), + _ => null + }; +#else var resolved = await resolver.TryGetAsync(item.Path); var storable = resolved.Item as IStorableChild; +#endif if (storable is null) continue; diff --git a/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs index 628228f37056..104be6ad8614 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ArchivePreviewViewModel.cs @@ -20,7 +20,7 @@ public override async Task> LoadPreviewAndDetailsAsync() var zipResult = await FilesystemTasks.WrapNullable(async () => { - #if WINDOWS +#if WINDOWS var arch = new SevenZipExtractor(await PreviewFile.OpenStreamForReadAsync()); #else var arch = new SevenZipExtractor(await OpenPreviewReadAsync(LoadCancelledTokenSource.Token)); diff --git a/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs index e9468de5030e..c1b626fc69ba 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/TextPreviewViewModel.cs @@ -1,7 +1,7 @@ -#if !WINDOWS // Copyright (c) Files Community // Licensed under the MIT License. +#if !WINDOWS using TextPreview = Files.App.UserControls.FilePreviews.DesktopTextPreview; using ColorCode; using Files.App.UserControls.FilePreviews; From 4d838972e186b7fce07c5b0dbead1b5106773f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Mon, 5 Oct 2026 22:22:42 +0300 Subject: [PATCH 4/4] Ratchet compat-off baseline after merge Co-Authored-By: GPT-6.1-sol (OpenAI Codex) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- docs/linux-port/compat-off-baseline.txt | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/docs/linux-port/compat-off-baseline.txt b/docs/linux-port/compat-off-baseline.txt index 5b2d54294b55..4b1653516cc4 100644 --- a/docs/linux-port/compat-off-baseline.txt +++ b/docs/linux-port/compat-off-baseline.txt @@ -1,32 +1,14 @@ # Compiler error keys of: dotnet build src/Files.App -f net10.0-desktop -p:FilesWin32Compat=false # One "file: error CODE: message" per line. Regenerate with scripts/linux/compat-off-check.sh --update. -src/Files.App/Data/Factories/ShellContextFlyoutHelper.cs: error CS0246: The type or namespace name 'ContextMenu' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/ContextMenu.cs: error CS0246: The type or namespace name 'MENU_ITEM_TYPE' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Data/Items/ListedItem.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Data/Items/ShellLinkItem.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/WidgetRecentItem.cs: error CS0246: The type or namespace name 'IShellItem' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) -src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) -src/Files.App/Helpers/UI/UIFilesystemHelpers.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/UserControls/Toolbar.xaml.cs: error CS0246: The type or namespace name 'OpenWithMenu' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/Enumerators/Win32StorageEnumerator.cs: error CS0246: The type or namespace name 'Win32PInvoke' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/Helpers/FileTypesHelper.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Utils/Storage/Helpers/FilesystemResult.cs: error CS0122: 'BOOL' is inaccessible due to its protection level src/Files.App/Utils/Storage/Helpers/FolderHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Helpers/SyncRootHelpers.cs: error CS0234: The type or namespace name 'WinRT' does not exist in the namespace 'Windows.Win32.System' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0103: The name 'SHOW_WINDOW_CMD' does not exist in the current context -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0122: 'HRESULT' is inaccessible due to its protection level -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0122: 'HWND' is inaccessible due to its protection level -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0234: The type or namespace name 'FILEOPERATION_FLAGS' does not exist in the namespace 'Windows.Win32.UI.Shell' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0234: The type or namespace name 'PROPERTYKEY' does not exist in the namespace 'Windows.Win32.Foundation' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0234: The type or namespace name 'SLR_FLAGS' does not exist in the namespace 'Windows.Win32.UI.Shell' (are you missing an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'Disposable' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'ShellFileOperations2' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FileOperationsHelpers.cs: error CS0246: The type or namespace name 'ShellItem' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) +src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0246: The type or namespace name 'FindCloseSafeHandle' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/Search/FolderSearch.cs: error CS0246: The type or namespace name 'WIN32_FIND_DATAW' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/StorageItems/ShellStorageFile.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Utils/Storage/StorageItems/ShellStorageFolder.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) @@ -35,4 +17,3 @@ src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs: error CS0122: src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs: error CS0122: 'STGMEDIUM' is inaccessible due to its protection level src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/ViewModels/ShellViewModel.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) -src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?)