From bc0bea03e0ee02f9670f27d3f7bb7267272e36b6 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 12 Jun 2024 00:01:50 +0200 Subject: [PATCH 01/13] add ICondition.Only() to check for a set of flags --- .../Game/ClientState/Conditions/Condition.cs | 19 +++++++++++++++++++ Dalamud/Plugin/Services/ICondition.cs | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index d281d7aec..23778288e 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -1,3 +1,5 @@ +using System.Linq; + using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -98,6 +100,20 @@ internal sealed class Condition : IInternalDisposableService, ICondition return false; } + /// + public bool Only(params ConditionFlag[] flags) + { + for (var i = 0; i < MaxConditionEntries; i++) + { + if (this[i] && flags.All(f => (int)f != i)) + { + return false; + } + } + + return true; + } + private void Dispose(bool disposing) { if (this.isDisposed) @@ -181,6 +197,9 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition /// public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags); + + /// + public bool Only(params ConditionFlag[] flags) => this.conditionService.Only(flags); private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value); } diff --git a/Dalamud/Plugin/Services/ICondition.cs b/Dalamud/Plugin/Services/ICondition.cs index 9700cef5a..3b74c333c 100644 --- a/Dalamud/Plugin/Services/ICondition.cs +++ b/Dalamud/Plugin/Services/ICondition.cs @@ -51,4 +51,12 @@ public interface ICondition /// Whether any single provided flag is set. /// The condition flags to check. public bool Any(params ConditionFlag[] flags); + + /// + /// Check if none but the provided condition flags are set. + /// This is not an exclusive check, it will return true if the provided flags are the only ones set. + /// + /// The condition flags to check for. + /// Whether only flags passed in are set. + public bool Only(params ConditionFlag[] flags); } From c926a13848985bcfee55670217180b6aa7388241 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 00:53:25 +0200 Subject: [PATCH 02/13] tweak notification ux * Swap the "up" and "down" arrows to make more sense * Toggle collapse state when clicking, instead of dismissing --- .../ImGuiNotification/Internal/ActiveNotification.ImGui.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index fdccaab7b..16d58bea5 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -124,7 +124,7 @@ internal sealed partial class ActiveNotification if (this.Click is null) { if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - this.DismissNow(NotificationDismissReason.Manual); + this.Minimized = !this.Minimized; } else { @@ -277,12 +277,12 @@ internal sealed partial class ActiveNotification if (this.underlyingNotification.Minimized) { - if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) this.Minimized = false; } else { - if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) this.Minimized = true; } From 8d18940108068622b0a624e9fd19a369d8a054d8 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 01:00:50 +0200 Subject: [PATCH 03/13] initial implementation of new auto-update UX --- .../Internal/AutoUpdatePreference.cs | 42 ++ .../Internal/DalamudConfiguration.cs | 29 +- Dalamud/Console/ConsoleManagerPluginScoped.cs | 1 - Dalamud/Game/ChatHandlers.cs | 136 ------ Dalamud/Interface/DalamudWindowOpenKinds.cs | 5 + .../Interface/Internal/DalamudInterface.cs | 9 + .../DesignSystem/DalamudComponents.Buttons.cs | 56 +++ .../DalamudComponents.PluginPicker.cs | 79 ++++ .../DesignSystem/DalamudComponents.cs | 8 + .../Internal/Windows/ChangelogWindow.cs | 141 +++++- .../PluginInstaller/ProfileManagerWidget.cs | 45 +- .../Windows/Settings/SettingsWindow.cs | 57 ++- .../Settings/Tabs/SettingsTabAutoUpdate.cs | 254 ++++++++++ .../Settings/Tabs/SettingsTabGeneral.cs | 10 +- Dalamud/Plugin/DalamudPluginInterface.cs | 6 +- .../Internal/AutoUpdate/AutoUpdateBehavior.cs | 27 ++ .../Internal/AutoUpdate/AutoUpdateManager.cs | 439 ++++++++++++++++++ 17 files changed, 1115 insertions(+), 229 deletions(-) create mode 100644 Dalamud/Configuration/Internal/AutoUpdatePreference.cs create mode 100644 Dalamud/Interface/Internal/DesignSystem/DalamudComponents.Buttons.cs create mode 100644 Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs create mode 100644 Dalamud/Interface/Internal/DesignSystem/DalamudComponents.cs create mode 100644 Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs create mode 100644 Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs create mode 100644 Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs 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; + } +} From bc2edf765f1b81adc93e2e8bc9d34fb71b0a9d30 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 18:35:47 +0200 Subject: [PATCH 04/13] refactor UpdatePluginsAsync() to take a list of plugins to update instead --- .../PluginInstaller/PluginInstallerWindow.cs | 6 ++- .../Internal/AutoUpdate/AutoUpdateManager.cs | 42 +++++-------------- Dalamud/Plugin/Internal/PluginManager.cs | 41 +++++++++++++++--- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 29b0253b8..d45f0b5ea 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -711,8 +711,12 @@ internal class PluginInstallerWindow : Window, IDisposable { this.updateStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll; + + var toUpdate = this.pluginListUpdatable + .Where(x => x.InstalledPlugin.IsLoaded) + .ToList(); - Task.Run(() => pluginManager.UpdatePluginsAsync(true, false)) + Task.Run(() => pluginManager.UpdatePluginsAsync(toUpdate, false)) .ContinueWith(task => { this.updateStatus = OperationStatus.Complete; diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 3e12ef600..869f0c114 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -255,8 +255,6 @@ internal class AutoUpdateManager : IServiceType private async Task RunAutoUpdates(ICollection updatablePlugins) { - var pluginStates = new List(); - Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count); if (updatablePlugins.Count == 0) @@ -274,35 +272,16 @@ internal class AutoUpdateManager : IServiceType 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) + + var progress = new Progress(); + progress.ProgressChanged += (_, progress) => { - 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.Content = $"Updating {progress.CurrentPluginManifest.Name}..."; + notification.Progress = (float)progress.PluginsProcessed / progress.TotalPlugins; + }; + var pluginStates = await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress); + notification.Progress = 1; notification.UserDismissable = true; notification.HardExpiry = DateTime.Now.AddSeconds(30); @@ -318,7 +297,8 @@ internal class AutoUpdateManager : IServiceType }; // Update the notification to show the final state - if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success)) + var pluginUpdateStatusEnumerable = pluginStates as PluginUpdateStatus[] ?? pluginStates.ToArray(); + if (pluginUpdateStatusEnumerable.All(x => x.Status == PluginUpdateStatus.StatusKind.Success)) { notification.Minimized = true; @@ -337,7 +317,7 @@ internal class AutoUpdateManager : IServiceType notification.Type = NotificationType.Error; notification.Content = "Some plugins failed to update. Please check the plugin installer for more information."; - var failedPlugins = pluginStates + var failedPlugins = pluginUpdateStatusEnumerable .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success) .Select(x => x.Name).ToList(); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 7517ae413..60d2bbe28 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -977,32 +977,39 @@ internal class PluginManager : IInternalDisposableService /// /// Update all non-dev plugins. /// - /// Ignore disabled plugins. + /// List of plugins to update. /// Perform a dry run, don't install anything. /// If this action was performed as part of an auto-update. + /// An implementation to receive progress updates about the installation status. /// Success or failure and a list of updated plugin metadata. - public async Task> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false) + public async Task> UpdatePluginsAsync( + ICollection toUpdate, + bool dryRun, + bool autoUpdate = false, + IProgress? progress = null) { Log.Information("Starting plugin update"); var updateTasks = new List>(); + var totalPlugins = toUpdate.Count; + var processedPlugins = 0; // Prevent collection was modified errors lock (this.pluginListLock) { - foreach (var plugin in this.updatablePluginsList) + foreach (var plugin in toUpdate) { // Can't update that! if (plugin.InstalledPlugin.IsDev) continue; - if (!plugin.InstalledPlugin.IsWantedByAnyProfile && ignoreDisabled) + if (!plugin.InstalledPlugin.IsWantedByAnyProfile) continue; if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion) continue; - updateTasks.Add(this.UpdateSinglePluginAsync(plugin, false, dryRun)); + updateTasks.Add(UpdateSinglePluginWithProgressAsync(plugin)); } } @@ -1013,9 +1020,26 @@ internal class PluginManager : IInternalDisposableService autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, updatedList.Select(x => x.InternalName)); - Log.Information("Plugin update OK. {updateCount} plugins updated.", updatedList.Length); + Log.Information("Plugin update OK. {UpdateCount} plugins updated", updatedList.Length); return updatedList; + + async Task UpdateSinglePluginWithProgressAsync(AvailablePluginUpdate plugin) + { + var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun); + + // Update the progress + if (progress != null) + { + var newProcessedAmount = Interlocked.Increment(ref processedPlugins); + progress.Report(new PluginUpdateProgress( + newProcessedAmount, + totalPlugins, + plugin.InstalledPlugin.Manifest)); + } + + return result; + } } /// @@ -1832,6 +1856,11 @@ internal class PluginManager : IInternalDisposableService } } + /// + /// Class representing progress of an update operation. + /// + public record PluginUpdateProgress(int PluginsProcessed, int TotalPlugins, IPluginManifest CurrentPluginManifest); + /// /// Simple class that tracks the internal names and public names of plugins that we are planning to load at startup, /// and are still actively loading. From 3b4178082a11be8cba245b436e74f052c7c4317e Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 18:54:01 +0200 Subject: [PATCH 05/13] localize all new auto-update strings --- .../Internal/AutoUpdate/AutoUpdateManager.cs | 73 ++++++++++++++----- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 869f0c114..d6d1165c3 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Threading.Tasks; +using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Console; using Dalamud.Game; @@ -22,8 +24,6 @@ using ImGuiNET; namespace Dalamud.Plugin.Internal.AutoUpdate; -// TODO: Loc - /// /// Class to manage automatic updates for plugins. /// @@ -262,8 +262,8 @@ internal class AutoUpdateManager : IServiceType var notification = this.GetBaseNotification(new Notification { - Title = "Updating plugins...", - Content = $"Preparing to update {updatablePlugins.Count} plugins...", + Title = Locs.NotificationTitleUpdatingPlugins, + Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count), Type = NotificationType.Info, InitialDuration = TimeSpan.MaxValue, ShowIndeterminateIfNoExpiry = false, @@ -276,7 +276,7 @@ internal class AutoUpdateManager : IServiceType var progress = new Progress(); progress.ProgressChanged += (_, progress) => { - notification.Content = $"Updating {progress.CurrentPluginManifest.Name}..."; + notification.Content = Locs.NotificationContentUpdating(progress.CurrentPluginManifest.Name); notification.Progress = (float)progress.PluginsProcessed / progress.TotalPlugins; }; @@ -289,7 +289,7 @@ internal class AutoUpdateManager : IServiceType notification.DrawActions += _ => { ImGuiHelpers.ScaledDummy(2); - if (DalamudComponents.PrimaryButton("Open Plugin Installer")) + if (DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller)) { Service.Get().OpenPluginInstaller(); notification.DismissNow(); @@ -305,23 +305,23 @@ internal class AutoUpdateManager : IServiceType // 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.Title = Locs.NotificationTitleUpdatesSuccessful; + notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized; notification.Type = NotificationType.Success; - notification.Content = "All plugins have been updated successfully."; + notification.Content = Locs.NotificationContentUpdatesSuccessful; } else { - notification.Title = "Updates failed!"; - notification.Title = "Plugins failed to update."; + notification.Title = Locs.NotificationTitleUpdatesFailed; + notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized; notification.Type = NotificationType.Error; - notification.Content = "Some plugins failed to update. Please check the plugin installer for more information."; + notification.Content = Locs.NotificationContentUpdatesFailed; var failedPlugins = pluginUpdateStatusEnumerable .Where(x => x.Status != PluginUpdateStatus.StatusKind.Success) .Select(x => x.Name).ToList(); - notification.Content += $"\nFailed plugins: {string.Join(", ", failedPlugins)}"; + notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins); } } @@ -332,8 +332,9 @@ internal class AutoUpdateManager : IServiceType var notification = this.GetBaseNotification(new Notification { - Title = "Updates available!", - Content = $"There are {updatablePlugins.Count} plugins that can be updated.", + Title = Locs.NotificationTitleUpdatesAvailable, + Content = Locs.NotificationContentUpdatesAvailable(updatablePlugins.Count), + MinimizedText = Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count), Type = NotificationType.Info, InitialDuration = TimeSpan.MaxValue, ShowIndeterminateIfNoExpiry = false, @@ -344,14 +345,14 @@ internal class AutoUpdateManager : IServiceType { ImGuiHelpers.ScaledDummy(2); - if (DalamudComponents.PrimaryButton("Update")) + if (DalamudComponents.PrimaryButton(Locs.NotificationButtonUpdate)) { this.KickOffAutoUpdates(updatablePlugins); notification.DismissNow(); } ImGui.SameLine(); - if (DalamudComponents.SecondaryButton("Open installer")) + if (DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller)) { Service.Get().OpenPluginInstaller(); notification.DismissNow(); @@ -416,4 +417,42 @@ internal class AutoUpdateManager : IServiceType { return this.pluginManager.ReposReady && this.pluginManager.PluginsReady && !this.pluginManager.SafeMode; } + + private static class Locs + { + public static string NotificationButtonOpenPluginInstaller => Loc.Localize("AutoUpdateOpenPluginInstaller", "Open installer"); + + public static string NotificationButtonUpdate => Loc.Localize("AutoUpdateUpdate", "Update"); + + public static string NotificationTitleUpdatesAvailable => Loc.Localize("AutoUpdateUpdatesAvailable", "Updates available!"); + + public static string NotificationTitleUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessful", "Updates successful!"); + + public static string NotificationTitleUpdatingPlugins => Loc.Localize("AutoUpdateUpdatingPlugins", "Updating plugins..."); + + public static string NotificationTitleUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailed", "Updates failed!"); + + public static string NotificationContentUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessfulContent", "All plugins have been updated successfully."); + + public static string NotificationContentUpdatesSuccessfulMinimized => Loc.Localize("AutoUpdateUpdatesSuccessfulContentMinimized", "Plugins updated successfully."); + + public static string NotificationContentUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailedContent", "Some plugins failed to update. Please check the plugin installer for more information."); + + public static string NotificationContentUpdatesFailedMinimized => Loc.Localize("AutoUpdateUpdatesFailedContentMinimized", "Plugins failed to update."); + + public static string NotificationContentUpdatesAvailable(int numUpdates) + => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "There are {0} plugins that can be updated."), numUpdates); + + public static string NotificationContentUpdatesAvailableMinimized(int numUpdates) + => string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContent", "{0} updates available."), numUpdates); + + public static string NotificationContentPreparingToUpdate(int numPlugins) + => string.Format(Loc.Localize("AutoUpdatePreparingToUpdate", "Preparing to update {0} plugins..."), numPlugins); + + public static string NotificationContentUpdating(string name) + => string.Format(Loc.Localize("AutoUpdateUpdating", "Updating {0}..."), name); + + public static string NotificationContentFailedPlugins(IEnumerable failedPlugins) + => string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugins: {0}"), string.Join(", ", failedPlugins)); + } } From ddcf01d073900b7f3b1e6ac85660eb8306d1cd1a Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 18:57:52 +0200 Subject: [PATCH 06/13] undo odd validity change i did without thinking about it --- .../Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index c96163835..9c43f2f11 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs @@ -3,8 +3,6 @@ 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; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] @@ -21,8 +19,9 @@ 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, - validity: (v) => + warning: v => { + // TODO: Maybe actually implement UI for the validity check... if (v == XivChatType.None) return Loc.Localize("DalamudSettingsChannelNone", "Do not pick \"None\"."); From 31ba979a831bed903cb5619c29e47e325875b0ff Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 19:29:13 +0200 Subject: [PATCH 07/13] fix warnings --- .../Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index 9c43f2f11..d33bfacfb 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; + namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] From c4e31bc5f165173902048c6d5a2f4796142fd9fc Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 19:29:23 +0200 Subject: [PATCH 08/13] use kaz's api for conditions instead --- .../Game/ClientState/Conditions/Condition.cs | 33 ++++++++++++++++--- .../Internal/AutoUpdate/AutoUpdateManager.cs | 2 +- Dalamud/Plugin/Services/ICondition.cs | 28 ++++++++++++---- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 23778288e..faafe05e0 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Dalamud.IoC; @@ -101,17 +102,33 @@ internal sealed class Condition : IInternalDisposableService, ICondition } /// - public bool Only(params ConditionFlag[] flags) + public bool OnlyAny(params ConditionFlag[] other) { + var resultSet = this.AsReadOnlySet(); + return !resultSet.Except(other).Any(); + } + + /// + public bool OnlyAll(params ConditionFlag[] other) + { + var resultSet = this.AsReadOnlySet(); + return resultSet.SetEquals(other); + } + + /// + public IReadOnlySet AsReadOnlySet() + { + var result = new HashSet(); + for (var i = 0; i < MaxConditionEntries; i++) { - if (this[i] && flags.All(f => (int)f != i)) + if (this[i]) { - return false; + result.Add((ConditionFlag)i); } } - return true; + return result; } private void Dispose(bool disposing) @@ -191,6 +208,9 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition this.ConditionChange = null; } + + /// + public IReadOnlySet AsReadOnlySet() => this.conditionService.AsReadOnlySet(); /// public bool Any() => this.conditionService.Any(); @@ -199,7 +219,10 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags); /// - public bool Only(params ConditionFlag[] flags) => this.conditionService.Only(flags); + public bool OnlyAny(params ConditionFlag[] other) => this.conditionService.OnlyAny(other); + + /// + public bool OnlyAll(params ConditionFlag[] other) => this.conditionService.OnlyAll(other); private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value); } diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index d6d1165c3..ce7b5af9d 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -407,7 +407,7 @@ internal class AutoUpdateManager : IServiceType var condition = Service.Get(); return this.IsPluginManagerReady() && !this.dalamudInterface.IsPluginInstallerOpen && - condition.Only(ConditionFlag.NormalConditions, + condition.OnlyAny(ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, ConditionFlag.UsingParasol); diff --git a/Dalamud/Plugin/Services/ICondition.cs b/Dalamud/Plugin/Services/ICondition.cs index 3b74c333c..7215cdac6 100644 --- a/Dalamud/Plugin/Services/ICondition.cs +++ b/Dalamud/Plugin/Services/ICondition.cs @@ -1,4 +1,6 @@ -using Dalamud.Game.ClientState.Conditions; +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Conditions; namespace Dalamud.Plugin.Services; @@ -53,10 +55,24 @@ public interface ICondition public bool Any(params ConditionFlag[] flags); /// - /// Check if none but the provided condition flags are set. - /// This is not an exclusive check, it will return true if the provided flags are the only ones set. + /// Check that *only* any of the condition flags specified are set. Useful to test if the client is in one of any + /// of a few specific condiiton states. /// - /// The condition flags to check for. - /// Whether only flags passed in are set. - public bool Only(params ConditionFlag[] flags); + /// The array of flags to check. + /// Returns a bool. + public bool OnlyAny(params ConditionFlag[] other); + + /// + /// Check that *only* the specified flags are set. Unlike , this method requires that all the + /// specified flags are set and no others are present. + /// + /// The array of flags to check. + /// Returns a bool. + public bool OnlyAll(params ConditionFlag[] other); + + /// + /// Convert the conditions array to a set of all set condition flags. + /// + /// Returns a set. + public IReadOnlySet AsReadOnlySet(); } From 9baf0905ec06bd5f5f80eec51c92c9012c989bd5 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 19:40:11 +0200 Subject: [PATCH 09/13] don't ask for auto-updates twice in changelog --- .../Internal/Windows/ChangelogWindow.cs | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 613fc7d28..665ad0f55 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -17,6 +17,7 @@ using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Storage.Assets; using Dalamud.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows; @@ -26,8 +27,6 @@ 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 = @@ -77,7 +76,10 @@ internal sealed class ChangelogWindow : Window, IDisposable private bool isFadingOutForStateChange = false; private State? stateAfterFadeOut; - private AutoUpdateBehavior autoUpdateBehavior = DefaultAutoUpdateBehavior; + private AutoUpdateBehavior? chosenAutoUpdateBehavior; + + private int currentFtueLevel; + private int updatedFtueLevel; /// /// Initializes a new instance of the class. @@ -147,8 +149,10 @@ internal sealed class ChangelogWindow : Window, IDisposable this.titleFade.Reset(); this.fadeOut.Reset(); this.needFadeRestart = true; + + this.chosenAutoUpdateBehavior = null; - this.autoUpdateBehavior = DefaultAutoUpdateBehavior; + this.currentFtueLevel = Service.Get().SeenFtueLevel; base.OnOpen(); } @@ -162,7 +166,13 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(false); var configuration = Service.Get(); - configuration.AutoUpdateBehavior = this.autoUpdateBehavior; + + if (this.chosenAutoUpdateBehavior.HasValue) + { + configuration.AutoUpdateBehavior = this.chosenAutoUpdateBehavior.Value; + } + + configuration.SeenFtueLevel = this.updatedFtueLevel; configuration.QueueSave(); } @@ -302,7 +312,7 @@ internal sealed class ChangelogWindow : Window, IDisposable this.fadeOut.Restart(); } - void DrawNextButton(State nextState) + bool DrawNextButton(State nextState) { // Draw big, centered next button at the bottom of the window var buttonHeight = 30 * ImGuiHelpers.GlobalScale; @@ -314,7 +324,10 @@ internal sealed class ChangelogWindow : Window, IDisposable if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight)) && !this.isFadingOutForStateChange) { GoToNextState(nextState); + return true; } + + return false; } switch (this.state) @@ -347,7 +360,18 @@ internal sealed class ChangelogWindow : Window, IDisposable this.apiBumpExplainerTexture.Value.ImGuiHandle, this.apiBumpExplainerTexture.Value.Size); - DrawNextButton(State.AskAutoUpdate); + if (this.currentFtueLevel < FtueLevels.AutoUpdateBehavior) + { + if (DrawNextButton(State.AskAutoUpdate)) + { + this.updatedFtueLevel = this.currentFtueLevel = FtueLevels.AutoUpdateBehavior; + } + } + else + { + DrawNextButton(State.Links); + } + break; case State.AskAutoUpdate: @@ -364,16 +388,6 @@ internal sealed class ChangelogWindow : Window, IDisposable 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; @@ -388,7 +402,7 @@ internal sealed class ChangelogWindow : Window, IDisposable { if (DrawCenteredButton("Enable auto-updates", 30)) { - this.autoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo; + this.chosenAutoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo; GoToNextState(State.Links); } } @@ -401,7 +415,7 @@ internal sealed class ChangelogWindow : Window, IDisposable buttonColor.Push(ImGuiCol.Border, ImGuiColors.DalamudGrey3); if (DrawCenteredButton("Disable auto-updates", 25)) { - this.autoUpdateBehavior = AutoUpdateBehavior.OnlyNotify; + this.chosenAutoUpdateBehavior = AutoUpdateBehavior.OnlyNotify; GoToNextState(State.Links); } } @@ -503,4 +517,10 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } + + private static class FtueLevels + { + public const int Default = 1; + public const int AutoUpdateBehavior = 2; + } } From 408c8e5d02ae83cfc0f2e20ab90ce0193cdeffda Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 21:49:16 +0200 Subject: [PATCH 10/13] remove double-negative --- Dalamud/Interface/Internal/Windows/ChangelogWindow.cs | 2 +- .../Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 665ad0f55..16e70d9e7 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -384,7 +384,7 @@ internal sealed class ChangelogWindow : Window, IDisposable "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.")); + "Dalamud will only notify you about updates while you are idle.")); ImGuiHelpers.ScaledDummy(15); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 0b18e59d9..5ed2c528c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -41,7 +41,7 @@ public class SettingsTabAutoUpdates : SettingsTab "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.")); + "Dalamud will only notify you about updates while you are idle.")); ImGuiHelpers.ScaledDummy(8); From ef72ebc72c943d87470d734ec5379b756cb903ae Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 22:03:17 +0200 Subject: [PATCH 11/13] more wording changes --- .../Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 5ed2c528c..77c79c96d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -87,7 +87,7 @@ public class SettingsTabAutoUpdates : SettingsTab using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) { ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2", - "You did not override auto-updates for any plugins yet.")); + "You don't have auto-update rules for any plugins.")); } ImGuiHelpers.ScaledDummy(2); From 08a411728cc5728d72c8465943f83dd71241f625 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 22:08:30 +0200 Subject: [PATCH 12/13] use a map to track ftue levels per-feature instead --- .../Internal/DalamudConfiguration.cs | 5 ++--- .../Internal/Windows/ChangelogWindow.cs | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index e5348d999..ca029307d 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -80,10 +80,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public string? LastVersion { get; set; } = null; /// - /// Gets or sets a value indicating the last seen FTUE version. - /// Unused for now, added to prevent existing users from seeing level 0 FTUE. + /// Gets or sets a dictionary of seen FTUE levels. /// - public int SeenFtueLevel { get; set; } = 1; + public Dictionary SeenFtueLevels { get; set; } = new(); /// /// Gets or sets the last loaded Dalamud version. diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 16e70d9e7..906fda2e4 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -78,8 +79,7 @@ internal sealed class ChangelogWindow : Window, IDisposable private AutoUpdateBehavior? chosenAutoUpdateBehavior; - private int currentFtueLevel; - private int updatedFtueLevel; + private Dictionary currentFtueLevels = new(); /// /// Initializes a new instance of the class. @@ -152,7 +152,7 @@ internal sealed class ChangelogWindow : Window, IDisposable this.chosenAutoUpdateBehavior = null; - this.currentFtueLevel = Service.Get().SeenFtueLevel; + this.currentFtueLevels = Service.Get().SeenFtueLevels; base.OnOpen(); } @@ -172,7 +172,7 @@ internal sealed class ChangelogWindow : Window, IDisposable configuration.AutoUpdateBehavior = this.chosenAutoUpdateBehavior.Value; } - configuration.SeenFtueLevel = this.updatedFtueLevel; + configuration.SeenFtueLevels = this.currentFtueLevels; configuration.QueueSave(); } @@ -360,11 +360,11 @@ internal sealed class ChangelogWindow : Window, IDisposable this.apiBumpExplainerTexture.Value.ImGuiHandle, this.apiBumpExplainerTexture.Value.Size); - if (this.currentFtueLevel < FtueLevels.AutoUpdateBehavior) + if (!this.currentFtueLevels.TryGetValue(FtueLevels.AutoUpdate.Name, out var autoUpdateLevel) || autoUpdateLevel < FtueLevels.AutoUpdate.AutoUpdateInitial) { if (DrawNextButton(State.AskAutoUpdate)) { - this.updatedFtueLevel = this.currentFtueLevel = FtueLevels.AutoUpdateBehavior; + this.currentFtueLevels[FtueLevels.AutoUpdate.Name] = FtueLevels.AutoUpdate.AutoUpdateInitial; } } else @@ -520,7 +520,10 @@ internal sealed class ChangelogWindow : Window, IDisposable private static class FtueLevels { - public const int Default = 1; - public const int AutoUpdateBehavior = 2; + public static class AutoUpdate + { + public const string Name = "AutoUpdate"; + public const int AutoUpdateInitial = 1; + } } } From 13fc380dd515912d246273164b4c661b391aa0cf Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 15 Jun 2024 23:09:08 +0200 Subject: [PATCH 13/13] disable autoupdate dry run by default --- Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index ce7b5af9d..4e2179be8 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -87,7 +87,7 @@ internal class AutoUpdateManager : IServiceType }); Service.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; }); - this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", true); + this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", false); console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () => { this.hasStartedInitialUpdateThisSession = false;