diff --git a/src/Baballonia.Tests/UtilsTest.cs b/src/Baballonia.Tests/UtilsTest.cs new file mode 100644 index 00000000..a106459d --- /dev/null +++ b/src/Baballonia.Tests/UtilsTest.cs @@ -0,0 +1,52 @@ +using Baballonia; +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Baballonia.Tests; + +[TestClass] +[TestSubject(typeof(Utils))] +public class UtilsTest +{ + + [TestMethod] + public void TestFindSemVersionSuccess() + { + var version = Utils.FindVersionInString("1.2.3.4"); + Assert.IsNotNull(version); + + version = Utils.FindVersionInString("v1.2.3.4"); + Assert.IsNotNull(version); + + version = Utils.FindVersionInString("1.2.3.4rc"); + Assert.IsNotNull(version); + + version = Utils.FindVersionInString("v1.2.3.4rc"); + Assert.IsNotNull(version); + + version = Utils.FindVersionInString("random text v1.2.3.4rc more text"); + Assert.IsNotNull(version); + } + + [TestMethod] + public void TestFindSemVersionFail() + { + var version = Utils.FindVersionInString("1.2.3"); + Assert.IsNull(version); + + version = Utils.FindVersionInString("v1.2.3"); + Assert.IsNull(version); + + version = Utils.FindVersionInString("v1.2.3rc"); + Assert.IsNull(version); + + version = Utils.FindVersionInString("some random text"); + Assert.IsNull(version); + + version = Utils.FindVersionInString(null); + Assert.IsNull(version); + + version = Utils.FindVersionInString(""); + Assert.IsNull(version); + } +} diff --git a/src/Baballonia/App.axaml.cs b/src/Baballonia/App.axaml.cs index fa0b6bc6..ffa290c6 100644 --- a/src/Baballonia/App.axaml.cs +++ b/src/Baballonia/App.axaml.cs @@ -97,6 +97,7 @@ public override void OnFrameworkInitializationCompleted() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Baballonia/Controls/DropdownMessage.axaml b/src/Baballonia/Controls/DropdownMessage.axaml new file mode 100644 index 00000000..de74b585 --- /dev/null +++ b/src/Baballonia/Controls/DropdownMessage.axaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Baballonia/Controls/DropdownMessage.axaml.cs b/src/Baballonia/Controls/DropdownMessage.axaml.cs new file mode 100644 index 00000000..594b9bfe --- /dev/null +++ b/src/Baballonia/Controls/DropdownMessage.axaml.cs @@ -0,0 +1,74 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using CommunityToolkit.Mvvm.Input; + +namespace Baballonia.Controls; + + +public class ButtonAction +{ + public string Text { get; set; } = string.Empty; + public IRelayCommand? Command { get; set; } +} + +public partial class DropdownMessage : UserControl +{ + public static readonly StyledProperty ImageSourceProperty = + AvaloniaProperty.Register(nameof(ImageSource)); + public static readonly StyledProperty TitleProperty = + AvaloniaProperty.Register(nameof(Title), string.Empty); + public static readonly StyledProperty MessageProperty = + AvaloniaProperty.Register(nameof(Message), string.Empty); + public static readonly StyledProperty AcceptProperty = + AvaloniaProperty.Register(nameof(AcceptText), string.Empty); + public static readonly StyledProperty DeclineProperty = + AvaloniaProperty.Register(nameof(DeclineText), string.Empty); + public static readonly StyledProperty AcceptCommandProperty = + AvaloniaProperty.Register(nameof(AcceptCommand)); + public static readonly StyledProperty DeclineCommandProperty = + AvaloniaProperty.Register(nameof(DeclineCommand)); + public IImage? ImageSource + { + get => GetValue(ImageSourceProperty); + set => SetValue(ImageSourceProperty, value); + } + public string Title + { + get => GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + public string Message + { + get => GetValue(MessageProperty); + set => SetValue(MessageProperty, value); + } + public string AcceptText + { + get => GetValue(AcceptProperty); + set => SetValue(AcceptProperty, value); + } + public string DeclineText + { + get => GetValue(DeclineProperty); + set => SetValue(DeclineProperty, value); + } + + public IRelayCommand? AcceptCommand + { + get => GetValue(AcceptCommandProperty); + set => SetValue(AcceptCommandProperty, value); + } + public IRelayCommand? DeclineCommand + { + get => GetValue(DeclineCommandProperty); + set => SetValue(DeclineCommandProperty, value); + } + + public DropdownMessage() + { + InitializeComponent(); + } +} + diff --git a/src/Baballonia/Services/GithubService.cs b/src/Baballonia/Services/GithubService.cs index d1c7ae0c..6bfe15d1 100644 --- a/src/Baballonia/Services/GithubService.cs +++ b/src/Baballonia/Services/GithubService.cs @@ -19,7 +19,7 @@ static GithubService() private static readonly HttpClient Client = new(); - public async Task> GetContributors(string owner, string repo) + public async Task> FetchContributors(string owner, string repo) { var response = await Client.GetAsync($"https://api.github.com/repos/{owner}/{repo}/contributors"); if (!response.IsSuccessStatusCode) @@ -30,7 +30,7 @@ public async Task> GetContributors(string owner, string return JsonSerializer.Deserialize>(content)!; } - public async Task GetReleases(string owner, string repo) + public async Task FetchLatestReleaseInfo(string owner, string repo) { var response = await Client.GetAsync($"https://api.github.com/repos/{owner}/{repo}/releases/latest"); if (!response.IsSuccessStatusCode) diff --git a/src/Baballonia/Services/UpdateService.cs b/src/Baballonia/Services/UpdateService.cs new file mode 100644 index 00000000..ee80ed37 --- /dev/null +++ b/src/Baballonia/Services/UpdateService.cs @@ -0,0 +1,69 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using Baballonia.Models; + +namespace Baballonia.Services; + +public class UpdateService +{ + private GithubRelease? _fetchedLatest = null; + private readonly GithubService _githubService; + + public UpdateService(GithubService githubService) + { + _githubService = githubService; + } + + public async Task TryGetLatestVersion() + { + if (_fetchedLatest != null) + return Utils.FindVersionInString(_fetchedLatest.tag_name); + + GithubRelease res; + try + { + res = await _githubService.FetchLatestReleaseInfo("Project-Babble", "Baballonia"); + _fetchedLatest = res; + } + catch + { + return null; + } + + var latestVersion = Utils.FindVersionInString(res.tag_name); + + return latestVersion; + } + + /// + /// Checks if current assembly is of the latest release from by the github releases + /// In absence of internet or github api assumes that current version is latest + /// + /// true if yes or error, false if no + public async Task IsLatest() + { + var latestVersion = await TryGetLatestVersion(); + if (latestVersion == null) + return true; + + var currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + if (currentVersion == null) + throw new InvalidOperationException("Challenge Complete!: How Did We Get Here? No Assembly version found"); + + var versionDifference = currentVersion.CompareTo(latestVersion); + + // false only if newer version exists + return versionDifference >= 0; + } + + public void NavigateToLatestWebPage() + { + string url = "https://github.com/Project-Babble/Baballonia/releases/latest"; + Utils.OpenUrl(url); + } + +} diff --git a/src/Baballonia/Utils.cs b/src/Baballonia/Utils.cs index 118781fa..880d6b95 100644 --- a/src/Baballonia/Utils.cs +++ b/src/Baballonia/Utils.cs @@ -120,4 +120,34 @@ public static string GenerateMD5(string filepath) var hash = hasher.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", ""); } + + + /// + /// Tries to find a Version in a string using sliding window search + /// + /// input string + /// A valid Version or null on faliure + public static Version? FindVersionInString(string str) + { + if (str == null) + return null; + var semVersionSize = "1.2.3.4".Length; + if (str.Length < semVersionSize) + return null; + + Version? ver = null; + for (var i = 0; i <= str.Length - semVersionSize; i++) + { + try + { + ver = new Version(str.Substring(i, semVersionSize)); + } + catch(Exception) + { + //ignore + } + } + + return ver; + } } diff --git a/src/Baballonia/ViewModels/MainViewModel.cs b/src/Baballonia/ViewModels/MainViewModel.cs index d8e6bc14..9ae9df70 100644 --- a/src/Baballonia/ViewModels/MainViewModel.cs +++ b/src/Baballonia/ViewModels/MainViewModel.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Threading; +using Baballonia.Contracts; using Baballonia.Views; using Baballonia.Models; using Baballonia.Services; @@ -12,25 +14,78 @@ using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.Logging; namespace Baballonia.ViewModels; public partial class MainViewModel : ViewModelBase { private readonly DropOverlayService _dropOverlayService; + private readonly UpdateService _updateService; + private readonly ILocalSettingsService _localSettingsService; + private readonly ILogger _logger; + + public MainViewModel() : this( + Ioc.Default.GetRequiredService(), + Ioc.Default.GetService>()!, + Ioc.Default.GetService()!) + { + } - public MainViewModel(IMessenger messenger) + public MainViewModel(UpdateService updateService, ILogger logger, + ILocalSettingsService localSettingsService) { - Items = Utils.IsSupportedDesktopOS ? - new ObservableCollection(_desktopTemplates) : - new ObservableCollection(_mobileTemplates); + _updateService = updateService; + _logger = logger; + _localSettingsService = localSettingsService; + + Items = Utils.IsSupportedDesktopOS + ? new ObservableCollection(_desktopTemplates) + : new ObservableCollection(_mobileTemplates); SelectedListItem = Items.First(vm => vm.ModelType == typeof(HomePageViewModel)); _dropOverlayService = Ioc.Default.GetService()!; _dropOverlayService.ShowOverlayChanged += SetOverlay; + + PromptUpdate(); + } + + private void PromptUpdate() + { + Task.Run(async () => + { + var shouldCheck = await _localSettingsService.ReadSettingAsync("AppSettings_CheckForUpdates", false); + if (!shouldCheck) + return; + + var isLatest = await _updateService.IsLatest(); + Version? latestVer = null; + if (!isLatest) + latestVer = await _updateService.TryGetLatestVersion(); + + _logger.LogInformation(isLatest + ? "The current version is latest!" + : $"New version {latestVer!.ToString()} available"); + + await Dispatcher.UIThread.InvokeAsync(() => { ShouldPromptUpdate = true; }, DispatcherPriority.Background); + }); } + [RelayCommand] + private async Task OpenBrowserOnLatest() + { + await Task.Run(() => { _updateService.NavigateToLatestWebPage(); }); + await Dispatcher.UIThread.InvokeAsync(() => { ShouldPromptUpdate = false; }); + } + + [RelayCommand] + private void CloseUpdatePrompt() + { + ShouldPromptUpdate = false; + } + + private void SetOverlay(bool show) { IsDropOverlayVisible = show; @@ -54,17 +109,12 @@ private void SetOverlay(bool show) new(typeof(AppSettingsViewModel), "SettingsRegular", "Settings"), ]; - public MainViewModel() : this(new WeakReferenceMessenger()) { } - - [ObservableProperty] - private bool _isPaneOpen; - [ObservableProperty] - private bool _isDropOverlayVisible; + [ObservableProperty] private bool _isPaneOpen; + [ObservableProperty] private bool _isDropOverlayVisible; + [ObservableProperty] private bool _shouldPromptUpdate = false; [ObservableProperty] private ViewModelBase _currentPage; - - [ObservableProperty] - private ListItemTemplate? _selectedListItem; + [ObservableProperty] private ListItemTemplate? _selectedListItem; partial void OnSelectedListItemChanged(ListItemTemplate? value) { @@ -82,6 +132,7 @@ partial void OnSelectedListItemChanged(ListItemTemplate? value) if (tmp is IDisposable disposable) disposable.Dispose(); } + private object CreateInstance(Type type) { // Manually resolve dependencies without container tracking diff --git a/src/Baballonia/Views/MainView.axaml b/src/Baballonia/Views/MainView.axaml index 374a222a..b8567ebd 100644 --- a/src/Baballonia/Views/MainView.axaml +++ b/src/Baballonia/Views/MainView.axaml @@ -1,97 +1,123 @@  - - - + d:DesignHeight="600" + d:DesignWidth="800" + mc:Ignorable="d" + x:Class="Baballonia.Views.MainView" + x:DataType="viewModels:MainViewModel" + xmlns="https://github.com/avaloniaui" + xmlns:converters="clr-namespace:Baballonia.Converters" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:models="clr-namespace:Baballonia.Models" + xmlns:viewModels="clr-namespace:Baballonia.ViewModels" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:Baballonia.Controls"> + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + +