Add plugin dependencies tab in config (which is the same as the setup window)
authorLiza Carvelli <liza@carvel.li>
Sun, 13 Apr 2025 10:03:47 +0000 (12:03 +0200)
committerLiza Carvelli <liza@carvel.li>
Sun, 13 Apr 2025 10:03:47 +0000 (12:03 +0200)
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs
Questionable/Windows/ConfigComponents/PluginConfigComponent.cs [new file with mode: 0644]
Questionable/Windows/ConfigWindow.cs
Questionable/Windows/OneTimeSetupWindow.cs

index f13f906..df3229d 100644 (file)
@@ -301,6 +301,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<PriorityWindow>();
 
         serviceCollection.AddSingleton<GeneralConfigComponent>();
+        serviceCollection.AddSingleton<PluginConfigComponent>();
         serviceCollection.AddSingleton<DutyConfigComponent>();
         serviceCollection.AddSingleton<SinglePlayerDutyConfigComponent>();
         serviceCollection.AddSingleton<NotificationConfigComponent>();
index 743ea7a..243a417 100644 (file)
@@ -19,8 +19,6 @@ internal sealed class GeneralConfigComponent : ConfigComponent
     private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")];
     private static readonly List<(EClassJob ClassJob, string Name)> DefaultClassJobs = [(EClassJob.Adventurer, "Auto (highest level/item level)")];
 
-    private readonly CombatController _combatController;
-
     private readonly uint[] _mountIds;
     private readonly string[] _mountNames;
     private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Wrath Combo", "Rotation Solver Reborn"];
@@ -34,13 +32,10 @@ internal sealed class GeneralConfigComponent : ConfigComponent
     public GeneralConfigComponent(
         IDalamudPluginInterface pluginInterface,
         Configuration configuration,
-        CombatController combatController,
         IDataManager dataManager,
         ClassJobUtils classJobUtils)
         : base(pluginInterface, configuration)
     {
-        _combatController = combatController;
-
         var mounts = dataManager.GetExcelSheet<Mount>()
             .Where(x => x is { RowId: > 0, Icon: > 0 })
             .Select(x => (MountId: x.RowId, Name: x.Singular.ToString()))
@@ -67,7 +62,7 @@ internal sealed class GeneralConfigComponent : ConfigComponent
         if (!tab)
             return;
 
-        using (ImRaii.Disabled(_combatController.IsRunning))
+
         {
             int selectedCombatModule = (int)Configuration.General.CombatModule;
             if (ImGui.Combo("Preferred Combat Module", ref selectedCombatModule, _combatModuleNames,
diff --git a/Questionable/Windows/ConfigComponents/PluginConfigComponent.cs b/Questionable/Windows/ConfigComponents/PluginConfigComponent.cs
new file mode 100644 (file)
index 0000000..b016b08
--- /dev/null
@@ -0,0 +1,337 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+using ImGuiNET;
+using Questionable.Controller;
+using Questionable.External;
+
+namespace Questionable.Windows.ConfigComponents;
+
+internal sealed class PluginConfigComponent : ConfigComponent
+{
+    private static readonly IReadOnlyList<PluginInfo> RequiredPlugins =
+    [
+        new("vnavmesh",
+            "vnavmesh",
+            """
+            vnavmesh handles the navigation within a zone, moving
+            your character to the next quest-related objective.
+            """,
+            new Uri("https://github.com/awgil/ffxiv_navmesh/"),
+            new Uri("https://puni.sh/api/repository/veyn")),
+        new("Lifestream",
+            "Lifestream",
+            """
+            Used to travel to aethernet shards in cities.
+            """,
+            new Uri("https://github.com/NightmareXIV/Lifestream"),
+            new Uri("https://github.com/NightmareXIV/MyDalamudPlugins/raw/main/pluginmaster.json")),
+        new("TextAdvance",
+            "TextAdvance",
+            """
+            Automatically accepts and turns in quests, skips cutscenes
+            and dialogue.
+            """,
+            new Uri("https://github.com/NightmareXIV/TextAdvance"),
+            new Uri("https://github.com/NightmareXIV/MyDalamudPlugins/raw/main/pluginmaster.json")),
+    ];
+
+    private static readonly ReadOnlyDictionary<Configuration.ECombatModule, PluginInfo> CombatPlugins =
+        new Dictionary<Configuration.ECombatModule, PluginInfo>
+        {
+            {
+                Configuration.ECombatModule.BossMod,
+                new("Boss Mod (VBM)",
+                    "BossMod",
+                    string.Empty,
+                    new Uri("https://github.com/awgil/ffxiv_bossmod"),
+                    new Uri("https://puni.sh/api/repository/veyn"))
+            },
+            {
+                Configuration.ECombatModule.WrathCombo,
+                new PluginInfo("Wrath Combo",
+                    "WrathCombo",
+                    string.Empty,
+                    new Uri("https://github.com/PunishXIV/WrathCombo"),
+                    new Uri("https://puni.sh/api/plugins"))
+            },
+            {
+                Configuration.ECombatModule.RotationSolverReborn,
+                new("Rotation Solver Reborn",
+                    "RotationSolver",
+                    string.Empty,
+                    new Uri("https://github.com/FFXIV-CombatReborn/RotationSolverReborn"),
+                    new Uri(
+                        "https://raw.githubusercontent.com/FFXIV-CombatReborn/CombatRebornRepo/main/pluginmaster.json"))
+            },
+        }.AsReadOnly();
+
+    private readonly IReadOnlyList<PluginInfo> _recommendedPlugins;
+
+    private readonly Configuration _configuration;
+    private readonly CombatController _combatController;
+    private readonly IDalamudPluginInterface _pluginInterface;
+    private readonly UiUtils _uiUtils;
+    private readonly ICommandManager _commandManager;
+
+    public PluginConfigComponent(
+        IDalamudPluginInterface pluginInterface,
+        Configuration configuration,
+        CombatController combatController,
+        UiUtils uiUtils,
+        ICommandManager commandManager,
+        AutomatonIpc automatonIpc,
+        PandorasBoxIpc pandorasBoxIpc)
+        : base(pluginInterface, configuration)
+    {
+        _configuration = configuration;
+        _combatController = combatController;
+        _pluginInterface = pluginInterface;
+        _uiUtils = uiUtils;
+        _commandManager = commandManager;
+        _recommendedPlugins =
+        [
+            new PluginInfo("CBT (formerly known as Automaton)",
+                "Automaton",
+                """
+                Automaton is a collection of automation-related tweaks.
+                """,
+                new Uri("https://github.com/Jaksuhn/Automaton"),
+                new Uri("https://puni.sh/api/repository/croizat"),
+                "/cbt",
+                [
+                    new PluginDetailInfo("'Sniper no sniping' enabled",
+                        "Automatically completes sniping tasks introduced in Stormblood",
+                        () => automatonIpc.IsAutoSnipeEnabled)
+                ]),
+            new PluginInfo("Pandora's Box",
+                "PandorasBox",
+                """
+                Pandora's Box is a collection of tweaks.
+                """,
+                new Uri("https://github.com/PunishXIV/PandorasBox"),
+                new Uri("https://puni.sh/api/plugins"),
+                "/pandora",
+                [
+                    new PluginDetailInfo("'Auto Active Time Maneuver' enabled",
+                        """
+                        Automatically completes active time maneuvers in
+                        single player instances, trials and raids"
+                        """,
+                        () => pandorasBoxIpc.IsAutoActiveTimeManeuverEnabled)
+                ]),
+            new("NotificationMaster",
+                "NotificationMaster",
+                """
+                Sends a configurable out-of-game notification if a quest
+                requires manual actions.
+                """,
+                new Uri("https://github.com/NightmareXIV/NotificationMaster"),
+                null),
+        ];
+    }
+
+    public override void DrawTab()
+    {
+        using var tab = ImRaii.TabItem("Dependencies###Plugins");
+        if (!tab)
+            return;
+
+        Draw(out bool allRequiredInstalled);
+
+        ImGui.Spacing();
+        ImGui.Separator();
+        ImGui.Spacing();
+
+        if (allRequiredInstalled)
+            ImGui.TextColored(ImGuiColors.ParsedGreen, "All required plugins are installed.");
+        else
+            ImGui.TextColored(ImGuiColors.DalamudRed,
+                "Required plugins are missing, Questionable will not work properly.");
+    }
+
+    public void Draw(out bool allRequiredInstalled)
+    {
+        float checklistPadding;
+        using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+        {
+            checklistPadding = ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X +
+                               ImGui.GetStyle().ItemSpacing.X;
+        }
+
+        ImGui.Text("Questionable requires the following plugins to work:");
+        allRequiredInstalled = true;
+        using (ImRaii.PushIndent())
+        {
+            foreach (var plugin in RequiredPlugins)
+                allRequiredInstalled &= DrawPlugin(plugin, checklistPadding);
+        }
+
+        ImGui.Spacing();
+        ImGui.Separator();
+        ImGui.Spacing();
+
+        ImGui.Text("Questionable supports multiple rotation/combat plugins, please pick the one\nyou want to use:");
+
+        using (ImRaii.PushIndent())
+        {
+            using (ImRaii.Disabled(_combatController.IsRunning))
+            {
+                if (ImGui.RadioButton("No rotation/combat plugin (combat must be done manually)",
+                        _configuration.General.CombatModule == Configuration.ECombatModule.None))
+                {
+                    _configuration.General.CombatModule = Configuration.ECombatModule.None;
+                    _pluginInterface.SavePluginConfig(_configuration);
+                }
+
+                allRequiredInstalled &= DrawCombatPlugin(Configuration.ECombatModule.BossMod, checklistPadding);
+                allRequiredInstalled &= DrawCombatPlugin(Configuration.ECombatModule.WrathCombo, checklistPadding);
+                allRequiredInstalled &=
+                    DrawCombatPlugin(Configuration.ECombatModule.RotationSolverReborn, checklistPadding);
+            }
+        }
+
+        ImGui.Spacing();
+        ImGui.Separator();
+        ImGui.Spacing();
+
+        ImGui.Text("The following plugins are recommended, but not required:");
+        using (ImRaii.PushIndent())
+        {
+            foreach (var plugin in _recommendedPlugins)
+                DrawPlugin(plugin, checklistPadding);
+        }
+    }
+
+    private bool DrawPlugin(PluginInfo plugin, float checklistPadding)
+    {
+        using (ImRaii.PushId("plugin_" + plugin.DisplayName))
+        {
+            IExposedPlugin? installedPlugin = FindInstalledPlugin(plugin);
+            bool isInstalled = installedPlugin != null;
+            string label = plugin.DisplayName;
+            if (installedPlugin != null)
+                label += $" v{installedPlugin.Version}";
+
+            _uiUtils.ChecklistItem(label, isInstalled);
+
+            DrawPluginDetails(plugin, checklistPadding, isInstalled);
+            return isInstalled;
+        }
+    }
+
+    private bool DrawCombatPlugin(Configuration.ECombatModule combatModule, float checklistPadding)
+    {
+        ImGui.Spacing();
+
+        PluginInfo plugin = CombatPlugins[combatModule];
+        using (ImRaii.PushId("plugin_" + plugin.DisplayName))
+        {
+            IExposedPlugin? installedPlugin = FindInstalledPlugin(plugin);
+            bool isInstalled = installedPlugin != null;
+            string label = plugin.DisplayName;
+            if (installedPlugin != null)
+                label += $" v{installedPlugin.Version}";
+
+            if (ImGui.RadioButton(label, _configuration.General.CombatModule == combatModule))
+            {
+                _configuration.General.CombatModule = combatModule;
+                _pluginInterface.SavePluginConfig(_configuration);
+            }
+
+            ImGui.SameLine(0);
+            using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+            {
+                var iconColor = isInstalled ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
+                var icon = isInstalled ? FontAwesomeIcon.Check : FontAwesomeIcon.Times;
+
+                ImGui.AlignTextToFramePadding();
+                ImGui.TextColored(iconColor, icon.ToIconString());
+            }
+
+            DrawPluginDetails(plugin, checklistPadding, isInstalled);
+            return isInstalled || _configuration.General.CombatModule != combatModule;
+        }
+    }
+
+    private void DrawPluginDetails(PluginInfo plugin, float checklistPadding, bool isInstalled)
+    {
+        using (ImRaii.PushIndent(checklistPadding))
+        {
+            if (!string.IsNullOrEmpty(plugin.Details))
+                ImGui.TextUnformatted(plugin.Details);
+
+            bool allDetailsOk = true;
+            if (plugin.DetailsToCheck != null)
+            {
+                foreach (var detail in plugin.DetailsToCheck)
+                {
+                    bool detailOk = detail.Predicate();
+                    allDetailsOk &= detailOk;
+
+                    _uiUtils.ChecklistItem(detail.DisplayName, isInstalled && detailOk);
+                    if (!string.IsNullOrEmpty(detail.Details))
+                    {
+                        using (ImRaii.PushIndent(checklistPadding))
+                        {
+                            ImGui.TextUnformatted(detail.Details);
+                        }
+                    }
+                }
+            }
+
+            ImGui.Spacing();
+
+            if (isInstalled)
+            {
+                if (!allDetailsOk && plugin.ConfigCommand != null && plugin.ConfigCommand.StartsWith('/'))
+                {
+                    if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Cog, "Open configuration"))
+                        _commandManager.ProcessCommand(plugin.ConfigCommand);
+                }
+            }
+            else
+            {
+                if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Globe, "Open Website"))
+                    Util.OpenLink(plugin.WebsiteUri.ToString());
+
+                ImGui.SameLine();
+                if (plugin.DalamudRepositoryUri != null)
+                {
+                    if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Code, "Open Repository"))
+                        Util.OpenLink(plugin.DalamudRepositoryUri.ToString());
+                }
+                else
+                {
+                    ImGui.AlignTextToFramePadding();
+                    ImGuiComponents.HelpMarker("Available on official Dalamud Repository");
+                }
+            }
+        }
+    }
+
+    private IExposedPlugin? FindInstalledPlugin(PluginInfo pluginInfo)
+    {
+        return _pluginInterface.InstalledPlugins.FirstOrDefault(x =>
+            x.InternalName == pluginInfo.InternalName && x.IsLoaded);
+    }
+
+    private sealed record PluginInfo(
+        string DisplayName,
+        string InternalName,
+        string Details,
+        Uri WebsiteUri,
+        Uri? DalamudRepositoryUri,
+        string? ConfigCommand = null,
+        List<PluginDetailInfo>? DetailsToCheck = null);
+
+    private sealed record PluginDetailInfo(string DisplayName, string Details, Func<bool> Predicate);
+}
index f3611a5..42f01dc 100644 (file)
@@ -11,6 +11,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
 {
     private readonly IDalamudPluginInterface _pluginInterface;
     private readonly GeneralConfigComponent _generalConfigComponent;
+    private readonly PluginConfigComponent _pluginConfigComponent;
     private readonly DutyConfigComponent _dutyConfigComponent;
     private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent;
     private readonly NotificationConfigComponent _notificationConfigComponent;
@@ -20,6 +21,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
     public ConfigWindow(
         IDalamudPluginInterface pluginInterface,
         GeneralConfigComponent generalConfigComponent,
+        PluginConfigComponent pluginConfigComponent,
         DutyConfigComponent dutyConfigComponent,
         SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent,
         NotificationConfigComponent notificationConfigComponent,
@@ -29,6 +31,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
     {
         _pluginInterface = pluginInterface;
         _generalConfigComponent = generalConfigComponent;
+        _pluginConfigComponent = pluginConfigComponent;
         _dutyConfigComponent = dutyConfigComponent;
         _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent;
         _notificationConfigComponent = notificationConfigComponent;
@@ -45,6 +48,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
             return;
 
         _generalConfigComponent.DrawTab();
+        _pluginConfigComponent.DrawTab();
         _dutyConfigComponent.DrawTab();
         _singlePlayerDutyConfigComponent.DrawTab();
         _notificationConfigComponent.DrawTab();
index 37c151f..5903ece 100644 (file)
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using Dalamud.Interface;
+using Dalamud.Interface;
 using Dalamud.Interface.Colors;
 using Dalamud.Interface.Components;
 using Dalamud.Interface.Utility.Raii;
 using Dalamud.Plugin;
-using Dalamud.Plugin.Services;
-using Dalamud.Utility;
 using ImGuiNET;
 using LLib.ImGui;
 using Microsoft.Extensions.Logging;
-using Questionable.External;
+using Questionable.Windows.ConfigComponents;
 
 namespace Questionable.Windows;
 
 internal sealed class OneTimeSetupWindow : LWindow
 {
-    private static readonly IReadOnlyList<PluginInfo> RequiredPlugins =
-    [
-        new("vnavmesh",
-            "vnavmesh",
-            """
-            vnavmesh handles the navigation within a zone, moving
-            your character to the next quest-related objective.
-            """,
-            new Uri("https://github.com/awgil/ffxiv_navmesh/"),
-            new Uri("https://puni.sh/api/repository/veyn")),
-        new("Lifestream",
-            "Lifestream",
-            """
-            Used to travel to aethernet shards in cities.
-            """,
-            new Uri("https://github.com/NightmareXIV/Lifestream"),
-            new Uri("https://github.com/NightmareXIV/MyDalamudPlugins/raw/main/pluginmaster.json")),
-        new("TextAdvance",
-            "TextAdvance",
-            """
-            Automatically accepts and turns in quests, skips cutscenes
-            and dialogue.
-            """,
-            new Uri("https://github.com/NightmareXIV/TextAdvance"),
-            new Uri("https://github.com/NightmareXIV/MyDalamudPlugins/raw/main/pluginmaster.json")),
-    ];
-
-    private static readonly ReadOnlyDictionary<Configuration.ECombatModule, PluginInfo> CombatPlugins = new Dictionary<Configuration.ECombatModule, PluginInfo>
-    {
-        {
-            Configuration.ECombatModule.BossMod,
-            new("Boss Mod (VBM)",
-                "BossMod",
-                string.Empty,
-                new Uri("https://github.com/awgil/ffxiv_bossmod"),
-                new Uri("https://puni.sh/api/repository/veyn"))
-        },
-        {
-            Configuration.ECombatModule.WrathCombo,
-            new PluginInfo("Wrath Combo",
-                "WrathCombo",
-                string.Empty,
-                new Uri("https://github.com/PunishXIV/WrathCombo"),
-                new Uri("https://puni.sh/api/plugins"))
-        },
-        {
-            Configuration.ECombatModule.RotationSolverReborn,
-            new("Rotation Solver Reborn",
-                "RotationSolver",
-                string.Empty,
-                new Uri("https://github.com/FFXIV-CombatReborn/RotationSolverReborn"),
-                new Uri(
-                    "https://raw.githubusercontent.com/FFXIV-CombatReborn/CombatRebornRepo/main/pluginmaster.json"))
-        },
-    }.AsReadOnly();
-
-    private readonly IReadOnlyList<PluginInfo> _recommendedPlugins;
-
+    private readonly PluginConfigComponent _pluginConfigComponent;
     private readonly Configuration _configuration;
     private readonly IDalamudPluginInterface _pluginInterface;
-    private readonly UiUtils _uiUtils;
     private readonly ILogger<OneTimeSetupWindow> _logger;
-    private readonly ICommandManager _commandManager;
 
     public OneTimeSetupWindow(
+        PluginConfigComponent pluginConfigComponent,
         Configuration configuration,
         IDalamudPluginInterface pluginInterface,
-        UiUtils uiUtils,
-        ILogger<OneTimeSetupWindow> logger,
-        AutomatonIpc automatonIpc,
-        PandorasBoxIpc pandorasBoxIpc,
-        ICommandManager commandManager)
+        ILogger<OneTimeSetupWindow> logger)
         : base("Questionable Setup###QuestionableOneTimeSetup",
             ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings, true)
     {
+        _pluginConfigComponent = pluginConfigComponent;
         _configuration = configuration;
         _pluginInterface = pluginInterface;
-        _uiUtils = uiUtils;
         _logger = logger;
-        _commandManager = commandManager;
-        _recommendedPlugins =
-        [
-            new PluginInfo("CBT (formerly known as Automaton)",
-                "Automaton",
-                """
-                Automaton is a collection of automation-related tweaks.
-                The 'Sniper no sniping' tweak can complete snipe tasks automatically.
-                """,
-                new Uri("https://github.com/Jaksuhn/Automaton"),
-                new Uri("https://puni.sh/api/repository/croizat"),
-                "/cbt",
-                [new PluginDetailInfo("'Sniper no sniping' enabled", () => automatonIpc.IsAutoSnipeEnabled)]),
-            new PluginInfo("Pandora's Box",
-                "PandorasBox",
-                """
-                Pandora's Box is a collection of tweaks.
-                The 'Auto Active Time Maneuver' tweak can complete any
-                active time maneuvers in single player instances, trials and raids.
-                """,
-                new Uri("https://github.com/PunishXIV/PandorasBox"),
-                new Uri("https://puni.sh/api/plugins"),
-                "/pandora",
-                [new PluginDetailInfo("'Auto Active Time Maneuver' enabled",
-                    () => pandorasBoxIpc.IsAutoActiveTimeManeuverEnabled)]),
-            new("NotificationMaster",
-                "NotificationMaster",
-                """
-                Sends a configurable out-of-game notification if a quest
-                requires manual actions.
-                """,
-                new Uri("https://github.com/NightmareXIV/NotificationMaster"),
-                null),
-        ];
 
         RespectCloseHotkey = false;
         ShowCloseButton = false;
@@ -142,51 +40,7 @@ internal sealed class OneTimeSetupWindow : LWindow
 
     public override void Draw()
     {
-        float checklistPadding;
-        using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
-        {
-            checklistPadding = ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X +
-                               ImGui.GetStyle().ItemSpacing.X;
-        }
-
-        ImGui.Text("Questionable requires the following plugins to work:");
-        bool allRequiredInstalled = true;
-        using (ImRaii.PushIndent())
-        {
-            foreach (var plugin in RequiredPlugins)
-                allRequiredInstalled &= DrawPlugin(plugin, checklistPadding);
-        }
-
-        ImGui.Spacing();
-        ImGui.Separator();
-        ImGui.Spacing();
-
-        ImGui.Text("Questionable supports multiple rotation/combat plugins, please pick the one\nyou want to use:");
-
-        using (ImRaii.PushIndent())
-        {
-            if (ImGui.RadioButton("No rotation/combat plugin (combat must be done manually)",
-                    _configuration.General.CombatModule == Configuration.ECombatModule.None))
-            {
-                _configuration.General.CombatModule = Configuration.ECombatModule.None;
-                _pluginInterface.SavePluginConfig(_configuration);
-            }
-
-            DrawCombatPlugin(Configuration.ECombatModule.BossMod, checklistPadding);
-            DrawCombatPlugin(Configuration.ECombatModule.WrathCombo, checklistPadding);
-            DrawCombatPlugin(Configuration.ECombatModule.RotationSolverReborn, checklistPadding);
-        }
-
-        ImGui.Spacing();
-        ImGui.Separator();
-        ImGui.Spacing();
-
-        ImGui.Text("The following plugins are recommended, but not required:");
-        using (ImRaii.PushIndent())
-        {
-            foreach (var plugin in _recommendedPlugins)
-                DrawPlugin(plugin, checklistPadding);
-        }
+        _pluginConfigComponent.Draw(out bool allRequiredInstalled);
 
         ImGui.Spacing();
         ImGui.Separator();
@@ -222,110 +76,4 @@ internal sealed class OneTimeSetupWindow : LWindow
             IsOpen = false;
         }
     }
-
-    private bool DrawPlugin(PluginInfo plugin, float checklistPadding)
-    {
-        using (ImRaii.PushId("plugin_" + plugin.DisplayName))
-        {
-            bool isInstalled = IsPluginInstalled(plugin);
-            _uiUtils.ChecklistItem(plugin.DisplayName, isInstalled);
-
-            DrawPluginDetails(plugin, checklistPadding, isInstalled);
-            return isInstalled;
-        }
-    }
-
-    private void DrawCombatPlugin(Configuration.ECombatModule combatModule, float checklistPadding)
-    {
-        ImGui.Spacing();
-
-        PluginInfo plugin = CombatPlugins[combatModule];
-        using (ImRaii.PushId("plugin_" + plugin.DisplayName))
-        {
-            bool isInstalled = IsPluginInstalled(plugin);
-            if (ImGui.RadioButton(plugin.DisplayName, _configuration.General.CombatModule == combatModule))
-            {
-                _configuration.General.CombatModule = combatModule;
-                _pluginInterface.SavePluginConfig(_configuration);
-            }
-
-            ImGui.SameLine(0);
-            using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
-            {
-                var iconColor = isInstalled ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
-                var icon = isInstalled ? FontAwesomeIcon.Check : FontAwesomeIcon.Times;
-
-                ImGui.AlignTextToFramePadding();
-                ImGui.TextColored(iconColor, icon.ToIconString());
-            }
-
-
-            DrawPluginDetails(plugin, checklistPadding, isInstalled);
-        }
-    }
-
-    private void DrawPluginDetails(PluginInfo plugin, float checklistPadding, bool isInstalled)
-    {
-        using (ImRaii.PushIndent(checklistPadding))
-        {
-            if (!string.IsNullOrEmpty(plugin.Details))
-                ImGui.TextUnformatted(plugin.Details);
-
-            bool allDetailsOk = true;
-            if (plugin.DetailsToCheck != null)
-            {
-                foreach (var detail in plugin.DetailsToCheck)
-                {
-                    bool detailOk = detail.Predicate();
-                    allDetailsOk &= detailOk;
-
-                    _uiUtils.ChecklistItem(detail.DisplayName, isInstalled && detailOk);
-                }
-            }
-
-            ImGui.Spacing();
-
-            if (isInstalled)
-            {
-                if (!allDetailsOk && plugin.ConfigCommand != null && plugin.ConfigCommand.StartsWith('/'))
-                {
-                    if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Cog, "Open configuration"))
-                        _commandManager.ProcessCommand(plugin.ConfigCommand);
-                }
-            }
-            else
-            {
-                if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Globe, "Open Website"))
-                    Util.OpenLink(plugin.WebsiteUri.ToString());
-
-                ImGui.SameLine();
-                if (plugin.DalamudRepositoryUri != null)
-                {
-                    if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Code, "Open Repository"))
-                        Util.OpenLink(plugin.DalamudRepositoryUri.ToString());
-                }
-                else
-                {
-                    ImGui.AlignTextToFramePadding();
-                    ImGuiComponents.HelpMarker("Available on official Dalamud Repository");
-                }
-            }
-        }
-    }
-
-    private bool IsPluginInstalled(PluginInfo pluginInfo)
-    {
-        return _pluginInterface.InstalledPlugins.Any(x => x.InternalName == pluginInfo.InternalName && x.IsLoaded);
-    }
-
-    private sealed record PluginInfo(
-        string DisplayName,
-        string InternalName,
-        string Details,
-        Uri WebsiteUri,
-        Uri? DalamudRepositoryUri,
-        string? ConfigCommand = null,
-        List<PluginDetailInfo>? DetailsToCheck = null);
-
-    private sealed record PluginDetailInfo(string DisplayName, Func<bool> Predicate);
 }