Skip to content
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
10 changes: 8 additions & 2 deletions src/ExternalLibraries.FilePickers/Classes/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal static class Helper
internal static string ShowOpen(nint windowHandle, FOS fos, List<string>? typeFilters = null)
{
FileOpenDialog dialog = new();
IShellItem item = null!;
try
{
dialog.SetOptions(fos);
Expand All @@ -36,13 +37,15 @@ internal static string ShowOpen(nint windowHandle, FOS fos, List<string>? typeFi
return string.Empty;
}

dialog.GetResult(out IShellItem item);
dialog.GetResult(out item);
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out string path);
return path;
}
finally
{
#pragma warning disable CA1416
if (item is not null)
Marshal.ReleaseComObject(item);
Marshal.ReleaseComObject(dialog);
#pragma warning restore CA1416
}
Expand All @@ -56,6 +59,7 @@ internal static string ShowSave(
)
{
FileSaveDialog dialog = new();
IShellItem item = null!;
try
{
dialog.SetOptions(fos);
Expand All @@ -79,7 +83,7 @@ internal static string ShowSave(
return string.Empty;
}

dialog.GetResult(out IShellItem item);
dialog.GetResult(out item);
item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out string path);

dialog.GetFileTypeIndex(out uint selection);
Expand All @@ -92,6 +96,8 @@ internal static string ShowSave(
finally
{
#pragma warning disable CA1416
if (item is not null)
Marshal.ReleaseComObject(item);
Marshal.ReleaseComObject(dialog);
#pragma warning restore CA1416
}
Expand Down
79 changes: 70 additions & 9 deletions src/UniGetUI.Avalonia/ViewModels/Controls/UserAvatarViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@

namespace UniGetUI.Avalonia.ViewModels.Controls;

public class UserAvatarViewModel : ViewModelBase
public class UserAvatarViewModel : ViewModelBase, IDisposable
{
private readonly CancellationTokenSource _lifetimeCancellation = new();
private readonly CancellationToken _lifetimeToken;
private int _isDisposed;
private long _refreshGeneration;

private bool _isAuthenticated;
public bool IsAuthenticated
{
Expand Down Expand Up @@ -38,15 +43,32 @@ public Bitmap? AvatarBitmap

public UserAvatarViewModel()
{
_lifetimeToken = _lifetimeCancellation.Token;
LoginCommand = new AsyncRelayCommand(LoginAsync);
LogoutCommand = new MvvmRelayCommand(Logout);
MoreDetailsCommand = new MvvmRelayCommand(() => CoreTools.Launch("https://devolutions.net/unigetui"));
GitHubAuthService.AuthStatusChanged += (_, _) => _ = RefreshAsync();
GitHubAuthService.AuthStatusChanged += OnAuthStatusChanged;
_ = RefreshAsync();
}

private bool IsDisposed => Volatile.Read(ref _isDisposed) != 0;

private bool CanApplyRefresh(long generation) =>
!IsDisposed
&& !_lifetimeToken.IsCancellationRequested
&& generation == Volatile.Read(ref _refreshGeneration);

private void OnAuthStatusChanged(object? sender, EventArgs e)
{
if (!IsDisposed)
_ = RefreshAsync();
}

private async Task RefreshAsync()
{
if (IsDisposed) return;

long generation = Interlocked.Increment(ref _refreshGeneration);
var service = new GitHubAuthService();
bool authenticated = GitHubAuthService.IsAuthenticated();

Expand All @@ -61,33 +83,56 @@ private async Task RefreshAsync()
if (client is not null)
{
GitHubUser user = await client.GetCurrentUserAsync();
if (!CanApplyRefresh(generation)) return;

displayName = string.IsNullOrEmpty(user.Name)
? $"@{user.Login}"
: $"{user.Name} (@{user.Login})";

if (!string.IsNullOrEmpty(user.AvatarUrl))
{
using var http = new HttpClient(CoreTools.GenericHttpClientParameters);
byte[] bytes = await http.GetByteArrayAsync(user.AvatarUrl);
byte[] bytes = await http.GetByteArrayAsync(user.AvatarUrl, _lifetimeToken);
if (!CanApplyRefresh(generation)) return;

using var ms = new MemoryStream(bytes);
bitmap = new Bitmap(ms);
}
}
}
catch (OperationCanceledException) when (_lifetimeToken.IsCancellationRequested)
{
return;
}
catch (Exception ex)
{
Logger.Warn("UserAvatarViewModel: failed to fetch GitHub user info");
Logger.Warn(ex);
if (!IsDisposed)
{
Logger.Warn("UserAvatarViewModel: failed to fetch GitHub user info");
Logger.Warn(ex);
}
authenticated = false;
}
}

if (!CanApplyRefresh(generation))
{
bitmap?.Dispose();
return;
}

await Dispatcher.UIThread.InvokeAsync(() =>
{
if (!CanApplyRefresh(generation)) return;

IsAuthenticated = authenticated;
UserDisplayName = displayName;
AvatarBitmap = bitmap;
bitmap = null;
});

// A queued UI update can become obsolete while it is waiting to run.
bitmap?.Dispose();
}

private async Task LoginAsync()
Expand All @@ -98,8 +143,11 @@ private async Task LoginAsync()
}
catch (Exception ex)
{
Logger.Error("UserAvatarViewModel: login failed");
Logger.Error(ex);
if (!IsDisposed)
{
Logger.Error("UserAvatarViewModel: login failed");
Logger.Error(ex);
}
}
}

Expand All @@ -108,8 +156,21 @@ private void Logout()
try { new GitHubAuthService().SignOut(); }
catch (Exception ex)
{
Logger.Error("UserAvatarViewModel: logout failed");
Logger.Error(ex);
if (!IsDisposed)
{
Logger.Error("UserAvatarViewModel: logout failed");
Logger.Error(ex);
}
}
}

public void Dispose()
{
if (Interlocked.Exchange(ref _isDisposed, 1) != 0) return;

GitHubAuthService.AuthStatusChanged -= OnAuthStatusChanged;
// In-flight HTTP work can still observe this token. Cancelling is sufficient;
// disposing the source here could race with that work.
_lifetimeCancellation.Cancel();
}
}
Loading
Loading