Browse Source

Add directional navigation to image viewer

pull/165/head
Ionite 1 year ago
parent
commit
fc1313307a
No known key found for this signature in database
  1. 94
      StabilityMatrix.Avalonia/Controls/CheckerboardBorder.cs
  2. 2
      StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml
  3. 20
      StabilityMatrix.Avalonia/Models/DirectionalNavigationEventArgs.cs
  4. 73
      StabilityMatrix.Avalonia/Styles/ThemeMaterials.axaml
  5. 18
      StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs
  6. 49
      StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs
  7. 36
      StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml
  8. 20
      StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs

94
StabilityMatrix.Avalonia/Controls/CheckerboardBorder.cs

@ -0,0 +1,94 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace StabilityMatrix.Avalonia.Controls;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class CheckerboardBorder : Control
{
public static readonly StyledProperty<byte> GridCellSizeProperty = AvaloniaProperty.Register<
AdvancedImageBox,
byte
>(nameof(GridCellSize), 15);
public byte GridCellSize
{
get => GetValue(GridCellSizeProperty);
set => SetValue(GridCellSizeProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> GridColorProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(GridColor),
SolidColorBrush.Parse("#181818")
);
/// <summary>
/// Gets or sets the color used to create the checkerboard style background
/// </summary>
public ISolidColorBrush GridColor
{
get => GetValue(GridColorProperty);
set => SetValue(GridColorProperty, value);
}
public static readonly StyledProperty<ISolidColorBrush> GridColorAlternateProperty =
AvaloniaProperty.Register<AdvancedImageBox, ISolidColorBrush>(
nameof(GridColorAlternate),
SolidColorBrush.Parse("#252525")
);
/// <summary>
/// Gets or sets the color used to create the checkerboard style background
/// </summary>
public ISolidColorBrush GridColorAlternate
{
get => GetValue(GridColorAlternateProperty);
set => SetValue(GridColorAlternateProperty, value);
}
static CheckerboardBorder()
{
AffectsRender<CheckerboardBorder>(GridCellSizeProperty);
AffectsRender<CheckerboardBorder>(GridColorProperty);
AffectsRender<CheckerboardBorder>(GridColorAlternateProperty);
}
/// <inheritdoc />
public override void Render(DrawingContext context)
{
var size = GridCellSize;
var square1Drawing = new GeometryDrawing
{
Brush = GridColorAlternate,
Geometry = new RectangleGeometry(new Rect(0.0, 0.0, size, size))
};
var square2Drawing = new GeometryDrawing
{
Brush = GridColorAlternate,
Geometry = new RectangleGeometry(new Rect(size, size, size, size))
};
var drawingGroup = new DrawingGroup { Children = { square1Drawing, square2Drawing } };
var tileBrush = new DrawingBrush(drawingGroup)
{
AlignmentX = AlignmentX.Left,
AlignmentY = AlignmentY.Top,
DestinationRect = new RelativeRect(new Size(2 * size, 2 * size), RelativeUnit.Absolute),
Stretch = Stretch.None,
TileMode = TileMode.Tile,
};
context.FillRectangle(GridColor, Bounds);
// context.DrawRectangle(new Pen(Brushes.Blue), new Rect(0.5, 0.5, Bounds.Width - 1.0, Bounds.Height - 1.0));
context.FillRectangle(tileBrush, Bounds);
// base.Render(context);
}
}

2
StabilityMatrix.Avalonia/Controls/ImageFolderCard.axaml

@ -72,7 +72,7 @@
<ItemsRepeater
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
VerticalCacheLength="16"
VerticalCacheLength="8"
ItemsSource="{Binding LocalImages}">
<ItemsRepeater.Resources>

20
StabilityMatrix.Avalonia/Models/DirectionalNavigationEventArgs.cs

@ -0,0 +1,20 @@
using System;
using System.Numerics;
namespace StabilityMatrix.Avalonia.Models;
public class DirectionalNavigationEventArgs : EventArgs
{
private Vector2 Direction { get; }
public DirectionalNavigationEventArgs(Vector2 direction)
{
Direction = direction;
}
public static DirectionalNavigationEventArgs Up => new(new Vector2(0, -1));
public static DirectionalNavigationEventArgs Down => new(new Vector2(0, 1));
public bool IsNext => Direction.X > 0 || Direction.Y > 0;
public bool IsPrevious => Direction.X < 0 || Direction.Y < 0;
}

73
StabilityMatrix.Avalonia/Styles/ThemeMaterials.axaml

@ -1,19 +1,60 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<ExperimentalAcrylicMaterial
x:Key="ThemeDarkAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="Black"
TintOpacity="0.78"
MaterialOpacity="0.9"
BackgroundSource="Digger" />
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sty="clr-namespace:FluentAvalonia.Styling;assembly=FluentAvalonia">
<ExperimentalAcrylicMaterial
x:Key="ThemeTransparentAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="Black"
TintOpacity="0.7"
MaterialOpacity="0.5"
BackgroundSource="Digger" />
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<ExperimentalAcrylicMaterial
x:Key="ThemeDarkAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="Black"
TintOpacity="0.78"
MaterialOpacity="0.9"
BackgroundSource="Digger" />
<ExperimentalAcrylicMaterial
x:Key="ThemeTransparentAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="{DynamicResource ThemeBackgroundColor}"
TintOpacity="0.7"
MaterialOpacity="0.5"
BackgroundSource="Digger" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<ExperimentalAcrylicMaterial
x:Key="ThemeDarkAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="Black"
TintOpacity="0.78"
MaterialOpacity="0.9"
BackgroundSource="Digger" />
<ExperimentalAcrylicMaterial
x:Key="ThemeTransparentAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="Black"
TintOpacity="0.7"
MaterialOpacity="0.5"
BackgroundSource="Digger" />
</ResourceDictionary>
<ResourceDictionary x:Key="{x:Static sty:FluentAvaloniaTheme.HighContrastTheme}">
<ExperimentalAcrylicMaterial
x:Key="ThemeDarkAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="Black"
TintOpacity="0.78"
MaterialOpacity="0.9"
BackgroundSource="Digger" />
<ExperimentalAcrylicMaterial
x:Key="ThemeTransparentAcrylicMaterial"
FallbackColor="{DynamicResource ThemeBackgroundColor}"
TintColor="Black"
TintOpacity="0.7"
MaterialOpacity="0.5"
BackgroundSource="Digger" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

18
StabilityMatrix.Avalonia/ViewModels/Dialogs/ImageViewerViewModel.cs

@ -1,4 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs;
@ -29,6 +31,8 @@ public partial class ImageViewerViewModel : ContentDialogViewModelBase
[ObservableProperty]
private string? imageSizeText;
public event EventHandler<DirectionalNavigationEventArgs>? NavigationRequested;
partial void OnLocalImageFileChanged(LocalImageFile? value)
{
if (value?.ImageSize is { IsEmpty: false } size)
@ -45,4 +49,16 @@ public partial class ImageViewerViewModel : ContentDialogViewModelBase
FileSizeText = Size.FormatBase10Bytes(localFile.GetSize(true));
}
}
[RelayCommand]
private void OnNavigateNext()
{
NavigationRequested?.Invoke(this, DirectionalNavigationEventArgs.Up);
}
[RelayCommand]
private void OnNavigatePrevious()
{
NavigationRequested?.Invoke(this, DirectionalNavigationEventArgs.Down);
}
}

49
StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs

@ -8,10 +8,12 @@ using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Navigation;
using FuzzySharp;
using FuzzySharp.PreProcess;
using FuzzySharp.SimilarityRatio.Scorer.Composite;
@ -132,10 +134,51 @@ public partial class ImageFolderCardViewModel : ViewModelBase
return;
}
var currentIndex = LocalImages.IndexOf(item);
var image = new ImageSource(new FilePath(imagePath));
// Preload
await image.GetBitmapAsync();
var vm = new ImageViewerViewModel { ImageSource = image, LocalImageFile = item };
using var onNext = Observable
.FromEventPattern<DirectionalNavigationEventArgs>(
vm,
nameof(ImageViewerViewModel.NavigationRequested)
)
.Subscribe(ctx =>
{
Dispatcher.UIThread
.InvokeAsync(async () =>
{
var sender = (ImageViewerViewModel)ctx.Sender!;
var newIndex = currentIndex + (ctx.EventArgs.IsNext ? 1 : -1);
if (newIndex >= 0 && newIndex < LocalImages.Count)
{
var newImage = LocalImages[newIndex];
var newImageSource = new ImageSource(
new FilePath(newImage.GetFullPath(settingsManager.ImagesDirectory))
);
// Preload
await newImageSource.GetBitmapAsync();
var oldImageSource = sender.ImageSource;
sender.ImageSource = newImageSource;
sender.LocalImageFile = newImage;
// oldImageSource?.Dispose();
currentIndex = newIndex;
}
})
.SafeFireAndForget();
});
var dialog = new BetterContentDialog
{
MaxDialogWidth = 1000,
@ -148,11 +191,7 @@ public partial class ImageFolderCardViewModel : ViewModelBase
{
Width = 1000,
Height = 1000,
DataContext = new ImageViewerViewModel
{
ImageSource = image,
LocalImageFile = item
}
DataContext = vm
}
};

36
StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml

@ -16,27 +16,37 @@
x:DataType="vmDialogs:ImageViewerViewModel"
mc:Ignorable="d">
<controls:UserControlBase.KeyBindings>
<KeyBinding Gesture="Up" Command="{Binding NavigateNextCommand}"/>
<KeyBinding Gesture="Down" Command="{Binding NavigatePreviousCommand}"/>
</controls:UserControlBase.KeyBindings>
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RowDefinitions="*,Auto">
<controls:AdvancedImageBox Image="{Binding ImageSource.BitmapAsync^}" SizeMode="Fit" />
<controls:AdvancedImageBox
Image="{Binding ImageSource.BitmapAsync^}"
SizeMode="Fit" />
<!-- Close button -->
<Grid
Grid.Row="0"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Button
Margin="0,8,8,0"
icons:Attached.Icon="fa-solid fa-xmark"
Classes="transparent"
Command="{Binding OnCloseButtonClick}" />
<UniformGrid Rows="3">
<Button
Margin="0,8,8,0"
Padding="8"
icons:Attached.Icon="fa-solid fa-xmark"
Classes="transparent-full"
Command="{Binding OnCloseButtonClick}" />
</UniformGrid>
</Grid>
<!-- Footer -->
<Border
<!--<Border
Grid.Row="1"
MinHeight="20"
Background="Transparent"
@ -57,14 +67,12 @@
FontSize="13"
Text="{Binding FileSizeText}" />
</UniformGrid>
</Border>
<!--<ExperimentalAcrylicBorder
</Border>-->
<ExperimentalAcrylicBorder
Grid.Row="1"
MinHeight="20"
Padding="4,0,0,0"
Padding="4,0,4,0"
VerticalAlignment="Bottom"
Material="{StaticResource ThemeTransparentAcrylicMaterial}"
Opacity="0.9">
Material="{StaticResource ThemeDarkAcrylicMaterial}">
<UniformGrid Columns="3" Margin="4">
<TextBlock
FontSize="13"
@ -80,7 +88,7 @@
HorizontalAlignment="Right"
Text="{Binding FileSizeText}"/>
</UniformGrid>
</ExperimentalAcrylicBorder>-->
</ExperimentalAcrylicBorder>
<!-- The preview tracker -->
<!--<Image

20
StabilityMatrix.Avalonia/Views/Dialogs/ImageViewerDialog.axaml.cs

@ -1,13 +1,25 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.Views.Dialogs;
public partial class ImageViewerDialog : UserControl
{
public static readonly StyledProperty<bool> IsFooterEnabledProperty = AvaloniaProperty.Register<
ImageViewerDialog,
bool
>("IsFooterEnabled");
/// <summary>
/// Whether the footer with file name / size will be shown.
/// </summary>
public bool IsFooterEnabled
{
get => GetValue(IsFooterEnabledProperty);
set => SetValue(IsFooterEnabledProperty, value);
}
public ImageViewerDialog()
{
InitializeComponent();

Loading…
Cancel
Save