mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
562 lines
24 KiB
C#
562 lines
24 KiB
C#
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.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;
|
|
using Dalamud.Interface.ImGuiNotification.Internal;
|
|
using Dalamud.Interface.Internal;
|
|
using Dalamud.Interface.Internal.DesignSystem;
|
|
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(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.
|
|
/// </summary>
|
|
private static readonly TimeSpan TimeBetweenUpdateChecksIfDismissed = TimeSpan.FromHours(12);
|
|
|
|
/// <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 readonly Task<DalamudLinkPayload> openInstallerWindowLinkTask;
|
|
|
|
private DateTime? loginTime;
|
|
private DateTime? nextUpdateCheckTime;
|
|
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 += (int type, int code) => this.OnLogout();
|
|
});
|
|
Service<Framework>.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; });
|
|
|
|
this.openInstallerWindowLinkTask =
|
|
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", () =>
|
|
{
|
|
this.hasStartedInitialUpdateThisSession = false;
|
|
this.OnLogin();
|
|
return true;
|
|
});
|
|
console.AddCommand("dalamud.autoupdate.force_check", "Force a check for updates", () =>
|
|
{
|
|
this.nextUpdateCheckTime = DateTime.Now + TimeSpan.FromSeconds(5);
|
|
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,
|
|
|
|
AutoUpdateBehavior.UpdateMainRepo => UpdateListingRestriction.AllowMainRepo,
|
|
AutoUpdateBehavior.UpdateAll => UpdateListingRestriction.Unrestricted,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null),
|
|
};
|
|
}
|
|
|
|
private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification)
|
|
{
|
|
if (primary ?
|
|
DalamudComponents.PrimaryButton(Locs.NotificationButtonOpenPluginInstaller) :
|
|
DalamudComponents.SecondaryButton(Locs.NotificationButtonOpenPluginInstaller))
|
|
{
|
|
Service<DalamudInterface>.Get().OpenPluginInstallerTo(kind);
|
|
notification.DismissNow();
|
|
}
|
|
}
|
|
|
|
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.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)
|
|
{
|
|
// List all plugins in the notification
|
|
Log.Verbose("Running initial auto-update, notifying for {Num} plugins", currentlyUpdatablePlugins.Count);
|
|
this.NotifyUpdatesAreAvailable(currentlyUpdatablePlugins);
|
|
return;
|
|
}
|
|
|
|
Log.Verbose("Running initial auto-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.nextUpdateCheckTime != null &&
|
|
DateTime.Now > this.nextUpdateCheckTime &&
|
|
this.updateNotification == null)
|
|
{
|
|
Log.Verbose("Starting periodic update check");
|
|
this.pluginManager.ReloadPluginMastersAsync()
|
|
.ContinueWith(
|
|
t =>
|
|
{
|
|
if (t.IsFaulted || t.IsCanceled)
|
|
{
|
|
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));
|
|
|
|
if (updatable.Count > 0)
|
|
{
|
|
this.NotifyUpdatesAreAvailable(updatable);
|
|
}
|
|
else
|
|
{
|
|
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
|
|
Log.Verbose(
|
|
"Auto update found nothing to do, next update at {Time}",
|
|
this.nextUpdateCheckTime);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// Schedule the next update opportunistically for when this closes.
|
|
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
|
|
};
|
|
|
|
return this.updateNotification!;
|
|
}
|
|
|
|
private void KickOffAutoUpdates(ICollection<AvailablePluginUpdate> updatablePlugins, IActiveNotification? notification = null)
|
|
{
|
|
this.autoUpdateTask =
|
|
Task.Run(() => this.RunAutoUpdates(updatablePlugins, notification))
|
|
.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, IActiveNotification? notification = null)
|
|
{
|
|
Log.Information("Found {UpdatablePluginsCount} plugins to update", updatablePlugins.Count);
|
|
|
|
if (updatablePlugins.Count == 0)
|
|
return;
|
|
|
|
notification ??= this.GetBaseNotification(new Notification());
|
|
notification.Title = Locs.NotificationTitleUpdatingPlugins;
|
|
notification.Content = Locs.NotificationContentPreparingToUpdate(updatablePlugins.Count);
|
|
notification.Type = NotificationType.Info;
|
|
notification.InitialDuration = TimeSpan.MaxValue;
|
|
notification.ShowIndeterminateIfNoExpiry = false;
|
|
notification.UserDismissable = false;
|
|
notification.Progress = 0;
|
|
notification.Icon = INotificationIcon.From(FontAwesomeIcon.Download);
|
|
notification.Minimized = false;
|
|
|
|
var progress = new Progress<PluginManager.PluginUpdateProgress>();
|
|
progress.ProgressChanged += (_, updateProgress) =>
|
|
{
|
|
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))
|
|
{
|
|
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 = pluginStates
|
|
.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),
|
|
MinimizedText = Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count),
|
|
Type = NotificationType.Info,
|
|
InitialDuration = TimeSpan.MaxValue,
|
|
ShowIndeterminateIfNoExpiry = false,
|
|
Icon = INotificationIcon.From(FontAwesomeIcon.Download),
|
|
});
|
|
|
|
void DrawNotificationContent(INotificationDrawArgs args)
|
|
{
|
|
ImGuiHelpers.ScaledDummy(2);
|
|
|
|
if (DalamudComponents.PrimaryButton(Locs.NotificationButtonUpdate))
|
|
{
|
|
notification.DrawActions -= DrawNotificationContent;
|
|
this.KickOffAutoUpdates(updatablePlugins, notification);
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
DrawOpenInstallerNotificationButton(false, PluginInstallerOpenKind.UpdateablePlugins, notification);
|
|
}
|
|
|
|
notification.DrawActions += DrawNotificationContent;
|
|
|
|
// If the user dismisses the notification, we don't want to spam them with notifications. Schedule the next
|
|
// auto update further out. Since this is registered after the initial OnDismiss, this should take precedence.
|
|
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.openInstallerWindowLinkTask.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(
|
|
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(ICollection<AvailablePluginUpdate> updatablePlugins)
|
|
=> (updatablePlugins.Count == 1
|
|
? Loc.Localize(
|
|
"AutoUpdateUpdatesAvailableContentSingular",
|
|
"There is a plugin that can be updated:")
|
|
: string.Format(
|
|
Loc.Localize(
|
|
"AutoUpdateUpdatesAvailableContentPlural",
|
|
"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") :
|
|
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...") :
|
|
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));
|
|
}
|
|
}
|