Skip to content

Commit cec20ff

Browse files
authored
Feature: Added a comparison text box for hashes (#16771)
1 parent 952b8b4 commit cec20ff

File tree

4 files changed

+161
-16
lines changed

4 files changed

+161
-16
lines changed

src/Files.App/Strings/en-US/Resources.resw

+16
Original file line numberDiff line numberDiff line change
@@ -4113,6 +4113,18 @@
41134113
<value>Add to shelf</value>
41144114
<comment>Tooltip that displays when dragging items to the Shelf Pane</comment>
41154115
</data>
4116+
<data name="EnterHashToCompare" xml:space="preserve">
4117+
<value>Enter a hash to compare</value>
4118+
<comment>Placeholder that appears in the compare hash text box</comment>
4119+
</data>
4120+
<data name="HashesMatch" xml:space="preserve">
4121+
<value>Matches {0}</value>
4122+
<comment>Appears when two compared hashes match, e.g. "Matches SHA256"</comment>
4123+
</data>
4124+
<data name="HashesDoNotMatch" xml:space="preserve">
4125+
<value>No matches found</value>
4126+
<comment>Appears when two compared hashes don't match</comment>
4127+
</data>
41164128
<data name="PathOrAlias" xml:space="preserve">
41174129
<value>Path or alias</value>
41184130
</data>
@@ -4156,4 +4168,8 @@
41564168
<value>Cannot clone repo</value>
41574169
<comment>Cannot clone repo dialog title</comment>
41584170
</data>
4171+
<data name="CompareFile" xml:space="preserve">
4172+
<value>Compare a file</value>
4173+
<comment>Button that appears in file hash properties that allows the user to compare two files</comment>
4174+
</data>
41594175
</root>

src/Files.App/ViewModels/Properties/HashesViewModel.cs

+90-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1-
// Copyright (c) Files Community
1+
// Copyright (c) Files Community
22
// Licensed under the MIT License.
33

44
using Files.Shared.Helpers;
5+
using Microsoft.UI.Windowing;
6+
using Microsoft.UI.Xaml.Controls;
57
using System.IO;
8+
using System.Security.Cryptography;
69
using System.Windows.Input;
710

811
namespace Files.App.ViewModels.Properties
912
{
1013
public sealed partial class HashesViewModel : ObservableObject, IDisposable
1114
{
15+
private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService<ICommonDialogService>();
1216
private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetService<IUserSettingsService>()!;
1317

18+
private readonly AppWindow _appWindow;
19+
1420
private HashInfoItem _selectedItem;
1521
public HashInfoItem SelectedItem
1622
{
@@ -23,16 +29,48 @@ public HashInfoItem SelectedItem
2329
public Dictionary<string, bool> ShowHashes { get; private set; }
2430

2531
public ICommand ToggleIsEnabledCommand { get; private set; }
32+
public ICommand CompareFileCommand { get; private set; }
2633

2734
private ListedItem _item;
2835

2936
private CancellationTokenSource _cancellationTokenSource;
3037

31-
public HashesViewModel(ListedItem item)
38+
private string _hashInput;
39+
public string HashInput
40+
{
41+
get => _hashInput;
42+
set
43+
{
44+
SetProperty(ref _hashInput, value);
45+
46+
OnHashInputTextChanged();
47+
OnPropertyChanged(nameof(IsInfoBarOpen));
48+
}
49+
}
50+
51+
private InfoBarSeverity _infoBarSeverity;
52+
public InfoBarSeverity InfoBarSeverity
53+
{
54+
get => _infoBarSeverity;
55+
set => SetProperty(ref _infoBarSeverity, value);
56+
}
57+
58+
private string _infoBarTitle;
59+
public string InfoBarTitle
60+
{
61+
get => _infoBarTitle;
62+
set => SetProperty(ref _infoBarTitle, value);
63+
}
64+
65+
public bool IsInfoBarOpen
66+
=> !string.IsNullOrEmpty(HashInput);
67+
68+
public HashesViewModel(ListedItem item, AppWindow appWindow)
3269
{
3370
ToggleIsEnabledCommand = new RelayCommand<string>(ToggleIsEnabled);
3471

3572
_item = item;
73+
_appWindow = appWindow;
3674
_cancellationTokenSource = new();
3775

3876
Hashes =
@@ -55,6 +93,8 @@ public HashesViewModel(ListedItem item)
5593
ShowHashes.TryAdd("SHA512", false);
5694

5795
Hashes.Where(x => ShowHashes[x.Algorithm]).ForEach(x => ToggleIsEnabledCommand.Execute(x.Algorithm));
96+
97+
CompareFileCommand = new RelayCommand(async () => await OnCompareFileAsync());
5898
}
5999

60100
private void ToggleIsEnabled(string? algorithm)
@@ -71,7 +111,7 @@ private void ToggleIsEnabled(string? algorithm)
71111
// Don't calculate hashes for online files
72112
if (_item.SyncStatusUI.SyncStatus is CloudDriveSyncStatus.FileOnline or CloudDriveSyncStatus.FolderOnline)
73113
{
74-
hashInfoItem.HashValue = "CalculationOnlineFileHashError".GetLocalizedResource();
114+
hashInfoItem.HashValue = Strings.CalculationOnlineFileHashError.GetLocalizedResource();
75115
return;
76116
}
77117

@@ -106,11 +146,11 @@ private void ToggleIsEnabled(string? algorithm)
106146
catch (IOException)
107147
{
108148
// File is currently open
109-
hashInfoItem.HashValue = "CalculationErrorFileIsOpen".GetLocalizedResource();
149+
hashInfoItem.HashValue = Strings.CalculationErrorFileIsOpen.GetLocalizedResource();
110150
}
111151
catch (Exception)
112152
{
113-
hashInfoItem.HashValue = "CalculationError".GetLocalizedResource();
153+
hashInfoItem.HashValue = Strings.CalculationError.GetLocalizedResource();
114154
}
115155
finally
116156
{
@@ -120,6 +160,51 @@ private void ToggleIsEnabled(string? algorithm)
120160
}
121161
}
122162

163+
public string FindMatchingAlgorithm(string hash)
164+
{
165+
if (string.IsNullOrEmpty(hash))
166+
return string.Empty;
167+
168+
return Hashes.FirstOrDefault(h => h.HashValue?.Equals(hash, StringComparison.OrdinalIgnoreCase) == true)?.Algorithm ?? string.Empty;
169+
}
170+
171+
public async Task<string> CalculateFileHashAsync(string filePath)
172+
{
173+
using var stream = File.OpenRead(filePath);
174+
using var md5 = MD5.Create();
175+
var hash = await Task.Run(() => md5.ComputeHash(stream));
176+
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
177+
}
178+
179+
private void OnHashInputTextChanged()
180+
{
181+
string matchingAlgorithm = FindMatchingAlgorithm(HashInput);
182+
183+
InfoBarSeverity = string.IsNullOrEmpty(matchingAlgorithm)
184+
? InfoBarSeverity.Error
185+
: InfoBarSeverity.Success;
186+
187+
InfoBarTitle = string.IsNullOrEmpty(matchingAlgorithm)
188+
? Strings.HashesDoNotMatch.GetLocalizedResource()
189+
: string.Format(Strings.HashesMatch.GetLocalizedResource(), matchingAlgorithm);
190+
}
191+
192+
private async Task OnCompareFileAsync()
193+
{
194+
var hWnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(_appWindow.Id);
195+
196+
var result = CommonDialogService.Open_FileOpenDialog(
197+
hWnd,
198+
false,
199+
[],
200+
Environment.SpecialFolder.Desktop,
201+
out var filePath);
202+
203+
HashInput = result && filePath != null
204+
? await CalculateFileHashAsync(filePath)
205+
: string.Empty;
206+
}
207+
123208
public void Dispose()
124209
{
125210
_cancellationTokenSource.Cancel();

src/Files.App/Views/Properties/HashesPage.xaml

+50-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- Copyright (c) Files Community. Licensed under the MIT License. -->
1+
<!-- Copyright (c) Files Community. Licensed under the MIT License. -->
22
<vm:BasePropertiesPage
33
x:Class="Files.App.Views.Properties.HashesPage"
44
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
@@ -25,20 +25,66 @@
2525
</vm:BasePropertiesPage.Resources>
2626

2727
<Grid x:Name="RootGrid">
28+
<Grid.RowDefinitions>
29+
<RowDefinition Height="Auto" />
30+
<RowDefinition Height="Auto" />
31+
<RowDefinition Height="*" />
32+
</Grid.RowDefinitions>
2833

34+
<!-- Hash Comparison Section -->
2935
<Grid
30-
x:Name="HashesListGrid"
31-
Margin="12"
36+
x:Name="HashComparisonGrid"
37+
Margin="12,12,12,4"
38+
Padding="12"
3239
VerticalAlignment="Top"
3340
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
3441
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
3542
BorderThickness="1"
43+
ColumnSpacing="8"
44+
CornerRadius="4">
45+
<Grid.ColumnDefinitions>
46+
<ColumnDefinition Width="*" />
47+
<ColumnDefinition Width="Auto" />
48+
</Grid.ColumnDefinitions>
49+
50+
<!-- Hash Input -->
51+
<TextBox
52+
x:Name="HashInputTextBox"
53+
Grid.Column="0"
54+
PlaceholderText="{helpers:ResourceString Name=EnterHashToCompare}"
55+
Text="{x:Bind HashesViewModel.HashInput, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
56+
57+
<!-- Compare File Button -->
58+
<Button
59+
x:Name="CompareFileButton"
60+
Grid.Column="1"
61+
Command="{x:Bind HashesViewModel.CompareFileCommand}"
62+
Content="{helpers:ResourceString Name=CompareFile}" />
63+
</Grid>
64+
65+
<InfoBar
66+
x:Name="HashMatchInfoBar"
67+
Grid.Row="1"
68+
Margin="12,4,12,4"
69+
x:Load="{x:Bind HashesViewModel.IsInfoBarOpen, Mode=OneWay}"
70+
IsClosable="False"
71+
IsOpen="True"
72+
Message="{x:Bind HashesViewModel.InfoBarTitle, Mode=OneWay}"
73+
Severity="{x:Bind HashesViewModel.InfoBarSeverity, Mode=OneWay}" />
74+
75+
<Grid
76+
x:Name="HashesListGrid"
77+
Grid.Row="2"
78+
Margin="12,4,12,12"
79+
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
80+
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
81+
BorderThickness="1"
3682
CornerRadius="4">
3783

3884
<!-- Hashes ListView -->
3985
<ListView
4086
x:Name="HashesListView"
41-
ItemsSource="{x:Bind HashesViewModel.Hashes, Mode=TwoWay}"
87+
ItemsSource="{x:Bind HashesViewModel.Hashes, Mode=OneWay}"
4288
SelectedItem="{x:Bind HashesViewModel.SelectedItem, Mode=TwoWay}">
4389

4490
<!-- Header -->

src/Files.App/Views/Properties/HashesPage.xaml.cs

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
// Copyright (c) Files Community
1+
// Copyright (c) Files Community
22
// Licensed under the MIT License.
33

4-
using Files.App.Data.Parameters;
5-
using Files.App.Utils;
64
using Files.App.ViewModels.Properties;
75
using Microsoft.UI.Xaml;
86
using Microsoft.UI.Xaml.Controls;
97
using Microsoft.UI.Xaml.Controls.Primitives;
108
using Microsoft.UI.Xaml.Navigation;
11-
using System.Threading.Tasks;
129

1310
namespace Files.App.Views.Properties
1411
{
@@ -25,9 +22,10 @@ public HashesPage()
2522

2623
protected override void OnNavigatedTo(NavigationEventArgs e)
2724
{
28-
var np = (PropertiesPageNavigationParameter)e.Parameter;
29-
if (np.Parameter is ListedItem listedItem)
30-
HashesViewModel = new(listedItem);
25+
var parameter = (PropertiesPageNavigationParameter)e.Parameter;
26+
27+
if (parameter.Parameter is ListedItem listedItem)
28+
HashesViewModel = new(listedItem, parameter.Window.AppWindow);
3129

3230
base.OnNavigatedTo(e);
3331
}

0 commit comments

Comments
 (0)