Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/App/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,20 @@
<MenuItem Header="Transplant Item..." Command="{Binding TransplantItemCommand}">
<MenuItem.Icon><i:Icon Value="fa-solid fa-arrow-right-arrow-left" FontSize="14" Foreground="#cba6f7"/></MenuItem.Icon>
</MenuItem>
<MenuItem Header="Merge Session Into Current…" x:Name="MergeSessionMenuItem">
<MenuItem.Icon><i:Icon Value="fa-solid fa-code-merge" FontSize="14" Foreground="#89b4fa"/></MenuItem.Icon>
<MenuItem Header="Import Items from other session" x:Name="ImportItemsMenuItem">
<MenuItem.Icon><i:Icon Value="fa-solid fa-cube" FontSize="14" Foreground="#89b4fa"/></MenuItem.Icon>
<MenuItem Header="Loading…" IsEnabled="False"/>
</MenuItem>
<MenuItem Header="Import Outfits from other session" x:Name="ImportOutfitsMenuItem">
<MenuItem.Icon><i:Icon Value="fa-solid fa-shirt" FontSize="14" Foreground="#a6e3a1"/></MenuItem.Icon>
<MenuItem Header="Loading…" IsEnabled="False"/>
</MenuItem>
<MenuItem Header="Import Effects from other session" x:Name="ImportEffectsMenuItem">
<MenuItem.Icon><i:Icon Value="fa-solid fa-wand-magic-sparkles" FontSize="14" Foreground="#f9e2af"/></MenuItem.Icon>
<MenuItem Header="Loading…" IsEnabled="False"/>
</MenuItem>
<MenuItem Header="Import Missiles from other session" x:Name="ImportMissilesMenuItem">
<MenuItem.Icon><i:Icon Value="fa-solid fa-location-arrow" FontSize="14" Foreground="#f38ba8"/></MenuItem.Icon>
<MenuItem Header="Loading…" IsEnabled="False"/>
</MenuItem>
</MenuItem>
Expand Down
82 changes: 45 additions & 37 deletions src/App/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,43 +60,11 @@ public MainWindow()

await vm.TryLoadLastSessionAsync();

// Wire Merge Session menu (dynamic submenu listing other sessions)
var mergeMenuItem = this.FindControl<Avalonia.Controls.MenuItem>("MergeSessionMenuItem");
if (mergeMenuItem != null)
{
mergeMenuItem.SubmenuOpened += (_, _) =>
{
mergeMenuItem.Items.Clear();
var sources = vm.Sessions
.Where(s => s != vm.ActiveSession && s.DatData != null && s.SprFile != null)
.ToList();

if (sources.Count == 0 || vm.ActiveSession?.DatData == null)
{
var empty = new Avalonia.Controls.MenuItem
{
Header = vm.ActiveSession?.DatData == null
? "Current session has no DAT loaded"
: "No other sessions with DAT/SPR loaded",
IsEnabled = false,
};
mergeMenuItem.Items.Add(empty);
}
else
{
foreach (var source in sources)
{
var mi = new Avalonia.Controls.MenuItem
{
Header = $"{source.Name} ({source.DatData!.Items.Count + source.DatData.Outfits.Count + source.DatData.Effects.Count + source.DatData.Missiles.Count} things)",
Tag = source,
};
mi.Click += async (_, _) => await vm.MergeSessionAsync(source);
mergeMenuItem.Items.Add(mi);
}
}
};
}
// Wire per-category Import menus (dynamic submenus listing other sessions)
WireImportMenu("ImportItemsMenuItem", ThingCategory.Item, vm);
WireImportMenu("ImportOutfitsMenuItem", ThingCategory.Outfit, vm);
WireImportMenu("ImportEffectsMenuItem", ThingCategory.Effect, vm);
WireImportMenu("ImportMissilesMenuItem", ThingCategory.Missile, vm);

// Wire confirmation dialog for palette delete operations
if (vm.Palette != null)
Expand Down Expand Up @@ -133,6 +101,46 @@ public MainWindow()
};
}

private void WireImportMenu(string controlName, ThingCategory category, MainWindowViewModel vm)
{
var menuItem = this.FindControl<Avalonia.Controls.MenuItem>(controlName);
if (menuItem == null) return;

menuItem.SubmenuOpened += (_, _) =>
{
menuItem.Items.Clear();
var sources = vm.Sessions
.Where(s => s != vm.ActiveSession && s.DatData != null && s.SprFile != null)
.ToList();

if (sources.Count == 0 || vm.ActiveSession?.DatData == null)
{
var empty = new Avalonia.Controls.MenuItem
{
Header = vm.ActiveSession?.DatData == null
? "Current session has no DAT loaded"
: "No other sessions with DAT/SPR loaded",
IsEnabled = false,
};
menuItem.Items.Add(empty);
}
else
{
foreach (var source in sources)
{
var dict = MainWindowViewModel.GetCategoryDict(source.DatData!, category);
var mi = new Avalonia.Controls.MenuItem
{
Header = $"{source.Name} ({dict.Count} {category.ToString().ToLowerInvariant()}s)",
Tag = source,
};
mi.Click += async (_, _) => await vm.MergeSessionAsync(source, category);

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Click handler is async and its exceptions will go unhandled (event handlers are effectively async void). Wrap the awaited call in a try/catch and route errors into StatusText/logging to avoid crashes if MergeSessionAsync throws.

Suggested change
mi.Click += async (_, _) => await vm.MergeSessionAsync(source, category);
mi.Click += async (_, _) =>
{
try
{
await vm.MergeSessionAsync(source, category);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error merging session '{source.Name}' for category '{category}': {ex}");
}
};

Copilot uses AI. Check for mistakes.
menuItem.Items.Add(mi);
}
}
};
}

private bool _closeConfirmed;

private async void OnWindowClosing(object? sender, WindowClosingEventArgs e)
Expand Down
16 changes: 11 additions & 5 deletions src/App/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,11 @@ private static void StripUnsupportedFlags(DatThingType thing, int targetProtocol
// ── Full session merge (DAT/SPR) ──

/// <summary>
/// Merge all items from a source session into the current (active) session.
/// Import things from a source session into the current (active) session.
/// When <paramref name="categoryFilter"/> is specified, only that category is imported.
/// Detects duplicates by comparing sprite images and shows a batch preview dialog.
/// </summary>
public async Task MergeSessionAsync(SessionViewModel sourceSession)
public async Task MergeSessionAsync(SessionViewModel sourceSession, ThingCategory? categoryFilter = null)
{
if (_datData == null || _sprFile == null)
{
Expand All @@ -591,16 +592,21 @@ public async Task MergeSessionAsync(SessionViewModel sourceSession)
var sourceProtocol = sourceDat.ProtocolVersion;
var targetProtocol = _datData.ProtocolVersion;

var categories = new[]
var allCategories = new[]
{
(ThingCategory.Item, sourceDat.Items, _datData.Items),
(ThingCategory.Outfit, sourceDat.Outfits, _datData.Outfits),
(ThingCategory.Effect, sourceDat.Effects, _datData.Effects),
(ThingCategory.Missile, sourceDat.Missiles, _datData.Missiles),
};

var categories = categoryFilter.HasValue
? allCategories.Where(c => c.Item1 == categoryFilter.Value).ToArray()
: allCategories;

int totalSource = categories.Sum(c => c.Item2.Count);
StatusText = $"Analyzing {totalSource} source things for duplicates…";
var label = categoryFilter?.ToString().ToLowerInvariant() ?? "thing";
StatusText = $"Analyzing {totalSource} source {label}s for duplicates…";

Copilot AI Mar 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StatusText pluralization is always "{label}s" which produces grammatically incorrect text for singular (e.g., "Analyzing 1 source items"). Consider using a simple singular/plural branch based on totalSource to keep status messages correct.

Suggested change
StatusText = $"Analyzing {totalSource} source {label}s for duplicates…";
var labelText = totalSource == 1 ? label : label + "s";
StatusText = $"Analyzing {totalSource} source {labelText} for duplicates…";

Copilot uses AI. Check for mistakes.

// Analyze each category
var entries = new List<TransplantEntry>();
Expand Down Expand Up @@ -5192,7 +5198,7 @@ private Dictionary<ushort, DatThingType> GetDatDictForCategory(ThingCategory cat
return GetCategoryDict(_datData!, category);
}

private static Dictionary<ushort, DatThingType> GetCategoryDict(DatData data, ThingCategory category)
public static Dictionary<ushort, DatThingType> GetCategoryDict(DatData data, ThingCategory category)
{
return category switch
{
Expand Down
Loading
Loading