Преглед изворни кода

avalonia: Created DocumentList, DocumentPage and DocumentViewer

Kenric Nugteren пре 3 месеци
родитељ
комит
5b919f3d87

+ 54 - 0
PRS.Avalonia/PRS.Avalonia/Components/DocumentList/DocumentList.axaml

@@ -0,0 +1,54 @@
+<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:listView="using:PRS.Avalonia.Components.ListView"
+			 xmlns:components="using:PRS.Avalonia.Components"
+			 xmlns:avalonia="using:PRS.Avalonia"
+			 xmlns:iAvalonia="using:InABox.Avalonia"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PRS.Avalonia.Components.DocumentList"
+			 x:DataType="components:DocumentList">
+	<listView:PrsListView Repository="{Binding $parent[components:DocumentList].Repository}"
+						  DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType = components:DocumentList}}">
+		<listView:PrsListView.ItemTemplate>
+			<DataTemplate x:DataType="iAvalonia:IEntityDocumentShell">
+				<Button Classes="Standard"
+					Margin="0,0,0,2"
+					Background="{StaticResource PrsTileBackground}"
+					Command="{Binding $parent[components:DocumentList].DocumentClickedCommand}"
+					CommandParameter="{Binding .}"
+					HorizontalContentAlignment="Stretch">
+					<Grid x:DataType="iAvalonia:IEntityDocumentShell">
+
+						<Grid.ColumnDefinitions>
+							<ColumnDefinition Width="Auto" />
+							<ColumnDefinition Width="*" />
+							<ColumnDefinition Width="Auto" />
+						</Grid.ColumnDefinitions>
+
+						<Grid.RowDefinitions>
+							<RowDefinition Height="*"/>
+							<RowDefinition Height="*"/>
+						</Grid.RowDefinitions>
+
+						<Image 
+							Grid.Row="0"
+							Grid.Column="0"
+							Grid.ColumnSpan="3"
+							Height="200"
+							Source="{Binding Thumbnail, Converter={StaticResource ByteArrayToImageSourceConverter}}"/>
+
+						<Label
+							Content="{Binding FileName}"
+							Grid.Row="1"
+							Grid.Column="1"
+							FontSize="{StaticResource PrsFontSizeNormal}"/>
+						
+					</Grid>
+				</Button>
+		
+			</DataTemplate>
+		</listView:PrsListView.ItemTemplate>
+	</listView:PrsListView>
+</UserControl>

+ 36 - 0
PRS.Avalonia/PRS.Avalonia/Components/DocumentList/DocumentList.axaml.cs

@@ -0,0 +1,36 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using CommunityToolkit.Mvvm.Input;
+using InABox.Avalonia;
+
+namespace PRS.Avalonia.Components;
+
+public partial class DocumentList : UserControl
+{
+    public static readonly StyledProperty<ICoreRepository?> RepositoryProperty =
+        AvaloniaProperty.Register<DocumentList, ICoreRepository?>(nameof(Repository));
+
+    public ICoreRepository? Repository
+    {
+        get => GetValue(RepositoryProperty);
+        set
+        {
+            SetValue(RepositoryProperty, value); 
+        }
+    }
+
+    public DocumentList()
+    {
+        InitializeComponent();
+    }
+
+    [RelayCommand]
+    private void DocumentClicked(IEntityDocumentShell shell)
+    {
+        Navigation.Navigate<DocumentPageViewModel>(x =>
+        {
+            x.DocumentID = shell.DocumentID;
+        });
+    }
+}

+ 21 - 0
PRS.Avalonia/PRS.Avalonia/Components/DocumentList/DocumentPageView.axaml

@@ -0,0 +1,21 @@
+<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:components="using:PRS.Avalonia.Components"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PRS.Avalonia.Components.DocumentPageView"
+			 x:DataType="components:DocumentPageViewModel">
+	<Grid>
+		<components:DocumentViewer Name="Viewer"
+								   IsVisible="{Binding Document,Converter={x:Static ObjectConverters.IsNotNull}}"
+								   FileName="{Binding Document?.FileName}"
+								   Data="{Binding Document?.Data}"/>
+		<Label Name="NoImage"
+			   Content="File Not Available!"
+			   HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
+			   HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
+			   Foreground="Gray"
+			   IsVisible="{Binding Document,Converter={x:Static ObjectConverters.IsNull}}"/>
+	</Grid>
+</UserControl>

+ 13 - 0
PRS.Avalonia/PRS.Avalonia/Components/DocumentList/DocumentPageView.axaml.cs

@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PRS.Avalonia.Components;
+
+public partial class DocumentPageView : UserControl
+{
+    public DocumentPageView()
+    {
+        InitializeComponent();
+    }
+}

+ 48 - 0
PRS.Avalonia/PRS.Avalonia/Components/DocumentList/DocumentPageViewModel.cs

@@ -0,0 +1,48 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using InABox.Core;
+using PRS.Avalonia.Modules;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRS.Avalonia.Components;
+
+internal partial class DocumentPageViewModel : ModuleViewModel
+{
+    public override string Title => "Unknown Document";
+
+    [ObservableProperty]
+    private Guid _documentID;
+
+    [ObservableProperty]
+    private DocumentModel _model;
+
+    [ObservableProperty]
+    private DocumentShell? _document;
+
+    public DocumentPageViewModel()
+    {
+        Model = new DocumentModel(DataAccess,
+            () => new Filter<Document>(x => x.ID).IsEqualTo(DocumentID),
+            () => DefaultCacheFileName<DocumentShell>(DocumentID));
+
+        ProgressVisible = true;
+    }
+
+    protected override async Task<TimeSpan> OnRefresh()
+    {
+        await Model.RefreshAsync(true);
+
+        Document = Model.FirstOrDefault();
+
+        if(Document is not null)
+        {
+            UpdateTitle(Document.FileName);
+        }
+
+        ProgressVisible = false;
+        return TimeSpan.Zero;
+    }
+}

+ 28 - 0
PRS.Avalonia/PRS.Avalonia/Components/DocumentList/DocumentViewer.axaml

@@ -0,0 +1,28 @@
+<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:components="using:PRS.Avalonia.Components"
+			 xmlns:iComponents="using:InABox.Avalonia.Components"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PRS.Avalonia.Components.DocumentViewer">
+	<Grid>
+		<Grid.RowDefinitions>
+			<RowDefinition Height="*"/>
+			<RowDefinition Height="Auto"/>
+		</Grid.RowDefinitions>
+		<Canvas Name="OuterCanvas" Grid.Row="0" Background="Transparent">
+			<Canvas.GestureRecognizers>
+				<iComponents:PanAndZoomGestureRecognizer/>
+			</Canvas.GestureRecognizers>
+			<Image Name="Image"/>
+		</Canvas>
+		<Label Name="NoImage" Grid.Row="0"
+			   Content="No Image Available"
+			   HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
+			   HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
+			   Foreground="Gray"/>
+		<Label Grid.Row="1" Content="{Binding $parent[components:DocumentViewer].FileName}"
+			   HorizontalContentAlignment="Center"/>
+	</Grid>
+</UserControl>

+ 188 - 0
PRS.Avalonia/PRS.Avalonia/Components/DocumentList/DocumentViewer.axaml.cs

@@ -0,0 +1,188 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using DynamicData.Binding;
+using InABox.Avalonia;
+using InABox.Avalonia.Components;
+using InABox.Avalonia.Converters;
+using InABox.Avalonia.Platform;
+using InABox.Core;
+using System;
+using System.IO;
+using System.Reactive;
+using System.Threading.Tasks;
+
+namespace PRS.Avalonia.Components;
+
+public partial class DocumentViewer : UserControl
+{
+    public static StyledProperty<string?> FileNameProperty =
+        AvaloniaProperty.Register<DocumentViewer, string?>(nameof(FileName));
+    public string? FileName
+    {
+        get => GetValue(FileNameProperty);
+        set => SetValue(FileNameProperty, value);
+    }
+
+    public static StyledProperty<byte[]?> DataProperty =
+        AvaloniaProperty.Register<DocumentViewer, byte[]?>(nameof(Data));
+    public byte[]? Data
+    {
+        get => GetValue(DataProperty);
+        set => SetValue(DataProperty, value);
+    }
+
+    private double ScaleFactor = 1.0;
+    private double _originalScaleFactor = 1.0;
+    private double _startScaleFactor = 1.0;
+
+    // Center of the image.
+    private Point ImageCenter = new();
+    private IImage? _image;
+    private double ImageWidth => _image?.Size.Width ?? 0;
+    private double ImageHeight => _image?.Size.Height ?? 0;
+
+    public DocumentViewer()
+    {
+        InitializeComponent();
+
+        this.GetPropertyChangedObservable(FileNameProperty).Subscribe(x => Update().ContinueWith(ContinueTask));
+        this.GetPropertyChangedObservable(DataProperty).Subscribe(x => Update().ContinueWith(ContinueTask));
+
+        OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, ImageContainer_PinchEnded);
+        OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, ImageContainer_Pinch);
+
+        OuterCanvas.SizeChanged += ImageContainer_SizeChanged;
+        OuterCanvas.PointerWheelChanged += OuterCanvas_PointerWheelChanged;
+    }
+
+    private const double _wheelSpeed = 0.1;
+    private const double _panSpeed = 30;
+
+    private void Pan(double x, double y)
+    {
+        ImageCenter += new Vector(x, y);
+        UpdateCanvasPosition();
+    }
+
+    private void OuterCanvas_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
+    {
+        if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
+        {
+            var pos = e.GetPosition(OuterCanvas);
+            var wheelSpeed = _wheelSpeed;
+            Zoom(pos, pos, e.Delta.Y > 0 ? ScaleFactor * (1 + e.Delta.Y * wheelSpeed) : ScaleFactor / (1 + (-e.Delta.Y) * wheelSpeed));
+        }
+        else if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
+        {
+            Pan(e.Delta.Y * _panSpeed, e.Delta.X * _panSpeed);
+        }
+        else
+        {
+            Pan(e.Delta.X * _panSpeed, e.Delta.Y * _panSpeed);
+        }
+    }
+
+    private void ImageContainer_SizeChanged(object? sender, SizeChangedEventArgs e)
+    {
+        PositionImage();
+    }
+
+    private void ImageContainer_Pinch(object? sender, PanAndZoomEventArgs e)
+    {
+        Zoom(e.ScaleOrigin - e.Pan, e.ScaleOrigin, _originalScaleFactor * e.Scale);
+    }
+
+    private void ImageContainer_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
+    {
+        _originalScaleFactor = ScaleFactor;
+    }
+
+    private void Zoom(Point originalOrigin, Point newOrigin, double newScaleFactor)
+    {
+        // Convert Scale Origin to image coordinates (relative to center).
+        // Work out where this position will move to under the new scaling.
+        // Adjust so that these are the same.
+
+        var pos = originalOrigin - ImageCenter;
+        var contentMPos = pos / ScaleFactor;
+
+        ScaleFactor = Math.Max(_startScaleFactor, newScaleFactor);
+
+        var scaledPos = ImageCenter + contentMPos * ScaleFactor;
+        var offset = scaledPos - newOrigin;
+
+        ImageCenter -= offset;
+
+        UpdateCanvasPosition();
+    }
+
+    private void PositionImage()
+    {
+        var canvasWidth = OuterCanvas.Bounds.Width;
+        var canvasHeight = OuterCanvas.Bounds.Height;
+
+        var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
+        ScaleFactor = scaleFactor;
+        _originalScaleFactor = ScaleFactor;
+        _startScaleFactor = ScaleFactor;
+
+        ImageCenter = new Point(
+            OuterCanvas.Bounds.Width / 2,
+            OuterCanvas.Bounds.Height / 2);
+
+        UpdateCanvasPosition();
+    }
+
+    private void UpdateCanvasPosition()
+    {
+        var imageWidth = ImageWidth * ScaleFactor;
+        var imageHeight = ImageHeight * ScaleFactor;
+
+        Image.Width = imageWidth;
+        Image.Height = imageHeight;
+
+        Canvas.SetLeft(Image, ImageCenter.X - imageWidth / 2);
+        Canvas.SetTop(Image, ImageCenter.Y - imageHeight / 2);
+
+        // OuterCanvas.Width = ImageWidth;
+        // OuterCanvas.Height = ImageHeight;
+    }
+
+    private void ContinueTask(Task task)
+    {
+        if(task.Exception is not null)
+        {
+            MobileLogging.Log(task.Exception);
+        }
+    }
+
+    private async Task Update()
+    {
+        if (Data is not null && FileName is not null)
+        {
+            byte[]? bitmap;
+            if(Path.GetExtension(FileName).ToUpper() == ".PDF")
+            {
+                bitmap = await PlatformTools.PdfRenderer.PdfToImageAsync(Data, 0, 300);
+            }
+            else
+            {
+                bitmap = Data;
+            }
+            if(bitmap is not null)
+            {
+                _image = ByteArrayToImageSourceConverter.Instance.Convert(bitmap);
+                Image.Source = _image;
+                Image.IsVisible = true;
+                NoImage.IsVisible = false;
+                return;
+            }
+        }
+
+        NoImage.IsVisible = true;
+        Image.IsVisible = false;
+    }
+}