diff --git a/.claude/skills/avalonia-mvvm.md b/.claude/skills/avalonia-mvvm/SKILL.md
similarity index 98%
rename from .claude/skills/avalonia-mvvm.md
rename to .claude/skills/avalonia-mvvm/SKILL.md
index c319f63..a0ab4db 100644
--- a/.claude/skills/avalonia-mvvm.md
+++ b/.claude/skills/avalonia-mvvm/SKILL.md
@@ -1,4 +1,5 @@
---
+name: avalonia-mvvm
description: Guide for using MVVM Community Toolkit with Avalonia in RemoteViewer.Client. Use when working with ViewModels, data binding, commands, or Avalonia UI patterns.
---
@@ -9,7 +10,7 @@ The RemoteViewer.Client uses **CommunityToolkit.Mvvm v8.4.0** with Avalonia's co
## Core Patterns
### ViewModels
-- **Base class**: `ViewModelBase` extends `ObservableObject` from MVVM Toolkit
+- **Base class**: `ObservableObject` from CommunityToolkit.Mvvm
- **Location**: Co-located with views in `Views/{Feature}/` directories
- **Creation**: Use `IViewModelFactory` with dependency injection for instantiation
- **Constructor injection**: All dependencies injected via constructor (services, logger, etc.)
diff --git a/.claude/skills/design-system/SKILL.md b/.claude/skills/design-system/SKILL.md
new file mode 100644
index 0000000..d0622f4
--- /dev/null
+++ b/.claude/skills/design-system/SKILL.md
@@ -0,0 +1,86 @@
+---
+name: design-system
+description: RemoteViewer.Client design system with spacing, colors, typography, and components. Use when building UI, styling components, or working with Avalonia XAML.
+---
+
+# Design System
+
+The client uses a comprehensive design system with markup extensions, design tokens, and reusable components. All UI should use these tokens instead of hardcoded values.
+
+## Markup Extensions
+
+Located in `Themes/` folder. Add `xmlns:theme="using:RemoteViewer.Client.Themes"` to use.
+
+### Spacing (Margin/Padding)
+```xml
+Padding="{theme:Spacing MD}"
+Margin="{theme:Spacing X=LG, Y=SM}"
+Margin="{theme:Spacing Top=XL, Right=MD}"
+```
+
+Values: `None=0, XXS=2, XS=4, SM=8, MD=12, LG=16, XL=24, XXL=32`
+
+The same extension works for `Spacing` (double) and `GridLength` properties:
+```xml
+
+
+```
+
+## Design Tokens
+
+### Colors (use DynamicResource for theme support)
+- **Muted text**: `SystemControlDisabledBaseMediumLowBrush` (Avalonia built-in)
+- **Accent**: `AccentButtonBackground`, `AccentButtonForeground` (Avalonia built-in)
+- **Surfaces**: `SurfaceElevatedBrush`, `SurfaceOverlayBrush`, `CardBackgroundBrush`
+- **Semantic**: `SuccessBrush`, `ErrorBrush`, `WarningBrush`
+
+## Typography Classes
+
+Apply via `Classes="class-name"` on TextBlock:
+
+- **Headings**: `h1` (22px Bold), `h2` (15px SemiBold), `h3` (13px SemiBold)
+- **Small text**: `m1` (12px), `m2` (10px)
+- **Special**: `credential` (18px Bold monospace)
+- **Color modifier**: `muted`
+
+## Button Styles
+
+All buttons automatically get `CornerRadius="6"` from the base style.
+
+```xml
+
diff --git a/src/RemoteViewer.Client/Controls/Dialogs/FileTransferConfirmationDialogViewModel.cs b/src/RemoteViewer.Client/Controls/Dialogs/FileTransferConfirmationDialogViewModel.cs
index ffefde4..12b8031 100644
--- a/src/RemoteViewer.Client/Controls/Dialogs/FileTransferConfirmationDialogViewModel.cs
+++ b/src/RemoteViewer.Client/Controls/Dialogs/FileTransferConfirmationDialogViewModel.cs
@@ -1,8 +1,8 @@
-using RemoteViewer.Client.Views;
+using CommunityToolkit.Mvvm.ComponentModel;
namespace RemoteViewer.Client.Controls.Dialogs;
-public class FileTransferConfirmationDialogViewModel : ViewModelBase
+public class FileTransferConfirmationDialogViewModel : ObservableObject
{
public string SenderDisplayName { get; }
public string FileName { get; }
diff --git a/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialog.axaml b/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialog.axaml
index a12f9d6..b99c8be 100644
--- a/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialog.axaml
+++ b/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialog.axaml
@@ -1,8 +1,10 @@
-
+ MaxWidth="400">
-
-
-
-
-
-
+
-
-
-
+
+
-
-
+
+
+
+
+
+ Classes="h3"
+ TextTrimming="CharacterEllipsis"
+ ToolTip.Tip="{Binding FileName}"/>
+ Classes="m1"/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
-
-
+
+
+
diff --git a/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialogViewModel.cs b/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialogViewModel.cs
index 83d7a40..8d321cd 100644
--- a/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialogViewModel.cs
+++ b/src/RemoteViewer.Client/Controls/Dialogs/ViewerSelectionDialogViewModel.cs
@@ -1,9 +1,9 @@
-using RemoteViewer.Client.Views;
+using CommunityToolkit.Mvvm.ComponentModel;
using RemoteViewer.Client.Views.Presenter;
namespace RemoteViewer.Client.Controls.Dialogs;
-public class ViewerSelectionDialogViewModel : ViewModelBase
+public class ViewerSelectionDialogViewModel : ObservableObject
{
public IReadOnlyList Viewers { get; }
public string FileName { get; }
diff --git a/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml b/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml
index 3b665d9..81ef8dc 100644
--- a/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml
+++ b/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml
@@ -14,7 +14,7 @@
diff --git a/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml.cs b/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml.cs
index 2ed3422..02c0159 100644
--- a/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml.cs
+++ b/src/RemoteViewer.Client/Controls/DisplayMiniMap.axaml.cs
@@ -1,8 +1,7 @@
-using System.Collections.Immutable;
+using System.Collections.Immutable;
using Avalonia;
using Avalonia.Controls;
using Material.Icons;
-using Material.Icons.Avalonia;
using RemoteViewer.Shared;
namespace RemoteViewer.Client.Controls;
@@ -113,11 +112,10 @@ private void UpdateMap()
private static StackPanel CreateButtonContent(DisplayInfo display)
{
- var icon = new MaterialIcon
+ var icon = new Icon
{
Kind = display.IsPrimary ? MaterialIconKind.MonitorStar : MaterialIconKind.Monitor,
- Width = 20,
- Height = 20
+ Size = IconSize.SM
};
if (display.IsPrimary)
@@ -133,7 +131,7 @@ private static StackPanel CreateButtonContent(DisplayInfo display)
{
icon,
new TextBlock { Text = display.FriendlyName },
- new TextBlock { Text = $"{display.Width} {display.Height}", Classes = { "dimensions" } }
+ new TextBlock { Text = $"{display.Width} × {display.Height}", Classes = { "dimensions" } }
}
};
}
diff --git a/src/RemoteViewer.Client/Controls/DropOverlay.axaml b/src/RemoteViewer.Client/Controls/DropOverlay.axaml
index 571fa0f..af6e67d 100644
--- a/src/RemoteViewer.Client/Controls/DropOverlay.axaml
+++ b/src/RemoteViewer.Client/Controls/DropOverlay.axaml
@@ -1,46 +1,44 @@
+ Padding="{theme:Spacing LG}">
-
-
+
+
-
+
-
+
-
+
-
+
diff --git a/src/RemoteViewer.Client/Controls/Icon.axaml b/src/RemoteViewer.Client/Controls/Icon.axaml
new file mode 100644
index 0000000..590931b
--- /dev/null
+++ b/src/RemoteViewer.Client/Controls/Icon.axaml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RemoteViewer.Client/Controls/Icon.axaml.cs b/src/RemoteViewer.Client/Controls/Icon.axaml.cs
new file mode 100644
index 0000000..b574394
--- /dev/null
+++ b/src/RemoteViewer.Client/Controls/Icon.axaml.cs
@@ -0,0 +1,61 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Material.Icons;
+
+namespace RemoteViewer.Client.Controls;
+
+public enum IconSize
+{
+ XXS = 12,
+ XS = 16,
+ SM = 20,
+ MD = 24,
+ LG = 32,
+ XL = 40,
+ XXL = 48
+}
+
+public partial class Icon : UserControl
+{
+ public static readonly StyledProperty KindProperty =
+ AvaloniaProperty.Register(nameof(Kind), MaterialIconKind.Star);
+
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size), IconSize.MD);
+
+ public static readonly StyledProperty ShowAsBadgeProperty =
+ AvaloniaProperty.Register(nameof(ShowAsBadge), false);
+
+ public static readonly StyledProperty BadgeBackgroundProperty =
+ AvaloniaProperty.Register(nameof(BadgeBackground));
+
+ public MaterialIconKind Kind
+ {
+ get => this.GetValue(KindProperty);
+ set => this.SetValue(KindProperty, value);
+ }
+
+ public IconSize Size
+ {
+ get => this.GetValue(SizeProperty);
+ set => this.SetValue(SizeProperty, value);
+ }
+
+ public bool ShowAsBadge
+ {
+ get => this.GetValue(ShowAsBadgeProperty);
+ set => this.SetValue(ShowAsBadgeProperty, value);
+ }
+
+ public IBrush? BadgeBackground
+ {
+ get => this.GetValue(BadgeBackgroundProperty);
+ set => this.SetValue(BadgeBackgroundProperty, value);
+ }
+
+ public Icon()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/src/RemoteViewer.Client/Controls/Toasts/ToastsView.axaml b/src/RemoteViewer.Client/Controls/Toasts/ToastsView.axaml
index 95d3a60..eb29954 100644
--- a/src/RemoteViewer.Client/Controls/Toasts/ToastsView.axaml
+++ b/src/RemoteViewer.Client/Controls/Toasts/ToastsView.axaml
@@ -1,17 +1,18 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mik="using:Material.Icons"
+ xmlns:controls="using:RemoteViewer.Client.Controls"
+ xmlns:toasts="using:RemoteViewer.Client.Controls.Toasts"
+ xmlns:ft="using:RemoteViewer.Client.Services.FileTransfer"
+ xmlns:conv="using:RemoteViewer.Client.Converters"
+ xmlns:theme="using:RemoteViewer.Client.Themes"
+ x:Class="RemoteViewer.Client.Controls.Toasts.ToastsView"
+ x:DataType="toasts:ToastsViewModel">
-
+
@@ -47,14 +48,13 @@
Width="32"
Height="32"
CornerRadius="8"
- Margin="14,14,12,14"
+ Margin="{theme:Spacing Left=MD, Right=MD, Y=MD}"
VerticalAlignment="Center"
Background="{DynamicResource ToastIconBadgeBrush}">
-
-
+
+
@@ -64,8 +64,8 @@
-
-
+
+
@@ -75,8 +75,8 @@
-
-
+
+
@@ -85,19 +85,18 @@
VerticalAlignment="Center"
TextWrapping="Wrap"
Foreground="{DynamicResource ToastForegroundBrush}"
- FontSize="13"
FontWeight="SemiBold"
- Margin="0,14,8,14"/>
+ Margin="{theme:Spacing Y=MD, Right=SM}"/>
@@ -105,15 +104,14 @@
-
-
-
+
+
+
@@ -151,14 +149,13 @@
Width="32"
Height="32"
CornerRadius="8"
- Margin="14,14,12,14"
+ Margin="{theme:Spacing Left=MD, Right=MD, Y=MD}"
VerticalAlignment="Top"
Background="{DynamicResource ToastIconBadgeBrush}">
-
-
+
+
@@ -168,8 +165,8 @@
-
-
+
+
@@ -179,21 +176,20 @@
-
-
+
+
-
+
@@ -213,17 +209,16 @@
-
-
+
+
+ Foreground="{DynamicResource AccentButtonForeground}"/>
@@ -233,10 +228,10 @@
Command="{Binding DismissCommand}"
Background="Transparent"
BorderThickness="0"
- Width="28"
- Height="28"
+ Width="32"
+ Height="32"
Padding="0"
- Margin="0,10,10,0"
+ Margin="{theme:Spacing Top=SM, Right=SM}"
VerticalAlignment="Top"
Cursor="Hand"
CornerRadius="6">
@@ -244,15 +239,14 @@
-
-
-
+
+
+
@@ -275,7 +269,7 @@
Background="{StaticResource ToastTransferAccentBrush}"/>
-
+
@@ -284,29 +278,27 @@
Height="32"
CornerRadius="8"
Background="{DynamicResource ToastIconBadgeBrush}"
- Margin="0,0,10,0"
+ Margin="{theme:Spacing Right=SM}"
VerticalAlignment="Center">
-
+
+ FontWeight="SemiBold" />
-
+
@@ -333,8 +324,8 @@
Text="{Binding Transfer.FileName}"
TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource ToastSecondaryTextBrush}"
- FontSize="12"
- Margin="0,8,0,10"
+ Classes="m1"
+ Margin="{theme:Spacing Top=SM, Bottom=SM}"
ToolTip.Tip="{Binding Transfer.FileName}"/>
@@ -361,9 +352,9 @@
+ Margin="{theme:Spacing Top=SM}">
@@ -378,7 +369,7 @@
+ Margin="{theme:Spacing Top=SM}">
@@ -391,12 +382,12 @@
+ Classes="m1"/>
diff --git a/src/RemoteViewer.Client/Themes/ButtonStyles.axaml b/src/RemoteViewer.Client/Themes/ButtonStyles.axaml
new file mode 100644
index 0000000..301eb0c
--- /dev/null
+++ b/src/RemoteViewer.Client/Themes/ButtonStyles.axaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RemoteViewer.Client/Themes/Colors.axaml b/src/RemoteViewer.Client/Themes/Colors.axaml
new file mode 100644
index 0000000..9855195
--- /dev/null
+++ b/src/RemoteViewer.Client/Themes/Colors.axaml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RemoteViewer.Client/Themes/SpacingExtension.cs b/src/RemoteViewer.Client/Themes/SpacingExtension.cs
new file mode 100644
index 0000000..0c7a935
--- /dev/null
+++ b/src/RemoteViewer.Client/Themes/SpacingExtension.cs
@@ -0,0 +1,63 @@
+using System.Reflection;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace RemoteViewer.Client.Themes;
+
+public enum SpacingSize
+{
+ None = 0,
+ XXS = 2,
+ XS = 4,
+ SM = 8,
+ MD = 12,
+ LG = 16,
+ XL = 24,
+ XXL = 32
+}
+
+public class SpacingExtension : MarkupExtension
+{
+ public SpacingSize All { get; set; } = SpacingSize.None;
+ public SpacingSize? X { get; set; }
+ public SpacingSize? Y { get; set; }
+ public SpacingSize? Top { get; set; }
+ public SpacingSize? Bottom { get; set; }
+ public SpacingSize? Left { get; set; }
+ public SpacingSize? Right { get; set; }
+
+ public SpacingExtension() { }
+
+ public SpacingExtension(SpacingSize all) => this.All = all;
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
+ var targetProperty = target?.TargetProperty;
+
+ Type? targetType = null;
+ if (targetProperty is PropertyInfo pi)
+ targetType = pi.PropertyType;
+ else if (targetProperty is AvaloniaProperty ap)
+ targetType = ap.PropertyType;
+
+ if (targetType == typeof(double))
+ return (double)this.All;
+
+ if (targetType == typeof(GridLength))
+ return new GridLength((double)this.All, GridUnitType.Pixel);
+
+ if (targetType == typeof(Thickness))
+ {
+ var top = (double)(this.Top ?? this.Y ?? this.All);
+ var bottom = (double)(this.Bottom ?? this.Y ?? this.All);
+ var left = (double)(this.Left ?? this.X ?? this.All);
+ var right = (double)(this.Right ?? this.X ?? this.All);
+
+ return new Thickness(left, top, right, bottom);
+ }
+
+ throw new InvalidOperationException($"Cannot convert SpacingExtension to target type '{targetType}'");
+ }
+}
diff --git a/src/RemoteViewer.Client/Themes/Typography.axaml b/src/RemoteViewer.Client/Themes/Typography.axaml
new file mode 100644
index 0000000..7d7bfc6
--- /dev/null
+++ b/src/RemoteViewer.Client/Themes/Typography.axaml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RemoteViewer.Client/ViewLocator.cs b/src/RemoteViewer.Client/ViewLocator.cs
index 78c10f7..a399395 100644
--- a/src/RemoteViewer.Client/ViewLocator.cs
+++ b/src/RemoteViewer.Client/ViewLocator.cs
@@ -1,7 +1,7 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
-using RemoteViewer.Client.Views;
+using CommunityToolkit.Mvvm.ComponentModel;
namespace RemoteViewer.Client;
@@ -26,6 +26,6 @@ public class ViewLocator : IDataTemplate
public bool Match(object? data)
{
- return data is ViewModelBase;
+ return data is ObservableObject;
}
}
diff --git a/src/RemoteViewer.Client/Views/About/AboutView.axaml b/src/RemoteViewer.Client/Views/About/AboutView.axaml
index 0182f76..1357374 100644
--- a/src/RemoteViewer.Client/Views/About/AboutView.axaml
+++ b/src/RemoteViewer.Client/Views/About/AboutView.axaml
@@ -1,103 +1,137 @@
-
-
-
-
+
+
-
+
+
+
+
-
+
+
-
-
+
-
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RemoteViewer.Client/Views/About/AboutViewModel.cs b/src/RemoteViewer.Client/Views/About/AboutViewModel.cs
index 8e8a7f6..0778ae6 100644
--- a/src/RemoteViewer.Client/Views/About/AboutViewModel.cs
+++ b/src/RemoteViewer.Client/Views/About/AboutViewModel.cs
@@ -1,8 +1,9 @@
-using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
namespace RemoteViewer.Client.Views.About;
-public partial class AboutViewModel : ViewModelBase
+public partial class AboutViewModel : ObservableObject
{
public string ApplicationName => "Remote Viewer";
diff --git a/src/RemoteViewer.Client/Views/Chat/ChatView.axaml b/src/RemoteViewer.Client/Views/Chat/ChatView.axaml
index 405ad87..5d6e7fa 100644
--- a/src/RemoteViewer.Client/Views/Chat/ChatView.axaml
+++ b/src/RemoteViewer.Client/Views/Chat/ChatView.axaml
@@ -1,8 +1,10 @@
-
+
-
+ TextWrapping="Wrap"/>
+ Margin="{theme:Spacing Top=XS}">
-
-
+ Padding="{theme:Spacing X=SM, Y=XS}"
+ Margin="{theme:Spacing Top=XXS}"
+ HorizontalAlignment="Left">
+
+
-
-
+
@@ -76,9 +76,8 @@
+ Classes="m2"
+ TextAlignment="Right"/>
@@ -89,17 +88,17 @@
-
+
-
+
-
+ Classes="icon-button"
+ Width="32"
+ Height="32">
+
diff --git a/src/RemoteViewer.Client/Views/Main/ConnectionStatus.cs b/src/RemoteViewer.Client/Views/Main/ConnectionStatus.cs
new file mode 100644
index 0000000..27e8219
--- /dev/null
+++ b/src/RemoteViewer.Client/Views/Main/ConnectionStatus.cs
@@ -0,0 +1,8 @@
+namespace RemoteViewer.Client.Views.Main;
+
+public enum ConnectionStatus
+{
+ Connecting,
+ Connected,
+ VersionMismatch
+}
diff --git a/src/RemoteViewer.Client/Views/Main/MainView.axaml b/src/RemoteViewer.Client/Views/Main/MainView.axaml
index af6ed40..df6588b 100644
--- a/src/RemoteViewer.Client/Views/Main/MainView.axaml
+++ b/src/RemoteViewer.Client/Views/Main/MainView.axaml
@@ -1,10 +1,12 @@
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ Classes="icon-button"
+ VerticalAlignment="Center"
+ Margin="{theme:Spacing Left=SM}"
+ Command="{Binding CopyCredentialsCommand}"
+ ToolTip.Tip="Copy ID and password">
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
diff --git a/src/RemoteViewer.Client/Views/Main/MainViewModel.cs b/src/RemoteViewer.Client/Views/Main/MainViewModel.cs
index f4a4f86..3c5208d 100644
--- a/src/RemoteViewer.Client/Views/Main/MainViewModel.cs
+++ b/src/RemoteViewer.Client/Views/Main/MainViewModel.cs
@@ -1,4 +1,4 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using RemoteViewer.Client.Controls.Toasts;
@@ -10,7 +10,7 @@
namespace RemoteViewer.Client.Views.Main;
-public partial class MainViewModel : ViewModelBase
+public partial class MainViewModel : ObservableObject
{
private readonly ConnectionHubClient _hubClient;
private readonly IDispatcher _dispatcher;
@@ -34,17 +34,27 @@ public partial class MainViewModel : ViewModelBase
[ObservableProperty]
private string? _targetPassword;
- [ObservableProperty]
- private bool _isConnected;
-
- [ObservableProperty]
- private string _statusText = "Connecting...";
+ public ConnectionStatus Status => this._hubClient switch
+ {
+ { HasVersionMismatch: true } => ConnectionStatus.VersionMismatch,
+ { IsConnected: true } => ConnectionStatus.Connected,
+ _ => ConnectionStatus.Connecting
+ };
- [ObservableProperty]
- private bool _hasVersionMismatch;
+ public string? VersionTooltipText
+ {
+ get
+ {
+ if (this._hubClient.HasVersionMismatch is false)
+ return null;
- [ObservableProperty]
- private string _versionMismatchText = string.Empty;
+ return $"""
+ Version Mismatch
+ Server: v{this._hubClient.ServerVersion}
+ Client: v{ThisAssembly.AssemblyInformationalVersion}
+ """;
+ }
+ }
public event EventHandler? RequestHideMainView;
public event EventHandler? RequestShowMainView;
@@ -62,21 +72,10 @@ public MainViewModel(ConnectionHubClient hubClient, IDispatcher dispatcher, IVie
{
this._dispatcher.Post(() =>
{
- this.IsConnected = this._hubClient.IsConnected;
- this.HasVersionMismatch = this._hubClient.HasVersionMismatch;
-
- if (this._hubClient.HasVersionMismatch)
- {
- this.VersionMismatchText = $"""
- Version mismatch!
- Server v{this._hubClient.ServerVersion}
- Client v{ThisAssembly.AssemblyInformationalVersion}
- """;
- }
-
- this.StatusText = this._hubClient.IsConnected ? "Connected" : "Connecting...";
+ this.OnPropertyChanged(nameof(this.Status));
+ this.OnPropertyChanged(nameof(this.VersionTooltipText));
- this._logger.HubConnectionStatusChanged(this._hubClient.IsConnected, this.StatusText);
+ this._logger.HubConnectionStatusChanged(this._hubClient.IsConnected, this.Status.ToString());
this.YourUsername = this._hubClient.IsConnected ? this._hubClient.Username : "...";
this.YourPassword = this._hubClient.IsConnected ? this._hubClient.Password : "...";
@@ -93,58 +92,22 @@ Client v{ThisAssembly.AssemblyInformationalVersion}
});
};
- this.IsConnected = this._hubClient.IsConnected;
-
- // Handle viewer connections - open viewer window when connected as viewer
- this._hubClient.ConnectionStarted += this.OnConnectionStarted;
- }
-
- private void OnConnectionStarted(object? sender, ConnectionStartedEventArgs e)
- {
- this._dispatcher.Post(() =>
+ this._hubClient.ConnectionStarted += (_, e) =>
{
- if (e.Connection.IsPresenter)
- {
- this._logger.ConnectionSuccessful("Presenter");
- this.OpenPresenterWindow(e.Connection);
- }
- else
+ this._dispatcher.Post(() =>
{
- this._logger.ConnectionSuccessful("Viewer");
- this.OpenViewerWindow(e.Connection);
- }
- });
- }
-
- private void OpenPresenterWindow(Connection connection)
- {
- this.RequestHideMainView?.Invoke(this, EventArgs.Empty);
-
- var viewModel = this._viewModelFactory.CreatePresenterViewModel(connection);
- this._sessionWindowHandle = this._dialogService.ShowPresenterWindow(viewModel);
- this._sessionWindowHandle.Closed += this.OnSessionWindowClosed;
- }
-
- private void OpenViewerWindow(Connection connection)
- {
- this.RequestHideMainView?.Invoke(this, EventArgs.Empty);
-
- var viewModel = this._viewModelFactory.CreateViewerViewModel(connection);
- this._sessionWindowHandle = this._dialogService.ShowViewerWindow(viewModel);
- this._sessionWindowHandle.Closed += this.OnSessionWindowClosed;
- }
-
- private void OnSessionWindowClosed(object? sender, EventArgs e)
- {
- this._logger.SessionWindowClosed();
-
- if (this._sessionWindowHandle is not null)
- {
- this._sessionWindowHandle.Closed -= this.OnSessionWindowClosed;
- this._sessionWindowHandle = null;
- }
-
- this.RequestShowMainView?.Invoke(this, EventArgs.Empty);
+ if (e.Connection.IsPresenter)
+ {
+ this._logger.ConnectionSuccessful("Presenter");
+ this.OpenPresenterWindow(e.Connection);
+ }
+ else
+ {
+ this._logger.ConnectionSuccessful("Viewer");
+ this.OpenViewerWindow(e.Connection);
+ }
+ });
+ };
}
[RelayCommand]
@@ -206,4 +169,31 @@ private async Task ShowAboutAsync()
{
await this._dialogService.ShowAboutDialogAsync();
}
+
+ private void OpenPresenterWindow(Connection connection)
+ {
+ this.RequestHideMainView?.Invoke(this, EventArgs.Empty);
+
+ var viewModel = this._viewModelFactory.CreatePresenterViewModel(connection);
+ this._sessionWindowHandle = this._dialogService.ShowPresenterWindow(viewModel);
+ this._sessionWindowHandle.Closed += this.OnSessionWindowClosed;
+ }
+
+ private void OpenViewerWindow(Connection connection)
+ {
+ this.RequestHideMainView?.Invoke(this, EventArgs.Empty);
+
+ var viewModel = this._viewModelFactory.CreateViewerViewModel(connection);
+ this._sessionWindowHandle = this._dialogService.ShowViewerWindow(viewModel);
+ this._sessionWindowHandle.Closed += this.OnSessionWindowClosed;
+ }
+
+ private void OnSessionWindowClosed(object? sender, EventArgs e)
+ {
+ this._logger.SessionWindowClosed();
+ this._sessionWindowHandle?.Closed -= this.OnSessionWindowClosed;
+ this._sessionWindowHandle = null;
+
+ this.RequestShowMainView?.Invoke(this, EventArgs.Empty);
+ }
}
diff --git a/src/RemoteViewer.Client/Views/Presenter/PresenterView.axaml b/src/RemoteViewer.Client/Views/Presenter/PresenterView.axaml
index b0c692b..b62061c 100644
--- a/src/RemoteViewer.Client/Views/Presenter/PresenterView.axaml
+++ b/src/RemoteViewer.Client/Views/Presenter/PresenterView.axaml
@@ -5,8 +5,8 @@
xmlns:toastControls="using:RemoteViewer.Client.Controls.Toasts"
xmlns:controls="using:RemoteViewer.Client.Controls"
xmlns:chatControls="using:RemoteViewer.Client.Views.Chat"
- xmlns:mi="using:Material.Icons.Avalonia"
xmlns:mik="using:Material.Icons"
+ xmlns:theme="using:RemoteViewer.Client.Themes"
x:Class="RemoteViewer.Client.Views.Presenter.PresenterView"
x:DataType="vm:PresenterViewModel"
@@ -15,7 +15,7 @@
Width="300"
CanResize="False"
SizeToContent="Height"
- WindowStartupLocation="CenterScreen"
+ WindowStartupLocation="CenterOwner"
DragDrop.AllowDrop="True"
DataContextChanged="Window_DataContextChanged"
@@ -24,31 +24,30 @@
-
+ Background="{DynamicResource SurfaceElevatedBrush}"
+ Padding="{theme:Spacing X=SM, Y=SM}">
+
-
+
-
+
+ Classes="icon-button">
-
+
+ Classes="h3"
+ Margin="{theme:Spacing Bottom=XS}"/>
-
+
-
+
+ Classes="m1 muted"
+ Margin="{theme:Spacing Top=SM}"/>
+ Classes="m1 muted"/>
-
+
-
+ Classes="icon-button">
+
-
+
-
+
+ Classes="h3"
+ Margin="{theme:Spacing Bottom=MD}"/>
-
+
+ Classes="m1 muted"/>
+ Classes="credential"
+ Margin="{theme:Spacing Y=SM}"/>
-
+ ToolTip.Tip="Copy ID and password">
+
-
+
-
+ Classes="m1 muted"/>
+
-
+ Classes="icon-button"
+ VerticalAlignment="Center"
+ Width="32" Height="32">
+
-
+
-
+ Classes="h3"
+ Margin="{theme:Spacing Bottom=MD}"/>
-
-
+
+
@@ -239,14 +221,9 @@
-
+ Classes="icon-button">
+
@@ -254,15 +231,15 @@
-
-
+
+
+ VerticalAlignment="Bottom"
+ Margin="{theme:Spacing Right=SM, Bottom=SM}"/>
@@ -60,31 +61,28 @@
-
+
-
+ Classes="icon-button">
+
+ Classes="icon-button">
-
+
+ Margin="{theme:Spacing X=SM, Top=XS, Bottom=SM}"/>
-
+
@@ -108,64 +104,57 @@
-
+
+ Classes="icon-button">
-
+
-
+ Classes="icon-button">
+
+ Margin="{theme:Spacing Y=XS}"
+ Background="{DynamicResource SystemControlForegroundBaseLowBrush}"/>
+ Classes="icon-button">
-
+
+ Margin="{theme:Spacing X=SM, Top=XS, Bottom=SM}"/>
-
-
-
+
+
@@ -175,19 +164,18 @@
-
+
-
+
+ Margin="{theme:Spacing Y=XS}"
+ Background="{DynamicResource SystemControlForegroundBaseLowBrush}"/>
-
+ Classes="icon-button">
+
@@ -244,10 +231,10 @@
+ Margin="{theme:Spacing X=LG}">
-
+
@@ -261,11 +248,13 @@
Opacity="0.6"
ToolTip.Tip="{Binding FriendlyName, StringFormat='Navigate to {0} (Ctrl+Left)'}">
-
-
-
+
+
+
@@ -275,10 +264,10 @@
+ Margin="{theme:Spacing X=LG}">
-
+
@@ -292,11 +281,13 @@
Opacity="0.6"
ToolTip.Tip="{Binding FriendlyName, StringFormat='Navigate to {0} (Ctrl+Right)'}">
-
-
-
+
+
+
@@ -306,10 +297,10 @@
+ Margin="{theme:Spacing Y=LG}">
-
+
@@ -323,11 +314,13 @@
Opacity="0.6"
ToolTip.Tip="{Binding FriendlyName, StringFormat='Navigate to {0} (Ctrl+Up)'}">
-
-
-
+
+
+
@@ -337,10 +330,10 @@
+ Margin="{theme:Spacing Y=LG}">
-
+
@@ -354,11 +347,13 @@
Opacity="0.6"
ToolTip.Tip="{Binding FriendlyName, StringFormat='Navigate to {0} (Ctrl+Down)'}">
-
-
-
+
+
+
@@ -369,9 +364,9 @@
+ VerticalAlignment="Bottom"
+ Margin="{theme:Spacing Right=SM, Bottom=SM}"/>