initial implementation of new auto-update UX

This commit is contained in:
goat 2024-06-15 01:00:50 +02:00
parent c926a13848
commit 8d18940108
17 changed files with 1115 additions and 229 deletions

View file

@ -0,0 +1,42 @@
namespace Dalamud.Configuration.Internal;
/// <summary>
/// Class representing a plugin that has opted in to auto-updating.
/// </summary>
internal class AutoUpdatePreference
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoUpdatePreference"/> class.
/// </summary>
/// <param name="pluginId">The unique ID representing the plugin.</param>
public AutoUpdatePreference(Guid pluginId)
{
this.WorkingPluginId = pluginId;
}
/// <summary>
/// The kind of opt-in.
/// </summary>
public enum OptKind
{
/// <summary>
/// Never auto-update this plugin.
/// </summary>
NeverUpdate,
/// <summary>
/// Always auto-update this plugin, regardless of the user's settings.
/// </summary>
AlwaysUpdate,
}
/// <summary>
/// Gets or sets the unique ID representing the plugin.
/// </summary>
public Guid WorkingPluginId { get; set; }
/// <summary>
/// Gets or sets the type of opt-in.
/// </summary>
public OptKind Kind { get; set; } = OptKind.AlwaysUpdate;
}

View file

@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Style; using Dalamud.Interface.Style;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Storage; using Dalamud.Storage;
using Dalamud.Utility; using Dalamud.Utility;
@ -196,6 +196,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugins should be auto-updated. /// Gets or sets a value indicating whether or not plugins should be auto-updated.
/// </summary> /// </summary>
[Obsolete("Use AutoUpdateBehavior instead.")]
public bool AutoUpdatePlugins { get; set; } public bool AutoUpdatePlugins { get; set; }
/// <summary> /// <summary>
@ -229,7 +230,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public int LogLinesLimit { get; set; } = 10000; public int LogLinesLimit { get; set; } = 10000;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public List<string> LogCommandHistory { get; set; } = new(); public List<string> LogCommandHistory { get; set; } = new();
@ -434,7 +435,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary> /// <summary>
/// Gets or sets a list of plugins that testing builds should be downloaded for. /// Gets or sets a list of plugins that testing builds should be downloaded for.
/// </summary> /// </summary>
public List<PluginTestingOptIn>? PluginTestingOptIns { get; set; } public List<PluginTestingOptIn> PluginTestingOptIns { get; set; } = [];
/// <summary>
/// Gets or sets a list of plugins that have opted into or out of auto-updating.
/// </summary>
public List<AutoUpdatePreference> PluginAutoUpdatePreferences { get; set; } = [];
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the FFXIV window should be toggled to immersive mode. /// 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
/// </summary> /// </summary>
public PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerOpenKind.AllPlugins; public PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerOpenKind.AllPlugins;
/// <summary>
/// Gets or sets a value indicating how auto-updating should behave.
/// </summary>
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
/// <summary>
/// Gets or sets a value indicating whether or not users should be notified regularly about pending updates.
/// </summary>
public bool CheckPeriodicallyForUpdates { get; set; } = true;
/// <summary> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>
@ -551,6 +567,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
private void SetDefaults() private void SetDefaults()
{ {
#pragma warning disable CS0618
// "Reduced motion" // "Reduced motion"
if (!this.ReduceMotions.HasValue) if (!this.ReduceMotions.HasValue)
{ {
@ -572,6 +589,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
this.ReduceMotions = winAnimEnabled == 0; 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() private void Save()

View file

@ -31,7 +31,6 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
/// Initializes a new instance of the <see cref="ConsoleManagerPluginScoped"/> class. /// Initializes a new instance of the <see cref="ConsoleManagerPluginScoped"/> class.
/// </summary> /// </summary>
/// <param name="plugin">The plugin this service belongs to.</param> /// <param name="plugin">The plugin this service belongs to.</param>
/// <param name="console">The console manager.</param>
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
internal ConsoleManagerPluginScoped(LocalPlugin plugin) internal ConsoleManagerPluginScoped(LocalPlugin plugin)
{ {

View file

@ -30,40 +30,6 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal class ChatHandlers : IServiceType internal class ChatHandlers : IServiceType
{ {
// private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
// {
// { "", "<:ffxive071:585847382210642069>" },
// { "", "<:ffxive083:585848592699490329>" },
// };
// private readonly Dictionary<XivChatType, Color> 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 static readonly ModuleLog Log = new("CHATHANDLER");
private readonly Regex rmtRegex = new( 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 Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
private readonly DalamudLinkPayload openInstallerWindowLink;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.Get(); private readonly Dalamud dalamud = Service<Dalamud>.Get();
@ -115,19 +79,12 @@ internal class ChatHandlers : IServiceType
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private bool hasSeenLoadingMsg; private bool hasSeenLoadingMsg;
private bool startedAutoUpdatingPlugins;
private CancellationTokenSource deferredAutoUpdateCts = new();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui) private ChatHandlers(ChatGui chatGui)
{ {
chatGui.CheckMessageHandled += this.OnCheckMessageHandled; chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
chatGui.ChatMessage += this.OnChatMessage; chatGui.ChatMessage += this.OnChatMessage;
this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins);
});
} }
/// <summary> /// <summary>
@ -135,11 +92,6 @@ internal class ChatHandlers : IServiceType
/// </summary> /// </summary>
public string? LastLink { get; private set; } public string? LastLink { get; private set; }
/// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary>
public bool IsAutoUpdateComplete { get; private set; }
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{ {
var textVal = message.TextValue; var textVal = message.TextValue;
@ -176,9 +128,6 @@ internal class ChatHandlers : IServiceType
{ {
if (!this.hasSeenLoadingMsg) if (!this.hasSeenLoadingMsg)
this.PrintWelcomeMessage(); this.PrintWelcomeMessage();
if (!this.startedAutoUpdatingPlugins)
this.AutoUpdatePluginsWithRetry();
} }
// For injections while logged in // For injections while logged in
@ -273,89 +222,4 @@ internal class ChatHandlers : IServiceType
this.hasSeenLoadingMsg = true; 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<ChatGui>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable();
var notifications = Service<NotificationManager>.GetNullable();
var condition = Service<Condition>.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<PluginManager>.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<Payload>()
{
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;
}
} }

View file

@ -36,6 +36,11 @@ public enum SettingsOpenKind
/// </summary> /// </summary>
LookAndFeel, LookAndFeel,
/// <summary>
/// Open to the "Auto Updates" page.
/// </summary>
AutoUpdates,
/// <summary> /// <summary>
/// Open to the "Server Info Bar" page. /// Open to the "Server Info Bar" page.
/// </summary> /// </summary>

View file

@ -211,6 +211,15 @@ internal class DalamudInterface : IInternalDisposableService
set => this.isImGuiDrawDevMenu = value; set => this.isImGuiDrawDevMenu = value;
} }
/// <summary>
/// Gets or sets a value indicating whether the plugin installer is open.
/// </summary>
public bool IsPluginInstallerOpen
{
get => this.pluginWindow.IsOpen;
set => this.pluginWindow.IsOpen = value;
}
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {

View file

@ -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;
/// <summary>
/// Private ImGui widgets for use inside Dalamud.
/// </summary>
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;
/// <summary>
/// Draw a "primary style" button.
/// </summary>
/// <param name="text">The text to show.</param>
/// <returns>True if the button was clicked.</returns>
internal static bool PrimaryButton(string text)
{
using (ImRaii.PushColor(ImGuiCol.Button, PrimaryButtonBackground))
{
return Button(text);
}
}
/// <summary>
/// Draw a "secondary style" button.
/// </summary>
/// <param name="text">The text to show.</param>
/// <returns>True if the button was clicked.</returns>
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);
}
}
}

View file

@ -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;
/// <summary>
/// Private ImGui widgets for use inside Dalamud.
/// </summary>
internal static partial class DalamudComponents
{
/// <summary>
/// Draw a "picker" popup to chose a plugin.
/// </summary>
/// <param name="id">The ID of the popup.</param>
/// <param name="pickerSearch">String holding the search input.</param>
/// <param name="onClicked">Action to be called if a plugin is clicked.</param>
/// <param name="pluginDisabled">Function that should return true if a plugin should show as disabled.</param>
/// <param name="pluginFiltered">Function that should return true if a plugin should not appear in the list.</param>
/// <returns>An ImGuiID to open the popup.</returns>
internal static uint DrawPluginPicker(string id, ref string pickerSearch, Action<LocalPlugin> onClicked, Func<LocalPlugin, bool> pluginDisabled, Func<LocalPlugin, bool>? pluginFiltered = null)
{
var pm = Service<PluginManager>.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...");
}
}

View file

@ -0,0 +1,8 @@
namespace Dalamud.Interface.Internal.DesignSystem;
/// <summary>
/// Private ImGui widgets for use inside Dalamud.
/// </summary>
internal static partial class DalamudComponents
{
}

View file

@ -1,6 +1,8 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using CheapLoc;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
@ -12,6 +14,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Storage.Assets; using Dalamud.Storage.Assets;
using Dalamud.Utility; using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
@ -23,6 +26,8 @@ namespace Dalamud.Interface.Internal.Windows;
/// </summary> /// </summary>
internal sealed class ChangelogWindow : Window, IDisposable internal sealed class ChangelogWindow : Window, IDisposable
{ {
private const AutoUpdateBehavior DefaultAutoUpdateBehavior = AutoUpdateBehavior.UpdateMainRepo;
private const string WarrantsChangelogForMajorMinor = "9.0."; private const string WarrantsChangelogForMajorMinor = "9.0.";
private const string ChangeLog = private const string ChangeLog =
@ -47,16 +52,33 @@ internal sealed class ChangelogWindow : Window, IDisposable
Point2 = new Vector2(2f), Point2 = new Vector2(2f),
}; };
private readonly InOutCubic bodyFade = new(TimeSpan.FromSeconds(1f)) private readonly InOutCubic bodyFade = new(TimeSpan.FromSeconds(1.3f))
{ {
Point1 = Vector2.Zero, Point1 = Vector2.Zero,
Point2 = Vector2.One, 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 State state = State.WindowFadeIn;
private bool needFadeRestart = false; private bool needFadeRestart = false;
private bool isFadingOutForStateChange = false;
private State? stateAfterFadeOut;
private AutoUpdateBehavior autoUpdateBehavior = DefaultAutoUpdateBehavior;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ChangelogWindow"/> class. /// Initializes a new instance of the <see cref="ChangelogWindow"/> class.
/// </summary> /// </summary>
@ -90,6 +112,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
WindowFadeIn, WindowFadeIn,
ExplainerIntro, ExplainerIntro,
ExplainerApiBump, ExplainerApiBump,
AskAutoUpdate,
Links, Links,
} }
@ -115,11 +138,18 @@ internal sealed class ChangelogWindow : Window, IDisposable
_ = this.bannerFont; _ = this.bannerFont;
this.isFadingOutForStateChange = false;
this.stateAfterFadeOut = null;
this.state = State.WindowFadeIn; this.state = State.WindowFadeIn;
this.windowFade.Reset(); this.windowFade.Reset();
this.bodyFade.Reset(); this.bodyFade.Reset();
this.titleFade.Reset();
this.fadeOut.Reset();
this.needFadeRestart = true; this.needFadeRestart = true;
this.autoUpdateBehavior = DefaultAutoUpdateBehavior;
base.OnOpen(); base.OnOpen();
} }
@ -130,6 +160,10 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.tsmWindow.AllowDrawing = true; this.tsmWindow.AllowDrawing = true;
Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(false); Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(false);
var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.autoUpdateBehavior;
configuration.QueueSave();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -144,10 +178,13 @@ internal sealed class ChangelogWindow : Window, IDisposable
if (this.needFadeRestart) if (this.needFadeRestart)
{ {
this.windowFade.Restart(); this.windowFade.Restart();
this.titleFade.Restart();
this.needFadeRestart = false; this.needFadeRestart = false;
} }
this.windowFade.Update(); this.windowFade.Update();
this.titleFade.Update();
this.fadeOut.Update();
ImGui.SetNextWindowBgAlpha(Math.Clamp(this.windowFade.EasedPoint.X, 0, 0.9f)); ImGui.SetNextWindowBgAlpha(Math.Clamp(this.windowFade.EasedPoint.X, 0, 0.9f));
this.Size = new Vector2(900, 400); this.Size = new Vector2(900, 400);
@ -208,7 +245,8 @@ internal sealed class ChangelogWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(20); 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(); using var font = this.bannerFont.Value.Push();
@ -223,6 +261,10 @@ internal sealed class ChangelogWindow : Window, IDisposable
ImGuiHelpers.CenteredText("Plugin Updates"); ImGuiHelpers.CenteredText("Plugin Updates");
break; break;
case State.AskAutoUpdate:
ImGuiHelpers.CenteredText("Auto-Updates");
break;
case State.Links: case State.Links:
ImGuiHelpers.CenteredText("Enjoy!"); ImGuiHelpers.CenteredText("Enjoy!");
break; break;
@ -237,9 +279,29 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.bodyFade.Restart(); this.bodyFade.Restart();
} }
this.bodyFade.Update(); if (this.isFadingOutForStateChange && this.fadeOut.IsDone)
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.bodyFade.EasedPoint.X, 0, 1f)))
{ {
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();
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) void DrawNextButton(State nextState)
{ {
// Draw big, centered next button at the bottom of the window // 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)); ImGui.SetCursorPosY(windowSize.Y - buttonHeight - (20 * ImGuiHelpers.GlobalScale));
ImGuiHelpers.CenterCursorFor((int)buttonWidth); 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; GoToNextState(nextState);
this.bodyFade.Restart();
} }
} }
@ -286,7 +347,65 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.apiBumpExplainerTexture.Value.ImGuiHandle, this.apiBumpExplainerTexture.Value.ImGuiHandle,
this.apiBumpExplainerTexture.Value.Size); 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; break;
case State.Links: case State.Links:
@ -356,12 +475,12 @@ internal sealed class ChangelogWindow : Window, IDisposable
// Draw close button in the top right corner // Draw close button in the top right corner
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 100f); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 100f);
var btnAlpha = Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f); 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)); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudWhite.WithAlpha(btnAlpha));
var childSize = ImGui.GetWindowSize(); var childSize = ImGui.GetWindowSize();
var closeButtonSize = 15 * ImGuiHelpers.GlobalScale; 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)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{ {
Dismiss(); Dismiss();

View file

@ -7,12 +7,12 @@ using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.DesignSystem;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility; using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
using Serilog; using Serilog;
@ -300,39 +300,16 @@ internal class ProfileManagerWidget
return; return;
} }
const string addPluginToProfilePopup = "###addPluginToProfile"; var addPluginToProfilePopupId = DalamudComponents.DrawPluginPicker(
var addPluginToProfilePopupId = ImGui.GetID(addPluginToProfilePopup); "###addPluginToProfilePicker",
using (var popup = ImRaii.Popup(addPluginToProfilePopup)) ref this.pickerSearch,
{ plugin =>
if (popup.Success)
{ {
var width = ImGuiHelpers.GlobalScale * 300; Task.Run(() => profile.AddOrUpdateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, true, false))
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
using var disabled = ImRaii.Disabled(profman.IsBusy); },
plugin => !plugin.Manifest.SupportsProfiles ||
ImGui.SetNextItemWidth(width); profile.Plugins.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId));
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();
}
}
}
var didAny = false; var didAny = false;
@ -603,8 +580,6 @@ internal class ProfileManagerWidget
public static string BackToOverview => Loc.Localize("ProfileManagerBackToOverview", "Back to overview"); 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 AddProfileHint => Loc.Localize("ProfileManagerAddProfileHint", "No collections! Add one!");
public static string CloneProfileHint => Loc.Localize("ProfileManagerCloneProfile", "Clone this collection"); public static string CloneProfileHint => Loc.Localize("ProfileManagerCloneProfile", "Clone this collection");

View file

@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings;
/// </summary> /// </summary>
internal class SettingsWindow : Window internal class SettingsWindow : Window
{ {
private SettingsTab[]? tabs; private readonly SettingsTab[] tabs;
private string searchInput = string.Empty; private string searchInput = string.Empty;
private bool isSearchInputPrefilled = false; private bool isSearchInputPrefilled = false;
@ -42,6 +42,16 @@ internal class SettingsWindow : Window
}; };
this.SizeCondition = ImGuiCond.FirstUseEver; this.SizeCondition = ImGuiCond.FirstUseEver;
this.tabs =
[
new SettingsTabGeneral(),
new SettingsTabLook(),
new SettingsTabAutoUpdates(),
new SettingsTabDtr(),
new SettingsTabExperimental(),
new SettingsTabAbout()
];
} }
/// <summary> /// <summary>
@ -75,15 +85,6 @@ internal class SettingsWindow : Window
/// <inheritdoc/> /// <inheritdoc/>
public override void OnOpen() public override void OnOpen()
{ {
this.tabs ??= new SettingsTab[]
{
new SettingsTabGeneral(),
new SettingsTabLook(),
new SettingsTabDtr(),
new SettingsTabExperimental(),
new SettingsTabAbout(),
};
foreach (var settingsTab in this.tabs) foreach (var settingsTab in this.tabs)
{ {
settingsTab.Load(); settingsTab.Load();
@ -152,10 +153,14 @@ internal class SettingsWindow : Window
settingsTab.OnOpen(); 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( using var tabChild = ImRaii.Child(
$"###settings_scrolling_{settingsTab.Title}", $"###settings_scrolling_{settingsTab.Title}",
new Vector2(-1, -1), new Vector2(-1, -1),
false); true);
if (tabChild) if (tabChild)
settingsTab.Draw(); settingsTab.Draw();
} }
@ -281,25 +286,15 @@ internal class SettingsWindow : Window
private void SetOpenTab(SettingsOpenKind kind) private void SetOpenTab(SettingsOpenKind kind)
{ {
switch (kind) this.setActiveTab = kind switch
{ {
case SettingsOpenKind.General: SettingsOpenKind.General => this.tabs[0],
this.setActiveTab = this.tabs[0]; SettingsOpenKind.LookAndFeel => this.tabs[1],
break; SettingsOpenKind.AutoUpdates => this.tabs[2],
case SettingsOpenKind.LookAndFeel: SettingsOpenKind.ServerInfoBar => this.tabs[3],
this.setActiveTab = this.tabs[1]; SettingsOpenKind.Experimental => this.tabs[4],
break; SettingsOpenKind.About => this.tabs[5],
case SettingsOpenKind.ServerInfoBar: _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
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);
}
} }
} }

View file

@ -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<AutoUpdatePreference> autoUpdatePreferences = [];
public override SettingsEntry[] Entries { get; } = Array.Empty<SettingsEntry>();
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<PluginImageCache>.Get();
var windowSize = ImGui.GetWindowSize();
var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
Guid? wantRemovePluginGuid = null;
foreach (var preference in this.autoUpdatePreferences)
{
var pmPlugin = Service<PluginManager>.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<AutoUpdatePreference.OptKind>())
{
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<DalamudConfiguration>.Get();
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
base.Load();
}
public override void Save()
{
var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.behavior;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;
base.Save();
}
}

View file

@ -3,6 +3,7 @@
using CheapLoc; using CheapLoc;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Plugin.Internal.AutoUpdate;
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; 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."), Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."),
c => c.GeneralChatType, c => c.GeneralChatType,
(v, c) => c.GeneralChatType = v, (v, c) => c.GeneralChatType = v,
warning: (v) => validity: (v) =>
{ {
// TODO: Maybe actually implement UI for the validity check...
if (v == XivChatType.None) if (v == XivChatType.None)
return Loc.Localize("DalamudSettingsChannelNone", "Do not pick \"None\"."); return Loc.Localize("DalamudSettingsChannelNone", "Do not pick \"None\".");
@ -62,12 +62,6 @@ public class SettingsTabGeneral : SettingsTab
c => c.PrintPluginsWelcomeMsg, c => c.PrintPluginsWelcomeMsg,
(v, c) => c.PrintPluginsWelcomeMsg = v), (v, c) => c.PrintPluginsWelcomeMsg = v),
new SettingsEntry<bool>(
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<bool>( new SettingsEntry<bool>(
Loc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"), Loc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"),
Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."), Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."),

View file

@ -9,7 +9,6 @@ using System.Reflection;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.Sanitizer; using Dalamud.Game.Text.Sanitizer;
@ -20,6 +19,7 @@ using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.Settings;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
@ -27,8 +27,6 @@ using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Ipc.Internal; using Dalamud.Plugin.Ipc.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using static Dalamud.Interface.Internal.Windows.PluginInstaller.PluginInstallerWindow;
namespace Dalamud.Plugin; namespace Dalamud.Plugin;
/// <summary> /// <summary>
@ -114,7 +112,7 @@ public sealed class DalamudPluginInterface : IDisposable
/// <summary> /// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session. /// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary> /// </summary>
public bool IsAutoUpdateComplete => Service<ChatHandlers>.Get().IsAutoUpdateComplete; public bool IsAutoUpdateComplete => Service<AutoUpdateManager>.Get().IsAutoUpdateComplete;
/// <summary> /// <summary>
/// Gets the repository from which this plugin was installed. /// Gets the repository from which this plugin was installed.

View file

@ -0,0 +1,27 @@
namespace Dalamud.Plugin.Internal.AutoUpdate;
/// <summary>
/// Enum describing how plugins should be auto-updated at startup-.
/// </summary>
internal enum AutoUpdateBehavior
{
/// <summary>
/// Plugins should not be updated and the user should not be notified.
/// </summary>
None,
/// <summary>
/// The user should merely be notified about updates.
/// </summary>
OnlyNotify,
/// <summary>
/// Only plugins from the main repository should be updated.
/// </summary>
UpdateMainRepo,
/// <summary>
/// All plugins should be updated.
/// </summary>
UpdateAll,
}

View file

@ -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
/// <summary>
/// Class to manage automatic updates for plugins.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class AutoUpdateManager : IServiceType
{
private static readonly ModuleLog Log = new("AUTOUPDATE");
/// <summary>
/// Time we should wait after login to update.
/// </summary>
private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20);
/// <summary>
/// Time we should wait between scheduled update checks.
/// </summary>
private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(1.5);
/// <summary>
/// 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.
/// </summary>
private static readonly TimeSpan CooldownAfterUnblock = TimeSpan.FromSeconds(30);
[ServiceManager.ServiceDependency]
private readonly PluginManager pluginManager = Service<PluginManager>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly NotificationManager notificationManager = Service<NotificationManager>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudInterface dalamudInterface = Service<DalamudInterface>.Get();
private readonly IConsoleVariable<bool> isDryRun;
private DateTime? loginTime;
private DateTime? lastUpdateCheckTime;
private DateTime? unblockedSince;
private bool hasStartedInitialUpdateThisSession;
private IActiveNotification? updateNotification;
private Task? autoUpdateTask;
/// <summary>
/// Initializes a new instance of the <see cref="AutoUpdateManager"/> class.
/// </summary>
/// <param name="console">Console service.</param>
[ServiceManager.ServiceConstructor]
public AutoUpdateManager(ConsoleManager console)
{
Service<ClientState>.GetAsync().ContinueWith(
t =>
{
t.Result.Login += this.OnLogin;
t.Result.Logout += this.OnLogout;
});
Service<Framework>.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,
}
/// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session.
/// </summary>
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<AvailablePluginUpdate> 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<AvailablePluginUpdate> updatablePlugins)
{
var pluginStates = new List<PluginUpdateStatus>();
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<DalamudInterface>.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<AvailablePluginUpdate> 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<DalamudInterface>.Get().OpenPluginInstaller();
notification.DismissNow();
}
};
}
private List<AvailablePluginUpdate> 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<Condition>.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;
}
}