Skip to content

Commit

Permalink
Added check for update for extensions (#1300)
Browse files Browse the repository at this point in the history
  • Loading branch information
veler authored Jul 17, 2024
1 parent db469a7 commit ffce467
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Runtime.InteropServices;
using DevToys.Core.Web;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NuGet.Packaging;

namespace DevToys.Blazor.BuiltInTools.ExtensionsManager;
Expand Down Expand Up @@ -117,6 +119,60 @@ string extensionInstallationPath
return new(AlreadyInstalled: false, nuspecReader, extensionInstallationPath);
}

internal static async Task<bool> CheckForAnyUpdateAsync(IWebClientService webClientService)
{

await TaskSchedulerAwaiter.SwitchOffMainThreadAsync(CancellationToken.None);

var updateCheckTasks = new List<Task<bool>>();

for (int i = 0; i < ExtensionInstallationFolders.Length; i++)
{
if (Directory.Exists(ExtensionInstallationFolders[i]))
{
IEnumerable<string> nuspecFiles
= Directory.EnumerateFiles(ExtensionInstallationFolders[i], "*.nuspec", SearchOption.AllDirectories);

foreach (string nuspecFile in nuspecFiles)
{
var nuspec = new NuspecReader(nuspecFile);
updateCheckTasks.Add(CheckForUpdateAsync(webClientService, nuspec));
}
}
}

await Task.WhenAll(updateCheckTasks);

return updateCheckTasks.Any(t => t.Result);
}

internal static async Task<bool> CheckForUpdateAsync(IWebClientService webClientService, NuspecReader nuspec)
{
const string NuGetOrgVersionUrl = "https://api.nuget.org/v3-flatcontainer/{0}/index.json";

await TaskSchedulerAwaiter.SwitchOffMainThreadAsync(CancellationToken.None);

string url = string.Format(NuGetOrgVersionUrl, nuspec.GetId());

string? response = await webClientService.SafeGetStringAsync(new Uri(url), CancellationToken.None);
if (response is not null)
{
var jObject = JObject.Parse(response);
if (jObject is not null)
{
IEnumerable<string?>? versions = jObject["versions"]?.Values<string>();
string? latestVersion = versions?.LastOrDefault();

if (!string.Equals(latestVersion, nuspec.GetVersion().OriginalVersion, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}

return false;
}

private static IEnumerable<string> GetPathToExclude()
{
if (OperatingSystem.IsWindows())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System.Collections.Immutable;
using DevToys.Api;
using DevToys.Blazor.BuiltInTools.Settings;
using DevToys.Core;
using DevToys.Core.Settings;
using DevToys.Core.Web;
using NuGet.Packaging;
using NuGet.Packaging.Core;

namespace DevToys.Blazor.BuiltInTools.ExtensionsManager;

[Export(typeof(IGuiTool))]
[Name(ExtensionmanagerToolName)]
[Name(ExtensionManagerToolName)]
[ToolDisplayInformation(
IconFontName = "FluentSystemIcons",
IconGlyph = '\uE9E8',
Expand All @@ -26,7 +29,7 @@ namespace DevToys.Blazor.BuiltInTools.ExtensionsManager;
[TargetPlatform(Platform.MacOS)]
internal sealed class ExtensionsManagerGuiTool : IGuiTool
{
internal const string ExtensionmanagerToolName = "Extensions Manager";
internal const string ExtensionManagerToolName = "Extensions Manager";

private enum GridRows
{
Expand All @@ -52,6 +55,12 @@ private enum GridColumns
#pragma warning disable IDE0044 // Add readonly modifier
[Import]
private IFileStorage _fileStorage = default!;

[Import]
private IWebClientService _webClientService = default!;

[Import]
private ISettingsProvider _settingsProvider = default!;
#pragma warning restore IDE0044 // Add readonly modifier

public UIToolView View
Expand Down Expand Up @@ -169,6 +178,11 @@ private void OnUninstallExtensionButtonClick(string extensionInstallationPath)
_extensionList.Items.RemoveValue(extensionInstallationPath);
}

private void OnUpdateExtensionButtonClick(NuspecReader nuspec)
{
OSHelper.OpenFileInShell(string.Format("https://www.nuget.org/packages/{0}", nuspec.GetId()));
}

private async Task LoadExtensionListAsync()
{
_extensionList.Items.Clear();
Expand Down Expand Up @@ -229,6 +243,26 @@ IUIButton uninstallButton
.Icon("FluentSystemIcons", '\uE47B')
.OnClick(() => OnUninstallExtensionButtonClick(extensionInstallationPath));
actionBuilder.Add(uninstallButton);
IUIStack actionStack = Stack();

if (_settingsProvider.GetSetting(PredefinedSettings.CheckForUpdate))
{
Task.Run(async () =>
{
bool updateAvailable = await ExtensionInstallationManager.CheckForUpdateAsync(_webClientService, nuspec);
if (updateAvailable)
{
// Add update button.
IUIButton updateButton
= Button()
.Icon("FluentSystemIcons", '\uF150')
.HyperlinkAppearance()
.OnClick(() => OnUpdateExtensionButtonClick(nuspec));
actionBuilder.Insert(0, updateButton);
actionStack.WithChildren(actionBuilder.ToArray());
}
}).ForgetSafely();
}

// Create the item.
return Item(
Expand All @@ -242,7 +276,7 @@ IUIButton uninstallButton
.MediumSpacing()
.WithChildren(
Label().Text(SizeWithUnit(size)),
Stack()
actionStack
.Horizontal()
.SmallSpacing()
.WithChildren(actionBuilder.ToArray()))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace DevToys.Blazor.BuiltInTools.SupportDevelopment;
[NotFavorable]
[NotSearchable]
[NoCompactOverlaySupport]
[Order(Before = ExtensionsManagerGuiTool.ExtensionmanagerToolName)]
[Order(Before = ExtensionsManagerGuiTool.ExtensionManagerToolName)]
internal sealed class SupportDevelopmentGuidTools : IGuiTool
{
// TODO: Finish this tool.
Expand Down
15 changes: 15 additions & 0 deletions src/app/dev/DevToys.Blazor/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@
</NavBarItemTitleTemplate>

<Footer>
@if (ViewModel.UpdateAvailableForExtension)
{
<Button Appearance="ButtonAppearance.Hyperlink"
Class="sidebar-footer-button"
@onclick="OnUpdateAvailableForExtensionButtonClick">
@if (_navBar.IsCollapsedMode)
{
<FontIcon Glyph="@('\uF150')" Height="16" Width="16" />
}
else
{
<TextBlock Text="@MainWindow.UpdateAvailableForExtension" NoWrap="true" />
}
</Button>
}
@if (ViewModel.UpdateAvailable)
{
<Button Appearance="ButtonAppearance.Hyperlink"
Expand Down
22 changes: 21 additions & 1 deletion src/app/dev/DevToys.Blazor/Pages/Index.razor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using DevToys.Blazor.BuiltInTools.ExtensionsManager;
using DevToys.Blazor.Components;
using DevToys.Blazor.Core.Services;
using DevToys.Blazor.Pages.Dialogs;
Expand All @@ -8,6 +9,7 @@
using DevToys.Core.Settings;
using DevToys.Core.Tools;
using DevToys.Core.Tools.ViewItems;
using DevToys.Core.Web;
using DevToys.Localization.Strings.ToolGroupPage;
using Microsoft.AspNetCore.Components.Web;

Expand Down Expand Up @@ -50,6 +52,9 @@ private static readonly SettingDefinition<NavBarSidebarStates> UserPreferredNavB
[Import]
internal CommandLineLauncherService CommandLineLauncherService { get; set; } = default!;

[Import]
internal IWebClientService WebClientService { get; set; } = default!;

[Inject]
internal ContextMenuService ContextMenuService { get; set; } = default!;

Expand All @@ -76,6 +81,7 @@ protected override void OnInitialized()
UIDialogService.IsDialogOpenedChanged += DialogService_IsDialogOpenedChanged;
ViewModel.SelectedMenuItemChanged += ViewModel_SelectedMenuItemChanged;
ViewModel.SelectedMenuItem ??= ViewModel.HeaderAndBodyToolViewItems[0];
ViewModel.CheckForExtensionUpdateRequested += ViewModel_CheckForExtensionUpdateRequested;
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
ContextMenuService.IsContextMenuOpenedChanged += ContextMenuService_IsContextMenuOpenedChanged;
WindowService.WindowActivated += WindowService_WindowActivated;
Expand All @@ -85,11 +91,13 @@ protected override void OnInitialized()

TitleBarInfoProvider.TitleBarMarginRight = 40;
WindowHasFocus = true;

ViewModel.Startup();
}

private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewModel.UpdateAvailable))
if (e.PropertyName == nameof(ViewModel.UpdateAvailable) || e.PropertyName == nameof(ViewModel.UpdateAvailableForExtension))
{
InvokeAsync(StateHasChanged);
}
Expand All @@ -108,6 +116,11 @@ private void ViewModel_SelectedMenuItemChanged(object? sender, EventArgs e)
StateHasChanged();
}

private async void ViewModel_CheckForExtensionUpdateRequested(object? sender, EventArgs e)
{
ViewModel.UpdateAvailableForExtension = await ExtensionInstallationManager.CheckForAnyUpdateAsync(WebClientService);
}

private void ContextMenuService_IsContextMenuOpenedChanged(object? sender, EventArgs e)
{
StateHasChanged();
Expand Down Expand Up @@ -225,6 +238,13 @@ private void OnUpdateAvailableButtonClick()
OSHelper.OpenFileInShell("https://github.com/DevToys-app/DevToys/releases");
}

private void OnUpdateAvailableForExtensionButtonClick()
{
GuiToolInstance? extensionManagerTool = GuiToolProvider.GetToolFromInternalName(ExtensionsManagerGuiTool.ExtensionManagerToolName);
Guard.IsNotNull(extensionManagerTool);
ViewModel.SelectedMenuItem = ViewModel.GetBestMenuItemToSelect(extensionManagerTool);
}

private async Task<bool> ShowFirstStartAndOrWhatsNewDialogsAsync()
{
bool openedDialog = false;
Expand Down
93 changes: 56 additions & 37 deletions src/app/dev/DevToys.Business/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,18 @@ public MainWindowViewModel(
_webClientService = webClientService;
_versionService = versionService;
Messenger.Register<MainWindowViewModel, ChangeSelectedMenuItemMessage>(this, OnChangeSelectedMenuItemMessageReceived);

if (_settingsProvider.GetSetting(PredefinedSettings.CheckForUpdate))
{
CheckForUpdateAsync().ForgetSafely();
}
}

/// <summary>
/// Raised when the <see cref="SelectedMenuItem"/> property changed.
/// </summary>
internal event EventHandler<EventArgs>? SelectedMenuItemChanged;

/// <summary>
/// Raised when the app should check for extension updates.
/// </summary>
internal event EventHandler<EventArgs>? CheckForExtensionUpdateRequested;

/// <summary>
/// Gets a hierarchical list containing all the tools available, ordered, to display in the top and body menu.
/// This includes "All tools" menu item, recents and favorites.
Expand Down Expand Up @@ -168,6 +168,23 @@ public INotifyPropertyChanged? SelectedMenuItem
[ObservableProperty]
private bool _updateAvailable = false;

/// <summary>
/// Gets or sets whether an update for an extension is available online.
/// </summary>
[ObservableProperty]
private bool _updateAvailableForExtension = false;

/// <summary>
/// Perform the startup tasks.
/// </summary>
internal void Startup()
{
if (_settingsProvider.GetSetting(PredefinedSettings.CheckForUpdate))
{
CheckForUpdateAsync().ForgetSafely();
}
}

/// <summary>
/// Navigates back to the previous <see cref="SelectedMenuItem"/>.
/// </summary>
Expand Down Expand Up @@ -275,38 +292,7 @@ IReadOnlyList<SmartDetectedTool> detectedTools
}
}

/// <summary>
/// Command invoked when the search box's text changed.
/// </summary>
[RelayCommand]
private void SearchBoxTextChanged()
{
_guiToolProvider.SearchTools(SearchQuery, SearchResults);
}

/// <summary>
/// Command invoked when the user press Enter in the search box or explicitly select an item in the search result list.
/// </summary>
/// <param name="chosenSuggestion">Equals to the selected item in the search result list, or null if nothing is selected by the user and or if there's no result at all.</param>
[RelayCommand]
private void SearchBoxQuerySubmitted(object? chosenSuggestion)
{
var selectedSearchResultItem = chosenSuggestion as GuiToolViewItem;
if (selectedSearchResultItem is null && SearchResults.Count > 0)
{
selectedSearchResultItem = SearchResults[0];
}

if (selectedSearchResultItem is null || selectedSearchResultItem == GuiToolProvider.NoResultFoundItem)
{
return;
}

// Select the actual menu item in the navigation view. This will trigger the navigation.
SelectedMenuItem = GetBestMenuItemToSelect(selectedSearchResultItem);
}

private INotifyPropertyChanged GetBestMenuItemToSelect(object currentSelectedMenuItem)
internal INotifyPropertyChanged GetBestMenuItemToSelect(object currentSelectedMenuItem)
{
Guard.IsNotEmpty((IReadOnlyList<INotifyPropertyChanged>)HeaderAndBodyToolViewItems);
Guard.IsNotNull(currentSelectedMenuItem);
Expand Down Expand Up @@ -340,6 +326,37 @@ private INotifyPropertyChanged GetBestMenuItemToSelect(object currentSelectedMen
return firstItem;
}

/// <summary>
/// Command invoked when the search box's text changed.
/// </summary>
[RelayCommand]
private void SearchBoxTextChanged()
{
_guiToolProvider.SearchTools(SearchQuery, SearchResults);
}

/// <summary>
/// Command invoked when the user press Enter in the search box or explicitly select an item in the search result list.
/// </summary>
/// <param name="chosenSuggestion">Equals to the selected item in the search result list, or null if nothing is selected by the user and or if there's no result at all.</param>
[RelayCommand]
private void SearchBoxQuerySubmitted(object? chosenSuggestion)
{
var selectedSearchResultItem = chosenSuggestion as GuiToolViewItem;
if (selectedSearchResultItem is null && SearchResults.Count > 0)
{
selectedSearchResultItem = SearchResults[0];
}

if (selectedSearchResultItem is null || selectedSearchResultItem == GuiToolProvider.NoResultFoundItem)
{
return;
}

// Select the actual menu item in the navigation view. This will trigger the navigation.
SelectedMenuItem = GetBestMenuItemToSelect(selectedSearchResultItem);
}

private void OnChangeSelectedMenuItemMessageReceived(MainWindowViewModel vm, ChangeSelectedMenuItemMessage message)
{
// Select the actual menu item in the navigation view. This will trigger the navigation.
Expand All @@ -359,6 +376,8 @@ private async Task CheckForUpdateAsync()
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5));
CancellationToken token = cancellationTokenSource.Token;

CheckForExtensionUpdateRequested?.Invoke(this, EventArgs.Empty);

UpdateAvailable = await AppHelper.CheckForUpdateAsync(_webClientService, _versionService, token);
}

Expand Down
Loading

0 comments on commit ffce467

Please sign in to comment.