diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml
index 6bfdfa6438c8..2f1a798bc881 100644
--- a/.github/workflows/ci-linux.yml
+++ b/.github/workflows/ci-linux.yml
@@ -71,9 +71,17 @@ jobs:
- name: Build Files.App.Storage (Uno desktop)
run: dotnet build src/Files.App.Storage/Files.App.Storage.csproj -c Release -f net10.0-desktop
- # Informational until the Phase 2 error inventory (docs/linux-port/phase2-errors.md) reaches zero.
- - name: Build Files.App (Uno desktop)
- run: dotnet build src/Files.App/Files.App.csproj -c Release -f net10.0-desktop
+ # Blocking: Win32 compat mode is off (the default since P4-Z), so no Win32/Shell code and no CsWin32 assembly may be needed.
+ - name: Build Files.App (Uno desktop, Win32 compat off)
+ run: dotnet build src/Files.App/Files.App.csproj -c Release -f net10.0-desktop -p:FilesWin32Compat=false
+
+ - name: Verify the desktop build did not build CsWin32
+ shell: bash
+ run: |
+ if [ -d src/Files.App.CsWin32/bin ] || [ -d src/Files.App.CsWin32/obj ]; then
+ echo "::error::Files.App.CsWin32 was built for the desktop target; something still depends on Win32 compat mode."
+ exit 1
+ fi
- name: Build and test platform test projects (if any)
shell: bash
diff --git a/Directory.Build.props b/Directory.Build.props
index 3db5d9e4871f..1c84d532f745 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -9,8 +9,8 @@
10.0.26100.67-preview
$(BaseTargetFramework)-windows$(TargetWindowsVersion)
$(BaseTargetFramework)-desktop
-
- true
+
+ false
preview
nullable
diff --git a/docs/linux-port/PLAN.md b/docs/linux-port/PLAN.md
index 89d443d8d8bc..dd7dfeea2141 100644
--- a/docs/linux-port/PLAN.md
+++ b/docs/linux-port/PLAN.md
@@ -43,7 +43,7 @@ The owner decided to focus on Linux first. Windows support is revisited only aft
| 1 | Abstraction skeleton (`Files.Platform.*`, tests, DI) | serial, small |
| 2 | **Linux bring-up: make `Files.App` compile and launch on Uno `net10.0-desktop`.** Retarget Controls + App to Uno. Exclude Windows-only folders (`Compile Remove`). Replace Win32/WinRT calls with interface calls, backed by stub or simple Linux implementations. Fix compile errors folder by folder. **✅ Done (2026-10-05):** the app compiles, launches and runs on Linux; the compat-off criterion moved to Phase 4. | parallel by folder ownership |
| 3 | Real Linux backends per interface (enumeration, watcher, file ops, trash, launcher, thumbnails, drives, clipboard…), with unit tests | parallel, one agent per interface |
-| 4 | Replace the legacy WinRT storage layer (`BaseStorageFile`/`Folder`) with OwlCore storables, then switch Win32 compat mode off (moved from Phase 2) | parallel work packages (see Phase 4 plan, Revision 3) |
+| 4 | Replace the legacy WinRT storage layer (`BaseStorageFile`/`Folder`) with OwlCore storables, then switch Win32 compat mode off (moved from Phase 2). **✅ Done (2026-10-05, P4-Z):** compat mode is off by default and the desktop build has 0 errors, which also meets Phase 2's criterion | parallel work packages (see Phase 4 plan, Revision 3) |
| 5 | UI blockers and UX parity: L-DND, SpeedGraph, StickyHeader, title bar, previews, single instance | parallel |
| 6 | Desktop integration and packaging (FileManager1 D-Bus, `.desktop`, Flatpak/AppImage/AUR) | parallel |
| 7 | Hardening (performance, HiDPI, themes, localisation) | parallel |
@@ -53,7 +53,7 @@ The Phase 2 workstreams in §7.3 still define folder ownership. Their goal is no
not "Windows code wrapped without behaviour change".
### Phase 2 build decisions (2026-10-04)
-- **Win32 compat mode is the default** (`FilesWin32Compat=true` in `Directory.Build.props`). Existing Win32/Shell code compiles on Linux against the CsWin32 assembly, which is built automatically for Platform=x64. It cannot run on Linux, so any call that is reached at runtime must be replaced by a `Files.Platform` service. Switching compat mode off was Phase 2's exit criterion; since 2026-10-05 it is Phase 4's (P4-Z), because it depends on removing the legacy storage layer.
+- **Win32 compat mode is off by default since P4-Z (2026-10-05)** (`FilesWin32Compat=false` in `Directory.Build.props`). The desktop build no longer compiles Win32/Shell code or builds the CsWin32 assembly. `-p:FilesWin32Compat=true` still works as an opt-in inventory lens: it compiles the Windows-only folders against CsWin32 and defines `FILES_WIN32_COMPAT`. Nothing compiled that way runs on Linux. Linux CI builds with compat off and fails on any error. (Until P4-Z compat mode was the default. Switching it off was Phase 2's exit criterion and became Phase 4's on 2026-10-05.)
- **Nullable diagnostics are warnings on `-desktop`**, because Uno's annotations differ from WinUI's. Re-enable once the build is clean.
- Stubbed behaviour is tagged `// LINUX-TODO()`. Use `rg -n LINUX-TODO src` to find the backlog.
@@ -247,7 +247,8 @@ Each workstream: define its interface methods from real call sites, implement th
(`Files.Core.Storage`) + the Phase 2 interfaces. `ShellViewModel` first, then properties, search, archives (`Zip*`),
FTP, virtual folders.
- Kept on a single owner (or a strictly serialized queue), because almost everything touches `ShellViewModel`.
-- **Exit:** `Utils/Storage/StorageItems/*` is no longer referenced by the Linux build.
+- **Exit:** `Utils/Storage/StorageItems/*` is no longer referenced by the Linux build. *Superseded by the Revision 3 exit below, which is met. The legacy items
+ (`FtpStorage*`, `SystemStorage*`, `ZipStorageFolder.IsZipPath`, `BaseStorage*`) still compile on Linux behind `LINUX-TODO(storage)` and remain follow-up work.*
#### Phase 4 plan, Revision 3 (2026-10-05): drop the legacy storage layer, then switch Win32 compat off
The two are one project: the legacy items (`Zip*`, `NativeStorageFile`, `VirtualStorageItem`, `Shell*`, `SystemStorageFolder`) call
@@ -287,6 +288,8 @@ Add first, delete last; default build stays green. Contracts (P4-0, then frozen)
| P4-Z | 1–2 | Flip `FilesWin32Compat=false`, fix leftovers, delete `#if WINDOWS` IStorageItem members, CI job blocking | all | strong |
Order: P4-0 → wave 1a (K, A, B, C, H, I) → wave 1b (F, D, G, J) → E → Z. Critical path ≈ 10 working days.
+**Status (2026-10-05):** all 14 packages are merged or in review; P4-E (#73) and P4-Z close the phase. Compat off by default, 0 errors, headless smoke
+test passed (browse, copy/move, trash/restore, browse into a zip, Properties, search, live updates), and the Linux CI compat-off build is blocking.
Gate after every merge: normal desktop build passes; `-p:FilesWin32Compat=false` error count ratchets down vs
`docs/linux-port/compat-off-baseline.txt`; `rg --no-ignore 'BaseStorage|StorageItems\.|Windows\.Win32|PInvoke\.|Win32Helper\.'` on the WP's
files (outside `*.Windows.cs`) → 0. Exit: compat off by default, 0 errors, headless smoke run passes, CI compat-off job blocking.
diff --git a/docs/linux-port/compat-off-baseline.txt b/docs/linux-port/compat-off-baseline.txt
index 5e497d2e8a98..ec88cc1608cf 100644
--- a/docs/linux-port/compat-off-baseline.txt
+++ b/docs/linux-port/compat-off-baseline.txt
@@ -1,5 +1,2 @@
# 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/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/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/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?)
diff --git a/docs/linux-port/phase2-errors.md b/docs/linux-port/phase2-errors.md
index af63bbe759cd..2ae4e5c76a07 100644
--- a/docs/linux-port/phase2-errors.md
+++ b/docs/linux-port/phase2-errors.md
@@ -1,5 +1,10 @@
# Phase 2: Files.App on Uno (net10.0-desktop) build inventory
+> **Status (2026-10-05, P4-Z): resolved.** Win32 compat mode is off by default, and `dotnet build src/Files.App -f net10.0-desktop` reports
+> 0 errors without building CsWin32. The last iterations went 2 → 53 → 2 → 0 errors (declaration errors hid the body errors), plus one more
+> round after `IStorageItemWithPath.Item` became Windows-only (10 → 0). `-p:FilesWin32Compat=true` remains as an opt-in lens and still
+> builds. The inventory below is historical.
+
Branch `linux/p2-head`. Generated from `dotnet build src/Files.App -f net10.0-desktop` (Uno.Sdk 6.7.30, Uno.WinUI 6.7.135).
## How the build is organised
diff --git a/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs b/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs
index 6e57587c0a9a..8e0939f003ae 100644
--- a/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs
+++ b/src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs
@@ -58,7 +58,11 @@ await confirmationDialog.TryShowAsync() is ContentDialogResult.Primary)
{
var banner = StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.InProgress);
+#if WINDOWS
bool result = await STATask.Run(StorageTrashBinService.EmptyTrashBin, App.Logger);
+#else
+ bool result = await Task.Run(StorageTrashBinService.EmptyTrashBin);
+#endif
StatusCenterViewModel.RemoveItem(banner);
diff --git a/src/Files.App/Data/Items/ContextMenu.cs b/src/Files.App/Data/Items/ContextMenu.cs
index 091657de6a25..399e6632e5bc 100644
--- a/src/Files.App/Data/Items/ContextMenu.cs
+++ b/src/Files.App/Data/Items/ContextMenu.cs
@@ -1,7 +1,9 @@
// Copyright (c) Files Community
// Licensed under the MIT License.
+#if WINDOWS || FILES_WIN32_COMPAT
using Windows.Win32.UI.WindowsAndMessaging;
+#endif
namespace Files.App.Data.Items
{
@@ -31,7 +33,9 @@ public class Win32ContextMenuItem
public int ID { get; set; } // Valid only in current menu to invoke item
public string? Label { get; set; }
public string? CommandString { get; set; }
+#if WINDOWS || FILES_WIN32_COMPAT
public MENU_ITEM_TYPE Type { get; set; }
+#endif
public List? SubItems { get; set; }
}
}
diff --git a/src/Files.App/Data/Items/WidgetRecentItem.cs b/src/Files.App/Data/Items/WidgetRecentItem.cs
index 5f27915f1c57..7cd7a94f3bc8 100644
--- a/src/Files.App/Data/Items/WidgetRecentItem.cs
+++ b/src/Files.App/Data/Items/WidgetRecentItem.cs
@@ -2,7 +2,9 @@
// SPDX-License-Identifier: MPL-2.0
using Microsoft.UI.Xaml.Media.Imaging;
+#if WINDOWS || FILES_WIN32_COMPAT
using Windows.Win32.UI.Shell;
+#endif
namespace Files.App.Data.Items
{
@@ -31,10 +33,12 @@ public BitmapImage? Icon
///
public required DateTime LastModified { get; set; }
+#if WINDOWS || FILES_WIN32_COMPAT
///
- /// Gets or initializes the Windows shell item of the recent item; unused on Linux.
+ /// Gets or initializes the Windows shell item of the recent item.
///
public IShellItem? ShellItem { get; init; }
+#endif
///
/// Loads thumbnail icon of the recent item.
diff --git a/src/Files.App/Data/Models/PinnedFoldersManager.cs b/src/Files.App/Data/Models/PinnedFoldersManager.cs
index db181e14bd98..dd4256f21f7b 100644
--- a/src/Files.App/Data/Models/PinnedFoldersManager.cs
+++ b/src/Files.App/Data/Models/PinnedFoldersManager.cs
@@ -138,12 +138,17 @@ public async Task CreateLocationItemFromPathAsync(string path)
private static string? GetLibraryDisplayName(string libraryPath)
{
+#if !WINDOWS
+ // Windows libraries (.library-ms) don't exist on Linux
+ return null;
+#else
using var storable = WindowsStorable.TryParse(libraryPath);
if (storable is null)
return null;
var displayName = storable.GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY);
return string.IsNullOrEmpty(displayName) ? null : displayName;
+#endif
}
private async Task LoadIconForLocationItemAsync(LocationItem locationItem, string path, bool isFolder = true)
diff --git a/src/Files.App/Extensions/ShellNewEntryExtensions.cs b/src/Files.App/Extensions/ShellNewEntryExtensions.cs
index b29a3badb133..77eca697b6bc 100644
--- a/src/Files.App/Extensions/ShellNewEntryExtensions.cs
+++ b/src/Files.App/Extensions/ShellNewEntryExtensions.cs
@@ -8,6 +8,7 @@ namespace Files.App.Extensions
{
public static class ShellNewEntryExtensions
{
+#if WINDOWS || FILES_WIN32_COMPAT
public static async Task> GetNewContextMenuEntries()
{
var shellEntryList = new List();
@@ -25,6 +26,7 @@ public static async Task> GetNewContextMenuEntries()
{
return await SafetyExtensions.IgnoreExceptions(() => ShellNewMenuHelper.GetNewContextMenuEntryForType(extension), App.Logger);
}
+#endif
public static async Task> Create(this ShellNewEntry shellEntry, string filePath, IShellPage associatedInstance)
{
diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj
index 82dd3eb7800d..9b9b62f4df4f 100644
--- a/src/Files.App/Files.App.csproj
+++ b/src/Files.App/Files.App.csproj
@@ -261,6 +261,7 @@
+
@@ -291,6 +292,9 @@
the Windows TFM with Platform=x64, which works on Linux) so Win32-touching code gets past declaration binding and the compiler
reports the remaining, Uno-specific errors. Nothing here runs on Linux; replace Win32 usage with Files.Platform interfaces,
then drop this switch. Prerequisite: dotnet build src/Files.App.CsWin32 -p:EnableWindowsTargeting=true -p:Platform=x64 -->
+
+ $(DefineConstants);FILES_WIN32_COMPAT
+
..\Files.App.CsWin32\bin\x64\$(Configuration)\$(WindowsTargetFramework)\Files.App.CsWin32.dll
diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs
index 63c6a7632ddc..55e01bf58ffb 100644
--- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs
+++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs
@@ -369,7 +369,11 @@ public static IServiceProvider ConfigureHost(AppModel appModel)
#endif
.AddSingleton()
.AddSingleton()
+#if WINDOWS
.AddSingleton()
+#else
+ .AddSingleton()
+#endif
.AddSingleton()
.AddSingleton()
.AddSingleton()
@@ -404,7 +408,11 @@ public static IServiceProvider ConfigureHost(AppModel appModel)
#endif
.AddSingleton()
.AddSingleton()
+#if WINDOWS
.AddSingleton()
+#else
+ .AddSingleton()
+#endif
.AddSingleton()
.AddSingleton()
.AddSingleton()
diff --git a/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs b/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs
index 0738c686bbbc..79372fb96256 100644
--- a/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs
+++ b/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs
@@ -7,6 +7,7 @@
using Files.Platform.Abstractions.Mime;
using Files.Platform.Linux.Launching;
using Files.Platform.Linux.Mime;
+using Files.Shared.Helpers;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.IO;
@@ -33,6 +34,22 @@ private static async Task OpenPathLinuxAsync(string path, IShellPage assoc
return true;
}
+ // Members of an archive being browsed; the archive file itself still opens with its default application
+ if (FileExtensionHelpers.IsZipPath(path, includeRoot: false))
+ {
+ var resolved = await Ioc.Default.GetRequiredService().TryGetAsync(path);
+ if (resolved.Item is OwlCore.Storage.IFolder)
+ {
+ var foldersSettings = Ioc.Default.GetRequiredService().FoldersSettingsService;
+ await OpenPath(forceOpenInNewTab, foldersSettings.OpenFoldersInNewTab, path, associatedInstance, selectItems);
+ return true;
+ }
+
+ // LINUX-TODO(archives): open archive members by extracting them to a private temporary copy first
+ await DialogDisplayHelper.ShowDialogAsync(Strings.LinuxOpenFailedTitle.GetLocalizedResource(), Strings.LinuxOpenFailedText.GetLocalizedFormatResource(DisplaySanitizer.Field(path)));
+ return false;
+ }
+
if (!File.Exists(path))
{
await DialogDisplayHelper.ShowDialogAsync(Strings.FileNotFoundDialogTitle.GetLocalizedResource(), Strings.FileNotFoundDialogText.GetLocalizedResource());
diff --git a/src/Files.App/Platforms/Desktop/Services/DesktopStubServices.cs b/src/Files.App/Platforms/Desktop/Services/DesktopStubServices.cs
index 8458104c5de7..1664d6f0ddab 100644
--- a/src/Files.App/Platforms/Desktop/Services/DesktopStubServices.cs
+++ b/src/Files.App/Platforms/Desktop/Services/DesktopStubServices.cs
@@ -58,4 +58,35 @@ internal sealed class DesktopJumpListService : IWindowsJumpListService
public Task> GetFoldersAsync() => Task.FromResult>([]);
}
+
+ // LINUX-TODO(pickers): open file/folder choosers through the xdg-desktop-portal FileChooser; until then pickers report "cancelled"
+ internal sealed class DesktopCommonDialogService : ICommonDialogService
+ {
+ public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath, Guid? clientGuid = null)
+ {
+ filePath = string.Empty;
+ return false;
+ }
+
+ public bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath)
+ {
+ filePath = string.Empty;
+ return false;
+ }
+
+ public bool Open_NetworkConnectionDialog(nint hWnd, bool hideRestoreConnectionCheckBox = false, bool persistConnectionAtLogon = false, bool readOnlyPath = false, string? remoteNetworkName = null, bool useMostRecentPath = false) => false;
+ }
+
+ // Linux has no Start Menu pins
+ internal sealed class DesktopStartMenuService : IStartMenuService
+ {
+ [Obsolete("Use IsPinnedAsync instead. This method is used for a workaround in ListedItem class to avoid major refactoring.")]
+ public bool IsPinned(string itemPath) => false;
+
+ public Task IsPinnedAsync(IStorable storable) => Task.FromResult(false);
+
+ public Task PinAsync(IStorable storable, string? displayName = null) => Task.CompletedTask;
+
+ public Task UnpinAsync(IStorable storable) => Task.CompletedTask;
+ }
}
diff --git a/src/Files.App/Platforms/Desktop/ViewModels/ShellViewModel.Linux.cs b/src/Files.App/Platforms/Desktop/ViewModels/ShellViewModel.Linux.cs
index a785bb1805b9..688552bbca31 100644
--- a/src/Files.App/Platforms/Desktop/ViewModels/ShellViewModel.Linux.cs
+++ b/src/Files.App/Platforms/Desktop/ViewModels/ShellViewModel.Linux.cs
@@ -1,13 +1,18 @@
// Copyright (c) Files Community
// Licensed under the MIT License.
+using Files.App.Storage.Archives;
+using Files.Core.Storage.Contracts;
+using Files.Core.Storage.Enums;
using Files.Platform.Abstractions.Enumeration;
using Files.Platform.Abstractions.Trash;
using Files.Platform.Abstractions.Watching;
using Files.Platform.Linux.Enumeration;
using Files.Platform.Linux.Mime;
using Files.Platform.Linux.Watching;
+using Files.Shared.Helpers;
using Microsoft.Extensions.Logging;
+using OwlCore.Storage;
using System.Globalization;
using System.IO;
using Windows.Storage;
@@ -30,6 +35,9 @@ public sealed partial class ShellViewModel
private CancellationTokenSource? _linuxChangeCts;
private int _linuxChangePending;
+ // The listed location as a storable; set for archive folders, which have no WinRT equivalent
+ private StorableWithPath? currentStorable;
+
///
/// Lists into filesAndFolders. Returns 3 on success and -1 on failure.
///
@@ -38,6 +46,10 @@ private async Task EnumerateLinuxFolderAsync(string path, CancellationToken
if (path.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal))
return await EnumerateLinuxTrashAsync(path, cancellationToken);
+ currentStorable = null;
+ if (FileExtensionHelpers.IsZipPath(path))
+ return await EnumerateLinuxArchiveAsync(path, cancellationToken, library);
+
if (!Directory.Exists(path))
{
ShowLocationInaccessibleOrMissing(path);
@@ -148,6 +160,127 @@ await Task.Run(async () =>
return 3;
}
+ ///
+ /// Lists an archive root or a folder inside an archive through (read-only, no watcher).
+ /// Returns 5 on success and -1 on failure.
+ ///
+ private async Task EnumerateLinuxArchiveAsync(string path, CancellationToken cancellationToken, LibraryItem? library)
+ {
+ HasNoWatcher = true;
+
+ StorableResult resolved;
+ try
+ {
+ resolved = await Ioc.Default.GetRequiredService().TryGetAsync(path, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ // Not our token: the password prompt was dismissed
+ if (!cancellationToken.IsCancellationRequested)
+ ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired);
+ return -1;
+ }
+
+ if (resolved.Item is not ArchiveFolder folder)
+ {
+ if (resolved.Status is StorableStatus.Error)
+ ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged);
+ else
+ ShowLocationInaccessibleOrMissing(path);
+ return -1;
+ }
+
+ currentStorable = StorableWithPath.FromStorable(path, folder);
+
+ var containerPath = FileExtensionHelpers.GetArchiveContainerPath(path) ?? path;
+ var containerModified = File.GetLastWriteTime(containerPath);
+ CurrentFolder = library ?? new ListedItem(null)
+ {
+ PrimaryItemAttribute = StorageItemTypes.Folder,
+ ItemPropertiesInitialized = true,
+ ItemNameRaw = folder.Name,
+ ItemDateModifiedReal = containerModified,
+ ItemDateCreatedReal = containerModified,
+ ItemType = folderTypeTextLocalized,
+ FileImage = null,
+ LoadFileIcon = false,
+ ItemPath = path,
+ FileSize = null,
+ FileSizeBytes = 0,
+ };
+
+ var iconCache = Ioc.Default.GetRequiredService();
+ var iconSize = GetPreloadIconSize();
+ var fileTypeText = Strings.File.GetLocalizedResource();
+
+ try
+ {
+ await Task.Run(async () =>
+ {
+ var items = new List();
+ await foreach (var child in folder.GetItemsAsync(StorableType.All, cancellationToken))
+ {
+ var isFolder = child is IFolder;
+ var modified = child is ArchiveEntryFile { Entry.Modified: { } entryModified } ? entryModified.ToLocalTime() : containerModified;
+ var item = new ListedItem(null)
+ {
+ PrimaryItemAttribute = isFolder ? StorageItemTypes.Folder : StorageItemTypes.File,
+ ItemNameRaw = child.Name,
+ ItemDateModifiedReal = modified,
+ ItemDateCreatedReal = modified,
+ FileImage = null,
+ Opacity = 1d,
+ LoadFileIcon = false,
+ ItemPath = child.Id,
+ FileSize = null,
+ FileSizeBytes = 0,
+ ItemType = folderTypeTextLocalized,
+ };
+
+ if (child is ArchiveEntryFile file)
+ {
+ item.FileExtension = Path.GetExtension(child.Name);
+ var localizedType = string.IsNullOrEmpty(item.FileExtension) ? null : FileTypesHelper.GetLocalizedTypeName(item.FileExtension);
+ item.ItemType = !string.IsNullOrEmpty(localizedType) ? localizedType
+ : string.IsNullOrEmpty(item.FileExtension) ? fileTypeText : item.FileExtension.Trim('.') + " " + fileTypeText;
+ item.FileSizeBytes = file.Entry.Size;
+ item.FileSize = file.Entry.Size.ToSizeString();
+ }
+
+ try { item.PreloadedIconData = await iconCache.GetIconAsync(item.ItemPath, item.FileExtension, isFolder, iconSize); }
+ catch (Exception ex) { App.Logger.LogWarning(ex, "Could not load icon for an archive entry"); }
+
+ items.Add(item);
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ filesAndFolders.AddRange(items);
+ await OrderFilesAndFoldersAsync();
+ await ApplyFilesAndFoldersChangesAsync();
+ }, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired);
+ return -1;
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarning(ex, "Could not list an archive");
+ ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged);
+ return -1;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ return -1;
+
+ IsLocationUnavailable = false;
+ return 5;
+ }
+
private async Task LoadLinuxRepositoryPropertiesAsync(CancellationToken cancellationToken)
{
if (!IsValidGitDirectory || EnabledGitProperties is GitProperties.None)
diff --git a/src/Files.App/Services/PreviewPopupProviders/PreviewPopupService.cs b/src/Files.App/Services/PreviewPopupProviders/PreviewPopupService.cs
index f110042cd1a6..7d13aa56974f 100644
--- a/src/Files.App/Services/PreviewPopupProviders/PreviewPopupService.cs
+++ b/src/Files.App/Services/PreviewPopupProviders/PreviewPopupService.cs
@@ -10,8 +10,10 @@ internal sealed partial class PreviewPopupService : ObservableObject, IPreviewPo
{
if (await QuickLookProvider.Instance.DetectAvailability())
return await Task.FromResult(QuickLookProvider.Instance);
+#if WINDOWS
if (await SeerProProvider.Instance.DetectAvailability())
return await Task.FromResult(SeerProProvider.Instance);
+#endif
if (await PowerToysPeekProvider.Instance.DetectAvailability())
return await Task.FromResult(PowerToysPeekProvider.Instance);
else
diff --git a/src/Files.App/Utils/Cloud/CloudDrivesDetector.cs b/src/Files.App/Utils/Cloud/CloudDrivesDetector.Windows.cs
similarity index 100%
rename from src/Files.App/Utils/Cloud/CloudDrivesDetector.cs
rename to src/Files.App/Utils/Cloud/CloudDrivesDetector.Windows.cs
diff --git a/src/Files.App/Utils/Cloud/Detector/CloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/CloudDetector.cs
index f04791e043e8..350dead580f1 100644
--- a/src/Files.App/Utils/Cloud/Detector/CloudDetector.cs
+++ b/src/Files.App/Utils/Cloud/Detector/CloudDetector.cs
@@ -44,9 +44,14 @@ private static IEnumerable EnumerateDetectors()
yield return new GoogleDriveCloudDetector();
yield return new DropBoxCloudDetector();
yield return new BoxCloudDetector();
+#if WINDOWS
+ // Registry and executable icon based
yield return new GenericCloudDetector();
+#endif
yield return new SynologyDriveCloudDetector();
+#if WINDOWS
yield return new OXDriveCloudDetector();
+#endif
}
}
}
diff --git a/src/Files.App/Utils/Cloud/Detector/GenericCloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/GenericCloudDetector.Windows.cs
similarity index 100%
rename from src/Files.App/Utils/Cloud/Detector/GenericCloudDetector.cs
rename to src/Files.App/Utils/Cloud/Detector/GenericCloudDetector.Windows.cs
diff --git a/src/Files.App/Utils/Cloud/Detector/OXDriveCloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/OXDriveCloudDetector.Windows.cs
similarity index 100%
rename from src/Files.App/Utils/Cloud/Detector/OXDriveCloudDetector.cs
rename to src/Files.App/Utils/Cloud/Detector/OXDriveCloudDetector.Windows.cs
diff --git a/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs b/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs
index f1001c78a214..a1f38d43f438 100644
--- a/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs
+++ b/src/Files.App/Utils/Storage/Helpers/IStorageItemWithPath.cs
@@ -13,8 +13,9 @@ public interface IStorageItemWithPath
public string Path { get; }
- // LINUX-TODO(storage): move under #if WINDOWS in P4-Z; desktop consumers (e.g. StorageHelpers.ToStorageItemResult) still read it
+#if WINDOWS
public IStorageItem? Item { get; }
+#endif
///
/// Gets the storable for this item, if one has been resolved; otherwise, .
diff --git a/src/Files.App/Utils/Storage/Helpers/MtpHelpers.cs b/src/Files.App/Utils/Storage/Helpers/MtpHelpers.Windows.cs
similarity index 100%
rename from src/Files.App/Utils/Storage/Helpers/MtpHelpers.cs
rename to src/Files.App/Utils/Storage/Helpers/MtpHelpers.Windows.cs
diff --git a/src/Files.App/Utils/Storage/Helpers/StorableWithPath.cs b/src/Files.App/Utils/Storage/Helpers/StorableWithPath.cs
index 35339d6428a4..68503787eaab 100644
--- a/src/Files.App/Utils/Storage/Helpers/StorableWithPath.cs
+++ b/src/Files.App/Utils/Storage/Helpers/StorableWithPath.cs
@@ -27,7 +27,9 @@ public sealed record StorableWithPath : IStorageItemWithPath
///
public string Name => Storable?.Name ?? IO.Path.GetFileName(IO.Path.TrimEndingDirectorySeparator(Path));
+#if WINDOWS
IStorageItem? IStorageItemWithPath.Item => null;
+#endif
///
/// Initializes a new instance of the record.
diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs
index 880213b48dd6..43fdebec4377 100644
--- a/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs
+++ b/src/Files.App/Utils/Storage/Helpers/StorageFileWithPath.cs
@@ -11,7 +11,9 @@ public sealed class StorageFileWithPath : IStorageItemWithPath
public string Path { get; }
public string Name => Item?.Name ?? IO.Path.GetFileName(Path)!;
+#if WINDOWS
IStorageItem? IStorageItemWithPath.Item => Item;
+#endif
public BaseStorageFile? Item { get; }
public FilesystemItemType ItemType => FilesystemItemType.File;
diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs b/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs
index 7a93240dc75e..ee8e81b08ac5 100644
--- a/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs
+++ b/src/Files.App/Utils/Storage/Helpers/StorageFolderWithPath.cs
@@ -11,7 +11,9 @@ public sealed class StorageFolderWithPath : IStorageItemWithPath
public string Path { get; }
public string Name => Item?.Name ?? IO.Path.GetFileName(Path)!;
+#if WINDOWS
IStorageItem? IStorageItemWithPath.Item => Item;
+#endif
public BaseStorageFolder? Item { get; }
public FilesystemItemType ItemType => FilesystemItemType.Directory;
diff --git a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs
index 98a8b481cb2c..1ec0416f9fb4 100644
--- a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs
+++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.Storables.cs
@@ -38,5 +38,15 @@ public static async Task> ToStorableResult(this ISto
return result;
}
+ ///
+ /// Gets whether the item is a member of an archive; such items can only be copied out.
+ ///
+ public static bool IsArchiveMember(this IStorageItemWithPath item)
+#if WINDOWS
+ => item.Item is ZipStorageFile || item.Item is ZipStorageFolder;
+#else
+ => item.Storable is Files.App.Storage.Archives.ArchiveEntryFile or Files.App.Storage.Archives.ArchiveFolder
+ || Files.Shared.Helpers.FileExtensionHelpers.IsZipPath(item.Path, includeRoot: false);
+#endif
}
}
diff --git a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs
index d89a5751f889..00205c1eb116 100644
--- a/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs
+++ b/src/Files.App/Utils/Storage/Helpers/StorageHelpers.cs
@@ -9,10 +9,7 @@ namespace Files.App.Helpers
public static partial class StorageHelpers
{
#if !WINDOWS
- // LINUX-TODO(storage): remove these WinRT entry points as downstream work packages adopt ToStorableResult.
- public static async Task ToStorageItem(this IStorageItemWithPath item)
- => (await item.ToStorageItemResult()).Result;
-
+ // LINUX-TODO(storage): remove the remaining WinRT entry points (ToStorageItem, FromStorageItem) once their callers use storables.
public static async Task ToStorageItem(string path) where TRequested : IStorageItem
{
var result = await GetStorableAsync(path);
@@ -25,22 +22,6 @@ public static partial class StorageHelpers
return item is TRequested requested ? requested : default;
}
- public static async Task> ToStorageItemResult(this IStorageItemWithPath item)
- {
- var result = await item.ToStorableResult();
- if (!result)
- return new(null, result.ErrorCode);
-
- IStorageItem? legacy = item.Item
- ?? (result.Result is IFolder
- ? await StorageFileExtensions.OpenLegacyFolderAsync(item.Path)
- : await StorageFileExtensions.OpenLegacyFileAsync(item.Path));
- return new(legacy, legacy is null ? FileSystemStatusCode.Generic : FileSystemStatusCode.Success);
- }
-
- public static async Task GetFileSize(this IStorageFile file)
- => (long)(await file.GetBasicPropertiesAsync()).Size;
-
public static IStorageItemWithPath FromPathAndType(string customPath, FilesystemItemType? itemType)
=> new StorableWithPath(customPath, itemType ?? FilesystemItemType.Directory);
@@ -54,9 +35,6 @@ public static bool Exists(string path)
=> FromPathAndType(!string.IsNullOrEmpty(item?.Path) ? item.Path : customPath
?? throw new InvalidOperationException("A path is required when converting a missing storage item."),
item is null ? itemType : item.IsOfType(StorageItemTypes.Folder) ? FilesystemItemType.Directory : FilesystemItemType.File);
-
- public static FilesystemResult ToType(FilesystemResult result) where T : class
- => new(result.Result as T, result.ErrorCode);
#endif
}
}
diff --git a/src/Files.App/Utils/Storage/Helpers/StorageSenseHelper.cs b/src/Files.App/Utils/Storage/Helpers/StorageSenseHelper.cs
index b66db47004a5..4cfb2f63dc16 100644
--- a/src/Files.App/Utils/Storage/Helpers/StorageSenseHelper.cs
+++ b/src/Files.App/Utils/Storage/Helpers/StorageSenseHelper.cs
@@ -10,6 +10,10 @@ internal sealed class StorageSenseHelper
{
public static async Task OpenStorageSenseAsync(string path)
{
+#if !WINDOWS
+ // LINUX-TODO(storage-sense): no system storage cleanup page to open
+ await Task.CompletedTask;
+#else
if (!path.StartsWith(Constants.UserEnvironmentPaths.SystemDrivePath, StringComparison.OrdinalIgnoreCase)
&& ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
{
@@ -19,6 +23,7 @@ public static async Task OpenStorageSenseAsync(string path)
{
await Launcher.LaunchUriAsync(new Uri("ms-settings:storagesense"));
}
+#endif
}
}
}
diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
index e56090bbb1cc..349e1deb66a4 100644
--- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
+++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
@@ -190,9 +190,7 @@ public async Task DragOverAsync(DragEventArgs? e)
// Some applications such as Edge can't raise the drop event by the Move flag (#14008), so we set the Copy flag as well.
e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy;
}
- else if (draggedItems.Any(x =>
- x.Item is ZipStorageFile ||
- x.Item is ZipStorageFolder) ||
+ else if (draggedItems.Any(x => x.IsArchiveMember()) ||
ZipStorageFolder.IsZipPath(workingDirectory))
{
e.DragUIOverride.Caption = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), folderName);
diff --git a/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs b/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs
index f679f6d2f8de..eccb2a35f8e5 100644
--- a/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs
+++ b/src/Files.App/ViewModels/Properties/Items/LibraryProperties.cs
@@ -44,7 +44,12 @@ public override void GetBaseProperties()
public async override Task GetSpecialPropertiesAsync()
{
var libraryPath = Library.GetRequiredPath();
+#if WINDOWS
var fileAttributes = Win32Helper.GetFileAttributes(libraryPath);
+#else
+ // Windows libraries (.library-ms) don't exist on Linux
+ var fileAttributes = default(System.IO.FileAttributes);
+#endif
ViewModel.IsReadOnly = fileAttributes.HasFlag(System.IO.FileAttributes.ReadOnly);
ViewModel.IsHidden = fileAttributes.HasFlag(System.IO.FileAttributes.Hidden);
ViewModel.CanCompressContent = false;
@@ -141,6 +146,7 @@ private async Task GetLibrarySizeAsync(List storageFolders, C
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
+#if WINDOWS
var libraryPath = Library.GetRequiredPath();
switch (e.PropertyName)
{
@@ -166,6 +172,7 @@ private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.Pro
break;
}
+#endif
}
}
}
diff --git a/src/Files.App/ViewModels/ShellViewModel.Windows.cs b/src/Files.App/ViewModels/ShellViewModel.Windows.cs
new file mode 100644
index 000000000000..772e11ccca35
--- /dev/null
+++ b/src/Files.App/ViewModels/ShellViewModel.Windows.cs
@@ -0,0 +1,938 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+using Files.Shared.Helpers;
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Windows.Foundation;
+using Windows.Storage;
+using Windows.Storage.FileProperties;
+using Windows.Storage.Search;
+using Windows.Win32;
+using Windows.Win32.System.SystemServices;
+using static Files.App.Helpers.Win32PInvoke;
+using ByteSize = ByteSizeLib.ByteSize;
+using FileAttributes = System.IO.FileAttributes;
+
+namespace Files.App.ViewModels
+{
+ ///
+ /// Windows folder listing (FindFirstFileEx, StorageFolder queries), ReadDirectoryChangesW watchers, alternate data streams and
+ /// Recycle Bin shell items of . Kept byte-identical to upstream where possible.
+ ///
+ public sealed partial class ShellViewModel
+ {
+ private async void RecycleBinRefreshRequestedAsync(object? sender, FileSystemEventArgs e)
+ {
+ if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase))
+ return;
+
+ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ RefreshItems(null);
+ });
+ }
+
+ private async void RecycleBinItemDeletedAsync(object? sender, FileSystemEventArgs e)
+ {
+ if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase))
+ return;
+
+ var removedItem = await RemoveFileOrFolderAsync(e.FullPath);
+
+ if (removedItem is not null)
+ await ApplyFilesAndFoldersChangesAsync();
+ }
+
+ private async void RecycleBinItemCreatedAsync(object? sender, FileSystemEventArgs e)
+ {
+ if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase))
+ return;
+
+ using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(e.FullPath));
+ if (folderItem is null)
+ return;
+
+ var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem);
+ if (shellFileItem is null)
+ throw new InvalidDataException("The recycle-bin item could not be converted to a shell item.");
+
+ var newListedItem = await AddFileOrFolderFromShellFile(shellFileItem);
+ if (newListedItem is null)
+ return;
+
+ await AddFileOrFolderAsync(newListedItem);
+ await OrderFilesAndFoldersAsync();
+ await ApplyFilesAndFoldersChangesAsync();
+ }
+
+ partial void ApplyDriveStorageDetails(ListedItem item, IDictionary properties)
+ {
+ // Drive Storage Details
+ 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))
+ {
+ var maxSpace = ByteSize.FromBytes(capacityRaw);
+ var freeSpace = ByteSize.FromBytes(freeSpaceRaw);
+
+ item.MaxSpace = maxSpace;
+ item.SpaceUsed = maxSpace - freeSpace;
+ item.FileSize = string.Format(Strings.DriveFreeSpaceAndCapacity.GetLocalizedResource(), freeSpace.ToSizeString(), maxSpace.ToSizeString());
+ item.ShowDriveStorageDetails = true;
+ }
+ }
+
+ private async Task PromptToUnlockBitlockerIfLockedAsync(string path, string pathRoot)
+ {
+ try
+ {
+ var rootFolder = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path));
+ if (await FolderHelpers.CheckBitlockerStatusAsync(rootFolder, path))
+ await ContextMenu.InvokeVerb("unlock-bde", pathRoot);
+ }
+ catch (Exception ex)
+ {
+ // Runs detached, so swallow: the property probe or the unlock-bde shell verb can throw (e.g. COMException)
+ App.Logger.LogWarning(ex, ex.Message);
+ }
+ }
+
+ private async Task EnumerateWindowsFolderAsync(string path, CancellationToken cancellationToken, LibraryItem? library)
+ {
+ // Flag to use FindFirstFileExFromApp or StorageFolder enumeration - Use storage folder for Box Drive (#4629)
+ var isBoxFolder = CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder && path.StartsWith(boxFolder);
+ bool isWslDistro = path.StartsWith(@"\\wsl$\", StringComparison.OrdinalIgnoreCase) || path.StartsWith(@"\\wsl.localhost\", StringComparison.OrdinalIgnoreCase)
+ || path.Equals(@"\\wsl$", StringComparison.OrdinalIgnoreCase) || path.Equals(@"\\wsl.localhost", StringComparison.OrdinalIgnoreCase);
+ bool isMtp = path.StartsWith(@"\\?\", StringComparison.Ordinal);
+ bool isShellFolder = path.StartsWith(@"\\SHELL\", StringComparison.Ordinal);
+ bool isNetwork = path.StartsWith(@"\\", StringComparison.Ordinal) &&
+ !isMtp &&
+ !isShellFolder &&
+ !isWslDistro;
+ bool isNetdisk = false;
+
+ try
+ {
+ // Special handling for network drives
+ if (!isNetwork)
+ isNetdisk = await Task.Run(() => new DriveInfo(path).DriveType == System.IO.DriveType.Network);
+ }
+ catch { }
+
+ bool isFtp = FtpHelpers.IsFtpPath(path);
+ bool enumFromStorageFolder = isBoxFolder || isFtp;
+
+ BaseStorageFolder? rootFolder = null;
+
+ if (isNetwork || isNetdisk)
+ {
+ var auth = await NetworkService.AuthenticateNetworkShare(path, cancellationToken);
+ if (!auth)
+ return -1;
+ }
+
+ // Off the UI thread: FindFirstFileEx blocks until the SMB timeout on an unreachable share.
+ Win32PInvoke.SafeFindHandle? hFile = null;
+ WIN32_FIND_DATA findData = default;
+ int errorCode = 0;
+ if (!enumFromStorageFolder)
+ {
+ (hFile, findData, errorCode) = await Task.Run(() =>
+ {
+ var hFileTsk = FindFirstFileExFromAppSafe(
+ path + "\\*.*",
+ FINDEX_INFO_LEVELS.FindExInfoBasic,
+ out WIN32_FIND_DATA findDataTsk,
+ FINDEX_SEARCH_OPS.FindExSearchNameMatch,
+ IntPtr.Zero,
+ FIND_FIRST_EX_LARGE_FETCH);
+
+ return (hFileTsk, findDataTsk, hFileTsk.IsInvalid ? Marshal.GetLastWin32Error() : 0);
+ })
+ .WithTimeoutAsync(TimeSpan.FromSeconds(5));
+ }
+
+ if (!enumFromStorageFolder && hFile is not null && !hFile.IsInvalid)
+ {
+ // Enumerate with the handle opened above; rootFolder only used for Bitlocker
+ currentStorageFolder = null;
+ }
+ else if (workingRoot is not null)
+ {
+ var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder));
+ if (!res)
+ return -1;
+
+ var storageFolder = res.Result
+ ?? throw new InvalidOperationException("A successful folder lookup did not return a storage folder.");
+
+ currentStorageFolder = storageFolder;
+ rootFolder = storageFolder.Item;
+ enumFromStorageFolder = true;
+ }
+ else
+ {
+ var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder));
+ if (res)
+ {
+ var storageFolder = res.Result
+ ?? throw new InvalidOperationException("A successful folder lookup did not return a storage folder.");
+ currentStorageFolder = storageFolder;
+ rootFolder = storageFolder.Item;
+ }
+ else if (res == FileSystemStatusCode.Unauthorized)
+ {
+ ShowLocationInaccessibleOrMissing(path);
+
+ return -1;
+ }
+ else if (res == FileSystemStatusCode.NotFound)
+ {
+ ShowLocationInaccessibleOrMissing(path);
+
+ return -1;
+ }
+ else
+ {
+ ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged, res.ErrorCode.ToString());
+
+ return -1;
+ }
+ }
+
+ var pathRoot = Path.GetPathRoot(path);
+ if (Path.IsPathRooted(path) && pathRoot == path)
+ {
+ // Off the critical path: a locked drive fails enumeration anyway, so don't block the listing on the BitLocker probe.
+ _ = PromptToUnlockBitlockerIfLockedAsync(path, pathRoot);
+ }
+
+ HasNoWatcher = isFtp || isWslDistro || isMtp || currentStorageFolder?.Item is ZipStorageFolder;
+
+ if (enumFromStorageFolder)
+ {
+ // The handle from the open above is unused on the storage-folder path.
+ hFile?.Dispose();
+
+ var basicProps = await rootFolder?.GetBasicPropertiesAsync();
+ var currentFolder = library ?? new ListedItem(rootFolder?.FolderRelativeId ?? string.Empty)
+ {
+ PrimaryItemAttribute = StorageItemTypes.Folder,
+ ItemPropertiesInitialized = true,
+ ItemNameRaw = rootFolder?.DisplayName ?? string.Empty,
+ ItemDateModifiedReal = basicProps.DateModified,
+ ItemType = rootFolder?.DisplayType ?? string.Empty,
+ FileImage = null,
+ LoadFileIcon = false,
+ ItemPath = string.IsNullOrEmpty(rootFolder?.Path) ? currentStorageFolder?.Path ?? string.Empty : rootFolder.Path,
+ FileSize = null,
+ FileSizeBytes = 0,
+ };
+
+ if (library is null)
+ currentFolder.ItemDateCreatedReal = rootFolder?.DateCreated ?? DateTimeOffset.Now;
+
+ CurrentFolder = currentFolder;
+ await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken);
+
+ // Workaround for #7428
+ return isBoxFolder ? 2 : 1;
+ }
+ else
+ {
+ var itemModifiedDate = DateTime.Now;
+ var itemCreatedDate = DateTime.Now;
+
+ try
+ {
+ FileTimeToSystemTime(in findData.ftLastWriteTime, out var systemModifiedTimeOutput);
+ itemModifiedDate = systemModifiedTimeOutput.ToDateTime();
+
+ FileTimeToSystemTime(in findData.ftCreationTime, out SYSTEMTIME systemCreatedTimeOutput);
+ itemCreatedDate = systemCreatedTimeOutput.ToDateTime();
+ }
+ catch (ArgumentException)
+ {
+ }
+
+ var isHidden = (((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden);
+ var opacity = isHidden ? Constants.UI.DimItemOpacity : 1d;
+
+ var currentFolder = library ?? new ListedItem(null)
+ {
+ PrimaryItemAttribute = StorageItemTypes.Folder,
+ ItemPropertiesInitialized = true,
+ ItemNameRaw = rootFolder?.DisplayName ?? Path.GetFileName(path.TrimEnd('\\')),
+ ItemDateModifiedReal = itemModifiedDate,
+ ItemDateCreatedReal = itemCreatedDate,
+ ItemType = folderTypeTextLocalized,
+ FileImage = null,
+ IsHiddenItem = isHidden,
+ Opacity = opacity,
+ LoadFileIcon = false,
+ ItemPath = path,
+ FileSize = null,
+ FileSizeBytes = 0,
+ };
+
+ CurrentFolder = currentFolder;
+
+ if (hFile is null)
+ {
+ ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged);
+
+ return -1;
+ }
+ else if (hFile.IsInvalid)
+ {
+ hFile.Dispose();
+ await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken);
+
+ // errorCode == ERROR_ACCESS_DENIED
+ if (filesAndFolders.Count == 0 && errorCode == 0x5)
+ {
+ ShowLocationInaccessibleOrMissing(path);
+
+ return -1;
+ }
+
+ return 1;
+ }
+ else
+ {
+ await Task.Run(async () =>
+ {
+ List fileList = await Win32StorageEnumerator.ListEntries(path, hFile, findData, cancellationToken, -1, GetPreloadIconSize(), intermediateAction: async (intermediateList) =>
+ {
+ filesAndFolders.AddRange(intermediateList);
+
+ // The stable sort lands new items in final position without reordering visible ones; capped for huge listings
+ if (filesAndFolders.Count <= 10_000)
+ await OrderFilesAndFoldersAsync();
+
+ await ApplyFilesAndFoldersChangesAsync();
+ });
+
+ filesAndFolders.AddRange(fileList);
+
+ await OrderFilesAndFoldersAsync();
+ await ApplyFilesAndFoldersChangesAsync();
+ // Not awaited here: with Low priority these don't run until the UI thread goes idle
+ // after the final list update, which would delay load completion and watcher setup.
+ // The desktop.ini task is awaited before applying the adaptive layout, which reads DesktopIni.
+ _ = dispatcherQueue.EnqueueOrInvokeAsync(CheckForSolutionFile, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
+ desktopIniUpdateTask = dispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ GetDesktopIniFileData();
+ CheckForBackgroundImage();
+ },
+ Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
+ });
+
+ // Cache the resolved folder so the post-enum switch reuses it.
+ currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path));
+ rootFolder ??= currentStorageFolder?.Item;
+ if (rootFolder is not null)
+ {
+ if (rootFolder.DisplayName is not null)
+ currentFolder.ItemNameRaw = rootFolder.DisplayName;
+
+ if (!string.Equals(path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase))
+ {
+ enumeratedCloudSyncStatus = await CheckCloudDriveSyncStatusAsync(rootFolder);
+ currentFolder.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(enumeratedCloudSyncStatus.Value);
+ }
+ }
+
+ return 0;
+ }
+ }
+ }
+
+ private async Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath? currentStorageFolder, CancellationToken cancellationToken)
+ {
+ if (rootFolder is null)
+ return;
+
+ // Null when a concurrent navigation or dispose cleared the context; this enumeration is stale
+ if (currentStorageFolder is null)
+ return;
+
+ if (rootFolder is IPasswordProtectedItem ppis)
+ ppis.PasswordRequestedCallback = async (item) =>
+ {
+ await dispatcherQueue.EnqueueOrInvokeAsync(() => ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired));
+
+ return await UIFilesystemHelpers.RequestPassword(item);
+ };
+
+ try
+ {
+ await Task.Run(async () =>
+ {
+ List finalList = await UniversalStorageEnumerator.ListEntries(
+ rootFolder,
+ currentStorageFolder,
+ cancellationToken,
+ -1,
+ GetPreloadIconSize(),
+ async (intermediateList) =>
+ {
+ filesAndFolders.AddRange(intermediateList);
+
+ // The stable sort lands new items in final position without reordering visible ones; capped for huge listings
+ if (filesAndFolders.Count <= 10_000)
+ await OrderFilesAndFoldersAsync();
+
+ await ApplyFilesAndFoldersChangesAsync();
+ });
+
+ filesAndFolders.AddRange(finalList);
+
+ await OrderFilesAndFoldersAsync();
+ await ApplyFilesAndFoldersChangesAsync();
+ }, cancellationToken);
+
+ IsLocationUnavailable = false;
+ }
+ catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) // Password dialog dismissed
+ {
+ ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired);
+ }
+
+ if (rootFolder is IPasswordProtectedItem ppiu)
+ ppiu.PasswordRequestedCallback = null;
+ }
+
+ private async Task WatchForStorageFolderChangesAsync(BaseStorageFolder? rootFolder)
+ {
+ if (rootFolder is null)
+ return;
+
+ await Task.Run(() =>
+ {
+ var options = new QueryOptions()
+ {
+ FolderDepth = FolderDepth.Shallow,
+ IndexerOption = IndexerOption.OnlyUseIndexerAndOptimizeForIndexedProperties
+ };
+
+ options.SetPropertyPrefetch(PropertyPrefetchOptions.None, null);
+ options.SetThumbnailPrefetch(ThumbnailMode.ListView, 0, ThumbnailOptions.ReturnOnlyIfCached);
+
+ if (rootFolder.AreQueryOptionsSupported(options))
+ {
+ var itemQueryResult = rootFolder.CreateItemQueryWithOptions(options).ToStorageItemQueryResult()
+ ?? throw new InvalidOperationException("The folder query could not be converted to a storage query.");
+
+ itemQueryResult.ContentsChanged += ItemQueryResult_ContentsChanged;
+
+ // Just get one item to start getting notifications
+ var watchedItemsOperation = itemQueryResult.GetItemsAsync(0, 1);
+
+ watcherCTS.Token.Register(() =>
+ {
+ itemQueryResult.ContentsChanged -= ItemQueryResult_ContentsChanged;
+ watchedItemsOperation?.Cancel();
+ });
+ }
+ });
+ }
+
+ private void WatchForWin32FolderChanges(string? folderPath)
+ {
+ if (!Directory.Exists(folderPath))
+ return;
+
+ // NOTE: Suppressed NullReferenceException caused by EnableRaisingEvents
+ SafetyExtensions.IgnoreExceptions(() =>
+ {
+ watcher = new FileSystemWatcher
+ {
+ Path = folderPath,
+ Filter = "*.*",
+ NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName
+ };
+
+ watcher.Created += DirectoryWatcher_Changed;
+ watcher.Deleted += DirectoryWatcher_Changed;
+ watcher.Renamed += DirectoryWatcher_Changed;
+ watcher.EnableRaisingEvents = true;
+ }, App.Logger);
+ }
+
+ private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e)
+ {
+ Debug.WriteLine($"Directory watcher event: {e.ChangeType}, {e.FullPath}");
+
+ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ RefreshItems(null);
+ });
+ }
+
+ private async void ItemQueryResult_ContentsChanged(IStorageQueryResultBase sender, object args)
+ {
+ // Query options have to be reapplied otherwise old results are returned
+ var options = new QueryOptions()
+ {
+ FolderDepth = FolderDepth.Shallow,
+ IndexerOption = IndexerOption.OnlyUseIndexerAndOptimizeForIndexedProperties
+ };
+
+ options.SetPropertyPrefetch(PropertyPrefetchOptions.None, null);
+ options.SetThumbnailPrefetch(ThumbnailMode.ListView, 0, ThumbnailOptions.ReturnOnlyIfCached);
+
+ sender.ApplyNewQueryOptions(options);
+
+ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ RefreshItems(null);
+ });
+ }
+
+ private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStatus)
+ {
+ // Enumeration is fire-and-forget; don't set up a watcher on a disposed view model.
+ if (isDisposed)
+ return;
+
+ Debug.WriteLine($"WatchForDirectoryChanges: {path}");
+ var hWatchDir = Win32PInvoke.CreateFileFromApp(path, 1, 1 | 2 | 4,
+ IntPtr.Zero, 3, (uint)Win32PInvoke.File_Attributes.BackupSemantics | (uint)Win32PInvoke.File_Attributes.Overlapped, IntPtr.Zero);
+ if (hWatchDir.ToInt64() == -1)
+ return;
+
+ var hasSyncStatus = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown;
+
+ aProcessQueueAction ??= Task.Run(() => ProcessOperationQueueAsync(watcherCTS.Token, hasSyncStatus));
+
+ var aWatcherAction = Windows.System.Threading.ThreadPool.RunAsync((x) =>
+ {
+ var buff = new byte[4096];
+ 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;
+
+ if (hasSyncStatus)
+ notifyFilters |= FILE_NOTIFY_CHANGE_ATTRIBUTES;
+
+ var overlapped = new NativeOverlapped();
+ using var eventHandle = PInvoke.CreateEvent(null, false, false, null);
+ overlapped.EventHandle = eventHandle.DangerousGetHandle();
+ const uint INFINITE = 0xFFFFFFFF;
+
+ while (x.Status != AsyncStatus.Canceled)
+ {
+ unsafe
+ {
+ fixed (byte* pBuff = buff)
+ {
+ ref var notifyInformation = ref Unsafe.As(ref buff[0]);
+ if (x.Status != AsyncStatus.Canceled)
+ {
+ PInvoke.ReadDirectoryChanges(
+ new Windows.Win32.Foundation.HANDLE(hWatchDir),
+ pBuff,
+ 4096,
+ false,
+ (Windows.Win32.Storage.FileSystem.FILE_NOTIFY_CHANGE)notifyFilters,
+ null,
+ &overlapped,
+ null);
+ }
+ else
+ {
+ break;
+ }
+
+ Debug.WriteLine("waiting: {0}", rand);
+ if (x.Status == AsyncStatus.Canceled)
+ break;
+
+ var rc = WaitForSingleObjectEx(overlapped.EventHandle, INFINITE, true);
+ Debug.WriteLine("wait done: {0}", rand);
+
+ uint offset = 0;
+ ref var notifyInfo = ref Unsafe.As(ref buff[offset]);
+ if (x.Status == AsyncStatus.Canceled)
+ break;
+
+ do
+ {
+ notifyInfo = ref Unsafe.As(ref buff[offset]);
+ string? FileName = null;
+ unsafe
+ {
+ fixed (char* name = notifyInfo.FileName)
+ {
+ FileName = Path.Combine(path, new string(name, 0, (int)notifyInfo.FileNameLength / 2));
+ }
+ }
+
+ uint action = notifyInfo.Action;
+
+ Debug.WriteLine("action: {0}", action);
+
+ operationQueue.Enqueue((action, FileName));
+
+ offset += notifyInfo.NextEntryOffset;
+ }
+ while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled);
+
+ operationEvent.Set();
+
+ //ResetEvent(overlapped.hEvent);
+ Debug.WriteLine("Task running...");
+ }
+ }
+ }
+
+ operationQueue.Clear();
+
+ Debug.WriteLine("aWatcherAction done: {0}", rand);
+ });
+
+ watcherCTS.Token.Register(() =>
+ {
+ if (aWatcherAction is not null)
+ {
+ aWatcherAction?.Cancel();
+
+ // Prevent duplicate execution of this block
+ aWatcherAction = null;
+
+ Debug.WriteLine("watcher canceled");
+ }
+
+ CancelIoEx(hWatchDir, IntPtr.Zero);
+ CloseHandle(hWatchDir);
+ });
+ }
+
+ private void WatchForGitChanges()
+ {
+ // Enumeration is fire-and-forget; don't set up a watcher on a disposed view model.
+ if (isDisposed)
+ return;
+
+ var hWatchDir = Win32PInvoke.CreateFileFromApp(
+ GitDirectory!,
+ 1,
+ 1 | 2 | 4,
+ IntPtr.Zero,
+ 3,
+ (uint)Win32PInvoke.File_Attributes.BackupSemantics | (uint)Win32PInvoke.File_Attributes.Overlapped,
+ IntPtr.Zero);
+
+ if (hWatchDir.ToInt64() == -1)
+ return;
+
+ gitProcessQueueAction ??= Task.Run(() => ProcessGitChangesQueueAsync(watcherCTS.Token));
+
+ var gitWatcherAction = Windows.System.Threading.ThreadPool.RunAsync((x) =>
+ {
+ var buff = new byte[4096];
+ 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 NativeOverlapped();
+ using var eventHandle = PInvoke.CreateEvent(null, false, false, null);
+ overlapped.EventHandle = eventHandle.DangerousGetHandle();
+ const uint INFINITE = 0xFFFFFFFF;
+
+ while (x.Status != AsyncStatus.Canceled)
+ {
+ unsafe
+ {
+ fixed (byte* pBuff = buff)
+ {
+ ref var notifyInformation = ref Unsafe.As(ref buff[0]);
+ if (x.Status == AsyncStatus.Canceled)
+ break;
+
+ 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.EventHandle, INFINITE, true);
+
+ uint offset = 0;
+ ref var notifyInfo = ref Unsafe.As(ref buff[offset]);
+ if (x.Status == AsyncStatus.Canceled)
+ break;
+
+ do
+ {
+ notifyInfo = ref Unsafe.As(ref buff[offset]);
+
+ uint action = notifyInfo.Action;
+
+ gitChangesQueue.Enqueue(action);
+
+ offset += notifyInfo.NextEntryOffset;
+ }
+ while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled);
+
+ gitChangedEvent.Set();
+ }
+ }
+ }
+
+ gitChangesQueue.Clear();
+ });
+
+ watcherCTS.Token.Register(() =>
+ {
+ if (gitWatcherAction is not null)
+ {
+ gitWatcherAction?.Cancel();
+
+ // Prevent duplicate execution of this block
+ gitWatcherAction = null;
+ }
+
+ CancelIoEx(hWatchDir, IntPtr.Zero);
+ CloseHandle(hWatchDir);
+ });
+ }
+
+ private async Task ProcessGitChangesQueueAsync(CancellationToken cancellationToken)
+ {
+ const int DELAY = 200;
+ var sampler = new IntervalSampler(100);
+ int changes = 0;
+
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ if (await gitChangedEvent.WaitAsync(DELAY, cancellationToken))
+ {
+ gitChangedEvent.Reset();
+ while (gitChangesQueue.TryDequeue(out var _))
+ ++changes;
+
+ if (changes != 0 && sampler.CheckNow())
+ {
+ await dispatcherQueue.EnqueueOrInvokeAsync(() => GitDirectoryUpdated?.Invoke(null, null!));
+ changes = 0;
+ }
+ }
+ }
+ }
+ catch { }
+ }
+
+ private async Task ProcessOperationQueueAsync(CancellationToken cancellationToken, bool hasSyncStatus)
+ {
+ const uint FILE_ACTION_ADDED = 0x00000001;
+ const uint FILE_ACTION_REMOVED = 0x00000002;
+ const uint FILE_ACTION_MODIFIED = 0x00000003;
+ const uint FILE_ACTION_RENAMED_OLD_NAME = 0x00000004;
+ const uint FILE_ACTION_RENAMED_NEW_NAME = 0x00000005;
+
+ const int UPDATE_BATCH_SIZE = 32;
+ var sampler = new IntervalSampler(200);
+ var updateQueue = new Queue();
+
+ var anyEdits = false;
+ ListedItem? lastItemAdded = null;
+ var rand = Guid.NewGuid();
+
+ // Call when any edits have occurred
+ async Task HandleChangesOccurredAsync()
+ {
+ await OrderFilesAndFoldersAsync();
+ await ApplyFilesAndFoldersChangesAsync();
+
+ if (lastItemAdded is not null)
+ {
+ await RequestSelectionAsync([lastItemAdded]);
+ lastItemAdded = null;
+ }
+
+ anyEdits = false;
+ }
+
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ if (await operationEvent.WaitAsync(200, cancellationToken))
+ {
+ operationEvent.Reset();
+
+ while (operationQueue.TryDequeue(out var operation))
+ {
+ if (cancellationToken.IsCancellationRequested)
+ break;
+
+ try
+ {
+ switch (operation.Action)
+ {
+ case FILE_ACTION_ADDED:
+ case FILE_ACTION_RENAMED_NEW_NAME:
+ lastItemAdded = await AddFileOrFolderAsync(operation.FileName);
+ if (lastItemAdded is not null)
+ anyEdits = true;
+ break;
+
+ case FILE_ACTION_MODIFIED:
+ if (!updateQueue.Contains(operation.FileName))
+ updateQueue.Enqueue(operation.FileName);
+ break;
+
+ case FILE_ACTION_REMOVED:
+ var itemRemoved = await RemoveFileOrFolderAsync(operation.FileName);
+ if (itemRemoved is not null)
+ anyEdits = true;
+ break;
+
+ case FILE_ACTION_RENAMED_OLD_NAME:
+ // Pair OLD_NAME with the following NEW_NAME so the item can be updated
+ // in place and stay at its current position instead of jumping to a new
+ // sorted slot after a rename (issue #4214). Leaving anyEdits false skips
+ // OrderFilesAndFoldersAsync; PropertyChanged keeps the visible name in sync.
+ if (operationQueue.TryPeek(out var nextOp) && nextOp.Action == FILE_ACTION_RENAMED_NEW_NAME &&
+ filesAndFolders.ToList().FirstOrDefault(x => x.GetRequiredPath().Equals(operation.FileName, StringComparison.OrdinalIgnoreCase)) is { } renamed)
+ {
+ operationQueue.TryDequeue(out _);
+ var newPath = nextOp.FileName;
+ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ renamed.ItemPath = newPath;
+ renamed.ItemNameRaw = Path.GetFileName(newPath);
+ if (renamed.PrimaryItemAttribute == StorageItemTypes.File)
+ renamed.FileExtension = Path.GetExtension(newPath);
+ });
+ }
+ else
+ {
+ var itemRenamedOld = await RemoveFileOrFolderAsync(operation.FileName);
+ if (itemRenamedOld is not null)
+ anyEdits = true;
+ }
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarning(ex, ex.Message);
+ }
+
+ if (anyEdits && sampler.CheckNow())
+ await HandleChangesOccurredAsync();
+ }
+
+ var itemsToUpdate = new List();
+ for (var i = 0; i < UPDATE_BATCH_SIZE && updateQueue.Count > 0; i++)
+ itemsToUpdate.Add(updateQueue.Dequeue());
+
+ await UpdateFilesOrFoldersAsync(itemsToUpdate, hasSyncStatus);
+ }
+
+ if (updateQueue.Count > 0)
+ {
+ var itemsToUpdate = new List();
+ for (var i = 0; i < UPDATE_BATCH_SIZE && updateQueue.Count > 0; i++)
+ itemsToUpdate.Add(updateQueue.Dequeue());
+
+ await UpdateFilesOrFoldersAsync(itemsToUpdate, hasSyncStatus);
+ }
+
+ if (anyEdits && sampler.CheckNow())
+ await HandleChangesOccurredAsync();
+ }
+ }
+ catch
+ {
+ // Prevent disposed cancellation token
+ }
+
+ Debug.WriteLine("aProcessQueueAction done: {0}", rand);
+ }
+
+ public Task AddFileOrFolderFromShellFile(ShellFileItem item)
+ {
+ return
+ item.IsFolder ?
+ UniversalStorageEnumerator.AddFolderAsync(ShellStorageFolder.FromShellItem(item), currentStorageFolder, addFilesCTS.Token) :
+ UniversalStorageEnumerator.AddFileAsync(ShellStorageFile.FromShellItem(item), currentStorageFolder, addFilesCTS.Token);
+ }
+
+ partial void AddAlternateStreamItems(ListedItem item)
+ {
+ if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible)
+ {
+ // New file added, enumerate ADS
+ foreach (var ads in Win32Helper.GetAlternateStreams(item.GetRequiredPath()))
+ {
+ var adsItem = Win32StorageEnumerator.GetAlternateStream(ads, item);
+ filesAndFolders.Add(adsItem);
+ }
+ }
+ }
+
+ private async Task AddFileOrFolderAsync(string fileOrFolderPath)
+ {
+ FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic;
+ var additionalFlags = FIND_FIRST_EX_CASE_SENSITIVE;
+
+ IntPtr hFile = FindFirstFileExFromApp(fileOrFolderPath, findInfoLevel, out WIN32_FIND_DATA findData, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero,
+ additionalFlags);
+ if (hFile.ToInt64() == -1)
+ {
+ // If we cannot find the file (probably since it doesn't exist anymore) simply exit without adding it
+ return null;
+ }
+
+ FindClose(hFile);
+
+ var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System;
+ var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden;
+ var startWithDot = findData.cFileName.StartsWith('.');
+ if ((isHidden &&
+ (!UserSettingsService.FoldersSettingsService.ShowHiddenItems ||
+ (isSystem && !UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) ||
+ (startWithDot && !UserSettingsService.FoldersSettingsService.ShowDotFiles))
+ {
+ // Do not add to file list if hidden/system attribute is set and system/hidden file are not to be shown
+ return null;
+ }
+
+ var parentPath = Directory.GetParent(fileOrFolderPath)?.FullName
+ ?? throw new InvalidOperationException("The added item does not have a parent directory.");
+
+ ListedItem? listedItem;
+
+ // FILE_ATTRIBUTE_DIRECTORY
+ if ((findData.dwFileAttributes & 0x10) > 0)
+ listedItem = await Win32StorageEnumerator.GetFolder(findData, parentPath, IsValidGitDirectory, addFilesCTS.Token);
+ else
+ listedItem = await Win32StorageEnumerator.GetFile(findData, parentPath, IsValidGitDirectory, addFilesCTS.Token);
+
+ if (listedItem is null)
+ return null;
+
+ await AddFileOrFolderAsync(listedItem);
+
+ return listedItem;
+ }
+ }
+}
diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs
index 0fa2873c91ce..0cc4af04d4ed 100644
--- a/src/Files.App/ViewModels/ShellViewModel.cs
+++ b/src/Files.App/ViewModels/ShellViewModel.cs
@@ -18,9 +18,6 @@
using Windows.Storage;
using Windows.Storage.FileProperties;
using Windows.Storage.Search;
-using Windows.Win32;
-using Windows.Win32.System.SystemServices;
-using static Files.App.Helpers.Win32PInvoke;
using ByteSize = ByteSizeLib.ByteSize;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using FileAttributes = System.IO.FileAttributes;
@@ -758,9 +755,11 @@ public ShellViewModel(LayoutPreferencesManager folderSettingsViewModel)
fileTagsSettingsService.OnTagsUpdated += FileTagsSettingsService_OnSettingUpdated;
folderSizeProvider.SizeChanged += FolderSizeProvider_SizeChanged;
folderSettings.LayoutModeChangeRequested += LayoutModeChangeRequested;
+#if WINDOWS
StorageTrashBinService.Watcher.ItemAdded += RecycleBinItemCreatedAsync;
StorageTrashBinService.Watcher.ItemDeleted += RecycleBinItemDeletedAsync;
StorageTrashBinService.Watcher.RefreshRequested += RecycleBinRefreshRequestedAsync;
+#endif
}
private async void LayoutModeChangeRequested(object? sender, LayoutModeEventArgs e)
@@ -768,50 +767,6 @@ private async void LayoutModeChangeRequested(object? sender, LayoutModeEventArgs
await dispatcherQueue.EnqueueOrInvokeAsync(CheckForBackgroundImage, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
}
- private async void RecycleBinRefreshRequestedAsync(object? sender, FileSystemEventArgs e)
- {
- if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase))
- return;
-
- await dispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- RefreshItems(null);
- });
- }
-
- private async void RecycleBinItemDeletedAsync(object? sender, FileSystemEventArgs e)
- {
- if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase))
- return;
-
- var removedItem = await RemoveFileOrFolderAsync(e.FullPath);
-
- if (removedItem is not null)
- await ApplyFilesAndFoldersChangesAsync();
- }
-
- private async void RecycleBinItemCreatedAsync(object? sender, FileSystemEventArgs e)
- {
- if (!Constants.UserEnvironmentPaths.RecycleBinPath.Equals(CurrentFolder?.ItemPath, StringComparison.OrdinalIgnoreCase))
- return;
-
- using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(e.FullPath));
- if (folderItem is null)
- return;
-
- var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem);
- if (shellFileItem is null)
- throw new InvalidDataException("The recycle-bin item could not be converted to a shell item.");
-
- var newListedItem = await AddFileOrFolderFromShellFile(shellFileItem);
- if (newListedItem is null)
- return;
-
- await AddFileOrFolderAsync(newListedItem);
- await OrderFilesAndFoldersAsync();
- await ApplyFilesAndFoldersChangesAsync();
- }
-
private async void FolderSizeProvider_SizeChanged(object? sender, Services.SizeProvider.SizeChangedEventArgs e)
{
try
@@ -1832,21 +1787,7 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() =>
var properties = extraProperties.Result
?? throw new InvalidOperationException("A folder-property lookup did not return properties.");
- // Drive Storage Details
- 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))
- {
- var maxSpace = ByteSize.FromBytes(capacityRaw);
- var freeSpace = ByteSize.FromBytes(freeSpaceRaw);
-
- item.MaxSpace = maxSpace;
- item.SpaceUsed = maxSpace - freeSpace;
- item.FileSize = string.Format(Strings.DriveFreeSpaceAndCapacity.GetLocalizedResource(), freeSpace.ToSizeString(), maxSpace.ToSizeString());
- item.ShowDriveStorageDetails = true;
- }
+ ApplyDriveStorageDetails(item, properties);
}
else
@@ -2212,6 +2153,7 @@ private async Task RapidAddItemsToCollectionAsync(string? path, LibraryItem? lib
switch (enumerated)
{
+#if WINDOWS
// Enumerated with FindFirstFileExFromApp
// Is folder synced to cloud storage?
case 0:
@@ -2244,6 +2186,7 @@ private async Task RapidAddItemsToCollectionAsync(string? path, LibraryItem? lib
if (!HasNoWatcher)
WatchForWin32FolderChanges(path);
break;
+#endif
// Enumerated through IFileSystemEnumerator (Linux)
case 3:
@@ -2261,6 +2204,11 @@ private async Task RapidAddItemsToCollectionAsync(string? path, LibraryItem? lib
WatchForLinuxTrashChanges();
break;
+ // Archive listed through IStorableResolver (Linux); read-only, so no watcher
+ case 5:
+ PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false });
+ break;
+
// Enumeration failed
case -1:
default:
@@ -2294,337 +2242,14 @@ public void CloseWatcher()
watcherCTS = new CancellationTokenSource();
}
- private async Task PromptToUnlockBitlockerIfLockedAsync(string path, string pathRoot)
- {
- try
- {
- var rootFolder = await FilesystemTasks.WrapNullable(() => StorageFileExtensions.DangerousGetFolderFromPathAsync(path));
- if (await FolderHelpers.CheckBitlockerStatusAsync(rootFolder, path))
- await ContextMenu.InvokeVerb("unlock-bde", pathRoot);
- }
- catch (Exception ex)
- {
- // Runs detached, so swallow: the property probe or the unlock-bde shell verb can throw (e.g. COMException)
- App.Logger.LogWarning(ex, ex.Message);
- }
- }
-
private async Task EnumerateItemsFromStandardFolderAsync(string path, CancellationToken cancellationToken, LibraryItem? library = null)
{
enumeratedCloudSyncStatus = null;
-#if !WINDOWS
- return await EnumerateLinuxFolderAsync(path, cancellationToken, library);
-#else
-
- // Flag to use FindFirstFileExFromApp or StorageFolder enumeration - Use storage folder for Box Drive (#4629)
- var isBoxFolder = CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder && path.StartsWith(boxFolder);
- bool isWslDistro = path.StartsWith(@"\\wsl$\", StringComparison.OrdinalIgnoreCase) || path.StartsWith(@"\\wsl.localhost\", StringComparison.OrdinalIgnoreCase)
- || path.Equals(@"\\wsl$", StringComparison.OrdinalIgnoreCase) || path.Equals(@"\\wsl.localhost", StringComparison.OrdinalIgnoreCase);
- bool isMtp = path.StartsWith(@"\\?\", StringComparison.Ordinal);
- bool isShellFolder = path.StartsWith(@"\\SHELL\", StringComparison.Ordinal);
- bool isNetwork = path.StartsWith(@"\\", StringComparison.Ordinal) &&
- !isMtp &&
- !isShellFolder &&
- !isWslDistro;
- bool isNetdisk = false;
-
- try
- {
- // Special handling for network drives
- if (!isNetwork)
- isNetdisk = await Task.Run(() => new DriveInfo(path).DriveType == System.IO.DriveType.Network);
- }
- catch { }
-
- bool isFtp = FtpHelpers.IsFtpPath(path);
- bool enumFromStorageFolder = isBoxFolder || isFtp;
-
- BaseStorageFolder? rootFolder = null;
-
- if (isNetwork || isNetdisk)
- {
- var auth = await NetworkService.AuthenticateNetworkShare(path, cancellationToken);
- if (!auth)
- return -1;
- }
-
- // Off the UI thread: FindFirstFileEx blocks until the SMB timeout on an unreachable share.
- Win32PInvoke.SafeFindHandle? hFile = null;
- WIN32_FIND_DATA findData = default;
- int errorCode = 0;
- if (!enumFromStorageFolder)
- {
- (hFile, findData, errorCode) = await Task.Run(() =>
- {
- var hFileTsk = FindFirstFileExFromAppSafe(
- path + "\\*.*",
- FINDEX_INFO_LEVELS.FindExInfoBasic,
- out WIN32_FIND_DATA findDataTsk,
- FINDEX_SEARCH_OPS.FindExSearchNameMatch,
- IntPtr.Zero,
- FIND_FIRST_EX_LARGE_FETCH);
-
- return (hFileTsk, findDataTsk, hFileTsk.IsInvalid ? Marshal.GetLastWin32Error() : 0);
- })
- .WithTimeoutAsync(TimeSpan.FromSeconds(5));
- }
-
- if (!enumFromStorageFolder && hFile is not null && !hFile.IsInvalid)
- {
- // Enumerate with the handle opened above; rootFolder only used for Bitlocker
- currentStorageFolder = null;
- }
- else if (workingRoot is not null)
- {
- var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder));
- if (!res)
- return -1;
-
- var storageFolder = res.Result
- ?? throw new InvalidOperationException("A successful folder lookup did not return a storage folder.");
-
- currentStorageFolder = storageFolder;
- rootFolder = storageFolder.Item;
- enumFromStorageFolder = true;
- }
- else
- {
- var res = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path, workingRoot, currentStorageFolder));
- if (res)
- {
- var storageFolder = res.Result
- ?? throw new InvalidOperationException("A successful folder lookup did not return a storage folder.");
- currentStorageFolder = storageFolder;
- rootFolder = storageFolder.Item;
- }
- else if (res == FileSystemStatusCode.Unauthorized)
- {
- ShowLocationInaccessibleOrMissing(path);
-
- return -1;
- }
- else if (res == FileSystemStatusCode.NotFound)
- {
- ShowLocationInaccessibleOrMissing(path);
-
- return -1;
- }
- else
- {
- ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged, res.ErrorCode.ToString());
-
- return -1;
- }
- }
-
- var pathRoot = Path.GetPathRoot(path);
- if (Path.IsPathRooted(path) && pathRoot == path)
- {
- // Off the critical path: a locked drive fails enumeration anyway, so don't block the listing on the BitLocker probe.
- _ = PromptToUnlockBitlockerIfLockedAsync(path, pathRoot);
- }
-
- HasNoWatcher = isFtp || isWslDistro || isMtp || currentStorageFolder?.Item is ZipStorageFolder;
-
- if (enumFromStorageFolder)
- {
- // The handle from the open above is unused on the storage-folder path.
- hFile?.Dispose();
-
- var basicProps = await rootFolder?.GetBasicPropertiesAsync();
- var currentFolder = library ?? new ListedItem(rootFolder?.FolderRelativeId ?? string.Empty)
- {
- PrimaryItemAttribute = StorageItemTypes.Folder,
- ItemPropertiesInitialized = true,
- ItemNameRaw = rootFolder?.DisplayName ?? string.Empty,
- ItemDateModifiedReal = basicProps.DateModified,
- ItemType = rootFolder?.DisplayType ?? string.Empty,
- FileImage = null,
- LoadFileIcon = false,
- ItemPath = string.IsNullOrEmpty(rootFolder?.Path) ? currentStorageFolder?.Path ?? string.Empty : rootFolder.Path,
- FileSize = null,
- FileSizeBytes = 0,
- };
-
- if (library is null)
- currentFolder.ItemDateCreatedReal = rootFolder?.DateCreated ?? DateTimeOffset.Now;
-
- CurrentFolder = currentFolder;
- await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken);
-
- // Workaround for #7428
- return isBoxFolder ? 2 : 1;
- }
- else
- {
- var itemModifiedDate = DateTime.Now;
- var itemCreatedDate = DateTime.Now;
-
- try
- {
- FileTimeToSystemTime(in findData.ftLastWriteTime, out var systemModifiedTimeOutput);
- itemModifiedDate = systemModifiedTimeOutput.ToDateTime();
-
- FileTimeToSystemTime(in findData.ftCreationTime, out SYSTEMTIME systemCreatedTimeOutput);
- itemCreatedDate = systemCreatedTimeOutput.ToDateTime();
- }
- catch (ArgumentException)
- {
- }
-
- var isHidden = (((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden);
- var opacity = isHidden ? Constants.UI.DimItemOpacity : 1d;
-
- var currentFolder = library ?? new ListedItem(null)
- {
- PrimaryItemAttribute = StorageItemTypes.Folder,
- ItemPropertiesInitialized = true,
- ItemNameRaw = rootFolder?.DisplayName ?? Path.GetFileName(path.TrimEnd('\\')),
- ItemDateModifiedReal = itemModifiedDate,
- ItemDateCreatedReal = itemCreatedDate,
- ItemType = folderTypeTextLocalized,
- FileImage = null,
- IsHiddenItem = isHidden,
- Opacity = opacity,
- LoadFileIcon = false,
- ItemPath = path,
- FileSize = null,
- FileSizeBytes = 0,
- };
-
- CurrentFolder = currentFolder;
-
- if (hFile is null)
- {
- ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged);
-
- return -1;
- }
- else if (hFile.IsInvalid)
- {
- hFile.Dispose();
- await EnumFromStorageFolderAsync(path, rootFolder, currentStorageFolder, cancellationToken);
-
- // errorCode == ERROR_ACCESS_DENIED
- if (filesAndFolders.Count == 0 && errorCode == 0x5)
- {
- ShowLocationInaccessibleOrMissing(path);
-
- return -1;
- }
-
- return 1;
- }
- else
- {
- await Task.Run(async () =>
- {
- List fileList = await Win32StorageEnumerator.ListEntries(path, hFile, findData, cancellationToken, -1, GetPreloadIconSize(), intermediateAction: async (intermediateList) =>
- {
- filesAndFolders.AddRange(intermediateList);
-
- // The stable sort lands new items in final position without reordering visible ones; capped for huge listings
- if (filesAndFolders.Count <= 10_000)
- await OrderFilesAndFoldersAsync();
-
- await ApplyFilesAndFoldersChangesAsync();
- });
-
- filesAndFolders.AddRange(fileList);
-
- await OrderFilesAndFoldersAsync();
- await ApplyFilesAndFoldersChangesAsync();
- // Not awaited here: with Low priority these don't run until the UI thread goes idle
- // after the final list update, which would delay load completion and watcher setup.
- // The desktop.ini task is awaited before applying the adaptive layout, which reads DesktopIni.
- _ = dispatcherQueue.EnqueueOrInvokeAsync(CheckForSolutionFile, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
- desktopIniUpdateTask = dispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- GetDesktopIniFileData();
- CheckForBackgroundImage();
- },
- Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
- });
-
- // Cache the resolved folder so the post-enum switch reuses it.
- currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path));
- rootFolder ??= currentStorageFolder?.Item;
- if (rootFolder is not null)
- {
- if (rootFolder.DisplayName is not null)
- currentFolder.ItemNameRaw = rootFolder.DisplayName;
-
- if (!string.Equals(path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase))
- {
- enumeratedCloudSyncStatus = await CheckCloudDriveSyncStatusAsync(rootFolder);
- currentFolder.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(enumeratedCloudSyncStatus.Value);
- }
- }
-
- return 0;
- }
- }
-#endif
- }
-
- private async Task EnumFromStorageFolderAsync(string path, BaseStorageFolder? rootFolder, StorageFolderWithPath? currentStorageFolder, CancellationToken cancellationToken)
- {
#if WINDOWS
- if (rootFolder is null)
- return;
-
- // Null when a concurrent navigation or dispose cleared the context; this enumeration is stale
- if (currentStorageFolder is null)
- return;
-
- if (rootFolder is IPasswordProtectedItem ppis)
- ppis.PasswordRequestedCallback = async (item) =>
- {
- await dispatcherQueue.EnqueueOrInvokeAsync(() => ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired));
-
- return await UIFilesystemHelpers.RequestPassword(item);
- };
-
- try
- {
- await Task.Run(async () =>
- {
- List finalList = await UniversalStorageEnumerator.ListEntries(
- rootFolder,
- currentStorageFolder,
- cancellationToken,
- -1,
- GetPreloadIconSize(),
- async (intermediateList) =>
- {
- filesAndFolders.AddRange(intermediateList);
-
- // The stable sort lands new items in final position without reordering visible ones; capped for huge listings
- if (filesAndFolders.Count <= 10_000)
- await OrderFilesAndFoldersAsync();
-
- await ApplyFilesAndFoldersChangesAsync();
- });
-
- filesAndFolders.AddRange(finalList);
-
- await OrderFilesAndFoldersAsync();
- await ApplyFilesAndFoldersChangesAsync();
- }, cancellationToken);
-
- IsLocationUnavailable = false;
- }
- catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) // Password dialog dismissed
- {
- ShowLocationUnavailable(LocationUnavailableKind.PasswordRequired);
- }
-
- if (rootFolder is IPasswordProtectedItem ppiu)
- ppiu.PasswordRequestedCallback = null;
+ return await EnumerateWindowsFolderAsync(path, cancellationToken, library);
#else
- // LINUX-TODO(shell): enumerate storables for non-local locations in P4-E.
- await Task.CompletedTask;
+ return await EnumerateLinuxFolderAsync(path, cancellationToken, library);
#endif
}
@@ -2713,6 +2338,7 @@ public void CheckForBackgroundImage()
public async Task CheckCloudDriveSyncStatusAsync(IStorageItem? item)
{
+ // LINUX-TODO(props): placeholder status comes from the Windows property system, which Uno doesn't back; Linux gets Unknown.
int? syncStatus = null;
if (item is BaseStorageFile file && file.Properties is not null)
{
@@ -2744,8 +2370,13 @@ public async Task CheckCloudDriveSyncStatusAsync(IStorageI
return (CloudDriveSyncStatus)syncStatus;
}
+ // Fills the drive capacity columns of removable shell folders (SFGAO flags); Windows only
+ partial void ApplyDriveStorageDetails(ListedItem item, IDictionary properties);
+
private async Task>?> GetExtraProperties(IStorageItem matchingStorageItem)
{
+ // LINUX-TODO(props): Uno has no Windows property system behind RetrievePropertiesAsync, so dimensions, duration, version and
+ // drive capacity stay empty on Linux. Read them from a metadata service when those columns are ported.
if (matchingStorageItem is BaseStorageFile file && file.Properties != null)
return await FilesystemTasks.Wrap(() => file.Properties.RetrievePropertiesAsync((string[])["System.Image.Dimensions", "System.Media.Duration", "System.FileVersion"]).AsTask());
@@ -2755,476 +2386,8 @@ public async Task CheckCloudDriveSyncStatusAsync(IStorageI
return null;
}
- private async Task WatchForStorageFolderChangesAsync(BaseStorageFolder? rootFolder)
- {
- if (rootFolder is null)
- return;
-
- await Task.Run(() =>
- {
- var options = new QueryOptions()
- {
- FolderDepth = FolderDepth.Shallow,
- IndexerOption = IndexerOption.OnlyUseIndexerAndOptimizeForIndexedProperties
- };
-
- options.SetPropertyPrefetch(PropertyPrefetchOptions.None, null);
- options.SetThumbnailPrefetch(ThumbnailMode.ListView, 0, ThumbnailOptions.ReturnOnlyIfCached);
-
- if (rootFolder.AreQueryOptionsSupported(options))
- {
- var itemQueryResult = rootFolder.CreateItemQueryWithOptions(options).ToStorageItemQueryResult()
- ?? throw new InvalidOperationException("The folder query could not be converted to a storage query.");
-
- itemQueryResult.ContentsChanged += ItemQueryResult_ContentsChanged;
-
- // Just get one item to start getting notifications
- var watchedItemsOperation = itemQueryResult.GetItemsAsync(0, 1);
-
- watcherCTS.Token.Register(() =>
- {
- itemQueryResult.ContentsChanged -= ItemQueryResult_ContentsChanged;
- watchedItemsOperation?.Cancel();
- });
- }
- });
- }
-
- private void WatchForWin32FolderChanges(string? folderPath)
- {
- if (!Directory.Exists(folderPath))
- return;
-
- // NOTE: Suppressed NullReferenceException caused by EnableRaisingEvents
- SafetyExtensions.IgnoreExceptions(() =>
- {
- watcher = new FileSystemWatcher
- {
- Path = folderPath,
- Filter = "*.*",
- NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName
- };
-
- watcher.Created += DirectoryWatcher_Changed;
- watcher.Deleted += DirectoryWatcher_Changed;
- watcher.Renamed += DirectoryWatcher_Changed;
- watcher.EnableRaisingEvents = true;
- }, App.Logger);
- }
-
- private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e)
- {
- Debug.WriteLine($"Directory watcher event: {e.ChangeType}, {e.FullPath}");
-
- await dispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- RefreshItems(null);
- });
- }
-
- private async void ItemQueryResult_ContentsChanged(IStorageQueryResultBase sender, object args)
- {
- // Query options have to be reapplied otherwise old results are returned
- var options = new QueryOptions()
- {
- FolderDepth = FolderDepth.Shallow,
- IndexerOption = IndexerOption.OnlyUseIndexerAndOptimizeForIndexedProperties
- };
-
- options.SetPropertyPrefetch(PropertyPrefetchOptions.None, null);
- options.SetThumbnailPrefetch(ThumbnailMode.ListView, 0, ThumbnailOptions.ReturnOnlyIfCached);
-
- sender.ApplyNewQueryOptions(options);
-
- await dispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- RefreshItems(null);
- });
- }
-
- private void WatchForDirectoryChanges(string path, CloudDriveSyncStatus syncStatus)
- {
- // Enumeration is fire-and-forget; don't set up a watcher on a disposed view model.
- if (isDisposed)
- return;
-
- Debug.WriteLine($"WatchForDirectoryChanges: {path}");
- var hWatchDir = Win32PInvoke.CreateFileFromApp(path, 1, 1 | 2 | 4,
- IntPtr.Zero, 3, (uint)Win32PInvoke.File_Attributes.BackupSemantics | (uint)Win32PInvoke.File_Attributes.Overlapped, IntPtr.Zero);
- if (hWatchDir.ToInt64() == -1)
- return;
-
- var hasSyncStatus = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown;
-
- aProcessQueueAction ??= Task.Run(() => ProcessOperationQueueAsync(watcherCTS.Token, hasSyncStatus));
-
- var aWatcherAction = Windows.System.Threading.ThreadPool.RunAsync((x) =>
- {
- var buff = new byte[4096];
- 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;
-
- if (hasSyncStatus)
- notifyFilters |= FILE_NOTIFY_CHANGE_ATTRIBUTES;
-
- var overlapped = new NativeOverlapped();
- using var eventHandle = PInvoke.CreateEvent(null, false, false, null);
- overlapped.EventHandle = eventHandle.DangerousGetHandle();
- const uint INFINITE = 0xFFFFFFFF;
-
- while (x.Status != AsyncStatus.Canceled)
- {
- unsafe
- {
- fixed (byte* pBuff = buff)
- {
- ref var notifyInformation = ref Unsafe.As(ref buff[0]);
- if (x.Status != AsyncStatus.Canceled)
- {
- PInvoke.ReadDirectoryChanges(
- new Windows.Win32.Foundation.HANDLE(hWatchDir),
- pBuff,
- 4096,
- false,
- (Windows.Win32.Storage.FileSystem.FILE_NOTIFY_CHANGE)notifyFilters,
- null,
- &overlapped,
- null);
- }
- else
- {
- break;
- }
-
- Debug.WriteLine("waiting: {0}", rand);
- if (x.Status == AsyncStatus.Canceled)
- break;
-
- var rc = WaitForSingleObjectEx(overlapped.EventHandle, INFINITE, true);
- Debug.WriteLine("wait done: {0}", rand);
-
- uint offset = 0;
- ref var notifyInfo = ref Unsafe.As(ref buff[offset]);
- if (x.Status == AsyncStatus.Canceled)
- break;
-
- do
- {
- notifyInfo = ref Unsafe.As(ref buff[offset]);
- string? FileName = null;
- unsafe
- {
- fixed (char* name = notifyInfo.FileName)
- {
- FileName = Path.Combine(path, new string(name, 0, (int)notifyInfo.FileNameLength / 2));
- }
- }
-
- uint action = notifyInfo.Action;
-
- Debug.WriteLine("action: {0}", action);
-
- operationQueue.Enqueue((action, FileName));
-
- offset += notifyInfo.NextEntryOffset;
- }
- while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled);
-
- operationEvent.Set();
-
- //ResetEvent(overlapped.hEvent);
- Debug.WriteLine("Task running...");
- }
- }
- }
-
- operationQueue.Clear();
-
- Debug.WriteLine("aWatcherAction done: {0}", rand);
- });
-
- watcherCTS.Token.Register(() =>
- {
- if (aWatcherAction is not null)
- {
- aWatcherAction?.Cancel();
-
- // Prevent duplicate execution of this block
- aWatcherAction = null;
-
- Debug.WriteLine("watcher canceled");
- }
-
- CancelIoEx(hWatchDir, IntPtr.Zero);
- CloseHandle(hWatchDir);
- });
- }
-
- private void WatchForGitChanges()
- {
- // Enumeration is fire-and-forget; don't set up a watcher on a disposed view model.
- if (isDisposed)
- return;
-
- var hWatchDir = Win32PInvoke.CreateFileFromApp(
- GitDirectory!,
- 1,
- 1 | 2 | 4,
- IntPtr.Zero,
- 3,
- (uint)Win32PInvoke.File_Attributes.BackupSemantics | (uint)Win32PInvoke.File_Attributes.Overlapped,
- IntPtr.Zero);
-
- if (hWatchDir.ToInt64() == -1)
- return;
-
- gitProcessQueueAction ??= Task.Run(() => ProcessGitChangesQueueAsync(watcherCTS.Token));
-
- var gitWatcherAction = Windows.System.Threading.ThreadPool.RunAsync((x) =>
- {
- var buff = new byte[4096];
- 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 NativeOverlapped();
- using var eventHandle = PInvoke.CreateEvent(null, false, false, null);
- overlapped.EventHandle = eventHandle.DangerousGetHandle();
- const uint INFINITE = 0xFFFFFFFF;
-
- while (x.Status != AsyncStatus.Canceled)
- {
- unsafe
- {
- fixed (byte* pBuff = buff)
- {
- ref var notifyInformation = ref Unsafe.As(ref buff[0]);
- if (x.Status == AsyncStatus.Canceled)
- break;
-
- 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.EventHandle, INFINITE, true);
-
- uint offset = 0;
- ref var notifyInfo = ref Unsafe.As(ref buff[offset]);
- if (x.Status == AsyncStatus.Canceled)
- break;
-
- do
- {
- notifyInfo = ref Unsafe.As(ref buff[offset]);
-
- uint action = notifyInfo.Action;
-
- gitChangesQueue.Enqueue(action);
-
- offset += notifyInfo.NextEntryOffset;
- }
- while (notifyInfo.NextEntryOffset != 0 && x.Status != AsyncStatus.Canceled);
-
- gitChangedEvent.Set();
- }
- }
- }
-
- gitChangesQueue.Clear();
- });
-
- watcherCTS.Token.Register(() =>
- {
- if (gitWatcherAction is not null)
- {
- gitWatcherAction?.Cancel();
-
- // Prevent duplicate execution of this block
- gitWatcherAction = null;
- }
-
- CancelIoEx(hWatchDir, IntPtr.Zero);
- CloseHandle(hWatchDir);
- });
- }
-
- private async Task ProcessGitChangesQueueAsync(CancellationToken cancellationToken)
- {
- const int DELAY = 200;
- var sampler = new IntervalSampler(100);
- int changes = 0;
-
- try
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- if (await gitChangedEvent.WaitAsync(DELAY, cancellationToken))
- {
- gitChangedEvent.Reset();
- while (gitChangesQueue.TryDequeue(out var _))
- ++changes;
-
- if (changes != 0 && sampler.CheckNow())
- {
- await dispatcherQueue.EnqueueOrInvokeAsync(() => GitDirectoryUpdated?.Invoke(null, null!));
- changes = 0;
- }
- }
- }
- }
- catch { }
- }
-
- private async Task ProcessOperationQueueAsync(CancellationToken cancellationToken, bool hasSyncStatus)
- {
- const uint FILE_ACTION_ADDED = 0x00000001;
- const uint FILE_ACTION_REMOVED = 0x00000002;
- const uint FILE_ACTION_MODIFIED = 0x00000003;
- const uint FILE_ACTION_RENAMED_OLD_NAME = 0x00000004;
- const uint FILE_ACTION_RENAMED_NEW_NAME = 0x00000005;
-
- const int UPDATE_BATCH_SIZE = 32;
- var sampler = new IntervalSampler(200);
- var updateQueue = new Queue();
-
- var anyEdits = false;
- ListedItem? lastItemAdded = null;
- var rand = Guid.NewGuid();
-
- // Call when any edits have occurred
- async Task HandleChangesOccurredAsync()
- {
- await OrderFilesAndFoldersAsync();
- await ApplyFilesAndFoldersChangesAsync();
-
- if (lastItemAdded is not null)
- {
- await RequestSelectionAsync([lastItemAdded]);
- lastItemAdded = null;
- }
-
- anyEdits = false;
- }
-
- try
- {
- while (!cancellationToken.IsCancellationRequested)
- {
- if (await operationEvent.WaitAsync(200, cancellationToken))
- {
- operationEvent.Reset();
-
- while (operationQueue.TryDequeue(out var operation))
- {
- if (cancellationToken.IsCancellationRequested)
- break;
-
- try
- {
- switch (operation.Action)
- {
- case FILE_ACTION_ADDED:
- case FILE_ACTION_RENAMED_NEW_NAME:
- lastItemAdded = await AddFileOrFolderAsync(operation.FileName);
- if (lastItemAdded is not null)
- anyEdits = true;
- break;
-
- case FILE_ACTION_MODIFIED:
- if (!updateQueue.Contains(operation.FileName))
- updateQueue.Enqueue(operation.FileName);
- break;
-
- case FILE_ACTION_REMOVED:
- var itemRemoved = await RemoveFileOrFolderAsync(operation.FileName);
- if (itemRemoved is not null)
- anyEdits = true;
- break;
-
- case FILE_ACTION_RENAMED_OLD_NAME:
- // Pair OLD_NAME with the following NEW_NAME so the item can be updated
- // in place and stay at its current position instead of jumping to a new
- // sorted slot after a rename (issue #4214). Leaving anyEdits false skips
- // OrderFilesAndFoldersAsync; PropertyChanged keeps the visible name in sync.
- if (operationQueue.TryPeek(out var nextOp) && nextOp.Action == FILE_ACTION_RENAMED_NEW_NAME &&
- filesAndFolders.ToList().FirstOrDefault(x => x.GetRequiredPath().Equals(operation.FileName, StringComparison.OrdinalIgnoreCase)) is { } renamed)
- {
- operationQueue.TryDequeue(out _);
- var newPath = nextOp.FileName;
- await dispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- renamed.ItemPath = newPath;
- renamed.ItemNameRaw = Path.GetFileName(newPath);
- if (renamed.PrimaryItemAttribute == StorageItemTypes.File)
- renamed.FileExtension = Path.GetExtension(newPath);
- });
- }
- else
- {
- var itemRenamedOld = await RemoveFileOrFolderAsync(operation.FileName);
- if (itemRenamedOld is not null)
- anyEdits = true;
- }
- break;
- }
- }
- catch (Exception ex)
- {
- App.Logger.LogWarning(ex, ex.Message);
- }
-
- if (anyEdits && sampler.CheckNow())
- await HandleChangesOccurredAsync();
- }
-
- var itemsToUpdate = new List();
- for (var i = 0; i < UPDATE_BATCH_SIZE && updateQueue.Count > 0; i++)
- itemsToUpdate.Add(updateQueue.Dequeue());
-
- await UpdateFilesOrFoldersAsync(itemsToUpdate, hasSyncStatus);
- }
-
- if (updateQueue.Count > 0)
- {
- var itemsToUpdate = new List();
- for (var i = 0; i < UPDATE_BATCH_SIZE && updateQueue.Count > 0; i++)
- itemsToUpdate.Add(updateQueue.Dequeue());
-
- await UpdateFilesOrFoldersAsync(itemsToUpdate, hasSyncStatus);
- }
-
- if (anyEdits && sampler.CheckNow())
- await HandleChangesOccurredAsync();
- }
- }
- catch
- {
- // Prevent disposed cancellation token
- }
-
- Debug.WriteLine("aProcessQueueAction done: {0}", rand);
- }
-
- public Task AddFileOrFolderFromShellFile(ShellFileItem item)
- {
-#if WINDOWS
- return
- item.IsFolder ?
- UniversalStorageEnumerator.AddFolderAsync(ShellStorageFolder.FromShellItem(item), currentStorageFolder, addFilesCTS.Token) :
- UniversalStorageEnumerator.AddFileAsync(ShellStorageFile.FromShellItem(item), currentStorageFolder, addFilesCTS.Token);
-#else
- // LINUX-TODO(shell): replace the Windows Shell item factory in P4-E.
- return Task.FromResult(null);
-#endif
- }
+ // Lists the alternate data streams of a newly added item when they're shown; NTFS only
+ partial void AddAlternateStreamItems(ListedItem item);
private async Task AddFileOrFolderAsync(ListedItem? item)
{
@@ -3244,74 +2407,12 @@ private async Task AddFileOrFolderAsync(ListedItem? item)
{
filesAndFolders.Add(item);
-#if WINDOWS
- if (UserSettingsService.FoldersSettingsService.AreAlternateStreamsVisible)
- {
- // New file added, enumerate ADS
- foreach (var ads in Win32Helper.GetAlternateStreams(item.GetRequiredPath()))
- {
- var adsItem = Win32StorageEnumerator.GetAlternateStream(ads, item);
- filesAndFolders.Add(adsItem);
- }
- }
-#endif
+ AddAlternateStreamItems(item);
}
enumFolderSemaphore.Release();
}
- private async Task AddFileOrFolderAsync(string fileOrFolderPath)
- {
-#if WINDOWS
- FINDEX_INFO_LEVELS findInfoLevel = FINDEX_INFO_LEVELS.FindExInfoBasic;
- var additionalFlags = FIND_FIRST_EX_CASE_SENSITIVE;
-
- IntPtr hFile = FindFirstFileExFromApp(fileOrFolderPath, findInfoLevel, out WIN32_FIND_DATA findData, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero,
- additionalFlags);
- if (hFile.ToInt64() == -1)
- {
- // If we cannot find the file (probably since it doesn't exist anymore) simply exit without adding it
- return null;
- }
-
- FindClose(hFile);
-
- var isSystem = ((FileAttributes)findData.dwFileAttributes & FileAttributes.System) == FileAttributes.System;
- var isHidden = ((FileAttributes)findData.dwFileAttributes & FileAttributes.Hidden) == FileAttributes.Hidden;
- var startWithDot = findData.cFileName.StartsWith('.');
- if ((isHidden &&
- (!UserSettingsService.FoldersSettingsService.ShowHiddenItems ||
- (isSystem && !UserSettingsService.FoldersSettingsService.ShowProtectedSystemFiles))) ||
- (startWithDot && !UserSettingsService.FoldersSettingsService.ShowDotFiles))
- {
- // Do not add to file list if hidden/system attribute is set and system/hidden file are not to be shown
- return null;
- }
-
- var parentPath = Directory.GetParent(fileOrFolderPath)?.FullName
- ?? throw new InvalidOperationException("The added item does not have a parent directory.");
-
- ListedItem? listedItem;
-
- // FILE_ATTRIBUTE_DIRECTORY
- if ((findData.dwFileAttributes & 0x10) > 0)
- listedItem = await Win32StorageEnumerator.GetFolder(findData, parentPath, IsValidGitDirectory, addFilesCTS.Token);
- else
- listedItem = await Win32StorageEnumerator.GetFile(findData, parentPath, IsValidGitDirectory, addFilesCTS.Token);
-
- if (listedItem is null)
- return null;
-
- await AddFileOrFolderAsync(listedItem);
-
- return listedItem;
-#else
- // LINUX-TODO(shell): replace the Win32 incremental item factory in P4-E.
- await Task.CompletedTask;
- return null;
-#endif
- }
-
private async Task<(ListedItem Item, CloudDriveSyncStatus? SyncStatus, long? Size, DateTimeOffset Created, DateTimeOffset Modified)?> GetFileOrFolderUpdateInfoAsync(ListedItem item, bool hasSyncStatus)
{
IStorageItem? storageItem = null;
@@ -3646,9 +2747,11 @@ public void Dispose()
updateTagGroupCTS?.Cancel();
App.Logger.LogInformation($"ShellViewModel.Dispose: CurrentFolder={LogPathHelper.RedactPath(CurrentFolder?.ItemPath)}");
+#if WINDOWS
StorageTrashBinService.Watcher.ItemAdded -= RecycleBinItemCreatedAsync;
StorageTrashBinService.Watcher.ItemDeleted -= RecycleBinItemDeletedAsync;
StorageTrashBinService.Watcher.RefreshRequested -= RecycleBinRefreshRequestedAsync;
+#endif
UserSettingsService.OnSettingChangedEvent -= UserSettingsService_OnSettingChangedEvent;
fileTagsSettingsService.OnSettingImportedEvent -= FileTagsSettingsService_OnSettingUpdated;
fileTagsSettingsService.OnTagsUpdated -= FileTagsSettingsService_OnSettingUpdated;
diff --git a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs
index 03ca6d89c6b0..eac3edf97ca3 100644
--- a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs
@@ -491,9 +491,7 @@ public async Task PathBoxItem_DragOver(object sender, DragEventArgs e)
}
// Copy be default when dragging from zip
- else if (storageItems.Any(x =>
- x.Item is ZipStorageFile ||
- x.Item is ZipStorageFolder) ||
+ else if (storageItems.Any(x => x.IsArchiveMember()) ||
ZipStorageFolder.IsZipPath(pathBoxItem.Path))
{
e.DragUIOverride.Caption = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), pathBoxItem.Title);
@@ -1008,10 +1006,15 @@ public void SaveSearchQueryToList(string searchQuery)
private static async Task LaunchApplicationFromPath(string currentInput, string workingDir)
{
+#if !WINDOWS
+ // LINUX-TODO(launch): running commands typed into the address bar; the input is reported as an invalid path
+ return await Task.FromResult(false);
+#else
var args = CommandLineParser.SplitArguments(currentInput);
return await LaunchHelper.LaunchAppAsync(
args.FirstOrDefault("").Trim('"'), string.Join(' ', args.Skip(1)), workingDir
);
+#endif
}
public async Task PopulateOmnibarSuggestionsForPathMode()
diff --git a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
index 7eed10430462..3e6be428c906 100644
--- a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
@@ -1338,7 +1338,7 @@ private async Task HandleLocationItemDragOverAsync(LocationItem locationItem, It
// Some applications such as Edge can't raise the drop event by the Move flag (#14008), so we set the Copy flag as well.
operationType = DataPackageOperation.Move | DataPackageOperation.Copy;
}
- else if (storageItems.Any(x => x.Item is ZipStorageFile || x.Item is ZipStorageFolder)
+ else if (storageItems.Any(x => x.IsArchiveMember())
|| ZipStorageFolder.IsZipPath(locationItem.Path))
{
captionText = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), locationItem.Text);
@@ -1494,7 +1494,11 @@ private async Task HandleTagItemDroppedAsync(FileTagItem fileTagItem, ItemDroppe
if (!filesTags.Contains(fileTag.Uid))
{
filesTags = [.. filesTags, fileTag.Uid];
+#if WINDOWS
var fileFRN = await FileTagsHelper.GetFileFRN(item.Item);
+#else
+ var fileFRN = FileTagsHelper.GetFileFRN(path);
+#endif
dbInstance.SetTags(path, fileFRN, filesTags);
await FileTagsHelper.WriteFileTagAsync(path, filesTags);
pathToTags[path] = filesTags;
diff --git a/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs
index b794b05ea41c..1ebb46f187c9 100644
--- a/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs
@@ -266,7 +266,11 @@ private Task ExecuteMapNetworkDriveCommand()
private void ExecuteFormatDriveCommand(WidgetDriveCardItem? item)
{
+#if !WINDOWS
+ _ = DriveHelpers.OpenFormatDialogAsync(item?.Path);
+#else
Win32Helper.OpenFormatDriveDialog(item?.Path ?? string.Empty);
+#endif
}
private void ExecuteOpenPropertiesCommand(WidgetDriveCardItem? item)
diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs
index bda8563f4639..96f7b4c68ef9 100644
--- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs
+++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs
@@ -1077,6 +1077,7 @@ protected virtual void FileList_DragItemsStarting(object sender, DragItemsStarti
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();
+#if WINDOWS
var shellItemList = SafetyExtensions.IgnoreExceptions(() => orderedItems.Select(item => new ShellItem(item.GetRequiredPath())).ToArray());
try
{
@@ -1106,6 +1107,7 @@ protected virtual void FileList_DragItemsStarting(object sender, DragItemsStarti
item.Dispose();
}
}
+#endif
// Set can window to front (#13255)
MainWindow.Instance.SetCanWindowToFront(false);
@@ -1193,7 +1195,7 @@ private async void Item_DragOver(object sender, DragEventArgs e)
// Some applications such as Edge can't raise the drop event by the Move flag (#14008), so we set the Copy flag as well.
e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy;
}
- else if (draggedItems.Any(x => x.Item is ZipStorageFile || x.Item is ZipStorageFolder)
+ else if (draggedItems.Any(x => x.IsArchiveMember())
|| ZipStorageFolder.IsZipPath(item.ItemPath!))
{
e.DragUIOverride.Caption = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), item.Name);
@@ -1482,8 +1484,10 @@ private void FileListItem_Loaded(object sender, RoutedEventArgs e)
protected internal void FileListItem_PointerPressed(object sender, PointerRoutedEventArgs e)
{
// Set can window to front and bring the window to the front if necessary (#13255)
+#if WINDOWS
if ((!itemDragging) && MainWindow.Instance.SetCanWindowToFront(true))
Win32Helper.BringToForegroundEx(new(MainWindow.Instance.WindowHandle));
+#endif
if (sender is not SelectorItem selectorItem)
return;
@@ -1578,23 +1582,29 @@ protected internal void FileListItem_PointerExited(object sender, PointerRoutedE
protected void FileListItem_Tapped(object sender, TappedRoutedEventArgs e)
{
// Set can window to front and bring the window to the front if necessary (#13255)
+#if WINDOWS
if ((!itemDragging) && MainWindow.Instance.SetCanWindowToFront(true))
Win32Helper.BringToForegroundEx(new(MainWindow.Instance.WindowHandle));
+#endif
}
protected void FileListItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
// Set can window to front and bring the window to the front if necessary (#13255)
+#if WINDOWS
if ((!itemDragging) && MainWindow.Instance.SetCanWindowToFront(true))
Win32Helper.BringToForegroundEx(new(MainWindow.Instance.WindowHandle));
+#endif
}
[DynamicWindowsRuntimeCast(typeof(SelectorItem))]
protected void FileListItem_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
// Set can window to front and bring the window to the front if necessary (#13255)
+#if WINDOWS
if ((!itemDragging) && MainWindow.Instance.SetCanWindowToFront(true))
Win32Helper.BringToForegroundEx(new(MainWindow.Instance.WindowHandle));
+#endif
var rightClickedItem = GetItemFromElement(sender);
@@ -1768,7 +1778,11 @@ public void CheckRenameDoubleClick(object? clickedItem)
tapDebounceTimer?.Stop();
}
},
+#if WINDOWS
TimeSpan.FromMilliseconds(OperatingSystem.IsWindows() ? PInvoke.GetDoubleClickTime() : 500));
+#else
+ TimeSpan.FromMilliseconds(500));
+#endif
}
else
{