feat(PostIt): responsive two-column blogs layout, camera streamer (1.0.8-rc27)
- BlogsPage switches to a two-column layout when width >= height (post on the left, search/commands/list on the right), with a compact navigation bar; layout update deferred on the UI thread to avoid "Visual was invalidated during the render pass". - Explicit DataContext on CommandBar items so overflowed buttons keep their bindings. - Preliminary CameraStreamer service (DirectShowLib / V4L2 via LibVLC). - Tests and architecture doc updated; CHANGELOG entry 1.0.8-rc27. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
cb8cf9406d
commit
08a3541749
8 changed files with 341 additions and 23 deletions
|
|
@ -16,6 +16,7 @@
|
|||
<PackageVersion Include="Avalonia.Headless" Version="12.1.3" />
|
||||
<PackageVersion Include="Avalonia.Headless.Xunit" Version="$(AvaloniaVersionBase)" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersionBase)" />
|
||||
<PackageVersion Include="DirectShowLib" Version="1.0.0" />
|
||||
<PackageVersion Include="LibVLCSharp.Avalonia" Version="3.10.1" />
|
||||
<PackageVersion Include="Live.Avalonia" Version="1.4.2" />
|
||||
<PackageVersion Include="Mapsui" Version="5.1.0" />
|
||||
|
|
@ -37,4 +38,4 @@
|
|||
<PackageVersion Include="Xamarin.UITest" Version="4.4.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
@ -2,7 +2,9 @@ using System.Net;
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Headless;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Logging;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
|
|
@ -95,6 +97,7 @@ public class BlogReaderTests
|
|||
window.Show();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
window.UpdateLayout();
|
||||
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
||||
Assert.True(page.FindControl<Grid>("PostReader")!.IsVisible);
|
||||
Assert.False(page.FindControl<Grid>("PostEditor")!.IsVisible);
|
||||
Assert.False(page.SaveButton.IsVisible);
|
||||
|
|
@ -164,6 +167,141 @@ public class BlogReaderTests
|
|||
}
|
||||
}
|
||||
|
||||
[AvaloniaTheory]
|
||||
[InlineData(360, 800, false)]
|
||||
[InlineData(1000, 420, true)]
|
||||
[InlineData(1000, 280, true)]
|
||||
[InlineData(600, 600, true)]
|
||||
[InlineData(599, 600, false)]
|
||||
[InlineData(601, 600, true)]
|
||||
public async Task Blog_layout_uses_columns_from_the_square_window_threshold(int width, int height, bool wide)
|
||||
{
|
||||
var vm = CreateViewModel();
|
||||
await vm.InitializeAsync();
|
||||
for (var i = 0; i < 40; i++)
|
||||
vm.FilteredPosts.Add(Post($"Billet {i}"));
|
||||
vm.SelectedPost = Post(string.Join("\n\n", Enumerable.Range(0, 100).Select(i => $"Paragraphe {i}.")));
|
||||
var page = new BlogsPage { DataContext = vm };
|
||||
var main = new MainView();
|
||||
var nav = main.FindControl<NavigationPage>("NavRoot")!;
|
||||
await nav.PushAsync(new ContentPage());
|
||||
await nav.PushAsync(page);
|
||||
var window = new Window { Width = width, Height = height, Content = main };
|
||||
try
|
||||
{
|
||||
window.Show();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
window.UpdateLayout();
|
||||
|
||||
var reader = page.FindControl<Grid>("PostReader")!;
|
||||
var list = page.FindControl<Border>("PostsListBorder")!;
|
||||
var splitter = page.FindControl<GridSplitter>("PostsSplitter")!;
|
||||
Assert.Equal(wide ? GridResizeDirection.Columns : GridResizeDirection.Rows, splitter.ResizeDirection);
|
||||
Assert.Equal(wide ? 36d : (double?)null, NavigationPage.GetBarHeightOverride(page));
|
||||
Assert.True(nav.IsBackButtonEffectivelyVisible);
|
||||
Assert.True(reader.Bounds.Width > 0 && reader.Bounds.Height > 0);
|
||||
Assert.True(list.Bounds.Width > 0 && list.Bounds.Height > 0);
|
||||
if (wide)
|
||||
{
|
||||
Assert.True(reader.Bounds.Right <= list.Bounds.Left);
|
||||
Assert.Equal(2, Grid.GetColumn(page.BlogsHeaderPanel));
|
||||
Assert.Equal(2, Grid.GetColumn(page.BlogsCommandBar));
|
||||
Assert.Equal(reader.Margin.Top, reader.Bounds.Top);
|
||||
Assert.Equal(page.StatusBar.Bounds.Top - reader.Margin.Bottom, reader.Bounds.Bottom);
|
||||
Assert.Equal(36, page.TranslatePoint(new Point(), nav)!.Value.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(list.Bounds.Bottom <= reader.Bounds.Top);
|
||||
Assert.Equal(0, Grid.GetColumn(page.BlogsHeaderPanel));
|
||||
Assert.Equal(0, Grid.GetColumn(page.BlogsCommandBar));
|
||||
}
|
||||
|
||||
var articleScroll = page.FindControl<MarkdownScrollViewer>("ArticleReader")!
|
||||
.GetVisualDescendants().OfType<ScrollViewer>().Single();
|
||||
Assert.InRange(articleScroll.Viewport.Height, 1, window.ClientSize.Height);
|
||||
Assert.True(articleScroll.Extent.Height > articleScroll.Viewport.Height);
|
||||
var listScroll = page.PostsListBox.GetVisualDescendants().OfType<ScrollViewer>().Single();
|
||||
Assert.InRange(listScroll.Viewport.Height, 1, window.ClientSize.Height);
|
||||
Assert.True(listScroll.Extent.Height > listScroll.Viewport.Height);
|
||||
|
||||
var originalReaderSize = reader.Bounds.Size;
|
||||
splitter.Focus();
|
||||
splitter.RaiseEvent(new KeyEventArgs
|
||||
{
|
||||
RoutedEvent = InputElement.KeyDownEvent,
|
||||
Key = wide ? Key.Right : Key.Down
|
||||
});
|
||||
window.UpdateLayout();
|
||||
if (wide)
|
||||
Assert.True(reader.Bounds.Width > originalReaderSize.Width);
|
||||
else
|
||||
Assert.True(reader.Bounds.Height < originalReaderSize.Height);
|
||||
|
||||
page.BlogsCommandBar.IsOpen = true;
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
||||
Assert.Same(vm, page.SaveButton.DataContext);
|
||||
Assert.False(page.SaveButton.IsVisible);
|
||||
Assert.True(page.EditPostButton.IsVisible);
|
||||
vm.EditPostCommand.Execute(null);
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.True(page.SaveButton.IsVisible);
|
||||
Assert.Same(vm.SaveCommand, page.SaveButton.Command);
|
||||
Assert.False(page.EditPostButton.IsVisible);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public async Task Resizing_between_portrait_and_landscape_preserves_the_editor_and_draft()
|
||||
{
|
||||
var vm = CreateViewModel();
|
||||
await vm.InitializeAsync();
|
||||
vm.SelectedPost = Post();
|
||||
vm.EditPostCommand.Execute(null);
|
||||
vm.DraftArticle = "Brouillon conservé pendant le redimensionnement.";
|
||||
var document = vm.DraftArticleDocument;
|
||||
var page = new BlogsPage { DataContext = vm };
|
||||
var editor = page.FindControl<Grid>("PostEditor")!;
|
||||
var nav = new NavigationPage();
|
||||
await nav.PushAsync(page);
|
||||
var window = new Window { Width = 360, Height = 800, Content = nav };
|
||||
try
|
||||
{
|
||||
window.Show();
|
||||
foreach (var size in new[] { new Size(360, 800), new Size(1000, 420), new Size(600, 600), new Size(599, 600), new Size(360, 800) })
|
||||
{
|
||||
window.Width = size.Width;
|
||||
window.Height = size.Height;
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
window.UpdateLayout();
|
||||
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
||||
|
||||
var wide = size.Width >= size.Height;
|
||||
Assert.Equal(wide ? 0 : 4, Grid.GetRow(editor));
|
||||
Assert.Equal(wide ? 3 : 1, Grid.GetRowSpan(editor));
|
||||
Assert.Equal(wide ? 36d : (double?)null, NavigationPage.GetBarHeightOverride(page));
|
||||
Assert.Same(editor, page.FindControl<Grid>("PostEditor"));
|
||||
Assert.True(editor.IsVisible);
|
||||
Assert.False(page.FindControl<Grid>("PostReader")!.IsVisible);
|
||||
Assert.True(vm.IsEditing);
|
||||
Assert.True(vm.IsDraftModified);
|
||||
Assert.Equal("Brouillon conservé pendant le redimensionnement.", vm.DraftArticle);
|
||||
Assert.Same(document, vm.DraftArticleDocument);
|
||||
Assert.Equal(42, vm.SelectedPost!.Id);
|
||||
Assert.True(editor.Bounds.Width > 0 && editor.Bounds.Height > 0);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public async Task Image_http_failure_is_logged_with_its_url_and_is_not_swallowed()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="DirectShowLib" />
|
||||
<PackageReference Include="LibVLCSharp.Avalonia" />
|
||||
<PackageReference Include="Live.Avalonia" />
|
||||
<PackageReference Include="Mapsui" />
|
||||
|
|
|
|||
93
src/PostIt/PostIt/Services/CameraStreamer.cs
Normal file
93
src/PostIt/PostIt/Services/CameraStreamer.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibVLCSharp.Shared;
|
||||
|
||||
// Note : Si vous compilez sous Linux, DirectShowLib n'est pas disponible.
|
||||
// Pour un projet multiplateforme propre, vous pouvez utiliser des directives de précompilation (#if WINDOWS)
|
||||
// ou charger la détection DirectShow dans une assembly séparée.
|
||||
using DirectShowLib;
|
||||
|
||||
public class CameraStreamer
|
||||
{
|
||||
private readonly LibVLC _libVLC;
|
||||
|
||||
public CameraStreamer(LibVLC libVLC)
|
||||
{
|
||||
_libVLC = libVLC ?? throw new ArgumentNullException(nameof(libVLC));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Récupère la liste des périphériques de capture disponibles sur la machine.
|
||||
/// Retourne un dictionnaire : Clé = Nom lisible, Valeur = Identifiant/Chemin système
|
||||
/// </summary>
|
||||
public Dictionary<string, string> GetCaptureDevices()
|
||||
{
|
||||
var devices = new Dictionary<string, string>();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Détection Windows via DirectShow
|
||||
DsDevice[] capDevices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice);
|
||||
foreach (var device in capDevices)
|
||||
{
|
||||
// Pour Windows, VLC utilise le nom de l'appareil
|
||||
devices.Add(device.Name, device.Name);
|
||||
}
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
// Détection Linux via V4L2 (Video4Linux2)
|
||||
if (Directory.Exists("/dev"))
|
||||
{
|
||||
var videoFiles = Directory.GetFiles("/dev", "video*");
|
||||
foreach (var path in videoFiles)
|
||||
{
|
||||
// Exemple : Clé = "Caméra (/dev/video0)", Valeur = "/dev/video0"
|
||||
devices.Add($"Caméra ({path})", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException("OS non supporté pour la capture automatique dans cet exemple.");
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crée un objet Media configuré pour le périphérique sélectionné.
|
||||
/// </summary>
|
||||
/// <param name="deviceValue">Le nom de la caméra (Windows) ou le chemin /dev/videoX (Linux)</param>
|
||||
public Media CreateCaptureMedia(string deviceValue)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Configuration pour DirectShow (Windows)
|
||||
var media = new Media(_libVLC, "dshow://", FromType.FromLocation);
|
||||
media.AddOption($":dshow-vdev={deviceValue}");
|
||||
|
||||
// Options optionnelles (Exemple : Forcer la résolution ou le framerate)
|
||||
// media.AddOption(":dshow-size=1280x720");
|
||||
// media.AddOption(":dshow-fps=30");
|
||||
|
||||
return media;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
// Configuration pour V4L2 (Linux)
|
||||
var media = new Media(_libVLC, "v4l2://", FromType.FromLocation);
|
||||
media.AddOption($":v4l2-vdev={deviceValue}");
|
||||
|
||||
// Options optionnelles pour Linux
|
||||
// media.AddOption(":v4l2-width=1280");
|
||||
// media.AddOption(":v4l2-height=720");
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<ContentPage
|
||||
x:Class="PostIt.Views.Blogs.BlogsPage"
|
||||
x:Name="BlogsPageRoot"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
|
|
@ -23,8 +24,8 @@
|
|||
<Setter Property="FontWeight" Value="Bold" />
|
||||
</Style>
|
||||
</ContentPage.Styles>
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto,*,Auto"
|
||||
<Grid x:Name="MainGrid"
|
||||
RowDefinitions="Auto,Auto,*,4,2*,Auto"
|
||||
Margin="5,5,10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
|
|
@ -45,18 +46,19 @@
|
|||
Grid.Row="1"
|
||||
HorizontalAlignment="Right" IsDynamicOverflowEnabled="True">
|
||||
<CommandBar.PrimaryCommands>
|
||||
<CommandBarButton Command="{Binding RefreshCommand}" Label="Actualiser">
|
||||
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" Command="{Binding RefreshCommand}" Label="Actualiser">
|
||||
<CommandBarButton.Icon>
|
||||
<PathIcon Data="{StaticResource RefreshIcon}" />
|
||||
</CommandBarButton.Icon>
|
||||
</CommandBarButton>
|
||||
<CommandBarButton x:Name="NewPostButton" Command="{Binding NewPostCommand}" Label="Nouveau billet" />
|
||||
<CommandBarButton x:Name="ReadPostButton" Command="{Binding ReadPostCommand}"
|
||||
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" x:Name="NewPostButton" Command="{Binding NewPostCommand}" Label="Nouveau billet" />
|
||||
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" x:Name="ReadPostButton" Command="{Binding ReadPostCommand}"
|
||||
IsVisible="{Binding IsEditing}" Label="Lire" />
|
||||
<CommandBarButton x:Name="EditPostButton" Command="{Binding EditPostCommand}"
|
||||
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" x:Name="EditPostButton" Command="{Binding EditPostCommand}"
|
||||
IsEnabled="{Binding SelectedPost, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
IsVisible="{Binding !IsEditing}" Label="Modifier" />
|
||||
<CommandBarButton
|
||||
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||
x:Name="SaveButton"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Command="{Binding SaveCommand}"
|
||||
|
|
@ -67,12 +69,13 @@
|
|||
<PathIcon Data="{StaticResource SaveIcon}" />
|
||||
</CommandBarButton.Icon>
|
||||
</CommandBarButton>
|
||||
<CommandBarButton Command="{Binding DeleteCommand}" IsVisible="{Binding IsEditing}" Label="Supprimer">
|
||||
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" Command="{Binding DeleteCommand}" IsVisible="{Binding IsEditing}" Label="Supprimer">
|
||||
<CommandBarButton.Icon>
|
||||
<PathIcon Data="{StaticResource DeleteIcon}" />
|
||||
</CommandBarButton.Icon>
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||
x:Name="AddAttachmentButton"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Click="AddAttachment_Click"
|
||||
|
|
@ -83,6 +86,7 @@
|
|||
</CommandBarButton.Icon>
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||
x:Name="ManageAclButton"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Command="{Binding ManageAclAsync}"
|
||||
|
|
@ -92,6 +96,7 @@
|
|||
</CommandBarButton.Icon>
|
||||
</CommandBarButton>
|
||||
<CommandBarButton
|
||||
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||
x:Name="OpenCirclesButton"
|
||||
Command="{Binding OpenCirclesAsync}"
|
||||
Label="Mes cercles">
|
||||
|
|
@ -100,6 +105,7 @@
|
|||
</CommandBarButton.Icon>
|
||||
</CommandBarButton>
|
||||
<CommandBarToggleButton
|
||||
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Command="{Binding TogglePublishCommand}"
|
||||
IsChecked="{Binding DraftIsPublished, Mode=TwoWay}"
|
||||
|
|
@ -108,12 +114,13 @@
|
|||
<PathIcon Data="{StaticResource PublishedIcon}" />
|
||||
</CommandBarToggleButton.Icon>
|
||||
</CommandBarToggleButton>
|
||||
<CommandBarToggleButton IsVisible="{Binding IsEditing}" IsChecked="{Binding IsMarkdownViewerVisible, Mode=TwoWay}" Label="Aperçu">
|
||||
<CommandBarToggleButton DataContext="{Binding #BlogsPageRoot.DataContext}" IsVisible="{Binding IsEditing}" IsChecked="{Binding IsMarkdownViewerVisible, Mode=TwoWay}" Label="Aperçu">
|
||||
<CommandBarToggleButton.Icon>
|
||||
<PathIcon Data="{StaticResource MarkdownIcon}" />
|
||||
</CommandBarToggleButton.Icon>
|
||||
</CommandBarToggleButton>
|
||||
<CommandBarButton
|
||||
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||
x:Name="OpenSignatureDevButton"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Command="{Binding OpenSignatureDevAsync}"
|
||||
|
|
@ -125,14 +132,9 @@
|
|||
</CommandBar.PrimaryCommands>
|
||||
</CommandBar>
|
||||
|
||||
<Grid Grid.Row="2" Margin="12" RowSpacing="12" x:Name="MainGrid">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="2*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
<Border x:Name="PostsListBorder"
|
||||
Grid.Row="2"
|
||||
Margin="12"
|
||||
Padding="8"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
|
|
@ -178,14 +180,16 @@
|
|||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<GridSplitter
|
||||
Grid.Row="1"
|
||||
<GridSplitter x:Name="PostsSplitter"
|
||||
Grid.Row="3"
|
||||
Margin="12,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ResizeDirection="Rows" />
|
||||
|
||||
<Grid x:Name="PostEditor"
|
||||
IsVisible="{Binding IsEditing}"
|
||||
Grid.Row="2"
|
||||
Grid.Row="4"
|
||||
Margin="12"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
RowSpacing="10">
|
||||
|
|
@ -237,7 +241,7 @@
|
|||
</Border>
|
||||
|
||||
</Grid>
|
||||
<Grid x:Name="PostReader" Grid.Row="2" IsVisible="{Binding !IsEditing}"
|
||||
<Grid x:Name="PostReader" Grid.Row="4" Margin="12" IsVisible="{Binding !IsEditing}"
|
||||
RowDefinitions="Auto,Auto,*" MaxWidth="900" HorizontalAlignment="Stretch" RowSpacing="12">
|
||||
<TextBlock Text="{Binding ReaderTitle}" FontSize="28" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" />
|
||||
|
|
@ -263,7 +267,6 @@
|
|||
Text="Choisissez un billet dans la liste pour le lire."
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<postitControls:StatusBar Grid.Row="3" DataContext="{Binding ActionStatus}" x:Name="StatusBar" />
|
||||
<postitControls:StatusBar Grid.Row="5" DataContext="{Binding ActionStatus}" x:Name="StatusBar" />
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
using System.IO;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
|
|
@ -13,14 +15,57 @@ public partial class BlogsPage : ContentPage
|
|||
{
|
||||
private static readonly RelayCommand<string?> DisabledLinkCommand =
|
||||
new(static _ => { }, static _ => false);
|
||||
private bool? _isWideLayout;
|
||||
|
||||
public BlogsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
SizeChanged += (_, _) =>
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
UpdateResponsiveLayout();
|
||||
});
|
||||
DataContextChanged += (_, _) => UpdateHyperlinkCommand();
|
||||
UpdateHyperlinkCommand();
|
||||
}
|
||||
|
||||
private void UpdateResponsiveLayout()
|
||||
{
|
||||
var size = TopLevel.GetTopLevel(this)?.ClientSize ?? Bounds.Size;
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
return;
|
||||
|
||||
// Use the window size so compacting the navigation bar cannot change the orientation.
|
||||
var isWide = size.Width >= size.Height;
|
||||
if (_isWideLayout == isWide)
|
||||
return;
|
||||
_isWideLayout = isWide;
|
||||
|
||||
MainGrid.RowDefinitions = new RowDefinitions(isWide ? "Auto,Auto,*,Auto" : "Auto,Auto,*,4,2*,Auto");
|
||||
MainGrid.ColumnDefinitions = new ColumnDefinitions(isWide ? "2*,4,*" : "*");
|
||||
var sidebarColumn = isWide ? 2 : 0;
|
||||
SetArea(BlogsHeaderPanel, 0, sidebarColumn);
|
||||
SetArea(BlogsCommandBar, 1, sidebarColumn);
|
||||
SetArea(PostsListBorder, 2, sidebarColumn);
|
||||
SetArea(PostsSplitter, isWide ? 0 : 3, isWide ? 1 : 0, isWide ? 3 : 1);
|
||||
SetArea(PostReader, isWide ? 0 : 4, 0, isWide ? 3 : 1);
|
||||
SetArea(PostEditor, isWide ? 0 : 4, 0, isWide ? 3 : 1);
|
||||
SetArea(StatusBar, isWide ? 3 : 5, 0, columnSpan: isWide ? 3 : 1);
|
||||
PostsSplitter.ResizeDirection = isWide ? GridResizeDirection.Columns : GridResizeDirection.Rows;
|
||||
PostsSplitter.HorizontalAlignment = isWide ? HorizontalAlignment.Center : HorizontalAlignment.Stretch;
|
||||
PostsSplitter.VerticalAlignment = isWide ? VerticalAlignment.Stretch : VerticalAlignment.Center;
|
||||
PostsSplitter.Margin = isWide ? new Thickness(0, 12) : new Thickness(12, 0);
|
||||
NavigationPage.SetBarHeightOverride(this, isWide ? 36 : null);
|
||||
}
|
||||
|
||||
private static void SetArea(Control control, int row, int column, int rowSpan = 1, int columnSpan = 1)
|
||||
{
|
||||
Grid.SetRow(control, row);
|
||||
Grid.SetColumn(control, column);
|
||||
Grid.SetRowSpan(control, rowSpan);
|
||||
Grid.SetColumnSpan(control, columnSpan);
|
||||
}
|
||||
|
||||
private void UpdateHyperlinkCommand()
|
||||
{
|
||||
if (ArticleReader.Engine is IMarkdownEngine2 engine)
|
||||
|
|
|
|||
Loading…
Reference in a new issue