Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added check for update for extensions #1300

Merged
merged 1 commit into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading