Skip to content

Commit ccc4ec4

Browse files
NeWbY100claude
andcommitted
feat: add 6 quality-of-life features
1. Copy from Property Grid — right-click context menu on Inspector and Compare property grids with Copy Name/Value/Both options 2. Taskbar Progress — Windows taskbar shows progress during SRR/SRS creation, reconstruction, and sample restoration 3. Remember Window State — persists window size, position, maximized state, and selected tab to %LOCALAPPDATA% between sessions 4. Drag-Drop Enhancements — SRR Creator stored files DataGrid accepts file drops; MainWindow accepts .rar files (opens in Inspector) 5. Batch SRS Creation — toggle batch mode in SRS Creator to process multiple media files at once with per-file status and progress 6. SRR Editing — add/remove stored files from existing SRR files via Inspector context menu (block-level binary modification in library) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3bba11f commit ccc4ec4

21 files changed

Lines changed: 784 additions & 27 deletions

ReScene.Lib

ReScene.NET/App.xaml.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ protected override void OnStartup(StartupEventArgs e)
1212
base.OnStartup(e);
1313

1414
var tempDir = new TempDirectoryService();
15+
var windowState = new WindowStateService();
1516
MainWindow = new MainWindow
1617
{
18+
WindowStateService = windowState,
1719
DataContext = new MainWindowViewModel(
1820
new SrrCreationService(), new SrsCreationService(), new SrsReconstructionService(),
19-
new SampleRestorerService(tempDir), new BruteForceService(), new FileCompareService(), new FileDialogService(), new RecentFilesService(), tempDir)
21+
new SampleRestorerService(tempDir), new BruteForceService(), new FileCompareService(), new FileDialogService(), new RecentFilesService(), tempDir, new SrrEditingService())
2022
};
2123
MainWindow.Show();
2224
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace ReScene.NET.Models;
2+
3+
/// <summary>
4+
/// Persisted window position, size, and UI state.
5+
/// </summary>
6+
public class WindowStateModel
7+
{
8+
public double Left { get; set; }
9+
public double Top { get; set; }
10+
public double Width { get; set; } = 1280;
11+
public double Height { get; set; } = 900;
12+
public bool IsMaximized { get; set; } = true;
13+
public int SelectedTabIndex { get; set; }
14+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace ReScene.NET.Services;
2+
3+
/// <summary>
4+
/// Service for editing existing SRR files by adding or removing stored files.
5+
/// </summary>
6+
public interface ISrrEditingService
7+
{
8+
/// <summary>
9+
/// Adds one or more stored files to an existing SRR file.
10+
/// </summary>
11+
/// <param name="srrFilePath">
12+
/// Path to the SRR file to modify.
13+
/// </param>
14+
/// <param name="files">
15+
/// List of tuples containing the stored name and file path for each file to add.
16+
/// </param>
17+
public void AddStoredFiles(string srrFilePath, IReadOnlyList<(string StoredName, string FilePath)> files);
18+
19+
/// <summary>
20+
/// Removes stored files from an existing SRR file by name.
21+
/// </summary>
22+
/// <param name="srrFilePath">
23+
/// Path to the SRR file to modify.
24+
/// </param>
25+
/// <param name="storedNames">
26+
/// List of stored file names to remove.
27+
/// </param>
28+
public void RemoveStoredFiles(string srrFilePath, IReadOnlyList<string> storedNames);
29+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using ReScene.NET.Models;
2+
3+
namespace ReScene.NET.Services;
4+
5+
/// <summary>
6+
/// Loads and saves the main window position, size, and UI state.
7+
/// </summary>
8+
public interface IWindowStateService
9+
{
10+
public WindowStateModel? Load();
11+
public void Save(WindowStateModel state);
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using ReScene.SRR;
2+
3+
namespace ReScene.NET.Services;
4+
5+
/// <summary>
6+
/// Service wrapper around <see cref="SRREditor"/> for editing existing SRR files.
7+
/// </summary>
8+
public class SrrEditingService : ISrrEditingService
9+
{
10+
/// <inheritdoc />
11+
public void AddStoredFiles(string srrFilePath, IReadOnlyList<(string StoredName, string FilePath)> files)
12+
=> SRREditor.AddStoredFiles(srrFilePath, files);
13+
14+
/// <inheritdoc />
15+
public void RemoveStoredFiles(string srrFilePath, IReadOnlyList<string> storedNames)
16+
=> SRREditor.RemoveStoredFiles(srrFilePath, storedNames);
17+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Text.Json;
2+
using ReScene.NET.Models;
3+
4+
namespace ReScene.NET.Services;
5+
6+
/// <summary>
7+
/// Persists window state to a JSON file in local app data.
8+
/// </summary>
9+
public class WindowStateService : IWindowStateService
10+
{
11+
private static readonly JsonSerializerOptions _serializerOptions = new() { WriteIndented = true };
12+
private static readonly string _appDataDir = Path.Combine(
13+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
14+
"ReScene.NET");
15+
private static readonly string _filePath = Path.Combine(_appDataDir, "window-state.json");
16+
17+
public WindowStateModel? Load()
18+
{
19+
try
20+
{
21+
if (!File.Exists(_filePath))
22+
{
23+
return null;
24+
}
25+
26+
string json = File.ReadAllText(_filePath);
27+
return JsonSerializer.Deserialize<WindowStateModel>(json);
28+
}
29+
catch
30+
{
31+
return null;
32+
}
33+
}
34+
35+
public void Save(WindowStateModel state)
36+
{
37+
try
38+
{
39+
Directory.CreateDirectory(_appDataDir);
40+
string json = JsonSerializer.Serialize(state, _serializerOptions);
41+
File.WriteAllText(_filePath, json);
42+
}
43+
catch
44+
{
45+
// Silently ignore persistence errors
46+
}
47+
}
48+
}

ReScene.NET/ViewModels/CreatorViewModel.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ private async Task AddStoredFileAsync()
147147
}
148148
}
149149

150+
/// <summary>
151+
/// Adds files to the stored files list, skipping duplicates. Called from code-behind drag-drop.
152+
/// </summary>
153+
public void AddStoredFiles(IEnumerable<string> paths)
154+
{
155+
foreach (string path in paths)
156+
{
157+
if (StoredFiles.Any(f => f.FullPath.Equals(path, StringComparison.OrdinalIgnoreCase)))
158+
{
159+
continue;
160+
}
161+
162+
StoredFiles.Add(new StoredFileItem
163+
{
164+
FullPath = path,
165+
StoredName = ComputeStoredName(path)
166+
});
167+
}
168+
}
169+
150170
[RelayCommand]
151171
private void RemoveStoredFile()
152172
{

ReScene.NET/ViewModels/InspectorViewModel.cs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212

1313
namespace ReScene.NET.ViewModels;
1414

15-
public partial class InspectorViewModel(IFileDialogService fileDialog) : ViewModelBase, IDisposable
15+
public partial class InspectorViewModel(IFileDialogService fileDialog, ISrrEditingService srrEditingService) : ViewModelBase, IDisposable
1616
{
1717
private const int ExportBufferSize = 80 * 1024;
1818

1919
private readonly IFileDialogService _fileDialog = fileDialog;
20+
private readonly ISrrEditingService _srrEditingService = srrEditingService;
2021
private SrrFileData? _srrData;
2122
private SrsInspectorData? _srsData;
2223
private List<RARDetailedBlock>? _rarDetailedBlocks;
@@ -215,6 +216,7 @@ partial void OnSelectedTreeNodeChanged(TreeNodeViewModel? value)
215216
HexSelectionOffset = -1;
216217
HexSelectionLength = 0;
217218
ExportBlockCommand.NotifyCanExecuteChanged();
219+
RemoveStoredFileFromSrrCommand.NotifyCanExecuteChanged();
218220

219221
if (value?.Tag is RARDetailedBlock detailedBlock)
220222
{
@@ -383,6 +385,75 @@ await Task.Run(() =>
383385
}
384386
}
385387

388+
private bool IsSrrFileLoaded()
389+
{
390+
if (string.IsNullOrEmpty(_loadedFilePathInternal))
391+
{
392+
return false;
393+
}
394+
395+
return Path.GetExtension(_loadedFilePathInternal)
396+
.Equals(".srr", StringComparison.OrdinalIgnoreCase);
397+
}
398+
399+
private bool CanAddStoredFile() => IsSrrFileLoaded();
400+
401+
[RelayCommand(CanExecute = nameof(CanAddStoredFile))]
402+
private async Task AddStoredFileToSrrAsync()
403+
{
404+
if (!IsSrrFileLoaded())
405+
{
406+
return;
407+
}
408+
409+
string? filePath = await _fileDialog.OpenFileAsync("Select File to Add",
410+
FileDialogFilters.AllFiles);
411+
412+
if (filePath is null)
413+
{
414+
return;
415+
}
416+
417+
try
418+
{
419+
string storedName = Path.GetFileName(filePath);
420+
_srrEditingService.AddStoredFiles(_loadedFilePathInternal!,
421+
[(storedName, filePath)]);
422+
423+
StatusMessage = $"Added stored file: {storedName}";
424+
LoadFile(_loadedFilePathInternal!);
425+
}
426+
catch (Exception ex)
427+
{
428+
StatusMessage = $"Error adding stored file: {ex.Message}";
429+
}
430+
}
431+
432+
private bool CanRemoveStoredFile()
433+
=> IsSrrFileLoaded() && SelectedTreeNode?.Tag is SrrStoredFileBlock;
434+
435+
[RelayCommand(CanExecute = nameof(CanRemoveStoredFile))]
436+
private void RemoveStoredFileFromSrr()
437+
{
438+
if (!IsSrrFileLoaded() || SelectedTreeNode?.Tag is not SrrStoredFileBlock stored)
439+
{
440+
return;
441+
}
442+
443+
try
444+
{
445+
_srrEditingService.RemoveStoredFiles(_loadedFilePathInternal!,
446+
[stored.FileName]);
447+
448+
StatusMessage = $"Removed stored file: {stored.FileName}";
449+
LoadFile(_loadedFilePathInternal!);
450+
}
451+
catch (Exception ex)
452+
{
453+
StatusMessage = $"Error removing stored file: {ex.Message}";
454+
}
455+
}
456+
386457
private void BuildTree()
387458
{
388459
TreeRoots.Clear();

ReScene.NET/ViewModels/MainWindowViewModel.cs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Reflection;
2+
using System.Windows.Shell;
23
using CommunityToolkit.Mvvm.ComponentModel;
34
using CommunityToolkit.Mvvm.Input;
45
using ReScene.NET.Helpers;
@@ -59,6 +60,12 @@ public FileCompareViewModel FileCompare
5960
[ObservableProperty]
6061
private bool _isBusy;
6162

63+
[ObservableProperty]
64+
private TaskbarItemProgressState _taskbarProgressState = TaskbarItemProgressState.None;
65+
66+
[ObservableProperty]
67+
private double _taskbarProgressValue;
68+
6269
public string AppVersion { get; } = GetAppVersion();
6370

6471
private static string GetAppVersion()
@@ -77,16 +84,16 @@ private static string GetAppVersion()
7784
}
7885

7986
public MainWindowViewModel()
80-
: this(new SrrCreationService(), new SrsCreationService(), new SrsReconstructionService(), new SampleRestorerService(new TempDirectoryService()), new BruteForceService(), new FileCompareService(), new FileDialogService(), new RecentFilesService(), new TempDirectoryService())
87+
: this(new SrrCreationService(), new SrsCreationService(), new SrsReconstructionService(), new SampleRestorerService(new TempDirectoryService()), new BruteForceService(), new FileCompareService(), new FileDialogService(), new RecentFilesService(), new TempDirectoryService(), new SrrEditingService())
8188
{
8289
}
8390

84-
public MainWindowViewModel(ISrrCreationService srrService, ISrsCreationService srsService, ISrsReconstructionService srsReconService, ISampleRestorerService sampleRestorerService, IBruteForceService bruteForceService, IFileCompareService fileCompareService, IFileDialogService fileDialog, IRecentFilesService recentFiles, ITempDirectoryService tempDir)
91+
public MainWindowViewModel(ISrrCreationService srrService, ISrsCreationService srsService, ISrsReconstructionService srsReconService, ISampleRestorerService sampleRestorerService, IBruteForceService bruteForceService, IFileCompareService fileCompareService, IFileDialogService fileDialog, IRecentFilesService recentFiles, ITempDirectoryService tempDir, ISrrEditingService srrEditingService)
8592
{
8693
_fileDialog = fileDialog;
8794
_recentFiles = recentFiles;
8895

89-
Inspector = new InspectorViewModel(fileDialog);
96+
Inspector = new InspectorViewModel(fileDialog, srrEditingService);
9097
Creator = new CreatorViewModel(srrService, srsService, fileDialog, tempDir);
9198
SrsCreator = new SrsCreatorViewModel(srsService, fileDialog, tempDir);
9299
Reconstructor = new ReconstructorViewModel(bruteForceService, fileDialog);
@@ -113,41 +120,46 @@ public MainWindowViewModel(ISrrCreationService srrService, ISrsCreationService s
113120

114121
Creator.PropertyChanged += (_, e) =>
115122
{
116-
if (e.PropertyName == nameof(CreatorViewModel.IsCreating))
123+
if (e.PropertyName is nameof(CreatorViewModel.IsCreating) or nameof(CreatorViewModel.ProgressPercent))
117124
{
118125
UpdateIsBusy();
126+
UpdateTaskbarProgress();
119127
}
120128
};
121129

122130
SrsCreator.PropertyChanged += (_, e) =>
123131
{
124-
if (e.PropertyName == nameof(SrsCreatorViewModel.IsCreating))
132+
if (e.PropertyName is nameof(SrsCreatorViewModel.IsCreating) or nameof(SrsCreatorViewModel.ProgressPercent))
125133
{
126134
UpdateIsBusy();
135+
UpdateTaskbarProgress();
127136
}
128137
};
129138

130139
Reconstructor.PropertyChanged += (_, e) =>
131140
{
132-
if (e.PropertyName == nameof(ReconstructorViewModel.IsRunning))
141+
if (e.PropertyName is nameof(ReconstructorViewModel.IsRunning) or nameof(ReconstructorViewModel.ProgressPercent))
133142
{
134143
UpdateIsBusy();
144+
UpdateTaskbarProgress();
135145
}
136146
};
137147

138148
SrsReconstructor.PropertyChanged += (_, e) =>
139149
{
140-
if (e.PropertyName == nameof(SrsReconstructorViewModel.IsRebuilding))
150+
if (e.PropertyName is nameof(SrsReconstructorViewModel.IsRebuilding) or nameof(SrsReconstructorViewModel.ProgressPercent))
141151
{
142152
UpdateIsBusy();
153+
UpdateTaskbarProgress();
143154
}
144155
};
145156

146157
SampleRestorer.PropertyChanged += (_, e) =>
147158
{
148-
if (e.PropertyName == nameof(SampleRestorerViewModel.IsRestoring))
159+
if (e.PropertyName is nameof(SampleRestorerViewModel.IsRestoring) or nameof(SampleRestorerViewModel.ProgressPercent))
149160
{
150161
UpdateIsBusy();
162+
UpdateTaskbarProgress();
151163
}
152164
};
153165
}
@@ -194,6 +206,40 @@ private void UpdateIsBusy()
194206
|| SampleRestorer.IsRestoring;
195207
}
196208

209+
private void UpdateTaskbarProgress()
210+
{
211+
if (Creator.IsCreating)
212+
{
213+
TaskbarProgressState = TaskbarItemProgressState.Normal;
214+
TaskbarProgressValue = Creator.ProgressPercent / 100.0;
215+
}
216+
else if (SrsCreator.IsCreating)
217+
{
218+
TaskbarProgressState = TaskbarItemProgressState.Normal;
219+
TaskbarProgressValue = SrsCreator.ProgressPercent / 100.0;
220+
}
221+
else if (Reconstructor.IsRunning)
222+
{
223+
TaskbarProgressState = TaskbarItemProgressState.Normal;
224+
TaskbarProgressValue = Reconstructor.ProgressPercent / 100.0;
225+
}
226+
else if (SrsReconstructor.IsRebuilding)
227+
{
228+
TaskbarProgressState = TaskbarItemProgressState.Normal;
229+
TaskbarProgressValue = SrsReconstructor.ProgressPercent / 100.0;
230+
}
231+
else if (SampleRestorer.IsRestoring)
232+
{
233+
TaskbarProgressState = TaskbarItemProgressState.Normal;
234+
TaskbarProgressValue = SampleRestorer.ProgressPercent / 100.0;
235+
}
236+
else
237+
{
238+
TaskbarProgressState = TaskbarItemProgressState.None;
239+
TaskbarProgressValue = 0;
240+
}
241+
}
242+
197243
/// <summary>
198244
/// Disposes child ViewModels that hold unmanaged resources.
199245
/// </summary>

0 commit comments

Comments
 (0)