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 @@ - + + + + + + + + + + + + + + + + +