Skip to content

Commit 5040670

Browse files
committed
feat: integrate the tray app with RPC
- Modularizes the tray window to use multiple Pages for the three different states: signed out, disconnected from RPC, normal - Adds the two new aforementioned pages - Adds a CredentialManager service for storing credentials in Windows Credential Manager - Adds a RpcController service for managing the connection to the backend service and reporting state changes to the ViewModel - Switches to using separate ViewModels in the Views - Switches to using Dependency Injection for instantiating Views, ViewModels and Services - Integrates the tray window into the new RpcController on Start/Stop interactions. Workspace agent updates will be handled in a separate PR
1 parent 4c6d2bd commit 5040670

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1594
-454
lines changed

.idea/.idea.Coder.Desktop/.idea/codeStyles/Project.xml

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.Coder.Desktop/.idea/codeStyles/codeStyleConfig.xml

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

App/App.csproj

+8-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<Nullable>enable</Nullable>
1313
<EnableMsixTooling>true</EnableMsixTooling>
1414
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
15+
<!-- To use CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute: -->
16+
<LangVersion>preview</LangVersion>
1517
</PropertyGroup>
1618

1719
<ItemGroup>
@@ -37,22 +39,11 @@
3739
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3840
</PackageReference>
3941
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.2.0" />
42+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
4043
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" />
4144
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" />
4245
</ItemGroup>
4346

44-
<ItemGroup>
45-
<Page Update="TrayIcon.xaml">
46-
<Generator>MSBuild:Compile</Generator>
47-
</Page>
48-
</ItemGroup>
49-
50-
<ItemGroup>
51-
<Page Update="HorizontalRule.xaml">
52-
<Generator>MSBuild:Compile</Generator>
53-
</Page>
54-
</ItemGroup>
55-
5647
<!--
5748
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
5849
Tools extension to be activated for this project even if the Windows App SDK Nuget
@@ -61,6 +52,11 @@
6152
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
6253
<ProjectCapability Include="Msix" />
6354
</ItemGroup>
55+
<ItemGroup>
56+
<ProjectReference Include="..\CoderSdk\CoderSdk.csproj" />
57+
<ProjectReference Include="..\Vpn.Proto\Vpn.Proto.csproj" />
58+
<ProjectReference Include="..\Vpn\Vpn.csproj" />
59+
</ItemGroup>
6460

6561
<!--
6662
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution

App/App.xaml.cs

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,55 @@
1+
using System;
2+
using System.Diagnostics;
3+
using Coder.Desktop.App.Models;
4+
using Coder.Desktop.App.Services;
5+
using Coder.Desktop.App.Views;
6+
using Coder.Desktop.App.Views.Pages;
7+
using Microsoft.Extensions.DependencyInjection;
18
using Microsoft.UI.Xaml;
29

310
namespace Coder.Desktop.App;
411

512
public partial class App : Application
613
{
7-
private TrayWindow? TrayWindow;
14+
private readonly IServiceProvider _services;
15+
private TrayWindow? _trayWindow;
16+
private readonly bool _handleClosedEvents = true;
817

918
public App()
1019
{
20+
var services = new ServiceCollection();
21+
services.AddSingleton<ICredentialManager, CredentialManager>();
22+
services.AddSingleton<IRpcController, RpcController>();
23+
24+
services.AddTransient<TrayWindowDisconnectedViewModel>();
25+
services.AddTransient<TrayWindowDisconnectedPage>();
26+
services.AddTransient<TrayWindowLoginRequiredViewModel>();
27+
services.AddTransient<TrayWindowLoginRequiredPage>();
28+
services.AddTransient<TrayWindowLoginRequiredViewModel>();
29+
services.AddTransient<TrayWindowLoginRequiredPage>();
30+
services.AddTransient<TrayWindowViewModel>();
31+
services.AddTransient<TrayWindowMainPage>();
32+
services.AddTransient<TrayWindow>();
33+
34+
_services = services.BuildServiceProvider();
35+
36+
#if DEBUG
37+
UnhandledException += (_, e) => { Debug.WriteLine(e.Exception.ToString()); };
38+
#endif
39+
1140
InitializeComponent();
1241
}
1342

14-
private bool HandleClosedEvents { get; } = true;
15-
1643
protected override void OnLaunched(LaunchActivatedEventArgs args)
1744
{
18-
TrayWindow = new TrayWindow();
19-
TrayWindow.Closed += (sender, args) =>
45+
_trayWindow = _services.GetRequiredService<TrayWindow>();
46+
_trayWindow.Closed += (sender, args) =>
2047
{
2148
// TODO: wire up HandleClosedEvents properly
22-
if (HandleClosedEvents)
49+
if (_handleClosedEvents)
2350
{
2451
args.Handled = true;
25-
TrayWindow.AppWindow.Hide();
52+
_trayWindow.AppWindow.Hide();
2653
}
2754
};
2855
}

App/HorizontalRule.xaml renamed to App/Controls/HorizontalRule.xaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22

33
<UserControl
4-
x:Class="Coder.Desktop.App.HorizontalRule"
4+
x:Class="Coder.Desktop.App.Controls.HorizontalRule"
55
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
66
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
77
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

App/HorizontalRule.xaml.cs renamed to App/Controls/HorizontalRule.xaml.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Microsoft.UI.Xaml.Controls;
22

3-
namespace Coder.Desktop.App;
3+
namespace Coder.Desktop.App.Controls;
44

55
public sealed partial class HorizontalRule : UserControl
66
{

App/TrayIcon.xaml renamed to App/Controls/TrayIcon.xaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22

33
<UserControl
4-
x:Class="Coder.Desktop.App.TrayIcon"
4+
x:Class="Coder.Desktop.App.Controls.TrayIcon"
55
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
66
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
77
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

App/TrayIcon.xaml.cs renamed to App/Controls/TrayIcon.xaml.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using Microsoft.UI.Xaml.Controls;
77
using Microsoft.UI.Xaml.Media.Imaging;
88

9-
namespace Coder.Desktop.App;
9+
namespace Coder.Desktop.App.Controls;
1010

1111
[DependencyProperty<ICommand>("OpenCommand")]
1212
[DependencyProperty<ICommand>("ExitCommand")]
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using DependencyPropertyGenerator;
3+
using Microsoft.UI.Xaml;
4+
using Microsoft.UI.Xaml.Data;
5+
6+
namespace Coder.Desktop.App.Converters;
7+
8+
[DependencyProperty<object>("TrueValue", DefaultValue = true)]
9+
[DependencyProperty<object>("FalseValue", DefaultValue = true)]
10+
public partial class BoolToObjectConverter : DependencyObject, IValueConverter
11+
{
12+
public object Convert(object value, Type targetType, object parameter, string language)
13+
{
14+
return value is true ? TrueValue : FalseValue;
15+
}
16+
17+
public object ConvertBack(object value, Type targetType, object parameter, string language)
18+
{
19+
throw new NotImplementedException();
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.UI.Xaml;
2+
3+
namespace Coder.Desktop.App.Converters;
4+
5+
public partial class BoolToVisibilityConverter : BoolToObjectConverter
6+
{
7+
public BoolToVisibilityConverter()
8+
{
9+
TrueValue = Visibility.Visible;
10+
FalseValue = Visibility.Collapsed;
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.UI.Xaml;
2+
3+
namespace Coder.Desktop.App.Converters;
4+
5+
public partial class InverseBoolToVisibilityConverter : BoolToObjectConverter
6+
{
7+
public InverseBoolToVisibilityConverter()
8+
{
9+
TrueValue = Visibility.Collapsed;
10+
FalseValue = Visibility.Visible;
11+
}
12+
}

App/Models/AgentModel.cs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Windows.ApplicationModel.DataTransfer;
2+
using Windows.UI;
3+
using CommunityToolkit.Mvvm.ComponentModel;
4+
using CommunityToolkit.Mvvm.Input;
5+
using Microsoft.UI.Xaml;
6+
using Microsoft.UI.Xaml.Controls;
7+
using Microsoft.UI.Xaml.Controls.Primitives;
8+
using Microsoft.UI.Xaml.Media;
9+
10+
namespace Coder.Desktop.App.Models;
11+
12+
public enum AgentConnectionStatus
13+
{
14+
Green,
15+
Red,
16+
Gray,
17+
}
18+
19+
public partial class AgentModel : ObservableObject
20+
{
21+
[ObservableProperty]
22+
[NotifyPropertyChangedFor(nameof(FullHostname))]
23+
public required partial string Hostname { get; set; }
24+
25+
[ObservableProperty]
26+
[NotifyPropertyChangedFor(nameof(FullHostname))]
27+
public required partial string HostnameSuffix { get; set; } // including leading dot
28+
29+
public string FullHostname => Hostname + HostnameSuffix;
30+
31+
[ObservableProperty]
32+
[NotifyPropertyChangedFor(nameof(ConnectionStatusColor))]
33+
public required partial AgentConnectionStatus ConnectionStatus { get; set; }
34+
35+
public Brush ConnectionStatusColor => ConnectionStatus switch
36+
{
37+
AgentConnectionStatus.Green => new SolidColorBrush(Color.FromArgb(255, 52, 199, 89)),
38+
AgentConnectionStatus.Red => new SolidColorBrush(Color.FromArgb(255, 255, 59, 48)),
39+
_ => new SolidColorBrush(Color.FromArgb(255, 142, 142, 147)),
40+
};
41+
42+
[ObservableProperty]
43+
public required partial string DashboardUrl { get; set; }
44+
45+
[RelayCommand]
46+
private void CopyHostname(object parameter)
47+
{
48+
var dataPackage = new DataPackage
49+
{
50+
RequestedOperation = DataPackageOperation.Copy,
51+
};
52+
dataPackage.SetText(FullHostname);
53+
Clipboard.SetContent(dataPackage);
54+
55+
if (parameter is not FrameworkElement frameworkElement) return;
56+
57+
var flyout = new Flyout
58+
{
59+
Content = new TextBlock
60+
{
61+
Text = "DNS Copied",
62+
Margin = new Thickness(4),
63+
},
64+
};
65+
FlyoutBase.SetAttachedFlyout(frameworkElement, flyout);
66+
FlyoutBase.ShowAttachedFlyout(frameworkElement);
67+
}
68+
}

App/Models/CredentialModel.cs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Coder.Desktop.App.Models;
2+
3+
public enum CredentialState
4+
{
5+
Invalid,
6+
Valid,
7+
}
8+
9+
public class CredentialModel
10+
{
11+
public CredentialState State { get; set; } = CredentialState.Invalid;
12+
13+
public string? CoderUrl { get; set; }
14+
public string? ApiToken { get; set; }
15+
16+
public CredentialModel Clone()
17+
{
18+
return new CredentialModel
19+
{
20+
State = State,
21+
CoderUrl = CoderUrl,
22+
ApiToken = ApiToken,
23+
};
24+
}
25+
}

App/Models/RpcModel.cs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Collections.Generic;
2+
3+
namespace Coder.Desktop.App.Models;
4+
5+
public enum RpcLifecycle
6+
{
7+
Disconnected,
8+
Connecting,
9+
Connected,
10+
}
11+
12+
public enum VpnLifecycle
13+
{
14+
Stopped,
15+
Starting,
16+
Started,
17+
Stopping,
18+
}
19+
20+
public class RpcModel
21+
{
22+
public RpcLifecycle RpcLifecycle { get; set; } = RpcLifecycle.Disconnected;
23+
24+
public VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Stopped;
25+
26+
// TODO: write a type for this or maybe use the Rpc Type directly
27+
public List<object> VisibleAgents { get; set; } = [];
28+
29+
public RpcModel Clone()
30+
{
31+
return new RpcModel
32+
{
33+
RpcLifecycle = RpcLifecycle,
34+
VpnLifecycle = VpnLifecycle,
35+
VisibleAgents = VisibleAgents,
36+
};
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Threading.Tasks;
2+
using Coder.Desktop.App.Services;
3+
using CommunityToolkit.Mvvm.ComponentModel;
4+
using CommunityToolkit.Mvvm.Input;
5+
6+
namespace Coder.Desktop.App.Models;
7+
8+
public partial class TrayWindowDisconnectedViewModel : ObservableObject
9+
{
10+
private readonly IRpcController _rpcController;
11+
12+
[ObservableProperty]
13+
public partial bool ReconnectButtonEnabled { get; set; } = true;
14+
15+
public TrayWindowDisconnectedViewModel(IRpcController rpcController)
16+
{
17+
_rpcController = rpcController;
18+
_rpcController.StateChanged += (_, rpcModel) => UpdateFromRpcModel(rpcModel);
19+
}
20+
21+
private void UpdateFromRpcModel(RpcModel rpcModel)
22+
{
23+
ReconnectButtonEnabled = rpcModel.RpcLifecycle != RpcLifecycle.Disconnected;
24+
}
25+
26+
[RelayCommand]
27+
public async Task Reconnect()
28+
{
29+
await _rpcController.Reconnect();
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using CommunityToolkit.Mvvm.ComponentModel;
2+
using CommunityToolkit.Mvvm.Input;
3+
4+
namespace Coder.Desktop.App.Models;
5+
6+
public partial class TrayWindowLoginRequiredViewModel : ObservableObject
7+
{
8+
[RelayCommand]
9+
public void Login()
10+
{
11+
// TODO: open the login window
12+
}
13+
}

0 commit comments

Comments
 (0)