feat: restore product-specific legacy project selection

This commit is contained in:
codex
2026-08-06 13:02:49 +03:00
parent 5c812f87fd
commit 72f127a427
3 changed files with 203 additions and 2 deletions
+67 -2
View File
@@ -15,8 +15,18 @@ public partial class MainWindow : System.Windows.Window
private readonly LegacyConfigurationAdapter adapter=new();
private LauncherProduct? selected;
private string? projectPath;
public MainWindow() { InitializeComponent(); Initialize(); }
internal MainWindow(LocalParityLaunchProfile profile) { localProfile=profile; InitializeComponent(); Initialize(); }
private string? manualRosterPath;
public MainWindow() { InitializeComponent(); WireSourceVerifiedSelection(); Initialize(); }
internal MainWindow(LocalParityLaunchProfile profile) { localProfile=profile; InitializeComponent(); WireSourceVerifiedSelection(); Initialize(); }
private void WireSourceVerifiedSelection()
{
SelectProjectButton.Click -= SelectProject_Click;
SelectProjectButton.Click += SelectProjectFile_Click;
AutomaticMode.Checked -= LoadMode_Changed;
ListMode.Checked -= LoadMode_Changed;
AutomaticMode.Checked += LoadModeChanged;
ListMode.Checked += LoadModeChanged;
}
private void Initialize()
{
var plant=new PlantProductDiscovery(new WindowsRegistryReader()).DiscoverPlant3D(2026);
@@ -34,7 +44,62 @@ public partial class MainWindow : System.Windows.Window
var path=Path.GetFullPath(dialog.FolderName); if(!Directory.Exists(path)){Status.Text="Выбранная папка недоступна.";return;} if(selected.Id is "plant3d" or "pnid" && !File.Exists(Path.Combine(path,"Project.xml"))){Status.Text="Выбранная папка не является проектом Plant 3D.";return;}
projectPath=path; ProjectPath.Text=path; ResolvePlugins();
}
private void SelectProjectFile_Click(object sender, System.Windows.RoutedEventArgs e)
{
if (selected is null) return;
var product = LegacyProductSelections.For(selected.Id);
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Select project", Filter = product.Filter, CheckFileExists = true, Multiselect = false };
if (dialog.ShowDialog() != true) return;
try
{
projectPath = LegacyProductSelections.ValidateSelectedFile(product, dialog.FileName);
manualRosterPath = null;
ProjectPath.Text = Path.GetFileName(projectPath);
ResolvePluginsSourceVerified();
}
catch (Exception ex) { Status.Text = UserMessage(ex.Message); }
}
private void LoadModeChanged(object sender, System.Windows.RoutedEventArgs e)
{
if (projectPath is null || selected is null) return;
if (ListMode.IsChecked == true)
{
var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Select download_list.xml", Filter = "download_list.xml (*.xml)|*.xml|All files (*.*)|*.*", CheckFileExists = true, Multiselect = false };
if (dialog.ShowDialog() != true) { AutomaticMode.IsChecked = true; return; }
manualRosterPath = dialog.FileName;
}
else manualRosterPath = null;
ResolvePluginsSourceVerified();
}
private void LoadMode_Changed(object sender,System.Windows.RoutedEventArgs e) { if(projectPath is not null)ResolvePlugins(); }
private void ResolvePluginsSourceVerified()
{
if (selected is null || projectPath is null) return;
if (localProfile is not null && selected.Id == "plant3d") { ResolvePlugins(); return; }
try
{
var product = LegacyProductSelections.For(selected.Id);
var plan = LegacyProductSelections.ReadRoster(product, projectPath, manualRosterPath, adapter);
Plugins.ItemsSource = plan.OrderedIds;
Plugins.IsEnabled = false;
PluginStatus.Text = $"List source: {plan.Source}.";
StartButton.IsEnabled = false;
Status.Text = plan.UnmappedIds.Count == 0
? "Packages for the selected plugins are not mapped to 2026 packages."
: $"Packages for {product.DisplayName} are not yet available for 2026: {string.Join(", ", plan.UnmappedIds)}";
}
catch (Exception ex)
{
Plugins.ItemsSource = null;
Plugins.IsEnabled = false;
StartButton.IsEnabled = false;
Status.Text = ex.Message == "LEGACY_DOWNLOAD_LIST_NOT_FOUND"
? "Для выбранного проекта не найден список плагинов"
: UserMessage(ex.Message);
}
}
private void ResolvePlugins()
{
if(selected is null||projectPath is null)return;
@@ -0,0 +1,37 @@
namespace Launcher.Next.Application;
public sealed record LegacyProductSelection(string Id, string DisplayName, string Filter, string RequiredFileName, string Pattern, string[] SourceVerifiedLegacyIds, string[] UnmappedLegacyIds, bool SupportsPlantBootstrap);
public sealed record LegacyPluginPlan(string RosterPath, string Source, IReadOnlyList<string> OrderedIds, IReadOnlyList<string> UnmappedIds);
public static class LegacyProductSelections
{
public static readonly LegacyProductSelection Plant3D = new("plant3d", "Plant 3D 2026", "Project files |Project.xml|All files (*.*)|*.*", "Project.xml", "Plant 3D_2019", [], [], true);
public static readonly LegacyProductSelection Pnid = new("pnid", "Plant P&ID 2026", "Project files |Project.xml|All files (*.*)|*.*", "Project.xml", "Plant PnID_2019", ["c1b999d6c16029e9ac5b1e1a8afe1500", "5b80379c3ec715e7da0ee414c53aa7bc", "1111"], ["c1b999d6c16029e9ac5b1e1a8afe1500", "5b80379c3ec715e7da0ee414c53aa7bc", "1111"], true);
public static readonly LegacyProductSelection Navisworks = new("navisworks", "Navisworks Manage 2026", "nwd files (*.nwd)|*.nwd|All files (*.*)|*.*", ".nwd", "Navisworks_Manage_2018", ["7dd981169fa14d21aaef3fcdb0e7e970"], ["7dd981169fa14d21aaef3fcdb0e7e970"], false);
public static LegacyProductSelection For(string id) => id switch { "plant3d" => Plant3D, "pnid" => Pnid, "navisworks" => Navisworks, _ => throw new InvalidOperationException("LEGACY_PRODUCT_UNKNOWN") };
public static string ValidateSelectedFile(LegacyProductSelection product, string selected)
{
var file = Path.GetFullPath(selected);
if (!File.Exists(file) || File.GetAttributes(file).HasFlag(FileAttributes.ReparsePoint)) throw new InvalidOperationException("LEGACY_PROJECT_FILE_INVALID");
var correctName = product.RequiredFileName.StartsWith('.') ? file.EndsWith(product.RequiredFileName, StringComparison.OrdinalIgnoreCase) : string.Equals(Path.GetFileName(file), product.RequiredFileName, StringComparison.OrdinalIgnoreCase);
if (!correctName) throw new InvalidOperationException("LEGACY_PROJECT_FILE_PRODUCT_MISMATCH");
return file;
}
public static string AutomaticRoster(LegacyProductSelection product, string selectedFile)
{
var projectDirectory = Path.GetDirectoryName(ValidateSelectedFile(product, selectedFile))!;
return Path.GetFullPath(Path.Combine(projectDirectory, "..", "_System", product.Pattern, "Applications", "download_list.xml"));
}
public static LegacyPluginPlan ReadRoster(LegacyProductSelection product, string selectedFile, string? manualXml, LegacyConfigurationAdapter parser)
{
var roster = manualXml is null ? AutomaticRoster(product, selectedFile) : Path.GetFullPath(manualXml);
if (!File.Exists(roster) || File.GetAttributes(roster).HasFlag(FileAttributes.ReparsePoint) || !string.Equals(Path.GetFileName(roster), "download_list.xml", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LEGACY_DOWNLOAD_LIST_NOT_FOUND");
var ids = parser.ParseDownloadList(File.ReadAllText(roster)).PluginIds;
if (product.SourceVerifiedLegacyIds.Length > 0 && ids.Any(id => !product.SourceVerifiedLegacyIds.Contains(id, StringComparer.OrdinalIgnoreCase))) throw new InvalidOperationException("LEGACY_PLUGIN_PRODUCT_MISMATCH");
return new(roster, manualXml is null ? "Automatic" : "Manual", ids, product.UnmappedLegacyIds.Intersect(ids, StringComparer.OrdinalIgnoreCase).ToArray());
}
}
@@ -0,0 +1,99 @@
using Launcher.Next.Application;
using Xunit;
namespace Launcher.Next.Tests;
public sealed class LegacyProductSelectionTests
{
[Fact]
public void SourceVerifiedProducts_ExposeExactOpenFileFilters()
{
Assert.Equal("Project files |Project.xml|All files (*.*)|*.*", LegacyProductSelections.Pnid.Filter);
Assert.Equal("nwd files (*.nwd)|*.nwd|All files (*.*)|*.*", LegacyProductSelections.Navisworks.Filter);
Assert.Equal("Project.xml", LegacyProductSelections.Pnid.RequiredFileName);
Assert.Equal(".nwd", LegacyProductSelections.Navisworks.RequiredFileName);
}
[Fact]
public void AutomaticRoster_UsesOnlyExactLegacyRelativeLocation()
{
using var fixture = new SelectionFixture();
var project = fixture.ProjectFile("Project.xml");
var actual = LegacyProductSelections.AutomaticRoster(LegacyProductSelections.Pnid, project);
Assert.Equal(Path.Combine(fixture.Root, "_System", "Plant PnID_2019", "Applications", "download_list.xml"), actual);
Assert.DoesNotContain("scan", actual, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void SelectedProjectFile_IsProductSpecificAndRejectsWrongExtension()
{
using var fixture = new SelectionFixture();
var nwd = fixture.ProjectFile("model.nwd");
Assert.Equal(nwd, LegacyProductSelections.ValidateSelectedFile(LegacyProductSelections.Navisworks, nwd));
Assert.Throws<InvalidOperationException>(() => LegacyProductSelections.ValidateSelectedFile(LegacyProductSelections.Pnid, nwd));
}
[Fact]
public void AutomaticPnidRoster_PreservesOrderAndReportsExactUnmappedIds()
{
using var fixture = new SelectionFixture();
var project = fixture.ProjectFile("Project.xml");
fixture.WriteAutomatic("Plant PnID_2019", "<root><id>c1b999d6c16029e9ac5b1e1a8afe1500</id><id>5b80379c3ec715e7da0ee414c53aa7bc</id><id>1111</id></root>");
var plan = LegacyProductSelections.ReadRoster(LegacyProductSelections.Pnid, project, null, new LegacyConfigurationAdapter());
Assert.Equal("Automatic", plan.Source);
Assert.Equal(["c1b999d6c16029e9ac5b1e1a8afe1500", "5b80379c3ec715e7da0ee414c53aa7bc", "1111"], plan.OrderedIds);
Assert.Equal(plan.OrderedIds, plan.UnmappedIds);
}
[Fact]
public void ManualRoster_DoesNotChangeProjectAndUsesExplicitDownloadList()
{
using var fixture = new SelectionFixture();
var project = fixture.ProjectFile("model.nwd");
var manual = Path.Combine(fixture.Root, "download_list.xml");
File.WriteAllText(manual, "<root><id>7dd981169fa14d21aaef3fcdb0e7e970</id></root>");
var plan = LegacyProductSelections.ReadRoster(LegacyProductSelections.Navisworks, project, manual, new LegacyConfigurationAdapter());
Assert.Equal("Manual", plan.Source);
Assert.Equal(manual, plan.RosterPath);
Assert.Equal(["7dd981169fa14d21aaef3fcdb0e7e970"], plan.UnmappedIds);
Assert.Equal(project, LegacyProductSelections.ValidateSelectedFile(LegacyProductSelections.Navisworks, project));
}
[Fact]
public void MissingAutomaticRoster_FailsClosedWithoutSearchingOtherDirectories()
{
using var fixture = new SelectionFixture();
var project = fixture.ProjectFile("model.nwd");
File.WriteAllText(Path.Combine(fixture.Root, "download_list.xml"), "<root><id>7dd981169fa14d21aaef3fcdb0e7e970</id></root>");
var ex = Assert.Throws<InvalidOperationException>(() => LegacyProductSelections.ReadRoster(LegacyProductSelections.Navisworks, project, null, new LegacyConfigurationAdapter()));
Assert.Equal("LEGACY_DOWNLOAD_LIST_NOT_FOUND", ex.Message);
}
[Fact]
public void ProductIncompatibleAndDuplicateIds_AreBlocked()
{
using var fixture = new SelectionFixture();
var project = fixture.ProjectFile("Project.xml");
fixture.WriteAutomatic("Plant PnID_2019", "<root><id>unexpected</id></root>");
Assert.Equal("LEGACY_PLUGIN_PRODUCT_MISMATCH", Assert.Throws<InvalidOperationException>(() => LegacyProductSelections.ReadRoster(LegacyProductSelections.Pnid, project, null, new LegacyConfigurationAdapter())).Message);
fixture.WriteAutomatic("Plant PnID_2019", "<root><id>1111</id><id>1111</id></root>");
Assert.Equal("LEGACY_DOWNLOAD_SELECTION_INVALID", Assert.Throws<InvalidOperationException>(() => LegacyProductSelections.ReadRoster(LegacyProductSelections.Pnid, project, null, new LegacyConfigurationAdapter())).Message);
}
[Fact]
public void Navisworks_DoesNotAdvertisePlantBootstrap()
{
Assert.False(LegacyProductSelections.Navisworks.SupportsPlantBootstrap);
Assert.True(LegacyProductSelections.Pnid.SupportsPlantBootstrap);
Assert.Equal(["7dd981169fa14d21aaef3fcdb0e7e970"], LegacyProductSelections.Navisworks.UnmappedLegacyIds);
}
private sealed class SelectionFixture : IDisposable
{
public SelectionFixture() { Root = Path.Combine(Path.GetTempPath(), "launcher-next-selection-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(Path.Combine(Root, "Project")); }
public string Root { get; }
public string ProjectFile(string name) { var path = Path.Combine(Root, "Project", name); File.WriteAllText(path, "fixture"); return path; }
public void WriteAutomatic(string pattern, string xml) { var directory = Path.Combine(Root, "_System", pattern, "Applications"); Directory.CreateDirectory(directory); File.WriteAllText(Path.Combine(directory, "download_list.xml"), xml); }
public void Dispose() { if (Directory.Exists(Root)) Directory.Delete(Root, true); }
}
}