diff --git a/MainWindow.xaml b/MainWindow.xaml
index a89d789..eb6cbd2 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -7,21 +7,15 @@
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -35,15 +29,14 @@
-
-
-
-
-
+
+
+
+
@@ -53,10 +46,82 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 7236c7d..5d26e99 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -15,6 +15,11 @@ using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.IO;
using XML;
+using TapeCatalog.Methods;
+using System.Windows.Threading;
+using System.Diagnostics;
+using System.Threading;
+using System.Globalization;
@@ -26,18 +31,75 @@ namespace TapeCatalog
public partial class MainWindow : Window
{
private readonly ObservableCollection _files = new ObservableCollection();
+ private DispatcherTimer _copySummaryTimer;
- private static string FormatBytesToMb(long bytes)
+ // NOTE: FormatBytesToMb moved to Methods.SettingsUi; no local implementation kept.
+
+ private void XmlFilesListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
- // convert to MB with two decimal places
- double mb = bytes / 1024.0 / 1024.0;
- return mb.ToString("F2");
+ if (!(XmlFilesListBox.SelectedItem is string path))
+ {
+ XmlSummaryListView.Items.Clear();
+ return;
+ }
+
+ try
+ {
+ if (!File.Exists(path))
+ {
+ XmlSummaryListView.Items.Clear();
+ return;
+ }
+
+ var doc = System.Xml.Linq.XDocument.Load(path);
+ var root = doc.Root;
+ if (root == null)
+ {
+ XmlSummaryListView.Items.Clear();
+ return;
+ }
+
+ XmlSummaryListView.Items.Clear();
+
+ if (string.Equals(root.Name.LocalName, "Files", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var fe in root.Elements("File"))
+ {
+ var fn = fe.Element("Filename")?.Value ?? string.Empty;
+ var fd = fe.Element("FileDir")?.Value ?? string.Empty;
+ var st = fe.Element("Status")?.Value ?? string.Empty;
+ XmlSummaryListView.Items.Add(new FileEntry { Filename = fn, FileDir = fd, Status = st });
+ }
+ }
+ else if (string.Equals(root.Name.LocalName, "TapeEntries", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var entry in root.Elements("Entry"))
+ {
+ var fn = entry.Element("Filename")?.Value ?? string.Empty;
+ var med = entry.Element("Medium")?.Value ?? string.Empty;
+ var mod = entry.Element("Modified")?.Value ?? string.Empty;
+ XmlSummaryListView.Items.Add(new FileEntry { Filename = fn, FileDir = med, Status = mod });
+ }
+ }
+ else
+ {
+ // Unknown format - show nothing
+ }
+ }
+ catch
+ {
+ XmlSummaryListView.Items.Clear();
+ }
}
public MainWindow()
{
InitializeComponent();
FilesListView.ItemsSource = _files;
+ // start periodic update of copy summary
+ _copySummaryTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
+ _copySummaryTimer.Tick += (s, e) => UpdateCopySummary();
+ _copySummaryTimer.Start();
}
#region (export Medium)
private void EXPORT_MEDIUM(object sender, RoutedEventArgs e)
@@ -82,8 +144,22 @@ namespace TapeCatalog
#region (COPY_TO_TAPECATALOG)
private void COPY_TO_TAPECATALOG(object sender, RoutedEventArgs e)
{
- // delegate to XML helper to copy entries to TapeCatalog list view
- XML.Methods.CopyToTapeCatalog(this, _files, TapeCatalogListView);
+ // Only copy entries where the verification status indicates OK (checked)
+ try
+ {
+ var okEntries = _files.Where(f => string.Equals(f.Status, "✔", StringComparison.Ordinal)).ToList();
+ if (okEntries.Count == 0)
+ {
+ MessageBox.Show(this, "No verified entries to copy to TapeCatalog.", "Nothing to copy", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ XML.Methods.CopyToTapeCatalog(this, okEntries, TapeCatalogListView);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this, $"Error copying to TapeCatalog: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
}
#endregion (COPY_TO_TAPECATALOG)
#region (COPY_FROM_SRCARC)
@@ -124,165 +200,104 @@ namespace TapeCatalog
private void SET_RES_DIR(object sender, RoutedEventArgs e)
{
- // Use the WinForms FolderBrowserDialog to select a folder
- using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
- {
- dlg.Description = "Select a folder";
- dlg.ShowNewFolderButton = true;
- var result = dlg.ShowDialog();
- if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dlg.SelectedPath))
- {
- SourceTextBox.Text = dlg.SelectedPath;
- }
- }
+ var sel = SettingsUi.SelectDirectory("Select Source folder");
+ if (!string.IsNullOrWhiteSpace(sel)) SourceTextBox.Text = sel;
}
#endregion (EXPORT_LIST)
#region (SET_ARC_DIR)
private void SET_ARC_DIR(object sender, RoutedEventArgs e)
{
- // Use the WinForms FolderBrowserDialog to select a folder
- using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
+ var sel = SettingsUi.SelectDirectory("Select Archive folder");
+ if (!string.IsNullOrWhiteSpace(sel)) ArchiveTextBox.Text = sel;
+ }
+ #endregion (SET_ARC_DIR)
+ #region (SET_XML_DIR)
+ private void SET_XML_DIR(object sender, RoutedEventArgs e)
+ {
+ var sel = SettingsUi.SelectDirectory("Select XML folder");
+ if (!string.IsNullOrWhiteSpace(sel)) XmlDirectoryTextBox.Text = sel;
+ }
+ #endregion (SET_XML_DIR)
+
+ #region (LOAD_XML_FILES)
+ private void LOAD_XML_FILES(object sender, RoutedEventArgs e)
+ {
+ var xmlPath = XmlDirectoryTextBox.Text?.Trim();
+ if (string.IsNullOrWhiteSpace(xmlPath) || !Directory.Exists(xmlPath))
{
- dlg.Description = "Select a folder";
- dlg.ShowNewFolderButton = true;
- var result = dlg.ShowDialog();
- if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dlg.SelectedPath))
+ MessageBox.Show(this, "Please select a valid XML directory first.", "Invalid XML Directory", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ string[] files;
+ try
+ {
+ files = Directory.GetFiles(xmlPath, "*.xml", SearchOption.TopDirectoryOnly);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this, $"Error enumerating XML files: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ XmlFilesListBox.Items.Clear();
+ XmlSummaryListView.Items.Clear();
+ foreach (var f in files)
+ {
+ XmlFilesListBox.Items.Add(f);
+ }
+
+ // switch to XML tab
+ foreach (var it in MainTabControl.Items)
+ {
+ if (it is TabItem ti && (ti.Header as string) == "XML")
{
- ArchiveTextBox.Text = dlg.SelectedPath;
+ MainTabControl.SelectedItem = ti;
+ break;
}
}
}
- #endregion (SET_ARC_DIR)
+ #endregion (LOAD_XML_FILES)
#region (SCAN_RES_DIR)
private void SCAN_RES_DIR(object sender, RoutedEventArgs e)
{
var path = SourceTextBox.Text?.Trim();
- if (string.IsNullOrWhiteSpace(path))
+ if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
{
- MessageBox.Show("Please select a directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- if (!Directory.Exists(path))
- {
- MessageBox.Show("The specified directory does not exist.", "Directory not found", MessageBoxButton.OK, MessageBoxImage.Error);
+ MessageBox.Show("Please select a valid Source directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
- foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
- {
- try
- {
- var fi = new FileInfo(file);
- // check if an entry with same filename and same source directory already exists
- var existing = _files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.OrdinalIgnoreCase)
- && string.Equals(f.FileDir, fi.DirectoryName, StringComparison.OrdinalIgnoreCase));
- if (existing != null)
- {
- var newSizeMb = FormatBytesToMb(fi.Length);
- if (existing.SizeSrc != newSizeMb)
- {
- // update size and modification date, do not add a new entry
- existing.SizeSrc = newSizeMb;
- existing.ModSrc = fi.LastWriteTime.ToString();
- // reset verification status and archive drive info
- existing.Status = string.Empty;
- existing.ArcDrive = string.Empty;
- }
- }
- else
- {
- _files.Add(new FileEntry
- {
- Filename = fi.Name,
- FileDir = fi.DirectoryName,
- SizeSrc = FormatBytesToMb(fi.Length),
- //SizeArc = string.Empty,
- ModSrc = fi.LastWriteTime.ToString(),
- //ModArc = string.Empty
- });
- }
- }
- catch
- {
- // ignore files we can't access
- }
- }
+ var count = SettingsUi.ScanSource(path, _files);
+ MessageBox.Show($"Scan abgeschlossen. Einträge: {count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Error scanning directory: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
- // inform user that scan finished
- MessageBox.Show($"Scan abgeschlossen. Einträge: {_files.Count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
}
#endregion (SCAN_RES_DIR)
#region(SCAN_ARC_DIR)
private void SCAN_ARC_DIR(object sender, RoutedEventArgs e)
{
var path = ArchiveTextBox.Text?.Trim();
- if (string.IsNullOrWhiteSpace(path))
+ if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
{
- MessageBox.Show("Please select an archive directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
- return;
- }
-
- if (!Directory.Exists(path))
- {
- MessageBox.Show("The specified archive directory does not exist.", "Directory not found", MessageBoxButton.OK, MessageBoxImage.Error);
+ MessageBox.Show("Please select a valid Archive directory first.", "No directory", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
- foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
- {
- try
- {
- var fi = new FileInfo(file);
-
- // look for existing entry by filename (case-insensitive)
- var existing = _files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.Ordinal));
- if (existing != null)
- {
- // update SizeArc
- existing.SizeArc = FormatBytesToMb(fi.Length);
-
- // update ModArc to the most recent modified date
- DateTime prevMod;
- if (!DateTime.TryParse(existing.ModArc, out prevMod))
- prevMod = DateTime.MinValue;
-
- var mod = fi.LastWriteTime > prevMod ? fi.LastWriteTime : prevMod;
- existing.ModArc = mod.ToString();
- }
- else
- {
- _files.Add(new FileEntry
- {
- Filename = fi.Name,
- FileDir = fi.DirectoryName,
- //SizeSrc = string.Empty,
- SizeArc = FormatBytesToMb(fi.Length),
- //ModSrc = string.Empty,
- ModArc = fi.LastWriteTime.ToString()
- });
- }
- }
- catch
- {
- // ignore inaccessible files
- }
- }
+ var count = SettingsUi.ScanArchive(path, _files);
+ MessageBox.Show($"Scan abgeschlossen. Einträge: {count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Error scanning archive directory: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
- // inform user that archive scan finished
- MessageBox.Show($"Scan abgeschlossen. Einträge: {_files.Count}", "Scan beendet", MessageBoxButton.OK, MessageBoxImage.Information);
}
#endregion(SCAN_ARC_DIR)
#region (CLEAR_FILES)
@@ -341,6 +356,190 @@ namespace TapeCatalog
}
}
}
+
+ // Populate CopyTreeView with directory structure from SourceTextBox
+ private void POPULATE_COPY_TREE(object sender, RoutedEventArgs e)
+ {
+ Copy.PopulateTree(CopyTreeView, SourceTextBox.Text);
+ }
+
+ private async void COPY_SELECTED_TO_ARCHIVE(object sender, RoutedEventArgs e)
+ {
+ var archivePath = ArchiveTextBox.Text?.Trim();
+ if (string.IsNullOrWhiteSpace(archivePath) || !Directory.Exists(archivePath))
+ {
+ MessageBox.Show(this, "Please select a valid Archive directory first.", "Invalid Archive", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var sourceRoot = SourceTextBox.Text?.Trim();
+ if (string.IsNullOrWhiteSpace(sourceRoot) || !Directory.Exists(sourceRoot))
+ {
+ MessageBox.Show(this, "Please select a valid Source directory first.", "Invalid Source", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var files = Copy.GetCheckedFiles(CopyTreeView);
+ if (files.Count == 0)
+ {
+ MessageBox.Show(this, "No files selected.", "Nothing to copy", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ // prepare progress UI based on bytes
+ long totalBytes = 0;
+ foreach (var f in files)
+ {
+ try { totalBytes += new FileInfo(f).Length; } catch { }
+ }
+
+ if (totalBytes <= 0) totalBytes = 1; // avoid zero maximum
+
+ CopyProgressBar.Minimum = 0;
+ CopyProgressBar.Maximum = (double)totalBytes;
+ CopyProgressBar.Value = 0;
+ CopyProgressText.Text = "Starting...";
+
+ long bytesCopied = 0;
+ int copied = 0, failed = 0;
+
+ // parse speed limit (MB/s) from settings; 0 or invalid = unlimited
+ double speedMb = 0;
+ var speedText = CopySpeedTextBox?.Text?.Trim();
+ if (!string.IsNullOrWhiteSpace(speedText))
+ double.TryParse(speedText, NumberStyles.Float, CultureInfo.InvariantCulture, out speedMb);
+
+ double speedBytesPerSec = speedMb > 0 ? speedMb * 1024.0 * 1024.0 : 0;
+
+ await Task.Run(() =>
+ {
+ const int bufferSize = 64 * 1024; // 64KB
+ var buffer = new byte[bufferSize];
+
+ for (int i = 0; i < files.Count; i++)
+ {
+ var src = files[i];
+ try
+ {
+ var fileDir = System.IO.Path.GetDirectoryName(src) ?? string.Empty;
+ string relative;
+ if (!string.IsNullOrWhiteSpace(sourceRoot) && fileDir.StartsWith(sourceRoot, StringComparison.OrdinalIgnoreCase))
+ relative = fileDir.Substring(sourceRoot.Length).TrimStart(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ else
+ relative = System.IO.Path.GetFileName(fileDir);
+
+ var destDir = System.IO.Path.Combine(archivePath, relative);
+ Directory.CreateDirectory(destDir);
+ var destFile = System.IO.Path.Combine(destDir, System.IO.Path.GetFileName(src));
+
+ using (var inStream = new FileStream(src, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (var outStream = new FileStream(destFile, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ long fileBytesCopied = 0;
+ var sw = Stopwatch.StartNew();
+ int read;
+ while ((read = inStream.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ outStream.Write(buffer, 0, read);
+ fileBytesCopied += read;
+ Interlocked.Add(ref bytesCopied, read);
+
+ // throttling per file
+ if (speedBytesPerSec > 0)
+ {
+ double desiredMs = (fileBytesCopied / speedBytesPerSec) * 1000.0;
+ double actualMs = sw.Elapsed.TotalMilliseconds;
+ if (desiredMs > actualMs)
+ {
+ int sleep = (int)Math.Ceiling(desiredMs - actualMs);
+ if (sleep > 0) Thread.Sleep(sleep);
+ }
+ }
+
+ // update UI progress
+ Dispatcher.Invoke(() =>
+ {
+ CopyProgressBar.Value = Math.Min((double)bytesCopied, CopyProgressBar.Maximum);
+ double pct = (CopyProgressBar.Value / CopyProgressBar.Maximum) * 100.0;
+ CopyProgressText.Text = $"{FormatBytes(bytesCopied)} / {FormatBytes(totalBytes)} ({pct:F1}%)";
+ });
+ }
+ }
+
+ copied++;
+ }
+ catch
+ {
+ failed++;
+ }
+ }
+ });
+
+ CopyProgressText.Text = $"Done. Success: {copied}, Failed: {failed}. {FormatBytes(bytesCopied)} copied.";
+ MessageBox.Show(this, $"Copy finished. Success: {copied}, Failed: {failed}", "Copy completed", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ private void UpdateCopySummary()
+ {
+ try
+ {
+ var files = Copy.GetCheckedFiles(CopyTreeView);
+ CopySelectedCountText.Text = files.Count.ToString();
+
+ double totalMb = 0;
+ foreach (var f in files)
+ {
+ try
+ {
+ var fi = new FileInfo(f);
+ totalMb += fi.Length / 1024.0 / 1024.0;
+ }
+ catch
+ {
+ // ignore
+ }
+ }
+ CopySelectedSizeText.Text = totalMb.ToString("F2");
+
+ var archivePath = ArchiveTextBox.Text?.Trim();
+ if (!string.IsNullOrWhiteSpace(archivePath))
+ {
+ try
+ {
+ var root = System.IO.Path.GetPathRoot(archivePath);
+ if (!string.IsNullOrWhiteSpace(root))
+ {
+ var di = new DriveInfo(root);
+ // AvailableFreeSpace in bytes
+ double freeMb = di.AvailableFreeSpace / 1024.0 / 1024.0;
+ ArchiveFreeText.Text = freeMb.ToString("F2") + " MB";
+ }
+ else
+ {
+ ArchiveFreeText.Text = "N/A";
+ }
+ }
+ catch
+ {
+ ArchiveFreeText.Text = "N/A";
+ }
+ }
+ else
+ {
+ ArchiveFreeText.Text = "N/A";
+ }
+ }
+ catch (Exception)
+ {
+ // swallow periodic update errors; avoid UI disruptions
+ }
+ }
+
+ private static string FormatBytes(long bytes)
+ {
+ double mb = bytes / 1024.0 / 1024.0;
+ return mb.ToString("F2") + " MB";
+ }
}
#endregion (VERIFY_SIZES)
public class FileEntry : System.ComponentModel.INotifyPropertyChanged
diff --git a/Methods/Copy.cs b/Methods/Copy.cs
new file mode 100644
index 0000000..9b1f2b5
--- /dev/null
+++ b/Methods/Copy.cs
@@ -0,0 +1,185 @@
+using System;
+using System.IO;
+using System.Windows.Controls;
+using System.Windows;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TapeCatalog.Methods
+{
+ // Reusable helper methods for copy-related UI functionality
+ public static class Copy
+ {
+ // guard to prevent recursive Checked/Unchecked handling while we programmatically change states
+ private static bool _suppressCheckEvents = false;
+ public static void PopulateTree(TreeView tree, string path)
+ {
+ if (tree == null) return;
+ tree.Items.Clear();
+ if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path)) return;
+
+ var rootDir = new DirectoryInfo(path);
+ var rootItem = CreateDirectoryNode(rootDir);
+ rootItem.IsExpanded = true;
+ tree.Items.Add(rootItem);
+ }
+
+ public static TreeViewItem CreateDirectoryNode(DirectoryInfo dir)
+ {
+ var item = new TreeViewItem { Tag = dir.FullName };
+
+ // Create header with a checkbox for the directory as well
+ var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
+ var dirCb = new CheckBox { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0) };
+ var dirTb = new TextBlock { Text = dir.Name, VerticalAlignment = VerticalAlignment.Center };
+ headerPanel.Children.Add(dirCb);
+ headerPanel.Children.Add(dirTb);
+ item.Header = headerPanel;
+
+ try
+ {
+ // add subdirectories
+ foreach (var sub in dir.GetDirectories())
+ {
+ item.Items.Add(CreateDirectoryNode(sub));
+ }
+
+ // add files
+ foreach (var f in dir.GetFiles())
+ {
+ var fileItem = new TreeViewItem { Tag = f.FullName };
+ var sp = new StackPanel { Orientation = Orientation.Horizontal };
+ var cb = new CheckBox { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0) };
+ var tb = new TextBlock { Text = f.Name, VerticalAlignment = VerticalAlignment.Center };
+ sp.Children.Add(cb);
+ sp.Children.Add(tb);
+ fileItem.Header = sp;
+ item.Items.Add(fileItem);
+ }
+ }
+ catch
+ {
+ // ignore access exceptions
+ }
+
+ // Wire events to propagate state to children when directory checkbox is toggled
+ dirCb.Checked += (s, e) =>
+ {
+ if (_suppressCheckEvents) return;
+ try { _suppressCheckEvents = true; SetCheckedState(item, true); }
+ finally { _suppressCheckEvents = false; }
+ };
+ dirCb.Unchecked += (s, e) =>
+ {
+ if (_suppressCheckEvents) return;
+ try { _suppressCheckEvents = true; SetCheckedState(item, false); }
+ finally { _suppressCheckEvents = false; }
+ };
+
+ return item;
+ }
+
+ private static void SetCheckedState(TreeViewItem node, bool isChecked)
+ {
+ foreach (var it in node.Items)
+ {
+ if (it is TreeViewItem child)
+ {
+ // child header expected to be a StackPanel with a CheckBox as first child
+ if (child.Header is StackPanel sp && sp.Children.Count > 0)
+ {
+ foreach (var ch in sp.Children)
+ {
+ if (ch is CheckBox childCb)
+ {
+ childCb.IsChecked = isChecked;
+ break;
+ }
+ }
+ }
+
+ // recurse into grandchildren
+ if (child.Items != null && child.Items.Count > 0)
+ {
+ SetCheckedState(child, isChecked);
+ }
+ }
+ }
+ }
+
+ // Collect checked files and copy them to archive preserving relative folders
+ public static Tuple CopySelectedToArchive(TreeView tree, string sourceRoot, string archivePath)
+ {
+ if (tree == null) return Tuple.Create(0, 0);
+ var files = new List();
+ foreach (var it in tree.Items)
+ {
+ if (it is TreeViewItem tvi)
+ CollectCheckedFiles(tvi, files);
+ }
+
+ int copied = 0, failed = 0;
+ foreach (var src in files)
+ {
+ try
+ {
+ var fileDir = System.IO.Path.GetDirectoryName(src) ?? string.Empty;
+ string relative;
+ if (!string.IsNullOrWhiteSpace(sourceRoot) && fileDir.StartsWith(sourceRoot, StringComparison.OrdinalIgnoreCase))
+ relative = fileDir.Substring(sourceRoot.Length).TrimStart(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ else
+ relative = System.IO.Path.GetFileName(fileDir);
+
+ var destDir = System.IO.Path.Combine(archivePath, relative);
+ Directory.CreateDirectory(destDir);
+ var destFile = System.IO.Path.Combine(destDir, System.IO.Path.GetFileName(src));
+ File.Copy(src, destFile, true);
+ copied++;
+ }
+ catch
+ {
+ failed++;
+ }
+ }
+
+ return Tuple.Create(copied, failed);
+ }
+
+ // Return a list of all checked file paths in the tree
+ public static List GetCheckedFiles(TreeView tree)
+ {
+ var files = new List();
+ if (tree == null) return files;
+ foreach (var it in tree.Items)
+ {
+ if (it is TreeViewItem tvi)
+ CollectCheckedFiles(tvi, files);
+ }
+ return files;
+ }
+
+ private static void CollectCheckedFiles(TreeViewItem node, List list)
+ {
+ if (node.Header is StackPanel sp)
+ {
+ foreach (var child in sp.Children)
+ {
+ if (child is CheckBox cb && cb.IsChecked == true)
+ {
+ if (node.Tag is string path && File.Exists(path))
+ {
+ list.Add(path);
+ }
+ break;
+ }
+ }
+ }
+
+ foreach (var it in node.Items)
+ {
+ if (it is TreeViewItem t)
+ CollectCheckedFiles(t, list);
+ }
+ }
+ }
+}
diff --git a/Methods/Settings.cs b/Methods/Settings.cs
new file mode 100644
index 0000000..baba20e
--- /dev/null
+++ b/Methods/Settings.cs
@@ -0,0 +1,206 @@
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using System.Linq;
+using System.Collections.ObjectModel;
+
+namespace TapeCatalog.Methods
+{
+ ///
+ /// Simple settings container and load/save helpers.
+ ///
+ [Serializable]
+ public class SettingsData
+ {
+ public string SourceDirectory { get; set; }
+ public string ArchiveDirectory { get; set; }
+ public string XmlDirectory { get; set; }
+
+ public static SettingsData Default => new SettingsData
+ {
+ SourceDirectory = string.Empty,
+ ArchiveDirectory = string.Empty,
+ XmlDirectory = string.Empty
+ };
+ }
+
+ public static class SettingsManager
+ {
+ private const string DefaultFileName = "appsettings.xml";
+
+ public static void Save(SettingsData data, string filePath = null)
+ {
+ if (data == null) throw new ArgumentNullException(nameof(data));
+ var path = string.IsNullOrWhiteSpace(filePath) ? DefaultFileName : filePath;
+ try
+ {
+ using (var fs = File.Create(path))
+ {
+ var ser = new XmlSerializer(typeof(SettingsData));
+ ser.Serialize(fs, data);
+ }
+ }
+ catch
+ {
+ // swallow errors; callers can handle existence/permission issues
+ }
+ }
+
+ public static SettingsData Load(string filePath = null)
+ {
+ var path = string.IsNullOrWhiteSpace(filePath) ? DefaultFileName : filePath;
+ if (!File.Exists(path)) return SettingsData.Default;
+ try
+ {
+ using (var fs = File.OpenRead(path))
+ {
+ var ser = new XmlSerializer(typeof(SettingsData));
+ return ser.Deserialize(fs) as SettingsData ?? SettingsData.Default;
+ }
+ }
+ catch
+ {
+ return SettingsData.Default;
+ }
+ }
+ }
+
+ // UI-related helpers used by the Settings tab. These methods avoid direct UI manipulation
+ // and return data so the MainWindow can update controls.
+ public static class SettingsUi
+ {
+ public static string SelectDirectory(string description)
+ {
+ using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
+ {
+ dlg.Description = description ?? "Select a folder";
+ dlg.ShowNewFolderButton = true;
+ var result = dlg.ShowDialog();
+ if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrWhiteSpace(dlg.SelectedPath))
+ return dlg.SelectedPath;
+ }
+ return null;
+ }
+
+ public static int ScanSource(string path, ObservableCollection files)
+ {
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("path");
+ if (!Directory.Exists(path)) throw new DirectoryNotFoundException(path);
+
+ // normalize source root for reliable relative path calculation
+ var rootFull = System.IO.Path.GetFullPath(path).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+
+ foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
+ {
+ try
+ {
+ var fi = new FileInfo(file);
+ // compute directory relative to source root (remove the source root prefix)
+ var dirFull = System.IO.Path.GetFullPath(fi.DirectoryName).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ string relativeDir = string.Empty;
+ if (dirFull.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
+ relativeDir = dirFull.Length == rootFull.Length ? string.Empty : dirFull.Substring(rootFull.Length).TrimStart(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ else
+ relativeDir = fi.DirectoryName;
+
+ var existing = files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(f.FileDir, relativeDir, StringComparison.OrdinalIgnoreCase));
+ if (existing != null)
+ {
+ var newSizeMb = FormatBytesToMb(fi.Length);
+ if (existing.SizeSrc != newSizeMb)
+ {
+ existing.SizeSrc = newSizeMb;
+ existing.ModSrc = fi.LastWriteTime.ToString();
+ existing.Status = string.Empty;
+ existing.ArcDrive = string.Empty;
+ }
+ }
+ else
+ {
+ files.Add(new TapeCatalog.FileEntry
+ {
+ Filename = fi.Name,
+ FileDir = relativeDir,
+ SizeSrc = FormatBytesToMb(fi.Length),
+ ModSrc = fi.LastWriteTime.ToString(),
+ });
+ }
+ }
+ catch
+ {
+ // ignore inaccessible files
+ }
+ }
+ return files.Count;
+ }
+
+ public static int ScanArchive(string path, ObservableCollection files)
+ {
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("path");
+ if (!Directory.Exists(path)) throw new DirectoryNotFoundException(path);
+ // normalize archive root for reliable relative path calculation
+ var rootFull = System.IO.Path.GetFullPath(path).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+
+ foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
+ {
+ try
+ {
+ var fi = new FileInfo(file);
+
+ var dirFull = System.IO.Path.GetFullPath(fi.DirectoryName).TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ string relativeDir = string.Empty;
+ if (dirFull.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
+ relativeDir = dirFull.Length == rootFull.Length ? string.Empty : dirFull.Substring(rootFull.Length).TrimStart(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar);
+ else
+ relativeDir = fi.DirectoryName;
+
+ var existing = files.FirstOrDefault(f => string.Equals(f.Filename, fi.Name, StringComparison.Ordinal)
+ && string.Equals(f.FileDir, relativeDir, StringComparison.OrdinalIgnoreCase));
+ if (existing != null)
+ {
+ existing.SizeArc = FormatBytesToMb(fi.Length);
+ DateTime prevMod;
+ if (!DateTime.TryParse(existing.ModArc, out prevMod)) prevMod = DateTime.MinValue;
+ var mod = fi.LastWriteTime > prevMod ? fi.LastWriteTime : prevMod;
+ existing.ModArc = mod.ToString();
+ }
+ else
+ {
+ files.Add(new TapeCatalog.FileEntry
+ {
+ Filename = fi.Name,
+ FileDir = relativeDir,
+ SizeArc = FormatBytesToMb(fi.Length),
+ ModArc = fi.LastWriteTime.ToString()
+ });
+ }
+ }
+ catch
+ {
+ // ignore inaccessible files
+ }
+ }
+ return files.Count;
+ }
+
+ public static string[] GetXmlFiles(string xmlPath)
+ {
+ if (string.IsNullOrWhiteSpace(xmlPath) || !Directory.Exists(xmlPath)) return new string[0];
+ try
+ {
+ return Directory.GetFiles(xmlPath, "*.xml", SearchOption.TopDirectoryOnly);
+ }
+ catch
+ {
+ return new string[0];
+ }
+ }
+
+ private static string FormatBytesToMb(long bytes)
+ {
+ double mb = bytes / 1024.0 / 1024.0;
+ return mb.ToString("F2");
+ }
+ }
+}
diff --git a/Methods/xml.cs b/Methods/xml.cs
new file mode 100644
index 0000000..adc82cb
--- /dev/null
+++ b/Methods/xml.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Linq;
+
+namespace TapeCatalog.Methods
+{
+ ///
+ /// Helpers to read XML files and produce a compact summary used by the UI.
+ ///
+ public static class Xml
+ {
+ public static List ParseSummary(string filePath)
+ {
+ var result = new List();
+ if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
+ return result;
+
+ try
+ {
+ var doc = XDocument.Load(filePath);
+ var root = doc.Root;
+ if (root == null) return result;
+
+ if (string.Equals(root.Name.LocalName, "Files", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var fe in root.Elements("File"))
+ {
+ try
+ {
+ var fn = fe.Element("Filename")?.Value ?? string.Empty;
+ var fd = fe.Element("FileDir")?.Value ?? string.Empty;
+ var st = fe.Element("Status")?.Value ?? string.Empty;
+ result.Add(new TapeCatalog.FileEntry { Filename = fn, FileDir = fd, Status = st });
+ }
+ catch
+ {
+ // ignore malformed entries
+ }
+ }
+ }
+ else if (string.Equals(root.Name.LocalName, "TapeEntries", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var entry in root.Elements("Entry"))
+ {
+ try
+ {
+ var fn = entry.Element("Filename")?.Value ?? string.Empty;
+ var med = entry.Element("Medium")?.Value ?? string.Empty;
+ var mod = entry.Element("Modified")?.Value ?? string.Empty;
+ result.Add(new TapeCatalog.FileEntry { Filename = fn, FileDir = med, Status = mod });
+ }
+ catch
+ {
+ // ignore malformed entries
+ }
+ }
+ }
+ }
+ catch
+ {
+ // ignore parse errors and return empty list
+ }
+
+ return result;
+ }
+
+ public static string[] GetXmlFiles(string xmlDirectory)
+ {
+ if (string.IsNullOrWhiteSpace(xmlDirectory) || !Directory.Exists(xmlDirectory))
+ return Array.Empty();
+
+ try
+ {
+ return Directory.GetFiles(xmlDirectory, "*.xml", SearchOption.TopDirectoryOnly);
+ }
+ catch
+ {
+ return Array.Empty();
+ }
+ }
+ }
+}
diff --git a/TapeCatalog.csproj b/TapeCatalog.csproj
index bda75f1..55e66dc 100644
--- a/TapeCatalog.csproj
+++ b/TapeCatalog.csproj
@@ -56,6 +56,9 @@
MSBuild:Compile
Designer
+
+
+
MSBuild:Compile