diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw
index b405a5de2436..9ddc306ebd2c 100644
--- a/src/Files.App/Strings/en-US/Resources.resw
+++ b/src/Files.App/Strings/en-US/Resources.resw
@@ -4113,6 +4113,18 @@
Add to shelf
Tooltip that displays when dragging items to the Shelf Pane
+
+ Enter a hash to compare
+ Placeholder that appears in the compare hash text box
+
+
+ Matches {0}
+ Appears when two compared hashes match, e.g. "Matches SHA256"
+
+
+ No matches found
+ Appears when two compared hashes don't match
+
Path or alias
@@ -4156,4 +4168,8 @@
Cannot clone repo
Cannot clone repo dialog title
+
+ Compare a file
+ Button that appears in file hash properties that allows the user to compare two files
+
\ No newline at end of file
diff --git a/src/Files.App/ViewModels/Properties/HashesViewModel.cs b/src/Files.App/ViewModels/Properties/HashesViewModel.cs
index 7bd2df6c4303..0b8d7ba17fb3 100644
--- a/src/Files.App/ViewModels/Properties/HashesViewModel.cs
+++ b/src/Files.App/ViewModels/Properties/HashesViewModel.cs
@@ -1,16 +1,22 @@
-// Copyright (c) Files Community
+// Copyright (c) Files Community
// Licensed under the MIT License.
using Files.Shared.Helpers;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml.Controls;
using System.IO;
+using System.Security.Cryptography;
using System.Windows.Input;
namespace Files.App.ViewModels.Properties
{
public sealed partial class HashesViewModel : ObservableObject, IDisposable
{
+ private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService();
private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetService()!;
+ private readonly AppWindow _appWindow;
+
private HashInfoItem _selectedItem;
public HashInfoItem SelectedItem
{
@@ -23,16 +29,48 @@ public HashInfoItem SelectedItem
public Dictionary ShowHashes { get; private set; }
public ICommand ToggleIsEnabledCommand { get; private set; }
+ public ICommand CompareFileCommand { get; private set; }
private ListedItem _item;
private CancellationTokenSource _cancellationTokenSource;
- public HashesViewModel(ListedItem item)
+ private string _hashInput;
+ public string HashInput
+ {
+ get => _hashInput;
+ set
+ {
+ SetProperty(ref _hashInput, value);
+
+ OnHashInputTextChanged();
+ OnPropertyChanged(nameof(IsInfoBarOpen));
+ }
+ }
+
+ private InfoBarSeverity _infoBarSeverity;
+ public InfoBarSeverity InfoBarSeverity
+ {
+ get => _infoBarSeverity;
+ set => SetProperty(ref _infoBarSeverity, value);
+ }
+
+ private string _infoBarTitle;
+ public string InfoBarTitle
+ {
+ get => _infoBarTitle;
+ set => SetProperty(ref _infoBarTitle, value);
+ }
+
+ public bool IsInfoBarOpen
+ => !string.IsNullOrEmpty(HashInput);
+
+ public HashesViewModel(ListedItem item, AppWindow appWindow)
{
ToggleIsEnabledCommand = new RelayCommand(ToggleIsEnabled);
_item = item;
+ _appWindow = appWindow;
_cancellationTokenSource = new();
Hashes =
@@ -55,6 +93,8 @@ public HashesViewModel(ListedItem item)
ShowHashes.TryAdd("SHA512", false);
Hashes.Where(x => ShowHashes[x.Algorithm]).ForEach(x => ToggleIsEnabledCommand.Execute(x.Algorithm));
+
+ CompareFileCommand = new RelayCommand(async () => await OnCompareFileAsync());
}
private void ToggleIsEnabled(string? algorithm)
@@ -71,7 +111,7 @@ private void ToggleIsEnabled(string? algorithm)
// Don't calculate hashes for online files
if (_item.SyncStatusUI.SyncStatus is CloudDriveSyncStatus.FileOnline or CloudDriveSyncStatus.FolderOnline)
{
- hashInfoItem.HashValue = "CalculationOnlineFileHashError".GetLocalizedResource();
+ hashInfoItem.HashValue = Strings.CalculationOnlineFileHashError.GetLocalizedResource();
return;
}
@@ -106,11 +146,11 @@ private void ToggleIsEnabled(string? algorithm)
catch (IOException)
{
// File is currently open
- hashInfoItem.HashValue = "CalculationErrorFileIsOpen".GetLocalizedResource();
+ hashInfoItem.HashValue = Strings.CalculationErrorFileIsOpen.GetLocalizedResource();
}
catch (Exception)
{
- hashInfoItem.HashValue = "CalculationError".GetLocalizedResource();
+ hashInfoItem.HashValue = Strings.CalculationError.GetLocalizedResource();
}
finally
{
@@ -120,6 +160,51 @@ private void ToggleIsEnabled(string? algorithm)
}
}
+ public string FindMatchingAlgorithm(string hash)
+ {
+ if (string.IsNullOrEmpty(hash))
+ return string.Empty;
+
+ return Hashes.FirstOrDefault(h => h.HashValue?.Equals(hash, StringComparison.OrdinalIgnoreCase) == true)?.Algorithm ?? string.Empty;
+ }
+
+ public async Task CalculateFileHashAsync(string filePath)
+ {
+ using var stream = File.OpenRead(filePath);
+ using var md5 = MD5.Create();
+ var hash = await Task.Run(() => md5.ComputeHash(stream));
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+
+ private void OnHashInputTextChanged()
+ {
+ string matchingAlgorithm = FindMatchingAlgorithm(HashInput);
+
+ InfoBarSeverity = string.IsNullOrEmpty(matchingAlgorithm)
+ ? InfoBarSeverity.Error
+ : InfoBarSeverity.Success;
+
+ InfoBarTitle = string.IsNullOrEmpty(matchingAlgorithm)
+ ? Strings.HashesDoNotMatch.GetLocalizedResource()
+ : string.Format(Strings.HashesMatch.GetLocalizedResource(), matchingAlgorithm);
+ }
+
+ private async Task OnCompareFileAsync()
+ {
+ var hWnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(_appWindow.Id);
+
+ var result = CommonDialogService.Open_FileOpenDialog(
+ hWnd,
+ false,
+ [],
+ Environment.SpecialFolder.Desktop,
+ out var filePath);
+
+ HashInput = result && filePath != null
+ ? await CalculateFileHashAsync(filePath)
+ : string.Empty;
+ }
+
public void Dispose()
{
_cancellationTokenSource.Cancel();
diff --git a/src/Files.App/Views/Properties/HashesPage.xaml b/src/Files.App/Views/Properties/HashesPage.xaml
index d945313e7669..6bcdc9aa8557 100644
--- a/src/Files.App/Views/Properties/HashesPage.xaml
+++ b/src/Files.App/Views/Properties/HashesPage.xaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Files.App/Views/Properties/HashesPage.xaml.cs b/src/Files.App/Views/Properties/HashesPage.xaml.cs
index 8671fd01cc77..ff1f7c722e3d 100644
--- a/src/Files.App/Views/Properties/HashesPage.xaml.cs
+++ b/src/Files.App/Views/Properties/HashesPage.xaml.cs
@@ -1,14 +1,11 @@
-// Copyright (c) Files Community
+// Copyright (c) Files Community
// Licensed under the MIT License.
-using Files.App.Data.Parameters;
-using Files.App.Utils;
using Files.App.ViewModels.Properties;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Navigation;
-using System.Threading.Tasks;
namespace Files.App.Views.Properties
{
@@ -25,9 +22,10 @@ public HashesPage()
protected override void OnNavigatedTo(NavigationEventArgs e)
{
- var np = (PropertiesPageNavigationParameter)e.Parameter;
- if (np.Parameter is ListedItem listedItem)
- HashesViewModel = new(listedItem);
+ var parameter = (PropertiesPageNavigationParameter)e.Parameter;
+
+ if (parameter.Parameter is ListedItem listedItem)
+ HashesViewModel = new(listedItem, parameter.Window.AppWindow);
base.OnNavigatedTo(e);
}