Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 3 additions & 2 deletions App/App.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
Expand All @@ -16,7 +16,7 @@
<!-- To use CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute: -->
<LangVersion>preview</LangVersion>
<!-- We have our own implementation of main with exception handling -->
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>

<AssemblyName>Coder Desktop</AssemblyName>
<ApplicationIcon>coder.ico</ApplicationIcon>
Expand Down Expand Up @@ -57,6 +57,7 @@
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="DependencyPropertyGenerator" Version="1.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
6 changes: 6 additions & 0 deletions App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Coder.Desktop.App.Views;
using Coder.Desktop.App.Views.Pages;
using Coder.Desktop.CoderSdk.Agent;
using Coder.Desktop.CoderSdk.Coder;
using Coder.Desktop.Vpn;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -62,8 +63,11 @@ public App()
loggerConfig.ReadFrom.Configuration(builder.Configuration);
});

services.AddSingleton<ICoderApiClientFactory, CoderApiClientFactory>();
services.AddSingleton<IAgentApiClientFactory, AgentApiClientFactory>();

services.AddSingleton<ICredentialBackend>(_ =>
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
services.AddSingleton<ICredentialManager, CredentialManager>();
services.AddSingleton<IRpcController, RpcController>();

Expand All @@ -90,6 +94,8 @@ public App()
services.AddTransient<TrayWindowLoginRequiredPage>();
services.AddTransient<TrayWindowLoginRequiredViewModel>();
services.AddTransient<TrayWindowLoginRequiredPage>();
services.AddSingleton<IAgentAppViewModelFactory, AgentAppViewModelFactory>();
services.AddSingleton<IAgentViewModelFactory, AgentViewModelFactory>();
services.AddTransient<TrayWindowViewModel>();
services.AddTransient<TrayWindowMainPage>();
services.AddTransient<TrayWindow>();
Expand Down
31 changes: 31 additions & 0 deletions App/Controls/ExpandChevron.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>

<UserControl
x:Class="Coder.Desktop.App.Controls.ExpandChevron"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:animatedVisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
mc:Ignorable="d">

<Grid>
<AnimatedIcon
Grid.Column="0"
x:Name="ChevronIcon"
Width="16"
Height="16"
Margin="0,0,8,0"
RenderTransformOrigin="0.5, 0.5"
Foreground="{x:Bind Foreground, Mode=OneWay}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AnimatedIcon.State="NormalOff">

<animatedVisuals:AnimatedChevronRightDownSmallVisualSource />
<AnimatedIcon.FallbackIconSource>
<FontIconSource Glyph="&#xE76C;" />
</AnimatedIcon.FallbackIconSource>
</AnimatedIcon>
</Grid>
</UserControl>
19 changes: 19 additions & 0 deletions App/Controls/ExpandChevron.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using DependencyPropertyGenerator;
using Microsoft.UI.Xaml.Controls;

namespace Coder.Desktop.App.Controls;

[DependencyProperty<bool>("IsOpen", DefaultValue = false)]
public sealed partial class ExpandChevron : UserControl
{
public ExpandChevron()
{
InitializeComponent();
}

partial void OnIsOpenChanged(bool oldValue, bool newValue)
{
var newState = newValue ? "NormalOn" : "NormalOff";
AnimatedIcon.SetState(ChevronIcon, newState);
}
}
61 changes: 61 additions & 0 deletions App/Controls/ExpandContent.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>

<UserControl
x:Class="Coder.Desktop.App.Controls.ExpandContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkit="using:CommunityToolkit.WinUI"
mc:Ignorable="d">

<Grid x:Name="CollapsiblePanel" Opacity="0" Visibility="Collapsed" toolkit:UIElementExtensions.ClipToBounds="True">
<Grid.RenderTransform>
<TranslateTransform x:Name="SlideTransform" Y="-10" />
</Grid.RenderTransform>

<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="ExpandedState">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="CollapsiblePanel"
Storyboard.TargetProperty="Visibility">

<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation
Storyboard.TargetName="CollapsiblePanel"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
<DoubleAnimation
Storyboard.TargetName="SlideTransform"
Storyboard.TargetProperty="Y"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</VisualState>

<VisualState x:Name="CollapsedState">
<Storyboard Completed="{x:Bind CollapseAnimation_Completed}">
<DoubleAnimation
Storyboard.TargetName="CollapsiblePanel"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation
Storyboard.TargetName="SlideTransform"
Storyboard.TargetProperty="Y"
To="-10"
Duration="0:0:0.2" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>
39 changes: 39 additions & 0 deletions App/Controls/ExpandContent.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using DependencyPropertyGenerator;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;

namespace Coder.Desktop.App.Controls;

[ContentProperty(Name = nameof(Children))]
[DependencyProperty<bool>("IsOpen", DefaultValue = false)]
public sealed partial class ExpandContent : UserControl
{
public UIElementCollection Children => CollapsiblePanel.Children;

public ExpandContent()
{
InitializeComponent();
}

public void CollapseAnimation_Completed(object? sender, object args)
{
// Hide the panel completely when the collapse animation is done. This
// cannot be done with keyframes for some reason.
//
// Without this, the space will still be reserved for the panel.
CollapsiblePanel.Visibility = Visibility.Collapsed;
}

partial void OnIsOpenChanged(bool oldValue, bool newValue)
{
var newState = newValue ? "ExpandedState" : "CollapsedState";

// The animation can't set visibility when starting or ending the
// animation.
if (newValue)
CollapsiblePanel.Visibility = Visibility.Visible;

VisualStateManager.GoToState(this, newState, true);
}
}
13 changes: 13 additions & 0 deletions App/Converters/DependencyObjectSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ private void UpdateSelectedObject()
ClearValue(SelectedObjectProperty);
}

private static void VerifyReferencesProperty(IObservableVector<DependencyObject> references)
{
// Ensure unique keys and that the values are DependencyObjectSelectorItem<K, V>.
var items = references.OfType<DependencyObjectSelectorItem<TK, TV>>().ToArray();
var keys = items.Select(i => i.Key).Distinct().ToArray();
if (keys.Length != references.Count)
throw new ArgumentException("ObservableCollection Keys must be unique.");
}

// Called when the References property is replaced.
private static void ReferencesPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
Expand All @@ -166,12 +175,16 @@ private static void ReferencesPropertyChanged(DependencyObject obj, DependencyPr
oldValue.VectorChanged -= self.OnVectorChangedReferences;
var newValue = args.NewValue as DependencyObjectCollection;
if (newValue != null)
{
VerifyReferencesProperty(newValue);
newValue.VectorChanged += self.OnVectorChangedReferences;
}
}

// Called when the References collection changes without being replaced.
private void OnVectorChangedReferences(IObservableVector<DependencyObject> sender, IVectorChangedEventArgs args)
{
VerifyReferencesProperty(sender);
UpdateSelectedObject();
}

Expand Down
8 changes: 5 additions & 3 deletions App/Models/CredentialModel.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
using System;

namespace Coder.Desktop.App.Models;

public enum CredentialState
{
// Unknown means "we haven't checked yet"
Unknown,

// Invalid means "we checked and there's either no saved credentials or they are not valid"
// Invalid means "we checked and there's either no saved credentials, or they are not valid"
Invalid,

// Valid means "we checked and there are saved credentials and they are valid"
// Valid means "we checked and there are saved credentials, and they are valid"
Valid,
}

public class CredentialModel
{
public CredentialState State { get; init; } = CredentialState.Unknown;

public string? CoderUrl { get; init; }
public Uri? CoderUrl { get; init; }
public string? ApiToken { get; init; }

public string? Username { get; init; }
Expand Down
37 changes: 26 additions & 11 deletions App/Services/CredentialManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class RawCredentials
[JsonSerializable(typeof(RawCredentials))]
public partial class RawCredentialsJsonContext : JsonSerializerContext;

public interface ICredentialManager
public interface ICredentialManager : ICoderApiClientCredentialProvider
{
public event EventHandler<CredentialModel> CredentialsChanged;

Expand Down Expand Up @@ -59,7 +59,8 @@ public interface ICredentialBackend
/// </summary>
public class CredentialManager : ICredentialManager
{
private const string CredentialsTargetName = "Coder.Desktop.App.Credentials";
private readonly ICredentialBackend Backend;
private readonly ICoderApiClientFactory CoderApiClientFactory;

// _opLock is held for the full duration of SetCredentials, and partially
// during LoadCredentials. _opLock protects _inFlightLoad, _loadCts, and
Expand All @@ -79,14 +80,6 @@ public class CredentialManager : ICredentialManager
// immediate).
private volatile CredentialModel? _latestCredentials;

private ICredentialBackend Backend { get; } = new WindowsCredentialBackend(CredentialsTargetName);

private ICoderApiClientFactory CoderApiClientFactory { get; } = new CoderApiClientFactory();

public CredentialManager()
{
}

public CredentialManager(ICredentialBackend backend, ICoderApiClientFactory coderApiClientFactory)
{
Backend = backend;
Expand All @@ -108,6 +101,20 @@ public CredentialModel GetCachedCredentials()
};
}

// Implements ICoderApiClientCredentialProvider
public CoderApiClientCredential? GetCoderApiClientCredential()
{
var latestCreds = _latestCredentials;
if (latestCreds is not { State: CredentialState.Valid } || latestCreds.CoderUrl is null)
return null;

return new CoderApiClientCredential
{
CoderUrl = latestCreds.CoderUrl,
ApiToken = latestCreds.ApiToken ?? "",
};
}

public async Task<string?> GetSignInUri()
{
try
Expand Down Expand Up @@ -253,6 +260,12 @@ private async Task<CredentialModel> PopulateModel(RawCredentials? credentials, C
State = CredentialState.Invalid,
};

if (!Uri.TryCreate(credentials.CoderUrl, UriKind.Absolute, out var uri))
return new CredentialModel
{
State = CredentialState.Invalid,
};

BuildInfo buildInfo;
User me;
try
Expand All @@ -279,7 +292,7 @@ private async Task<CredentialModel> PopulateModel(RawCredentials? credentials, C
return new CredentialModel
{
State = CredentialState.Valid,
CoderUrl = credentials.CoderUrl,
CoderUrl = uri,
ApiToken = credentials.ApiToken,
Username = me.Username,
};
Expand All @@ -298,6 +311,8 @@ private void UpdateState(CredentialModel newModel)

public class WindowsCredentialBackend : ICredentialBackend
{
public const string CoderCredentialsTargetName = "Coder.Desktop.App.Credentials";

private readonly string _credentialsTargetName;

public WindowsCredentialBackend(string credentialsTargetName)
Expand Down
2 changes: 1 addition & 1 deletion App/Services/RpcController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public async Task StartVpn(CancellationToken ct = default)
{
Start = new StartRequest
{
CoderUrl = credentials.CoderUrl,
CoderUrl = credentials.CoderUrl?.ToString(),
ApiToken = credentials.ApiToken,
},
}, ct);
Expand Down
2 changes: 1 addition & 1 deletion App/DisplayScale.cs → App/Utils/DisplayScale.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
using WinRT.Interop;

namespace Coder.Desktop.App;
namespace Coder.Desktop.App.Utils;

/// <summary>
/// A static utility class to house methods related to the visual scale of the display monitor.
Expand Down
Loading
Loading