Skip to content

Commit a323308

Browse files
committed
Init
1 parent 762ba7a commit a323308

23 files changed

+453
-338
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using System.Security.Principal;
5+
6+
namespace Files.App.Storage.Watchers
7+
{
8+
public class RecycleBinWatcher : ITrashWatcher
9+
{
10+
private readonly List<SystemIO.FileSystemWatcher> _watchers = [];
11+
12+
/// <inheritdoc/>
13+
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemAdded;
14+
15+
/// <inheritdoc/>
16+
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemDeleted;
17+
18+
/// <inheritdoc/>
19+
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemChanged;
20+
21+
/// <inheritdoc/>
22+
public event EventHandler<SystemIO.FileSystemEventArgs>? ItemRenamed;
23+
24+
/// <inheritdoc/>
25+
public event EventHandler<SystemIO.FileSystemEventArgs>? RefreshRequested;
26+
27+
/// <summary>
28+
/// Initializes an instance of <see cref="RecycleBinWatcher"/> class.
29+
/// </summary>
30+
public RecycleBinWatcher()
31+
{
32+
StartWatcher();
33+
}
34+
35+
/// <inheritdoc/>
36+
public void StartWatcher()
37+
{
38+
// NOTE:
39+
// SHChangeNotifyRegister only works if recycle bin is open in File Explorer.
40+
// Create file system watcher to monitor recycle bin folder(s) instead.
41+
42+
// Listen changes only on the Recycle Bin that the current logon user has
43+
var sid = WindowsIdentity.GetCurrent().User?.ToString() ?? string.Empty;
44+
if (string.IsNullOrEmpty(sid))
45+
return;
46+
47+
// TODO: Use IStorageDevicesService to enumerate drives
48+
foreach (var drive in SystemIO.DriveInfo.GetDrives())
49+
{
50+
var recyclePath = SystemIO.Path.Combine(drive.Name, "$RECYCLE.BIN", sid);
51+
52+
if (drive.DriveType is SystemIO.DriveType.Network ||
53+
!SystemIO.Directory.Exists(recyclePath))
54+
continue;
55+
56+
SystemIO.FileSystemWatcher watcher = new()
57+
{
58+
Path = recyclePath,
59+
Filter = "*.*",
60+
NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName | SystemIO.NotifyFilters.DirectoryName
61+
};
62+
63+
watcher.Created += Watcher_Changed;
64+
watcher.Deleted += Watcher_Changed;
65+
watcher.EnableRaisingEvents = true;
66+
67+
_watchers.Add(watcher);
68+
}
69+
}
70+
71+
/// <inheritdoc/>
72+
public void StopWatcher()
73+
{
74+
foreach (var watcher in _watchers)
75+
watcher.Dispose();
76+
}
77+
78+
private void Watcher_Changed(object sender, SystemIO.FileSystemEventArgs e)
79+
{
80+
// Don't listen changes on files starting with '$I'
81+
if (string.IsNullOrEmpty(e.Name) ||
82+
e.Name.StartsWith("$I", StringComparison.Ordinal))
83+
return;
84+
85+
switch (e.ChangeType)
86+
{
87+
case SystemIO.WatcherChangeTypes.Created:
88+
ItemAdded?.Invoke(this, e);
89+
break;
90+
case SystemIO.WatcherChangeTypes.Deleted:
91+
ItemDeleted?.Invoke(this, e);
92+
break;
93+
case SystemIO.WatcherChangeTypes.Renamed:
94+
ItemRenamed?.Invoke(this, e);
95+
break;
96+
default:
97+
RefreshRequested?.Invoke(this, e);
98+
break;
99+
}
100+
}
101+
102+
/// <inheritdoc/>
103+
public void Dispose()
104+
{
105+
StopWatcher();
106+
}
107+
}
108+
}

src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4+
using Microsoft.UI.Xaml.Controls;
5+
using Windows.Foundation.Metadata;
6+
47
namespace Files.App.Actions
58
{
69
internal sealed class EmptyRecycleBinAction : BaseUIAction, IAction
710
{
11+
private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService<IWindowsRecycleBinService>();
12+
private readonly StatusCenterViewModel StatusCenterViewModel = Ioc.Default.GetRequiredService<StatusCenterViewModel>();
13+
private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
814
private readonly IContentPageContext context;
915

1016
public string Label
@@ -19,7 +25,7 @@ public RichGlyph Glyph
1925
public override bool IsExecutable =>
2026
UIHelpers.CanShowDialog &&
2127
((context.PageType == ContentPageTypes.RecycleBin && context.HasItem) ||
22-
RecycleBinHelpers.RecycleBinHasItems());
28+
WindowsRecycleBinService.HasItems());
2329

2430
public EmptyRecycleBinAction()
2531
{
@@ -30,7 +36,31 @@ public EmptyRecycleBinAction()
3036

3137
public async Task ExecuteAsync(object? parameter = null)
3238
{
33-
await RecycleBinHelpers.EmptyRecycleBinAsync();
39+
// TODO: Use AppDialogService
40+
var confirmationDialog = new ContentDialog()
41+
{
42+
Title = "ConfirmEmptyBinDialogTitle".GetLocalizedResource(),
43+
Content = "ConfirmEmptyBinDialogContent".GetLocalizedResource(),
44+
PrimaryButtonText = "Yes".GetLocalizedResource(),
45+
SecondaryButtonText = "Cancel".GetLocalizedResource(),
46+
DefaultButton = ContentDialogButton.Primary
47+
};
48+
49+
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
50+
confirmationDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;
51+
52+
if (UserSettingsService.FoldersSettingsService.DeleteConfirmationPolicy is DeleteConfirmationPolicies.Never ||
53+
await confirmationDialog.TryShowAsync() is ContentDialogResult.Primary)
54+
{
55+
var banner = StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.InProgress);
56+
57+
bool result = await Task.Run(WindowsRecycleBinService.DeleteAllAsync);
58+
59+
StatusCenterViewModel.RemoveItem(banner);
60+
61+
// Post a status based on the result
62+
StatusCenterHelper.AddCard_EmptyRecycleBin(result ? ReturnResult.Success : ReturnResult.Failed);
63+
}
3464
}
3565

3666
private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)

src/Files.App/Actions/FileSystem/RestoreAllRecycleBinAction.cs

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4+
using Microsoft.UI.Xaml.Controls;
5+
using Windows.Foundation.Metadata;
6+
47
namespace Files.App.Actions
58
{
69
internal sealed class RestoreAllRecycleBinAction : BaseUIAction, IAction
710
{
11+
private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService<IWindowsRecycleBinService>();
12+
813
public string Label
914
=> "RestoreAllItems".GetLocalizedResource();
1015

@@ -16,11 +21,42 @@ public RichGlyph Glyph
1621

1722
public override bool IsExecutable =>
1823
UIHelpers.CanShowDialog &&
19-
RecycleBinHelpers.RecycleBinHasItems();
24+
WindowsRecycleBinService.HasItems();
2025

2126
public async Task ExecuteAsync(object? parameter = null)
2227
{
23-
await RecycleBinHelpers.RestoreRecycleBinAsync();
28+
// TODO: Use AppDialogService
29+
var confirmationDialog = new ContentDialog()
30+
{
31+
Title = "ConfirmRestoreBinDialogTitle".GetLocalizedResource(),
32+
Content = "ConfirmRestoreBinDialogContent".GetLocalizedResource(),
33+
PrimaryButtonText = "Yes".GetLocalizedResource(),
34+
SecondaryButtonText = "Cancel".GetLocalizedResource(),
35+
DefaultButton = ContentDialogButton.Primary
36+
};
37+
38+
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
39+
confirmationDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;
40+
41+
if (await confirmationDialog.TryShowAsync() is not ContentDialogResult.Primary)
42+
return;
43+
44+
bool result = await Task.Run(WindowsRecycleBinService.RestoreAllAsync);
45+
46+
// Show error dialog when failed
47+
if (!result)
48+
{
49+
var errorDialog = new ContentDialog()
50+
{
51+
Title = "FailedToRestore".GetLocalizedResource(),
52+
PrimaryButtonText = "OK".GetLocalizedResource(),
53+
};
54+
55+
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
56+
errorDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;
57+
58+
await errorDialog.TryShowAsync();
59+
}
2460
}
2561
}
2662
}

src/Files.App/Actions/FileSystem/RestoreRecycleBinAction.cs

+30-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4+
using Microsoft.UI.Xaml.Controls;
5+
using Windows.Foundation.Metadata;
6+
using Windows.Storage;
7+
48
namespace Files.App.Actions
59
{
610
internal sealed class RestoreRecycleBinAction : BaseUIAction, IAction
@@ -30,8 +34,32 @@ public RestoreRecycleBinAction()
3034

3135
public async Task ExecuteAsync(object? parameter = null)
3236
{
33-
if (context.ShellPage is not null)
34-
await RecycleBinHelpers.RestoreSelectionRecycleBinAsync(context.ShellPage);
37+
var confirmationDialog = new ContentDialog()
38+
{
39+
Title = "ConfirmRestoreSelectionBinDialogTitle".GetLocalizedResource(),
40+
Content = string.Format("ConfirmRestoreSelectionBinDialogContent".GetLocalizedResource(), context.SelectedItems.Count),
41+
PrimaryButtonText = "Yes".GetLocalizedResource(),
42+
SecondaryButtonText = "Cancel".GetLocalizedResource(),
43+
DefaultButton = ContentDialogButton.Primary
44+
};
45+
46+
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
47+
confirmationDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;
48+
49+
ContentDialogResult result = await confirmationDialog.TryShowAsync();
50+
51+
if (result is not ContentDialogResult.Primary)
52+
return;
53+
54+
var items = context.SelectedItems.ToList().Where(x => x is RecycleBinItem).Select((item) => new
55+
{
56+
Source = StorageHelpers.FromPathAndType(
57+
item.ItemPath,
58+
item.PrimaryItemAttribute == StorageItemTypes.File ? FilesystemItemType.File : FilesystemItemType.Directory),
59+
Dest = ((RecycleBinItem)item).ItemOriginalPath
60+
});
61+
62+
await context.ShellPage!.FilesystemHelpers.RestoreItemsFromTrashAsync(items.Select(x => x.Source), items.Select(x => x.Dest), true);
3563
}
3664

3765
private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
namespace Files.App.Data.Contracts
5+
{
6+
/// <summary>
7+
/// Provides service for Recycle Bin on Windows.
8+
/// </summary>
9+
public interface IWindowsRecycleBinService
10+
{
11+
/// <summary>
12+
/// Gets the watcher of Recycle Bin folder.
13+
/// </summary>
14+
RecycleBinWatcher Watcher { get; }
15+
16+
/// <summary>
17+
/// Gets all Recycle Bin shell folders.
18+
/// </summary>
19+
/// <returns>A collection of Recycle Bin shell folders.</returns>
20+
Task<List<ShellFileItem>> GetAllRecycleBinFoldersAsync();
21+
22+
/// <summary>
23+
/// Gets the used size of Recycle Bin.
24+
/// </summary>
25+
/// <returns></returns>
26+
ulong GetSize();
27+
28+
/// <summary>
29+
/// Gets the value that indicates whether Recycle Bin folder has item(s).
30+
/// </summary>
31+
/// <returns></returns>
32+
bool HasItems();
33+
34+
/// <summary>
35+
/// Gets the file or folder specified is already moved to Recycle Bin.
36+
/// </summary>
37+
/// <param name="path">The path that indicates to a file or folder.</param>
38+
/// <returns>True if the file or path is recycled; otherwise, false.</returns>
39+
bool IsRecycled(string? path);
40+
41+
/// <summary>
42+
/// Gets the file or folder specified can be moved to Recycle Bin.
43+
/// </summary>
44+
/// <param name="path"></param>
45+
/// <returns></returns>
46+
Task<bool> IsRecyclableAsync(string? path);
47+
48+
/// <summary>
49+
/// Deletes files and folders in Recycle Bin permanently.
50+
/// </summary>
51+
/// <returns>True if succeeded; otherwise, false</returns>
52+
bool DeleteAllAsync();
53+
54+
/// <summary>
55+
/// Restores files and folders in Recycle Bin to original paths.
56+
/// </summary>
57+
/// <returns>True if succeeded; otherwise, false</returns>
58+
bool RestoreAllAsync();
59+
}
60+
}

src/Files.App/Data/Items/ListedItem.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -501,8 +501,8 @@ public FtpItem(FtpListItem item, string folder) : base(null)
501501

502502
public async Task<IStorageItem> ToStorageItem() => PrimaryItemAttribute switch
503503
{
504-
StorageItemTypes.File => await new FtpStorageFile(ItemPath, ItemNameRaw, ItemDateCreatedReal).ToStorageFileAsync(),
505-
StorageItemTypes.Folder => new FtpStorageFolder(ItemPath, ItemNameRaw, ItemDateCreatedReal),
504+
StorageItemTypes.File => await new Utils.Storage.FtpStorageFile(ItemPath, ItemNameRaw, ItemDateCreatedReal).ToStorageFileAsync(),
505+
StorageItemTypes.Folder => new Utils.Storage.FtpStorageFolder(ItemPath, ItemNameRaw, ItemDateCreatedReal),
506506
_ => throw new InvalidDataException(),
507507
};
508508
}

src/Files.App/Data/Items/LocationItem.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,13 @@ public int CompareTo(INavigationControlItem other)
124124

125125
public sealed class RecycleBinLocationItem : LocationItem
126126
{
127-
public void RefreshSpaceUsed(object sender, FileSystemEventArgs e)
127+
private readonly IWindowsRecycleBinService WindowsRecycleBinService = Ioc.Default.GetRequiredService<IWindowsRecycleBinService>();
128+
129+
public void RefreshSpaceUsed(object? sender, FileSystemEventArgs e)
128130
{
129131
MainWindow.Instance.DispatcherQueue.TryEnqueue(() =>
130132
{
131-
SpaceUsed = RecycleBinHelpers.GetSize();
133+
SpaceUsed = WindowsRecycleBinService.GetSize();
132134
});
133135
}
134136

@@ -150,10 +152,10 @@ public override object ToolTip
150152

151153
public RecycleBinLocationItem()
152154
{
153-
SpaceUsed = RecycleBinHelpers.GetSize();
155+
SpaceUsed = WindowsRecycleBinService.GetSize();
154156

155-
RecycleBinManager.Default.RecycleBinItemCreated += RefreshSpaceUsed;
156-
RecycleBinManager.Default.RecycleBinItemDeleted += RefreshSpaceUsed;
157+
WindowsRecycleBinService.Watcher.ItemAdded += RefreshSpaceUsed;
158+
WindowsRecycleBinService.Watcher.ItemDeleted += RefreshSpaceUsed;
157159
}
158160
}
159161
}

src/Files.App/GlobalUsings.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
global using global::Files.App.Utils.Git;
3131
global using global::Files.App.Utils.Library;
3232
global using global::Files.App.Utils.RecentItem;
33-
global using global::Files.App.Utils.RecycleBin;
3433
global using global::Files.App.Utils.Serialization;
3534
global using global::Files.App.Utils.Shell;
3635
global using global::Files.App.Utils.StatusCenter;
@@ -79,6 +78,11 @@
7978
global using global::Files.Core.Storage.Extensions;
8079
global using global::Files.Core.Storage.StorageEnumeration;
8180

81+
// Files.App.Storage
82+
83+
global using global::Files.App.Storage.Storables;
84+
global using global::Files.App.Storage.Watchers;
85+
8286
// Files.Shared
8387
global using global::Files.Shared;
8488
global using global::Files.Shared.Extensions;

0 commit comments

Comments
 (0)