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)