Skip to content

Commit 641f1bc

Browse files
feat: Sign in Window views & view models (#20)
Adds views for the Sign In dialog window, and a backing view model. Co-authored-by: Dean Sheather <dean@deansheather.com>
1 parent 88a4a97 commit 641f1bc

11 files changed

+470
-7
lines changed

App/App.xaml.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ namespace Coder.Desktop.App;
1212
public partial class App : Application
1313
{
1414
private readonly IServiceProvider _services;
15-
private TrayWindow? _trayWindow;
1615
private readonly bool _handleClosedEvents = true;
1716

1817
public App()
@@ -21,6 +20,11 @@ public App()
2120
services.AddSingleton<ICredentialManager, CredentialManager>();
2221
services.AddSingleton<IRpcController, RpcController>();
2322

23+
// SignInWindow views and view models
24+
services.AddTransient<SignInViewModel>();
25+
services.AddTransient<SignInWindow>();
26+
27+
// TrayWindow views and view models
2428
services.AddTransient<TrayWindowDisconnectedViewModel>();
2529
services.AddTransient<TrayWindowDisconnectedPage>();
2630
services.AddTransient<TrayWindowLoginRequiredViewModel>();
@@ -42,14 +46,14 @@ public App()
4246

4347
protected override void OnLaunched(LaunchActivatedEventArgs args)
4448
{
45-
_trayWindow = _services.GetRequiredService<TrayWindow>();
46-
_trayWindow.Closed += (sender, args) =>
49+
var trayWindow = _services.GetRequiredService<TrayWindow>();
50+
trayWindow.Closed += (sender, args) =>
4751
{
4852
// TODO: wire up HandleClosedEvents properly
4953
if (_handleClosedEvents)
5054
{
5155
args.Handled = true;
52-
_trayWindow.AppWindow.Hide();
56+
trayWindow.AppWindow.Hide();
5357
}
5458
};
5559
}

App/DisplayScale.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using Microsoft.UI.Xaml;
4+
using WinRT.Interop;
5+
6+
namespace Coder.Desktop.App;
7+
8+
/// <summary>
9+
/// A static utility class to house methods related to the visual scale of the display monitor.
10+
/// </summary>
11+
public static class DisplayScale
12+
{
13+
public static double WindowScale(Window win)
14+
{
15+
var hwnd = WindowNative.GetWindowHandle(win);
16+
var dpi = NativeApi.GetDpiForWindow(hwnd);
17+
if (dpi == 0) return 1; // assume scale of 1
18+
return dpi / 96.0; // 96 DPI == 1
19+
}
20+
21+
public class NativeApi
22+
{
23+
[DllImport("user32.dll")]
24+
public static extern int GetDpiForWindow(IntPtr hwnd);
25+
}
26+
}

App/Services/CredentialManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
6767
try
6868
{
6969
var sdkClient = new CoderApiClient(uri);
70+
sdkClient.SetSessionToken(apiToken);
7071
// TODO: we should probably perform a version check here too,
7172
// rather than letting the service do it on Start
7273
_ = await sdkClient.GetBuildInfo(ct);

App/ViewModels/SignInViewModel.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Coder.Desktop.App.Services;
5+
using Coder.Desktop.App.Views;
6+
using CommunityToolkit.Mvvm.ComponentModel;
7+
using CommunityToolkit.Mvvm.Input;
8+
using Microsoft.UI.Xaml;
9+
10+
namespace Coder.Desktop.App.ViewModels;
11+
12+
/// <summary>
13+
/// The View Model backing the sign in window and all its associated pages.
14+
/// </summary>
15+
public partial class SignInViewModel : ObservableObject
16+
{
17+
private readonly ICredentialManager _credentialManager;
18+
19+
[ObservableProperty]
20+
[NotifyPropertyChangedFor(nameof(CoderUrlError))]
21+
[NotifyPropertyChangedFor(nameof(GenTokenUrl))]
22+
public partial string CoderUrl { get; set; } = string.Empty;
23+
24+
[ObservableProperty]
25+
[NotifyPropertyChangedFor(nameof(CoderUrlError))]
26+
public partial bool CoderUrlTouched { get; set; } = false;
27+
28+
[ObservableProperty]
29+
[NotifyPropertyChangedFor(nameof(ApiTokenError))]
30+
public partial string ApiToken { get; set; } = string.Empty;
31+
32+
[ObservableProperty]
33+
[NotifyPropertyChangedFor(nameof(ApiTokenError))]
34+
public partial bool ApiTokenTouched { get; set; } = false;
35+
36+
[ObservableProperty]
37+
public partial string? SignInError { get; set; } = null;
38+
39+
[ObservableProperty]
40+
public partial bool SignInLoading { get; set; } = false;
41+
42+
public string? CoderUrlError => CoderUrlTouched ? _coderUrlError : null;
43+
44+
private string? _coderUrlError
45+
{
46+
get
47+
{
48+
if (!Uri.TryCreate(CoderUrl, UriKind.Absolute, out var uri))
49+
return "Invalid URL";
50+
if (uri.Scheme is not "http" and not "https")
51+
return "Must be a HTTP or HTTPS URL";
52+
if (uri.PathAndQuery != "/")
53+
return "Must be a root URL with no path or query";
54+
return null;
55+
}
56+
}
57+
58+
public string? ApiTokenError => ApiTokenTouched ? _apiTokenError : null;
59+
60+
private string? _apiTokenError => string.IsNullOrWhiteSpace(ApiToken) ? "Invalid token" : null;
61+
62+
public Uri GenTokenUrl
63+
{
64+
get
65+
{
66+
// In case somehow the URL is invalid, just default to coder.com.
67+
// The HyperlinkButton will crash the entire app if the URL is
68+
// invalid.
69+
try
70+
{
71+
var baseUri = new Uri(CoderUrl.Trim());
72+
var cliAuthUri = new Uri(baseUri, "/cli-auth");
73+
return cliAuthUri;
74+
}
75+
catch
76+
{
77+
return new Uri("https://coder.com");
78+
}
79+
}
80+
}
81+
82+
public SignInViewModel(ICredentialManager credentialManager)
83+
{
84+
_credentialManager = credentialManager;
85+
}
86+
87+
public void CoderUrl_FocusLost(object sender, RoutedEventArgs e)
88+
{
89+
CoderUrlTouched = true;
90+
}
91+
92+
public void ApiToken_FocusLost(object sender, RoutedEventArgs e)
93+
{
94+
ApiTokenTouched = true;
95+
}
96+
97+
[RelayCommand]
98+
public void UrlPage_Next(SignInWindow signInWindow)
99+
{
100+
CoderUrlTouched = true;
101+
if (_coderUrlError != null) return;
102+
signInWindow.NavigateToTokenPage();
103+
}
104+
105+
[RelayCommand]
106+
public void TokenPage_Back(SignInWindow signInWindow)
107+
{
108+
ApiToken = "";
109+
signInWindow.NavigateToUrlPage();
110+
}
111+
112+
[RelayCommand]
113+
public async Task TokenPage_SignIn(SignInWindow signInWindow)
114+
{
115+
CoderUrlTouched = true;
116+
ApiTokenTouched = true;
117+
if (_coderUrlError != null || _apiTokenError != null) return;
118+
119+
try
120+
{
121+
SignInLoading = true;
122+
SignInError = null;
123+
124+
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
125+
await _credentialManager.SetCredentials(CoderUrl.Trim(), ApiToken.Trim(), cts.Token);
126+
127+
signInWindow.Close();
128+
}
129+
catch (Exception e)
130+
{
131+
SignInError = $"Failed to sign in: {e}";
132+
}
133+
finally
134+
{
135+
SignInLoading = false;
136+
}
137+
}
138+
}
Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
1-
using CommunityToolkit.Mvvm.ComponentModel;
1+
using System;
2+
using Coder.Desktop.App.Views;
23
using CommunityToolkit.Mvvm.Input;
4+
using Microsoft.Extensions.DependencyInjection;
35

46
namespace Coder.Desktop.App.ViewModels;
57

6-
public partial class TrayWindowLoginRequiredViewModel : ObservableObject
8+
public partial class TrayWindowLoginRequiredViewModel
79
{
10+
private readonly IServiceProvider _services;
11+
12+
private SignInWindow? _signInWindow;
13+
14+
public TrayWindowLoginRequiredViewModel(IServiceProvider services)
15+
{
16+
_services = services;
17+
}
18+
819
[RelayCommand]
920
public void Login()
1021
{
11-
// TODO: open the login window
22+
// This is safe against concurrent access since it all happens in the
23+
// UI thread.
24+
if (_signInWindow != null)
25+
{
26+
_signInWindow.Activate();
27+
return;
28+
}
29+
30+
_signInWindow = _services.GetRequiredService<SignInWindow>();
31+
_signInWindow.Closed += (_, _) => _signInWindow = null;
32+
_signInWindow.Activate();
1233
}
1334
}

App/Views/Pages/SignInTokenPage.xaml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<Page
4+
x:Class="Coder.Desktop.App.Views.Pages.SignInTokenPage"
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+
mc:Ignorable="d"
10+
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
11+
12+
<StackPanel
13+
Orientation="Vertical"
14+
HorizontalAlignment="Stretch"
15+
VerticalAlignment="Top"
16+
Padding="20"
17+
Spacing="10">
18+
19+
<TextBlock
20+
Text="Coder Desktop"
21+
FontSize="24"
22+
VerticalAlignment="Center"
23+
HorizontalAlignment="Center" />
24+
25+
<Grid>
26+
<Grid.ColumnDefinitions>
27+
<ColumnDefinition Width="Auto" />
28+
<ColumnDefinition Width="*" />
29+
</Grid.ColumnDefinitions>
30+
<Grid.RowDefinitions>
31+
<RowDefinition Height="1*" />
32+
<RowDefinition Height="1*" />
33+
<RowDefinition Height="1*" />
34+
<RowDefinition Height="1*" />
35+
<RowDefinition Height="1*" />
36+
</Grid.RowDefinitions>
37+
38+
<TextBlock
39+
Grid.Column="0"
40+
Grid.Row="0"
41+
Text="Server URL"
42+
HorizontalAlignment="Right"
43+
Padding="10" />
44+
45+
<TextBlock
46+
Grid.Column="1"
47+
Grid.Row="0"
48+
HorizontalAlignment="Stretch"
49+
VerticalAlignment="Center"
50+
Padding="10"
51+
Text="{x:Bind ViewModel.CoderUrl, Mode=OneWay}" />
52+
53+
<TextBlock
54+
Grid.Column="0"
55+
Grid.Row="2"
56+
Text="Session Token"
57+
HorizontalAlignment="Right"
58+
Padding="10" />
59+
60+
<TextBox
61+
Grid.Column="1"
62+
Grid.Row="2"
63+
HorizontalAlignment="Stretch"
64+
PlaceholderText="Paste your token here"
65+
LostFocus="{x:Bind ViewModel.ApiToken_FocusLost, Mode=OneWay}"
66+
Text="{x:Bind ViewModel.ApiToken, Mode=TwoWay}" />
67+
68+
<TextBlock
69+
Grid.Column="1"
70+
Grid.Row="3"
71+
Text="{x:Bind ViewModel.ApiTokenError, Mode=OneWay}"
72+
Foreground="Red" />
73+
74+
<HyperlinkButton
75+
Grid.Column="1"
76+
Grid.Row="4"
77+
Content="Generate a token via the Web UI"
78+
NavigateUri="{x:Bind ViewModel.GenTokenUrl, Mode=OneWay}" />
79+
</Grid>
80+
81+
<StackPanel
82+
Orientation="Horizontal"
83+
HorizontalAlignment="Center"
84+
Spacing="10">
85+
86+
<Button
87+
Content="Back" HorizontalAlignment="Right"
88+
Command="{x:Bind ViewModel.TokenPage_BackCommand, Mode=OneWay}"
89+
CommandParameter="{x:Bind SignInWindow, Mode=OneWay}" />
90+
91+
<Button
92+
Content="Sign In"
93+
HorizontalAlignment="Left"
94+
Style="{StaticResource AccentButtonStyle}"
95+
Command="{x:Bind ViewModel.TokenPage_SignInCommand, Mode=OneWay}"
96+
CommandParameter="{x:Bind SignInWindow, Mode=OneWay}" />
97+
</StackPanel>
98+
99+
<TextBlock
100+
Text="{x:Bind ViewModel.SignInError, Mode=OneWay}"
101+
HorizontalAlignment="Center"
102+
Foreground="Red" />
103+
</StackPanel>
104+
</Page>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Coder.Desktop.App.ViewModels;
2+
using Microsoft.UI.Xaml.Controls;
3+
4+
namespace Coder.Desktop.App.Views.Pages;
5+
6+
/// <summary>
7+
/// A sign in page to accept the user's Coder token.
8+
/// </summary>
9+
public sealed partial class SignInTokenPage : Page
10+
{
11+
public readonly SignInViewModel ViewModel;
12+
public readonly SignInWindow SignInWindow;
13+
14+
public SignInTokenPage(SignInWindow parent, SignInViewModel viewModel)
15+
{
16+
InitializeComponent();
17+
ViewModel = viewModel;
18+
SignInWindow = parent;
19+
}
20+
}

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