diff --git a/src/Constants.cs b/src/Constants.cs index 46bfa2e..3628271 100644 --- a/src/Constants.cs +++ b/src/Constants.cs @@ -43,4 +43,9 @@ public class Constants public const string SupportURl = "https://bento.me/nor0x"; public const string GithubUrl = "https://github.com/nor0x/Dots"; + + public const string DownloadingText = "Downloading..."; + public const string InstallingText = "Installing..."; + public const string UninstallingText = "Uninstalling..."; + public const string OpeningText = "Opening..."; } \ No newline at end of file diff --git a/src/Dots.csproj b/src/Dots.csproj index 5d23fcf..072892e 100644 --- a/src/Dots.csproj +++ b/src/Dots.csproj @@ -12,7 +12,6 @@ net8.0 WinExe Assets/appicon.ico - 2.0.0 @@ -67,58 +66,27 @@ - - - - - + + + + + - + - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/src/Helpers/Extensions.cs b/src/Helpers/Extensions.cs index 5061ec6..4d33b98 100644 --- a/src/Helpers/Extensions.cs +++ b/src/Helpers/Extensions.cs @@ -7,10 +7,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Dots.Helpers; @@ -145,4 +148,52 @@ public static void OpenFilePath(this string path) Process.Start("open", path); return; } + + + //credits https://gist.github.com/dalexsoto/9fd3c5bdbe9f61a717d47c5843384d11 + public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress progress = null, CancellationToken cancellationToken = default(CancellationToken)) + { + using (var response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead)) + { + var contentLength = response.Content.Headers.ContentLength; + using (var download = await response.Content.ReadAsStreamAsync()) + { + // no progress... no contentLength... very sad + if (progress is null || !contentLength.HasValue) + { + await download.CopyToAsync(destination); + return; + } + // Such progress and contentLength much reporting Wow! + var progressWrapper = new Progress(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value))); + await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken); + } + } + + float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f; + } + + static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress = null, CancellationToken cancellationToken = default(CancellationToken)) + { + if (bufferSize < 0) + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + if (source is null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new InvalidOperationException($"'{nameof(source)}' is not readable."); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new InvalidOperationException($"'{nameof(destination)}' is not writable."); + + var buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); + } + } } \ No newline at end of file diff --git a/src/MainWindow.axaml b/src/MainWindow.axaml index defdc31..37c2117 100644 --- a/src/MainWindow.axaml +++ b/src/MainWindow.axaml @@ -1,62 +1,68 @@ - - + + + + + + - - + + @@ -64,345 +70,253 @@ - - + + - - - - - - - - + + + + + + @@ -410,340 +324,418 @@ - + - + + + + + + + + + - + - - + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + @@ -752,24 +744,20 @@ - - - - + + + + diff --git a/src/MainWindow.axaml.cs b/src/MainWindow.axaml.cs index 1b84787..d26b4ca 100644 --- a/src/MainWindow.axaml.cs +++ b/src/MainWindow.axaml.cs @@ -3,13 +3,14 @@ using Avalonia.Media; using Dots.Helpers; using Dots.Models; +using Dots.Services; using System.Reactive.Linq; namespace Dots { public partial class MainWindow : Window { - MainViewModel _vm = new MainViewModel(new Services.DotnetService(), new Helpers.ErrorPopupHelper()); + MainViewModel _vm = new MainViewModel(new DotnetService(), new ErrorPopupHelper()); AboutWindow _aboutWindow = new AboutWindow(); bool _aboutWindowOpen = false; @@ -36,7 +37,7 @@ protected override async void OnInitialized() var lastChecked = await BlobCache.UserAccount.GetObject(Constants.LastCheckedKey); bool force = false; - if (DateTime.Now.Subtract(lastChecked).TotalDays > 15) + if (DateTime.Now.Subtract(lastChecked).TotalDays > 10) { force = true; await BlobCache.UserAccount.InsertObject(Constants.LastCheckedKey, DateTime.Now); @@ -109,5 +110,10 @@ private void PathTextBlock_Tapped(object? sender, Avalonia.Input.TappedEventArgs var path = ((TextBlock)sender).Text; path.OpenFilePath(); } + + private void Filter_Tapped(object? sender, Avalonia.Input.TappedEventArgs e) + { + FilterButton.Flyout.Hide(); + } } } \ No newline at end of file diff --git a/src/Models/ProgressTask.cs b/src/Models/ProgressTask.cs new file mode 100644 index 0000000..63586d6 --- /dev/null +++ b/src/Models/ProgressTask.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Dots.Models; + +public partial class ProgressTask : ObservableObject +{ + public string Title { get; set; } + public string Url { get; set; } + public CancellationTokenSource CancellationTokenSource { get; set; } + public IProgress Progress { get; set; } + + [ObservableProperty] + float _value; +} diff --git a/src/Models/SDK.cs b/src/Models/SDK.cs index a8167f4..c0e33b7 100644 --- a/src/Models/SDK.cs +++ b/src/Models/SDK.cs @@ -15,7 +15,7 @@ public partial class Sdk : ObservableObject //UI public string ColorHex { get; set; } - public string Group => Data.Sdk.Version.First().ToString(); + public string Group => VersionDisplay.First().ToString(); [JsonIgnore] public IBrush Color => SolidColorBrush.Parse(ColorHex); @@ -26,15 +26,32 @@ public partial class Sdk : ObservableObject public bool Installed => !string.IsNullOrEmpty(Path); [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsBusy))] [JsonIgnore] public bool _isDownloading; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsBusy))] [JsonIgnore] public bool _isInstalling; + [ObservableProperty] + [JsonIgnore] + public string _statusMessage; + + [ObservableProperty] + [JsonIgnore] + double _progress; + + [JsonIgnore] + public bool IsBusy => _isDownloading || _isInstalling; + [JsonIgnore] public string VersionDisplay { get; set; } + + [ObservableProperty] + [JsonIgnore] + ProgressTask _progressTask; } diff --git a/src/Services/DotnetService.cs b/src/Services/DotnetService.cs index 309f2fc..adc6354 100644 --- a/src/Services/DotnetService.cs +++ b/src/Services/DotnetService.cs @@ -1,9 +1,11 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.IO; using System.Net.Http; using System.Reactive.Linq; using System.Runtime.InteropServices; using System.Text.Json; +using System.Threading; using Akavache; using CliWrap; using CliWrap.Buffered; @@ -23,18 +25,21 @@ public class DotnetService ReleaseIndex[] _releaseIndex; Dictionary _releases = new(); + public DotnetService() + { + } + public async Task> GetSdks(bool force = false) { var result = new List(); var index = await GetReleaseIndex(force); var releaseInfos = new List(); - var installed = GetInstalledSdks(force); + await GetInstalledSdks(force); foreach (var item in index) { var infos = await GetReleaseInfos(item.ChannelVersion, force); releaseInfos.AddRange(infos); } - int i = 0; foreach (var release in releaseInfos) { var sdk = new Sdk() @@ -44,10 +49,46 @@ public async Task> GetSdks(bool force = false) Path = _installedSdks.FirstOrDefault(x => x.Version == release.Sdk.Version)?.Path ?? string.Empty, VersionDisplay = release.Sdk.Version, }; - i++; result.Add(sdk); + + if (release.Sdks is not null) + { + foreach (var subSdk in release.Sdks) + { + var sub = new Sdk() + { + Data = release, + ColorHex = ColorHelper.GenerateHexColor(release.Sdk.Version.First().ToString()), + Path = _installedSdks.FirstOrDefault(x => x.Version == subSdk.Version)?.Path ?? string.Empty, + VersionDisplay = subSdk.Version, + }; + + if (result.FirstOrDefault(s => s.VersionDisplay == subSdk.VersionDisplay) is null) + { + result.Add(sub); + } + + } + } + } + + foreach(var installed in _installedSdks) + { + if(result.FirstOrDefault(x => x.VersionDisplay == installed.Version) is null) + { + result.Add( + new Sdk() + { + Data = null, + VersionDisplay = installed.Version, + Path = installed.Path , + ColorHex = ColorHelper.GenerateHexColor(installed.Version.First().ToString()), + } + ); + } } - return result; + + return result.OrderByDescending(x => x.VersionDisplay).ToList(); } async Task GetReleaseIndex(bool force = false) @@ -106,7 +147,7 @@ async Task GetReleaseInfos(string channel, bool force = false) } - public async ValueTask> GetInstalledSdks(bool force = false) + async ValueTask> GetInstalledSdks(bool force = false) { try { @@ -149,7 +190,7 @@ public async ValueTask> GetInstalledSdks(bool force = false) catch (Exception ex) { Debug.WriteLine(ex); - ////Analytics.TrackEvent("GetInstalledSdks", new Dictionary() { { "Error", ex.Message } }); + //Analytics.TrackEvent("GetInstalledSdks", new Dictionary() { { "Error", ex.Message } }); return null; } @@ -188,17 +229,35 @@ public async ValueTask Download(Sdk sdk, bool toDesktop = false) } return path; } + + var progress = new ProgressTask(); + progress.Title = $"Downloading {sdk.Data.Sdk.Version}"; + progress.Url = info.Url.ToString(); + progress.CancellationTokenSource = new CancellationTokenSource(); + + var p = new Progress(); + p.ProgressChanged += (s, e) => + { + progress.Value = e; + }; + progress.Progress = p; + + + sdk.ProgressTask = progress; + + // Use the provided extension method + using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); using var client = new HttpClient(); - var response = await client.GetByteArrayAsync(info.Url); - await File.WriteAllBytesAsync(path, response); + await client.DownloadDataAsync(info.Url.ToString(), file, p); + if (toDesktop) { - //copy to desktop var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); var filename = Path.Combine(desktop, sdkFile); - await File.WriteAllBytesAsync(filename, response); - return desktop; + await File.WriteAllBytesAsync(Path.Combine(desktop, sdkFile), await File.ReadAllBytesAsync(path)); + path = desktop; } + return path; } return null; @@ -277,14 +336,14 @@ public async Task Uninstall(Sdk sdk, string setupPath = "") string[] files = Directory.GetFiles(path, filename, SearchOption.AllDirectories); - if(!files.IsNullOrEmpty()) + if (!files.IsNullOrEmpty()) { var result = await Cli.Wrap(files.First()).WithArguments(" /uninstall /quiet /qn /norestart").WithValidation(CommandResultValidation.None).ExecuteAsync(); return result.ExitCode == 0; } else { - + var setupInLocalDirectory = Path.Combine(Constants.AppDataPath, filename); if (!string.IsNullOrEmpty(setupPath)) { diff --git a/src/ViewModels/MainViewModel.cs b/src/ViewModels/MainViewModel.cs index 7999b22..c9bb107 100644 --- a/src/ViewModels/MainViewModel.cs +++ b/src/ViewModels/MainViewModel.cs @@ -25,6 +25,8 @@ public MainViewModel(DotnetService dotnet, ErrorPopupHelper errorHelper) { _dotnet = dotnet; _errorHelper = errorHelper; + _progressTasks = new ObservableCollection(); + SelectedFilterIcon = LucideIcons.ListFilter; } string _query = ""; @@ -46,19 +48,23 @@ public MainViewModel(DotnetService dotnet, ErrorPopupHelper errorHelper) [ObservableProperty] ObservableView _sdks; - [ObservableProperty] string _lastUpdated; [ObservableProperty] - bool _showOnline = true; + bool _showDetails = false; [ObservableProperty] - bool _showInstalled = true; + ObservableCollection _progressTasks; [ObservableProperty] - bool _showDetails = false; + string _selectedFilterIcon; + bool _showOnline = true; + bool _showInstalled = true; + + [ObservableProperty] + bool _emptyData; public bool SetSelectedSdk(Sdk sdk) { @@ -67,15 +73,17 @@ public bool SetSelectedSdk(Sdk sdk) { showDetails = false; } - else if(SelectedSdk is null) + else if (SelectedSdk is null) { showDetails = true; } - else if (sdk is not null && sdk.Data.Sdk.Version == SelectedSdk.Data.Sdk.Version) + else if (sdk is not null && sdk.VersionDisplay == SelectedSdk.VersionDisplay) { showDetails = !ShowDetails; } ShowDetails = showDetails; + EmptyData = sdk?.Data is null; + if (sdk?.VersionDisplay == SelectedSdk?.VersionDisplay) { SelectedSdk = null; @@ -128,7 +136,7 @@ void FilterSdks(string query) { _query = query; Sdks.Search(_query); - + var filteredCollection = _baseSdks.Where(s => s.Data.Sdk.Version.ToLowerInvariant().Contains(query.ToLowerInvariant()) || s.Path.ToLowerInvariant().Contains(query.ToLowerInvariant())).ToList(); @@ -148,8 +156,40 @@ void FilterSdks(string query) [RelayCommand] void ToggleSelection() + { } + + [RelayCommand] + void ApplyFilter(string f) { - SelectionEnabled = !SelectionEnabled; + int filter = int.Parse(f); + //0 all + //1 online + //2 installed + if(filter == 0) + { + _showOnline = true; + _showInstalled = true; + SelectedFilterIcon = LucideIcons.ListFilter; + } + else if (filter == 1) + { + _showInstalled = false; + _showOnline = true; + SelectedFilterIcon = LucideIcons.Cloudy; + } + else if(filter == 2) + { + _showOnline = false; + _showInstalled = true; + SelectedFilterIcon = LucideIcons.HardDrive; + } + Sdks.Search(" "); + Sdks.Search(_query); + + if (!Sdks.View.Contains(SelectedSdk)) + { + SelectedSdk = null; + } } public ICommand MyTestCommand { get; set; } @@ -162,10 +202,12 @@ async Task OpenOrDownload(Sdk sdk) sdk.IsDownloading = true; if (sdk.Installed) { + sdk.StatusMessage = Constants.OpeningText; await _dotnet.OpenFolder(sdk); } else { + sdk.StatusMessage = Constants.DownloadingText; var path = await _dotnet.Download(sdk, true); await _dotnet.OpenFolder(path); } @@ -185,8 +227,10 @@ async Task InstallOrUninstall(Sdk sdk) try { sdk.IsInstalling = true; + if (sdk.Installed) { + sdk.StatusMessage = Constants.UninstallingText; var result = await _dotnet.Uninstall(sdk); if (result) { @@ -195,9 +239,11 @@ async Task InstallOrUninstall(Sdk sdk) } else { + sdk.StatusMessage = Constants.DownloadingText; var path = await _dotnet.Download(sdk); if (!string.IsNullOrEmpty(path)) { + sdk.StatusMessage = Constants.InstallingText; var result = await _dotnet.Install(path); if (result) { @@ -219,35 +265,6 @@ async Task InstallOrUninstall(Sdk sdk) } } - [RelayCommand] - void ToggleOnline() - { - ShowOnline = !ShowOnline; - Sdks.Search(" "); - Sdks.Search(_query); - IsBusy = (!ShowOnline && !ShowInstalled); - - if(!Sdks.View.Contains(SelectedSdk)) - { - SelectedSdk = null; - } - } - - [RelayCommand] - void ToggleInstalled() - { - ShowInstalled = !ShowInstalled; - Sdks.Search(" "); - Sdks.Search(_query); - IsBusy = (!ShowOnline && !ShowInstalled); - if (!Sdks.View.Contains(SelectedSdk)) - { - SelectedSdk = null; - } - } - - - [RelayCommand] void ToggleMultiSelection() { @@ -260,15 +277,15 @@ void OpenSettings() void Sdks_FilterHandler(object sender, ObservableView.Filtering.FilterEventArgs e) { - if (ShowOnline && ShowInstalled) + if (_showOnline && _showInstalled) { e.IsAllowed = true; } - else if (ShowOnline && !ShowInstalled) + else if (_showOnline && !_showInstalled) { e.IsAllowed = !e.Item.Installed; } - else if (!ShowOnline && ShowInstalled) + else if (!_showOnline && _showInstalled) { e.IsAllowed = e.Item.Installed; }