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;
}