瀏覽代碼

avalonia: Finished DocumentScanner

Kenric Nugteren 3 月之前
父節點
當前提交
c3f13837e1

+ 9 - 0
PRS.Avalonia/PRS.Avalonia/Dialogs/MessageDialog.cs

@@ -34,4 +34,13 @@ public static class MessageDialog
         //     model.Message = message;
         // });
     }
+
+    public static async Task ShowError(Exception e)
+    {
+        await MessageDialogViewModel.ShowMessage($"Error: {e.Message}");
+        // var result = await Navigation.Popup<MessageDialogViewModel, MessageDialogResult>(model =>
+        // {
+        //     model.Message = message;
+        // });
+    }
 }

+ 24 - 0
PRS.Avalonia/PRS.Avalonia/Modules/DocumentScanner/DocumentScannerEditorView.axaml

@@ -0,0 +1,24 @@
+<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:modules="using:PRS.Avalonia.Modules"
+			 xmlns:converters="using:InABox.Avalonia.Converters"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PRS.Avalonia.Modules.DocumentScannerEditorView"
+			 x:DataType="modules:DocumentScannerEditorViewModel">
+	<Grid Margin="{StaticResource PrsControlSpacing}">
+		<Grid.RowDefinitions>
+			<RowDefinition Height="*"/>
+			<RowDefinition Height="150"/>
+		</Grid.RowDefinitions>
+		<Image Grid.Row="0" Source="{Binding Document.Data,Converter={x:Static converters:ByteArrayToImageSourceConverter.Instance}}"/>
+		<TextBox Grid.Row="1"
+				 Watermark="Enter Note Here"
+				 Text="{Binding DataEntryDocument.Note}"
+				 AcceptsReturn="True"
+				 TextAlignment="Start"
+				 VerticalContentAlignment="Top"
+				 FontSize="{StaticResource PrsFontSizeSmall}"/>
+	</Grid>
+</UserControl>

+ 13 - 0
PRS.Avalonia/PRS.Avalonia/Modules/DocumentScanner/DocumentScannerEditorView.axaml.cs

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

+ 66 - 0
PRS.Avalonia/PRS.Avalonia/Modules/DocumentScanner/DocumentScannerEditorViewModel.cs

@@ -0,0 +1,66 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using InABox.Avalonia;
+using InABox.Avalonia.Components;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRS.Avalonia.Modules;
+
+internal partial class DocumentScannerEditorViewModel : ModuleViewModel
+{
+    public override string Title => "Document Editor";
+
+    [ObservableProperty]
+    private DataEntryDocumentShell _dataEntryDocument = null!;
+
+    [ObservableProperty]
+    public DocumentShell? _document;
+
+    [ObservableProperty]
+    private DocumentModel? _documentModel;
+
+    public DocumentScannerEditorViewModel()
+    {
+        PrimaryMenu.Add(new AvaloniaMenuItem(Images.save, Save));
+    }
+
+    protected override Task OnActivated()
+    {
+        DocumentModel = new DocumentModel(DataAccess,
+            () => new Filter<Document>(x => x.ID).IsEqualTo(DataEntryDocument.DocumentID),
+            () => DefaultCacheFileName<DocumentShell>(DataEntryDocument.DocumentID));
+        return Task.CompletedTask;
+    }
+
+    protected override async Task<TimeSpan> OnRefresh()
+    {
+        await DocumentModel!.RefreshAsync(false);
+
+        Document = DocumentModel.FirstOrDefault();
+
+        return TimeSpan.Zero;
+    }
+
+    public override bool OnBackButtonPressed()
+    {
+        DataEntryDocument.Cancel();
+        return base.OnBackButtonPressed();
+    }
+
+    private async Task<bool> Save()
+    {
+        ProgressVisible = true;
+
+        await DataEntryDocument.SaveAsync("Updated from Mobile Device");
+
+        ProgressVisible = false;
+
+        Navigation.Back();
+
+        return true;
+    }
+}

+ 71 - 20
PRS.Avalonia/PRS.Avalonia/Modules/DocumentScanner/DocumentScannerView.axaml

@@ -4,27 +4,78 @@
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:local="clr-namespace:PRS.Avalonia.Modules"
              xmlns:converters="clr-namespace:InABox.Avalonia.Converters;assembly=InABox.Avalonia"
+			 xmlns:listView="using:PRS.Avalonia.Components.ListView"
+			 xmlns:prs="using:PRS.Avalonia"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PRS.Avalonia.Modules.DocumentScannerView"
              x:DataType="local:DocumentScannerViewModel">
-    <UserControl.Resources>
-        <converters:BooleanToColorConverter x:Key="BooleanToColorConverter" />
-    </UserControl.Resources>
-    <Grid>
-        <Grid.RowDefinitions>
-            <RowDefinition Height="*"/>
-            <RowDefinition Height="*"/>
-        </Grid.RowDefinitions>
-        <Image
-            Grid.Row="0"
-            Source="{Binding ImageSource}" />
-    <Button 
-        Grid.Row="1"
-        Command="{Binding ToggleCameraCommand}" 
-        Content="Start" 
-        Background="{Binding Active, Converter={StaticResource BooleanToColorConverter}}"
-        VerticalAlignment="Center" 
-        HorizontalAlignment="Center" 
-        Padding="40" />
-    </Grid>
+	<listView:PrsListView Repository="{Binding Documents}"
+						  RefreshVisible="True"
+						  Margin="{StaticResource PrsControlSpacing}"
+						  SearchVisible="False">
+		<listView:PrsListView.ItemTemplate>
+			<DataTemplate DataType="prs:DataEntryDocumentShell">
+				<Button Classes="Standard"
+						CommandParameter="{Binding}"
+						Command="{Binding $parent[local:DocumentScannerView].((local:DocumentScannerViewModel)DataContext).ClickCommand}"
+						HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
+					<Button.Styles>
+						<Style Selector="Button.Standard">
+							<Setter Property="Background" Value="White"/>
+							<Setter Property="Padding" Value="0"/>
+							<Setter Property="Margin" Value="0,0,0,2"/>
+						</Style>
+					</Button.Styles>
+					<Grid Height="150">
+						<Grid.ColumnDefinitions>
+							<ColumnDefinition Width="150"/>
+							<ColumnDefinition Width="0.75"/>
+							<ColumnDefinition Width="*"/>
+							<ColumnDefinition Width="Auto"/>
+						</Grid.ColumnDefinitions>
+						<Grid.RowDefinitions>
+							<RowDefinition Height="*"/>
+							<RowDefinition Height="Auto"/>
+						</Grid.RowDefinitions>
+
+						<Image Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"
+							   Margin="5"
+							   Source="{Binding Thumbnail,Converter={x:Static converters:ByteArrayToImageSourceConverter.Instance}}"/>
+						<Rectangle Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Fill="Black"/>
+						<TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2"
+								   FontSize="{StaticResource PrsFontSizeSmall}"
+								   Foreground="Black"
+								   Text="{Binding Note}"
+								   IsVisible="{Binding Note,Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
+								   TextAlignment="Start"
+								   TextWrapping="Wrap"
+								   Margin="5"/>
+						<TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2"
+								   FontSize="{StaticResource PrsFontSizeSmall}"
+								   Foreground="Gray"
+								   Text="(Tap to enter notes)"
+								   FontStyle="Italic"
+								   IsVisible="{Binding Note,Converter={x:Static StringConverters.IsNullOrEmpty}}"
+								   TextAlignment="Start"
+								   TextWrapping="Wrap"
+								   Margin="5"/>
+						<TextBlock Grid.Row="1" Grid.Column="2"
+								   FontSize="{StaticResource PrsFontSizeSmall}"
+								   Foreground="Black"
+								   Text="{Binding TagName}"
+								   FontStyle="Italic"
+								   TextAlignment="Start"
+								   Margin="5,0,5,5"/>
+						<TextBlock Grid.Row="1" Grid.Column="3"
+								   FontSize="{StaticResource PrsFontSizeSmall}"
+								   Foreground="Black"
+								   Text="{Binding Created,StringFormat=\{0:dd MMM yy\}}"
+								   FontStyle="Italic"
+								   TextAlignment="End"
+								   Margin="0,0,5,5"/>
+					</Grid>
+				</Button>
+			</DataTemplate>
+		</listView:PrsListView.ItemTemplate>
+	</listView:PrsListView>
 </UserControl>

+ 140 - 9
PRS.Avalonia/PRS.Avalonia/Modules/DocumentScanner/DocumentScannerViewModel.cs

@@ -1,31 +1,162 @@
+using System;
+using System.IO;
+using System.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Media;
+using Comal.Classes;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
 using InABox.Avalonia;
+using InABox.Avalonia.Components;
+using InABox.Avalonia.Platform;
+using InABox.Clients;
+using InABox.Core;
+using PRS.Avalonia.Components;
 
 namespace PRS.Avalonia.Modules;
 
 public partial class DocumentScannerViewModel : ModuleViewModel
 {
     public override string Title => "Doc Scanner";
-    
-    [ObservableProperty] private IImage? _imageSource;
 
-    [ObservableProperty] private bool _active;
+    [ObservableProperty]
+    private DataEntryDocumentModel _documents;
+
+    [ObservableProperty]
+    private DataEntryTagModel _tags;
     
+    public DocumentScannerViewModel()
+    {
+        Documents = new(DataAccess,
+            () => new Filter<DataEntryDocument>(x => x.Employee.ID).IsEqualTo(Repositories.Me.ID)
+                .And(x => x.Archived).IsEqualTo(DateTime.MinValue),
+            () => DefaultCacheFileName<DataEntryDocumentShell>());
+
+        Tags = new(DataAccess,
+            () => new Filter<DataEntryTag>().All(),
+            () => DefaultCacheFileName<DataEntryTagShell>());
+
+        PrimaryMenu.Add(new AvaloniaMenuItem(Images.plus, () =>
+        {
+            var menu = new CoreMenu<IImage>();
+
+            menu.AddItem("Take Photo", TakePhoto);
+            menu.AddItem("Browse Library", BrowseLibrary);
+
+            return menu;
+        }));
+
+        ProgressVisible = true;
+    }
+
+    protected override async Task<TimeSpan> OnRefresh()
+    {
+        await Task.WhenAll(Documents.RefreshAsync(false), Tags.RefreshAsync(false));
+
+        ProgressVisible = false;
+
+        return TimeSpan.Zero;
+    }
+
     [RelayCommand]
-    private async Task ToggleCamera(Button button)
+    private void Click(DataEntryDocumentShell shell)
+    {
+        Navigation.Navigate<DocumentScannerEditorViewModel>(model =>
+        {
+            model.DataEntryDocument = shell;
+        });
+    }
+
+    private async Task<bool> AddImage<T, TOptions>(TOptions options)
+        where T : MobileDocumentSource
+        where TOptions : MobileImageOptions<T>
     {
-        if (Active)
+        MobileDocument? file = null;
+        try
+        {
+            file = await MobileDocument.From(App.TopLevel, options);
+        }
+        catch(Exception e)
         {
-            Active = false;
+            MobileLogging.LogExceptionMessage(e);
+            await MessageDialog.ShowError(e);
         }
-        else
+
+        if (file is null) return false;
+
+        if(file.Data.Length == 0)
         {
-            Active = true;
+            await MessageDialog.ShowMessage("The selected file was empty. No document created.");
+            return false;
         }
-        var doc = await MobileDocument.From(App.TopLevel, new MobileDocumentCameraOptions());
+
+        var extension = Path.GetExtension(file.FileName);
+        file.FileName = Path.ChangeExtension(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), extension);
+
+        var shell = Documents.CreateItem();
+        if(await ConfirmScan(shell))
+        {
+            shell.Thumbnail = PlatformTools.ImageTools.CreateThumbnail(file.Data, 256, 256);
+            shell.FileName = file.FileName;
+
+            ProgressVisible = true;
+
+            var document = new Document
+            {
+                FileName = file.FileName,
+                Data = file.Data,
+                CRC = CoreUtils.CalculateCRC(file.Data),
+                TimeStamp = DateTime.Now
+            };
+            await Client.SaveAsync(document, "Created on Mobile Device");
+
+            shell.DocumentID = document.ID;
+            await shell.SaveAsync("Created on Mobile Device");
+
+            Documents.CommitItem(shell);
+
+            ProgressVisible = false;
+        }
+
+        return true;
+    }
+
+    private async Task<bool> ConfirmScan(DataEntryDocumentShell shell)
+    {
+        shell.EmployeeID = Repositories.Me.ID;
+        if(Tags.ItemCount == 0)
+        {
+            shell.TagID = Guid.Empty;
+            return true;
+        }
+
+        var tags = await SelectionViewModel.ExecutePopup<DataEntryTagShell>(model =>
+        {
+            model.CanRefresh = false;
+            model.SelectionTitle = "Choose Tag";
+            model.Columns.Add(new AvaloniaDataGridTextColumn<DataEntryTagShell>
+            {
+                Column = x => x.Name,
+                Caption = "Tag",
+                Width = GridLength.Star
+            });
+        }, (args) => Tags);
+        if (tags is null || tags.FirstOrDefault() is not DataEntryTagShell tag) return false;
+        shell.TagID = tag.ID;
+        shell.TagName = tag.Name;
+        return true;
+    }
+
+    private async Task<bool> BrowseLibrary()
+    {
+        await AddImage<MobileDocumentPhotoLibrarySource, MobileDocumentPhotoLibraryOptions>(PhotoUtils.CreatePhotoLibraryOptions());
+        return true;
+    }
+
+    private async Task<bool> TakePhoto()
+    {
+        await AddImage<MobileDocumentCameraSource, MobileDocumentCameraOptions>(PhotoUtils.CreateCameraOptions());
+        return true;
     }
 }

+ 3 - 0
PRS.Avalonia/PRS.Avalonia/PRS.Avalonia.csproj

@@ -346,6 +346,9 @@
             <DependentUpon>DeliveriesView.axaml</DependentUpon>
             <SubType>Code</SubType>
         </Compile>
+        <Compile Update="Modules\DocumentScanner\DocumentScannerEditorView.axaml.cs">
+          <DependentUpon>DocumentScannerEditorView.axaml</DependentUpon>
+        </Compile>
         <Compile Update="Modules\Equipment\EquipmentView.axaml.cs">
             <DependentUpon>EquipmentView.axaml</DependentUpon>
             <SubType>Code</SubType>

+ 33 - 0
PRS.Avalonia/PRS.Avalonia/PhotoUtils/PhotoUtils.cs

@@ -0,0 +1,33 @@
+using Comal.Classes;
+using InABox.Avalonia;
+using InABox.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRS.Avalonia;
+
+public static class PhotoUtils
+{
+    public static MobileDocumentCameraOptions CreateCameraOptions()
+    {
+        var photoSettings = new GlobalConfiguration<MobilePhotoSettings>().Load();
+        return new MobileDocumentCameraOptions()
+        {
+            Compression = photoSettings.PhotoCompression,
+            Constraints = new System.Drawing.Size(photoSettings.MaxPhotoWidth, photoSettings.MaxPhotoHeight)
+        };
+    }
+
+    public static MobileDocumentPhotoLibraryOptions CreatePhotoLibraryOptions()
+    {
+        var photoSettings = new GlobalConfiguration<MobilePhotoSettings>().Load();
+        return new MobileDocumentPhotoLibraryOptions()
+        {
+            Compression = photoSettings.PhotoCompression,
+            Constraints = new System.Drawing.Size(photoSettings.MaxPhotoWidth, photoSettings.MaxPhotoHeight)
+        };
+    }
+}

+ 1 - 1
PRS.Avalonia/PRS.Avalonia/Repositories/DataEntryDocument/DataEntryDocumentModel.cs

@@ -7,7 +7,7 @@ namespace PRS.Avalonia;
 
 public class DataEntryDocumentModel : CoreRepository<DataEntryDocumentModel, DataEntryDocumentShell, DataEntryDocument>
 {
-    public DataEntryDocumentModel(IModelHost host, Func<Filter<DataEntryDocument>> baseFilter) : base(host, baseFilter)
+    public DataEntryDocumentModel(IModelHost host, Func<Filter<DataEntryDocument>> baseFilter, Func<string>? filename = null) : base(host, baseFilter, filename)
     {
     }
 }

+ 1 - 0
PRS.Avalonia/PRS.Avalonia/Repositories/DataEntryDocument/DataEntryDocumentShell.cs

@@ -1,6 +1,7 @@
 using System;
 using Comal.Classes;
 using InABox.Avalonia;
+using InABox.Avalonia.Converters;
 
 namespace PRS.Avalonia;
 

+ 1 - 1
PRS.Avalonia/PRS.Avalonia/Repositories/DataEntryTag/DataEntryTagModel.cs

@@ -7,7 +7,7 @@ namespace PRS.Avalonia;
 
 public class DataEntryTagModel : CoreRepository<DataEntryTagModel, DataEntryTagShell, DataEntryTag>
 {
-    public DataEntryTagModel(IModelHost host, Func<Filter<DataEntryTag>> baseFilter) : base(host, baseFilter)
+    public DataEntryTagModel(IModelHost host, Func<Filter<DataEntryTag>> baseFilter, Func<string>? filename = null) : base(host, baseFilter, filename)
     {
     }
 }