Skip to content

Commit 23b56d5

Browse files
committed
Make the undo and redo actions work.
1 parent ffa75ac commit 23b56d5

File tree

10 files changed

+96
-46
lines changed

10 files changed

+96
-46
lines changed

src/ImageSort.Avalonia/ImageSort.Avalonia.csproj

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19-
<PackageReference Include="Avalonia" Version="11.0.10" />
20-
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
21-
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
22-
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
19+
<PackageReference Include="Avalonia" Version="11.3.0" />
20+
<PackageReference Include="Avalonia.Desktop" Version="11.3.0" />
21+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.0" />
22+
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.0" />
2323
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
24-
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.10">
24+
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.0">
2525
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
2626
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
2727
</PackageReference>
28-
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
29-
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />
28+
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
29+
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.0" />
3030
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
3131
<PackageReference Include="System.Reactive" Version="6.0.1" />
3232
<PackageReference Include="BinToss.GroupBox.Avalonia" Version="1.0.0" />
33-
<PackageReference Include="MessageBox.Avalonia" Version="3.1.0" />
33+
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
3434
<!-- Add AdonisUI packages if you plan to use them -->
3535
<!-- <PackageReference Include="AdonisUI.Avalonia" Version="X.Y.Z" /> -->
3636
<!-- <PackageReference Include="AdonisUI.ClassicTheme.Avalonia" Version="X.Y.Z" /> -->

src/ImageSort.Avalonia/ViewModels/MainWindowViewModel.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using MsBox.Avalonia.Enums; // For message box button/icon enums
1414
using System.Threading.Tasks; // For Task
1515
using ImageSort.Avalonia.Views; // For InputDialog
16+
using ImageSort.Localization; // For Text resource
1617

1718
namespace ImageSort.Avalonia.ViewModels;
1819

@@ -145,5 +146,24 @@ public MainWindowViewModel(FoldersViewModel foldersViewModel, ImagesViewModel im
145146

146147
interaction.SetOutput(Unit.Default);
147148
});
149+
150+
// Handler for ActionsViewModel.NotifyUserOfError
151+
this.Actions.NotifyUserOfError.RegisterHandler(async interaction =>
152+
{
153+
var message = interaction.Input;
154+
var box = MessageBoxManager.GetMessageBoxStandard("Error", message, ButtonEnum.Ok, Icon.Error);
155+
156+
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
157+
if (mainWindow != null)
158+
{
159+
await box.ShowWindowDialogAsync(mainWindow);
160+
}
161+
else
162+
{
163+
await box.ShowAsync(); // Show as a standalone window if main window not found
164+
}
165+
166+
interaction.SetOutput(Unit.Default);
167+
});
148168
}
149169
}

src/ImageSort.Avalonia/Views/ActionsView.axaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
44
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
55
xmlns:vm="using:ImageSort.ViewModels"
6-
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="300"
6+
xmlns:text="using:ImageSort.Localization"
7+
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="Auto"
78
x:Class="ImageSort.Avalonia.Views.ActionsView"
89
x:DataType="vm:ActionsViewModel">
910
<Design.DataContext>
1011
<vm:ActionsViewModel/>
1112
</Design.DataContext>
12-
<TextBlock Text="Actions View Content (Placeholder)" HorizontalAlignment="Center" VerticalAlignment="Center"/>
13+
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto">
14+
<Button Name="UndoButton" Content="{x:Static text:Text.Undo}" Command="{Binding Undo}" IsEnabled="{Binding CanUndo}" Margin="0,0,2,0" Grid.Column="0"/>
15+
<Button Name="RedoButton" Content="{x:Static text:Text.Redo}" Command="{Binding Redo}" IsEnabled="{Binding CanRedo}" Margin="2,0,0,0" Grid.Column="1"/>
16+
</Grid>
1317
</UserControl>

src/ImageSort.Localization/Text.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ImageSort.Localization/Text.de-DE.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ Hinweis: In den meisten Fällen ist die Datei beschädigt</value>
175175
<data name="MoveActionMessage" xml:space="preserve">
176176
<value>{FileName} nach {Directory} verschieben</value>
177177
</data>
178+
<data name="MoveActionFileExistsError" xml:space="preserve">
179+
<value>Das Bild kann nicht verschoben werden. Eine Datei mit dem Namen "{FileName}" existiert bereits am Zielort.</value>
180+
</data>
178181
<data name="NewFolderPromptText" xml:space="preserve">
179182
<value>Welchen Namen soll der Ordner haben?</value>
180183
</data>

src/ImageSort.Localization/Text.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ Note: Usually this is because the file is damaged</value>
175175
<data name="MoveActionMessage" xml:space="preserve">
176176
<value>Move {FileName} to {Directory}</value>
177177
</data>
178+
<data name="MoveActionFileExistsError" xml:space="preserve">
179+
<value>Cannot move the image. A file named "{FileName}" already exists at the destination.</value>
180+
</data>
178181
<data name="NewFolderPromptText" xml:space="preserve">
179182
<value>What name should the folder have?</value>
180183
</data>

src/ImageSort/Actions/MoveAction.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public MoveAction(string file, string toFolder, IFileSystem fileSystem,
4444

4545
public void Act()
4646
{
47+
if (fileSystem.FileExists(newDestination))
48+
{
49+
throw new IOException(Text.MoveActionFileExistsError.Replace("{FileName}", newDestination, StringComparison.OrdinalIgnoreCase));
50+
}
4751
fileSystem.Move(oldDestination, newDestination);
4852

4953
notifyAct?.Invoke(oldDestination, newDestination);

src/ImageSort/ImageSort.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.133">
11+
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
1212
<PrivateAssets>all</PrivateAssets>
1313
</PackageReference>
14-
<PackageReference Include="DynamicData" Version="9.2.2" />
14+
<PackageReference Include="DynamicData" Version="9.3.2" />
1515
<PackageReference Include="ReactiveUI" Version="20.2.45" />
1616
<PackageReference Include="MetadataExtractor" Version="2.8.1" />
1717
</ItemGroup>

src/ImageSort/ViewModels/ActionsViewModel.cs

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Reactive;
77
using System.Reactive.Linq;
8+
using System.Reactive.Subjects; // Added for Subject
89

910
namespace ImageSort.ViewModels;
1011

@@ -19,33 +20,46 @@ public class ActionsViewModel : ReactiveObject
1920
private readonly ObservableAsPropertyHelper<string> lastUndone;
2021
public string LastUndone => lastUndone.Value;
2122

23+
private readonly ObservableAsPropertyHelper<bool> _canUndo;
24+
public bool CanUndo => _canUndo.Value;
25+
26+
private readonly ObservableAsPropertyHelper<bool> _canRedo;
27+
public bool CanRedo => _canRedo.Value;
28+
2229
public Interaction<string, Unit> NotifyUserOfError { get; } = new Interaction<string, Unit>();
2330

2431
public ReactiveCommand<IReversibleAction, Unit> Execute { get; }
2532
public ReactiveCommand<Unit, Unit> Undo { get; }
2633
public ReactiveCommand<Unit, Unit> Redo { get; }
2734
public ReactiveCommand<Unit, Unit> Clear { get; }
2835

36+
private readonly Subject<Unit> _historyChangedSignal = new Subject<Unit>();
37+
2938
public ActionsViewModel()
3039
{
40+
var canUndoObservable = _historyChangedSignal
41+
.Select(_ => done.Count > 0)
42+
.StartWith(done.Count > 0);
43+
44+
var canRedoObservable = _historyChangedSignal
45+
.Select(_ => undone.Count > 0)
46+
.StartWith(undone.Count > 0);
47+
3148
Execute = ReactiveCommand.CreateFromTask<IReversibleAction>(async action =>
3249
{
3350
try
3451
{
3552
action.Act();
53+
done.Push(action);
54+
undone.Clear(); // Clear redo stack on new action
55+
_historyChangedSignal.OnNext(Unit.Default);
3656
}
3757
catch (Exception ex)
3858
{
3959
await NotifyUserOfError.Handle(Text.CouldNotActErrorText
4060
.Replace("{ErrorMessage}", ex.Message, StringComparison.OrdinalIgnoreCase)
4161
.Replace("{ActMessage}", action.DisplayName, StringComparison.OrdinalIgnoreCase));
42-
43-
return;
4462
}
45-
46-
done.Push(action);
47-
48-
undone.Clear();
4963
});
5064

5165
Undo = ReactiveCommand.CreateFromTask(async () =>
@@ -55,65 +69,59 @@ await NotifyUserOfError.Handle(Text.CouldNotActErrorText
5569
try
5670
{
5771
action.Revert();
72+
undone.Push(action);
73+
_historyChangedSignal.OnNext(Unit.Default);
5874
}
5975
catch (Exception ex)
6076
{
6177
await NotifyUserOfError.Handle(Text.CouldNotUndoErrorText
6278
.Replace("{ErrorMessage}", ex.Message, StringComparison.OrdinalIgnoreCase)
6379
.Replace("{ActMessage}", action.DisplayName, StringComparison.OrdinalIgnoreCase));
64-
65-
return;
80+
// Signal change even if revert fails, because 'done' stack changed.
81+
_historyChangedSignal.OnNext(Unit.Default);
6682
}
67-
68-
undone.Push(action);
6983
}
70-
});
84+
}, canUndoObservable);
7185

7286
Redo = ReactiveCommand.CreateFromTask(async () =>
7387
{
7488
if (undone.TryPop(out var action))
7589
{
7690
try
7791
{
78-
action.Act();
92+
action.Act(); // Re-apply the action
93+
done.Push(action);
94+
_historyChangedSignal.OnNext(Unit.Default);
7995
}
8096
catch (Exception ex)
8197
{
8298
await NotifyUserOfError.Handle(Text.CouldNotRedoErrorText
8399
.Replace("{ErrorMessage}", ex.Message, StringComparison.OrdinalIgnoreCase)
84100
.Replace("{ActMessage}", action.DisplayName, StringComparison.OrdinalIgnoreCase));
85-
86-
return;
101+
// Signal change even if re-act fails, because 'undone' stack changed.
102+
_historyChangedSignal.OnNext(Unit.Default);
87103
}
88-
89-
done.Push(action);
90104
}
91-
});
105+
}, canRedoObservable);
92106

93107
Clear = ReactiveCommand.Create(() =>
94108
{
95109
done.Clear();
96110
undone.Clear();
111+
_historyChangedSignal.OnNext(Unit.Default);
97112
});
98113

99-
var historyChanges = Execute.Merge(Undo).Merge(Redo).Merge(Clear);
100-
101-
lastDone = historyChanges
102-
.Select(_ =>
103-
{
104-
if (done.TryPeek(out var action)) return action.DisplayName;
114+
_canUndo = canUndoObservable.ToProperty(this, vm => vm.CanUndo);
115+
_canRedo = canRedoObservable.ToProperty(this, vm => vm.CanRedo);
105116

106-
return null;
107-
})
117+
lastDone = _historyChangedSignal
118+
.Select(_ => done.TryPeek(out var action) ? action.DisplayName : null)
119+
.StartWith(done.TryPeek(out var action) ? action.DisplayName : null)
108120
.ToProperty(this, vm => vm.LastDone);
109121

110-
lastUndone = historyChanges
111-
.Select(_ =>
112-
{
113-
if (undone.TryPeek(out var action)) return action.DisplayName;
114-
115-
return null;
116-
})
122+
lastUndone = _historyChangedSignal
123+
.Select(_ => undone.TryPeek(out var action) ? action.DisplayName : null)
124+
.StartWith(undone.TryPeek(out var undoneAction) ? undoneAction.DisplayName : null) // Renamed variable here
117125
.ToProperty(this, vm => vm.LastUndone);
118126
}
119127
}

src/ImageSort/ViewModels/FoldersViewModel.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77
using System;
88
using System.Collections.Generic;
99
using System.Collections.ObjectModel;
10+
using System.IO; // Required for Path.GetFileName
1011
using System.Linq;
1112
using System.Reactive;
1213
using System.Reactive.Concurrency;
1314
using System.Reactive.Linq;
14-
using System.Collections.Generic; // Required for List<T>
15-
using DynamicData.Binding; // Required for ToCollection()
1615

1716
namespace ImageSort.ViewModels;
1817

0 commit comments

Comments
 (0)