From 198071300a555d4da1e8976738c157adeaa84b98 Mon Sep 17 00:00:00 2001 From: Spike Curtis <spike@coder.com> Date: Fri, 7 Feb 2025 12:03:58 +0400 Subject: [PATCH] feat: Sign in Window views & view models --- App/App.csproj | 18 ++++++ App/App.xaml.cs | 5 +- App/DisplayScale.cs | 26 +++++++++ App/SignInTokenPage.xaml | 93 +++++++++++++++++++++++++++++++ App/SignInTokenPage.xaml.cs | 37 ++++++++++++ App/SignInURLPage.xaml | 53 ++++++++++++++++++ App/SignInURLPage.xaml.cs | 33 +++++++++++ App/SignInWindow.xaml | 15 +++++ App/SignInWindow.xaml.cs | 60 ++++++++++++++++++++ App/TrayWindow.xaml | 5 +- App/TrayWindow.xaml.cs | 26 ++++----- App/ViewModels/SignInViewModel.cs | 45 +++++++++++++++ 12 files changed, 400 insertions(+), 16 deletions(-) create mode 100644 App/DisplayScale.cs create mode 100644 App/SignInTokenPage.xaml create mode 100644 App/SignInTokenPage.xaml.cs create mode 100644 App/SignInURLPage.xaml create mode 100644 App/SignInURLPage.xaml.cs create mode 100644 App/SignInWindow.xaml create mode 100644 App/SignInWindow.xaml.cs create mode 100644 App/ViewModels/SignInViewModel.cs diff --git a/App/App.csproj b/App/App.csproj index cae1812..5f06f25 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -41,6 +41,24 @@ <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" /> </ItemGroup> + <ItemGroup> + <Page Update="SignInTokenPage.xaml"> + <Generator>MSBuild:Compile</Generator> + </Page> + </ItemGroup> + + <ItemGroup> + <Page Update="SignInWindow.xaml"> + <Generator>MSBuild:Compile</Generator> + </Page> + </ItemGroup> + + <ItemGroup> + <Page Update="SignInURLPage.xaml"> + <Generator>MSBuild:Compile</Generator> + </Page> + </ItemGroup> + <ItemGroup> <Page Update="TrayIcon.xaml"> <Generator>MSBuild:Compile</Generator> diff --git a/App/App.xaml.cs b/App/App.xaml.cs index 4fbde75..6081d69 100644 --- a/App/App.xaml.cs +++ b/App/App.xaml.cs @@ -1,3 +1,4 @@ +using Coder.Desktop.App.ViewModels; using Microsoft.UI.Xaml; namespace Coder.Desktop.App; @@ -5,9 +6,11 @@ namespace Coder.Desktop.App; public partial class App : Application { private TrayWindow? TrayWindow; + public SignInViewModel SignInViewModel { get; } public App() { + SignInViewModel = new SignInViewModel(); InitializeComponent(); } @@ -15,7 +18,7 @@ public App() protected override void OnLaunched(LaunchActivatedEventArgs args) { - TrayWindow = new TrayWindow(); + TrayWindow = new TrayWindow(SignInViewModel); TrayWindow.Closed += (sender, args) => { // TODO: wire up HandleClosedEvents properly diff --git a/App/DisplayScale.cs b/App/DisplayScale.cs new file mode 100644 index 0000000..3712351 --- /dev/null +++ b/App/DisplayScale.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.UI.Xaml; +using WinRT.Interop; +using System.Runtime.InteropServices; + +namespace Coder.Desktop.App; + +/// <summary> +/// A static utility class to house methods related to the visual scale of the display monitor. +/// </summary> +public static class DisplayScale +{ + public static double WindowScale(Window win) + { + var hwnd = WindowNative.GetWindowHandle(win); + var dpi = NativeApi.GetDpiForWindow(hwnd); + if (dpi == 0) return 1; // assume scale of 1 + return dpi / 96.0; // 96 DPI == 1 + } + + public class NativeApi + { + [DllImport("user32.dll")] + public static extern int GetDpiForWindow(IntPtr hwnd); + } +} diff --git a/App/SignInTokenPage.xaml b/App/SignInTokenPage.xaml new file mode 100644 index 0000000..97194ae --- /dev/null +++ b/App/SignInTokenPage.xaml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<Page + x:Class="Coder.Desktop.App.SignInTokenPage" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="using:Coder.Desktop.App" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" + Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> + + <StackPanel + Orientation="Vertical" + HorizontalAlignment="Stretch" + VerticalAlignment="Top" + Padding="20" + Spacing="10"> + + <TextBlock + Text="Coder Desktop" + FontSize="24" + VerticalAlignment="Center" + HorizontalAlignment="Center"/> + + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + <Grid.RowDefinitions> + <RowDefinition Height="1*" /> + <RowDefinition Height="1*" /> + <RowDefinition Height="1*" /> + </Grid.RowDefinitions> + + <TextBlock + Grid.Column="0" + Grid.Row="0" + Text="Server URL" + HorizontalAlignment="Right" + Padding="10"/> + + <TextBlock + Grid.Column="1" + Grid.Row="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Center" + Padding="10" + Text="{x:Bind Path=ViewModel.Url, Mode=OneTime}" + /> + + <TextBlock + Grid.Column="0" + Grid.Row="1" + Text="Session Token" + HorizontalAlignment="Right" + Padding="10"/> + + <TextBox + Grid.Column="1" + Grid.Row="1" + HorizontalAlignment="Stretch" + PlaceholderText="paste your token here" + Text="{x:Bind Path=ViewModel.CoderToken, Mode=TwoWay}" + /> + + <HyperlinkButton + Grid.Column="1" + Grid.Row="2" + Content="Generate a token via the Web UI" + NavigateUri="{x:Bind ViewModel.GenTokenURL, Mode=OneTime}" /> + + </Grid> + + <StackPanel + Orientation="Horizontal" + HorizontalAlignment="Center" + Spacing="10"> + <Button Content="Back" HorizontalAlignment="Right" + Click="{x:Bind SignInWindow.NavigateToURLPage}"/> + <Button Content="Sign In" HorizontalAlignment="Left" Style="{StaticResource AccentButtonStyle}" + Click="{x:Bind ViewModel.SignIn_Click}"/> + </StackPanel> + + <TextBlock + Text="{x:Bind ViewModel.LoginError, Mode=OneWay}" + HorizontalAlignment="Center" + Foreground="Red" + /> + + + </StackPanel> +</Page> diff --git a/App/SignInTokenPage.xaml.cs b/App/SignInTokenPage.xaml.cs new file mode 100644 index 0000000..374ade8 --- /dev/null +++ b/App/SignInTokenPage.xaml.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Coder.Desktop.App.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Coder.Desktop.App +{ + /// <summary> + /// A sign in page to accept the user's Coder token. + /// </summary> + public sealed partial class SignInTokenPage : Page + { + public SignInTokenPage(SignInWindow parent, SignInViewModel viewModel) + { + InitializeComponent(); + ViewModel = viewModel; + SignInWindow = parent; + } + + public readonly SignInViewModel ViewModel; + public readonly SignInWindow SignInWindow; + } +} diff --git a/App/SignInURLPage.xaml b/App/SignInURLPage.xaml new file mode 100644 index 0000000..d04fe26 --- /dev/null +++ b/App/SignInURLPage.xaml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<Page + x:Class="Coder.Desktop.App.SignInURLPage" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="using:Coder.Desktop.App" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" + Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> + + <StackPanel + Orientation="Vertical" + HorizontalAlignment="Stretch" + VerticalAlignment="Top" + Padding="20" + Spacing="10"> + + <TextBlock + Text="Coder Desktop" + FontSize="24" + VerticalAlignment="Center" + HorizontalAlignment="Center"/> + + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + <TextBlock + Grid.Column="0" + Text="Server URL" + HorizontalAlignment="Right" + Padding="10"/> + + <TextBox + Name="URLTextBox" + Grid.Column="1" + HorizontalAlignment="Stretch" + PlaceholderText="https://coder.example.com" + Text="{x:Bind Path=ViewModel.Url, Mode=TwoWay}" + /> + + </Grid> + + <Button + Content="Next" + HorizontalAlignment="Center" + Click="{x:Bind SignInWindow.NavigateToTokenPage}" + Style="{StaticResource AccentButtonStyle}" /> + + </StackPanel> +</Page> diff --git a/App/SignInURLPage.xaml.cs b/App/SignInURLPage.xaml.cs new file mode 100644 index 0000000..8c1cf49 --- /dev/null +++ b/App/SignInURLPage.xaml.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Coder.Desktop.App.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +namespace Coder.Desktop.App; + +/// <summary> +/// A login page to enter the Coder Server URL +/// </summary> +public sealed partial class SignInURLPage : Page +{ + public SignInURLPage(SignInWindow parent, SignInViewModel viewModel) + { + InitializeComponent(); + ViewModel = viewModel; + SignInWindow = parent; + } + + public readonly SignInViewModel ViewModel; + public readonly SignInWindow SignInWindow; +} diff --git a/App/SignInWindow.xaml b/App/SignInWindow.xaml new file mode 100644 index 0000000..8dc397f --- /dev/null +++ b/App/SignInWindow.xaml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<Window + x:Class="Coder.Desktop.App.SignInWindow" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="using:Coder.Desktop.App" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" + Title="Sign in to Coder"> + + <Window.SystemBackdrop> + <DesktopAcrylicBackdrop /> + </Window.SystemBackdrop> +</Window> diff --git a/App/SignInWindow.xaml.cs b/App/SignInWindow.xaml.cs new file mode 100644 index 0000000..96db63b --- /dev/null +++ b/App/SignInWindow.xaml.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Coder.Desktop.App.ViewModels; +using CommunityToolkit.Mvvm.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.Graphics; + +namespace Coder.Desktop.App; + +/// <summary> +/// The dialog window to allow the user to sign into their Coder server. +/// </summary> +public sealed partial class SignInWindow : Window +{ + private const double WIDTH = 600.0; + private const double HEIGHT = 300.0; + + private readonly SignInViewModel viewModel; + public SignInWindow(SignInViewModel vm) + { + viewModel = vm; + InitializeComponent(); + var urlPage = new SignInURLPage(this, viewModel); + Content = urlPage; + ResizeWindow(); + } + + [RelayCommand] + public void NavigateToTokenPage() + { + var tokenPage = new SignInTokenPage(this, viewModel); + Content = tokenPage; + } + + [RelayCommand] + public void NavigateToURLPage() + { + var urlPage = new SignInURLPage(this, viewModel); + Content = urlPage; + } + + private void ResizeWindow() + { + var scale = DisplayScale.WindowScale(this); + var height = (int)(HEIGHT * scale); + var width = (int)(WIDTH * scale); + AppWindow.Resize(new SizeInt32(width, height)); + } +} diff --git a/App/TrayWindow.xaml b/App/TrayWindow.xaml index a779bb5..a2f737a 100644 --- a/App/TrayWindow.xaml +++ b/App/TrayWindow.xaml @@ -159,9 +159,10 @@ <HyperlinkButton Margin="-10,0,-10,0" HorizontalAlignment="Stretch" - HorizontalContentAlignment="Left"> + HorizontalContentAlignment="Left" + Click="{x:Bind SignIn_Click}"> - <TextBlock Text="Sign out" Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" /> + <TextBlock Text="Sign in" Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" /> </HyperlinkButton> <!-- For some strange reason, setting OpenCommand and ExitCommand here doesn't work, see .cs --> diff --git a/App/TrayWindow.xaml.cs b/App/TrayWindow.xaml.cs index fcd1c0e..54b0476 100644 --- a/App/TrayWindow.xaml.cs +++ b/App/TrayWindow.xaml.cs @@ -19,6 +19,7 @@ using Microsoft.UI.Xaml.Media; using WinRT.Interop; using WindowActivatedEventArgs = Microsoft.UI.Xaml.WindowActivatedEventArgs; +using Coder.Desktop.App.ViewModels; namespace Coder.Desktop.App; @@ -139,8 +140,11 @@ public sealed partial class TrayWindow : Window }, ]; - public TrayWindow() + public SignInViewModel SignInViewModel { get; } + + public TrayWindow(SignInViewModel signInViewModel) { + SignInViewModel = signInViewModel; InitializeComponent(); AppWindow.Hide(); SystemBackdrop = new DesktopAcrylicBackdrop(); @@ -190,20 +194,12 @@ private void ResizeWindow() var desiredSize = content.DesiredSize; // Adjust the AppWindow size - var scale = GetDisplayScale(); + var scale = DisplayScale.WindowScale(this); var height = (int)(desiredSize.Height * scale); var width = (int)(WIDTH * scale); AppWindow.Resize(new SizeInt32(width, height)); } - private double GetDisplayScale() - { - var hwnd = WindowNative.GetWindowHandle(this); - var dpi = NativeApi.GetDpiForWindow(hwnd); - if (dpi == 0) return 1; // assume scale of 1 - return dpi / 96.0; // 96 DPI == 1 - } - public void MoveResizeAndActivate() { SaveCursorPos(); @@ -313,13 +309,17 @@ public class NativeApi [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hwnd); - [DllImport("user32.dll")] - public static extern int GetDpiForWindow(IntPtr hwnd); - public struct POINT { public int X; public int Y; } } + + [RelayCommand] + private void SignIn_Click() + { + var window = new SignInWindow(SignInViewModel); + window.Activate(); + } } diff --git a/App/ViewModels/SignInViewModel.cs b/App/ViewModels/SignInViewModel.cs new file mode 100644 index 0000000..ea233f8 --- /dev/null +++ b/App/ViewModels/SignInViewModel.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace Coder.Desktop.App.ViewModels; + +/// <summary> +/// The View Model backing the sign in window and all its associated pages. +/// </summary> +public partial class SignInViewModel : ObservableObject +{ + public SignInViewModel() + { + _url = string.Empty; + CoderToken = string.Empty; + } + + [ObservableProperty] + private string _url; + + public string CoderToken; + + [ObservableProperty] + private string? _loginError; + + [RelayCommand] + public void SignIn_Click() + { + // TODO: this should call into the backing model to do the login with _url and Token. + LoginError = "This is a placeholder error."; + } + + public string GenTokenURL + { + get + { + // TODO: use a real URL parsing library + return _url + "/cli-auth"; + } + } +}