Prechádzať zdrojové kódy

Reworked MobileDocument stuff to better integrate with Avalonia

frankvandenbos 3 týždňov pred
rodič
commit
191c0e7a81

+ 29 - 10
InABox.Avalonia.Platform.Android/ImageTools.Android.cs

@@ -1,10 +1,10 @@
 using System.Drawing;
 using Android.Graphics;
 using Android.Media;
+using Avalonia.Controls;
 using InABox.Core;
 using Java.IO;
 using Microsoft.Maui.Media;
-using Microsoft.Maui.Storage;
 using Bitmap = Android.Graphics.Bitmap;
 using File = System.IO.File;
 using Path = System.IO.Path;
@@ -61,25 +61,45 @@ namespace InABox.Avalonia.Platform.Android
             Rotate270 = 8
         }
         
-        public async Task<FileResult> PickPhotoAsync(int? compression, Size? constraints)
+        public async Task<ImageFile?> PickPhotoAsync(TopLevel window, int? compression, Size? constraints)
         {
             var fileResult = await MediaPicker.PickPhotoAsync();
             if (fileResult == null)
                 return null;
-            return await ProcessFile(fileResult, compression, constraints);
+            await using var stream = await fileResult.OpenReadAsync();
+            return await ProcessFile(stream, compression, constraints);
         }
         
-        public async Task<FileResult> CapturePhotoAsync(int? compression, Size? constraints)
+        public async Task<ImageFile?> CapturePhotoAsync(TopLevel window, int? compression, Size? constraints)
         {
             var fileResult = await MediaPicker.CapturePhotoAsync();
             if (fileResult == null)
                 return null;
-            return await ProcessFile(fileResult, compression, constraints);
+            await using var stream = await fileResult.OpenReadAsync();
+            return await ProcessFile(stream, compression, constraints);
         }
-
-        private async Task<FileResult> ProcessFile(FileResult fileResult, int? compression, Size? constraints)
+        
+        public async Task<ImageFile?> PickVideoAsync(TopLevel window)
+        {
+            var fileResult = await MediaPicker.PickPhotoAsync();
+            if (fileResult == null)
+                return null;
+            await using var stream = await fileResult.OpenReadAsync();
+            return await ProcessFile(stream, null, null);
+        }
+        
+        public async Task<ImageFile?> CaptureVideoAsync(TopLevel window)
         {
+            var fileResult = await MediaPicker.CapturePhotoAsync();
+            if (fileResult == null)
+                return null;
             await using var stream = await fileResult.OpenReadAsync();
+            return await ProcessFile(stream, null, null);
+        }
+
+        private async Task<ImageFile> ProcessFile(Stream stream, int? compression, Size? constraints)
+        {
+            //await using var stream = await fileResult.OpenReadAsync();
 
             var orientation = GetImageOrientation(stream);
 
@@ -87,8 +107,7 @@ namespace InABox.Avalonia.Platform.Android
             var rotated = RotateImage(source, orientation);
 
             var scaled = ScaleImage(rotated, constraints);
-
-            var jpegFilename = Path.Combine(FileSystem.CacheDirectory, $"{Guid.NewGuid()}.jpg");
+            var jpegFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), $"{Guid.NewGuid()}.jpg");
             using (var outStream = new MemoryStream())
             {
                 await scaled.CompressAsync(Bitmap.CompressFormat.Jpeg, compression ?? 100, outStream);
@@ -96,7 +115,7 @@ namespace InABox.Avalonia.Platform.Android
                 await File.WriteAllBytesAsync(jpegFilename, outStream.ToArray());
             }
 
-            return new FileResult(jpegFilename);
+            return new ImageFile(jpegFilename);
         }
 
         private static Bitmap RotateImage(Bitmap source, ImageOrientation orientation)

+ 113 - 0
InABox.Avalonia.Platform.Desktop/ImageTools.Desktop.cs

@@ -0,0 +1,113 @@
+using System.Drawing;
+using Windows.Media.Capture;
+using Windows.Media.MediaProperties;
+using Windows.Storage;
+using Avalonia.Controls;
+using Avalonia.Platform.Storage;
+using InABox.Core;
+using Microsoft.Maui.Media;
+
+
+namespace InABox.Avalonia.Platform.Desktop;
+
+public class Desktop_ImageTools : IImageTools
+{
+    public Logger? Logger { get; set; }
+    
+    // private static MediaCapture? _captureManager;
+    //
+    // private async Task<MediaCapture> InitMediaCapture()
+    // {
+    //     if (_captureManager == null)
+    //     {
+    //         _captureManager = new MediaCapture();
+    //         MediaCaptureInitializationSettings _settings = new();
+    //         await _captureManager.InitializeAsync(_settings);
+    //     }
+    //     return _captureManager;
+    // }
+    
+    public byte[] CreateVideoThumbnail(byte[] video, int maxwidth, int maxheight)
+    { 
+        Logger?.Error("CreateVideoThumbnail() is not implemented on this platform");
+        return video;
+    }
+
+    public byte[] CreateThumbnail(byte[] image, int maxwidth, int maxheight)
+    { 
+        Logger?.Error("CreateThumbnail() is not implemented on this platform");
+        return image;
+    }
+
+    public async Task<ImageFile?> CapturePhotoAsync(TopLevel window, int? compression, Size? constraints)
+    {
+        // _captureManager = await InitMediaCapture();
+        // var filename = $"{Path.GetFileNameWithoutExtension(Path.GetTempFileName())}.jpeg";
+        // var location = await StorageFolder.GetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures));
+        // var storage = await location.CreateFileAsync(filename, CreationCollisionOption.GenerateUniqueName);
+        // await _captureManager.CapturePhotoToStorageFileAsync(ImageEncodingProperties.CreateJpeg(), storage);
+        // var data = await File.ReadAllBytesAsync(storage.Path);
+        // var result = new ImageFile(storage.Name, data);
+        // return result;
+        
+        // For the purposes of the desktop (demo) app, ignore the camera (above)
+        // and always select a file
+        return await PickPhotoAsync(window,compression,constraints);
+    }
+    
+    public async Task<ImageFile?> PickPhotoAsync(TopLevel window, int? compression, Size? constraints) 
+        => await PickMediaFile(window, "Select Image", WellKnownFolder.Pictures, [FilePickerFileTypes.ImageAll]);
+    
+    
+    public async Task<ImageFile?> CaptureVideoAsync(TopLevel window)
+    {
+        return await PickVideoAsync(window);
+    }
+
+    private FilePickerFileType VideoFiles = new("Video File")
+    {
+        Patterns = ["*.mpeg", "*.avi", "*.mov", "*.mkv", "*mp4"],
+        AppleUniformTypeIdentifiers = ["public.movie"],
+        MimeTypes = ["video/mp4", "video/quicktime", "video/webm", "video/avi", "video/mpeg"]
+    };
+    
+    public async Task<ImageFile?> PickVideoAsync(TopLevel window)
+        => await PickMediaFile(window, "Select Video", WellKnownFolder.Videos, [VideoFiles]);
+    
+    private static async Task<ImageFile?> PickMediaFile(TopLevel window, string caption, WellKnownFolder folder, FilePickerFileType[] filters)
+    {
+        var startlocation = await window.StorageProvider.TryGetWellKnownFolderAsync(folder);
+        var files = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()
+        {
+            Title = caption,
+            FileTypeFilter = filters,
+            AllowMultiple = false,
+            SuggestedStartLocation = startlocation
+        });
+        var first = files.FirstOrDefault();
+        if (first == null)
+            return null;
+        
+        using var ms = new MemoryStream();
+        await using var stream = await first.OpenReadAsync();
+        await stream.CopyToAsync(ms);
+        return new ImageFile(first.Name, ms.GetBuffer());
+    }
+
+    private async Task<ImageFile> ProcessFile(string filename, byte[]? data, int? compression, Size? constraints)
+    {
+        return new ImageFile(filename, data);
+    }
+
+    public byte[] RotateImage(byte[] image, float angle, int quality = 100)
+    { 
+        Logger?.Error("RotateImage() is not implemented on this platform");
+        return image;
+    }
+
+    public byte[] ScaleImage(byte[] image, Size? constraints, int quality = 100)
+    { 
+        Logger?.Error("ScaleImage() is not implemented on this platform");
+        return image;
+    }
+}

+ 1 - 1
InABox.Avalonia.Platform.Desktop/InABox.Avalonia.Platform.Desktop.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
+        <TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
         <Nullable>enable</Nullable>

+ 17 - 6
InABox.Avalonia.Platform.iOS/ImageTools.iOS.cs

@@ -1,10 +1,9 @@
 using System.Drawing;
+using Avalonia.Controls;
 using AVFoundation;
 using CoreMedia;
 using InABox.Core;
 using Microsoft.Maui.ApplicationModel;
-using Microsoft.Maui.Storage;
-
 
 namespace InABox.Avalonia.Platform.iOS
 {
@@ -48,22 +47,34 @@ namespace InABox.Avalonia.Platform.iOS
             return ScaleImage(source, new Size(maxwidth, maxheight), 60);
         }
         
-        public async Task<FileResult> PickPhotoAsync(int? compression, Size? constraints)
+        public async Task<ImageFile?> PickPhotoAsync(TopLevel window, int? compression, Size? constraints)
         {
             return await MainThread.InvokeOnMainThreadAsync(async () =>
                 await InternalGetPhotoAsync<Permissions.Photos>(UIImagePickerControllerSourceType.PhotoLibrary, compression, constraints));
         }
         
-         public async Task<FileResult> CapturePhotoAsync(int? compression, Size? constraints)
+         public async Task<ImageFile?> CapturePhotoAsync(TopLevel window, int? compression, Size? constraints)
         {
             return await MainThread.InvokeOnMainThreadAsync(async () =>
                 await InternalGetPhotoAsync<Permissions.Camera>(UIImagePickerControllerSourceType.Camera, compression, constraints));
         }
+         
+        public async Task<ImageFile?> PickVideoAsync(TopLevel window)
+        {
+            return await MainThread.InvokeOnMainThreadAsync(async () =>
+                await InternalGetPhotoAsync<Permissions.Photos>(UIImagePickerControllerSourceType.PhotoLibrary, null, null));
+        }
+        
+        public async Task<ImageFile?> CaptureVideoAsync(TopLevel window)
+        {
+            return await MainThread.InvokeOnMainThreadAsync(async () =>
+                await InternalGetPhotoAsync<Permissions.Camera>(UIImagePickerControllerSourceType.Camera, null, null));
+        }
 
-        private async Task<FileResult> InternalGetPhotoAsync<TPermission>(UIImagePickerControllerSourceType source, int? compression, Size? constraints)
+        private async Task<ImageFile> InternalGetPhotoAsync<TPermission>(UIImagePickerControllerSourceType source, int? compression, Size? constraints)
             where TPermission : Permissions.BasePermission, new()
         {
-            var taskCompletionSource = new TaskCompletionSource<FileResult>();
+            var taskCompletionSource = new TaskCompletionSource<ImageFile>();
             // if (await Permissions.RequestAsync<TPermission>() == PermissionStatus.Granted)
             // {
             //     var imagePicker = new UIImagePickerController

+ 17 - 4
InABox.Avalonia.Platform/ImageTools/DefaultImageTools.cs

@@ -1,4 +1,5 @@
 using System.Drawing;
+using Avalonia.Controls;
 using InABox.Core;
 using Microsoft.Maui.Storage;
 
@@ -20,16 +21,28 @@ public class DefaultImageTools : IImageTools
         return image;
     }
 
-    public Task<FileResult> CapturePhotoAsync(int? compression, Size? constraints)
+    public Task<ImageFile?> CapturePhotoAsync(TopLevel window, int? compression, Size? constraints)
     {
         Logger?.Error("CapturePhotoAsync() is not implemented on this platform");
-        return Task.Run(() => new FileResult($"{Guid.NewGuid().ToString()}.tmp"));
+        return Task.FromResult<ImageFile?>(null);
     }
 
-    public Task<FileResult> PickPhotoAsync(int? compression, Size? constraints)
+    public Task<ImageFile?> PickPhotoAsync(TopLevel window, int? compression, Size? constraints)
     {
         Logger?.Error("PickPhotoAsync() is not implemented on this platform");
-        return Task.Run(() => new FileResult($"{Guid.NewGuid().ToString()}.tmp"));
+        return Task.FromResult<ImageFile?>(null);
+    }
+    
+    public Task<ImageFile?> CaptureVideoAsync(TopLevel window)
+    {
+        Logger?.Error("CapturePhotoAsync() is not implemented on this platform");
+        return Task.FromResult<ImageFile?>(null);
+    }
+
+    public Task<ImageFile?> PickVideoAsync(TopLevel window)
+    {
+        Logger?.Error("PickPhotoAsync() is not implemented on this platform");
+        return Task.FromResult<ImageFile?>(null);
     }
 
     public byte[] RotateImage(byte[] image, float angle, int quality = 100)

+ 13 - 3
InABox.Avalonia.Platform/ImageTools/IImageTools.cs

@@ -1,8 +1,15 @@
 using System.Drawing;
-using Microsoft.Maui.Storage;
+using Avalonia.Controls;
 
 namespace InABox.Avalonia.Platform
 {
+
+    public class ImageFile(string filename, byte[]? data = null)
+    {
+        public string FileName { get; private set; } = filename;
+        public byte[]? Data { get; set; } = data;
+    }
+    
     public interface IImageTools : ILoggable
     {
 
@@ -11,8 +18,11 @@ namespace InABox.Avalonia.Platform
         byte[] CreateThumbnail(byte[] image, int maxwidth, int maxheight);
         
         // Goal - to return a properly rotated, scaled and compressed JPEG Image
-        Task<FileResult> CapturePhotoAsync(int? compression, Size? constraints);
-        Task<FileResult> PickPhotoAsync(int? compression, Size? constraints);
+        Task<ImageFile?> CapturePhotoAsync(TopLevel window, int? compression, Size? constraints);
+        Task<ImageFile?> PickPhotoAsync(TopLevel window, int? compression, Size? constraints);
+        
+        Task<ImageFile?> CaptureVideoAsync(TopLevel window);
+        Task<ImageFile?> PickVideoAsync(TopLevel window);
 
         byte[] RotateImage(byte[] image, float angle, int quality = 100);
 

+ 1 - 0
InABox.Avalonia.Platform/InABox.Avalonia.Platform.csproj

@@ -10,6 +10,7 @@
 
     <ItemGroup>
       <PackageReference Include="Autofac" Version="8.2.0" />
+      <PackageReference Include="Avalonia" Version="11.2.2" />
       <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.70" />
     </ItemGroup>
 

+ 3 - 2
InABox.Avalonia/MobileDocument/Images/MobileDocumentCameraSource.cs

@@ -1,3 +1,4 @@
+using Avalonia.Controls;
 using Microsoft.Maui.ApplicationModel;
 using Microsoft.Maui.Storage;
 using InABox.Avalonia.Platform;
@@ -13,8 +14,8 @@ namespace InABox.Avalonia
         protected override async Task<bool> IsEnabled() 
             => await IsEnabled<Permissions.Camera>();
         
-        protected override async Task<FileResult> Capture() 
-            => await PlatformTools.ImageTools.CapturePhotoAsync(Options.Compression, Options.Constraints);
+        protected override async Task<ImageFile> Capture(TopLevel window) 
+            => await PlatformTools.ImageTools.CapturePhotoAsync(window, Options.Compression, Options.Constraints);
 
 
     }

+ 3 - 2
InABox.Avalonia/MobileDocument/Images/MobileDocumentPhotoLibrarySource.cs

@@ -1,3 +1,4 @@
+using Avalonia.Controls;
 using Microsoft.Maui.ApplicationModel;
 using Microsoft.Maui.Storage;
 using InABox.Avalonia.Platform;
@@ -14,8 +15,8 @@ namespace InABox.Avalonia
         protected override async Task<bool> IsEnabled()
             => await IsEnabled<Permissions.Photos>();
 
-        protected override async Task<FileResult> Capture() 
-            => await PlatformTools.ImageTools.PickPhotoAsync(Options.Compression, Options.Constraints);
+        protected override async Task<ImageFile> Capture(TopLevel window) 
+            => await PlatformTools.ImageTools.PickPhotoAsync(window, Options.Compression, Options.Constraints);
         
     }
 }

+ 7 - 32
InABox.Avalonia/MobileDocument/MobileDocument.cs

@@ -1,3 +1,4 @@
+using Avalonia.Controls;
 using Syncfusion.Pdf;
 using Syncfusion.Pdf.Graphics;
 
@@ -20,47 +21,21 @@ namespace InABox.Avalonia
             Data = data;
         }
         
-        public void ConvertToPDF()
-        {
-            using (var img = new MemoryStream(Data))
-            {
-                var image = new PdfBitmap(img);
-
-                var pdfDoc = new PdfDocument();
-
-                var section = pdfDoc.Sections.Add();
-                section.PageSettings.Margins.All = 0;
-                section.PageSettings.Width = image.Width;
-                section.PageSettings.Height = image.Height;
-
-                var page = section.Pages.Add();
-                page.Graphics.DrawImage(image, 0, 0, page.Size.Width, page.Size.Height);
-
-                using (var ms = new MemoryStream())
-                {
-                    pdfDoc.Save(ms);
-                    Data = ms.GetBuffer();
-                    FileName = Path.ChangeExtension(FileName, "pdf");                
-                }
-            }
-
-        }
-        
-        public bool IsPDF() => FileName.ToUpper().EndsWith(".PDF");
-
         
-        public static async Task<MobileDocument> From<TSource, TOptions>(TSource source) 
+        public static async Task<MobileDocument> From<TSource, TOptions>(TopLevel window, TSource source) 
             where TSource : MobileDocumentSource
             where TOptions : MobileDocumentOptions<TSource>
         {
-            var result = await source.From();
+            var result = await source.From(window);
             return result;
         }
         
-        public static async Task<MobileDocument> From<T>(MobileDocumentOptions<T> options) where T : MobileDocumentSource
+        public static async Task<MobileDocument> From<T>(TopLevel? window, MobileDocumentOptions<T> options) where T : MobileDocumentSource
         {
+            if (window == null)
+                throw new Exception("Window is null");
             var source = (T)Activator.CreateInstance(typeof(T), new object[] { options });
-            var result = await source.From();
+            var result = await source.From(window);
             return result;
         }
         

+ 29 - 0
InABox.Avalonia/MobileDocument/MobileDocumentExtensions.cs

@@ -1,7 +1,36 @@
+using Syncfusion.Pdf;
+using Syncfusion.Pdf.Graphics;
+
 namespace InABox.Avalonia
 {
     public static class MobileDocumentExtensions
     {
+        public static void ConvertToPDF(this MobileDocument document)
+        {
+            using (var img = new MemoryStream(document.Data))
+            {
+                var image = new PdfBitmap(img);
+
+                var pdfDoc = new PdfDocument();
+
+                var section = pdfDoc.Sections.Add();
+                section.PageSettings.Margins.All = 0;
+                section.PageSettings.Width = image.Width;
+                section.PageSettings.Height = image.Height;
+
+                var page = section.Pages.Add();
+                page.Graphics.DrawImage(image, 0, 0, page.Size.Width, page.Size.Height);
+
+                using (var ms = new MemoryStream())
+                {
+                    pdfDoc.Save(ms);
+                    document.Data = ms.GetBuffer();
+                    document.FileName = Path.ChangeExtension(document.FileName, "pdf");                
+                }
+            }
+
+        }
         
+        public static bool IsPDF(this MobileDocument document) => document.FileName.ToUpper().EndsWith(".PDF"); 
     }
 }

+ 15 - 13
InABox.Avalonia/MobileDocument/MobileDocumentSource.cs

@@ -1,13 +1,13 @@
+using Avalonia.Controls;
+using InABox.Avalonia.Platform;
 using Microsoft.Maui.ApplicationModel;
-using Microsoft.Maui.Storage;
-
 
 namespace InABox.Avalonia
 {
     
     public abstract class MobileDocumentSource
     {
-        public abstract Task<MobileDocument> From();
+        public abstract Task<MobileDocument> From(TopLevel window);
     }
     
     public abstract class MobileDocumentSource<TOptions> : MobileDocumentSource 
@@ -32,24 +32,26 @@ namespace InABox.Avalonia
             return status == PermissionStatus.Granted;
         }
         
-        protected abstract Task<FileResult> Capture();
+        protected abstract Task<ImageFile> Capture(TopLevel window);
         
-        public override async Task<MobileDocument> From()
+        public override async Task<MobileDocument> From(TopLevel window)
         {
             var result = new MobileDocument();
             if (await IsEnabled())
             {
                 try
                 {
-                    var file = await Capture();
-                    if (file != null)
+                    var file = await Capture(window);
+                    if (file?.Data != null)
                     {
-                        result.FileName = Path.GetFileName(file.FileName); //file.OriginalFilename ?? file.Path);
-                        await using (var stream = await file.OpenReadAsync())
-                        {
-                            BinaryReader br = new BinaryReader(stream);
-                            result.Data = br.ReadBytes((int)stream.Length);
-                        }
+                        result.FileName = Path.GetFileName(file.FileName);
+                        result.Data = file.Data; 
+                        //file.OriginalFilename ?? file.Path);
+                        //await using (var stream = await file.OpenReadAsync())
+                        //{
+                        //    BinaryReader br = new BinaryReader(file.Stream);
+                        //    result.Data = br.ReadBytes((int)file.Stream.Length);
+                        //}
                     }
                     ApplyOptions(result);
                 }

+ 5 - 3
InABox.Avalonia/MobileDocument/Videos/MobileDocumentVideoLibrarySource.cs

@@ -1,5 +1,7 @@
 
 
+using Avalonia.Controls;
+using InABox.Avalonia.Platform;
 using Microsoft.Maui.ApplicationModel;
 using Microsoft.Maui.Media;
 using Microsoft.Maui.Storage;
@@ -15,9 +17,9 @@ namespace InABox.Avalonia
         
         protected override async Task<bool> IsEnabled() 
             => await IsEnabled<Permissions.Photos>();
-        
-        protected override async Task<FileResult> Capture() 
-            => await MediaPicker.PickVideoAsync();
+
+        protected override async Task<ImageFile> Capture(TopLevel window)
+            => await PlatformTools.ImageTools.PickVideoAsync(window);
 
 
     }

+ 4 - 4
InABox.Avalonia/MobileDocument/Videos/MobileDocumentVideoSource.cs

@@ -1,8 +1,8 @@
 
 
+using Avalonia.Controls;
+using InABox.Avalonia.Platform;
 using Microsoft.Maui.ApplicationModel;
-using Microsoft.Maui.Media;
-using Microsoft.Maui.Storage;
 
 namespace InABox.Avalonia
 {
@@ -16,8 +16,8 @@ namespace InABox.Avalonia
         protected override async Task<bool> IsEnabled() 
             => await IsEnabled<Permissions.Photos>();
         
-        protected override async Task<FileResult> Capture() 
-            => await MediaPicker.CaptureVideoAsync();
+        protected override async Task<ImageFile> Capture(TopLevel window) 
+            => await PlatformTools.ImageTools.CaptureVideoAsync(window);
 
 
     }