Skip to content

feat: MediaGallery #1146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions doc/helpers/mediagallery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
uid: Toolkit.Helpers.MediaGallery
---
# MediaGallery

## Summary

`MediaGallery` is a static class that allows interaction with the device's media gallery, providing methods to check access permissions and save media files.

## Remarks

This class is designed to work on iOS, Mac Catalyst and Android platforms, utilizing platform-specific implementations for its methods.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This class is designed to work on iOS, Mac Catalyst and Android platforms, utilizing platform-specific implementations for its methods.
This class is designed to work on iOS, Mac Catalyst, and Android platforms, utilizing platform-specific implementations for its methods.


The API allows setting the `targetFileName`, which **should ideally be unique** - otherwise the OS will either overwrite an existing file with the same name (Android behavior), or generate a new name instead (iOS behavior).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The API allows setting the `targetFileName`, which **should ideally be unique** - otherwise the OS will either overwrite an existing file with the same name (Android behavior), or generate a new name instead (iOS behavior).
The API allows setting the `targetFileName`, which **should ideally be unique** - otherwise the OS will either overwrite an existing file with the same name (Android behavior) or generate a new name instead (iOS behavior).


## Methods

### CheckAccessAsync

```csharp
public static async Task<bool> CheckAccessAsync()
```

#### Summary

Checks the user's permission to access the device's gallery. Will trigger the permission request if not already granted.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Checks the user's permission to access the device's gallery. Will trigger the permission request if not already granted.
Checks the user's permission to access the device's gallery. It will trigger the permission request if it has not already been granted.


#### Returns

A `Task<bool>` that completes with `true` if access is granted, and `false` otherwise.

### SaveAsync (Stream)

```csharp
public static async Task SaveAsync(MediaFileType type, Stream stream, string targetFileName)
```

#### Summary

Saves a media file to the device's gallery using a stream.

#### Parameters

- `MediaFileType type`: The type of the media file (e.g., image, video).
- `Stream stream`: A stream representing the media file.
- `string targetFileName`: The desired file name for the saved media.

#### Returns

A `Task` that completes when the save operation is finished.

### SaveAsync (byte array)

```csharp
public static async Task SaveAsync(MediaFileType type, byte[] data, string targetFileName)
```

#### Summary

Saves a media file to the device's gallery using a byte array.

#### Parameters

- `MediaFileType type`: The type of the media file (e.g., image, video).
- `byte[] data`: A byte array representing the media file.
- `string targetFileName`: The desired file name for the saved media.

#### Returns

A `Task` that completes when the save operation is finished.

## Permissions

### Android

If your app supports only Android 10 and newer, no manifest changes are required. If you support earlier versions of Android, add the following into your manifest:

```xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```

### iOS & Mac Catalyst

If your app only supports iOS 14 and newer, update your `Info.plist` as follows:

```xml
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs access to the photo gallery for saving photos and videos</string>
```

If you want to support earlier versions iOS, add the following as well:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you want to support earlier versions iOS, add the following as well:
If you want to support earlier versions of iOS, add the following as well:


```xml
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to the photo gallery for saving photos and videos</string>
```

## Usage

Make sure to wrap the usage of `MediaGallery` in a `#if __ANDROID__ || __IOS__` ... `#endif` block, as the class is only available on these targets.

### Checking for gallery access

```csharp
#if __ANDROID__ || __IOS__
bool hasAccess = await MediaGallery.CheckAccessAsync();
#endif
```

### Saving an image to the gallery using a byte array

```csharp
#if __ANDROID__ || __IOS__
byte[] imageData = ...; // Image data
await MediaGallery.SaveAsync(MediaFileType.Image, imageData, "MyImage.jpg");
#endif
```

### Saving a video to the gallery using a stream

```csharp
#if __ANDROID__ || __IOS__
using Stream videoStream = ...; // Video stream
await MediaGallery.SaveAsync(MediaFileType.Video, videoStream, "MyVideo.mp4");
#endif
```

### Copying an application package file to gallery
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### Copying an application package file to gallery
### Copying an application package file to the gallery


```csharp
#if __ANDROID__ || __IOS__
if (await MediaGallery.CheckAccessAsync())
{
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/UnoLogo.png", UriKind.Absolute));
using var stream = await file.OpenStreamForReadAsync();
await MediaGallery.SaveAsync(MediaFileType.Image, stream, "UnoLogo.png");
}
else
{
await new ContentDialog
{
Title = "Permission required",
Content = "The app requires access to the device's gallery to save the image.",
CloseButtonText = "OK",
XamlRoot = XamlRoot
}.ShowAsync();
}
#endif
```
2 changes: 2 additions & 0 deletions doc/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
href: helpers/input-extensions.md
- name: ItemsRepeater Extensions
href: helpers/itemsrepeater-extensions.md
- name: MediaGallery
href: helpers/mediagallery.md
- name: Progress Extensions
href: helpers/progress-extensions.md
- name: Responsive markup extension
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Page
x:Class="Uno.Toolkit.Samples.Content.Helpers.MediaGalleryHelperSamplePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Uno.Toolkit.Samples.Content.Helpers"
xmlns:local="using:Uno.Toolkit.Samples.Content.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sample="using:Uno.Toolkit.Samples"
xmlns:utu="using:Uno.Toolkit.UI"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<sample:SamplePageLayout IsDesignAgnostic="True">
<sample:SamplePageLayout.DesignAgnosticTemplate>
<DataTemplate>
<StackPanel>
<Button Command="{Binding Data.CheckAccessCommand}">Check access</Button>
<Button Command="{Binding Data.SaveCommand}">Save UnoLogo.png to gallery</Button>
<Button Command="{Binding Data.SaveRandomNameCommand}">Save with random name to gallery</Button>
</StackPanel>
</DataTemplate>
</sample:SamplePageLayout.DesignAgnosticTemplate>
</sample:SamplePageLayout>
</Grid>
</Page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using Uno.Toolkit.Samples.Entities;
using Uno.Toolkit.Samples.Helpers;
using Uno.Toolkit.Samples.ViewModels;
using Uno.Toolkit.UI;
using Windows.Foundation;
using Windows.Foundation.Collections;
using System.Windows.Input;
using System.Net.WebSockets;
using Windows.Storage;

#if IS_WINUI
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
#else
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
#endif

namespace Uno.Toolkit.Samples.Content.Helpers;

#if __ANDROID__ || __IOS__
[SamplePage(SampleCategory.Helpers, "MediaGalleryHelper", SourceSdk.Uno, IconSymbol = Symbol.BrowsePhotos, DataType = typeof(MediaGalleryHelperSampleVM))]
#endif
public sealed partial class MediaGalleryHelperSamplePage : Page
{
public MediaGalleryHelperSamplePage()
{
this.InitializeComponent();
this.Loaded += (s, e) =>
{
if ((DataContext as Sample)?.Data is MediaGalleryHelperSampleVM vm)
{
vm.XamlRoot = this.XamlRoot;
}
};
}
}

public class MediaGalleryHelperSampleVM : ViewModelBase
{
public XamlRoot XamlRoot { get; set; }

#if __ANDROID__ || __IOS__
public ICommand CheckAccessCommand => new Command(async (_) =>
{
var success = await MediaGallery.CheckAccessAsync();
await new ContentDialog
{
Title = "Permission check",
Content = $"Access {(success ? "granted" : "denied")}.",
CloseButtonText = "OK",
XamlRoot = XamlRoot
}.ShowAsync();
});

public ICommand SaveCommand => new Command(async (_) =>
{
if (await MediaGallery.CheckAccessAsync())
{
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/UnoLogo.png", UriKind.Absolute));
using var stream = await file.OpenStreamForReadAsync();
await MediaGallery.SaveAsync(MediaFileType.Image, stream, "UnoLogo.png");
}
else
{
await new ContentDialog
{
Title = "Permission required",
Content = "The app requires access to the device's gallery to save the image.",
CloseButtonText = "OK",
XamlRoot = XamlRoot
}.ShowAsync();
}
});

public ICommand SaveRandomNameCommand => new Command(async (_) =>
{
if (await MediaGallery.CheckAccessAsync())
{
var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/UnoLogo.png", UriKind.Absolute));
using var stream = await file.OpenStreamForReadAsync();

var fileName = Guid.NewGuid() + ".png";
await MediaGallery.SaveAsync(MediaFileType.Image, stream, fileName);
}
else
{
await new ContentDialog
{
Title = "Permission required",
Content = "The app requires access to the device's gallery to save the image.",
CloseButtonText = "OK",
XamlRoot = XamlRoot
}.ShowAsync();
}
});
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Content\Helpers\BindingExtensionsSamplePage.xaml.cs">
<DependentUpon>BindingExtensionsSamplePage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Content\Helpers\MediaGalleryHelperSamplePage.xaml.cs">
<DependentUpon>MediaGalleryHelperSamplePage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Content\Helpers\ResponsiveExtensionsSamplePage.xaml.cs">
<DependentUpon>ResponsiveExtensionsSamplePage.xaml</DependentUpon>
</Compile>
Expand Down Expand Up @@ -316,6 +319,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Content\Helpers\MediaGalleryHelperSamplePage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Content\Helpers\ResponsiveExtensionsSamplePage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/iconapp.appiconset</string>

<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app would like to save photos to your gallery</string>
<!--
Adjust this to your application's encryption usage.
<key>ITSAppUsesNonExemptEncryption</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
<string>Assets.xcassets/iconapp.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>

<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app would like to save photos to your gallery</string>
<!--
Adjust this to your application's encryption usage.
<key>ITSAppUsesNonExemptEncryption</key>
Expand Down
2 changes: 2 additions & 0 deletions src/Uno.Toolkit.RuntimeTests/Tests/NavigationBarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ public async Task MainCommand_Works_From_Code_Init()
await UnitTestsUIContentHelper.WaitForIdle();

var page = frame.Content as LabelTitlePage;
#if HAS_UNO
page?.FindChild<NavigationBar>()?.MainCommand.RaiseClick();
#endif

Assert.IsTrue(success);
}
Expand Down
19 changes: 19 additions & 0 deletions src/Uno.Toolkit.UI/Helpers/MediaGallery/MediaFileType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#if __IOS__ || __ANDROID__
namespace Uno.Toolkit.UI;

/// <summary>
/// Represents a media file type.
/// </summary>
public enum MediaFileType
{
/// <summary>
/// Image media file type.
/// </summary>
Image,

/// <summary>
/// Video media file type.
/// </summary>
Video,
}
#endif
Loading
Loading