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
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -1,5 +1,28 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.0.8-rc27] - unstable
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* [PostIt] Service `CameraStreamer` (préliminaire) : détection des
|
||||||
|
périphériques de capture vidéo (DirectShow sous Windows via le paquet
|
||||||
|
`DirectShowLib`, `/dev/video*` sous Linux) et création d'un média
|
||||||
|
LibVLC de capture pour le périphérique choisi.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
* [PostIt] La page des blogs passe en deux colonnes lorsque la largeur de
|
||||||
|
la fenêtre atteint sa hauteur : billet à gauche, recherche, commandes et
|
||||||
|
liste à droite, avec séparateur vertical redimensionnable. La disposition
|
||||||
|
verticale revient en portrait sans perdre le brouillon ; la barre de
|
||||||
|
navigation est plus compacte en mode large et conserve le bouton Retour.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* [PostIt] Le changement de disposition de la page des blogs est différé
|
||||||
|
sur le thread UI, évitant l'exception « Visual was invalidated during
|
||||||
|
the render pass » au redimensionnement.
|
||||||
|
|
||||||
## [1.0.8-rc26] - unstable
|
## [1.0.8-rc26] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,20 @@ et les commandes restent visibles, tandis que la liste des billets et
|
||||||
l'éditeur se partagent l'espace disponible. Un `GridSplitter` permet
|
l'éditeur se partagent l'espace disponible. Un `GridSplitter` permet
|
||||||
de modifier cette répartition. La liste et l'article défilent séparément.
|
de modifier cette répartition. La liste et l'article défilent séparément.
|
||||||
|
|
||||||
|
Lorsque la largeur de la fenêtre atteint ou dépasse sa hauteur, le lecteur
|
||||||
|
ou l'éditeur passe à gauche (deux tiers de la largeur par défaut), et
|
||||||
|
l'en-tête, la recherche, les commandes et la liste passent à droite.
|
||||||
|
Le séparateur devient vertical. En portrait, la disposition verticale
|
||||||
|
revient sans recréer les contrôles ni perdre le brouillon.
|
||||||
|
Le seuil utilise les dimensions clientes de la fenêtre, indépendamment
|
||||||
|
de la hauteur occupée par la navigation et la bannière de session.
|
||||||
|
|
||||||
|
La page applique `NavigationPage.BarHeightOverride` à 36 en mode large ;
|
||||||
|
le `NavRoot` de `MainView` conserve ainsi le bouton Retour, sans changer
|
||||||
|
les autres pages. En portrait, la hauteur de navigation par défaut revient.
|
||||||
|
Les commandes ont un contexte de données explicite pour conserver leurs
|
||||||
|
liaisons, y compris lorsqu'elles passent dans le menu de débordement.
|
||||||
|
|
||||||
Dans `MarkdownEditorControl`, AvaloniaEdit gère son propre défilement ;
|
Dans `MarkdownEditorControl`, AvaloniaEdit gère son propre défilement ;
|
||||||
l'aperçu Markdown est placé dans un `ScrollViewer`. Ne pas limiter
|
l'aperçu Markdown est placé dans un `ScrollViewer`. Ne pas limiter
|
||||||
la hauteur du `MarkdownViewer` lui-même : son contenu doit pouvoir
|
la hauteur du `MarkdownViewer` lui-même : son contenu doit pouvoir
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<PackageVersion Include="Avalonia.Headless" Version="12.1.3" />
|
<PackageVersion Include="Avalonia.Headless" Version="12.1.3" />
|
||||||
<PackageVersion Include="Avalonia.Headless.Xunit" Version="$(AvaloniaVersionBase)" />
|
<PackageVersion Include="Avalonia.Headless.Xunit" Version="$(AvaloniaVersionBase)" />
|
||||||
<PackageVersion Include="Avalonia.Fonts.Inter" 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="LibVLCSharp.Avalonia" Version="3.10.1" />
|
||||||
<PackageVersion Include="Live.Avalonia" Version="1.4.2" />
|
<PackageVersion Include="Live.Avalonia" Version="1.4.2" />
|
||||||
<PackageVersion Include="Mapsui" Version="5.1.0" />
|
<PackageVersion Include="Mapsui" Version="5.1.0" />
|
||||||
|
|
@ -37,4 +38,4 @@
|
||||||
<PackageVersion Include="Xamarin.UITest" Version="4.4.2" />
|
<PackageVersion Include="Xamarin.UITest" Version="4.4.2" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -2,7 +2,9 @@ using System.Net;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Templates;
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.Headless;
|
||||||
using Avalonia.Headless.XUnit;
|
using Avalonia.Headless.XUnit;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Logging;
|
using Avalonia.Logging;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
|
|
@ -95,6 +97,7 @@ public class BlogReaderTests
|
||||||
window.Show();
|
window.Show();
|
||||||
Dispatcher.UIThread.RunJobs();
|
Dispatcher.UIThread.RunJobs();
|
||||||
window.UpdateLayout();
|
window.UpdateLayout();
|
||||||
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
|
||||||
Assert.True(page.FindControl<Grid>("PostReader")!.IsVisible);
|
Assert.True(page.FindControl<Grid>("PostReader")!.IsVisible);
|
||||||
Assert.False(page.FindControl<Grid>("PostEditor")!.IsVisible);
|
Assert.False(page.FindControl<Grid>("PostEditor")!.IsVisible);
|
||||||
Assert.False(page.SaveButton.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]
|
[AvaloniaFact]
|
||||||
public async Task Image_http_failure_is_logged_with_its_url_and_is_not_swallowed()
|
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>
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="DirectShowLib" />
|
||||||
<PackageReference Include="LibVLCSharp.Avalonia" />
|
<PackageReference Include="LibVLCSharp.Avalonia" />
|
||||||
<PackageReference Include="Live.Avalonia" />
|
<PackageReference Include="Live.Avalonia" />
|
||||||
<PackageReference Include="Mapsui" />
|
<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
|
<ContentPage
|
||||||
x:Class="PostIt.Views.Blogs.BlogsPage"
|
x:Class="PostIt.Views.Blogs.BlogsPage"
|
||||||
|
x:Name="BlogsPageRoot"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
|
@ -23,8 +24,8 @@
|
||||||
<Setter Property="FontWeight" Value="Bold" />
|
<Setter Property="FontWeight" Value="Bold" />
|
||||||
</Style>
|
</Style>
|
||||||
</ContentPage.Styles>
|
</ContentPage.Styles>
|
||||||
<Grid
|
<Grid x:Name="MainGrid"
|
||||||
RowDefinitions="Auto,Auto,*,Auto"
|
RowDefinitions="Auto,Auto,*,4,2*,Auto"
|
||||||
Margin="5,5,10,5"
|
Margin="5,5,10,5"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
|
|
@ -45,18 +46,19 @@
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
HorizontalAlignment="Right" IsDynamicOverflowEnabled="True">
|
HorizontalAlignment="Right" IsDynamicOverflowEnabled="True">
|
||||||
<CommandBar.PrimaryCommands>
|
<CommandBar.PrimaryCommands>
|
||||||
<CommandBarButton Command="{Binding RefreshCommand}" Label="Actualiser">
|
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" Command="{Binding RefreshCommand}" Label="Actualiser">
|
||||||
<CommandBarButton.Icon>
|
<CommandBarButton.Icon>
|
||||||
<PathIcon Data="{StaticResource RefreshIcon}" />
|
<PathIcon Data="{StaticResource RefreshIcon}" />
|
||||||
</CommandBarButton.Icon>
|
</CommandBarButton.Icon>
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
<CommandBarButton x:Name="NewPostButton" Command="{Binding NewPostCommand}" Label="Nouveau billet" />
|
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" x:Name="NewPostButton" Command="{Binding NewPostCommand}" Label="Nouveau billet" />
|
||||||
<CommandBarButton x:Name="ReadPostButton" Command="{Binding ReadPostCommand}"
|
<CommandBarButton DataContext="{Binding #BlogsPageRoot.DataContext}" x:Name="ReadPostButton" Command="{Binding ReadPostCommand}"
|
||||||
IsVisible="{Binding IsEditing}" Label="Lire" />
|
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}}"
|
IsEnabled="{Binding SelectedPost, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||||
IsVisible="{Binding !IsEditing}" Label="Modifier" />
|
IsVisible="{Binding !IsEditing}" Label="Modifier" />
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
|
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||||
x:Name="SaveButton"
|
x:Name="SaveButton"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
Command="{Binding SaveCommand}"
|
Command="{Binding SaveCommand}"
|
||||||
|
|
@ -67,12 +69,13 @@
|
||||||
<PathIcon Data="{StaticResource SaveIcon}" />
|
<PathIcon Data="{StaticResource SaveIcon}" />
|
||||||
</CommandBarButton.Icon>
|
</CommandBarButton.Icon>
|
||||||
</CommandBarButton>
|
</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>
|
<CommandBarButton.Icon>
|
||||||
<PathIcon Data="{StaticResource DeleteIcon}" />
|
<PathIcon Data="{StaticResource DeleteIcon}" />
|
||||||
</CommandBarButton.Icon>
|
</CommandBarButton.Icon>
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
|
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||||
x:Name="AddAttachmentButton"
|
x:Name="AddAttachmentButton"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
Click="AddAttachment_Click"
|
Click="AddAttachment_Click"
|
||||||
|
|
@ -83,6 +86,7 @@
|
||||||
</CommandBarButton.Icon>
|
</CommandBarButton.Icon>
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
|
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||||
x:Name="ManageAclButton"
|
x:Name="ManageAclButton"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
Command="{Binding ManageAclAsync}"
|
Command="{Binding ManageAclAsync}"
|
||||||
|
|
@ -92,6 +96,7 @@
|
||||||
</CommandBarButton.Icon>
|
</CommandBarButton.Icon>
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
|
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||||
x:Name="OpenCirclesButton"
|
x:Name="OpenCirclesButton"
|
||||||
Command="{Binding OpenCirclesAsync}"
|
Command="{Binding OpenCirclesAsync}"
|
||||||
Label="Mes cercles">
|
Label="Mes cercles">
|
||||||
|
|
@ -100,6 +105,7 @@
|
||||||
</CommandBarButton.Icon>
|
</CommandBarButton.Icon>
|
||||||
</CommandBarButton>
|
</CommandBarButton>
|
||||||
<CommandBarToggleButton
|
<CommandBarToggleButton
|
||||||
|
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
Command="{Binding TogglePublishCommand}"
|
Command="{Binding TogglePublishCommand}"
|
||||||
IsChecked="{Binding DraftIsPublished, Mode=TwoWay}"
|
IsChecked="{Binding DraftIsPublished, Mode=TwoWay}"
|
||||||
|
|
@ -108,12 +114,13 @@
|
||||||
<PathIcon Data="{StaticResource PublishedIcon}" />
|
<PathIcon Data="{StaticResource PublishedIcon}" />
|
||||||
</CommandBarToggleButton.Icon>
|
</CommandBarToggleButton.Icon>
|
||||||
</CommandBarToggleButton>
|
</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>
|
<CommandBarToggleButton.Icon>
|
||||||
<PathIcon Data="{StaticResource MarkdownIcon}" />
|
<PathIcon Data="{StaticResource MarkdownIcon}" />
|
||||||
</CommandBarToggleButton.Icon>
|
</CommandBarToggleButton.Icon>
|
||||||
</CommandBarToggleButton>
|
</CommandBarToggleButton>
|
||||||
<CommandBarButton
|
<CommandBarButton
|
||||||
|
DataContext="{Binding #BlogsPageRoot.DataContext}"
|
||||||
x:Name="OpenSignatureDevButton"
|
x:Name="OpenSignatureDevButton"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
Command="{Binding OpenSignatureDevAsync}"
|
Command="{Binding OpenSignatureDevAsync}"
|
||||||
|
|
@ -125,14 +132,9 @@
|
||||||
</CommandBar.PrimaryCommands>
|
</CommandBar.PrimaryCommands>
|
||||||
</CommandBar>
|
</CommandBar>
|
||||||
|
|
||||||
<Grid Grid.Row="2" Margin="12" RowSpacing="12" x:Name="MainGrid">
|
<Border x:Name="PostsListBorder"
|
||||||
<Grid.RowDefinitions>
|
Grid.Row="2"
|
||||||
<RowDefinition Height="*" />
|
Margin="12"
|
||||||
<RowDefinition Height="4" />
|
|
||||||
<RowDefinition Height="2*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Border
|
|
||||||
Grid.Row="0"
|
|
||||||
Padding="8"
|
Padding="8"
|
||||||
BorderBrush="Gray"
|
BorderBrush="Gray"
|
||||||
BorderThickness="1">
|
BorderThickness="1">
|
||||||
|
|
@ -178,14 +180,16 @@
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<GridSplitter
|
<GridSplitter x:Name="PostsSplitter"
|
||||||
Grid.Row="1"
|
Grid.Row="3"
|
||||||
|
Margin="12,0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ResizeDirection="Rows" />
|
ResizeDirection="Rows" />
|
||||||
|
|
||||||
<Grid x:Name="PostEditor"
|
<Grid x:Name="PostEditor"
|
||||||
IsVisible="{Binding IsEditing}"
|
IsVisible="{Binding IsEditing}"
|
||||||
Grid.Row="2"
|
Grid.Row="4"
|
||||||
|
Margin="12"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
RowSpacing="10">
|
RowSpacing="10">
|
||||||
|
|
@ -237,7 +241,7 @@
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
</Grid>
|
</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">
|
RowDefinitions="Auto,Auto,*" MaxWidth="900" HorizontalAlignment="Stretch" RowSpacing="12">
|
||||||
<TextBlock Text="{Binding ReaderTitle}" FontSize="28" FontWeight="SemiBold"
|
<TextBlock Text="{Binding ReaderTitle}" FontSize="28" FontWeight="SemiBold"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
|
|
@ -263,7 +267,6 @@
|
||||||
Text="Choisissez un billet dans la liste pour le lire."
|
Text="Choisissez un billet dans la liste pour le lire."
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" />
|
HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
<postitControls:StatusBar Grid.Row="5" DataContext="{Binding ActionStatus}" x:Name="StatusBar" />
|
||||||
<postitControls:StatusBar Grid.Row="3" DataContext="{Binding ActionStatus}" x:Name="StatusBar" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
|
|
@ -13,14 +15,57 @@ public partial class BlogsPage : ContentPage
|
||||||
{
|
{
|
||||||
private static readonly RelayCommand<string?> DisabledLinkCommand =
|
private static readonly RelayCommand<string?> DisabledLinkCommand =
|
||||||
new(static _ => { }, static _ => false);
|
new(static _ => { }, static _ => false);
|
||||||
|
private bool? _isWideLayout;
|
||||||
|
|
||||||
public BlogsPage()
|
public BlogsPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
SizeChanged += (_, _) =>
|
||||||
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
UpdateResponsiveLayout();
|
||||||
|
});
|
||||||
DataContextChanged += (_, _) => UpdateHyperlinkCommand();
|
DataContextChanged += (_, _) => UpdateHyperlinkCommand();
|
||||||
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()
|
private void UpdateHyperlinkCommand()
|
||||||
{
|
{
|
||||||
if (ArticleReader.Engine is IMarkdownEngine2 engine)
|
if (ArticleReader.Engine is IMarkdownEngine2 engine)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue