diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs index cd7d5c186..c98a0d6df 100644 --- a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs +++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs @@ -1,17 +1,15 @@ using System; using System.ComponentModel; using System.Globalization; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Windows.UI.Notifications; -using Artemis.UI.Screens.Settings; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Updating; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services.MainWindow; using Avalonia.Threading; using Microsoft.Toolkit.Uwp.Notifications; -using ReactiveUI; namespace Artemis.UI.Windows.Providers; @@ -20,18 +18,34 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider private readonly Func _getReleaseInstaller; private readonly IMainWindowService _mainWindowService; private readonly IUpdateService _updateService; + private readonly IWorkshopUpdateService _workshopUpdateService; private readonly IRouter _router; private CancellationTokenSource? _cancellationTokenSource; - public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, IUpdateService updateService, IRouter router, Func getReleaseInstaller) + public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, + IUpdateService updateService, + IWorkshopUpdateService workshopUpdateService, + IRouter router, Func getReleaseInstaller) { _mainWindowService = mainWindowService; _updateService = updateService; + _workshopUpdateService = workshopUpdateService; _router = router; _getReleaseInstaller = getReleaseInstaller; ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated; } + /// + public void ShowWorkshopNotification(int updatedEntries) + { + new ToastContentBuilder().AddText(updatedEntries == 1 ? "Workshop update installed" : "Workshop updates installed") + .AddText(updatedEntries == 1 ? "A workshop update has been installed" : $"{updatedEntries} workshop updates have been installed") + .AddArgument("action", "view-library") + .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-library")) + .AddButton(new ToastButton().SetContent("Don't show again").AddArgument("action", "disable-workshop-notifications")) + .Show(); + } + /// public void ShowNotification(Guid releaseId, string releaseVersion) { @@ -57,14 +71,8 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider private void ViewRelease(Guid? releaseId) { - Dispatcher.UIThread.Invoke(async () => - { - _mainWindowService.OpenMainWindow(); - if (releaseId != null && releaseId.Value != Guid.Empty) - await _router.Navigate($"settings/releases/{releaseId}"); - else - await _router.Navigate("settings/releases"); - }); + string route = releaseId != null && releaseId.Value != Guid.Empty ? $"settings/releases/{releaseId}" : "settings/releases"; + NavigateToRoute(route); } private async Task InstallRelease(Guid releaseId, string releaseVersion) @@ -153,11 +161,9 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider ToastArguments args = ToastArguments.Parse(e.Argument); Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty; - string releaseVersion = args.Get("releaseVersion"); - string action = "view-changes"; - if (args.Contains("action")) - action = args.Get("action"); - + string releaseVersion = args.Contains("releaseVersion") ? args.Get("releaseVersion") : string.Empty; + string action = args.Contains("action") ? args.Get("action") : "view-changes"; + if (action == "install") await InstallRelease(releaseId, releaseVersion); else if (action == "view-changes") @@ -166,5 +172,18 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider _cancellationTokenSource?.Cancel(); else if (action == "restart-for-update") _updateService.RestartForUpdate("WindowsNotification", false); + else if (action == "disable-workshop-notifications") + _workshopUpdateService.DisableNotifications(); + else if (action == "view-library") + NavigateToRoute("workshop/library"); + } + + private void NavigateToRoute(string route) + { + Dispatcher.UIThread.Invoke(async () => + { + _mainWindowService.OpenMainWindow(); + await _router.Navigate(route); + }); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml index 861293d66..89666ed0a 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml @@ -194,7 +194,7 @@ Auto-install updates - If enabled, new updates will automatically be installed. + Automatically install new versions of Artemis in the background when available. @@ -202,6 +202,21 @@ + + + + + Show workshop update notifications + + + Show a desktop notification whenever workshop updates are installed. + + + + + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index 87bb9c949..10ff14cc2 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -102,6 +102,7 @@ public class GeneralTabViewModel : RoutableScreen public bool IsAutoRunSupported => _autoRunProvider != null; public bool IsWindows11 => OSVersionHelper.IsWindows11(); + public bool IsWindows => OSVersionHelper.IsWindows(); public ObservableCollection LayerBrushDescriptors { get; } public ObservableCollection GraphicsContexts { get; } @@ -158,6 +159,7 @@ public class GeneralTabViewModel : RoutableScreen public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); public PluginSetting EnableMica => _settingsService.GetSetting("UI.EnableMica", true); public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); + public PluginSetting WorkshopShowNotifications => _settingsService.GetSetting("Workshop.ShowNotifications", true); public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true); public PluginSetting ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); public PluginSetting CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information); diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs index db9f6d8c2..2c15c9017 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs @@ -101,14 +101,27 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase return; } + // If not the latest version, warn and offer to disable auto-updates + bool disableAutoUpdates = false; + if (Release.Id != Release.Entry.LatestReleaseId) + { + disableAutoUpdates = await _windowService.ShowConfirmContentDialog( + "You are installing an older version of this entry", + "Would you like to disable auto-updates for this entry?", + "Yes", + "No" + ); + } + _cts = new CancellationTokenSource(); InstallProgress = 0; InstallationInProgress = true; try { EntryInstallResult result = await _workshopService.InstallEntry(Release.Entry, Release, _progress, _cts.Token); - if (result.IsSuccess) + if (result.IsSuccess && result.Entry != null) { + _workshopService.SetAutoUpdate(result.Entry, !disableAutoUpdates); _notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show(); InstallationInProgress = false; await Manage(); @@ -156,7 +169,7 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase await UninstallPluginPrerequisites(installedEntry); await _workshopService.UninstallEntry(installedEntry, CancellationToken.None); - + _notificationService.CreateNotification().WithTitle("Entry uninstalled").WithSeverity(NotificationSeverity.Success).Show(); } finally diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml index 7576497be..bebac42fd 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml @@ -66,11 +66,9 @@ - - - - downloads + + @@ -89,7 +87,7 @@ - Auto-update + Auto-update diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs index 1ff8406e2..54504c0c8 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml.cs @@ -1,7 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using Avalonia.ReactiveUI; +using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Workshop.Library.Tabs; diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs index 990db9401..6307a8557 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Reactive; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.Core; @@ -9,6 +10,7 @@ using Artemis.Core.Services; using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; using Artemis.UI.Screens.Plugins; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; @@ -25,16 +27,19 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase { private readonly IWorkshopClient _client; private readonly IWorkshopService _workshopService; + private readonly IWorkshopUpdateService _workshopUpdateService; private readonly IRouter _router; private readonly IWindowService _windowService; private readonly IPluginManagementService _pluginManagementService; private readonly ISettingsVmFactory _settingsVmFactory; [Notify] private bool _updateAvailable; + [Notify] private bool _autoUpdate; public InstalledTabItemViewModel(InstalledEntry entry, IWorkshopClient client, IWorkshopService workshopService, + IWorkshopUpdateService workshopUpdateService, IRouter router, IWindowService windowService, IPluginManagementService pluginManagementService, @@ -42,10 +47,13 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase { _client = client; _workshopService = workshopService; + _workshopUpdateService = workshopUpdateService; _router = router; _windowService = windowService; _pluginManagementService = pluginManagementService; _settingsVmFactory = settingsVmFactory; + _autoUpdate = entry.AutoUpdate; + Entry = entry; this.WhenActivatedAsync(async _ => @@ -65,6 +73,8 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId; } }); + + this.WhenAnyValue(vm => vm.AutoUpdate).Skip(1).Subscribe(_ => AutoUpdateToggled()); } public InstalledEntry Entry { get; } @@ -108,4 +118,18 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { })); await pluginViewModel.ExecuteRemovePrerequisites(true); } + + private void AutoUpdateToggled() + { + _workshopService.SetAutoUpdate(Entry, AutoUpdate); + + if (!AutoUpdate) + return; + + Task.Run(async () => + { + await _workshopUpdateService.AutoUpdateEntry(Entry); + UpdateAvailable = Entry.ReleaseId != Entry.LatestReleaseId; + }); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs b/src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs new file mode 100644 index 000000000..316c3b479 --- /dev/null +++ b/src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Artemis.WebClient.Workshop.Models; + +namespace Artemis.UI.Services.Interfaces; + +public interface IWorkshopUpdateService : IArtemisUIService +{ + /// + /// Automatically updates all installed entries that have auto-update enabled and have a new version available. + /// + /// A task that represents the asynchronous operation + Task AutoUpdateEntries(); + + /// + /// Automatically updates the provided entry if a new version is available. + /// + /// The entry to update. + /// A task of if the entry was updated, otherwise. + Task AutoUpdateEntry(InstalledEntry entry); + + /// + /// Disable workshop update notifications. + /// + void DisableNotifications(); +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/BasicUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/BasicUpdateNotificationProvider.cs index b5e84fa9d..4757f31d5 100644 --- a/src/Artemis.UI/Services/Updating/BasicUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/BasicUpdateNotificationProvider.cs @@ -57,6 +57,18 @@ public class BasicUpdateNotificationProvider : IUpdateNotificationProvider await _router.Navigate("settings/releases"); } + /// + public void ShowWorkshopNotification(int updatedEntries) + { + _notificationService.CreateNotification() + .WithTitle(updatedEntries == 1 ? "Workshop update installed" : "Workshop updates installed") + .WithMessage(updatedEntries == 1 ? "A workshop update has been installed" : $"{updatedEntries} workshop updates have been installed") + .WithSeverity(NotificationSeverity.Success) + .WithTimeout(TimeSpan.FromSeconds(15)) + .HavingButton(b => b.WithText("View library").WithAction(async () => await _router.Navigate("settings/workshop"))) + .Show(); + } + /// public void ShowNotification(Guid releaseId, string releaseVersion) { diff --git a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs index 0dc3bf356..3c3c32523 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs @@ -4,6 +4,7 @@ namespace Artemis.UI.Services.Updating; public interface IUpdateNotificationProvider { + void ShowWorkshopNotification(int updatedEntries); void ShowNotification(Guid releaseId, string releaseVersion); void ShowInstalledNotification(string installedVersion); } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index a6259db37..633e0c69e 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -8,8 +8,10 @@ using Artemis.Core.Services; using Artemis.Storage.Repositories; using Artemis.Storage.Repositories.Interfaces; using Artemis.UI.Exceptions; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; +using Artemis.WebClient.Workshop.Services; using Serilog; using StrawberryShake; using Timer = System.Timers.Timer; @@ -26,6 +28,7 @@ public class UpdateService : IUpdateService private readonly ILogger _logger; private readonly IMainWindowService _mainWindowService; private readonly IReleaseRepository _releaseRepository; + private readonly IWorkshopUpdateService _workshopUpdateService; private readonly Lazy _updateNotificationProvider; private readonly Platform _updatePlatform; private readonly IUpdatingClient _updatingClient; @@ -38,6 +41,7 @@ public class UpdateService : IUpdateService IMainWindowService mainWindowService, IUpdatingClient updatingClient, IReleaseRepository releaseRepository, + IWorkshopUpdateService workshopUpdateService, Lazy updateNotificationProvider, Func getReleaseInstaller) { @@ -45,6 +49,7 @@ public class UpdateService : IUpdateService _mainWindowService = mainWindowService; _updatingClient = updatingClient; _releaseRepository = releaseRepository; + _workshopUpdateService = workshopUpdateService; _updateNotificationProvider = updateNotificationProvider; _getReleaseInstaller = getReleaseInstaller; @@ -65,72 +70,7 @@ public class UpdateService : IUpdateService timer.Elapsed += HandleAutoUpdateEvent; timer.Start(); } - - private void ProcessReleaseStatus() - { - string currentVersion = Constants.CurrentVersion; - bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion); - PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version; - - if (!Directory.Exists(Constants.UpdatingFolder)) - return; - - // Clean up the update folder, leaving only the last ZIP - foreach (string file in Directory.GetFiles(Constants.UpdatingFolder)) - { - if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip") - continue; - - try - { - _logger.Debug("Cleaning up old update file at {FilePath}", file); - File.Delete(file); - } - catch (Exception e) - { - _logger.Warning(e, "Failed to clean up old update file at {FilePath}", file); - } - } - - if (updated) - _updateNotificationProvider.Value.ShowInstalledNotification(currentVersion); - } - - private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release) - { - _updateNotificationProvider.Value.ShowNotification(release.Id, release.Version); - } - - private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release) - { - ReleaseInstaller installer = _getReleaseInstaller(release.Id); - await installer.InstallAsync(CancellationToken.None); - RestartForUpdate("AutoInstallUpdate", true); - } - - private async void HandleAutoUpdateEvent(object? sender, EventArgs e) - { - if (Constants.CurrentVersion == "local") - return; - - // The event can trigger from multiple sources with a timer acting as a fallback, only actually perform an action once per max 59 minutes - if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59)) - return; - _lastAutoUpdateCheck = DateTime.UtcNow; - - if (!_autoCheck.Value || _suspendAutoCheck) - return; - - try - { - await CheckForUpdate(); - } - catch (Exception ex) - { - _logger.Warning(ex, "Auto update-check failed"); - } - } - + /// public string Channel { get; private set; } = "master"; @@ -139,7 +79,7 @@ public class UpdateService : IUpdateService /// public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; } - + /// public async Task CacheLatestRelease() { @@ -257,4 +197,86 @@ public class UpdateService : IUpdateService _logger.Information("Update service initialized for {Channel} channel", Channel); return false; } + + private async Task AutoCheckForUpdates() + { + // Don't perform auto-updates if the current version is local + if (Constants.CurrentVersion == "local") + return false; + + // Don't perform auto-updates if the setting is disabled or an update was found but not yet installed + if (!_autoCheck.Value || _suspendAutoCheck) + return false; + + try + { + return await CheckForUpdate() && _autoInstall.Value; + } + catch (Exception ex) + { + _logger.Warning(ex, "Auto update-check failed"); + } + + return false; + } + + private void ProcessReleaseStatus() + { + string currentVersion = Constants.CurrentVersion; + bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion); + PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version; + + if (!Directory.Exists(Constants.UpdatingFolder)) + return; + + // Clean up the update folder, leaving only the last ZIP + foreach (string file in Directory.GetFiles(Constants.UpdatingFolder)) + { + if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip") + continue; + + try + { + _logger.Debug("Cleaning up old update file at {FilePath}", file); + File.Delete(file); + } + catch (Exception e) + { + _logger.Warning(e, "Failed to clean up old update file at {FilePath}", file); + } + } + + if (updated) + _updateNotificationProvider.Value.ShowInstalledNotification(currentVersion); + } + + private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release) + { + _updateNotificationProvider.Value.ShowNotification(release.Id, release.Version); + } + + private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release) + { + ReleaseInstaller installer = _getReleaseInstaller(release.Id); + await installer.InstallAsync(CancellationToken.None); + RestartForUpdate("AutoInstallUpdate", true); + } + + private async void HandleAutoUpdateEvent(object? sender, EventArgs e) + { + // The event can trigger from multiple sources with a timer acting as a fallback, only actually perform an action once per max 59 minutes + if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59)) + return; + + _lastAutoUpdateCheck = DateTime.UtcNow; + + if (await AutoCheckForUpdates()) + { + _logger.Information("Auto-installing update, not performing workshop update check"); + } + else + { + await _workshopUpdateService.AutoUpdateEntries(); + } + } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs b/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs new file mode 100644 index 000000000..a617e3786 --- /dev/null +++ b/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using Serilog; +using StrawberryShake; + +namespace Artemis.UI.Services.Updating; + +public class WorkshopUpdateService : IWorkshopUpdateService +{ + private readonly ILogger _logger; + private readonly IWorkshopClient _client; + private readonly INotificationService _notificationService; + private readonly IWorkshopService _workshopService; + private readonly Lazy _updateNotificationProvider; + private readonly PluginSetting _showNotifications; + + public WorkshopUpdateService(ILogger logger, IWorkshopClient client, IWorkshopService workshopService, ISettingsService settingsService, + Lazy updateNotificationProvider) + { + _logger = logger; + _client = client; + _workshopService = workshopService; + _updateNotificationProvider = updateNotificationProvider; + _showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true); + } + + public async Task AutoUpdateEntries() + { + _logger.Information("Checking for workshop updates"); + int checkedEntries = 0; + int updatedEntries = 0; + + foreach (InstalledEntry entry in _workshopService.GetInstalledEntries()) + { + if (!entry.AutoUpdate) + continue; + + checkedEntries++; + bool updated = await AutoUpdateEntry(entry); + if (updated) + updatedEntries++; + } + + _logger.Information("Checked {CheckedEntries} entries, updated {UpdatedEntries}", checkedEntries, updatedEntries); + + if (updatedEntries > 0 && _showNotifications.Value) + _updateNotificationProvider.Value.ShowWorkshopNotification(updatedEntries); + } + + public async Task AutoUpdateEntry(InstalledEntry entry) + { + // Query the latest version + IOperationResult latestReleaseResult = await _client.GetEntryLatestReleaseById.ExecuteAsync(entry.Id); + + if (latestReleaseResult.Data?.Entry?.LatestRelease is not IRelease latestRelease) + return false; + if (latestRelease.Id == entry.ReleaseId) + return false; + + _logger.Information("Auto-updating entry {Entry} to version {Version}", entry, latestRelease.Version); + + EntryInstallResult updateResult = await _workshopService.InstallEntry(entry, latestRelease, new Progress(), CancellationToken.None); + + // This happens during installation too but not on our reference of the entry + if (updateResult.IsSuccess) + entry.ApplyRelease(latestRelease); + + _logger.Information("Auto-update result: {Result}", updateResult); + + return updateResult.IsSuccess; + } + + /// + public void DisableNotifications() + { + _showNotifications.Value = false; + _showNotifications.Save(); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs index 53717506f..e3a2ecde4 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs @@ -26,4 +26,10 @@ public class EntryInstallResult Entry = installedEntry }; } + + /// + public override string ToString() + { + return $"{nameof(IsSuccess)}: {IsSuccess}, {nameof(Message)}: {Message}"; + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs index 476b658dc..d597eb4e7 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs @@ -30,7 +30,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler { // If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done if (installedEntry.GetReleaseDirectory(release).Exists) - return EntryInstallResult.FromSuccess(installedEntry); + return ApplyAndSave(installedEntry, release); } else { @@ -102,10 +102,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler return EntryInstallResult.FromFailure(e.Message); } - installedEntry.ApplyRelease(release); - - _workshopService.SaveInstalledEntry(installedEntry); - return EntryInstallResult.FromSuccess(installedEntry); + return ApplyAndSave(installedEntry, release); } public Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) @@ -135,4 +132,11 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler _workshopService.RemoveInstalledEntry(installedEntry); return Task.FromResult(EntryUninstallResult.FromSuccess(message)); } + + private EntryInstallResult ApplyAndSave(InstalledEntry installedEntry, IRelease release) + { + installedEntry.ApplyRelease(release); + _workshopService.SaveInstalledEntry(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry); + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs index e06b88910..0b200740b 100644 --- a/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs +++ b/src/Artemis.WebClient.Workshop/Models/InstalledEntry.cs @@ -18,6 +18,9 @@ public class InstalledEntry : CorePropertyChanged, IEntrySummary private DateTimeOffset _createdAt; private long? _latestReleaseId; private IReadOnlyList _categories; + private long _releaseId; + private string _releaseVersion = string.Empty; + private bool _autoUpdate; internal InstalledEntry(EntryEntity entity) { @@ -36,12 +39,26 @@ public class InstalledEntry : CorePropertyChanged, IEntrySummary AutoUpdate = true; } - public long ReleaseId { get; set; } - public string ReleaseVersion { get; set; } = string.Empty; - public DateTimeOffset InstalledAt { get; set; } - public bool AutoUpdate { get; set; } - internal EntryEntity Entity { get; } + public DateTimeOffset InstalledAt { get; set; } + + public long ReleaseId + { + get => _releaseId; + set => SetAndNotify(ref _releaseId, value); + } + + public string ReleaseVersion + { + get => _releaseVersion; + set => SetAndNotify(ref _releaseVersion, value); + } + + public bool AutoUpdate + { + get => _autoUpdate; + set => SetAndNotify(ref _autoUpdate, value); + } internal void Load() { @@ -233,4 +250,10 @@ public class InstalledEntry : CorePropertyChanged, IEntrySummary } #endregion + + /// + public override string ToString() + { + return $"[{EntryType}] {Id} - {Name}"; + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql index 4725c2f9c..6e9b3ceec 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql @@ -32,4 +32,12 @@ query GetEntrySummaryById($id: Long!) { entry(id: $id) { ...entrySummary } +} + +query GetEntryLatestReleaseById($id: Long!) { + entry(id: $id) { + latestRelease { + ...releaseDetails + } + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs index 884e615a2..922ed3d0d 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -133,5 +133,6 @@ public interface IWorkshopService public event EventHandler? OnInstalledEntrySaved; public event EventHandler? OnEntryUninstalled; public event EventHandler? OnEntryInstalled; - + + void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs index 41f294e28..7fcf5f847 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -165,7 +165,7 @@ public class WorkshopService : IWorkshopService if (result.IsSuccess && result.Entry != null) OnEntryInstalled?.Invoke(this, result.Entry); else - _logger.Warning("Failed to install entry {EntryId}: {Message}", entry.Id, result.Message); + _logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message); return result; } @@ -254,6 +254,16 @@ public class WorkshopService : IWorkshopService } } + /// + public void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate) + { + if (installedEntry.AutoUpdate == autoUpdate) + return; + + installedEntry.AutoUpdate = autoUpdate; + SaveInstalledEntry(installedEntry); + } + private void RemoveOrphanedFiles() { List entries = GetInstalledEntries(); @@ -308,6 +318,8 @@ public class WorkshopService : IWorkshopService } public event EventHandler? OnInstalledEntrySaved; + public event EventHandler? OnEntryUninstalled; + public event EventHandler? OnEntryInstalled; } \ No newline at end of file