Skip to content

Commit a6f7bb6

Browse files
authored
feat: add workspace app icons to tray window (#86)
Closes #50
1 parent 9e4ebf2 commit a6f7bb6

38 files changed

+2057
-242
lines changed

App/App.csproj

Lines changed: 3 additions & 2 deletions
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

Lines changed: 7 additions & 1 deletion
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;
@@ -19,9 +20,9 @@
1920
using Microsoft.UI.Xaml;
2021
using Microsoft.Win32;
2122
using Microsoft.Windows.AppLifecycle;
23+
using Microsoft.Windows.AppNotifications;
2224
using Serilog;
2325
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
24-
using Microsoft.Windows.AppNotifications;
2526

2627
namespace Coder.Desktop.App;
2728

@@ -64,8 +65,11 @@ public App()
6465
loggerConfig.ReadFrom.Configuration(builder.Configuration);
6566
});
6667

68+
services.AddSingleton<ICoderApiClientFactory, CoderApiClientFactory>();
6769
services.AddSingleton<IAgentApiClientFactory, AgentApiClientFactory>();
6870

71+
services.AddSingleton<ICredentialBackend>(_ =>
72+
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
6973
services.AddSingleton<ICredentialManager, CredentialManager>();
7074
services.AddSingleton<IRpcController, RpcController>();
7175

@@ -95,6 +99,8 @@ public App()
9599
services.AddTransient<TrayWindowLoginRequiredPage>();
96100
services.AddTransient<TrayWindowLoginRequiredViewModel>();
97101
services.AddTransient<TrayWindowLoginRequiredPage>();
102+
services.AddSingleton<IAgentAppViewModelFactory, AgentAppViewModelFactory>();
103+
services.AddSingleton<IAgentViewModelFactory, AgentViewModelFactory>();
98104
services.AddTransient<TrayWindowViewModel>();
99105
services.AddTransient<TrayWindowMainPage>();
100106
services.AddTransient<TrayWindow>();

App/Controls/ExpandChevron.xaml

Lines changed: 31 additions & 0 deletions
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

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

App/Controls/ExpandContent.xaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
<DoubleAnimation
22+
Storyboard.TargetName="CollapsiblePanel"
23+
Storyboard.TargetProperty="Opacity"
24+
To="1"
25+
Duration="0:0:0.2" />
26+
<DoubleAnimation
27+
Storyboard.TargetName="SlideTransform"
28+
Storyboard.TargetProperty="Y"
29+
To="0"
30+
Duration="0:0:0.2" />
31+
</Storyboard>
32+
</VisualState>
33+
34+
<VisualState x:Name="CollapsedState">
35+
<Storyboard Completed="{x:Bind CollapseAnimation_Completed}">
36+
<DoubleAnimation
37+
Storyboard.TargetName="CollapsiblePanel"
38+
Storyboard.TargetProperty="Opacity"
39+
To="0"
40+
Duration="0:0:0.2" />
41+
<DoubleAnimation
42+
Storyboard.TargetName="SlideTransform"
43+
Storyboard.TargetProperty="Y"
44+
To="-10"
45+
Duration="0:0:0.2" />
46+
</Storyboard>
47+
</VisualState>
48+
</VisualStateGroup>
49+
</VisualStateManager.VisualStateGroups>
50+
</Grid>
51+
</UserControl>

App/Controls/ExpandContent.xaml.cs

Lines changed: 39 additions & 0 deletions
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

Lines changed: 13 additions & 0 deletions
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

Lines changed: 5 additions & 3 deletions
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/Program.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,9 @@ private static void Main(string[] args)
6363
notificationManager.NotificationInvoked += app.HandleNotification;
6464
notificationManager.Register();
6565
if (activationArgs.Kind != ExtendedActivationKind.Launch)
66-
{
6766
// this means we were activated without having already launched, so handle
6867
// the activation as well.
6968
app.OnActivated(null, activationArgs);
70-
}
7169
});
7270
}
7371
catch (Exception e)

App/Services/CredentialManager.cs

Lines changed: 26 additions & 11 deletions
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 } || latestCreds.CoderUrl is null)
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)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy