-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMainWindow.xaml.cs
More file actions
396 lines (319 loc) · 16.7 KB
/
Copy pathMainWindow.xaml.cs
File metadata and controls
396 lines (319 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using File_Renamer.Models;
using File_Renamer.Services;
using File_Renamer.Utilities;
namespace File_Renamer
{
public partial class MainWindow : Window
{
#region Fields
private readonly ObservableCollection<LogEntry> _logs = new ObservableCollection<LogEntry>();
private AppSettings _appSettings;
private CancellationTokenSource _cancellationTokenSource;
private bool _isUsingRegex;
private bool _hasUserMovedWindow;
// Frozen brushes for high-performance rendering (WPF Best Practice)
private readonly SolidColorBrush _readOnlyBrush;
private readonly SolidColorBrush _previewBlueBrush;
#endregion
#region Constructor & Initialization
public MainWindow()
{
InitializeComponent();
// Initialize optimized frozen brushes
_readOnlyBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F5F5F5"));
_readOnlyBrush.Freeze();
_previewBlueBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#0056B3"));
_previewBlueBrush.Freeze();
InitializeCoreComponents();
}
// Bootstraps initial state, bindings, and configurations
private void InitializeCoreComponents()
{
_appSettings = AppSettings.Load();
bool isDarkMode = _appSettings.Theme == "Dark";
if (tglTheme != null) tglTheme.IsChecked = isDarkMode;
ApplyTheme(isDarkMode);
DgLog.ItemsSource = _logs;
TxtSourceFolder.TextChanged += (s, e) => ValidateRestoreAvailability();
TxtOutputFolder.TextChanged += (s, e) => ValidateRestoreAvailability();
}
private string GetActiveTargetFolder() => RbCopy?.IsChecked == true ? TxtOutputFolder.Text : TxtSourceFolder.Text;
#endregion
#region Theme
// Dynamically injects frozen brushes into application resources based on theme
private void ApplyTheme(bool isDark)
{
var res = this.Resources;
BrushConverter converter = new BrushConverter();
void SetResource(string key, string hexColor)
{
var brush = (SolidColorBrush)converter.ConvertFromString(hexColor);
brush.Freeze(); // Prevents memory leaks and improves WPF render thread speed
res[key] = brush;
}
SetResource("AppBgBrush", isDark ? "#202020" : "#F0F2F5");
SetResource("CardBgBrush", isDark ? "#2D2D30" : "#FFFFFF");
SetResource("TextPrimaryBrush", isDark ? "#FFFFFF" : "#212529");
SetResource("TextSecondaryBrush", isDark ? "#CCCCCC" : "#495057");
SetResource("BorderDefaultBrush", isDark ? "#434346" : "#DEE2E6");
SetResource("ControlBgBrush", isDark ? "#3E3E42" : "#E9ECEF");
}
private void SetTheme(bool isDark)
{
ApplyTheme(isDark);
_appSettings.Theme = isDark ? "Dark" : "Light";
_appSettings.Save();
}
private void tglTheme_Checked(object sender, RoutedEventArgs e) => SetTheme(true);
private void tglTheme_Unchecked(object sender, RoutedEventArgs e) => SetTheme(false);
#endregion
#region Window Chrome
private void BtnAbout_Click(object sender, RoutedEventArgs e) => new AboutWindow { Owner = this }.ShowDialog();
private void BtnCancel_Click(object sender, RoutedEventArgs e) => _cancellationTokenSource?.Cancel();
private void BtnClose_Click(object sender, RoutedEventArgs e) => Close();
private void Window_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.LeftButton != System.Windows.Input.MouseButtonState.Pressed) return;
// DragMove() blocks until mouse-up; only a real position change counts as "user moved the window"
// (a plain click to focus a TextBox/ComboBox etc. must not permanently disable auto re-centering).
double startLeft = this.Left, startTop = this.Top;
DragMove();
if (this.Left != startLeft || this.Top != startTop) _hasUserMovedWindow = true;
}
#endregion
#region Folder Browsing
private void BtnBrowseSource_Click(object sender, RoutedEventArgs e)
{
string path = DialogUtility.SelectFolder("Select Source Folder");
if (path != null) { TxtSourceFolder.Text = path; ValidateRestoreAvailability(); }
}
private void BtnBrowseOutput_Click(object sender, RoutedEventArgs e)
{
string path = DialogUtility.SelectFolder("Select Output Folder");
if (path != null) { TxtOutputFolder.Text = path; ValidateRestoreAvailability(); }
}
#endregion
#region Mode & Panel Toggling
private void BtnToggleAdvanced_Click(object sender, RoutedEventArgs e)
{
if (AdvancedPanel == null) return;
bool isCollapsed = AdvancedPanel.Visibility == Visibility.Collapsed;
AdvancedPanel.Visibility = isCollapsed ? Visibility.Visible : Visibility.Collapsed;
BtnToggleAdvanced.Content = isCollapsed ? "Advanced Setting ▲" : "Advanced ▼";
if (isCollapsed && CmbPresets.SelectedIndex == -1) CmbPresets.SelectedIndex = 0;
else if (isCollapsed) EvaluatePresetSelection();
UpdateLayout();
if (!_hasUserMovedWindow)
{
this.Left = (SystemParameters.WorkArea.Width - this.ActualWidth) / 2;
this.Top = (SystemParameters.WorkArea.Height - this.ActualHeight) / 2;
}
}
private void ModeSelection_Changed(object sender, RoutedEventArgs e)
{
if (OutputFolderSection == null || RbCopy == null || TxtOutputFolder == null) return;
bool isCopyMode = RbCopy.IsChecked == true;
OutputFolderSection.IsEnabled = isCopyMode;
TxtOutputFolder.Background = isCopyMode ? Brushes.Transparent : _readOnlyBrush;
}
#endregion
#region Live Preview & Presets
private void TxtFind_TextChanged(object sender, TextChangedEventArgs e) => UpdateLivePreview();
private void TxtReplace_TextChanged(object sender, TextChangedEventArgs e) => UpdateLivePreview();
// Updates real-time string replacement feedback visually
private void UpdateLivePreview()
{
if (!IsInitialized || CmbPresets == null || TxtExOutput == null || TxtExInput == null) return;
if (_isUsingRegex && !string.IsNullOrEmpty(TxtFind.Text))
{
try
{
TxtExOutput.Text = Regex.Replace(TxtExInput.Text, TxtFind.Text, TxtReplace.Text);
TxtExOutput.Foreground = _previewBlueBrush;
}
catch (ArgumentException)
{
TxtExOutput.Text = "Invalid Regex...";
TxtExOutput.Foreground = Brushes.Red;
}
}
else
{
TxtExOutput.Text = TxtExInput.Text;
TxtExOutput.Foreground = _previewBlueBrush;
}
}
private void CmbPresets_SelectionChanged(object sender, SelectionChangedEventArgs e) => EvaluatePresetSelection();
// Locked preset definitions: index 0 ("Standard Replace") and the last index ("Custom Regex") are free-text.
// Everything in between is a fixed Find/Replace pair the user can't edit. Keeping Find+Replace paired (rather
// than always assuming an empty replace) lets presets do real substitutions, not just deletions.
// NOTE: must stay in the same order as the ComboBoxItems in MainWindow.xaml.
private static readonly string[] PresetFindPatterns = {
"", // 0 Standard Replace (free text)
@"[\[\]\(\)\{\}]", // 1 Remove Brackets
@"\d+", // 2 Remove Numbers
@"[^a-zA-Z0-9]", // 3 Remove Symbols
@"\s{2,}", // 4 Remove Extra Spaces
@"^\s+|\s+$", // 5 Trim Leading/Trailing Spaces
@"\s+", // 6 Spaces to Underscores
@"\s+", // 7 Spaces to Dashes
@"^\d+[\s\-_.]*", // 8 Remove Leading Track Numbers
"" // 9 Custom Regex (free text)
};
private static readonly string[] PresetReplacePatterns = { "", "", "", "", " ", "", "_", "-", "", "" };
// Enforces input restrictions based on selected Regex/Text preset
private void EvaluatePresetSelection()
{
if (TxtExInput == null || TxtExOutput == null || TxtFind == null || TxtReplace == null || CheatSheetPanel == null) return;
const string SampleText = "tester (@123#)★";
TxtExInput.Text = SampleText;
CheatSheetPanel.Visibility = Visibility.Visible;
// Only a *locked* preset force-fills TxtFind; remember that before resetting IsReadOnly below so we
// know whether clearing it afterward is actually warranted (fixes: reopening the Advanced panel, or
// any other re-entry into this method, used to wipe whatever the user had freely typed).
bool wasLocked = TxtFind.IsReadOnly;
TxtFind.IsReadOnly = false;
TxtFind.Background = Brushes.Transparent;
TxtReplace.IsReadOnly = false;
TxtReplace.Background = Brushes.Transparent;
int idx = CmbPresets.SelectedIndex;
int lastIdx = PresetFindPatterns.Length - 1;
if (idx > 0 && idx < lastIdx)
{
TxtFind.Text = PresetFindPatterns[idx];
TxtFind.IsReadOnly = true;
TxtFind.Background = _readOnlyBrush;
TxtReplace.Text = PresetReplacePatterns[idx];
TxtReplace.IsReadOnly = true;
TxtReplace.Background = _readOnlyBrush;
TxtExOutput.Text = Regex.Replace(SampleText, TxtFind.Text, TxtReplace.Text);
_isUsingRegex = true;
}
else
{
// Clear only a leftover forced pattern from a locked preset - never wipe genuinely free-typed text.
if (wasLocked) TxtFind.Text = "";
TxtExOutput.Text = SampleText;
_isUsingRegex = idx == lastIdx;
}
}
#endregion
#region Logging & Restore Validation
// Throttle UI rendering to prevent hanging during large batch operations
private void AddLogAndScroll(LogEntry log)
{
_logs.Add(log);
if (log.Number % 10 == 0 || log.Number == 0)
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
if (DgLog.Items.Count > 0)
{
DgLog.ScrollIntoView(DgLog.Items[DgLog.Items.Count - 1]);
}
}, System.Windows.Threading.DispatcherPriority.Background);
}
}
private void ValidateRestoreAvailability()
{
if (!IsInitialized || BtnRestore == null) return;
string targetFolder = GetActiveTargetFolder();
BtnRestore.IsEnabled = !string.IsNullOrWhiteSpace(targetFolder) && File.Exists(Path.Combine(targetFolder, FileOperationService.RestoreLogFileName));
}
#endregion
#region Execution Engine
// Generic background task coordinator. Handles UI locking, progress tracking, and exceptions safely.
private async Task ExecuteEngineAsync(Func<IProgress<ProgressData>, CancellationToken, Task<int>> taskAction, string emptyDataMsg)
{
ToggleUI(false);
_logs.Clear();
_cancellationTokenSource = new CancellationTokenSource();
// Progress<T> automatically marshals to the UI Thread.
var progress = new Progress<ProgressData>(data => AddLogAndScroll(data.Log));
try
{
int processedCount = await taskAction(progress, _cancellationTokenSource.Token);
if (processedCount == 0 && !string.IsNullOrEmpty(emptyDataMsg))
{
MessageBox.Show(emptyDataMsg, "Information", MessageBoxButton.OK, MessageBoxImage.Information);
}
else if (processedCount > 0 || string.IsNullOrEmpty(emptyDataMsg))
{
AddLogAndScroll(new LogEntry { Action = "INFO", OriginalName = "---", NewName = "---", Status = "COMPLETE" });
if (ChkSoundNotif.IsChecked == true) System.Media.SystemSounds.Asterisk.Play();
}
}
catch (OperationCanceledException) { MessageBox.Show("Process cancelled.", "Cancelled", MessageBoxButton.OK, MessageBoxImage.Warning); }
catch (Exception ex) { MessageBox.Show($"Execution Error: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); }
finally
{
ToggleUI(true);
ValidateRestoreAvailability();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
}
// Locks or unlocks UI inputs during execution states
private void ToggleUI(bool isEnabled)
{
BtnBrowseSource.IsEnabled = BtnBrowseOutput.IsEnabled = isEnabled;
RbInPlace.IsEnabled = RbCopy.IsEnabled = isEnabled;
BtnRename.IsEnabled = BtnToggleAdvanced.IsEnabled = CmbPresets.IsEnabled = isEnabled;
TxtExtension.IsEnabled = ChkCreateRestoreLog.IsEnabled = isEnabled;
BtnRestore.IsEnabled = isEnabled; // prevents restore from racing an in-flight rename (re-validated below once idle)
BtnCancel.IsEnabled = !isEnabled;
bool isLockedPreset = CmbPresets.SelectedIndex > 0 && CmbPresets.SelectedIndex < PresetFindPatterns.Length - 1;
TxtFind.IsReadOnly = TxtReplace.IsReadOnly = !isEnabled || isLockedPreset;
}
#endregion
#region Rename & Restore Actions
private async void BtnRename_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(TxtSourceFolder.Text) || !Directory.Exists(TxtSourceFolder.Text))
{
MessageBox.Show("Please select a valid Source Folder.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning); return;
}
if (string.IsNullOrEmpty(TxtFind.Text))
{
MessageBox.Show("The search text cannot be empty!", "Input Validation", MessageBoxButton.OK, MessageBoxImage.Warning); return;
}
if (_isUsingRegex)
{
try { Regex.Match("", TxtFind.Text); }
catch { MessageBox.Show("Invalid Regular Expression syntax.", "Regex Error", MessageBoxButton.OK, MessageBoxImage.Error); return; }
}
string rawExt = TxtExtension.Text.Trim();
string safeFilter = string.IsNullOrWhiteSpace(rawExt) ? "*.*" : (rawExt.Contains("*") ? rawExt : $@"*{(!rawExt.StartsWith(".") ? "." : "")}{rawExt}");
var config = new RenameConfig
{
SourceFolder = TxtSourceFolder.Text,
OutputFolder = GetActiveTargetFolder(),
IsCopyMode = RbCopy.IsChecked == true,
FindText = TxtFind.Text,
ReplaceText = TxtReplace.Text,
Filter = safeFilter,
CreateLog = ChkCreateRestoreLog.IsChecked == true,
UseRegex = _isUsingRegex
};
await ExecuteEngineAsync((p, t) => Task.Run(() => FileOperationService.ProcessRename(config, p, t)), "The search text was not found in any file.");
}
private async void BtnRestore_Click(object sender, RoutedEventArgs e)
{
string restoreLogPath = Path.Combine(GetActiveTargetFolder(), FileOperationService.RestoreLogFileName);
if (MessageBox.Show("Revert changes based on the generated log?", "Confirm Restore", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
await ExecuteEngineAsync((p, t) => Task.Run(() => FileOperationService.ProcessRestore(restoreLogPath, p, t)), "No restore log found, or nothing to restore.");
}
}
#endregion
}
}