diff --git a/.gitignore b/.gitignore index beee2e0..75ade75 100644 --- a/.gitignore +++ b/.gitignore @@ -349,4 +349,7 @@ obj/ packages/ #Android Resource designer -Resource.designer.cs \ No newline at end of file +Resource.designer.cs + +#Ignore macOS .DS_Store files. +.DS_Store diff --git a/MediaGallery/MediaGallery.csproj b/MediaGallery/MediaGallery.csproj index 007b66e..ebbe9a3 100644 --- a/MediaGallery/MediaGallery.csproj +++ b/MediaGallery/MediaGallery.csproj @@ -7,7 +7,7 @@ Xamarin.MediaGallery maui, xamarin, .net6, ios, android, toolkit, xamarin.forms, media, picker, photos, videos, mediapicker This plugin is designed for picking and saving photos and video files from the native gallery of Android and iOS devices - 2.2.1 + 2.2.2-preview1 dimonovdd dimonovdd https://github.com/dimonovdd/Xamarin.MediaGallery diff --git a/MediaGallery/MediaGallery/MediaGallery.netstandard.cs b/MediaGallery/MediaGallery/MediaGallery.netstandard.cs index 566e21f..78e2790 100644 --- a/MediaGallery/MediaGallery/MediaGallery.netstandard.cs +++ b/MediaGallery/MediaGallery/MediaGallery.netstandard.cs @@ -10,13 +10,13 @@ public static partial class MediaGallery static Task> PlatformPickAsync(MediaPickRequest request, CancellationToken token) => Task.FromResult>(null); - static Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName) + static Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null) => Task.CompletedTask; - static Task PlatformSaveAsync(MediaFileType type, string filePath) + static Task PlatformSaveAsync(MediaFileType type, string filePath, string albumName = null) => Task.CompletedTask; - static Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName) + static Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null) => Task.CompletedTask; static bool PlatformCheckCapturePhotoSupport() diff --git a/MediaGallery/MediaGallery/MediaGallery.shared.cs b/MediaGallery/MediaGallery/MediaGallery.shared.cs index 8c50853..64bf129 100644 --- a/MediaGallery/MediaGallery/MediaGallery.shared.cs +++ b/MediaGallery/MediaGallery/MediaGallery.shared.cs @@ -31,38 +31,40 @@ public static async Task PickAsync(MediaPickRequest request, Ca /// Type of media file to save. /// The stream to output the file to. /// The name of the saved file including the extension. + /// The name of the album to save the file to. Null for default behaviour, empty string for no album, anything else to create/use an album. /// A task representing the asynchronous save operation. - public static async Task SaveAsync(MediaFileType type, Stream fileStream, string fileName) + public static async Task SaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null) { await CheckPossibilitySave(); + if (fileStream == null) throw new ArgumentNullException(nameof(fileStream)); CheckFileName(fileName); - await PlatformSaveAsync(type, fileStream, fileName).ConfigureAwait(false); + await PlatformSaveAsync(type, fileStream, fileName, albumName).ConfigureAwait(false); } /// A byte array to save to the file. - /// - public static async Task SaveAsync(MediaFileType type, byte[] data, string fileName) + /// + public static async Task SaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null) { await CheckPossibilitySave(); if (!(data?.Length > 0)) throw new ArgumentNullException(nameof(data)); CheckFileName(fileName); - await PlatformSaveAsync(type, data, fileName).ConfigureAwait(false); + await PlatformSaveAsync(type, data, fileName, albumName).ConfigureAwait(false); } /// Full path to a local file. - /// - public static async Task SaveAsync(MediaFileType type, string filePath) + /// + public static async Task SaveAsync(MediaFileType type, string filePath, string albumName = null) { await CheckPossibilitySave(); if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) throw new ArgumentException(nameof(filePath)); - await PlatformSaveAsync(type, filePath).ConfigureAwait(false); + await PlatformSaveAsync(type, filePath, albumName).ConfigureAwait(false); } /// Checks camera support on a device diff --git a/MediaGallery/MediaGallery/SaveMedia.android.cs b/MediaGallery/MediaGallery/SaveMedia.android.cs index a5df8d7..77c0ca2 100644 --- a/MediaGallery/MediaGallery/SaveMedia.android.cs +++ b/MediaGallery/MediaGallery/SaveMedia.android.cs @@ -14,21 +14,24 @@ namespace NativeMedia { public static partial class MediaGallery { - static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName) + static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null) { using var ms = new MemoryStream(data); - await PlatformSaveAsync(type, ms, fileName).ConfigureAwait(false); + await PlatformSaveAsync(type, ms, fileName, albumName).ConfigureAwait(false); } - static async Task PlatformSaveAsync(MediaFileType type, string filePath) + static async Task PlatformSaveAsync(MediaFileType type, string filePath, string albumName = null) { using var fileStream = System.IO.File.OpenRead(filePath); - await PlatformSaveAsync(type, fileStream, Path.GetFileName(filePath)).ConfigureAwait(false); + await PlatformSaveAsync(type, fileStream, Path.GetFileName(filePath), albumName).ConfigureAwait(false); } - static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName) + static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null) { - var albumName = AppInfo.Name; + if (albumName == null) + { + albumName = AppInfo.Name; + } var context = Platform.AppActivity; var dateTimeNow = DateTime.Now; diff --git a/MediaGallery/MediaGallery/SaveMedia.ios.cs b/MediaGallery/MediaGallery/SaveMedia.ios.cs index 33c23d1..4f98f3d 100644 --- a/MediaGallery/MediaGallery/SaveMedia.ios.cs +++ b/MediaGallery/MediaGallery/SaveMedia.ios.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; +using System.Xml; using Foundation; using Photos; @@ -8,7 +9,7 @@ namespace NativeMedia { public static partial class MediaGallery { - static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName) + static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string fileName, string albumName = null) { string filePath = null; @@ -17,7 +18,7 @@ static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string file filePath = GetFilePath(fileName); await File.WriteAllBytesAsync(filePath, data); - await PlatformSaveAsync(type, filePath).ConfigureAwait(false); + await PlatformSaveAsync(type, filePath, albumName).ConfigureAwait(false); } finally { @@ -25,7 +26,7 @@ static async Task PlatformSaveAsync(MediaFileType type, byte[] data, string file } } - static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName) + static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, string fileName, string albumName = null) { string filePath = null; @@ -36,7 +37,7 @@ static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, strin await fileStream.CopyToAsync(stream); stream.Close(); - await PlatformSaveAsync(type, filePath).ConfigureAwait(false); + await PlatformSaveAsync(type, filePath, albumName).ConfigureAwait(false); } finally { @@ -44,18 +45,80 @@ static async Task PlatformSaveAsync(MediaFileType type, Stream fileStream, strin } } - static async Task PlatformSaveAsync(MediaFileType type, string filePath) + static async Task PlatformSaveAsync(MediaFileType type, string filePath, string albumName = null) { using var fileUri = new NSUrl(filePath); + + PHAssetCollection collection = null; + // If albumName is null we do what we always used to do and not create an album. + // If albumName is an empty string we don't wish to create an album (which is the same as null for iOS) + // Otherwise we wish to create an album. + if (string.IsNullOrEmpty(albumName) == false) + { + // Fetch album. + var fetchOptions = new PHFetchOptions() + { + Predicate = NSPredicate.FromFormat($"title=\"{albumName}\"") + }; + #if __NET6__ + collection = PHAssetCollection.FetchAssetCollections(PHAssetCollectionType.Album, PHAssetCollectionSubtype.AlbumRegular, fetchOptions).firstObject as PHAssetCollection; + #else + collection = PHAssetCollection.FetchAssetCollections(PHAssetCollectionType.Album, PHAssetCollectionSubtype.AlbumRegular, fetchOptions).FirstObject as PHAssetCollection; + #endif + + // Album does not exist, create it. + if (collection == null) + { + collection = await PhotoLibraryCreateAlbum(albumName).ConfigureAwait(false); + } + } await PhotoLibraryPerformChanges(() => { using var request = type == MediaFileType.Video ? PHAssetChangeRequest.FromVideo(fileUri) : PHAssetChangeRequest.FromImage(fileUri); + + // If we have a collection we should put the asset into it. + if (collection != null) + { + var assetCollectionChangeRequest = PHAssetCollectionChangeRequest.ChangeRequest(collection); + assetCollectionChangeRequest.AddAssets(new PHObject[] { request.PlaceholderForCreatedAsset }); + } + }).ConfigureAwait(false); } + static async Task PhotoLibraryCreateAlbum(string albumName) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + PHObjectPlaceholder placeholderForCreatedAssetCollection = null; + PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() => + { + var createAlbum = PHAssetCollectionChangeRequest.CreateAssetCollection(albumName); + placeholderForCreatedAssetCollection = createAlbum.PlaceholderForCreatedAssetCollection; + }, (bool success, NSError error) => + { + if (success) + { + var collectionFetchResult = PHAssetCollection.FetchAssetCollections(new string[] { placeholderForCreatedAssetCollection.LocalIdentifier }, null); + #if __NET6__ + var newCollection = collectionFetchResult.firstObject as PHAssetCollection; + #else + var newCollection = collectionFetchResult.FirstObject as PHAssetCollection; + #endif + tcs.TrySetResult(newCollection); + } + else + { + tcs.TrySetResult(null); + } + }); + + return await tcs.Task; + } + static async Task PhotoLibraryPerformChanges(Action action) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/Permission/Xamarim.MediaGallery.Permission.targets b/Permission/Xamarim.MediaGallery.Permission.targets index 10d2556..e40a6e9 100644 --- a/Permission/Xamarim.MediaGallery.Permission.targets +++ b/Permission/Xamarim.MediaGallery.Permission.targets @@ -3,7 +3,7 @@ maui, xamarin, .net6, ios, android, toolkit, xamarin.forms, media, picker, photos, videos, mediapicker This plugin is designed for picking and saving photos and video files from the native gallery of Android and iOS devices - 2.2.1 + 2.2.2-preview1 dimonovdd dimonovdd https://github.com/dimonovdd/Xamarin.MediaGallery diff --git a/README.md b/README.md index 82baa66..f6f3ff8 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,10 @@ await MediaGallery.SaveAsync(MediaFileType.Video, filePath); await MediaGallery.SaveAsync(MediaFileType.Image, stream, fileName); +//OR You can also save to a specific album + +await MediaGallery.SaveAsync(MediaFileType.Image, stream, fileName, "MyAppsAlbum"); + //The name or the path to the saved file must contain the extension. //... diff --git a/Sample/Common/src/ViewModels/SaveVM.cs b/Sample/Common/src/ViewModels/SaveVM.cs index 4b54593..eb7720b 100644 --- a/Sample/Common/src/ViewModels/SaveVM.cs +++ b/Sample/Common/src/ViewModels/SaveVM.cs @@ -41,6 +41,8 @@ public SaveVM() public ICommand SaveVideoCommand { get; } + public string AlbumName { get; set; } + async void Save(MediaFileType type, string name) { @@ -57,22 +59,29 @@ async void Save(MediaFileType type, string name) { using var stream = EmbeddedResourceProvider.Load(name); + // Note on the albumName parameter. + // If the albumName parameter is an empty string no album will be created but it will be just saved into photos/gallery. + // If the albumName parameter is a string then an album by that name will created and/or used. + // If the albumName parameter is null we use the behaviour from Xamarin.MediaGallery v2.2.1 (app name as album for Android, no album for iOS) + // In this sample we do not allow it to be null. + var albumName = AlbumName ?? String.Empty; + if (FromStream) { - await MediaGallery.SaveAsync(type, stream, name); + await MediaGallery.SaveAsync(type, stream, name, albumName); } else if (FromByteArray) { using var memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); - await MediaGallery.SaveAsync(type, memoryStream.ToArray(), name); + await MediaGallery.SaveAsync(type, memoryStream.ToArray(), name, albumName); } else if (FromCacheDirectory) { var filePath = await FilesHelper.SaveToCacheAsync(stream, name); - await MediaGallery.SaveAsync(type, filePath); + await MediaGallery.SaveAsync(type, filePath, albumName); } await DisplayAlertAsync("Save Completed Successfully"); diff --git a/Sample/MAUI/Views/SavePage.xaml b/Sample/MAUI/Views/SavePage.xaml index 0eeb06c..45d5943 100644 --- a/Sample/MAUI/Views/SavePage.xaml +++ b/Sample/MAUI/Views/SavePage.xaml @@ -20,6 +20,10 @@ + + diff --git a/Sample/Xamarin/Sample/Views/SavePage.xaml b/Sample/Xamarin/Sample/Views/SavePage.xaml index f3615c7..911923f 100644 --- a/Sample/Xamarin/Sample/Views/SavePage.xaml +++ b/Sample/Xamarin/Sample/Views/SavePage.xaml @@ -20,6 +20,10 @@ + + diff --git a/Xamarim.MediaGallery.targets b/Xamarim.MediaGallery.targets index f1c4ec6..f295078 100644 --- a/Xamarim.MediaGallery.targets +++ b/Xamarim.MediaGallery.targets @@ -3,8 +3,8 @@ <_UseNuget>true <_UseNuget Condition="'$(Configuration)'=='Release'">true - <_LibVersion>2.2.1 - <_PermissionLibVersion>2.2.1 + <_LibVersion>2.2.2-preview1 + <_PermissionLibVersion>2.2.2-preview1 <_IsSample Condition="'$(_IsSample)'!='false'">true <_NET6 Condition=" $(TargetFramework.StartsWith('net6')) ">true <_IsMobele Condition=" $(TargetFramework.Contains('droid')) OR $(TargetFramework.ToLowerInvariant().Contains('ios')) ">true