diff --git a/Dalamud/Configuration/Internal/AutoUpdatePreference.cs b/Dalamud/Configuration/Internal/AutoUpdatePreference.cs
new file mode 100644
index 000000000..2b7dced11
--- /dev/null
+++ b/Dalamud/Configuration/Internal/AutoUpdatePreference.cs
@@ -0,0 +1,42 @@
+namespace Dalamud.Configuration.Internal;
+
+///
+/// Class representing a plugin that has opted in to auto-updating.
+///
+internal class AutoUpdatePreference
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The unique ID representing the plugin.
+ public AutoUpdatePreference(Guid pluginId)
+ {
+ this.WorkingPluginId = pluginId;
+ }
+
+ ///
+ /// The kind of opt-in.
+ ///
+ public enum OptKind
+ {
+ ///
+ /// Never auto-update this plugin.
+ ///
+ NeverUpdate,
+
+ ///
+ /// Always auto-update this plugin, regardless of the user's settings.
+ ///
+ AlwaysUpdate,
+ }
+
+ ///
+ /// Gets or sets the unique ID representing the plugin.
+ ///
+ public Guid WorkingPluginId { get; set; }
+
+ ///
+ /// Gets or sets the type of opt-in.
+ ///
+ public OptKind Kind { get; set; } = OptKind.AlwaysUpdate;
+}
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 0267042ed..e5348d999 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
-using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Style;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Storage;
using Dalamud.Utility;
@@ -196,6 +196,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
///
/// Gets or sets a value indicating whether or not plugins should be auto-updated.
///
+ [Obsolete("Use AutoUpdateBehavior instead.")]
public bool AutoUpdatePlugins { get; set; }
///
@@ -229,7 +230,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public int LogLinesLimit { get; set; } = 10000;
///
- /// Gets or sets a list of commands that have been run in the console window.
+ /// Gets or sets a list representing the command history for the Dalamud Console.
///
public List LogCommandHistory { get; set; } = new();
@@ -434,7 +435,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
///
/// Gets or sets a list of plugins that testing builds should be downloaded for.
///
- public List? PluginTestingOptIns { get; set; }
+ public List PluginTestingOptIns { get; set; } = [];
+
+ ///
+ /// Gets or sets a list of plugins that have opted into or out of auto-updating.
+ ///
+ public List PluginAutoUpdatePreferences { get; set; } = [];
///
/// Gets or sets a value indicating whether the FFXIV window should be toggled to immersive mode.
@@ -466,6 +472,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
///
public PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerOpenKind.AllPlugins;
+ ///
+ /// Gets or sets a value indicating how auto-updating should behave.
+ ///
+ public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
+
+ ///
+ /// Gets or sets a value indicating whether or not users should be notified regularly about pending updates.
+ ///
+ public bool CheckPeriodicallyForUpdates { get; set; } = true;
+
///
/// Load a configuration from the provided path.
///
@@ -551,6 +567,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
private void SetDefaults()
{
+#pragma warning disable CS0618
// "Reduced motion"
if (!this.ReduceMotions.HasValue)
{
@@ -572,6 +589,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
this.ReduceMotions = winAnimEnabled == 0;
}
}
+
+ // Migrate old auto-update setting to new auto-update behavior
+ this.AutoUpdateBehavior ??= this.AutoUpdatePlugins
+ ? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll
+ : Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify;
+#pragma warning restore CS0618
}
private void Save()
diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs
index 755dd23e0..b338ae0d8 100644
--- a/Dalamud/Console/ConsoleManagerPluginScoped.cs
+++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs
@@ -31,7 +31,6 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
/// Initializes a new instance of the class.
///
/// The plugin this service belongs to.
- /// The console manager.
[ServiceManager.ServiceConstructor]
internal ConsoleManagerPluginScoped(LocalPlugin plugin)
{
diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs
index 7f93ce6c2..09d825552 100644
--- a/Dalamud/Game/ChatHandlers.cs
+++ b/Dalamud/Game/ChatHandlers.cs
@@ -30,40 +30,6 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService]
internal class ChatHandlers : IServiceType
{
- // private static readonly Dictionary UnicodeToDiscordEmojiDict = new()
- // {
- // { "", "<:ffxive071:585847382210642069>" },
- // { "", "<:ffxive083:585848592699490329>" },
- // };
-
- // private readonly Dictionary handledChatTypeColors = new()
- // {
- // { XivChatType.CrossParty, Color.DodgerBlue },
- // { XivChatType.Party, Color.DodgerBlue },
- // { XivChatType.FreeCompany, Color.DeepSkyBlue },
- // { XivChatType.CrossLinkShell1, Color.ForestGreen },
- // { XivChatType.CrossLinkShell2, Color.ForestGreen },
- // { XivChatType.CrossLinkShell3, Color.ForestGreen },
- // { XivChatType.CrossLinkShell4, Color.ForestGreen },
- // { XivChatType.CrossLinkShell5, Color.ForestGreen },
- // { XivChatType.CrossLinkShell6, Color.ForestGreen },
- // { XivChatType.CrossLinkShell7, Color.ForestGreen },
- // { XivChatType.CrossLinkShell8, Color.ForestGreen },
- // { XivChatType.Ls1, Color.ForestGreen },
- // { XivChatType.Ls2, Color.ForestGreen },
- // { XivChatType.Ls3, Color.ForestGreen },
- // { XivChatType.Ls4, Color.ForestGreen },
- // { XivChatType.Ls5, Color.ForestGreen },
- // { XivChatType.Ls6, Color.ForestGreen },
- // { XivChatType.Ls7, Color.ForestGreen },
- // { XivChatType.Ls8, Color.ForestGreen },
- // { XivChatType.TellIncoming, Color.HotPink },
- // { XivChatType.PvPTeam, Color.SandyBrown },
- // { XivChatType.Urgent, Color.DarkViolet },
- // { XivChatType.NoviceNetwork, Color.SaddleBrown },
- // { XivChatType.Echo, Color.Gray },
- // };
-
private static readonly ModuleLog Log = new("CHATHANDLER");
private readonly Regex rmtRegex = new(
@@ -106,8 +72,6 @@ internal class ChatHandlers : IServiceType
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
- private readonly DalamudLinkPayload openInstallerWindowLink;
-
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service.Get();
@@ -115,19 +79,12 @@ internal class ChatHandlers : IServiceType
private readonly DalamudConfiguration configuration = Service.Get();
private bool hasSeenLoadingMsg;
- private bool startedAutoUpdatingPlugins;
- private CancellationTokenSource deferredAutoUpdateCts = new();
[ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui)
{
chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
chatGui.ChatMessage += this.OnChatMessage;
-
- this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
- {
- Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins);
- });
}
///
@@ -135,11 +92,6 @@ internal class ChatHandlers : IServiceType
///
public string? LastLink { get; private set; }
- ///
- /// Gets a value indicating whether or not auto-updates have already completed this session.
- ///
- public bool IsAutoUpdateComplete { get; private set; }
-
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{
var textVal = message.TextValue;
@@ -176,9 +128,6 @@ internal class ChatHandlers : IServiceType
{
if (!this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
-
- if (!this.startedAutoUpdatingPlugins)
- this.AutoUpdatePluginsWithRetry();
}
// For injections while logged in
@@ -273,89 +222,4 @@ internal class ChatHandlers : IServiceType
this.hasSeenLoadingMsg = true;
}
-
- private void AutoUpdatePluginsWithRetry()
- {
- var firstAttempt = this.AutoUpdatePlugins();
- if (!firstAttempt)
- {
- Task.Run(() =>
- {
- Task.Delay(30_000, this.deferredAutoUpdateCts.Token);
- this.AutoUpdatePlugins();
- });
- }
- }
-
- private bool AutoUpdatePlugins()
- {
- var chatGui = Service.GetNullable();
- var pluginManager = Service.GetNullable();
- var notifications = Service.GetNullable();
- var condition = Service.GetNullable();
-
- if (chatGui == null || pluginManager == null || notifications == null || condition == null)
- {
- Log.Warning("Aborting auto-update because a required service was not loaded.");
- return false;
- }
-
- if (condition.Any(ConditionFlag.BoundByDuty, ConditionFlag.BoundByDuty56, ConditionFlag.BoundByDuty95))
- {
- Log.Warning("Aborting auto-update because the player is in a duty.");
- return false;
- }
-
- if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any())
- {
- // Plugins aren't ready yet.
- // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice.
- Log.Warning("Aborting auto-update because plugins weren't loaded or ready.");
- return false;
- }
-
- this.startedAutoUpdatingPlugins = true;
-
- Log.Debug("Beginning plugin auto-update process...");
- Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task =>
- {
- this.IsAutoUpdateComplete = true;
-
- if (task.IsFaulted)
- {
- Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates."));
- return;
- }
-
- var updatedPlugins = task.Result.ToList();
- if (updatedPlugins.Any())
- {
- if (this.configuration.AutoUpdatePlugins)
- {
- Service.Get().PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
- notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info);
- }
- else
- {
- chatGui.Print(new XivChatEntry
- {
- Message = new SeString(new List()
- {
- new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")),
- new TextPayload(" ["),
- new UIForegroundPayload(500),
- this.openInstallerWindowLink,
- new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
- RawPayload.LinkTerminator,
- new UIForegroundPayload(0),
- new TextPayload("]"),
- }),
- Type = XivChatType.Urgent,
- });
- }
- }
- });
-
- return true;
- }
}
diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs
index 18ecf5386..1f82cca49 100644
--- a/Dalamud/Interface/DalamudWindowOpenKinds.cs
+++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs
@@ -35,6 +35,11 @@ public enum SettingsOpenKind
/// Open to the "Look & Feel" page.
///
LookAndFeel,
+
+ ///
+ /// Open to the "Auto Updates" page.
+ ///
+ AutoUpdates,
///
/// Open to the "Server Info Bar" page.
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index 2eb8299b3..f57355112 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -211,6 +211,15 @@ internal class DalamudInterface : IInternalDisposableService
set => this.isImGuiDrawDevMenu = value;
}
+ ///
+ /// Gets or sets a value indicating whether the plugin installer is open.
+ ///
+ public bool IsPluginInstallerOpen
+ {
+ get => this.pluginWindow.IsOpen;
+ set => this.pluginWindow.IsOpen = value;
+ }
+
///
void IInternalDisposableService.DisposeService()
{
diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs
new file mode 100644
index 000000000..d525af484
--- /dev/null
+++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs
@@ -0,0 +1,56 @@
+using System.Numerics;
+
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.DesignSystem;
+
+///
+/// Private ImGui widgets for use inside Dalamud.
+///
+internal static partial class DalamudComponents
+{
+ private static readonly Vector2 ButtonPadding = new(8 * ImGuiHelpers.GlobalScale, 6 * ImGuiHelpers.GlobalScale);
+ private static readonly Vector4 SecondaryButtonBackground = new(0, 0, 0, 0);
+
+ private static Vector4 PrimaryButtonBackground => ImGuiColors.TankBlue;
+
+ ///
+ /// Draw a "primary style" button.
+ ///
+ /// The text to show.
+ /// True if the button was clicked.
+ internal static bool PrimaryButton(string text)
+ {
+ using (ImRaii.PushColor(ImGuiCol.Button, PrimaryButtonBackground))
+ {
+ return Button(text);
+ }
+ }
+
+ ///
+ /// Draw a "secondary style" button.
+ ///
+ /// The text to show.
+ /// True if the button was clicked.
+ internal static bool SecondaryButton(string text)
+ {
+ using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1))
+ using (var buttonColor = ImRaii.PushColor(ImGuiCol.Button, SecondaryButtonBackground))
+ {
+ buttonColor.Push(ImGuiCol.Border, ImGuiColors.DalamudGrey3);
+ return Button(text);
+ }
+ }
+
+ private static bool Button(string text)
+ {
+ using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, ButtonPadding))
+ {
+ return ImGui.Button(text);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs
new file mode 100644
index 000000000..f0ce6bc82
--- /dev/null
+++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs
@@ -0,0 +1,79 @@
+using System.Linq;
+using System.Numerics;
+
+using CheapLoc;
+
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.Types;
+using Dalamud.Utility;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.DesignSystem;
+
+///
+/// Private ImGui widgets for use inside Dalamud.
+///
+internal static partial class DalamudComponents
+{
+ ///
+ /// Draw a "picker" popup to chose a plugin.
+ ///
+ /// The ID of the popup.
+ /// String holding the search input.
+ /// Action to be called if a plugin is clicked.
+ /// Function that should return true if a plugin should show as disabled.
+ /// Function that should return true if a plugin should not appear in the list.
+ /// An ImGuiID to open the popup.
+ internal static uint DrawPluginPicker(string id, ref string pickerSearch, Action onClicked, Func pluginDisabled, Func? pluginFiltered = null)
+ {
+ var pm = Service.GetNullable();
+ if (pm == null)
+ return 0;
+
+ var addPluginToProfilePopupId = ImGui.GetID(id);
+ using var popup = ImRaii.Popup(id);
+
+ if (popup.Success)
+ {
+ var width = ImGuiHelpers.GlobalScale * 300;
+
+ ImGui.SetNextItemWidth(width);
+ ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref pickerSearch, 255);
+
+ var currentSearchString = pickerSearch;
+ if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
+ {
+ // TODO: Plugin searching should be abstracted... installer and this should use the same search
+ var plugins = pm.InstalledPlugins.Where(
+ x => x.Manifest.SupportsProfiles &&
+ (currentSearchString.IsNullOrWhitespace() || x.Manifest.Name.Contains(
+ currentSearchString,
+ StringComparison.InvariantCultureIgnoreCase)))
+ .Where(pluginFiltered ?? (_ => true));
+
+ foreach (var plugin in plugins)
+ {
+ using var disabled2 =
+ ImRaii.Disabled(pluginDisabled(plugin));
+
+ if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}"))
+ {
+ onClicked(plugin);
+ }
+ }
+
+ ImGui.EndListBox();
+ }
+ }
+
+ return addPluginToProfilePopupId;
+ }
+
+ private static partial class Locs
+ {
+ public static string SearchHint => Loc.Localize("ProfileManagerSearchHint", "Search...");
+ }
+}
diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs
new file mode 100644
index 000000000..be3c90640
--- /dev/null
+++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs
@@ -0,0 +1,8 @@
+namespace Dalamud.Interface.Internal.DesignSystem;
+
+///
+/// Private ImGui widgets for use inside Dalamud.
+///
+internal static partial class DalamudComponents
+{
+}
diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
index ae59db36a..613fc7d28 100644
--- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
@@ -1,6 +1,8 @@
using System.Linq;
using System.Numerics;
+using CheapLoc;
+
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
@@ -12,6 +14,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
@@ -23,6 +26,8 @@ namespace Dalamud.Interface.Internal.Windows;
///
internal sealed class ChangelogWindow : Window, IDisposable
{
+ private const AutoUpdateBehavior DefaultAutoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo;
+
private const string WarrantsChangelogForMajorMinor = "9.0.";
private const string ChangeLog =
@@ -47,15 +52,32 @@ internal sealed class ChangelogWindow : Window, IDisposable
Point2 = new Vector2(2f),
};
- private readonly InOutCubic bodyFade = new(TimeSpan.FromSeconds(1f))
+ private readonly InOutCubic bodyFade = new(TimeSpan.FromSeconds(1.3f))
{
Point1 = Vector2.Zero,
Point2 = Vector2.One,
};
+ private readonly InOutCubic titleFade = new(TimeSpan.FromSeconds(1f))
+ {
+ Point1 = Vector2.Zero,
+ Point2 = Vector2.One,
+ };
+
+ private readonly InOutCubic fadeOut = new(TimeSpan.FromSeconds(0.8f))
+ {
+ Point1 = Vector2.One,
+ Point2 = Vector2.Zero,
+ };
+
private State state = State.WindowFadeIn;
-
+
private bool needFadeRestart = false;
+
+ private bool isFadingOutForStateChange = false;
+ private State? stateAfterFadeOut;
+
+ private AutoUpdateBehavior autoUpdateBehavior = DefaultAutoUpdateBehavior;
///
/// Initializes a new instance of the class.
@@ -90,6 +112,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
WindowFadeIn,
ExplainerIntro,
ExplainerApiBump,
+ AskAutoUpdate,
Links,
}
@@ -114,12 +137,19 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.tsmWindow.AllowDrawing = false;
_ = this.bannerFont;
+
+ this.isFadingOutForStateChange = false;
+ this.stateAfterFadeOut = null;
this.state = State.WindowFadeIn;
this.windowFade.Reset();
this.bodyFade.Reset();
+ this.titleFade.Reset();
+ this.fadeOut.Reset();
this.needFadeRestart = true;
+ this.autoUpdateBehavior = DefaultAutoUpdateBehavior;
+
base.OnOpen();
}
@@ -130,6 +160,10 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.tsmWindow.AllowDrawing = true;
Service.Get().SetCreditsDarkeningAnimation(false);
+
+ var configuration = Service.Get();
+ configuration.AutoUpdateBehavior = this.autoUpdateBehavior;
+ configuration.QueueSave();
}
///
@@ -144,10 +178,13 @@ internal sealed class ChangelogWindow : Window, IDisposable
if (this.needFadeRestart)
{
this.windowFade.Restart();
+ this.titleFade.Restart();
this.needFadeRestart = false;
}
this.windowFade.Update();
+ this.titleFade.Update();
+ this.fadeOut.Update();
ImGui.SetNextWindowBgAlpha(Math.Clamp(this.windowFade.EasedPoint.X, 0, 0.9f));
this.Size = new Vector2(900, 400);
@@ -207,8 +244,9 @@ internal sealed class ChangelogWindow : Window, IDisposable
return;
ImGuiHelpers.ScaledDummy(20);
-
- using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f)))
+
+ var titleFadeVal = this.isFadingOutForStateChange ? this.fadeOut.EasedPoint.X : this.titleFade.EasedPoint.X;
+ using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(titleFadeVal, 0f, 1f)))
{
using var font = this.bannerFont.Value.Push();
@@ -223,6 +261,10 @@ internal sealed class ChangelogWindow : Window, IDisposable
ImGuiHelpers.CenteredText("Plugin Updates");
break;
+ case State.AskAutoUpdate:
+ ImGuiHelpers.CenteredText("Auto-Updates");
+ break;
+
case State.Links:
ImGuiHelpers.CenteredText("Enjoy!");
break;
@@ -236,10 +278,30 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.state = State.ExplainerIntro;
this.bodyFade.Restart();
}
+
+ if (this.isFadingOutForStateChange && this.fadeOut.IsDone)
+ {
+ this.state = this.stateAfterFadeOut ?? throw new Exception("State after fade out is null");
+
+ this.bodyFade.Restart();
+ this.titleFade.Restart();
+
+ this.isFadingOutForStateChange = false;
+ this.stateAfterFadeOut = null;
+ }
this.bodyFade.Update();
- using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.bodyFade.EasedPoint.X, 0, 1f)))
+ var bodyFadeVal = this.isFadingOutForStateChange ? this.fadeOut.EasedPoint.X : this.bodyFade.EasedPoint.X;
+ using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(bodyFadeVal, 0, 1f)))
{
+ void GoToNextState(State nextState)
+ {
+ this.isFadingOutForStateChange = true;
+ this.stateAfterFadeOut = nextState;
+
+ this.fadeOut.Restart();
+ }
+
void DrawNextButton(State nextState)
{
// Draw big, centered next button at the bottom of the window
@@ -249,10 +311,9 @@ internal sealed class ChangelogWindow : Window, IDisposable
ImGui.SetCursorPosY(windowSize.Y - buttonHeight - (20 * ImGuiHelpers.GlobalScale));
ImGuiHelpers.CenterCursorFor((int)buttonWidth);
- if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight)))
+ if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight)) && !this.isFadingOutForStateChange)
{
- this.state = nextState;
- this.bodyFade.Restart();
+ GoToNextState(nextState);
}
}
@@ -286,7 +347,65 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.apiBumpExplainerTexture.Value.ImGuiHandle,
this.apiBumpExplainerTexture.Value.Size);
- DrawNextButton(State.Links);
+ DrawNextButton(State.AskAutoUpdate);
+ break;
+
+ case State.AskAutoUpdate:
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateHint",
+ "Dalamud can update your plugins automatically, making sure that you always " +
+ "have the newest features and bug fixes. You can choose when and how auto-updates are run here."));
+ ImGuiHelpers.ScaledDummy(2);
+
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer1",
+ "You can always update your plugins manually by clicking the update button in the plugin list. " +
+ "You can also opt into updates for specific plugins by right-clicking them and selecting \"Always auto-update\"."));
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer2",
+ "Dalamud will never bother you about updates while you are not idle."));
+
+ ImGuiHelpers.ScaledDummy(15);
+
+ /*
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior",
+ "When the game starts..."));
+ var behaviorInt = (int)this.autoUpdateBehavior;
+ ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNone", "Do not check for updates automatically"), ref behaviorInt, (int)AutoUpdateBehavior.None);
+ ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNotify", "Only notify me of new updates"), ref behaviorInt, (int)AutoUpdateBehavior.OnlyNotify);
+ ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateMainRepo", "Auto-update main repository plugins"), ref behaviorInt, (int)AutoUpdateBehavior.UpdateMainRepo);
+ this.autoUpdateBehavior = (AutoUpdateBehavior)behaviorInt;
+ */
+
+ bool DrawCenteredButton(string text, float height)
+ {
+ var buttonHeight = height * ImGuiHelpers.GlobalScale;
+ var buttonWidth = ImGui.CalcTextSize(text).X + 50 * ImGuiHelpers.GlobalScale;
+ ImGuiHelpers.CenterCursorFor((int)buttonWidth);
+
+ return ImGui.Button(text, new Vector2(buttonWidth, buttonHeight)) &&
+ !this.isFadingOutForStateChange;
+ }
+
+ using (ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.DPSRed))
+ {
+ if (DrawCenteredButton("Enable auto-updates", 30))
+ {
+ this.autoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo;
+ GoToNextState(State.Links);
+ }
+ }
+
+ ImGuiHelpers.ScaledDummy(2);
+
+ using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1))
+ using (var buttonColor = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero))
+ {
+ buttonColor.Push(ImGuiCol.Border, ImGuiColors.DalamudGrey3);
+ if (DrawCenteredButton("Disable auto-updates", 25))
+ {
+ this.autoUpdateBehavior = AutoUpdateBehavior.OnlyNotify;
+ GoToNextState(State.Links);
+ }
+ }
+
break;
case State.Links:
@@ -356,12 +475,12 @@ internal sealed class ChangelogWindow : Window, IDisposable
// Draw close button in the top right corner
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 100f);
var btnAlpha = Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f);
- ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.DalamudRed.WithAlpha(btnAlpha).Desaturate(0.3f));
+ ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.DPSRed.WithAlpha(btnAlpha).Desaturate(0.3f));
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudWhite.WithAlpha(btnAlpha));
var childSize = ImGui.GetWindowSize();
var closeButtonSize = 15 * ImGuiHelpers.GlobalScale;
- ImGui.SetCursorPos(new Vector2(childSize.X - closeButtonSize - 5, 10 * ImGuiHelpers.GlobalScale));
+ ImGui.SetCursorPos(new Vector2(childSize.X - closeButtonSize - (10 * ImGuiHelpers.GlobalScale), 10 * ImGuiHelpers.GlobalScale));
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{
Dismiss();
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
index d0dc01ce5..379485517 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
@@ -7,12 +7,12 @@ using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiNotification.Internal;
+using Dalamud.Interface.Internal.DesignSystem;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Profiles;
-using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
@@ -300,39 +300,16 @@ internal class ProfileManagerWidget
return;
}
- const string addPluginToProfilePopup = "###addPluginToProfile";
- var addPluginToProfilePopupId = ImGui.GetID(addPluginToProfilePopup);
- using (var popup = ImRaii.Popup(addPluginToProfilePopup))
- {
- if (popup.Success)
+ var addPluginToProfilePopupId = DalamudComponents.DrawPluginPicker(
+ "###addPluginToProfilePicker",
+ ref this.pickerSearch,
+ plugin =>
{
- var width = ImGuiHelpers.GlobalScale * 300;
-
- using var disabled = ImRaii.Disabled(profman.IsBusy);
-
- ImGui.SetNextItemWidth(width);
- ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref this.pickerSearch, 255);
-
- if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
- {
- // TODO: Plugin searching should be abstracted... installer and this should use the same search
- foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles &&
- (this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant()))))
- {
- using var disabled2 =
- ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName));
-
- if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}"))
- {
- Task.Run(() => profile.AddOrUpdateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, true, false))
- .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
- }
- }
-
- ImGui.EndListBox();
- }
- }
- }
+ Task.Run(() => profile.AddOrUpdateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, true, false))
+ .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
+ },
+ plugin => !plugin.Manifest.SupportsProfiles ||
+ profile.Plugins.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId));
var didAny = false;
@@ -603,8 +580,6 @@ internal class ProfileManagerWidget
public static string BackToOverview => Loc.Localize("ProfileManagerBackToOverview", "Back to overview");
- public static string SearchHint => Loc.Localize("ProfileManagerSearchHint", "Search...");
-
public static string AddProfileHint => Loc.Localize("ProfileManagerAddProfileHint", "No collections! Add one!");
public static string CloneProfileHint => Loc.Localize("ProfileManagerCloneProfile", "Clone this collection");
diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
index fd4949533..196a11ab1 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
@@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings;
///
internal class SettingsWindow : Window
{
- private SettingsTab[]? tabs;
+ private readonly SettingsTab[] tabs;
private string searchInput = string.Empty;
private bool isSearchInputPrefilled = false;
@@ -42,6 +42,16 @@ internal class SettingsWindow : Window
};
this.SizeCondition = ImGuiCond.FirstUseEver;
+
+ this.tabs =
+ [
+ new SettingsTabGeneral(),
+ new SettingsTabLook(),
+ new SettingsTabAutoUpdates(),
+ new SettingsTabDtr(),
+ new SettingsTabExperimental(),
+ new SettingsTabAbout()
+ ];
}
///
@@ -75,15 +85,6 @@ internal class SettingsWindow : Window
///
public override void OnOpen()
{
- this.tabs ??= new SettingsTab[]
- {
- new SettingsTabGeneral(),
- new SettingsTabLook(),
- new SettingsTabDtr(),
- new SettingsTabExperimental(),
- new SettingsTabAbout(),
- };
-
foreach (var settingsTab in this.tabs)
{
settingsTab.Load();
@@ -142,7 +143,7 @@ internal class SettingsWindow : Window
flags |= ImGuiTabItemFlags.SetSelected;
this.setActiveTab = null;
}
-
+
using var tab = ImRaii.TabItem(settingsTab.Title, flags);
if (tab)
{
@@ -152,10 +153,14 @@ internal class SettingsWindow : Window
settingsTab.OnOpen();
}
+ // Don't add padding for the about tab(credits)
+ using var padding = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(2, 2),
+ settingsTab is not SettingsTabAbout);
+ using var borderColor = ImRaii.PushColor(ImGuiCol.Border, ImGui.GetColorU32(ImGuiCol.ChildBg));
using var tabChild = ImRaii.Child(
$"###settings_scrolling_{settingsTab.Title}",
new Vector2(-1, -1),
- false);
+ true);
if (tabChild)
settingsTab.Draw();
}
@@ -281,25 +286,15 @@ internal class SettingsWindow : Window
private void SetOpenTab(SettingsOpenKind kind)
{
- switch (kind)
+ this.setActiveTab = kind switch
{
- case SettingsOpenKind.General:
- this.setActiveTab = this.tabs[0];
- break;
- case SettingsOpenKind.LookAndFeel:
- this.setActiveTab = this.tabs[1];
- break;
- case SettingsOpenKind.ServerInfoBar:
- this.setActiveTab = this.tabs[2];
- break;
- case SettingsOpenKind.Experimental:
- this.setActiveTab = this.tabs[3];
- break;
- case SettingsOpenKind.About:
- this.setActiveTab = this.tabs[4];
- break;
- default:
- throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
- }
+ SettingsOpenKind.General => this.tabs[0],
+ SettingsOpenKind.LookAndFeel => this.tabs[1],
+ SettingsOpenKind.AutoUpdates => this.tabs[2],
+ SettingsOpenKind.ServerInfoBar => this.tabs[3],
+ SettingsOpenKind.Experimental => this.tabs[4],
+ SettingsOpenKind.About => this.tabs[5],
+ _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
+ };
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs
new file mode 100644
index 000000000..0b18e59d9
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs
@@ -0,0 +1,254 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Numerics;
+
+using CheapLoc;
+using Dalamud.Configuration.Internal;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Internal.DesignSystem;
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.AutoUpdate;
+using Dalamud.Plugin.Internal.Types;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
+
+[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
+public class SettingsTabAutoUpdates : SettingsTab
+{
+ private AutoUpdateBehavior behavior;
+ private bool checkPeriodically;
+ private string pickerSearch = string.Empty;
+ private List autoUpdatePreferences = [];
+
+ public override SettingsEntry[] Entries { get; } = Array.Empty();
+
+ public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates");
+
+ public override void Draw()
+ {
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateHint",
+ "Dalamud can update your plugins automatically, making sure that you always " +
+ "have the newest features and bug fixes. You can choose when and how auto-updates are run here."));
+ ImGuiHelpers.ScaledDummy(2);
+
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer1",
+ "You can always update your plugins manually by clicking the update button in the plugin list. " +
+ "You can also opt into updates for specific plugins by right-clicking them and selecting \"Always auto-update\"."));
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer2",
+ "Dalamud will never bother you about updates while you are not idle."));
+
+ ImGuiHelpers.ScaledDummy(8);
+
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior",
+ "When the game starts..."));
+ var behaviorInt = (int)this.behavior;
+ ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNone", "Do not check for updates automatically"), ref behaviorInt, (int)AutoUpdateBehavior.None);
+ ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateNotify", "Only notify me of new updates"), ref behaviorInt, (int)AutoUpdateBehavior.OnlyNotify);
+ ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateMainRepo", "Auto-update main repository plugins"), ref behaviorInt, (int)AutoUpdateBehavior.UpdateMainRepo);
+ ImGui.RadioButton(Loc.Localize("DalamudSettingsAutoUpdateAll", "Auto-update all plugins"), ref behaviorInt, (int)AutoUpdateBehavior.UpdateAll);
+ this.behavior = (AutoUpdateBehavior)behaviorInt;
+
+ if (this.behavior == AutoUpdateBehavior.UpdateAll)
+ {
+ var warning = Loc.Localize(
+ "DalamudSettingsAutoUpdateAllWarning",
+ "Warning: This will update all plugins, including those not from the main repository.\n" +
+ "These updates are not reviewed by the Dalamud team and may contain malicious code.");
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudOrange, warning);
+ }
+
+ ImGuiHelpers.ScaledDummy(8);
+
+ ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically);
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint",
+ "Plugins won't update automatically after startup, you will only receive a notification while you are not actively playing."));
+
+ ImGuiHelpers.ScaledDummy(5);
+ ImGui.Separator();
+ ImGuiHelpers.ScaledDummy(5);
+
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOptedIn",
+ "Per-plugin overrides"));
+
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOverrideHint",
+ "Here, you can choose to receive or not to receive updates for specific plugins. " +
+ "This will override the settings above for the selected plugins."));
+
+ if (this.autoUpdatePreferences.Count == 0)
+ {
+ ImGuiHelpers.ScaledDummy(20);
+
+ using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
+ {
+ ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2",
+ "You did not override auto-updates for any plugins yet."));
+ }
+
+ ImGuiHelpers.ScaledDummy(2);
+ }
+ else
+ {
+ ImGuiHelpers.ScaledDummy(5);
+
+ var pic = Service.Get();
+
+ var windowSize = ImGui.GetWindowSize();
+ var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
+ Guid? wantRemovePluginGuid = null;
+
+ foreach (var preference in this.autoUpdatePreferences)
+ {
+ var pmPlugin = Service.Get().InstalledPlugins
+ .FirstOrDefault(x => x.EffectiveWorkingPluginId == preference.WorkingPluginId);
+
+ var btnOffset = 2;
+
+ if (pmPlugin != null)
+ {
+ var cursorBeforeIcon = ImGui.GetCursorPos();
+ pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon, out _);
+ icon ??= pic.DefaultIcon;
+
+ ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
+
+ if (pmPlugin.IsDev)
+ {
+ ImGui.SetCursorPos(cursorBeforeIcon);
+ ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f);
+ ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight));
+ ImGui.PopStyleVar();
+ }
+
+ ImGui.SameLine();
+
+ var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}";
+ var textHeight = ImGui.CalcTextSize(text);
+ var before = ImGui.GetCursorPos();
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
+ ImGui.TextUnformatted(text);
+
+ ImGui.SetCursorPos(before);
+ }
+ else
+ {
+ ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight));
+ ImGui.SameLine();
+
+ var text = Loc.Localize("DalamudSettingsAutoUpdateOptInUnknownPlugin", "Unknown plugin");
+ var textHeight = ImGui.CalcTextSize(text);
+ var before = ImGui.GetCursorPos();
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
+ ImGui.TextUnformatted(text);
+
+ ImGui.SetCursorPos(before);
+ }
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 320));
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
+
+ string OptKindToString(AutoUpdatePreference.OptKind kind)
+ {
+ return kind switch
+ {
+ AutoUpdatePreference.OptKind.NeverUpdate => Loc.Localize("DalamudSettingsAutoUpdateOptInNeverUpdate", "Never update this"),
+ AutoUpdatePreference.OptKind.AlwaysUpdate => Loc.Localize("DalamudSettingsAutoUpdateOptInAlwaysUpdate", "Always update this"),
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+ }
+
+ ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 250);
+ if (ImGui.BeginCombo(
+ $"###autoUpdateBehavior{preference.WorkingPluginId}",
+ OptKindToString(preference.Kind)))
+ {
+ foreach (var kind in Enum.GetValues())
+ {
+ if (ImGui.Selectable(OptKindToString(kind)))
+ {
+ preference.Kind = kind;
+ }
+ }
+
+ ImGui.EndCombo();
+ }
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5);
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
+
+ if (ImGuiComponents.IconButton($"###removePlugin{preference.WorkingPluginId}", FontAwesomeIcon.Trash))
+ {
+ wantRemovePluginGuid = preference.WorkingPluginId;
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Loc.Localize("DalamudSettingsAutoUpdateOptInRemove", "Remove this override"));
+ }
+
+ if (wantRemovePluginGuid != null)
+ {
+ this.autoUpdatePreferences.RemoveAll(x => x.WorkingPluginId == wantRemovePluginGuid);
+ }
+ }
+
+ void OnPluginPicked(LocalPlugin plugin)
+ {
+ var id = plugin.EffectiveWorkingPluginId;
+ if (id == Guid.Empty)
+ throw new InvalidOperationException("Plugin ID is empty.");
+
+ this.autoUpdatePreferences.Add(new AutoUpdatePreference(id));
+ }
+
+ bool IsPluginDisabled(LocalPlugin plugin)
+ => this.autoUpdatePreferences.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId);
+
+ bool IsPluginFiltered(LocalPlugin plugin)
+ => !plugin.IsDev;
+
+ var pickerId = DalamudComponents.DrawPluginPicker(
+ "###autoUpdatePicker", ref this.pickerSearch, OnPluginPicked, IsPluginDisabled, IsPluginFiltered);
+
+ const FontAwesomeIcon addButtonIcon = FontAwesomeIcon.Plus;
+ var addButtonText = Loc.Localize("DalamudSettingsAutoUpdateOptInAdd", "Add new override");
+ ImGuiHelpers.CenterCursorFor(ImGuiComponents.GetIconButtonWithTextWidth(addButtonIcon, addButtonText));
+ if (ImGuiComponents.IconButtonWithText(addButtonIcon, addButtonText))
+ {
+ this.pickerSearch = string.Empty;
+ ImGui.OpenPopup(pickerId);
+ }
+
+ base.Draw();
+ }
+
+ public override void Load()
+ {
+ var configuration = Service.Get();
+
+ this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
+ this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
+ this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
+
+ base.Load();
+ }
+
+ public override void Save()
+ {
+ var configuration = Service.Get();
+
+ configuration.AutoUpdateBehavior = this.behavior;
+ configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
+ configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;
+
+ base.Save();
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs
index ea345e9cf..c96163835 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs
@@ -3,6 +3,7 @@
using CheapLoc;
using Dalamud.Game.Text;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
+using Dalamud.Plugin.Internal.AutoUpdate;
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
@@ -20,9 +21,8 @@ public class SettingsTabGeneral : SettingsTab
Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."),
c => c.GeneralChatType,
(v, c) => c.GeneralChatType = v,
- warning: (v) =>
+ validity: (v) =>
{
- // TODO: Maybe actually implement UI for the validity check...
if (v == XivChatType.None)
return Loc.Localize("DalamudSettingsChannelNone", "Do not pick \"None\".");
@@ -62,12 +62,6 @@ public class SettingsTabGeneral : SettingsTab
c => c.PrintPluginsWelcomeMsg,
(v, c) => c.PrintPluginsWelcomeMsg = v),
- new SettingsEntry(
- Loc.Localize("DalamudSettingsAutoUpdatePlugins", "Auto-update plugins"),
- Loc.Localize("DalamudSettingsAutoUpdatePluginsMsgHint", "Automatically update plugins when logging in with a character."),
- c => c.AutoUpdatePlugins,
- (v, c) => c.AutoUpdatePlugins = v),
-
new SettingsEntry(
Loc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"),
Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."),
diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs
index d53c620f4..cf56ee0fd 100644
--- a/Dalamud/Plugin/DalamudPluginInterface.cs
+++ b/Dalamud/Plugin/DalamudPluginInterface.cs
@@ -9,7 +9,6 @@ using System.Reflection;
using Dalamud.Configuration;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
-using Dalamud.Game;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.Sanitizer;
@@ -20,6 +19,7 @@ using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings;
using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Plugin.Ipc;
@@ -27,8 +27,6 @@ using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Ipc.Internal;
using Dalamud.Utility;
-using static Dalamud.Interface.Internal.Windows.PluginInstaller.PluginInstallerWindow;
-
namespace Dalamud.Plugin;
///
@@ -114,7 +112,7 @@ public sealed class DalamudPluginInterface : IDisposable
///
/// Gets a value indicating whether or not auto-updates have already completed this session.
///
- public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete;
+ public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete;
///
/// Gets the repository from which this plugin was installed.
diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs
new file mode 100644
index 000000000..2d5dec970
--- /dev/null
+++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs
@@ -0,0 +1,27 @@
+namespace Dalamud.Plugin.Internal.AutoUpdate;
+
+///
+/// Enum describing how plugins should be auto-updated at startup-.
+///
+internal enum AutoUpdateBehavior
+{
+ ///
+ /// Plugins should not be updated and the user should not be notified.
+ ///
+ None,
+
+ ///
+ /// The user should merely be notified about updates.
+ ///
+ OnlyNotify,
+
+ ///
+ /// Only plugins from the main repository should be updated.
+ ///
+ UpdateMainRepo,
+
+ ///
+ /// All plugins should be updated.
+ ///
+ UpdateAll,
+}
diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
new file mode 100644
index 000000000..3e12ef600
--- /dev/null
+++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
@@ -0,0 +1,439 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Console;
+using Dalamud.Game;
+using Dalamud.Game.ClientState;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Interface;
+using Dalamud.Interface.ImGuiNotification;
+using Dalamud.Interface.ImGuiNotification.Internal;
+using Dalamud.Interface.Internal;
+using Dalamud.Interface.Internal.DesignSystem;
+using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Interface.Utility;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Services;
+
+using ImGuiNET;
+
+namespace Dalamud.Plugin.Internal.AutoUpdate;
+
+// TODO: Loc
+
+///
+/// Class to manage automatic updates for plugins.
+///
+[ServiceManager.EarlyLoadedService]
+internal class AutoUpdateManager : IServiceType
+{
+ private static readonly ModuleLog Log = new("AUTOUPDATE");
+
+ ///
+ /// Time we should wait after login to update.
+ ///
+ private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20);
+
+ ///
+ /// Time we should wait between scheduled update checks.
+ ///
+ private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(1.5);
+
+ ///
+ /// Time we should wait after unblocking to nag the user.
+ /// Used to prevent spamming a nag, for example, right after an user leaves a duty.
+ ///
+ private static readonly TimeSpan CooldownAfterUnblock = TimeSpan.FromSeconds(30);
+
+ [ServiceManager.ServiceDependency]
+ private readonly PluginManager pluginManager = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly DalamudConfiguration config = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly NotificationManager notificationManager = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly DalamudInterface dalamudInterface = Service.Get();
+
+ private readonly IConsoleVariable isDryRun;
+
+ private DateTime? loginTime;
+ private DateTime? lastUpdateCheckTime;
+ private DateTime? unblockedSince;
+
+ private bool hasStartedInitialUpdateThisSession;
+
+ private IActiveNotification? updateNotification;
+
+ private Task? autoUpdateTask;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Console service.
+ [ServiceManager.ServiceConstructor]
+ public AutoUpdateManager(ConsoleManager console)
+ {
+ Service.GetAsync().ContinueWith(
+ t =>
+ {
+ t.Result.Login += this.OnLogin;
+ t.Result.Logout += this.OnLogout;
+ });
+ Service.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; });
+
+ this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", true);
+ console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () =>
+ {
+ this.hasStartedInitialUpdateThisSession = false;
+ this.OnLogin();
+ return true;
+ });
+ console.AddCommand("dalamud.autoupdate.force_check", "Force a check for updates", () =>
+ {
+ this.lastUpdateCheckTime = DateTime.Now - TimeBetweenUpdateChecks;
+ return true;
+ });
+ }
+
+ private enum UpdateListingRestriction
+ {
+ Unrestricted,
+ AllowNone,
+ AllowMainRepo,
+ }
+
+ ///
+ /// Gets a value indicating whether or not auto-updates have already completed this session.
+ ///
+ public bool IsAutoUpdateComplete { get; private set; }
+
+ private static UpdateListingRestriction DecideUpdateListingRestriction(AutoUpdateBehavior behavior)
+ {
+ return behavior switch
+ {
+ // We don't generally allow any updates in this mode, but specific opt-ins.
+ AutoUpdateBehavior.None => UpdateListingRestriction.AllowNone,
+
+ // If we're only notifying, I guess it's fine to list all plugins.
+ AutoUpdateBehavior.OnlyNotify => UpdateListingRestriction.Unrestricted,
+
+ AutoUpdateBehavior.UpdateMainRepo => UpdateListingRestriction.AllowMainRepo,
+ AutoUpdateBehavior.UpdateAll => UpdateListingRestriction.Unrestricted,
+ _ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null),
+ };
+ }
+
+ private void OnUpdate(IFramework framework)
+ {
+ if (this.loginTime == null)
+ return;
+
+ var autoUpdateTaskInProgress = this.autoUpdateTask is not null && !this.autoUpdateTask.IsCompleted;
+ var isUnblocked = this.CanUpdateOrNag() && !autoUpdateTaskInProgress;
+
+ if (this.unblockedSince == null && isUnblocked)
+ {
+ this.unblockedSince = DateTime.Now;
+ }
+ else if (this.unblockedSince != null && !isUnblocked)
+ {
+ this.unblockedSince = null;
+
+ // Remove all notifications if we're not actively updating. The user probably doesn't care now.
+ if (this.updateNotification != null && !autoUpdateTaskInProgress)
+ {
+ this.updateNotification.DismissNow();
+ this.updateNotification = null;
+ }
+ }
+
+ // If we're blocked, we don't do anything.
+ if (!isUnblocked)
+ return;
+
+ var isInUnblockedCooldown =
+ this.unblockedSince != null && DateTime.Now - this.unblockedSince < CooldownAfterUnblock;
+
+ // If we're in the unblock cooldown period, we don't nag the user. This is intended to prevent us
+ // from showing update notifications right after the user leaves a duty, for example.
+ if (isInUnblockedCooldown && this.hasStartedInitialUpdateThisSession)
+ return;
+
+ var behavior = this.config.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
+
+ // 1. This is the initial update after login. We only run this exactly once and this is
+ // the only time we actually install updates automatically.
+ if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin))
+ {
+ this.lastUpdateCheckTime = DateTime.Now;
+ this.hasStartedInitialUpdateThisSession = true;
+
+ var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior));
+
+ if (currentlyUpdatablePlugins.Count == 0)
+ {
+ this.IsAutoUpdateComplete = true;
+ return;
+ }
+
+ // TODO: This is not 100% what we want... Plugins that are opted-in should be updated regardless of the behavior,
+ // and we should show a notification for the others afterwards.
+ if (behavior == AutoUpdateBehavior.OnlyNotify)
+ {
+ // List all plugins in the notification
+ Log.Verbose("Ran initial update, notifying for {Num} plugins", currentlyUpdatablePlugins.Count);
+ this.NotifyUpdatesAreAvailable(currentlyUpdatablePlugins);
+ return;
+ }
+
+ Log.Verbose("Ran initial update, updating {Num} plugins", currentlyUpdatablePlugins.Count);
+ this.KickOffAutoUpdates(currentlyUpdatablePlugins);
+ return;
+ }
+
+ // 2. Continuously check for updates while the game is running. We run these every once in a while and
+ // will only show a notification here that lets people start the update or open the installer.
+ if (this.config.CheckPeriodicallyForUpdates &&
+ this.lastUpdateCheckTime != null &&
+ DateTime.Now - this.lastUpdateCheckTime > TimeBetweenUpdateChecks &&
+ this.updateNotification == null)
+ {
+ this.pluginManager.ReloadPluginMastersAsync()
+ .ContinueWith(
+ t =>
+ {
+ if (t.IsFaulted || t.IsCanceled)
+ {
+ Log.Error(t.Exception!, "Failed to reload plugin masters for auto-update");
+ }
+
+ this.NotifyUpdatesAreAvailable(
+ this.GetAvailablePluginUpdates(
+ DecideUpdateListingRestriction(behavior)));
+ });
+
+ this.lastUpdateCheckTime = DateTime.Now;
+ }
+ }
+
+ private IActiveNotification GetBaseNotification(Notification notification)
+ {
+ if (this.updateNotification != null)
+ throw new InvalidOperationException("Already showing a notification");
+
+ this.updateNotification = this.notificationManager.AddNotification(notification);
+ this.updateNotification.Dismiss += _ => this.updateNotification = null;
+
+ return this.updateNotification!;
+ }
+
+ private void KickOffAutoUpdates(ICollection updatablePlugins)
+ {
+ this.autoUpdateTask =
+ Task.Run(() => this.RunAutoUpdates(updatablePlugins))
+ .ContinueWith(t =>
+ {
+ if (t.IsFaulted)
+ {
+ Log.Error(t.Exception!, "Failed to run auto-updates");
+ }
+ else if (t.IsCanceled)
+ {
+ Log.Warning("Auto-update task was canceled");
+ }
+
+ this.autoUpdateTask = null;
+ this.IsAutoUpdateComplete = true;
+ });
+ }
+
+ private async Task RunAutoUpdates(ICollection updatablePlugins)
+ {
+ var pluginStates = new List();
+
+ Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count);
+
+ if (updatablePlugins.Count == 0)
+ return;
+
+ var notification = this.GetBaseNotification(new Notification
+ {
+ Title = "Updating plugins...",
+ Content = $"Preparing to update {updatablePlugins.Count} plugins...",
+ Type = NotificationType.Info,
+ InitialDuration = TimeSpan.MaxValue,
+ ShowIndeterminateIfNoExpiry = false,
+ UserDismissable = false,
+ Progress = 0,
+ Icon = INotificationIcon.From(FontAwesomeIcon.Download),
+ Minimized = false,
+ });
+
+ var numDone = 0;
+ // TODO: This is NOT correct, we need to do this inside PM to be able to avoid notifying for each of these and avoid
+ // refreshing the plugin list until we're done. See PluginManager::UpdatePluginsAsync().
+ // Maybe have a function in PM that can take a list of AvailablePluginUpdate instead and update them all,
+ // and get rid of UpdatePluginsAsync()? Will have to change the installer a bit but that might be for the better API-wise.
+ foreach (var plugin in updatablePlugins)
+ {
+ try
+ {
+ notification.Content = $"Updating {plugin.InstalledPlugin.Manifest.Name}...";
+ notification.Progress = (float)numDone / updatablePlugins.Count;
+
+ if (this.isDryRun.Value)
+ {
+ await Task.Delay(5000);
+ }
+
+ var status = await this.pluginManager.UpdateSinglePluginAsync(plugin, true, this.isDryRun.Value);
+ pluginStates.Add(status);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to auto-update plugin {PluginName}", plugin.InstalledPlugin.Manifest.Name);
+ }
+
+ numDone++;
+ }
+
+ notification.Progress = 1;
+ notification.UserDismissable = true;
+ notification.HardExpiry = DateTime.Now.AddSeconds(30);
+
+ notification.DrawActions += _ =>
+ {
+ ImGuiHelpers.ScaledDummy(2);
+ if (DalamudComponents.PrimaryButton("Open Plugin Installer"))
+ {
+ Service.Get().OpenPluginInstaller();
+ notification.DismissNow();
+ }
+ };
+
+ // Update the notification to show the final state
+ if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success))
+ {
+ notification.Minimized = true;
+
+ // Janky way to make sure the notification does not change before it's minimized...
+ await Task.Delay(500);
+
+ notification.Title = "Updates successful!";
+ notification.MinimizedText = "Plugins updated successfully.";
+ notification.Type = NotificationType.Success;
+ notification.Content = "All plugins have been updated successfully.";
+ }
+ else
+ {
+ notification.Title = "Updates failed!";
+ notification.Title = "Plugins failed to update.";
+ notification.Type = NotificationType.Error;
+ notification.Content = "Some plugins failed to update. Please check the plugin installer for more information.";
+
+ var failedPlugins = pluginStates
+ .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success)
+ .Select(x => x.Name).ToList();
+
+ notification.Content += $"\nFailed plugins: {string.Join(", ", failedPlugins)}";
+ }
+ }
+
+ private void NotifyUpdatesAreAvailable(ICollection updatablePlugins)
+ {
+ if (updatablePlugins.Count == 0)
+ return;
+
+ var notification = this.GetBaseNotification(new Notification
+ {
+ Title = "Updates available!",
+ Content = $"There are {updatablePlugins.Count} plugins that can be updated.",
+ Type = NotificationType.Info,
+ InitialDuration = TimeSpan.MaxValue,
+ ShowIndeterminateIfNoExpiry = false,
+ Icon = INotificationIcon.From(FontAwesomeIcon.Download),
+ });
+
+ notification.DrawActions += _ =>
+ {
+ ImGuiHelpers.ScaledDummy(2);
+
+ if (DalamudComponents.PrimaryButton("Update"))
+ {
+ this.KickOffAutoUpdates(updatablePlugins);
+ notification.DismissNow();
+ }
+
+ ImGui.SameLine();
+ if (DalamudComponents.SecondaryButton("Open installer"))
+ {
+ Service.Get().OpenPluginInstaller();
+ notification.DismissNow();
+ }
+ };
+ }
+
+ private List GetAvailablePluginUpdates(UpdateListingRestriction restriction)
+ {
+ var optIns = this.config.PluginAutoUpdatePreferences.ToArray();
+
+ // Get all of our updatable plugins and do some initial filtering that must apply to all plugins.
+ var updateablePlugins = this.pluginManager.UpdatablePlugins
+ .Where(
+ p =>
+ !p.InstalledPlugin.IsDev && // Never update dev-plugins
+ p.InstalledPlugin.IsWantedByAnyProfile && // Never update plugins that are not wanted by any profile(not enabled)
+ !p.InstalledPlugin.Manifest.ScheduledForDeletion); // Never update plugins that we want to get rid of
+
+ return updateablePlugins.Where(FilterPlugin).ToList();
+
+ bool FilterPlugin(AvailablePluginUpdate availablePluginUpdate)
+ {
+ var optIn = optIns.FirstOrDefault(x => x.WorkingPluginId == availablePluginUpdate.InstalledPlugin.EffectiveWorkingPluginId);
+
+ // If this is an opt-out, we don't update.
+ if (optIn is { Kind: AutoUpdatePreference.OptKind.NeverUpdate })
+ return false;
+
+ if (restriction == UpdateListingRestriction.AllowNone && optIn is not { Kind: AutoUpdatePreference.OptKind.AlwaysUpdate })
+ return false;
+
+ if (restriction == UpdateListingRestriction.AllowMainRepo && availablePluginUpdate.InstalledPlugin.IsThirdParty)
+ return false;
+
+ return true;
+ }
+ }
+
+ private void OnLogin()
+ {
+ this.loginTime = DateTime.Now;
+ }
+
+ private void OnLogout()
+ {
+ this.loginTime = null;
+ }
+
+ private bool CanUpdateOrNag()
+ {
+ var condition = Service.Get();
+ return this.IsPluginManagerReady() &&
+ !this.dalamudInterface.IsPluginInstallerOpen &&
+ condition.Only(ConditionFlag.NormalConditions,
+ ConditionFlag.Jumping,
+ ConditionFlag.Mounted,
+ ConditionFlag.UsingParasol);
+ }
+
+ private bool IsPluginManagerReady()
+ {
+ return this.pluginManager.ReposReady && this.pluginManager.PluginsReady && !this.pluginManager.SafeMode;
+ }
+}