diff --git a/Mobile/Extensions/MauiExtensions.cs b/Mobile/Extensions/MauiExtensions.cs new file mode 100644 index 00000000..de525edb --- /dev/null +++ b/Mobile/Extensions/MauiExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Carmen.Mobile.Extensions +{ + internal static class MauiExtensions + { + /// This is primarily required because ImageButton doesn't work. + /// It completely ignores the Aspect setting and displays like junk, it is unusable. + /// This extension allows you to add a Tapped event handler to a non-tappable Image, which DOES work. + public static void AddTapHandler(this View view, EventHandler handler, int number_of_taps_required = 1) + { + var tapped = new TapGestureRecognizer + { + NumberOfTapsRequired = number_of_taps_required + }; + tapped.Tapped += handler; + view.GestureRecognizers.Add(tapped); + } + } +} diff --git a/Mobile/Platforms/Android/AndroidManifest.xml b/Mobile/Platforms/Android/AndroidManifest.xml index e9937ad7..26086dcd 100644 --- a/Mobile/Platforms/Android/AndroidManifest.xml +++ b/Mobile/Platforms/Android/AndroidManifest.xml @@ -1,6 +1,10 @@  - + + + + + \ No newline at end of file diff --git a/Mobile/Platforms/Android/MainApplication.cs b/Mobile/Platforms/Android/MainApplication.cs index 1ad05766..5357f0a0 100644 --- a/Mobile/Platforms/Android/MainApplication.cs +++ b/Mobile/Platforms/Android/MainApplication.cs @@ -3,6 +3,14 @@ [assembly: UsesPermission(Android.Manifest.Permission.Internet)] +// Needed for Picking photo +[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage, MaxSdkVersion = 32)] +[assembly: UsesPermission(Android.Manifest.Permission.ReadMediaImages)] + +// Needed for Taking photo +[assembly: UsesPermission(Android.Manifest.Permission.Camera)] +[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage, MaxSdkVersion = 32)] + namespace Carmen.Mobile { [Application] diff --git a/Mobile/Platforms/MacCatalyst/Info.plist b/Mobile/Platforms/MacCatalyst/Info.plist index ae01c102..c8538564 100644 --- a/Mobile/Platforms/MacCatalyst/Info.plist +++ b/Mobile/Platforms/MacCatalyst/Info.plist @@ -31,5 +31,13 @@ David Lang © 2023 ITSAppUsesNonExemptEncryption + NSCameraUsageDescription + This app needs access to the camera to take photos. + NSMicrophoneUsageDescription + This app needs access to microphone for taking videos. + NSPhotoLibraryAddUsageDescription + This app needs access to the photo gallery for picking photos and videos. + NSPhotoLibraryUsageDescription + This app needs access to photos gallery for picking photos and videos. diff --git a/Mobile/Platforms/iOS/Info.plist b/Mobile/Platforms/iOS/Info.plist index 4fdd5bac..023df02c 100644 --- a/Mobile/Platforms/iOS/Info.plist +++ b/Mobile/Platforms/iOS/Info.plist @@ -30,5 +30,13 @@ Assets.xcassets/appicon.appiconset UILaunchStoryboardName MauiSplash + NSCameraUsageDescription + This app needs access to the camera to take photos. + NSMicrophoneUsageDescription + This app needs access to microphone for taking videos. + NSPhotoLibraryAddUsageDescription + This app needs access to the photo gallery for picking photos and videos. + NSPhotoLibraryUsageDescription + This app needs access to photos gallery for picking photos and videos. diff --git a/Mobile/Popups/ListPopup.cs b/Mobile/Popups/ListPopup.cs new file mode 100644 index 00000000..d1c3f7fd --- /dev/null +++ b/Mobile/Popups/ListPopup.cs @@ -0,0 +1,33 @@ +using CommunityToolkit.Maui.Views; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Carmen.Mobile.Popups +{ + internal class ListPopup : Popup + { + public ListPopup(T[] items, Func display_getter) + { + var layout = new VerticalStackLayout + { + Spacing = 5 + }; + foreach (var item in items) + { + var button = new Button + { + Text = display_getter(item) + }; + button.Clicked += (s, e) => + { + Close(item); + }; + layout.Children.Add(button); + } + Content = layout; + } + } +} diff --git a/Mobile/Resources/Images/no_photo.png b/Mobile/Resources/Images/no_photo.png new file mode 100644 index 00000000..47156a49 Binary files /dev/null and b/Mobile/Resources/Images/no_photo.png differ diff --git a/Mobile/Views/ApplicantDetails.cs b/Mobile/Views/ApplicantDetails.cs index 739deddb..d8a4ee0f 100644 --- a/Mobile/Views/ApplicantDetails.cs +++ b/Mobile/Views/ApplicantDetails.cs @@ -1,13 +1,17 @@ using Carmen.Desktop.Converters; using Carmen.Mobile.Converters; +using Carmen.Mobile.Extensions; using Carmen.Mobile.Models; +using Carmen.Mobile.Popups; using Carmen.ShowModel; using Carmen.ShowModel.Applicants; using Carmen.ShowModel.Criterias; using Carmen.ShowModel.Structure; +using CommunityToolkit.Maui.Views; using Serilog; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -125,7 +129,7 @@ private async void ViewApplicant_Loaded(object? sender, EventArgs e) else if (await Task.Run(() => applicant.Photo) is SM.Image image) source = await ActualImage(image); else - source = null; + source = ImageSource.FromFile("no_photo.png"); model.LoadedPhoto(source); } @@ -246,6 +250,7 @@ private View GenerateSideView() activity.SetBinding(ActivityIndicator.IsVisibleProperty, new Binding(nameof(ApplicantModel.IsLoadingPhoto))); var image = new MC.Image(); image.SetBinding(MC.Image.SourceProperty, new Binding(nameof(ApplicantModel.Photo))); + image.AddTapHandler(Image_Clicked); return new Grid { image, @@ -253,6 +258,50 @@ private View GenerateSideView() }; } + private async void Image_Clicked(object? sender, EventArgs e) + { + if (model.Applicant == null) + return; + // choose how we get the image + Func> getter; + if (MediaPicker.Default.IsCaptureSupported) + { + var options = new[] + { + "Take a photo", + "Pick an existing photo" + }; + var popup = new ListPopup(options, s => s); + var result = await this.ShowPopupAsync(popup); + if (result == options[0]) + getter = MediaPicker.Default.CapturePhotoAsync; + else if (result == options[1]) + getter = MediaPicker.Default.PickPhotoAsync; + else + return; + } + else + { + getter = MediaPicker.Default.PickPhotoAsync; + } + // actually get the image + if (await getter(null) is FileResult file) + { + var photo = new SM.Image + { + Name = file.FileName + }; + using Stream source_stream = await file.OpenReadAsync(); + using (var memory_stream = new MemoryStream()) + { + source_stream.CopyTo(memory_stream); + photo.ImageData = memory_stream.ToArray(); + } + model.Applicant.Photo = photo; + model.LoadedPhoto(await ActualImage(photo)); + } + } + private async void Save_Clicked(object? sender, EventArgs e) { if (context == null)