mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Merge pull request #1832 from goatcorp/apiX-rollup
[apiX] Rollup changes from master
This commit is contained in:
commit
80555d92ec
28 changed files with 1526 additions and 311 deletions
42
Dalamud/Configuration/Internal/AutoUpdatePreference.cs
Normal file
42
Dalamud/Configuration/Internal/AutoUpdatePreference.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -80,10 +80,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
public string? LastVersion { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public int SeenFtueLevel { get; set; } = 1;
|
||||
public Dictionary<string, int> SeenFtueLevels { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last loaded Dalamud version.
|
||||
|
|
@ -196,6 +195,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not plugins should be auto-updated.
|
||||
/// </summary>
|
||||
[Obsolete("Use AutoUpdateBehavior instead.")]
|
||||
public bool AutoUpdatePlugins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -228,6 +228,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
public int LogLinesLimit { get; set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a list representing the command history for the Dalamud Console.
|
||||
/// </summary>
|
||||
public List<string> LogCommandHistory { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the dev bar should open at startup.
|
||||
/// </summary>
|
||||
|
|
@ -429,7 +434,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// <summary>
|
||||
/// Gets or sets a list of plugins that testing builds should be downloaded for.
|
||||
/// </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>
|
||||
/// Gets or sets a value indicating whether the FFXIV window should be toggled to immersive mode.
|
||||
|
|
@ -464,6 +474,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
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>
|
||||
/// Load a configuration from the provided path.
|
||||
/// </summary>
|
||||
|
|
@ -549,6 +569,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
|
||||
private void SetDefaults()
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
// "Reduced motion"
|
||||
if (!this.ReduceMotions.HasValue)
|
||||
{
|
||||
|
|
@ -570,6 +591,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()
|
||||
|
|
|
|||
|
|
@ -270,7 +270,9 @@ internal partial class ConsoleManager : IServiceType
|
|||
for (var i = parsedArguments.Count; i < entry.ValidArguments.Count; i++)
|
||||
{
|
||||
var argument = entry.ValidArguments[i];
|
||||
if (argument.DefaultValue == null)
|
||||
|
||||
// If the default value is DBNull, we need to error out as that means it was not specified
|
||||
if (argument.DefaultValue == DBNull.Value)
|
||||
{
|
||||
Log.Error("Not enough arguments for command {CommandName}", entryName);
|
||||
PrintUsage(entry);
|
||||
|
|
@ -382,11 +384,8 @@ internal partial class ConsoleManager : IServiceType
|
|||
/// <param name="defaultValue">The default value to use if none is specified.</param>
|
||||
/// <returns>An <see cref="ArgumentInfo"/> instance.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the given type cannot be handled by the console system.</exception>
|
||||
protected static ArgumentInfo TypeToArgument(Type type, object? defaultValue = null)
|
||||
protected static ArgumentInfo TypeToArgument(Type type, object? defaultValue)
|
||||
{
|
||||
// If the default value is DBNull, we want to treat it as null
|
||||
defaultValue = defaultValue == DBNull.Value ? null : defaultValue;
|
||||
|
||||
if (type == typeof(string))
|
||||
return new ArgumentInfo(ConsoleArgumentType.String, defaultValue);
|
||||
|
||||
|
|
@ -490,7 +489,7 @@ internal partial class ConsoleManager : IServiceType
|
|||
public ConsoleVariable(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
this.ValidArguments = new List<ArgumentInfo> { TypeToArgument(typeof(T)) };
|
||||
this.ValidArguments = new List<ArgumentInfo> { TypeToArgument(typeof(T), null) };
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -500,7 +499,20 @@ internal partial class ConsoleManager : IServiceType
|
|||
public override bool Invoke(IEnumerable<object> arguments)
|
||||
{
|
||||
var first = arguments.FirstOrDefault();
|
||||
if (first == null || first.GetType() != typeof(T))
|
||||
|
||||
if (first == null)
|
||||
{
|
||||
// Invert the value if it's a boolean
|
||||
if (this.Value is bool boolValue)
|
||||
{
|
||||
this.Value = (T)(object)!boolValue;
|
||||
}
|
||||
|
||||
Log.WriteLog(LogEventLevel.Information, "{VariableName} = {VariableValue}", null, this.Name, this.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (first.GetType() != typeof(T))
|
||||
throw new ArgumentException($"Console variable must be set with an argument of type {typeof(T).Name}.");
|
||||
|
||||
this.Value = (T)first;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ namespace Dalamud.Console;
|
|||
#pragma warning restore SA1015
|
||||
public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
|
||||
{
|
||||
private readonly ConsoleManager console;
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ConsoleManager console = Service<ConsoleManager>.Get();
|
||||
|
||||
private readonly List<IConsoleEntry> trackedEntries = new();
|
||||
|
||||
|
|
@ -29,12 +30,9 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
|
|||
/// Initializes a new instance of the <see cref="ConsoleManagerPluginScoped"/> class.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin this service belongs to.</param>
|
||||
/// <param name="console">The console manager.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
internal ConsoleManagerPluginScoped(LocalPlugin plugin, ConsoleManager console)
|
||||
internal ConsoleManagerPluginScoped(LocalPlugin plugin)
|
||||
{
|
||||
this.console = console;
|
||||
|
||||
this.Prefix = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(plugin.InternalName);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
<DalamudVersion>9.1.0.10</DalamudVersion>
|
||||
<DalamudVersion>9.1.0.12</DalamudVersion>
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
|
|
|
|||
|
|
@ -29,40 +29,6 @@ namespace Dalamud.Game;
|
|||
[ServiceManager.EarlyLoadedService]
|
||||
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 readonly Regex rmtRegex = new(
|
||||
|
|
@ -105,8 +71,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<Dalamud>.Get();
|
||||
|
||||
|
|
@ -114,19 +78,12 @@ internal class ChatHandlers : IServiceType
|
|||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.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<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -175,9 +132,6 @@ internal class ChatHandlers : IServiceType
|
|||
{
|
||||
if (!this.hasSeenLoadingMsg)
|
||||
this.PrintWelcomeMessage();
|
||||
|
||||
if (!this.startedAutoUpdatingPlugins)
|
||||
this.AutoUpdatePluginsWithRetry();
|
||||
}
|
||||
|
||||
// For injections while logged in
|
||||
|
|
@ -272,89 +226,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<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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.Gui;
|
||||
|
|
@ -122,6 +124,27 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
|
|||
/// Gets client state address resolver.
|
||||
/// </summary>
|
||||
internal ClientStateAddressResolver AddressResolver => this.address;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsClientIdle(out ConditionFlag blockingFlag)
|
||||
{
|
||||
blockingFlag = 0;
|
||||
if (this.LocalPlayer is null) return true;
|
||||
|
||||
var condition = Service<Conditions.Condition>.GetNullable();
|
||||
|
||||
var blockingConditions = condition.AsReadOnlySet().Except([
|
||||
ConditionFlag.NormalConditions,
|
||||
ConditionFlag.Jumping,
|
||||
ConditionFlag.Mounted,
|
||||
ConditionFlag.UsingParasol]);
|
||||
|
||||
blockingFlag = blockingConditions.FirstOrDefault();
|
||||
return blockingFlag == 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsClientIdle() => this.IsClientIdle(out _);
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
|
|
@ -269,6 +292,12 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
|
|||
/// <inheritdoc/>
|
||||
public bool IsGPosing => this.clientStateService.IsGPosing;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsClientIdle(out ConditionFlag blockingFlag) => this.clientStateService.IsClientIdle(out blockingFlag);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsClientIdle() => this.clientStateService.IsClientIdle();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
|
@ -67,6 +70,22 @@ internal sealed class Condition : IInternalDisposableService, ICondition
|
|||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService() => this.Dispose(true);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlySet<ConditionFlag> AsReadOnlySet()
|
||||
{
|
||||
var result = new HashSet<ConditionFlag>();
|
||||
|
||||
for (var i = 0; i < MaxConditionEntries; i++)
|
||||
{
|
||||
if (this[i])
|
||||
{
|
||||
result.Add((ConditionFlag)i);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Any()
|
||||
|
|
@ -96,6 +115,25 @@ internal sealed class Condition : IInternalDisposableService, ICondition
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool AnyExcept(params ConditionFlag[] excluded)
|
||||
{
|
||||
return !this.AsReadOnlySet().Intersect(excluded).Any();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnlyAny(params ConditionFlag[] other)
|
||||
{
|
||||
return !this.AsReadOnlySet().Except(other).Any();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EqualTo(params ConditionFlag[] other)
|
||||
{
|
||||
var resultSet = this.AsReadOnlySet();
|
||||
return resultSet.SetEquals(other);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
|
|
@ -173,6 +211,9 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition
|
|||
|
||||
this.ConditionChange = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlySet<ConditionFlag> AsReadOnlySet() => this.conditionService.AsReadOnlySet();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Any() => this.conditionService.Any();
|
||||
|
|
@ -180,5 +221,14 @@ internal class ConditionPluginScoped : IInternalDisposableService, ICondition
|
|||
/// <inheritdoc/>
|
||||
public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool AnyExcept(params ConditionFlag[] except) => this.conditionService.AnyExcept(except);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnlyAny(params ConditionFlag[] other) => this.conditionService.OnlyAny(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool EqualTo(params ConditionFlag[] other) => this.conditionService.EqualTo(other);
|
||||
|
||||
private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -495,6 +495,9 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
|
|||
private char HandleImmDetour(IntPtr framework, char a2, byte a3)
|
||||
{
|
||||
var result = this.handleImmHook.Original(framework, a2, a3);
|
||||
if (!ImGuiHelpers.IsImGuiInitialized)
|
||||
return result;
|
||||
|
||||
return ImGui.GetIO().WantTextInput
|
||||
? (char)0
|
||||
: result;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ public enum SettingsOpenKind
|
|||
/// Open to the "Look & Feel" page.
|
||||
/// </summary>
|
||||
LookAndFeel,
|
||||
|
||||
/// <summary>
|
||||
/// Open to the "Auto Updates" page.
|
||||
/// </summary>
|
||||
AutoUpdates,
|
||||
|
||||
/// <summary>
|
||||
/// Open to the "Server Info Bar" page.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,15 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
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/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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...");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace Dalamud.Interface.Internal.DesignSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Private ImGui widgets for use inside Dalamud.
|
||||
/// </summary>
|
||||
internal static partial class DalamudComponents
|
||||
{
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using CheapLoc;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Interface.Animation.EasingFunctions;
|
||||
using Dalamud.Interface.Colors;
|
||||
|
|
@ -12,8 +15,10 @@ 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;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows;
|
||||
|
|
@ -47,15 +52,34 @@ 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? chosenAutoUpdateBehavior;
|
||||
|
||||
private Dictionary<string, int> currentFtueLevels = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChangelogWindow"/> class.
|
||||
|
|
@ -90,6 +114,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
WindowFadeIn,
|
||||
ExplainerIntro,
|
||||
ExplainerApiBump,
|
||||
AskAutoUpdate,
|
||||
Links,
|
||||
}
|
||||
|
||||
|
|
@ -114,11 +139,20 @@ 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.chosenAutoUpdateBehavior = null;
|
||||
|
||||
this.currentFtueLevels = Service<DalamudConfiguration>.Get().SeenFtueLevels;
|
||||
|
||||
base.OnOpen();
|
||||
}
|
||||
|
|
@ -130,6 +164,16 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
|
||||
this.tsmWindow.AllowDrawing = true;
|
||||
Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(false);
|
||||
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
if (this.chosenAutoUpdateBehavior.HasValue)
|
||||
{
|
||||
configuration.AutoUpdateBehavior = this.chosenAutoUpdateBehavior.Value;
|
||||
}
|
||||
|
||||
configuration.SeenFtueLevels = this.currentFtueLevels;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -144,10 +188,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 +254,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 +271,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,11 +288,31 @@ 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 DrawNextButton(State nextState)
|
||||
void GoToNextState(State nextState)
|
||||
{
|
||||
this.isFadingOutForStateChange = true;
|
||||
this.stateAfterFadeOut = nextState;
|
||||
|
||||
this.fadeOut.Restart();
|
||||
}
|
||||
|
||||
bool DrawNextButton(State nextState)
|
||||
{
|
||||
// Draw big, centered next button at the bottom of the window
|
||||
var buttonHeight = 30 * ImGuiHelpers.GlobalScale;
|
||||
|
|
@ -249,11 +321,13 @@ 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);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.state)
|
||||
|
|
@ -286,7 +360,66 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
this.apiBumpExplainerTexture.Value.ImGuiHandle,
|
||||
this.apiBumpExplainerTexture.Value.Size);
|
||||
|
||||
DrawNextButton(State.Links);
|
||||
if (!this.currentFtueLevels.TryGetValue(FtueLevels.AutoUpdate.Name, out var autoUpdateLevel) || autoUpdateLevel < FtueLevels.AutoUpdate.AutoUpdateInitial)
|
||||
{
|
||||
if (DrawNextButton(State.AskAutoUpdate))
|
||||
{
|
||||
this.currentFtueLevels[FtueLevels.AutoUpdate.Name] = FtueLevels.AutoUpdate.AutoUpdateInitial;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawNextButton(State.Links);
|
||||
}
|
||||
|
||||
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 only notify you about updates while you are idle."));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(15);
|
||||
|
||||
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.chosenAutoUpdateBehavior = 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.chosenAutoUpdateBehavior = AutoUpdateBehavior.OnlyNotify;
|
||||
GoToNextState(State.Links);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case State.Links:
|
||||
|
|
@ -356,12 +489,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();
|
||||
|
|
@ -384,4 +517,13 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private static class FtueLevels
|
||||
{
|
||||
public static class AutoUpdate
|
||||
{
|
||||
public const string Name = "AutoUpdate";
|
||||
public const int AutoUpdateInitial = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
private const int LogLinesMinimum = 100;
|
||||
private const int LogLinesMaximum = 1000000;
|
||||
private const int HistorySize = 50;
|
||||
|
||||
// Only this field may be touched from any thread.
|
||||
private readonly ConcurrentQueue<(string Line, LogEvent LogEvent)> newLogEntries;
|
||||
|
|
@ -44,9 +45,10 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
// Fields below should be touched only from the main thread.
|
||||
private readonly RollingList<LogEntry> logText;
|
||||
private readonly RollingList<LogEntry> filteredLogEntries;
|
||||
|
||||
private readonly List<string> history = new();
|
||||
|
||||
private readonly List<PluginFilterEntry> pluginFilters = new();
|
||||
|
||||
private readonly DalamudConfiguration configuration;
|
||||
|
||||
private int newRolledLines;
|
||||
private bool pendingRefilter;
|
||||
|
|
@ -78,6 +80,9 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
private int historyPos;
|
||||
private int copyStart = -1;
|
||||
|
||||
private string? completionZipText = null;
|
||||
private int completionTabIdx = 0;
|
||||
|
||||
private IActiveNotification? prevCopyNotification;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ConsoleWindow"/> class.</summary>
|
||||
|
|
@ -85,6 +90,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
public ConsoleWindow(DalamudConfiguration configuration)
|
||||
: base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
|
||||
this.autoScroll = configuration.LogAutoScroll;
|
||||
this.autoOpen = configuration.LogOpenAtStartup;
|
||||
SerilogEventSink.Instance.LogLine += this.OnLogLine;
|
||||
|
|
@ -111,7 +118,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
this.logText = new(limit);
|
||||
this.filteredLogEntries = new(limit);
|
||||
|
||||
configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
|
||||
this.configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
|
||||
|
||||
unsafe
|
||||
{
|
||||
|
|
@ -130,7 +137,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
SerilogEventSink.Instance.LogLine -= this.OnLogLine;
|
||||
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
|
||||
this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
|
||||
if (Service<Framework>.GetNullable() is { } framework)
|
||||
framework.Update -= this.FrameworkOnUpdate;
|
||||
|
||||
|
|
@ -314,9 +321,10 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
ref this.commandText,
|
||||
255,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion |
|
||||
ImGuiInputTextFlags.CallbackHistory,
|
||||
ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit,
|
||||
this.CommandInputCallback))
|
||||
{
|
||||
this.newLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
|
||||
this.ProcessCommand();
|
||||
getFocus = true;
|
||||
}
|
||||
|
|
@ -460,8 +468,6 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
private void DrawOptionsToolbar()
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
ImGui.PushItemWidth(150.0f * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.BeginCombo("##log_level", $"{EntryPoint.LogLevelSwitch.MinimumLevel}+"))
|
||||
{
|
||||
|
|
@ -470,8 +476,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
if (ImGui.Selectable(value.ToString(), value == EntryPoint.LogLevelSwitch.MinimumLevel))
|
||||
{
|
||||
EntryPoint.LogLevelSwitch.MinimumLevel = value;
|
||||
configuration.LogLevel = value;
|
||||
configuration.QueueSave();
|
||||
this.configuration.LogLevel = value;
|
||||
this.configuration.QueueSave();
|
||||
this.QueueRefilter();
|
||||
}
|
||||
}
|
||||
|
|
@ -484,13 +490,13 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
var settingsPopup = ImGui.BeginPopup("##console_settings");
|
||||
if (settingsPopup)
|
||||
{
|
||||
this.DrawSettingsPopup(configuration);
|
||||
this.DrawSettingsPopup();
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
else if (this.settingsPopupWasOpen)
|
||||
{
|
||||
// Prevent side effects in case Apply wasn't clicked
|
||||
this.logLinesLimit = configuration.LogLinesLimit;
|
||||
this.logLinesLimit = this.configuration.LogLinesLimit;
|
||||
}
|
||||
|
||||
this.settingsPopupWasOpen = settingsPopup;
|
||||
|
|
@ -638,18 +644,18 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawSettingsPopup(DalamudConfiguration configuration)
|
||||
private void DrawSettingsPopup()
|
||||
{
|
||||
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))
|
||||
{
|
||||
configuration.LogOpenAtStartup = this.autoOpen;
|
||||
configuration.QueueSave();
|
||||
this.configuration.LogOpenAtStartup = this.autoOpen;
|
||||
this.configuration.QueueSave();
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
|
||||
{
|
||||
configuration.LogAutoScroll = this.autoScroll;
|
||||
configuration.QueueSave();
|
||||
this.configuration.LogAutoScroll = this.autoScroll;
|
||||
this.configuration.QueueSave();
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted("Logs buffer");
|
||||
|
|
@ -658,8 +664,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
this.logLinesLimit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||
|
||||
configuration.LogLinesLimit = this.logLinesLimit;
|
||||
configuration.QueueSave();
|
||||
this.configuration.LogLinesLimit = this.logLinesLimit;
|
||||
this.configuration.QueueSave();
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
|
@ -795,23 +801,18 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
try
|
||||
{
|
||||
this.historyPos = -1;
|
||||
for (var i = this.history.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (this.history[i] == this.commandText)
|
||||
{
|
||||
this.history.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.history.Add(this.commandText);
|
||||
|
||||
if (this.commandText is "clear" or "cls")
|
||||
{
|
||||
this.QueueClear();
|
||||
if (string.IsNullOrEmpty(this.commandText))
|
||||
return;
|
||||
}
|
||||
|
||||
this.historyPos = -1;
|
||||
|
||||
if (this.commandText != this.configuration.LogCommandHistory.LastOrDefault())
|
||||
this.configuration.LogCommandHistory.Add(this.commandText);
|
||||
|
||||
if (this.configuration.LogCommandHistory.Count > HistorySize)
|
||||
this.configuration.LogCommandHistory.RemoveAt(0);
|
||||
|
||||
this.configuration.QueueSave();
|
||||
|
||||
this.lastCmdSuccess = Service<ConsoleManager>.Get().ProcessCommand(this.commandText);
|
||||
this.commandText = string.Empty;
|
||||
|
|
@ -831,6 +832,11 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
switch (data->EventFlag)
|
||||
{
|
||||
case ImGuiInputTextFlags.CallbackEdit:
|
||||
this.completionZipText = null;
|
||||
this.completionTabIdx = 0;
|
||||
break;
|
||||
|
||||
case ImGuiInputTextFlags.CallbackCompletion:
|
||||
var textBytes = new byte[data->BufTextLen];
|
||||
Marshal.Copy((IntPtr)data->Buf, textBytes, 0, data->BufTextLen);
|
||||
|
|
@ -841,22 +847,47 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
// We can't do any completion for parameters at the moment since it just calls into CommandHandler
|
||||
if (words.Length > 1)
|
||||
return 0;
|
||||
|
||||
var wordToComplete = words[0];
|
||||
if (wordToComplete.IsNullOrWhitespace())
|
||||
return 0;
|
||||
|
||||
if (this.completionZipText is not null)
|
||||
wordToComplete = this.completionZipText;
|
||||
|
||||
// TODO: Improve this, add partial completion, arguments, description, etc.
|
||||
// https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484
|
||||
var candidates = Service<ConsoleManager>.Get().Entries
|
||||
.Where(x => x.Key.StartsWith(words[0]))
|
||||
.Where(x => x.Key.StartsWith(wordToComplete))
|
||||
.Select(x => x.Key);
|
||||
|
||||
candidates = candidates.Union(
|
||||
Service<CommandManager>.Get().Commands
|
||||
.Where(x => x.Key.StartsWith(words[0])).Select(x => x.Key));
|
||||
.Where(x => x.Key.StartsWith(wordToComplete)).Select(x => x.Key))
|
||||
.ToArray();
|
||||
|
||||
var enumerable = candidates as string[] ?? candidates.ToArray();
|
||||
if (enumerable.Length != 0)
|
||||
if (candidates.Any())
|
||||
{
|
||||
ptr.DeleteChars(0, ptr.BufTextLen);
|
||||
ptr.InsertChars(0, enumerable[0]);
|
||||
string? toComplete = null;
|
||||
if (this.completionZipText == null)
|
||||
{
|
||||
// Find the "common" prefix of all matches
|
||||
toComplete = candidates.Aggregate(
|
||||
(prefix, candidate) => string.Concat(prefix.Zip(candidate, (a, b) => a == b ? a : '\0')));
|
||||
|
||||
this.completionZipText = toComplete;
|
||||
}
|
||||
else
|
||||
{
|
||||
toComplete = candidates.ElementAt(this.completionTabIdx);
|
||||
this.completionTabIdx = (this.completionTabIdx + 1) % candidates.Count();
|
||||
}
|
||||
|
||||
if (toComplete != null)
|
||||
{
|
||||
ptr.DeleteChars(0, ptr.BufTextLen);
|
||||
ptr.InsertChars(0, toComplete);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -867,7 +898,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
if (ptr.EventKey == ImGuiKey.UpArrow)
|
||||
{
|
||||
if (this.historyPos == -1)
|
||||
this.historyPos = this.history.Count - 1;
|
||||
this.historyPos = this.configuration.LogCommandHistory.Count - 1;
|
||||
else if (this.historyPos > 0)
|
||||
this.historyPos--;
|
||||
}
|
||||
|
|
@ -875,7 +906,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
if (this.historyPos != -1)
|
||||
{
|
||||
if (++this.historyPos >= this.history.Count)
|
||||
if (++this.historyPos >= this.configuration.LogCommandHistory.Count)
|
||||
{
|
||||
this.historyPos = -1;
|
||||
}
|
||||
|
|
@ -884,7 +915,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
if (prevPos != this.historyPos)
|
||||
{
|
||||
var historyStr = this.historyPos >= 0 ? this.history[this.historyPos] : string.Empty;
|
||||
var historyStr = this.historyPos >= 0 ? this.configuration.LogCommandHistory[this.historyPos] : string.Empty;
|
||||
|
||||
ptr.DeleteChars(0, ptr.BufTextLen);
|
||||
ptr.InsertChars(0, historyStr);
|
||||
|
|
|
|||
|
|
@ -208,6 +208,19 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
EnabledDisabled,
|
||||
ProfileOrNot,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum PluginHeaderFlags
|
||||
{
|
||||
None = 0,
|
||||
IsThirdParty = 1 << 0,
|
||||
HasTrouble = 1 << 1,
|
||||
UpdateAvailable = 1 << 2,
|
||||
IsNew = 1 << 3,
|
||||
IsInstallableOutdated = 1 << 4,
|
||||
IsOrphan = 1 << 5,
|
||||
IsTesting = 1 << 6,
|
||||
}
|
||||
|
||||
private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress ||
|
||||
this.updateStatus == OperationStatus.InProgress ||
|
||||
|
|
@ -712,8 +725,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;
|
||||
|
|
@ -1807,22 +1824,62 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
return ready;
|
||||
}
|
||||
|
||||
private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index)
|
||||
private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, IPluginManifest manifest, PluginHeaderFlags flags, Action drawContextMenuAction, int index)
|
||||
{
|
||||
ImGui.Separator();
|
||||
|
||||
var isOpen = this.openPluginCollapsibles.Contains(index);
|
||||
|
||||
var sectionSize = ImGuiHelpers.GlobalScale * 66;
|
||||
var tapeCursor = ImGui.GetCursorPos();
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
var startCursor = ImGui.GetCursorPos();
|
||||
|
||||
if (flags.HasFlag(PluginHeaderFlags.IsTesting))
|
||||
{
|
||||
void DrawCautionTape(Vector2 position, Vector2 size, float stripeWidth, float skewAmount)
|
||||
{
|
||||
var wdl = ImGui.GetWindowDrawList();
|
||||
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
var scroll = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY());
|
||||
|
||||
var adjustedPosition = windowPos + position - scroll;
|
||||
|
||||
var yellow = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.9f, 0.0f, 0.10f));
|
||||
var numStripes = (int)(size.X / stripeWidth) + (int)(size.Y / skewAmount) + 1; // +1 to cover partial stripe
|
||||
|
||||
for (var i = 0; i < numStripes; i++)
|
||||
{
|
||||
var x0 = adjustedPosition.X + i * stripeWidth;
|
||||
var x1 = x0 + stripeWidth;
|
||||
var y0 = adjustedPosition.Y;
|
||||
var y1 = y0 + size.Y;
|
||||
|
||||
var p0 = new Vector2(x0, y0);
|
||||
var p1 = new Vector2(x1, y0);
|
||||
var p2 = new Vector2(x1 - skewAmount, y1);
|
||||
var p3 = new Vector2(x0 - skewAmount, y1);
|
||||
|
||||
if (i % 2 != 0)
|
||||
continue;
|
||||
|
||||
wdl.AddQuadFilled(p0, p1, p2, p3, yellow);
|
||||
}
|
||||
}
|
||||
|
||||
DrawCautionTape(tapeCursor + new Vector2(0, 1), new Vector2(ImGui.GetWindowWidth(), sectionSize + ImGui.GetStyle().ItemSpacing.Y), ImGuiHelpers.GlobalScale * 40, 20);
|
||||
}
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, isOpen ? new Vector4(0.5f, 0.5f, 0.5f, 0.1f) : Vector4.Zero);
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0.5f, 0.5f, 0.5f, 0.2f));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.5f, 0.5f, 0.5f, 0.35f));
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
|
||||
|
||||
ImGui.SetCursorPos(tapeCursor);
|
||||
|
||||
if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize)))
|
||||
if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize + ImGui.GetStyle().ItemSpacing.Y)))
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
|
|
@ -1854,7 +1911,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize))
|
||||
{
|
||||
var iconTex = this.imageCache.DefaultIcon;
|
||||
var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex, out var loadedSince);
|
||||
var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, flags.HasFlag(PluginHeaderFlags.IsThirdParty), out var cachedIconTex, out var loadedSince);
|
||||
if (hasIcon && cachedIconTex != null)
|
||||
{
|
||||
iconTex = cachedIconTex;
|
||||
|
|
@ -1868,7 +1925,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
float EaseOutCubic(float t) => 1 - MathF.Pow(1 - t, 3);
|
||||
|
||||
var secondsSinceLoad = (float)DateTime.Now.Subtract(loadedSince.Value).TotalSeconds;
|
||||
var fadeTo = pluginDisabled || installableOutdated ? 0.4f : 1f;
|
||||
var fadeTo = pluginDisabled || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated) ? 0.4f : 1f;
|
||||
|
||||
float Interp(float to) => Math.Clamp(EaseOutCubic(Math.Min(secondsSinceLoad, fadeTime) / fadeTime) * to, 0, 1);
|
||||
iconAlpha = Interp(fadeTo);
|
||||
|
|
@ -1886,11 +1943,11 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var isLoaded = plugin is { IsLoaded: true };
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha);
|
||||
if (updateAvailable)
|
||||
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
|
||||
ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize);
|
||||
else if ((trouble && !pluginDisabled) || isOrphan)
|
||||
else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan))
|
||||
ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize);
|
||||
else if (installableOutdated)
|
||||
else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
|
||||
ImGui.Image(this.imageCache.OutdatedInstallableIcon.ImGuiHandle, iconSize);
|
||||
else if (pluginDisabled)
|
||||
ImGui.Image(this.imageCache.DisabledIcon.ImGuiHandle, iconSize);
|
||||
|
|
@ -1934,7 +1991,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Wrench, devIconOutlineColor, devIconColor);
|
||||
this.VerifiedCheckmarkFadeTooltip(label, "This is a dev plugin. You added it.");
|
||||
}
|
||||
else if (!isThirdParty)
|
||||
else if (!flags.HasFlag(PluginHeaderFlags.IsThirdParty))
|
||||
{
|
||||
this.DrawFontawesomeIconOutlined(FontAwesomeIcon.CheckCircle, verifiedOutlineColor, verifiedIconColor);
|
||||
this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_VerifiedTooltip);
|
||||
|
|
@ -1954,7 +2011,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadCountText);
|
||||
|
||||
if (isNew)
|
||||
if (flags.HasFlag(PluginHeaderFlags.IsNew))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextColored(ImGuiColors.TankBlue, Locs.PluginTitleMod_New);
|
||||
|
|
@ -1964,12 +2021,12 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGui.SetCursorPos(cursor);
|
||||
|
||||
// Outdated warning
|
||||
if (plugin is { IsOutdated: true, IsBanned: false } || installableOutdated)
|
||||
if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||
|
||||
var bodyText = Locs.PluginBody_Outdated + " ";
|
||||
if (updateAvailable)
|
||||
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
|
||||
bodyText += Locs.PluginBody_Outdated_CanNowUpdate;
|
||||
else
|
||||
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
|
||||
|
|
@ -1987,7 +2044,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
: Locs.PluginBody_BannedReason(plugin.BanReason);
|
||||
bodyText += " ";
|
||||
|
||||
if (updateAvailable)
|
||||
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
|
||||
bodyText += Locs.PluginBody_Outdated_CanNowUpdate;
|
||||
else
|
||||
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
|
||||
|
|
@ -2031,7 +2088,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGui.SetCursorPosX(cursor.X);
|
||||
|
||||
// Description
|
||||
if (plugin is null or { IsOutdated: false, IsBanned: false } && !trouble)
|
||||
if (plugin is null or { IsOutdated: false, IsBanned: false } && !flags.HasFlag(PluginHeaderFlags.HasTrouble))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(manifest.Punchline))
|
||||
{
|
||||
|
|
@ -2152,11 +2209,22 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
label += Locs.PluginTitleMod_TestingAvailable;
|
||||
}
|
||||
|
||||
var isThirdParty = manifest.SourceRepo.IsThirdParty;
|
||||
|
||||
ImGui.PushID($"available{index}{manifest.InternalName}");
|
||||
|
||||
var isThirdParty = manifest.SourceRepo.IsThirdParty;
|
||||
if (this.DrawPluginCollapsingHeader(label, null, manifest, isThirdParty, false, false, !wasSeen, isOutdated, false, () => this.DrawAvailablePluginContextMenu(manifest), index))
|
||||
|
||||
var flags = PluginHeaderFlags.None;
|
||||
if (isThirdParty)
|
||||
flags |= PluginHeaderFlags.IsThirdParty;
|
||||
if (!wasSeen)
|
||||
flags |= PluginHeaderFlags.IsNew;
|
||||
if (isOutdated)
|
||||
flags |= PluginHeaderFlags.IsInstallableOutdated;
|
||||
if (useTesting || manifest.IsTestingExclusive)
|
||||
flags |= PluginHeaderFlags.IsTesting;
|
||||
|
||||
if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index))
|
||||
{
|
||||
if (!wasSeen)
|
||||
configuration.SeenPluginInternalName.Add(manifest.InternalName);
|
||||
|
|
@ -2420,7 +2488,19 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var hasChangelog = !applicableChangelog.IsNullOrWhitespace();
|
||||
var didDrawChangelogInsideCollapsible = false;
|
||||
|
||||
if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index))
|
||||
var flags = PluginHeaderFlags.None;
|
||||
if (plugin.IsThirdParty)
|
||||
flags |= PluginHeaderFlags.IsThirdParty;
|
||||
if (trouble)
|
||||
flags |= PluginHeaderFlags.HasTrouble;
|
||||
if (availablePluginUpdate != default)
|
||||
flags |= PluginHeaderFlags.UpdateAvailable;
|
||||
if (plugin.IsOrphaned)
|
||||
flags |= PluginHeaderFlags.IsOrphan;
|
||||
if (plugin.IsTesting)
|
||||
flags |= PluginHeaderFlags.IsTesting;
|
||||
|
||||
if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, flags, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index))
|
||||
{
|
||||
if (!this.WasPluginSeen(plugin.Manifest.InternalName))
|
||||
configuration.SeenPluginInternalName.Add(plugin.Manifest.InternalName);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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 +299,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 +579,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");
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings;
|
|||
/// </summary>
|
||||
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()
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -75,15 +85,6 @@ internal class SettingsWindow : Window
|
|||
/// <inheritdoc/>
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 only notify you about updates while you are 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 don't have auto-update rules for any plugins."));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ 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) =>
|
||||
warning: v =>
|
||||
{
|
||||
// TODO: Maybe actually implement UI for the validity check...
|
||||
if (v == XivChatType.None)
|
||||
|
|
@ -62,12 +62,6 @@ public class SettingsTabGeneral : SettingsTab
|
|||
c => c.PrintPluginsWelcomeMsg,
|
||||
(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>(
|
||||
Loc.Localize("DalamudSettingsSystemMenu", "Dalamud buttons in system menu"),
|
||||
Loc.Localize("DalamudSettingsSystemMenuMsgHint", "Add buttons for Dalamud plugins and settings to the system menu."),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -114,7 +112,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <summary>
|
||||
/// Gets a value indicating whether or not auto-updates have already completed this session.
|
||||
/// </summary>
|
||||
public bool IsAutoUpdateComplete => Service<ChatHandlers>.Get().IsAutoUpdateComplete;
|
||||
public bool IsAutoUpdateComplete => Service<AutoUpdateManager>.Get().IsAutoUpdateComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository from which this plugin was installed.
|
||||
|
|
|
|||
27
Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs
Normal file
27
Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateBehavior.cs
Normal 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,
|
||||
}
|
||||
458
Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
Normal file
458
Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CheapLoc;
|
||||
|
||||
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;
|
||||
|
||||
/// <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", false);
|
||||
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)
|
||||
{
|
||||
Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count);
|
||||
|
||||
if (updatablePlugins.Count == 0)
|
||||
return;
|
||||
|
||||
var notification = this.GetBaseNotification(new Notification
|
||||
{
|
||||
Title = Locs.NotificationTitleUpdatingPlugins,
|
||||
Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count),
|
||||
Type = NotificationType.Info,
|
||||
InitialDuration = TimeSpan.MaxValue,
|
||||
ShowIndeterminateIfNoExpiry = false,
|
||||
UserDismissable = false,
|
||||
Progress = 0,
|
||||
Icon = INotificationIcon.From(FontAwesomeIcon.Download),
|
||||
Minimized = false,
|
||||
});
|
||||
|
||||
var progress = new Progress<PluginManager.PluginUpdateProgress>();
|
||||
progress.ProgressChanged += (_, progress) =>
|
||||
{
|
||||
notification.Content = Locs.NotificationContentUpdating(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);
|
||||
|
||||
notification.DrawActions += _ =>
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
if (DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller))
|
||||
{
|
||||
Service<DalamudInterface>.Get().OpenPluginInstaller();
|
||||
notification.DismissNow();
|
||||
}
|
||||
};
|
||||
|
||||
// Update the notification to show the final state
|
||||
var pluginUpdateStatusEnumerable = pluginStates as PluginUpdateStatus[] ?? pluginStates.ToArray();
|
||||
if (pluginUpdateStatusEnumerable.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 = Locs.NotificationTitleUpdatesSuccessful;
|
||||
notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized;
|
||||
notification.Type = NotificationType.Success;
|
||||
notification.Content = Locs.NotificationContentUpdatesSuccessful;
|
||||
}
|
||||
else
|
||||
{
|
||||
notification.Title = Locs.NotificationTitleUpdatesFailed;
|
||||
notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized;
|
||||
notification.Type = NotificationType.Error;
|
||||
notification.Content = Locs.NotificationContentUpdatesFailed;
|
||||
|
||||
var failedPlugins = pluginUpdateStatusEnumerable
|
||||
.Where(x => x.Status != PluginUpdateStatus.StatusKind.Success)
|
||||
.Select(x => x.Name).ToList();
|
||||
|
||||
notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyUpdatesAreAvailable(ICollection<AvailablePluginUpdate> updatablePlugins)
|
||||
{
|
||||
if (updatablePlugins.Count == 0)
|
||||
return;
|
||||
|
||||
var notification = this.GetBaseNotification(new Notification
|
||||
{
|
||||
Title = Locs.NotificationTitleUpdatesAvailable,
|
||||
Content = Locs.NotificationContentUpdatesAvailable(updatablePlugins.Count),
|
||||
MinimizedText = Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count),
|
||||
Type = NotificationType.Info,
|
||||
InitialDuration = TimeSpan.MaxValue,
|
||||
ShowIndeterminateIfNoExpiry = false,
|
||||
Icon = INotificationIcon.From(FontAwesomeIcon.Download),
|
||||
});
|
||||
|
||||
notification.DrawActions += _ =>
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
|
||||
if (DalamudComponents.PrimaryButton(Locs.NotificationButtonUpdate))
|
||||
{
|
||||
this.KickOffAutoUpdates(updatablePlugins);
|
||||
notification.DismissNow();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller))
|
||||
{
|
||||
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.OnlyAny(ConditionFlag.NormalConditions,
|
||||
ConditionFlag.Jumping,
|
||||
ConditionFlag.Mounted,
|
||||
ConditionFlag.UsingParasol);
|
||||
}
|
||||
|
||||
private bool IsPluginManagerReady()
|
||||
{
|
||||
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<string> failedPlugins)
|
||||
=> string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugins: {0}"), string.Join(", ", failedPlugins));
|
||||
}
|
||||
}
|
||||
|
|
@ -977,32 +977,39 @@ internal class PluginManager : IInternalDisposableService
|
|||
/// <summary>
|
||||
/// Update all non-dev plugins.
|
||||
/// </summary>
|
||||
/// <param name="ignoreDisabled">Ignore disabled plugins.</param>
|
||||
/// <param name="toUpdate">List of plugins to update.</param>
|
||||
/// <param name="dryRun">Perform a dry run, don't install anything.</param>
|
||||
/// <param name="autoUpdate">If this action was performed as part of an auto-update.</param>
|
||||
/// <param name="progress">An <see cref="IProgress{T}"/> implementation to receive progress updates about the installation status.</param>
|
||||
/// <returns>Success or failure and a list of updated plugin metadata.</returns>
|
||||
public async Task<IEnumerable<PluginUpdateStatus>> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false)
|
||||
public async Task<IEnumerable<PluginUpdateStatus>> UpdatePluginsAsync(
|
||||
ICollection<AvailablePluginUpdate> toUpdate,
|
||||
bool dryRun,
|
||||
bool autoUpdate = false,
|
||||
IProgress<PluginUpdateProgress>? progress = null)
|
||||
{
|
||||
Log.Information("Starting plugin update");
|
||||
|
||||
var updateTasks = new List<Task<PluginUpdateStatus>>();
|
||||
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<PluginUpdateStatus> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1832,6 +1856,11 @@ internal class PluginManager : IInternalDisposableService
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class representing progress of an update operation.
|
||||
/// </summary>
|
||||
public record PluginUpdateProgress(int PluginsProcessed, int TotalPlugins, IPluginManifest CurrentPluginManifest);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
|
@ -81,4 +82,19 @@ public interface IClientState
|
|||
/// Gets a value indicating whether the client is currently in Group Pose (GPose) mode.
|
||||
/// </summary>
|
||||
public bool IsGPosing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the client is currently "idle". This means a player is not logged in, or is notctively in combat
|
||||
/// or doing anything that we may not want to disrupt.
|
||||
/// </summary>
|
||||
/// <param name="blockingFlag">An outvar containing the first observed condition blocking the "idle" state. 0 if idle.</param>
|
||||
/// <returns>Returns true if the client is idle, false otherwise.</returns>
|
||||
public bool IsClientIdle(out ConditionFlag blockingFlag);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the client is currently "idle". This means a player is not logged in, or is notctively in combat
|
||||
/// or doing anything that we may not want to disrupt.
|
||||
/// </summary>
|
||||
/// <returns>Returns true if the client is idle, false otherwise.</returns>
|
||||
public bool IsClientIdle() => this.IsClientIdle(out _);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Dalamud.Game.ClientState.Conditions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
|
|
@ -38,6 +40,12 @@ public interface ICondition
|
|||
|
||||
/// <inheritdoc cref="this[int]"/>
|
||||
public bool this[ConditionFlag flag] => this[(int)flag];
|
||||
|
||||
/// <summary>
|
||||
/// Convert the conditions array to a set of all set condition flags.
|
||||
/// </summary>
|
||||
/// <returns>Returns a set.</returns>
|
||||
public IReadOnlySet<ConditionFlag> AsReadOnlySet();
|
||||
|
||||
/// <summary>
|
||||
/// Check if any condition flags are set.
|
||||
|
|
@ -51,4 +59,26 @@ public interface ICondition
|
|||
/// <returns>Whether any single provided flag is set.</returns>
|
||||
/// <param name="flags">The condition flags to check.</param>
|
||||
public bool Any(params ConditionFlag[] flags);
|
||||
|
||||
/// <summary>
|
||||
/// Check that the specified condition flags are *not* present in the current conditions.
|
||||
/// </summary>
|
||||
/// <param name="except">The array of flags to check.</param>
|
||||
/// <returns>Returns false if any of the listed conditions are present, true otherwise.</returns>
|
||||
public bool AnyExcept(params ConditionFlag[] except);
|
||||
|
||||
/// <summary>
|
||||
/// Check that *only* any of the condition flags specified are set.
|
||||
/// </summary>
|
||||
/// <param name="other">The array of flags to check.</param>
|
||||
/// <returns>Returns a bool.</returns>
|
||||
public bool OnlyAny(params ConditionFlag[] other);
|
||||
|
||||
/// <summary>
|
||||
/// Check that *only* the specified flags are set. Unlike <see cref="OnlyAny"/>, this method requires that all the
|
||||
/// specified flags are set and no others are present.
|
||||
/// </summary>
|
||||
/// <param name="other">The array of flags to check.</param>
|
||||
/// <returns>Returns a bool.</returns>
|
||||
public bool EqualTo(params ConditionFlag[] other);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue