Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1d3d9df

Browse files
committed
feat: add workspace app icons to tray window
- Adds AgentAppViewModel to handle each button - Adds collapsible control components to handle the collapsing section - Adds Uuid type to work around issues with the built-in Guid type - Adds ModelMerge utility for merging lists with minimal updates to work around constant flashing in the UI
1 parent 2495779 commit 1d3d9df

25 files changed

+1333
-194
lines changed

App/App.csproj

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>WinExe</OutputType>
44
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
@@ -16,7 +16,7 @@
1616
<!-- To use CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute: -->
1717
<LangVersion>preview</LangVersion>
1818
<!-- We have our own implementation of main with exception handling -->
19-
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
19+
<DefineConstants>DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
2020

2121
<AssemblyName>Coder Desktop</AssemblyName>
2222
<ApplicationIcon>coder.ico</ApplicationIcon>
@@ -57,6 +57,7 @@
5757
<ItemGroup>
5858
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
5959
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
60+
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
6061
<PackageReference Include="DependencyPropertyGenerator" Version="1.5.0">
6162
<PrivateAssets>all</PrivateAssets>
6263
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

App/App.xaml.cs

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Coder.Desktop.App.Views;
1212
using Coder.Desktop.App.Views.Pages;
1313
using Coder.Desktop.CoderSdk.Agent;
14+
using Coder.Desktop.CoderSdk.Coder;
1415
using Coder.Desktop.Vpn;
1516
using Microsoft.Extensions.Configuration;
1617
using Microsoft.Extensions.DependencyInjection;
@@ -62,8 +63,11 @@ public App()
6263
loggerConfig.ReadFrom.Configuration(builder.Configuration);
6364
});
6465

66+
services.AddSingleton<ICoderApiClientFactory, CoderApiClientFactory>();
6567
services.AddSingleton<IAgentApiClientFactory, AgentApiClientFactory>();
6668

69+
services.AddSingleton<ICredentialBackend>(_ =>
70+
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
6771
services.AddSingleton<ICredentialManager, CredentialManager>();
6872
services.AddSingleton<IRpcController, RpcController>();
6973

@@ -90,6 +94,8 @@ public App()
9094
services.AddTransient<TrayWindowLoginRequiredPage>();
9195
services.AddTransient<TrayWindowLoginRequiredViewModel>();
9296
services.AddTransient<TrayWindowLoginRequiredPage>();
97+
services.AddSingleton<IAgentAppViewModelFactory, AgentAppViewModelFactory>();
98+
services.AddSingleton<IAgentViewModelFactory, AgentViewModelFactory>();
9399
services.AddTransient<TrayWindowViewModel>();
94100
services.AddTransient<TrayWindowMainPage>();
95101
services.AddTransient<TrayWindow>();

App/Controls/ExpandChevron.xaml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<UserControl
4+
x:Class="Coder.Desktop.App.Controls.ExpandChevron"
5+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
7+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
9+
xmlns:animatedVisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
10+
mc:Ignorable="d">
11+
12+
<Grid>
13+
<AnimatedIcon
14+
Grid.Column="0"
15+
x:Name="ChevronIcon"
16+
Width="16"
17+
Height="16"
18+
Margin="0,0,8,0"
19+
RenderTransformOrigin="0.5, 0.5"
20+
Foreground="{x:Bind Foreground, Mode=Oneway}"
21+
HorizontalAlignment="Center"
22+
VerticalAlignment="Center"
23+
AnimatedIcon.State="NormalOff">
24+
25+
<animatedVisuals:AnimatedChevronRightDownSmallVisualSource />
26+
<AnimatedIcon.FallbackIconSource>
27+
<FontIconSource Glyph="&#xE76C;" />
28+
</AnimatedIcon.FallbackIconSource>
29+
</AnimatedIcon>
30+
</Grid>
31+
</UserControl>

App/Controls/ExpandChevron.xaml.cs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using DependencyPropertyGenerator;
2+
using Microsoft.UI.Xaml.Controls;
3+
using Microsoft.UI.Xaml.Media;
4+
5+
namespace Coder.Desktop.App.Controls;
6+
7+
[DependencyProperty<bool>("IsOpen", DefaultValue = false)]
8+
[DependencyProperty<SolidColorBrush>("Foreground")]
9+
public sealed partial class ExpandChevron : UserControl
10+
{
11+
public ExpandChevron()
12+
{
13+
InitializeComponent();
14+
}
15+
16+
partial void OnIsOpenChanged(bool oldValue, bool newValue)
17+
{
18+
var newState = newValue ? "NormalOn" : "NormalOff";
19+
AnimatedIcon.SetState(ChevronIcon, newState);
20+
}
21+
}

App/Controls/ExpandContent.xaml

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<UserControl
4+
x:Class="Coder.Desktop.App.Controls.ExpandContent"
5+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
7+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
9+
xmlns:toolkit="using:CommunityToolkit.WinUI"
10+
mc:Ignorable="d">
11+
12+
<Grid x:Name="CollapsiblePanel" Opacity="0" Visibility="Collapsed" toolkit:UIElementExtensions.ClipToBounds="True">
13+
<Grid.RenderTransform>
14+
<TranslateTransform x:Name="SlideTransform" Y="-10" />
15+
</Grid.RenderTransform>
16+
17+
<VisualStateManager.VisualStateGroups>
18+
<VisualStateGroup>
19+
<VisualState x:Name="ExpandedState">
20+
<Storyboard>
21+
<ObjectAnimationUsingKeyFrames
22+
Storyboard.TargetName="CollapsiblePanel"
23+
Storyboard.TargetProperty="Visibility">
24+
25+
<DiscreteObjectKeyFrame KeyTime="0">
26+
<DiscreteObjectKeyFrame.Value>
27+
<Visibility>Visible</Visibility>
28+
</DiscreteObjectKeyFrame.Value>
29+
</DiscreteObjectKeyFrame>
30+
</ObjectAnimationUsingKeyFrames>
31+
<DoubleAnimation
32+
Storyboard.TargetName="CollapsiblePanel"
33+
Storyboard.TargetProperty="Opacity"
34+
To="1"
35+
Duration="0:0:0.2" />
36+
<DoubleAnimation
37+
Storyboard.TargetName="SlideTransform"
38+
Storyboard.TargetProperty="Y"
39+
To="0"
40+
Duration="0:0:0.2" />
41+
</Storyboard>
42+
</VisualState>
43+
44+
<VisualState x:Name="CollapsedState">
45+
<Storyboard Completed="{x:Bind CollapseAnimation_Completed}">
46+
<DoubleAnimation
47+
Storyboard.TargetName="CollapsiblePanel"
48+
Storyboard.TargetProperty="Opacity"
49+
To="0"
50+
Duration="0:0:0.2" />
51+
<DoubleAnimation
52+
Storyboard.TargetName="SlideTransform"
53+
Storyboard.TargetProperty="Y"
54+
To="-10"
55+
Duration="0:0:0.2" />
56+
</Storyboard>
57+
</VisualState>
58+
</VisualStateGroup>
59+
</VisualStateManager.VisualStateGroups>
60+
</Grid>
61+
</UserControl>

App/Controls/ExpandContent.xaml.cs

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using DependencyPropertyGenerator;
2+
using Microsoft.UI.Xaml;
3+
using Microsoft.UI.Xaml.Controls;
4+
using Microsoft.UI.Xaml.Markup;
5+
6+
namespace Coder.Desktop.App.Controls;
7+
8+
[ContentProperty(Name = nameof(Children))]
9+
[DependencyProperty<bool>("IsOpen", DefaultValue = false)]
10+
public sealed partial class ExpandContent : UserControl
11+
{
12+
public UIElementCollection Children => CollapsiblePanel.Children;
13+
14+
public ExpandContent()
15+
{
16+
InitializeComponent();
17+
}
18+
19+
public void CollapseAnimation_Completed(object? sender, object args)
20+
{
21+
// Hide the panel completely when the collapse animation is done. This
22+
// cannot be done with keyframes for some reason.
23+
//
24+
// Without this, the space will still be reserved for the panel.
25+
CollapsiblePanel.Visibility = Visibility.Collapsed;
26+
}
27+
28+
partial void OnIsOpenChanged(bool oldValue, bool newValue)
29+
{
30+
var newState = newValue ? "ExpandedState" : "CollapsedState";
31+
32+
// The animation can't set visibility when starting or ending the
33+
// animation.
34+
if (newValue)
35+
CollapsiblePanel.Visibility = Visibility.Visible;
36+
37+
VisualStateManager.GoToState(this, newState, true);
38+
}
39+
}

App/Converters/DependencyObjectSelector.cs

+13
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,15 @@ private void UpdateSelectedObject()
156156
ClearValue(SelectedObjectProperty);
157157
}
158158

159+
private static void VerifyReferencesProperty(IObservableVector<DependencyObject> references)
160+
{
161+
// Ensure unique keys and that the values are DependencyObjectSelectorItem<K, V>.
162+
var items = references.OfType<DependencyObjectSelectorItem<TK, TV>>().ToArray();
163+
var keys = items.Select(i => i.Key).Distinct().ToArray();
164+
if (keys.Length != references.Count)
165+
throw new ArgumentException("ObservableCollection Keys must be unique.");
166+
}
167+
159168
// Called when the References property is replaced.
160169
private static void ReferencesPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
161170
{
@@ -166,12 +175,16 @@ private static void ReferencesPropertyChanged(DependencyObject obj, DependencyPr
166175
oldValue.VectorChanged -= self.OnVectorChangedReferences;
167176
var newValue = args.NewValue as DependencyObjectCollection;
168177
if (newValue != null)
178+
{
179+
VerifyReferencesProperty(newValue);
169180
newValue.VectorChanged += self.OnVectorChangedReferences;
181+
}
170182
}
171183

172184
// Called when the References collection changes without being replaced.
173185
private void OnVectorChangedReferences(IObservableVector<DependencyObject> sender, IVectorChangedEventArgs args)
174186
{
187+
VerifyReferencesProperty(sender);
175188
UpdateSelectedObject();
176189
}
177190

App/Models/CredentialModel.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1+
using System;
2+
13
namespace Coder.Desktop.App.Models;
24

35
public enum CredentialState
46
{
57
// Unknown means "we haven't checked yet"
68
Unknown,
79

8-
// Invalid means "we checked and there's either no saved credentials or they are not valid"
10+
// Invalid means "we checked and there's either no saved credentials, or they are not valid"
911
Invalid,
1012

11-
// Valid means "we checked and there are saved credentials and they are valid"
13+
// Valid means "we checked and there are saved credentials, and they are valid"
1214
Valid,
1315
}
1416

1517
public class CredentialModel
1618
{
1719
public CredentialState State { get; init; } = CredentialState.Unknown;
1820

19-
public string? CoderUrl { get; init; }
21+
public Uri? CoderUrl { get; init; }
2022
public string? ApiToken { get; init; }
2123

2224
public string? Username { get; init; }

App/Services/CredentialManager.cs

+26-11
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class RawCredentials
2121
[JsonSerializable(typeof(RawCredentials))]
2222
public partial class RawCredentialsJsonContext : JsonSerializerContext;
2323

24-
public interface ICredentialManager
24+
public interface ICredentialManager : ICoderApiClientCredentialProvider
2525
{
2626
public event EventHandler<CredentialModel> CredentialsChanged;
2727

@@ -59,7 +59,8 @@ public interface ICredentialBackend
5959
/// </summary>
6060
public class CredentialManager : ICredentialManager
6161
{
62-
private const string CredentialsTargetName = "Coder.Desktop.App.Credentials";
62+
private readonly ICredentialBackend Backend;
63+
private readonly ICoderApiClientFactory CoderApiClientFactory;
6364

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

82-
private ICredentialBackend Backend { get; } = new WindowsCredentialBackend(CredentialsTargetName);
83-
84-
private ICoderApiClientFactory CoderApiClientFactory { get; } = new CoderApiClientFactory();
85-
86-
public CredentialManager()
87-
{
88-
}
89-
9083
public CredentialManager(ICredentialBackend backend, ICoderApiClientFactory coderApiClientFactory)
9184
{
9285
Backend = backend;
@@ -108,6 +101,20 @@ public CredentialModel GetCachedCredentials()
108101
};
109102
}
110103

104+
// Implements ICoderApiClientCredentialProvider
105+
public CoderApiClientCredential? GetCoderApiClientCredential()
106+
{
107+
var latestCreds = _latestCredentials;
108+
if (latestCreds is not { State: CredentialState.Valid })
109+
return null;
110+
111+
return new CoderApiClientCredential
112+
{
113+
CoderUrl = latestCreds.CoderUrl,
114+
ApiToken = latestCreds.ApiToken ?? "",
115+
};
116+
}
117+
111118
public async Task<string?> GetSignInUri()
112119
{
113120
try
@@ -253,6 +260,12 @@ private async Task<CredentialModel> PopulateModel(RawCredentials? credentials, C
253260
State = CredentialState.Invalid,
254261
};
255262

263+
if (!Uri.TryCreate(credentials.CoderUrl, UriKind.Absolute, out var uri))
264+
return new CredentialModel
265+
{
266+
State = CredentialState.Invalid,
267+
};
268+
256269
BuildInfo buildInfo;
257270
User me;
258271
try
@@ -279,7 +292,7 @@ private async Task<CredentialModel> PopulateModel(RawCredentials? credentials, C
279292
return new CredentialModel
280293
{
281294
State = CredentialState.Valid,
282-
CoderUrl = credentials.CoderUrl,
295+
CoderUrl = uri,
283296
ApiToken = credentials.ApiToken,
284297
Username = me.Username,
285298
};
@@ -298,6 +311,8 @@ private void UpdateState(CredentialModel newModel)
298311

299312
public class WindowsCredentialBackend : ICredentialBackend
300313
{
314+
public const string CoderCredentialsTargetName = "Coder.Desktop.App.Credentials";
315+
301316
private readonly string _credentialsTargetName;
302317

303318
public WindowsCredentialBackend(string credentialsTargetName)

App/Services/RpcController.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public async Task StartVpn(CancellationToken ct = default)
170170
{
171171
Start = new StartRequest
172172
{
173-
CoderUrl = credentials.CoderUrl,
173+
CoderUrl = credentials.CoderUrl?.ToString(),
174174
ApiToken = credentials.ApiToken,
175175
},
176176
}, ct);

App/DisplayScale.cs renamed to App/Utils/DisplayScale.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Microsoft.UI.Xaml;
44
using WinRT.Interop;
55

6-
namespace Coder.Desktop.App;
6+
namespace Coder.Desktop.App.Utils;
77

88
/// <summary>
99
/// A static utility class to house methods related to the visual scale of the display monitor.

0 commit comments

Comments
 (0)