Commit: 3feb1de
Parent: 984ffad

Styled DateTime panel

Mårten Åsberg committed on 2026-06-05 at 14:19
.editorconfig +2 -2
diff --git a/.editorconfig b/.editorconfig
index 01a63a4..88237d3 100644
@@ -17,7 +17,7 @@ max_line_length = 120
dotnet_diagnostic.IDE0005.severity = warning
# "HTML"/CSS/JS
[*.{html,razor,css,js,ts,vue}]
# AXAML
[*.{axaml}]
indent_size = 2
max_line_length = 120
Directory.Build.props +1 -1
diff --git a/Directory.Build.props b/Directory.Build.props
index 77bcdd4..9cfda96 100644
@@ -5,7 +5,7 @@
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors>IDE0005</WarningsAsErrors>
<WarningsAsErrors>IDE0005;MC8000</WarningsAsErrors>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
Directory.Packages.props +5 -2
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9a468b9..366d36b 100644
@@ -15,6 +15,9 @@
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.4" />
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.2" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="11.0.0-preview.4.26230.115" />
<PackageVersion
Include="Microsoft.Extensions.DependencyInjection"
Version="11.0.0-preview.4.26230.115"
/>
</ItemGroup>
</Project>
\ No newline at end of file
</Project>
src/App/App.axaml +8 -1
diff --git a/src/App/App.axaml b/src/App/App.axaml
index 78f2f97..2bc83b1 100644
@@ -2,8 +2,15 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MMirror.App.App"
RequestedThemeVariant="Dark">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="/Styles/MainResources.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="/Styles/MainStyles.axaml" />
</Application.Styles>
</Application>
src/App/App.axaml.cs +1 -4
diff --git a/src/App/App.axaml.cs b/src/App/App.axaml.cs
index c4018e7..453fe5a 100644
@@ -16,10 +16,7 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
var collection = new ServiceCollection();
collection.AddViewModels();
var services = collection.BuildServiceProvider();
var services = new ServiceCollection().AddViews().AddViewModels().BuildServiceProvider();
DataTemplates.Add(new ViewLocator(services));
var vm = services.GetRequiredService<MainViewModel>();
src/App/App.csproj +1 -1
diff --git a/src/App/App.csproj b/src/App/App.csproj
index 42285b2..985ef05 100644
@@ -2,7 +2,7 @@
<PropertyGroup>
<RootNamespace>MMirror.App</RootNamespace>
<AssemblyName>MMirror.App</AssemblyName>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
src/App/Styles/MainResources.axaml +5 -0
diff --git a/src/App/Styles/MainResources.axaml b/src/App/Styles/MainResources.axaml
new file mode 100644
index 0000000..db89bfc
@@ -0,0 +1,5 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:TimeSpan x:Key="FadeInDuration">0:0:1</x:TimeSpan>
<Easing x:Key="FadeInEasing">CubicEaseOut</Easing>
</ResourceDictionary>
src/App/Styles/MainStyles.axaml +21 -0
diff --git a/src/App/Styles/MainStyles.axaml b/src/App/Styles/MainStyles.axaml
new file mode 100644
index 0000000..7e94c1b
@@ -0,0 +1,21 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="TextBlock.h1">
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Light"/>
</Style>
<Style Selector=":is(Control).fade">
<Setter Property="Opacity" Value="0.5" />
<Setter Property="RenderTransform" Value="translateY(10px)" />
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="{StaticResource FadeInDuration}" Easing="{StaticResource FadeInEasing}" />
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FadeInDuration}" Easing="{StaticResource FadeInEasing}" />
</Transitions>
</Setter>
</Style>
<Style Selector=".active :is(Control).fade">
<Setter Property="Opacity" Value="1" />
<Setter Property="RenderTransform" Value="translateY(0px)" />
</Style>
</Styles>
src/App/ViewLocator.cs +14 -1
diff --git a/src/App/ViewLocator.cs b/src/App/ViewLocator.cs
index b6590b7..0dcdd52 100644
@@ -3,7 +3,9 @@ using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Microsoft.Extensions.DependencyInjection;
using MMirror.App.ViewModels;
using MMirror.App.ViewModels.Panels;
using MMirror.App.Views;
using MMirror.App.Views.Panels;
namespace MMirror.App;
@@ -15,9 +17,20 @@ public class ViewLocator(IServiceProvider services) : IDataTemplate
public Control Build(object? data) =>
data switch
{
MainViewModel => services.GetRequiredService<MainView>(),
MainViewModel => services.GetControlOrDefault<MainView>(),
DateTimePanelViewModel => services.GetControlOrDefault<DateTimePanel>(),
_ => new TextBlock { Text = $"No view for {data?.GetType().Name}" },
};
public bool Match(object? data) => data is ViewModelBase;
}
file static class ViewLocatorServiceProviderExtensions
{
extension(IServiceProvider services)
{
public Control GetControlOrDefault<T>()
where T : Control =>
services.GetService<T>() ?? (Control)new TextBlock { Text = $"No view for type {typeof(T).Name}" };
}
}
src/App/ViewModels/MainViewModel.cs +3 -32
diff --git a/src/App/ViewModels/MainViewModel.cs b/src/App/ViewModels/MainViewModel.cs
index a8bd765..2ef0763 100644
@@ -1,42 +1,13 @@
using System;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using MMirror.App.ViewModels.Panels;
namespace MMirror.App.ViewModels;
public partial class MainViewModel : ViewModelBase
public partial class MainViewModel(DateTimePanelViewModel dateTimePanelViewModel) : ViewModelBase
{
private readonly TimeProvider timeProvider;
private readonly DispatcherTimer dispatcherTimer;
[ObservableProperty]
public partial DateTimeOffset CurrentTime { get; private set; }
public MainViewModel(TimeProvider timeProvider)
{
this.timeProvider = timeProvider;
CurrentTime = timeProvider.GetLocalNow();
dispatcherTimer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
dispatcherTimer.Tick += OnTick;
}
public void SubscribeToUpdates()
{
dispatcherTimer.Start();
}
private void OnTick(object? sender, EventArgs e)
{
CurrentTime = timeProvider.GetLocalNow();
}
public void UnsubscribeFromUpdates()
{
dispatcherTimer.Stop();
}
public ViewModelBase UpperRight { get; } = dateTimePanelViewModel;
}
internal static class MainViewModelServiceCollectionExtensions
src/App/ViewModels/Panels/DateTimePanelViewModel.cs +53 -0
diff --git a/src/App/ViewModels/Panels/DateTimePanelViewModel.cs b/src/App/ViewModels/Panels/DateTimePanelViewModel.cs
new file mode 100644
index 0000000..f8badd3
@@ -0,0 +1,53 @@
using System;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace MMirror.App.ViewModels.Panels;
public partial class DateTimePanelViewModel : ViewModelBase
{
private readonly TimeProvider timeProvider;
private readonly DispatcherTimer dispatcherTimer;
[ObservableProperty]
public partial DateTimeOffset CurrentTime { get; private set; }
public DateTimePanelViewModel(TimeProvider timeProvider)
{
this.timeProvider = timeProvider;
CurrentTime = timeProvider.GetLocalNow();
dispatcherTimer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
dispatcherTimer.Tick += OnTick;
}
public void SubscribeToUpdates()
{
dispatcherTimer.Start();
}
private void OnTick(object? sender, EventArgs e)
{
CurrentTime = timeProvider.GetLocalNow();
}
public void UnsubscribeFromUpdates()
{
dispatcherTimer.Stop();
}
}
internal static class DateTimePanelViewModelServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddDateTimePanelViewModel()
{
services.TryAddSingleton(TimeProvider.System);
services.AddTransient<DateTimePanelViewModel>();
return services;
}
}
}
src/App/ViewModels/Panels/ServiceCollectionExtensions.cs +11 -0
diff --git a/src/App/ViewModels/Panels/ServiceCollectionExtensions.cs b/src/App/ViewModels/Panels/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..1f65e56
@@ -0,0 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
namespace MMirror.App.ViewModels.Panels;
internal static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddPanelViewModels() => services.AddDateTimePanelViewModel();
}
}
src/App/ViewModels/ServiceCollectionExtensions.cs +2 -1
diff --git a/src/App/ViewModels/ServiceCollectionExtensions.cs b/src/App/ViewModels/ServiceCollectionExtensions.cs
index 388f511..2d039c4 100644
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using MMirror.App.ViewModels.Panels;
namespace MMirror.App.ViewModels;
@@ -6,6 +7,6 @@ internal static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddViewModels() => services.AddMainViewModel();
public IServiceCollection AddViewModels() => services.AddMainViewModel().AddPanelViewModels();
}
}
src/App/Views/MainView.axaml +3 -5
diff --git a/src/App/Views/MainView.axaml b/src/App/Views/MainView.axaml
index 881bce4..47851cb 100644
@@ -10,9 +10,7 @@
<vm:MainViewModel />
</Design.DataContext>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8">
<TextBlock Text="{Binding CurrentTime, StringFormat=HH:mm:ss}"/>
<TextBlock Text="{Binding CurrentTime, StringFormat=yyyy-MM-dd}"/>
<Rectangle Width="32" Height="32" Fill="#bada55" />
</StackPanel>
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,*,*">
<ContentControl Grid.Row="0" Grid.Column="2" Content="{Binding UpperRight}" />
</Grid>
</UserControl>
src/App/Views/MainView.axaml.cs +0 -22
diff --git a/src/App/Views/MainView.axaml.cs b/src/App/Views/MainView.axaml.cs
index ac48bc1..44f20ae 100644
@@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using MMirror.App.ViewModels;
namespace MMirror.App.Views;
@@ -10,24 +8,4 @@ public partial class MainView : UserControl
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (DataContext is MainViewModel vm)
{
vm.SubscribeToUpdates();
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
if (DataContext is MainViewModel vm)
{
vm.UnsubscribeFromUpdates();
}
base.OnDetachedFromVisualTree(e);
}
}
src/App/Views/MainWindow.axaml +0 -1
diff --git a/src/App/Views/MainWindow.axaml b/src/App/Views/MainWindow.axaml
index 39f2adf..e577833 100644
@@ -1,6 +1,5 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MMirror.App.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:MMirror.App.Views"
src/App/Views/MainWindow.axaml.cs +15 -0
diff --git a/src/App/Views/MainWindow.axaml.cs b/src/App/Views/MainWindow.axaml.cs
index e964cb1..19b9147 100644
@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Input;
namespace MMirror.App.Views;
@@ -8,4 +9,18 @@ public partial class MainWindow : Window
{
InitializeComponent();
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);
Classes.Add("active");
}
protected override void OnPointerExited(PointerEventArgs e)
{
Classes.Remove("active");
base.OnPointerExited(e);
}
}
src/App/Views/Panels/DateTimePanel.axaml +50 -0
diff --git a/src/App/Views/Panels/DateTimePanel.axaml b/src/App/Views/Panels/DateTimePanel.axaml
new file mode 100644
index 0000000..0c33ab9
@@ -0,0 +1,50 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:p="using:MMirror.App.Views.Panels"
xmlns:vm="using:MMirror.App.ViewModels.Panels"
mc:Ignorable="d"
d:DesignWidth="720"
d:DesignHeight="1280"
x:Class="MMirror.App.Views.Panels.DateTimePanel"
x:DataType="vm:DateTimePanelViewModel">
<Design.DataContext>
<vm:DateTimePanelViewModel/>
</Design.DataContext>
<UserControl.Resources>
<p:TranslateXConverter x:Key="translateXConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="StackPanel#clock">
<Setter Property="RenderTransform" Value="{Binding #seconds.DesiredSize.Width, Converter={StaticResource translateXConverter}}" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FadeInDuration}" Easing="{StaticResource FadeInEasing}" />
</Transitions>
</Setter>
</Style>
<Style Selector=".active StackPanel#clock">
<Setter Property="RenderTransform" Value="translateX(0px)" />
</Style>
<Style Selector="TextBlock#seconds">
<Setter Property="Foreground" Value="Transparent" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Foreground" Duration="{StaticResource FadeInDuration}" Easing="{StaticResource FadeInEasing}" />
</Transitions>
</Setter>
</Style>
<Style Selector=".active TextBlock#seconds">
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
</Style>
</UserControl.Styles>
<StackPanel Classes="fade" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8" TextElement.FontFeatures="+tnum">
<TextBlock Classes="h1" Text="{Binding CurrentTime, StringFormat=yyyy-MM-dd}"/>
<StackPanel Name="clock" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Classes="h1" Text="{Binding CurrentTime, StringFormat=HH:mm}" />
<TextBlock Name="seconds" Classes="h1" Text="{Binding CurrentTime, StringFormat=:ss}" />
</StackPanel>
</StackPanel>
</UserControl>
src/App/Views/Panels/DateTimePanel.axaml.cs +70 -0
diff --git a/src/App/Views/Panels/DateTimePanel.axaml.cs b/src/App/Views/Panels/DateTimePanel.axaml.cs
new file mode 100644
index 0000000..53ddf19
@@ -0,0 +1,70 @@
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media.Transformation;
using MMirror.App.ViewModels.Panels;
namespace MMirror.App.Views.Panels;
public partial class DateTimePanel : UserControl
{
public DateTimePanel()
{
InitializeComponent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (DataContext is DateTimePanelViewModel vm)
{
vm.SubscribeToUpdates();
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
if (DataContext is DateTimePanelViewModel vm)
{
vm.UnsubscribeFromUpdates();
}
base.OnDetachedFromVisualTree(e);
}
}
public class TranslateXConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not double x)
{
return value;
}
var builder = TransformOperations.CreateBuilder(1);
builder.AppendTranslate(x, 0);
return builder.Build();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (
value
is not TransformOperations
{
Operations: [
TransformOperation { Type: TransformOperation.OperationType.Translate, Matrix: var matrix },
]
}
)
{
return value;
}
return matrix.M31;
}
}
src/App/Views/Panels/ServiceCollectionExtensions.cs +11 -0
diff --git a/src/App/Views/Panels/ServiceCollectionExtensions.cs b/src/App/Views/Panels/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..42355e1
@@ -0,0 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
namespace MMirror.App.Views.Panels;
internal static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddPanels() => services.AddTransient<DateTimePanel>();
}
}
src/App/Views/ServiceCollectionExtensions.cs +3 -1
diff --git a/src/App/Views/ServiceCollectionExtensions.cs b/src/App/Views/ServiceCollectionExtensions.cs
index 60282f7..b3200b6 100644
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using MMirror.App.Views.Panels;
namespace MMirror.App.Views;
@@ -6,6 +7,7 @@ internal static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddViews() => services.AddTransient<MainView>().AddTransient<MainWindow>();
public IServiceCollection AddViews() =>
services.AddTransient<MainView>().AddTransient<MainWindow>().AddPanels();
}
}