Skip to content

Commit ffa2eac

Browse files
authored
Feature: Replaced 'folder not found' dialog with inline text (#18757)
1 parent f18ae43 commit ffa2eac

6 files changed

Lines changed: 217 additions & 14 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!-- Copyright (c) Files Community. Licensed under the MIT License. -->
2+
<UserControl
3+
x:Class="Files.App.UserControls.LocationUnavailableIndicator"
4+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
d:DesignHeight="300"
9+
d:DesignWidth="400"
10+
mc:Ignorable="d">
11+
12+
<StackPanel HorizontalAlignment="Center" Spacing="8">
13+
<FontIcon
14+
HorizontalAlignment="Center"
15+
FontSize="32"
16+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
17+
Glyph="{x:Bind Glyph, Mode=OneWay}" />
18+
<TextBlock
19+
HorizontalAlignment="Center"
20+
Style="{StaticResource BodyStrongTextBlockStyle}"
21+
Text="{x:Bind Title, Mode=OneWay}"
22+
TextAlignment="Center"
23+
TextWrapping="Wrap" />
24+
<TextBlock
25+
HorizontalAlignment="Center"
26+
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
27+
Text="{x:Bind Message, Mode=OneWay}"
28+
TextAlignment="Center"
29+
TextWrapping="Wrap" />
30+
</StackPanel>
31+
</UserControl>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using CommunityToolkit.WinUI;
5+
using Microsoft.UI.Xaml.Controls;
6+
7+
namespace Files.App.UserControls
8+
{
9+
public sealed partial class LocationUnavailableIndicator : UserControl
10+
{
11+
[GeneratedDependencyProperty]
12+
public partial string? Glyph { get; set; }
13+
14+
[GeneratedDependencyProperty]
15+
public partial string? Title { get; set; }
16+
17+
[GeneratedDependencyProperty]
18+
public partial string? Message { get; set; }
19+
20+
public LocationUnavailableIndicator()
21+
{
22+
InitializeComponent();
23+
}
24+
}
25+
}

src/Files.App/ViewModels/ShellViewModel.cs

Lines changed: 125 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,122 @@ public bool IsNetworkDiscoveryInfoBarOpen
319319
set => SetProperty(ref isNetworkDiscoveryInfoBarOpen, value);
320320
}
321321

322+
private bool isLocationUnavailable;
323+
public bool IsLocationUnavailable
324+
{
325+
get => isLocationUnavailable;
326+
set
327+
{
328+
if (SetProperty(ref isLocationUnavailable, value))
329+
UpdateEmptyTextType();
330+
}
331+
}
332+
333+
private string? locationUnavailableGlyph;
334+
public string? LocationUnavailableGlyph
335+
{
336+
get => locationUnavailableGlyph;
337+
set => SetProperty(ref locationUnavailableGlyph, value);
338+
}
339+
340+
private string? locationUnavailableTitle;
341+
public string? LocationUnavailableTitle
342+
{
343+
get => locationUnavailableTitle;
344+
set => SetProperty(ref locationUnavailableTitle, value);
345+
}
346+
347+
private string? locationUnavailableMessage;
348+
public string? LocationUnavailableMessage
349+
{
350+
get => locationUnavailableMessage;
351+
set => SetProperty(ref locationUnavailableMessage, value);
352+
}
353+
354+
private enum LocationUnavailableKind
355+
{
356+
AccessDenied,
357+
NotFound,
358+
DriveUnplugged,
359+
}
360+
361+
private void ShowLocationUnavailable(LocationUnavailableKind kind, string? message = null)
362+
{
363+
(LocationUnavailableGlyph, LocationUnavailableTitle, LocationUnavailableMessage) = kind switch
364+
{
365+
LocationUnavailableKind.AccessDenied => ("\uE72E", Strings.AccessDenied.GetLocalizedResource(), Strings.AccessDeniedToFolder.GetLocalizedResource()),
366+
LocationUnavailableKind.NotFound => ("\uE838", Strings.FolderNotFoundDialogTitle.GetLocalizedResource(), Strings.FolderNotFoundDialogText.GetLocalizedResource()),
367+
_ => ("\uE7BA", Strings.DriveUnpluggedDialogTitle.GetLocalizedResource(), message ?? Strings.DriveUnpluggedDialogText.GetLocalizedResource()),
368+
};
369+
370+
IsLocationUnavailable = true;
371+
}
372+
373+
private void ShowLocationInaccessibleOrMissing(string path)
374+
{
375+
// A folder pending deletion fails enumeration with ERROR_ACCESS_DENIED;
376+
// Directory.Exists is false for it but true for folders that deny listing
377+
if (Directory.Exists(path))
378+
{
379+
ShowLocationUnavailable(LocationUnavailableKind.AccessDenied);
380+
}
381+
else
382+
{
383+
ShowLocationUnavailable(LocationUnavailableKind.NotFound);
384+
WatchForLocationRestoration(path);
385+
}
386+
}
387+
388+
private FileSystemWatcher? locationRestorationWatcher;
389+
390+
private void WatchForLocationRestoration(string path)
391+
{
392+
StopWatchingForLocationRestoration();
393+
394+
var trimmedPath = path.TrimEnd(Path.DirectorySeparatorChar);
395+
var parentPath = Path.GetDirectoryName(trimmedPath);
396+
var folderName = Path.GetFileName(trimmedPath);
397+
if (string.IsNullOrEmpty(parentPath) || string.IsNullOrEmpty(folderName) || !Directory.Exists(parentPath))
398+
return;
399+
400+
try
401+
{
402+
var restorationWatcher = new FileSystemWatcher(parentPath, folderName)
403+
{
404+
NotifyFilter = NotifyFilters.DirectoryName
405+
};
406+
restorationWatcher.Created += LocationRestorationWatcher_Restored;
407+
restorationWatcher.Renamed += LocationRestorationWatcher_Restored;
408+
locationRestorationWatcher = restorationWatcher;
409+
restorationWatcher.EnableRaisingEvents = true;
410+
}
411+
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
412+
{
413+
// Parent folder was removed or is inaccessible
414+
StopWatchingForLocationRestoration();
415+
return;
416+
}
417+
418+
// The folder may have been restored before the watcher was armed
419+
if (Directory.Exists(path))
420+
LocationRestorationWatcher_Restored(this, new FileSystemEventArgs(WatcherChangeTypes.Created, parentPath, folderName));
421+
}
422+
423+
private void StopWatchingForLocationRestoration()
424+
{
425+
if (Interlocked.Exchange(ref locationRestorationWatcher, null) is FileSystemWatcher restorationWatcher)
426+
restorationWatcher.Dispose();
427+
}
428+
429+
private async void LocationRestorationWatcher_Restored(object sender, FileSystemEventArgs e)
430+
{
431+
if (Interlocked.Exchange(ref locationRestorationWatcher, null) is not FileSystemWatcher restorationWatcher)
432+
return;
433+
434+
restorationWatcher.Dispose();
435+
await dispatcherQueue.EnqueueOrInvokeAsync(() => RefreshItems(null));
436+
}
437+
322438
private NetworkAvailability networkAvailability = NetworkAvailability.All;
323439
public NetworkAvailability NetworkAvailability
324440
{
@@ -823,7 +939,7 @@ public string? SearchHeaderTitle
823939

824940
public void UpdateEmptyTextType()
825941
{
826-
var isFolderEmpty = FilesAndFolders.Count == 0;
942+
var isFolderEmpty = FilesAndFolders.Count == 0 && !IsLocationUnavailable;
827943

828944
EmptyTextType = isFolderEmpty ? (IsSearchResults ? EmptyTextType.NoSearchResultsFound : EmptyTextType.FolderEmpty) : EmptyTextType.None;
829945
}
@@ -1735,6 +1851,8 @@ private async Task RapidAddItemsToCollectionAsync(string path, string? previousD
17351851
{
17361852
IsSearchResults = false;
17371853
HasNoWatcher = false;
1854+
IsLocationUnavailable = false;
1855+
StopWatchingForLocationRestoration();
17381856
ItemLoadStatusChanged?.Invoke(this, new ItemLoadStatusChangedEventArgs() { Status = ItemLoadStatusChangedEventArgs.ItemLoadStatus.Starting });
17391857

17401858
CancelLoadAndClearFiles();
@@ -1938,25 +2056,19 @@ private async Task<int> EnumerateItemsFromStandardFolderAsync(string path, Cance
19382056
}
19392057
else if (res == FileSystemStatusCode.Unauthorized)
19402058
{
1941-
await DialogDisplayHelper.ShowDialogAsync(
1942-
Strings.AccessDenied.GetLocalizedResource(),
1943-
Strings.AccessDeniedToFolder.GetLocalizedResource());
2059+
ShowLocationInaccessibleOrMissing(path);
19442060

19452061
return -1;
19462062
}
19472063
else if (res == FileSystemStatusCode.NotFound)
19482064
{
1949-
await DialogDisplayHelper.ShowDialogAsync(
1950-
Strings.FolderNotFoundDialogTitle.GetLocalizedResource(),
1951-
Strings.FolderNotFoundDialogText.GetLocalizedResource());
2065+
ShowLocationInaccessibleOrMissing(path);
19522066

19532067
return -1;
19542068
}
19552069
else
19562070
{
1957-
await DialogDisplayHelper.ShowDialogAsync(
1958-
Strings.DriveUnpluggedDialogTitle.GetLocalizedResource(),
1959-
res.ErrorCode.ToString());
2071+
ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged, res.ErrorCode.ToString());
19602072

19612073
return -1;
19622074
}
@@ -2056,7 +2168,7 @@ await DialogDisplayHelper.ShowDialogAsync(
20562168

20572169
if (hFile == IntPtr.Zero)
20582170
{
2059-
await DialogDisplayHelper.ShowDialogAsync(Strings.DriveUnpluggedDialogTitle.GetLocalizedResource(), "");
2171+
ShowLocationUnavailable(LocationUnavailableKind.DriveUnplugged);
20602172

20612173
return -1;
20622174
}
@@ -2067,9 +2179,7 @@ await DialogDisplayHelper.ShowDialogAsync(
20672179
// errorCode == ERROR_ACCESS_DENIED
20682180
if (filesAndFolders.Count == 0 && errorCode == 0x5)
20692181
{
2070-
await DialogDisplayHelper.ShowDialogAsync(
2071-
Strings.AccessDenied.GetLocalizedResource(),
2072-
Strings.AccessDeniedToFolder.GetLocalizedResource());
2182+
ShowLocationInaccessibleOrMissing(path);
20732183

20742184
return -1;
20752185
}
@@ -3029,6 +3139,7 @@ public void UpdateDateDisplay()
30293139
public void Dispose()
30303140
{
30313141
CancelLoadAndClearFiles();
3142+
StopWatchingForLocationRestoration();
30323143
filterDebounceCS?.Cancel();
30333144
filterDebounceCS?.Dispose();
30343145
networkAvailabilityCTS?.Dispose();

src/Files.App/Views/Layouts/ColumnLayoutPage.xaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@
163163
Canvas.ZIndex="0"
164164
EmptyTextType="{x:Bind ParentShellPageInstance.ShellViewModel.EmptyTextType, Mode=OneWay}" />
165165

166+
<!-- Location Unavailable Indicator -->
167+
<uc:LocationUnavailableIndicator
168+
Title="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableTitle, Mode=OneWay}"
169+
Margin="12,0"
170+
HorizontalAlignment="Center"
171+
VerticalAlignment="Center"
172+
Canvas.ZIndex="0"
173+
Glyph="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableGlyph, Mode=OneWay}"
174+
Message="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableMessage, Mode=OneWay}"
175+
Visibility="{x:Bind ParentShellPageInstance.ShellViewModel.IsLocationUnavailable, Mode=OneWay}" />
176+
166177
<!-- Invalid Item Name Tip -->
167178
<TeachingTip
168179
x:Name="FileNameTeachingTip"

src/Files.App/Views/Layouts/DetailsLayoutPage.xaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,19 @@
202202
Canvas.ZIndex="0"
203203
EmptyTextType="{x:Bind ParentShellPageInstance.ShellViewModel.EmptyTextType, Mode=OneWay}" />
204204

205+
<!-- Location Unavailable Indicator -->
206+
<uc:LocationUnavailableIndicator
207+
Title="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableTitle, Mode=OneWay}"
208+
Grid.Row="3"
209+
MaxWidth="400"
210+
Margin="24,125,24,0"
211+
HorizontalAlignment="Center"
212+
VerticalAlignment="Top"
213+
Canvas.ZIndex="0"
214+
Glyph="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableGlyph, Mode=OneWay}"
215+
Message="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableMessage, Mode=OneWay}"
216+
Visibility="{x:Bind ParentShellPageInstance.ShellViewModel.IsLocationUnavailable, Mode=OneWay}" />
217+
205218
<!-- Invalid Item Name Tip -->
206219
<TeachingTip
207220
x:Name="FileNameTeachingTip"

src/Files.App/Views/Layouts/GridLayoutPage.xaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,18 @@
981981
Canvas.ZIndex="0"
982982
EmptyTextType="{x:Bind ParentShellPageInstance.ShellViewModel.EmptyTextType, Mode=OneWay}" />
983983

984+
<!-- Location Unavailable Indicator -->
985+
<uc:LocationUnavailableIndicator
986+
Title="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableTitle, Mode=OneWay}"
987+
MaxWidth="400"
988+
Margin="24,125,24,0"
989+
HorizontalAlignment="Center"
990+
VerticalAlignment="Top"
991+
Canvas.ZIndex="0"
992+
Glyph="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableGlyph, Mode=OneWay}"
993+
Message="{x:Bind ParentShellPageInstance.ShellViewModel.LocationUnavailableMessage, Mode=OneWay}"
994+
Visibility="{x:Bind ParentShellPageInstance.ShellViewModel.IsLocationUnavailable, Mode=OneWay}" />
995+
984996
<!-- Invalid Item Name Tip -->
985997
<TeachingTip
986998
x:Name="FileNameTeachingTip"

0 commit comments

Comments
 (0)