- Add chat notification back to AutoUpdate (#2146)

- ImRaii UI elements in AutoUpdate tab
- Fix scoping in IconButton
This commit is contained in:
Infi 2024-12-19 22:19:50 +01:00 committed by GitHub
parent f7ef68d65e
commit 2e6cb6ef00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 166 additions and 114 deletions

View file

@ -9,6 +9,10 @@ using Dalamud.Console;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.EventArgs;
@ -31,17 +35,17 @@ namespace Dalamud.Plugin.Internal.AutoUpdate;
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(2);
/// <summary>
/// Time we should wait between scheduled update checks if the user has dismissed the notification,
/// instead of updating. We don't want to spam the user with notifications.
@ -56,28 +60,30 @@ internal class AutoUpdateManager : IServiceType
[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? nextUpdateCheckTime;
private DateTime? unblockedSince;
private bool hasStartedInitialUpdateThisSession;
private IActiveNotification? updateNotification;
private Task? autoUpdateTask;
private readonly Task<DalamudLinkPayload> openInstallerWindowLink;
/// <summary>
/// Initializes a new instance of the <see cref="AutoUpdateManager"/> class.
/// </summary>
@ -92,7 +98,18 @@ internal class AutoUpdateManager : IServiceType
t.Result.Logout += (int type, int code) => this.OnLogout();
});
Service<Framework>.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; });
this.openInstallerWindowLink =
Service<ChatGui>.GetAsync().ContinueWith(
chatGuiTask => chatGuiTask.Result.AddChatLinkHandler(
"Dalamud",
1001,
(_, _) =>
{
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins);
}));
this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", false);
console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () =>
{
@ -106,36 +123,36 @@ internal class AutoUpdateManager : IServiceType
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; }
/// <summary>
/// Gets the time of the next scheduled update check.
/// </summary>
public DateTime? NextUpdateCheckTime => this.nextUpdateCheckTime;
/// <summary>
/// Gets the time the auto-update was unblocked.
/// </summary>
public DateTime? UnblockedSince => this.unblockedSince;
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,
@ -144,7 +161,7 @@ internal class AutoUpdateManager : IServiceType
_ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null),
};
}
private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification)
{
if (primary ?
@ -179,7 +196,7 @@ internal class AutoUpdateManager : IServiceType
this.updateNotification = null;
}
}
// If we're blocked, we don't do anything.
if (!isUnblocked)
return;
@ -199,16 +216,16 @@ internal class AutoUpdateManager : IServiceType
if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin))
{
this.hasStartedInitialUpdateThisSession = true;
var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior));
if (currentlyUpdatablePlugins.Count == 0)
{
this.IsAutoUpdateComplete = true;
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
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)
@ -241,6 +258,7 @@ internal class AutoUpdateManager : IServiceType
Log.Error(t.Exception!, "Failed to reload plugin masters for auto-update");
}
Log.Verbose($"Available Updates: {string.Join(", ", this.pluginManager.UpdatablePlugins.Select(s => s.UpdateManifest.InternalName))}");
var updatable = this.GetAvailablePluginUpdates(
DecideUpdateListingRestriction(behavior));
@ -252,7 +270,7 @@ internal class AutoUpdateManager : IServiceType
{
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
Log.Verbose(
"Auto update found nothing to do, next update at {Time}",
"Auto update found nothing to do, next update at {Time}",
this.nextUpdateCheckTime);
}
});
@ -263,13 +281,13 @@ internal class AutoUpdateManager : IServiceType
{
if (this.updateNotification != null)
throw new InvalidOperationException("Already showing a notification");
this.updateNotification = this.notificationManager.AddNotification(notification);
this.updateNotification.Dismiss += _ =>
{
this.updateNotification = null;
// Schedule the next update opportunistically for when this closes.
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
};
@ -291,7 +309,7 @@ internal class AutoUpdateManager : IServiceType
{
Log.Warning("Auto-update task was canceled");
}
this.autoUpdateTask = null;
this.IsAutoUpdateComplete = true;
});
@ -321,20 +339,20 @@ internal class AutoUpdateManager : IServiceType
notification.Content = Locs.NotificationContentUpdating(updateProgress.CurrentPluginManifest.Name);
notification.Progress = (float)updateProgress.PluginsProcessed / updateProgress.TotalPlugins;
};
var pluginStates = (await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress)).ToList();
this.pluginManager.PrintUpdatedPlugins(pluginStates, Loc.Localize("DalamudPluginAutoUpdate", "The following plugins were auto-updated:"));
notification.Progress = 1;
notification.UserDismissable = true;
notification.HardExpiry = DateTime.Now.AddSeconds(30);
notification.DrawActions += _ =>
{
ImGuiHelpers.ScaledDummy(2);
DrawOpenInstallerNotificationButton(true, PluginInstallerOpenKind.InstalledPlugins, notification);
};
// Update the notification to show the final state
if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success))
{
@ -342,7 +360,7 @@ internal class AutoUpdateManager : IServiceType
// 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;
@ -354,11 +372,11 @@ internal class AutoUpdateManager : IServiceType
notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized;
notification.Type = NotificationType.Error;
notification.Content = Locs.NotificationContentUpdatesFailed;
var failedPlugins = pluginStates
.Where(x => x.Status != PluginUpdateStatus.StatusKind.Success)
.Select(x => x.Name).ToList();
notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins);
}
}
@ -367,7 +385,7 @@ internal class AutoUpdateManager : IServiceType
{
if (updatablePlugins.Count == 0)
return;
var notification = this.GetBaseNotification(new Notification
{
Title = Locs.NotificationTitleUpdatesAvailable,
@ -400,16 +418,44 @@ internal class AutoUpdateManager : IServiceType
notification.Dismiss += args =>
{
if (args.Reason != NotificationDismissReason.Manual) return;
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed;
Log.Verbose("User dismissed update notification, next check at {Time}", this.nextUpdateCheckTime);
};
// Send out a chat message only if the user requested so
if (!this.config.SendUpdateNotificationToChat)
return;
var chatGui = Service<ChatGui>.GetNullable();
if (chatGui == null)
{
Log.Verbose("Unable to get chat gui, discard notification for chat.");
return;
}
chatGui.Print(new XivChatEntry
{
Message = new SeString(new List<Payload>
{
new TextPayload(Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count)),
new TextPayload(" ["),
new UIForegroundPayload(500),
this.openInstallerWindowLink.Result,
new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
RawPayload.LinkTerminator,
new UIForegroundPayload(0),
new TextPayload("]"),
}),
Type = XivChatType.Urgent,
});
}
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(
@ -423,14 +469,14 @@ internal class AutoUpdateManager : IServiceType
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;
@ -442,7 +488,7 @@ internal class AutoUpdateManager : IServiceType
{
this.loginTime = DateTime.Now;
}
private void OnLogout()
{
this.loginTime = null;
@ -452,7 +498,7 @@ internal class AutoUpdateManager : IServiceType
{
var condition = Service<Condition>.Get();
return this.IsPluginManagerReady() &&
!this.dalamudInterface.IsPluginInstallerOpen &&
!this.dalamudInterface.IsPluginInstallerOpen &&
condition.OnlyAny(ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
@ -469,21 +515,21 @@ internal class AutoUpdateManager : IServiceType
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(ICollection<AvailablePluginUpdate> updatablePlugins)
@ -497,20 +543,20 @@ internal class AutoUpdateManager : IServiceType
"There are {0} plugins that can be updated:"),
updatablePlugins.Count))
+ "\n\n" + string.Join(", ", updatablePlugins.Select(x => x.InstalledPlugin.Manifest.Name));
public static string NotificationContentUpdatesAvailableMinimized(int numUpdates)
=> numUpdates == 1 ?
Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") :
Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") :
string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedPlural", "{0} plugin updates available"), numUpdates);
public static string NotificationContentPreparingToUpdate(int numPlugins)
=> numPlugins == 1 ?
Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") :
Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") :
string.Format(Loc.Localize("AutoUpdatePreparingToUpdatePlural", "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 plugin(s): {0}"), string.Join(", ", failedPlugins));
}