Развитие системы растений

This commit is contained in:
ThomasCharester committed 2026-03-02 18:25:46 +03:00
1 parent 1b578067f3
commit ba49ef5015
218 files changed
+70415 -1519

No files matched your search

@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 61413ea24ac5d754f9bfe1eff712957a
m_currentHash:
serializedVersion: 2
Hash: cbf960f9f88de008b8321897911b184c
Hash: 90049ca0b13b395f1730f0a26f18f452
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0
@@ -24,11 +24,11 @@ MonoBehaviour:
m_InternalBundleIdMode: 1
m_AssetLoadMode: 0
m_BundledAssetProviderType:
m_AssemblyName:
m_ClassName:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider
m_AssetBundleProviderType:
m_AssemblyName:
m_ClassName:
m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider
m_IgnoreUnsupportedFilesInBuild: 0
m_UniqueBundleIds: 0
m_EnableJsonCatalog: 0
@@ -106,6 +106,7 @@ MonoBehaviour:
- UIPanelConfig
- SceneConfig
- LayerMaskConfig
- ConstantImageConfig
m_SchemaTemplates: []
m_GroupTemplateObjects:
- {fileID: 11400000, guid: 73eda305665711b4cafd86da588a0749, type: 2}
@@ -15,6 +15,12 @@ MonoBehaviour:
m_GroupName: Default Local Group
m_GUID: 61413ea24ac5d754f9bfe1eff712957a
m_SerializeEntries:
- m_GUID: 057464e7ebb719a498ba74eaf05d79c4
m_Address: Assets/Resources_moved/Configs/UI/LogoPanel.asset
m_ReadOnly: 0
m_SerializedLabels:
- UIPanelConfig
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 17c5e56eb6c99db40b6093c9d543bb4f
m_Address: Assets/Resources_moved/Configs/UI/CanvasSettingsConfig.asset
m_ReadOnly: 0
@@ -30,17 +36,44 @@ MonoBehaviour:
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 34c615d91f8c753d1af79f0bc394d317
m_Address: Assets/Resources_moved/UI/PlantAdd.prefab
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4a6908602eab38258aeb79ae3c499a8a
m_Address: Assets/Resources_moved/UI/Loading.prefab
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4bf97e6c7f046f849912182e4ded6c9e
m_Address: Assets/Resources_moved/Configs/Audio/Settings/Full3D.asset
m_ReadOnly: 0
m_SerializedLabels:
- AudioSettingsConfig
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 782c95a3b9a7e71eebcf593e1fdc9c30
m_Address: Assets/Resources_moved/UI/Logo.prefab
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d3cf10166e7fbeb3586d613033c78618
m_Address: Assets/Resources_moved/Configs/UI/AddPlant.asset
m_ReadOnly: 0
m_SerializedLabels:
- UIPanelConfig
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: d7dd69cfadda3d6e18ec390724be0d80
m_Address: Assets/Resources_moved/Configs/Audio/AudioSourcePrefabConfig.asset
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: dab114367c8e14ccc96c193b6ac77c94
m_Address: Assets/Resources_moved/Configs/UI/LoadingPanel.asset
m_ReadOnly: 0
m_SerializedLabels:
- UIPanelConfig
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: e1fbe5b2798657342b569f723d16c9de
m_Address: Assets/Resources_moved/UI/Canvas.prefab
m_ReadOnly: 0
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 98b04a1dab924594a3bb3f93d863cc02
timeCreated: 1772122466
@@ -0,0 +1,18 @@
using Code.Infrastructure.Generators;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Calendar.Constants
{
[Config("Configs/ImageConfig", true)]
public class ConstantImage
{
[ToConfig] public AssetReference image;
[ToConfig] public ConstantImageType imageType;
}
public enum ConstantImageType
{
NoPhoto
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 188200b6891f44d993888cc4c68a3bea
timeCreated: 1772122627
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Linq;
using Code.Infrastructure.Configs;
using Code.Infrastructure.EventBus;
using Cysharp.Threading.Tasks;
using UnityEngine.AddressableAssets;
namespace Code.Calendar.Constants
{
// Написать кодрайтер, который будет находить конфиги и добавлять их сюда пока написано само
public class ConstantImagesService
{
private readonly EventBus _eventBus;
public List<ConstantImageConfig> Configs { get; private set; }
public ConstantImagesService(EventBus eventBus, IConfigLocator configLocator)
{
_eventBus = eventBus;
var label = configLocator.TryGetManyConfigAssetReferences<ConstantImageConfig>();
LoadImage(label).Forget();
}
private async UniTaskVoid LoadImage(string imagesLabel)
{
if ((await Addressables.LoadResourceLocationsAsync(imagesLabel).Task).Count != 0)
{
var images =
Addressables.LoadAssetsAsync<ConstantImageConfig>(imagesLabel, null, Addressables.MergeMode.Union);
await images.ToUniTask();
Configs = images.Result.ToList();
}
_eventBus.RaiseEvent<IConstantImagesServiceHandle>(x=> x.ImagesLoaded());
}
}
public interface IConstantImagesServiceHandle : IGlobalSubscriber
{
void ImagesLoaded();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e08f396c88804dac9946217cd9474bfa
timeCreated: 1772122482
@@ -3,19 +3,38 @@ using System.Collections.Generic;
namespace Code.Calendar.DataStructures
{
public struct Plant
public struct Plant : IEquatable<Plant>
{
public string Name;
public string IconPath;
public int Id;
public Guid Id;
public bool Equals(Plant other) => Id == other.Id;
public override bool Equals(object obj) => obj is Plant other && Equals(other);
public override int GetHashCode() => Id.GetHashCode();
}
public struct Task
public struct Task : IEquatable<Task>
{
public Guid Id;
public bool Active;
public string Description;
public DateTime Date;
public bool IsPeriodic;
public int PeriodicInterval;
public int PlantId;
public DateTime PeriodicUntil;
public Guid PlantId;
public List<string> PhotoPaths;
public bool Equals(Task other) => Id == other.Id;
public override bool Equals(object obj) => obj is Task other && Equals(other);
public override int GetHashCode() => Id.GetHashCode();
}
}
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Code.Infrastructure.EventBus;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
namespace Code.Calendar.DataStructures
{
public interface IPlantService
{
void AddPlant(Plant plant);
void DeletePlant(Plant plant);
Plant GetPlant(Guid plantId);
List<Plant> GetPlants();
void EditPlant(Guid guid, Plant newPlant);
}
public class PlantService : IDisposable, IPlantService
{
private readonly EventBus _eventBus;
private List<Plant> _plants;
public PlantService(EventBus eventBus)
{
_eventBus = eventBus;
LoadJsonTasks().Forget();
}
private async UniTaskVoid LoadJsonTasks()
{
var path = Path.Combine(Application.persistentDataPath, "Plants.json");
if (File.Exists(path))
{
using StreamReader reader = new(path);
_plants = JsonConvert.DeserializeObject<List<Plant>>(await reader.ReadToEndAsync()) ?? new List<Plant>();
}
else
{
await using (File.Create(path)) { }
}
_eventBus.RaiseEvent<IPlantsServiceInitHandle>(x=> x.PlantsLoaded());
}
private bool _inAction, _reUseRequest;
private async UniTaskVoid SaveJsonTasks()
{
if (_inAction)
{
_reUseRequest = true;
return;
}
_inAction = true;
var json = JsonConvert.SerializeObject(_plants, Formatting.Indented);
await using StreamWriter writer = new(Path.Combine(Application.persistentDataPath, "Plants.json"));
await writer.WriteAsync(json).AsUniTask();
_inAction = false;
if(!_reUseRequest) _eventBus.RaiseEvent<IPlantsServiceInitHandle>(x=> x.PlantsSaved());
else SaveJsonTasks().Forget();
}
public void AddPlant(Plant plant)
{
plant.Id = Guid.NewGuid();
_plants.Add(plant);
SaveJsonTasks().Forget();
}
public void EditPlant(Guid guid, Plant newPlant)
{
_plants[_plants.FindIndex(x => x.Id == guid)] = newPlant;
SaveJsonTasks().Forget();
}
public void DeletePlant(Plant plant)
{
_plants.Remove(plant);
SaveJsonTasks().Forget();
}
public Plant GetPlant(Guid plantId) => _plants.FirstOrDefault(x => x.Id == plantId);
public List<Plant> GetPlants() => _plants;
public void Dispose()
{
SaveJsonTasks().Forget();
}
}
public interface IPlantsServiceInitHandle : IGlobalSubscriber
{
void PlantsLoaded();
void PlantsSaved();
}
public interface IPlantsServiceHandle : IGlobalSubscriber
{
void PlantAdded();
void PlantRemoved();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: eb3e195da68845ea872e2eff73582745
timeCreated: 1772116279
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Code.Infrastructure.EventBus;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
namespace Code.Calendar.DataStructures
{
public interface ITaskService
{
void AddTask(Task task);
void DeleteTask(Task task);
List<Task> GetByPlant(Guid plantId);
List<Task> GetTasks();
List<Task> GetByDate(DateTime date);
void EditTask(Guid guid, Task newTask);
}
public class TaskService : IDisposable, ITaskService
{
private readonly EventBus _eventBus;
private List<Task> _tasks;
public TaskService(EventBus eventBus)
{
_eventBus = eventBus;
LoadJsonTasks().Forget();
}
private async UniTaskVoid LoadJsonTasks()
{
var path = Path.Combine(Application.persistentDataPath, "Tasks.json");
if (File.Exists(path))
{
using StreamReader reader = new(path);
_tasks = JsonConvert.DeserializeObject<List<Task>>(await reader.ReadToEndAsync()) ?? new List<Task>();
}
else
{
await using (File.Create(path)) { }
}
_eventBus.RaiseEvent<ITaskServiceHandle>(x => x.TasksLoaded());
}
private bool _inAction, _reUseRequest;
private async UniTaskVoid SaveJsonTasks()
{
if (_inAction)
{
_reUseRequest = true;
return;
}
_inAction = true;
var json = JsonConvert.SerializeObject(_tasks, Formatting.Indented);
await using StreamWriter writer = new(Path.Combine(Application.persistentDataPath, "Tasks.json"));
await writer.WriteAsync(json).AsUniTask();
_inAction = false;
if (!_reUseRequest) _eventBus.RaiseEvent<ITaskServiceHandle>(x => x.TasksSaved());
else SaveJsonTasks().Forget();
}
public void AddTask(Task task)
{
_tasks.Add(task);
task.Id = Guid.NewGuid();
SaveJsonTasks().Forget();
}
public void EditTask(Guid guid, Task newTask)
{
_tasks[_tasks.FindIndex(x => x.Id == guid)] = newTask;
SaveJsonTasks().Forget();
}
public void DeleteTask(Task task)
{
_tasks.Remove(task);
SaveJsonTasks().Forget();
}
public List<Task> GetByPlant(Guid plantId) => _tasks.Where(x => x.PlantId == plantId).ToList();
public List<Task> GetByDate(DateTime date)
{
var byDay = _tasks
.Where(x => x.Date.Day == date.Day)
.ToList();
var periodicOnes = _tasks
.Where(x => x.IsPeriodic && x.Date.Day + new TimeSpan(0, x.PeriodicInterval, 0).Days == date.Day)
.ToList();
byDay.AddRange(periodicOnes);
return byDay;
}
public List<Task> GetTasks() => _tasks;
public void Dispose()
{
SaveJsonTasks().Forget();
}
}
public interface ITaskServiceHandle : IGlobalSubscriber
{
void TasksLoaded();
void TasksSaved();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e0a70b7566234451ad60128860af9517
timeCreated: 1772113905
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 073ac04536184a2b9b164161a2f41a61
timeCreated: 1772119626
@@ -0,0 +1,22 @@
using System;
using Code.Infrastructure.UI;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace Code.Calendar.Panels
{
public class LoadingPanel : UIPanel
{
[SerializeField] private Image loadingScreen;
public override void Open(Action callback = null)
{
Tween.Alpha(loadingScreen, 1f, 1f).OnComplete(target: this, _ => callback?.Invoke());
}
public override void Close(Action callback = null)
{
Tween.Alpha(loadingScreen, 0f, 1f).OnComplete(target: this, _ => callback?.Invoke());
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4466c13a031a42498f42c15baf25ec03
timeCreated: 1772123986
+22
View File
@@ -0,0 +1,22 @@
using System;
using Code.Infrastructure.UI;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace Code.Calendar.Panels
{
public class LogoPanel : UIPanel
{
[SerializeField] private Image logo;
public override void Open(Action callback = null)
{
Tween.Alpha(logo, 1f, 1f).OnComplete(target: this, _ => callback?.Invoke());
}
public override void Close(Action callback = null)
{
Tween.Alpha(logo, 0f, 1f).OnComplete(target: this, _ => callback?.Invoke());
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 03f95468478e4025944889f8b4a54798
timeCreated: 1772123906
@@ -0,0 +1,19 @@
using System;
using Code.Infrastructure.UI;
namespace Code.Calendar.Panels
{
public class PictureChooseMenu : UIPanel
{
public override void Open(Action callback = null)
{
}
public override void Close(Action callback = null)
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 46e10e555bb5495899af512610cde913
timeCreated: 1772119767
@@ -0,0 +1,43 @@
using System;
using Code.Calendar.Constants;
using Code.Calendar.DataStructures;
using Code.Infrastructure.UI;
using PrimeTween;
using Reflex.Attributes;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Code.Calendar.Panels
{
public class PlantAddPanel : UIPanel
{
[Inject] private IPlantService _plantService;
[SerializeField] private TMP_InputField plantName;
[SerializeField] private Image image;
public void TryGetIcon()
{
}
public void TryAddPlant()
{
Plant newPlant = new()
{
Name = plantName.text,
IconPath = null
};
_plantService.AddPlant(newPlant);
}
public override void Open(Action callback = null)
{
Tween.Alpha(image, 1f, 1f).OnComplete(target: this, _ => callback?.Invoke());
}
public override void Close(Action callback = null)
{
Tween.Alpha(image, 0f, 1f).OnComplete(target: this, _ => callback?.Invoke());
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2b11bfff50fe458c8799204f39f395ce
timeCreated: 1772119636
@@ -7,10 +7,7 @@ namespace Code.Infrastructure.App
{
public enum AppStates
{
Bootstrap,
Gameplay,
MainMenu,
Platformer
Bootstrap
}
public class AppStateMachineContext
+46 -13
View File
@@ -1,3 +1,5 @@
using Code.Calendar.Constants;
using Code.Calendar.DataStructures;
using Code.Infrastructure.Audio;
using Code.Infrastructure.Configs;
using Code.Infrastructure.Input;
@@ -8,7 +10,6 @@ using Reflex.Core;
using Reflex.Enums;
using Reflex.Injectors;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using Resolution = Reflex.Enums.Resolution;
namespace Code.Infrastructure.App
@@ -20,7 +21,10 @@ namespace Code.Infrastructure.App
IUIPanelLoaded,
IAudioSourcePoolInitializedHandle,
IAudioConfigsLoadedHandle,
ILayerMasksInitializedHandle
ILayerMasksInitializedHandle,
ITaskServiceHandle,
IPlantsServiceInitHandle,
IConstantImagesServiceHandle
{
public void InstallBindings(ContainerBuilder containerBuilder)
{
@@ -50,18 +54,29 @@ namespace Code.Infrastructure.App
Resolution.Eager);
containerBuilder.RegisterType(typeof(AudioService), new[] { typeof(IAudioService) }, Lifetime.Singleton,
Resolution.Eager);
containerBuilder.RegisterType(typeof(TaskService), new[] { typeof(ITaskService) }, Lifetime.Singleton,
Resolution.Eager);
containerBuilder.RegisterType(typeof(PlantService), new[] { typeof(IPlantService) }, Lifetime.Singleton,
Resolution.Eager);
containerBuilder.RegisterType(typeof(ConstantImagesService), Lifetime.Singleton,
Resolution.Eager);
}
private void InitLoadings()
{
// UIFactory
// UIFactory =
// Logo
// Scene configs
// Input service
// Audio Factory
// Audio Pool
// Scene configs =
// Input service
// Audio Factory =
// Audio Pool =
// Layer masks
_initialLoading = new Loading(7, () => EssentialLoadEnded(_thatRightContainer));
// Tasks =
// Plants =
// CIS =
_initialLoading = new Loading(10, () => EssentialLoadEnded(_thatRightContainer));
}
@@ -81,9 +96,7 @@ namespace Code.Infrastructure.App
private void EssentialLoadEnded(Container container)
{
container.Resolve<EventBus.EventBus>().Unsubscribe(this);
container.Resolve<AppStateMachine>().SwitchState(AppStates.Bootstrap)
.AllowStateTransition(AppStates.Gameplay)
.AllowStateTransition(AppStates.Platformer);
container.Resolve<AppStateMachine>().SwitchState(AppStates.Bootstrap);
container.Resolve<AppStateMachineContext>().StateMachine = container.Resolve<AppStateMachine>();
}
@@ -96,8 +109,7 @@ namespace Code.Infrastructure.App
{
_initialLoading.DoProgress();
}
public void UIInitialized()
{
_initialLoading.DoProgress();
@@ -138,5 +150,26 @@ namespace Code.Infrastructure.App
{
_initialLoading.DoProgress();
}
public void TasksLoaded()
{
_initialLoading.DoProgress();
}
public void TasksSaved()
{ }
public void PlantsLoaded()
{
_initialLoading.DoProgress();
}
public void PlantsSaved()
{ }
public void ImagesLoaded()
{
_initialLoading.DoProgress();
}
}
}
@@ -21,7 +21,8 @@ namespace Code.Infrastructure.App.States
_eventBus.Subscribe(this);
Context.UIService.TryRemovePanel(UIPanelType.Logo);
Context.UIService.TryRemovePanel(UIPanelType.Loading);
Context.UIService.TryLoadPanel(UIPanelType.PlantAdd);
// Context.AudioFactory.LoadAudioClips(AudioNames.UISounds);
}
@@ -33,9 +34,9 @@ namespace Code.Infrastructure.App.States
public void UILoaded(UIPanelType uiPanelType)
{
// if (uiPanelType != UIPanelType.DebugSceneChooser) return;
//
// Context.UIService.OpenPanel(uiPanelType);
if (uiPanelType != UIPanelType.PlantAdd) return;
Context.UIService.OpenPanel(uiPanelType);
}
public void OnReadyToPlay(AudioNames groupName)
{
@@ -29,7 +29,7 @@ namespace Code.Infrastructure.Audio
{
_eventBus = eventBus;
_label = configLocator.TryGetManyConfigAssetReferences<AudioInfoConfig>();
for (var i = 0; i < typeof(AudioNames).GetEnumValues().Length; i++)
{
_audioInfos.Add((AudioNames)i, new Dictionary<int, AssetReference>());
@@ -38,15 +38,17 @@ namespace Code.Infrastructure.Audio
LoadConfigs().Forget();
}
private async UniTaskVoid LoadConfigs()
{
_configsHandle = Addressables.LoadAssetsAsync<AudioInfoConfig>(_label);
if ((await Addressables.LoadResourceLocationsAsync(_label).Task).Count != 0)
{
_configsHandle = Addressables.LoadAssetsAsync<AudioInfoConfig>(_label);
await _configsHandle.ToUniTask();
await _configsHandle.ToUniTask();
foreach (AudioInfoConfig config in _configsHandle.Result)
_audioInfos[config.GroupName].Add(config.id, config.fileReference);
foreach (var config in _configsHandle.Result)
_audioInfos[config.GroupName].Add(config.id, config.fileReference);
}
_eventBus.RaiseEvent<IAudioConfigsLoadedHandle>(ui => ui.AudioConfigsLoaded());
}
@@ -54,14 +56,15 @@ namespace Code.Infrastructure.Audio
public async UniTaskVoid LoadAudioClips(AudioNames groupName)
{
UnityEngine.Debug.LogWarning($"Audios loaded {groupName}");
foreach (var audioInfo in _audioInfos[groupName])
if (!_clipsHandles[groupName].ContainsKey(audioInfo.Key))
_clipsHandles[groupName].Add(audioInfo.Key, Addressables.LoadAssetAsync<AudioClip>(audioInfo.Value));
_clipsHandles[groupName]
.Add(audioInfo.Key, Addressables.LoadAssetAsync<AudioClip>(audioInfo.Value));
else
_clipsHandles[groupName][audioInfo.Key] = Addressables.LoadAssetAsync<AudioClip>(audioInfo.Value);
foreach (var handle in _clipsHandles[groupName])
foreach (var handle in _clipsHandles[groupName])
await handle.Value.ToUniTask();
_eventBus.RaiseEvent<IAudioClipsEventHandle>(ui =>
@@ -73,11 +76,12 @@ namespace Code.Infrastructure.Audio
public void ReleaseAudios(AudioNames groupName)
{
UnityEngine.Debug.LogWarning($"Audios unloaded {groupName}");
_eventBus.RaiseEvent<IAudioClipsEventHandle>(x=> x.AudioClipsUnLoaded(groupName));
foreach (var handle in _clipsHandles)
_eventBus.RaiseEvent<IAudioClipsEventHandle>(x => x.AudioClipsUnLoaded(groupName));
foreach (var handle in _clipsHandles)
Addressables.Release(handle.Value);
}
public void Dispose()
{
foreach (var handles in _clipsHandles)
@@ -88,7 +92,8 @@ namespace Code.Infrastructure.Audio
}
}
_configsHandle.Release();
if (_configsHandle.IsValid())
_configsHandle.Release();
}
}
@@ -118,7 +118,7 @@ namespace Code.Infrastructure.Audio
{
Addressables.Release(_handles);
Addressables.Release(_prefabConfigHandle);
Addressables.Release(_sourcePrefab);
if(_sourcePrefab) Addressables.Release(_sourcePrefab);
}
}
@@ -32,15 +32,21 @@ namespace Code.Infrastructure.Configs
}
private async UniTaskVoid SetupMasks(IConfigLocator configLocator, EventBus.EventBus eventBus)
{
var handle =
Addressables.LoadAssetsAsync<LayerMasksConfigConfig>(
configLocator.TryGetManyConfigAssetReferences<LayerMasksConfigConfig>(), null, Addressables.MergeMode.Union);
var label = configLocator.TryGetManyConfigAssetReferences<LayerMasksConfigConfig>();
if ((await Addressables.LoadResourceLocationsAsync(label).Task).Count != 0)
{
await handle.ToUniTask();
var handle =
Addressables.LoadAssetsAsync<LayerMasksConfigConfig>(
label, null, Addressables.MergeMode.Union);
SetMasks(handle.Result);
await handle.ToUniTask();
Addressables.Release(handle);
SetMasks(handle.Result);
Addressables.Release(handle);
}
eventBus.RaiseEvent<ILayerMasksInitializedHandle>(x => x.LayerMasksInitialized());
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7b23da77f3794e41b162b2ba2e392936
timeCreated: 1772114910
@@ -0,0 +1,7 @@
namespace Code.Infrastructure.JSON
{
public class JsonService
{
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0c0d29a568964f799b9fda906d71323f
timeCreated: 1772115192
@@ -5,6 +5,7 @@ using Code.Infrastructure.Configs;
using Code.Infrastructure.EventBus;
using Code.Infrastructure.Generators;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
@@ -12,23 +13,24 @@ using UnityEngine.SceneManagement;
namespace Code.Infrastructure.SceneLoader
{
[Config("Configs/SceneConfig",true)]
[Config("Configs/SceneConfig", true)]
public class SceneInfo
{
[ToConfig] public AssetReference SceneAsset;
[ToConfig] public SceneId SceneId;
[ToConfig] public SceneId SceneId;
}
public partial class SceneLoader : IDisposable
{
private Dictionary<SceneId, SceneInfo> _sceneInfos = new();
private Dictionary<SceneId, Action> _scenesActions = new();
private EventBus.EventBus _eventBus;
partial void SetupScenesInfo();
private AsyncOperationHandle<IList<SceneInfoConfig>> _loadHandle;
public SceneLoader(EventBus.EventBus eventBus, IConfigLocator configLocator)
{
_eventBus = eventBus;
@@ -41,16 +43,17 @@ namespace Code.Infrastructure.SceneLoader
if (loadMode == LoadSceneMode.Single)
for (var i = 0; i < _loadedScenes.Count; i++)
UnloadSceneAsync(_loadedScenes.Keys.ToList()[i]).Forget();
_loadedScenes.Add(id,Addressables.LoadSceneAsync(_sceneInfos[id].SceneAsset, loadMode));
_loadedScenes.Add(id, Addressables.LoadSceneAsync(_sceneInfos[id].SceneAsset, loadMode));
await _loadedScenes[id];
_scenesActions[id]();
_scenesActions[id]();
}
private Dictionary<SceneId, AsyncOperationHandle<SceneInstance>> _loadedScenes = new();
public Scene GetScene(SceneId id) => _loadedScenes[id].Result.Scene;
public async UniTaskVoid UnloadSceneAsync(SceneId id)
{
await Addressables.UnloadSceneAsync(_loadedScenes[id]);
@@ -61,35 +64,41 @@ namespace Code.Infrastructure.SceneLoader
private async UniTaskVoid LoadConfigs(string configReferences)
{
_configHandle = Addressables.LoadAssetsAsync<SceneInfoConfig>(configReferences, OnSceneConfigLoaded, Addressables.MergeMode.Union);
await _configHandle.ToUniTask();
SetupScenesInfo();
_eventBus.RaiseEvent<ISceneConfigsLoadedHandle>(h => h.SceneConfigsLoaded());
if ((await Addressables.LoadResourceLocationsAsync(configReferences).Task).Count != 0)
{
_configHandle = Addressables.LoadAssetsAsync<SceneInfoConfig>(configReferences, OnSceneConfigLoaded,
Addressables.MergeMode.Union);
await _configHandle.ToUniTask();
SetupScenesInfo();
}
_eventBus.RaiseEvent<ISceneConfigsLoadedHandle>(h => h.SceneConfigsLoaded());
}
private void OnSceneConfigLoaded(SceneInfoConfig sceneInfoConfig)
{
SceneInfo sceneInfo = new();
sceneInfo.SceneId = sceneInfoConfig.SceneId;
sceneInfo.SceneAsset = sceneInfoConfig.SceneAsset;
_sceneInfos.Add(sceneInfoConfig.SceneId, sceneInfo);
}
public void Dispose()
{
Addressables.Release(_configHandle);
if(_configHandle.IsValid()) _configHandle.Release();
}
}
public interface ISceneConfigsLoadedHandle : IGlobalSubscriber
{
void SceneConfigsLoaded();
}
public interface ILoadedSceneHandle : IGlobalSubscriber
{
void SceneLoaded(SceneId sceneId);
+5 -1
View File
@@ -3,6 +3,10 @@ namespace Code.Infrastructure.UI
public enum UIPanelType
{
Logo,
Loading
Loading,
Calendar,
Tasks,
Plants,
PlantAdd
}
}
@@ -1,7 +1,7 @@
fileFormatVersion: 2
guid: fbefdb800f5c8e4468bf456469caaf61
guid: e2f21cd7d33ee2b4a9aa49675b556cb4
folderAsset: yes
timeCreated: 1596983915
timeCreated: 1525098654
licenseType: Store
DefaultImporter:
userData:
@@ -1,7 +1,7 @@
fileFormatVersion: 2
guid: e5c273c78eb8ebb47a9c6005d4041d73
guid: 5fc918beb0f7cfe49a7f596d5b6b8768
folderAsset: yes
timeCreated: 1596971200
timeCreated: 1525098661
licenseType: Store
DefaultImporter:
userData:
@@ -2,16 +2,16 @@
using System;
using UnityEngine;
namespace NativeFilePickerNamespace
namespace NativeCameraNamespace
{
public class FPCallbackHelper : MonoBehaviour
public class NCCallbackHelper : MonoBehaviour
{
private bool autoDestroyWithCallback;
private Action mainThreadAction = null;
public static FPCallbackHelper Create( bool autoDestroyWithCallback )
public static NCCallbackHelper Create( bool autoDestroyWithCallback )
{
FPCallbackHelper result = new GameObject( "FPCallbackHelper" ).AddComponent<FPCallbackHelper>();
NCCallbackHelper result = new GameObject( "NCCallbackHelper" ).AddComponent<NCCallbackHelper>();
result.autoDestroyWithCallback = autoDestroyWithCallback;
DontDestroyOnLoad( result.gameObject );
return result;
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: b2bbe0051e738ea4585119c46d863f19
timeCreated: 1545147258
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/Android/NCCallbackHelper.cs
uploadId: 746912
@@ -0,0 +1,24 @@
#if UNITY_EDITOR || UNITY_ANDROID
using UnityEngine;
namespace NativeCameraNamespace
{
public class NCCameraCallbackAndroid : AndroidJavaProxy
{
private readonly NativeCamera.CameraCallback callback;
private readonly NCCallbackHelper callbackHelper;
public NCCameraCallbackAndroid( NativeCamera.CameraCallback callback ) : base( "com.yasirkula.unity.NativeCameraMediaReceiver" )
{
this.callback = callback;
callbackHelper = NCCallbackHelper.Create( true );
}
[UnityEngine.Scripting.Preserve]
public void OnMediaReceived( string path )
{
callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) );
}
}
}
#endif
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: 3cc8df584d2a4344b929a4f13a53723a
timeCreated: 1519060539
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/Android/NCCameraCallbackAndroid.cs
uploadId: 746912
@@ -0,0 +1,24 @@
#if UNITY_EDITOR || UNITY_ANDROID
using UnityEngine;
namespace NativeCameraNamespace
{
public class NCPermissionCallbackAndroid : AndroidJavaProxy
{
private readonly NativeCamera.PermissionCallback callback;
private readonly NCCallbackHelper callbackHelper;
public NCPermissionCallbackAndroid( NativeCamera.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeCameraPermissionReceiver" )
{
this.callback = callback;
callbackHelper = NCCallbackHelper.Create( true );
}
[UnityEngine.Scripting.Preserve]
public void OnPermissionResult( int result )
{
callbackHelper.CallOnMainThread( () => callback( (NativeCamera.Permission) result ) );
}
}
}
#endif
@@ -1,6 +1,6 @@
fileFormatVersion: 2
guid: f43d316f7eb1cdf4abcf327a1fbc025b
timeCreated: 1596973837
guid: bafa24bbc8c455f44a2b98dcbe6451bd
timeCreated: 1519060539
licenseType: Store
MonoImporter:
serializedVersion: 2
@@ -12,8 +12,8 @@ MonoImporter:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/Android/FPResultCallbackAndroid.cs
uploadId: 757770
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/Android/NCPermissionCallbackAndroid.cs
uploadId: 746912
Binary file not shown.
@@ -1,6 +1,6 @@
fileFormatVersion: 2
guid: 3ed817dcf924721488f9672995f51cb3
timeCreated: 1607368610
guid: 284037eba2526f54d9cf51b5d9bffcfa
timeCreated: 1569764737
licenseType: Store
PluginImporter:
serializedVersion: 2
@@ -33,8 +33,8 @@ PluginImporter:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/Android/NativeFilePicker.aar
uploadId: 757770
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/Android/NativeCamera.aar
uploadId: 746912
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f387670006405804d9b5c216ebd8ab1f
guid: 16fe39fd709533a4eba946790a8e3123
folderAsset: yes
timeCreated: 1521452097
licenseType: Store
@@ -0,0 +1,113 @@
using System.IO;
using UnityEngine;
using UnityEditor;
#if UNITY_IOS
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
#endif
namespace NativeCameraNamespace
{
[System.Serializable]
public class Settings
{
private const string SAVE_PATH = "ProjectSettings/NativeCamera.json";
public bool AutomatedSetup = true;
public string CameraUsageDescription = "The app requires access to the camera to take pictures or record videos with it.";
public string MicrophoneUsageDescription = "The app will capture microphone input in the recorded video.";
private static Settings m_instance = null;
public static Settings Instance
{
get
{
if( m_instance == null )
{
try
{
if( File.Exists( SAVE_PATH ) )
m_instance = JsonUtility.FromJson<Settings>( File.ReadAllText( SAVE_PATH ) );
else
m_instance = new Settings();
}
catch( System.Exception e )
{
Debug.LogException( e );
m_instance = new Settings();
}
}
return m_instance;
}
}
public void Save()
{
File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) );
}
[SettingsProvider]
public static SettingsProvider CreatePreferencesGUI()
{
return new SettingsProvider( "Project/yasirkula/Native Camera", SettingsScope.Project )
{
guiHandler = ( searchContext ) => PreferencesGUI(),
keywords = new System.Collections.Generic.HashSet<string>() { "Native", "Camera", "Android", "iOS" }
};
}
public static void PreferencesGUI()
{
EditorGUI.BeginChangeCheck();
Instance.AutomatedSetup = EditorGUILayout.Toggle( "Automated Setup", Instance.AutomatedSetup );
EditorGUI.BeginDisabledGroup( !Instance.AutomatedSetup );
Instance.CameraUsageDescription = EditorGUILayout.DelayedTextField( "Camera Usage Description", Instance.CameraUsageDescription );
Instance.MicrophoneUsageDescription = EditorGUILayout.DelayedTextField( "Microphone Usage Description", Instance.MicrophoneUsageDescription );
EditorGUI.EndDisabledGroup();
if( EditorGUI.EndChangeCheck() )
Instance.Save();
}
}
public class NCPostProcessBuild
{
#if UNITY_IOS
[PostProcessBuild]
public static void OnPostprocessBuild( BuildTarget target, string buildPath )
{
if( !Settings.Instance.AutomatedSetup )
return;
if( target == BuildTarget.iOS )
{
string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath );
string plistPath = Path.Combine( buildPath, "Info.plist" );
PBXProject pbxProject = new PBXProject();
pbxProject.ReadFromFile( pbxProjectPath );
string targetGUID = pbxProject.GetUnityFrameworkTargetGuid();
pbxProject.AddFrameworkToProject( targetGUID, "MobileCoreServices.framework", false );
pbxProject.AddFrameworkToProject( targetGUID, "ImageIO.framework", false );
File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
PlistDocument plist = new PlistDocument();
plist.ReadFromString( File.ReadAllText( plistPath ) );
PlistElementDict rootDict = plist.root;
if( !string.IsNullOrEmpty( Settings.Instance.CameraUsageDescription ) )
rootDict.SetString( "NSCameraUsageDescription", Settings.Instance.CameraUsageDescription );
if( !string.IsNullOrEmpty( Settings.Instance.MicrophoneUsageDescription ) )
rootDict.SetString( "NSMicrophoneUsageDescription", Settings.Instance.MicrophoneUsageDescription );
File.WriteAllText( plistPath, plist.WriteToString() );
}
}
#endif
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d466ecf51c239f9419715f6b59348d6c
guid: fa3b57e342928704cb910789ae4dde20
timeCreated: 1521452119
licenseType: Store
MonoImporter:
@@ -12,8 +12,8 @@ MonoImporter:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/Editor/NativeFilePickerPostProcessBuild.cs
uploadId: 757770
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/Editor/NCPostProcessBuild.cs
uploadId: 746912
@@ -1,5 +1,5 @@
{
"name": "NativeFilePicker.Editor",
"name": "NativeCamera.Editor",
"references": [],
"includePlatforms": [
"Editor"
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 31117d0234af0084b91a7e53b3d9e0a3
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/Editor/NativeCamera.Editor.asmdef
uploadId: 746912
@@ -0,0 +1,3 @@
{
"name": "NativeCamera.Runtime"
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: b107fd1956cb3e04985108f5ee29e115
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/NativeCamera.Runtime.asmdef
uploadId: 746912
+563
View File
@@ -0,0 +1,563 @@
using System;
using System.Globalization;
using System.IO;
using UnityEngine;
using System.Threading.Tasks;
using UnityEngine.Networking;
#if UNITY_ANDROID || UNITY_IOS
using NativeCameraNamespace;
#endif
using Object = UnityEngine.Object;
public static class NativeCamera
{
public struct ImageProperties
{
public readonly int width;
public readonly int height;
public readonly string mimeType;
public readonly ImageOrientation orientation;
public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation )
{
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.orientation = orientation;
}
}
public struct VideoProperties
{
public readonly int width;
public readonly int height;
public readonly long duration;
public readonly float rotation;
public VideoProperties( int width, int height, long duration, float rotation )
{
this.width = width;
this.height = height;
this.duration = duration;
this.rotation = rotation;
}
}
public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
public enum Quality { Default = -1, Low = 0, Medium = 1, High = 2 };
public enum PreferredCamera { Default = -1, Rear = 0, Front = 1 }
// EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)
public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 };
public delegate void PermissionCallback( Permission permission );
public delegate void CameraCallback( string path );
#region Platform Specific Elements
#if !UNITY_EDITOR && UNITY_ANDROID
private static AndroidJavaClass m_ajc = null;
private static AndroidJavaClass AJC
{
get
{
if( m_ajc == null )
m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeCamera" );
return m_ajc;
}
}
private static AndroidJavaObject m_context = null;
private static AndroidJavaObject Context
{
get
{
if( m_context == null )
{
using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
{
m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
}
}
return m_context;
}
}
#elif !UNITY_EDITOR && UNITY_IOS
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeCamera_CheckPermission();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeCamera_RequestPermission();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeCamera_OpenSettings();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeCamera_HasCamera();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeCamera_TakePicture( string imageSavePath, int maxSize, int preferredCamera );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeCamera_RecordVideo( int quality, int maxDuration, int preferredCamera );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeCamera_GetImageProperties( string path );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeCamera_GetVideoProperties( string path );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeCamera_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeCamera_LoadImageAtPath( string path, string temporaryFilePath, int maxSize );
#endif
#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
private static string m_temporaryImagePath = null;
private static string TemporaryImagePath
{
get
{
if( m_temporaryImagePath == null )
{
m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_temporaryImagePath;
}
}
#endif
#if !UNITY_EDITOR && UNITY_IOS
private static string m_iOSSelectedImagePath = null;
private static string IOSSelectedImagePath
{
get
{
if( m_iOSSelectedImagePath == null )
{
m_iOSSelectedImagePath = Path.Combine( Application.temporaryCachePath, "CameraImg" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_iOSSelectedImagePath;
}
}
#endif
#endregion
#region Runtime Permissions
public static bool CheckPermission( bool isPicturePermission )
{
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<int>( "CheckPermission", Context, isPicturePermission ) == 1;
#elif !UNITY_EDITOR && UNITY_IOS
return _NativeCamera_CheckPermission() == 1;
#else
return true;
#endif
}
public static void RequestPermissionAsync( PermissionCallback callback, bool isPicturePermission )
{
#if !UNITY_EDITOR && UNITY_ANDROID
NCPermissionCallbackAndroid nativeCallback = new( callback );
AJC.CallStatic( "RequestPermission", Context, nativeCallback, isPicturePermission );
#elif !UNITY_EDITOR && UNITY_IOS
NCPermissionCallbackiOS.Initialize( callback );
_NativeCamera_RequestPermission();
#else
callback( Permission.Granted );
#endif
}
public static Task<Permission> RequestPermissionAsync( bool isPicturePermission )
{
TaskCompletionSource<Permission> tcs = new TaskCompletionSource<Permission>();
RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), isPicturePermission );
return tcs.Task;
}
public static void OpenSettings()
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "OpenSettings", Context );
#elif !UNITY_EDITOR && UNITY_IOS
_NativeCamera_OpenSettings();
#endif
}
#endregion
#region Camera Functions
public static void TakePicture( CameraCallback callback, int maxSize = -1, bool saveAsJPEG = true, PreferredCamera preferredCamera = PreferredCamera.Default )
{
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsCameraBusy() )
{
callback?.Invoke( null );
return;
}
#if UNITY_EDITOR
string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select image", "", new string[] { "Image files", "png,jpg,jpeg", "All files", "*" } );
if( callback != null )
callback( pickedFile != "" ? pickedFile : null );
#elif UNITY_ANDROID
AJC.CallStatic( "TakePicture", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera );
#elif UNITY_IOS
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
NCCameraCallbackiOS.Initialize( callback );
_NativeCamera_TakePicture( IOSSelectedImagePath + ( saveAsJPEG ? ".jpeg" : ".png" ), maxSize, (int) preferredCamera );
#else
if( callback != null )
callback( null );
#endif
}, true );
}
public static void RecordVideo( CameraCallback callback, Quality quality = Quality.Default, int maxDuration = 0, long maxSizeBytes = 0L, PreferredCamera preferredCamera = PreferredCamera.Default )
{
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsCameraBusy() )
{
callback?.Invoke( null );
return;
}
#if UNITY_EDITOR
string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select video", "", new string[] { "Video files", "mp4,mov,webm,avi", "All files", "*" } );
if( callback != null )
callback( pickedFile != "" ? pickedFile : null );
#elif UNITY_ANDROID
AJC.CallStatic( "RecordVideo", Context, new NCCameraCallbackAndroid( callback ), (int) preferredCamera, (int) quality, maxDuration, maxSizeBytes );
#elif UNITY_IOS
NCCameraCallbackiOS.Initialize( callback );
_NativeCamera_RecordVideo( (int) quality, maxDuration, (int) preferredCamera );
#else
if( callback != null )
callback( null );
#endif
}, false );
}
public static bool DeviceHasCamera()
{
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<bool>( "HasCamera", Context );
#elif !UNITY_EDITOR && UNITY_IOS
return _NativeCamera_HasCamera() == 1;
#else
return true;
#endif
}
public static bool IsCameraBusy()
{
#if !UNITY_EDITOR && UNITY_IOS
return NCCameraCallbackiOS.IsBusy;
#else
return false;
#endif
}
#endregion
#region Utility Functions
public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
{
if( string.IsNullOrEmpty( imagePath ) )
throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string loadPath = AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize );
#elif !UNITY_EDITOR && UNITY_IOS
string loadPath = _NativeCamera_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize );
#else
string loadPath = imagePath;
#endif
string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace );
try
{
if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
{
Debug.LogWarning( "Couldn't load image at path: " + loadPath );
Object.DestroyImmediate( result );
return null;
}
}
catch( Exception e )
{
Debug.LogException( e );
Object.DestroyImmediate( result );
return null;
}
finally
{
if( loadPath != imagePath )
{
try
{
File.Delete( loadPath );
}
catch { }
}
}
return result;
}
public static async Task<Texture2D> LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true )
{
if( string.IsNullOrEmpty( imagePath ) )
throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) );
#elif !UNITY_EDITOR && UNITY_IOS
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string loadPath = await Task.Run( () => _NativeCamera_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) );
#else
string loadPath = imagePath;
#endif
Texture2D result = null;
using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) )
{
UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest();
while( !asyncOperation.isDone )
await Task.Yield();
if( www.result != UnityWebRequest.Result.Success )
Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error );
else
result = DownloadHandlerTexture.GetContent( www );
}
if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong
{
string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
result = new Texture2D( 2, 2, format, true, false );
try
{
if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
{
Debug.LogWarning( "Couldn't load image at path: " + loadPath );
Object.DestroyImmediate( result );
return null;
}
}
catch( Exception e )
{
Debug.LogException( e );
Object.DestroyImmediate( result );
return null;
}
finally
{
if( loadPath != imagePath )
{
try
{
File.Delete( loadPath );
}
catch { }
}
}
}
return result;
}
public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
{
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string thumbnailPath = AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds );
#elif !UNITY_EDITOR && UNITY_IOS
string thumbnailPath = _NativeCamera_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds );
#else
string thumbnailPath = null;
#endif
if( !string.IsNullOrEmpty( thumbnailPath ) )
return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace );
else
return null;
}
public static async Task<Texture2D> GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true )
{
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) );
#elif !UNITY_EDITOR && UNITY_IOS
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string thumbnailPath = await Task.Run( () => _NativeCamera_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) );
#else
string thumbnailPath = null;
#endif
if( !string.IsNullOrEmpty( thumbnailPath ) )
return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable );
else
return null;
}
#if UNITY_ANDROID
private static async Task<T> TryCallNativeAndroidFunctionOnSeparateThread<T>( Func<T> function )
{
T result = default( T );
bool hasResult = false;
await Task.Run( () =>
{
if( AndroidJNI.AttachCurrentThread() != 0 )
Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" );
else
{
try
{
result = function();
hasResult = true;
}
finally
{
AndroidJNI.DetachCurrentThread();
}
}
} );
return hasResult ? result : function();
}
#endif
public static ImageProperties GetImageProperties( string imagePath )
{
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
#if !UNITY_EDITOR && UNITY_ANDROID
string value = AJC.CallStatic<string>( "GetImageProperties", Context, imagePath );
#elif !UNITY_EDITOR && UNITY_IOS
string value = _NativeCamera_GetImageProperties( imagePath );
#else
string value = null;
#endif
int width = 0, height = 0;
string mimeType = null;
ImageOrientation orientation = ImageOrientation.Unknown;
if( !string.IsNullOrEmpty( value ) )
{
string[] properties = value.Split( '>' );
if( properties != null && properties.Length >= 4 )
{
if( !int.TryParse( properties[0].Trim(), out width ) )
width = 0;
if( !int.TryParse( properties[1].Trim(), out height ) )
height = 0;
mimeType = properties[2].Trim();
if( mimeType.Length == 0 )
{
string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
if( extension == ".png" )
mimeType = "image/png";
else if( extension == ".jpg" || extension == ".jpeg" )
mimeType = "image/jpeg";
else if( extension == ".gif" )
mimeType = "image/gif";
else if( extension == ".bmp" )
mimeType = "image/bmp";
else
mimeType = null;
}
int orientationInt;
if( int.TryParse( properties[3].Trim(), out orientationInt ) )
orientation = (ImageOrientation) orientationInt;
}
}
return new ImageProperties( width, height, mimeType, orientation );
}
public static VideoProperties GetVideoProperties( string videoPath )
{
if( !File.Exists( videoPath ) )
throw new FileNotFoundException( "File not found at " + videoPath );
#if !UNITY_EDITOR && UNITY_ANDROID
string value = AJC.CallStatic<string>( "GetVideoProperties", Context, videoPath );
#elif !UNITY_EDITOR && UNITY_IOS
string value = _NativeCamera_GetVideoProperties( videoPath );
#else
string value = null;
#endif
int width = 0, height = 0;
long duration = 0L;
float rotation = 0f;
if( !string.IsNullOrEmpty( value ) )
{
string[] properties = value.Split( '>' );
if( properties != null && properties.Length >= 4 )
{
if( !int.TryParse( properties[0].Trim(), out width ) )
width = 0;
if( !int.TryParse( properties[1].Trim(), out height ) )
height = 0;
if( !long.TryParse( properties[2].Trim(), out duration ) )
duration = 0L;
if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) )
rotation = 0f;
}
}
if( rotation == -90f )
rotation = 270f;
return new VideoProperties( width, height, duration, rotation );
}
#endregion
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5ecdc359fc7b7e448a5aa6433eafcb8b
guid: ff758a73b21d4a04aa6f95679b3da605
timeCreated: 1498722610
licenseType: Store
MonoImporter:
@@ -12,8 +12,8 @@ MonoImporter:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/NativeFilePicker.cs
uploadId: 757770
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/NativeCamera.cs
uploadId: 746912
+6
View File
@@ -0,0 +1,6 @@
= Native Camera for Android & iOS (v1.5.0) =
Documentation: https://github.com/yasirkula/UnityNativeCamera
FAQ: https://github.com/yasirkula/UnityNativeCamera#faq
Example code: https://github.com/yasirkula/UnityNativeCamera#example-code
E-mail: yasirkula@gmail.com
@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 5a88d1b1b9d7b904b862304c20ed4db4
timeCreated: 1563308465
licenseType: Store
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/README.txt
uploadId: 746912
@@ -1,7 +1,7 @@
fileFormatVersion: 2
guid: 8d99a20b76631e440a062e4fad822fc8
guid: 5576acd2a06eb72409e6ec4b7f204a4e
folderAsset: yes
timeCreated: 1592132993
timeCreated: 1498722622
licenseType: Store
DefaultImporter:
userData:
@@ -0,0 +1,73 @@
#if UNITY_EDITOR || UNITY_IOS
using UnityEngine;
namespace NativeCameraNamespace
{
public class NCCameraCallbackiOS : MonoBehaviour
{
private static NCCameraCallbackiOS instance;
private NativeCamera.CameraCallback callback;
private float nextBusyCheckTime;
public static bool IsBusy { get; private set; }
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeCamera_IsCameraBusy();
public static void Initialize( NativeCamera.CameraCallback callback )
{
if( IsBusy )
return;
if( instance == null )
{
instance = new GameObject( "NCCameraCallbackiOS" ).AddComponent<NCCameraCallbackiOS>();
DontDestroyOnLoad( instance.gameObject );
}
instance.callback = callback;
instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
IsBusy = true;
}
private void Update()
{
if( IsBusy )
{
if( Time.realtimeSinceStartup >= nextBusyCheckTime )
{
nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
if( _NativeCamera_IsCameraBusy() == 0 )
{
IsBusy = false;
NativeCamera.CameraCallback _callback = callback;
callback = null;
if( _callback != null )
_callback( null );
}
}
}
}
[UnityEngine.Scripting.Preserve]
public void OnMediaReceived( string path )
{
IsBusy = false;
if( string.IsNullOrEmpty( path ) )
path = null;
NativeCamera.CameraCallback _callback = callback;
callback = null;
if( _callback != null )
_callback( path );
}
}
}
#endif
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: d8f19d5713752dc41bd377562677d8ee
timeCreated: 1519060539
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/iOS/NCCameraCallbackiOS.cs
uploadId: 746912
@@ -0,0 +1,35 @@
#if UNITY_EDITOR || UNITY_IOS
using UnityEngine;
namespace NativeCameraNamespace
{
public class NCPermissionCallbackiOS : MonoBehaviour
{
private static NCPermissionCallbackiOS instance;
private NativeCamera.PermissionCallback callback;
public static void Initialize( NativeCamera.PermissionCallback callback )
{
if( instance == null )
{
instance = new GameObject( "NCPermissionCallbackiOS" ).AddComponent<NCPermissionCallbackiOS>();
DontDestroyOnLoad( instance.gameObject );
}
else if( instance.callback != null )
instance.callback( NativeCamera.Permission.ShouldAsk );
instance.callback = callback;
}
[UnityEngine.Scripting.Preserve]
public void OnPermissionRequested( string message )
{
NativeCamera.PermissionCallback _callback = callback;
callback = null;
if( _callback != null )
_callback( (NativeCamera.Permission) int.Parse( message ) );
}
}
}
#endif
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b11d2999e16ca864f8974988d73c2017
guid: 1efd0cf9fb7457142b76fb1bc8672e8d
timeCreated: 1519060539
licenseType: Store
MonoImporter:
@@ -12,8 +12,8 @@ MonoImporter:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/iOS/FPResultCallbackiOS.cs
uploadId: 757770
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/iOS/NCPermissionCallbackiOS.cs
uploadId: 746912
@@ -0,0 +1,561 @@
#import <Foundation/Foundation.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <ImageIO/ImageIO.h>
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
extern UIViewController* UnityGetGLViewController();
@interface UNativeCamera:NSObject
+ (int)checkPermission;
+ (void)requestPermission;
+ (void)openSettings;
+ (int)hasCamera;
+ (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration;
+ (int)isCameraBusy;
+ (char *)getImageProperties:(NSString *)path;
+ (char *)getVideoProperties:(NSString *)path;
+ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime;
+ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize;
@end
@implementation UNativeCamera
static NSString *pickedMediaSavePath;
static UIImagePickerController *imagePicker;
static int cameraMaxImageSize = -1;
static int imagePickerState = 0; // 0 -> none, 1 -> showing, 2 -> finished
static BOOL recordingVideo = NO;
static AVAudioSessionCategory unityAudioSessionCategory = AVAudioSessionCategoryAmbient;
static NSUInteger unityAudioSessionCategoryOptions = 1;
static AVAudioSessionMode unityAudioSessionMode = AVAudioSessionModeDefault;
// Credit: https://stackoverflow.com/a/20464727/2373034
+ (int)checkPermission
{
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if( status == AVAuthorizationStatusAuthorized )
return 1;
else if( status == AVAuthorizationStatusNotDetermined )
return 2;
else
return 0;
}
// Credit: https://stackoverflow.com/a/20464727/2373034
+ (void)requestPermission
{
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if( status == AVAuthorizationStatusAuthorized )
UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", "1" );
else if( status == AVAuthorizationStatusNotDetermined )
{
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted )
{
UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", granted ? "1" : "0" );
}];
}
else
UnitySendMessage( "NCPermissionCallbackiOS", "OnPermissionRequested", "0" );
}
// Credit: https://stackoverflow.com/a/25453667/2373034
+ (void)openSettings
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
}
+ (int)hasCamera
{
return [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] ? 1 : 0;
}
// Credit: https://stackoverflow.com/a/10531752/2373034
+ (void)openCamera:(BOOL)imageMode defaultCamera:(int)defaultCamera savePath:(NSString *)imageSavePath maxImageSize:(int)maxImageSize videoQuality:(int)videoQuality maxVideoDuration:(int)maxVideoDuration
{
if( ![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] )
{
NSLog( @"Device has no registered cameras!" );
UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" );
return;
}
if( ( imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeImage] ) ||
( !imageMode && ![[UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera] containsObject:(NSString*)kUTTypeMovie] ) )
{
NSLog( @"Camera does not support this operation!" );
UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" );
return;
}
imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = (id) self;
imagePicker.allowsEditing = NO;
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
if( imageMode )
imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
else
{
imagePicker.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeMovie];
if( maxVideoDuration > 0 )
imagePicker.videoMaximumDuration = maxVideoDuration;
if( videoQuality == 0 )
imagePicker.videoQuality = UIImagePickerControllerQualityTypeLow;
else if( videoQuality == 1 )
imagePicker.videoQuality = UIImagePickerControllerQualityTypeMedium;
else if( videoQuality == 2 )
imagePicker.videoQuality = UIImagePickerControllerQualityTypeHigh;
}
if( defaultCamera == 0 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear] )
imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
else if( defaultCamera == 1 && [UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront] )
imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceFront;
// Bugfix for https://github.com/yasirkula/UnityNativeCamera/issues/45
if( !imageMode )
{
unityAudioSessionCategory = [[AVAudioSession sharedInstance] category];
unityAudioSessionCategoryOptions = [[AVAudioSession sharedInstance] categoryOptions];
unityAudioSessionMode = [[AVAudioSession sharedInstance] mode];
}
recordingVideo = !imageMode;
pickedMediaSavePath = imageSavePath;
cameraMaxImageSize = maxImageSize;
imagePickerState = 1;
[UnityGetGLViewController() presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }];
}
+ (int)isCameraBusy
{
if( imagePickerState == 2 )
return 1;
if( imagePicker != nil )
{
if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() )
return 1;
imagePicker = nil;
[self restoreAudioSession];
return 0;
}
return 0;
}
+ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
NSString *path = nil;
if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] )
{
NSLog( @"UIImagePickerController finished taking picture" );
UIImage *image = info[UIImagePickerControllerEditedImage] ?: info[UIImagePickerControllerOriginalImage];
if( image == nil )
path = nil;
else
{
NSString *extension = [pickedMediaSavePath pathExtension];
BOOL saveAsJPEG = [extension caseInsensitiveCompare:@"jpg"] == NSOrderedSame || [extension caseInsensitiveCompare:@"jpeg"] == NSOrderedSame;
// Try to save the image with metadata
// CANCELED: a number of users reported that this method results in 90-degree rotated images, uncomment at your own risk
// Credit: https://stackoverflow.com/a/15858955
/*NSDictionary *metadata = [info objectForKey:UIImagePickerControllerMediaMetadata];
NSMutableDictionary *mutableMetadata = nil;
CFDictionaryRef metadataRef;
CFStringRef imageType;
if( saveAsJPEG )
{
mutableMetadata = [metadata mutableCopy];
[mutableMetadata setObject:@(1.0) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality];
metadataRef = (__bridge CFDictionaryRef) mutableMetadata;
imageType = kUTTypeJPEG;
}
else
{
metadataRef = (__bridge CFDictionaryRef) metadata;
imageType = kUTTypePNG;
}
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:pickedMediaSavePath], imageType , 1, NULL );
if( imageDestination == NULL )
NSLog( @"Failed to create image destination" );
else
{
CGImageDestinationAddImage( imageDestination, image.CGImage, metadataRef );
if( CGImageDestinationFinalize( imageDestination ) )
path = pickedMediaSavePath;
else
NSLog( @"Failed to finalize the image" );
CFRelease( imageDestination );
}*/
if( path == nil )
{
//NSLog( @"Attempting to save the image without metadata as fallback" );
if( ( saveAsJPEG && [UIImageJPEGRepresentation( [self scaleImage:image maxSize:cameraMaxImageSize], 1.0 ) writeToFile:pickedMediaSavePath atomically:YES] ) ||
( !saveAsJPEG && [UIImagePNGRepresentation( [self scaleImage:image maxSize:cameraMaxImageSize] ) writeToFile:pickedMediaSavePath atomically:YES] ) )
path = pickedMediaSavePath;
else
{
NSLog( @"Error saving image without metadata" );
path = nil;
}
}
}
}
else
{
NSLog( @"UIImagePickerController finished recording video" );
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];
#pragma clang diagnostic pop
if( mediaUrl == nil )
path = nil;
else
path = [mediaUrl path];
}
imagePicker = nil;
imagePickerState = 2;
UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", [self getCString:path] );
[picker dismissViewControllerAnimated:NO completion:nil];
[self restoreAudioSession];
}
+ (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
NSLog( @"UIImagePickerController cancelled" );
imagePicker = nil;
UnitySendMessage( "NCCameraCallbackiOS", "OnMediaReceived", "" );
[picker dismissViewControllerAnimated:NO completion:nil];
[self restoreAudioSession];
}
// Bugfix for https://github.com/yasirkula/UnityNativeCamera/issues/45
+ (void)restoreAudioSession
{
if( recordingVideo )
{
NSError *error = nil;
if( ![[AVAudioSession sharedInstance] setCategory:unityAudioSessionCategory mode:unityAudioSessionMode options:unityAudioSessionCategoryOptions error:&error] )
{
if( error != nil )
NSLog( @"Error setting audio session category back to %@ with mode %@ and options %lu: %@", unityAudioSessionCategory, unityAudioSessionMode, (unsigned long) unityAudioSessionCategoryOptions, error );
else
NSLog( @"Error setting audio session category back to %@ with mode %@ and options %lu", unityAudioSessionCategory, unityAudioSessionMode, (unsigned long) unityAudioSessionCategoryOptions );
}
}
}
// Credit: https://stackoverflow.com/a/4170099/2373034
+ (NSArray *)getImageMetadata:(NSString *)path
{
int width = 0;
int height = 0;
int orientation = -1;
CGImageSourceRef imageSource = CGImageSourceCreateWithURL( (__bridge CFURLRef) [NSURL fileURLWithPath:path], nil );
if( imageSource != nil )
{
NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:NO] forKey:(__bridge NSString *)kCGImageSourceShouldCache];
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex( imageSource, 0, (__bridge CFDictionaryRef) options );
CFRelease( imageSource );
CGFloat widthF = 0.0f, heightF = 0.0f;
if( imageProperties != nil )
{
if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelWidth ) )
CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelWidth ), kCFNumberCGFloatType, &widthF );
if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyPixelHeight ) )
CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyPixelHeight ), kCFNumberCGFloatType, &heightF );
if( CFDictionaryContainsKey( imageProperties, kCGImagePropertyOrientation ) )
{
CFNumberGetValue( (CFNumberRef) CFDictionaryGetValue( imageProperties, kCGImagePropertyOrientation ), kCFNumberIntType, &orientation );
if( orientation > 4 )
{
// Landscape image
CGFloat temp = widthF;
widthF = heightF;
heightF = temp;
}
}
CFRelease( imageProperties );
}
width = (int) roundf( widthF );
height = (int) roundf( heightF );
}
return [[NSArray alloc] initWithObjects:[NSNumber numberWithInt:width], [NSNumber numberWithInt:height], [NSNumber numberWithInt:orientation], nil];
}
+ (char *)getImageProperties:(NSString *)path
{
NSArray *metadata = [self getImageMetadata:path];
int orientationUnity;
int orientation = [metadata[2] intValue];
// To understand the magic numbers, see ImageOrientation enum in NativeCamera.cs
// and http://sylvana.net/jpegcrop/exif_orientation.html
if( orientation == 1 )
orientationUnity = 0;
else if( orientation == 2 )
orientationUnity = 4;
else if( orientation == 3 )
orientationUnity = 2;
else if( orientation == 4 )
orientationUnity = 6;
else if( orientation == 5 )
orientationUnity = 5;
else if( orientation == 6 )
orientationUnity = 1;
else if( orientation == 7 )
orientationUnity = 7;
else if( orientation == 8 )
orientationUnity = 3;
else
orientationUnity = -1;
return [self getCString:[NSString stringWithFormat:@"%d>%d> >%d", [metadata[0] intValue], [metadata[1] intValue], orientationUnity]];
}
+ (char *)getVideoProperties:(NSString *)path
{
CGSize size = CGSizeZero;
float rotation = 0;
long long duration = 0;
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
if( asset != nil )
{
duration = (long long) round( CMTimeGetSeconds( [asset duration] ) * 1000 );
CGAffineTransform transform = [asset preferredTransform];
NSArray<AVAssetTrack *>* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
if( videoTracks != nil && [videoTracks count] > 0 )
{
size = [[videoTracks objectAtIndex:0] naturalSize];
transform = [[videoTracks objectAtIndex:0] preferredTransform];
}
rotation = atan2( transform.b, transform.a ) * ( 180.0 / M_PI );
}
return [self getCString:[NSString stringWithFormat:@"%d>%d>%lld>%f", (int) roundf( size.width ), (int) roundf( size.height ), duration, rotation]];
}
+ (char *)getVideoThumbnail:(NSString *)path savePath:(NSString *)savePath maximumSize:(int)maximumSize captureTime:(double)captureTime
{
AVAssetImageGenerator *thumbnailGenerator = [[AVAssetImageGenerator alloc] initWithAsset:[[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:path] options:nil]];
thumbnailGenerator.appliesPreferredTrackTransform = YES;
thumbnailGenerator.maximumSize = CGSizeMake( (CGFloat) maximumSize, (CGFloat) maximumSize );
thumbnailGenerator.requestedTimeToleranceBefore = kCMTimeZero;
thumbnailGenerator.requestedTimeToleranceAfter = kCMTimeZero;
if( captureTime < 0.0 )
captureTime = 0.0;
else
{
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path] options:nil];
if( asset != nil )
{
double videoDuration = CMTimeGetSeconds( [asset duration] );
if( videoDuration > 0.0 && captureTime >= videoDuration - 0.1 )
{
if( captureTime > videoDuration )
captureTime = videoDuration;
thumbnailGenerator.requestedTimeToleranceBefore = CMTimeMakeWithSeconds( 1.0, 600 );
}
}
}
NSError *error = nil;
CGImageRef image = [thumbnailGenerator copyCGImageAtTime:CMTimeMakeWithSeconds( captureTime, 600 ) actualTime:nil error:&error];
if( image == nil )
{
if( error != nil )
NSLog( @"Error generating video thumbnail: %@", error );
else
NSLog( @"Error generating video thumbnail..." );
return [self getCString:@""];
}
UIImage *thumbnail = [[UIImage alloc] initWithCGImage:image];
CGImageRelease( image );
if( ![UIImagePNGRepresentation( thumbnail ) writeToFile:savePath atomically:YES] )
{
NSLog( @"Error saving thumbnail image" );
return [self getCString:@""];
}
return [self getCString:savePath];
}
+ (UIImage *)scaleImage:(UIImage *)image maxSize:(int)maxSize
{
CGFloat width = image.size.width;
CGFloat height = image.size.height;
UIImageOrientation orientation = image.imageOrientation;
if( width <= maxSize && height <= maxSize && orientation != UIImageOrientationDown &&
orientation != UIImageOrientationLeft && orientation != UIImageOrientationRight &&
orientation != UIImageOrientationLeftMirrored && orientation != UIImageOrientationRightMirrored &&
orientation != UIImageOrientationUpMirrored && orientation != UIImageOrientationDownMirrored )
return image;
CGFloat scaleX = 1.0f;
CGFloat scaleY = 1.0f;
if( width > maxSize )
scaleX = maxSize / width;
if( height > maxSize )
scaleY = maxSize / height;
// Credit: https://github.com/mbcharbonneau/UIImage-Categories/blob/master/UIImage%2BAlpha.m
CGImageAlphaInfo alpha = CGImageGetAlphaInfo( image.CGImage );
BOOL hasAlpha = alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast;
CGFloat scaleRatio = scaleX < scaleY ? scaleX : scaleY;
CGRect imageRect = CGRectMake( 0, 0, width * scaleRatio, height * scaleRatio );
UIGraphicsImageRendererFormat *format = [image imageRendererFormat];
format.opaque = !hasAlpha;
format.scale = image.scale;
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:imageRect.size format:format];
image = [renderer imageWithActions:^( UIGraphicsImageRendererContext* _Nonnull myContext )
{
[image drawInRect:imageRect];
}];
return image;
}
+ (char *)loadImageAtPath:(NSString *)path tempFilePath:(NSString *)tempFilePath maximumSize:(int)maximumSize
{
// Check if the image can be loaded by Unity without requiring a conversion to PNG
// Credit: https://stackoverflow.com/a/12048937/2373034
NSString *extension = [path pathExtension];
BOOL conversionNeeded = [extension caseInsensitiveCompare:@"jpg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"jpeg"] != NSOrderedSame && [extension caseInsensitiveCompare:@"png"] != NSOrderedSame;
if( !conversionNeeded )
{
// Check if the image needs to be processed at all
NSArray *metadata = [self getImageMetadata:path];
int orientationInt = [metadata[2] intValue]; // 1: correct orientation, [1,8]: valid orientation range
if( orientationInt == 1 && [metadata[0] intValue] <= maximumSize && [metadata[1] intValue] <= maximumSize )
return [self getCString:path];
}
UIImage *image = [UIImage imageWithContentsOfFile:path];
if( image == nil )
return [self getCString:path];
UIImage *scaledImage = [self scaleImage:image maxSize:maximumSize];
if( conversionNeeded || scaledImage != image )
{
if( ![UIImagePNGRepresentation( scaledImage ) writeToFile:tempFilePath atomically:YES] )
{
NSLog( @"Error creating scaled image" );
return [self getCString:path];
}
return [self getCString:tempFilePath];
}
else
return [self getCString:path];
}
// Credit: https://stackoverflow.com/a/37052118/2373034
+ (char *)getCString:(NSString *)source
{
if( source == nil )
source = @"";
const char *sourceUTF8 = [source UTF8String];
char *result = (char*) malloc( strlen( sourceUTF8 ) + 1 );
strcpy( result, sourceUTF8 );
return result;
}
@end
extern "C" int _NativeCamera_CheckPermission()
{
return [UNativeCamera checkPermission];
}
extern "C" void _NativeCamera_RequestPermission()
{
[UNativeCamera requestPermission];
}
extern "C" void _NativeCamera_OpenSettings()
{
[UNativeCamera openSettings];
}
extern "C" int _NativeCamera_HasCamera()
{
return [UNativeCamera hasCamera];
}
extern "C" void _NativeCamera_TakePicture( const char* imageSavePath, int maxSize, int preferredCamera )
{
[UNativeCamera openCamera:YES defaultCamera:preferredCamera savePath:[NSString stringWithUTF8String:imageSavePath] maxImageSize:maxSize videoQuality:-1 maxVideoDuration:-1];
}
extern "C" void _NativeCamera_RecordVideo( int quality, int maxDuration, int preferredCamera )
{
[UNativeCamera openCamera:NO defaultCamera:preferredCamera savePath:nil maxImageSize:4096 videoQuality:quality maxVideoDuration:maxDuration];
}
extern "C" int _NativeCamera_IsCameraBusy()
{
return [UNativeCamera isCameraBusy];
}
extern "C" char* _NativeCamera_GetImageProperties( const char* path )
{
return [UNativeCamera getImageProperties:[NSString stringWithUTF8String:path]];
}
extern "C" char* _NativeCamera_GetVideoProperties( const char* path )
{
return [UNativeCamera getVideoProperties:[NSString stringWithUTF8String:path]];
}
extern "C" char* _NativeCamera_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds )
{
return [UNativeCamera getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds];
}
extern "C" char* _NativeCamera_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize )
{
return [UNativeCamera loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize];
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 4c65eb8688946be4ea1af62f377f8891
guid: f71ce2f3d3a5dbd46af575e628ed9d6e
timeCreated: 1498722774
licenseType: Store
PluginImporter:
@@ -33,8 +33,8 @@ PluginImporter:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/iOS/NativeFilePicker.mm
uploadId: 757770
productId: 117802
packageName: Native Camera for Android & iOS
packageVersion: 1.5.0
assetPath: Assets/Plugins/NativeCamera/iOS/NativeCamera.mm
uploadId: 746912
@@ -1,19 +0,0 @@
fileFormatVersion: 2
guid: 41423473566db0b4ebd62882996f08bc
timeCreated: 1596973837
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/Android/FPCallbackHelper.cs
uploadId: 757770
@@ -1,24 +0,0 @@
#if UNITY_EDITOR || UNITY_ANDROID
using UnityEngine;
namespace NativeFilePickerNamespace
{
public class FPPermissionCallbackAndroid : AndroidJavaProxy
{
private readonly NativeFilePicker.PermissionCallback callback;
private readonly FPCallbackHelper callbackHelper;
public FPPermissionCallbackAndroid( NativeFilePicker.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeFilePickerPermissionReceiver" )
{
this.callback = callback;
callbackHelper = FPCallbackHelper.Create( true );
}
[UnityEngine.Scripting.Preserve]
public void OnPermissionResult( int result )
{
callbackHelper.CallOnMainThread( () => callback( (NativeFilePicker.Permission) result ) );
}
}
}
#endif
@@ -1,19 +0,0 @@
fileFormatVersion: 2
guid: 5f8c2de0178a90140b030975551436a5
timeCreated: 1596973837
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/Android/FPPermissionCallbackAndroid.cs
uploadId: 757770
@@ -1,71 +0,0 @@
#if UNITY_EDITOR || UNITY_ANDROID
using UnityEngine;
namespace NativeFilePickerNamespace
{
public class FPResultCallbackAndroid : AndroidJavaProxy
{
private readonly NativeFilePicker.FilePickedCallback pickCallback;
private readonly NativeFilePicker.MultipleFilesPickedCallback pickCallbackMultiple;
private readonly NativeFilePicker.FilesExportedCallback exportCallback;
private readonly FPCallbackHelper callbackHelper;
public FPResultCallbackAndroid( NativeFilePicker.FilePickedCallback pickCallback, NativeFilePicker.MultipleFilesPickedCallback pickCallbackMultiple, NativeFilePicker.FilesExportedCallback exportCallback ) : base( "com.yasirkula.unity.NativeFilePickerResultReceiver" )
{
this.pickCallback = pickCallback;
this.pickCallbackMultiple = pickCallbackMultiple;
this.exportCallback = exportCallback;
callbackHelper = FPCallbackHelper.Create( true );
}
[UnityEngine.Scripting.Preserve]
public void OnFilePicked( string path )
{
callbackHelper.CallOnMainThread( () => pickCallback( !string.IsNullOrEmpty( path ) ? path : null ) );
}
[UnityEngine.Scripting.Preserve]
public void OnMultipleFilesPicked( string paths )
{
string[] result = null;
if( !string.IsNullOrEmpty( paths ) )
{
string[] pathsSplit = paths.Split( '>' );
int validPathCount = 0;
for( int i = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPathCount++;
}
if( validPathCount == 0 )
pathsSplit = new string[0];
else if( validPathCount != pathsSplit.Length )
{
string[] validPaths = new string[validPathCount];
for( int i = 0, j = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPaths[j++] = pathsSplit[i];
}
pathsSplit = validPaths;
}
result = pathsSplit;
}
callbackHelper.CallOnMainThread( () => pickCallbackMultiple( ( result != null && result.Length > 0 ) ? result : null ) );
}
[UnityEngine.Scripting.Preserve]
public void OnFilesExported( bool result )
{
callbackHelper.CallOnMainThread( () => exportCallback( result ) );
}
}
}
#endif
@@ -1,14 +0,0 @@
fileFormatVersion: 2
guid: 31a0463be93a160428f78ae484a99f5b
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/Editor/NativeFilePicker.Editor.asmdef
uploadId: 757770
@@ -1,65 +0,0 @@
using System.IO;
using UnityEditor;
using UnityEngine;
namespace NativeFilePickerNamespace
{
[HelpURL( "https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_declare/understand_utis_declare.html" )]
public class NativeFilePickerCustomTypes : ScriptableObject
{
[System.Serializable]
public class TypeHolder
{
[Tooltip( "A unique identifier for the type (UTI) (e.g. com.mycompany.mydata)" )]
public string identifier;
[Tooltip( "Description of the type (e.g. Save data for My App)" )]
public string description;
[Tooltip( "If this type is owned by your app (i.e. exclusive to your app), set this value to true. If another app generates files of this type and your app is merely importing/modifying them, set this value to false." )]
public bool isExported;
[Tooltip( "Which official UTI(s) is this type part of (e.g. public.data for generic types or public.image for image types)\nFull list available at: https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html" )]
public string[] conformsTo;
[Tooltip( "Extension(s) associated with this type (don't include the period; i.e. use myextension instead of .myextension)" )]
public string[] extensions;
}
private const string INITIAL_SAVE_PATH = "Assets/Plugins/NativeFilePicker/NativeFilePickerCustomTypes.asset";
#pragma warning disable 0649
[SerializeField]
private TypeHolder[] customTypes;
#pragma warning restore 0649
[MenuItem( "Window/NativeFilePicker Custom Types" )]
private static void InspectCustomTypes()
{
Selection.activeObject = GetInstance( true );
}
private static NativeFilePickerCustomTypes GetInstance( bool createIfNotExists )
{
NativeFilePickerCustomTypes instance = null;
string[] instances = AssetDatabase.FindAssets( "t:NativeFilePickerCustomTypes" );
if( instances != null && instances.Length > 0 )
instance = AssetDatabase.LoadAssetAtPath<NativeFilePickerCustomTypes>( AssetDatabase.GUIDToAssetPath( instances[0] ) );
if( !instance && createIfNotExists )
{
Directory.CreateDirectory( Path.GetDirectoryName( INITIAL_SAVE_PATH ) );
AssetDatabase.CreateAsset( CreateInstance<NativeFilePickerCustomTypes>(), INITIAL_SAVE_PATH );
AssetDatabase.SaveAssets();
instance = AssetDatabase.LoadAssetAtPath<NativeFilePickerCustomTypes>( INITIAL_SAVE_PATH );
Debug.Log( "Created NativeFilePickerCustomTypes file at " + INITIAL_SAVE_PATH + ". You can move this file around freely.", instance );
}
return instance;
}
public static TypeHolder[] GetCustomTypes()
{
NativeFilePickerCustomTypes instance = GetInstance( false );
return instance ? instance.customTypes : null;
}
}
}
@@ -1,19 +0,0 @@
fileFormatVersion: 2
guid: f281670325986984c95fde82c23195e0
timeCreated: 1592237398
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/Editor/NativeFilePickerCustomTypes.cs
uploadId: 757770
@@ -1,199 +0,0 @@
using System.IO;
using UnityEditor;
using UnityEngine;
#if UNITY_IOS
using System.Collections.Generic;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
#endif
namespace NativeFilePickerNamespace
{
[System.Serializable]
public class Settings
{
private const string SAVE_PATH = "ProjectSettings/NativeFilePicker.json";
public bool AutoSetupFrameworks = true;
public bool AutoSetupiCloud = false;
private static Settings m_instance = null;
public static Settings Instance
{
get
{
if( m_instance == null )
{
try
{
if( File.Exists( SAVE_PATH ) )
m_instance = JsonUtility.FromJson<Settings>( File.ReadAllText( SAVE_PATH ) );
else
m_instance = new Settings();
}
catch( System.Exception e )
{
Debug.LogException( e );
m_instance = new Settings();
}
}
return m_instance;
}
}
public void Save()
{
File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) );
}
[SettingsProvider]
public static SettingsProvider CreatePreferencesGUI()
{
return new SettingsProvider( "Project/yasirkula/Native File Picker", SettingsScope.Project )
{
guiHandler = ( searchContext ) => PreferencesGUI(),
keywords = new System.Collections.Generic.HashSet<string>() { "Native", "File", "Picker", "Android", "iOS" }
};
}
public static void PreferencesGUI()
{
EditorGUI.BeginChangeCheck();
Instance.AutoSetupFrameworks = EditorGUILayout.Toggle( new GUIContent( "Auto Setup Frameworks", "Automatically adds MobileCoreServices and CloudKit frameworks to the generated Xcode project" ), Instance.AutoSetupFrameworks );
Instance.AutoSetupiCloud = EditorGUILayout.Toggle( new GUIContent( "Auto Setup iCloud", "Automatically enables iCloud capability of the generated Xcode project" ), Instance.AutoSetupiCloud );
if( EditorGUI.EndChangeCheck() )
Instance.Save();
}
}
public class NativeFilePickerPostProcessBuild
{
#if UNITY_IOS
#pragma warning disable 0162
[PostProcessBuild]
public static void OnPostprocessBuild( BuildTarget target, string buildPath )
{
// Add declared custom types to Info.plist
if( target == BuildTarget.iOS )
{
NativeFilePickerCustomTypes.TypeHolder[] customTypes = NativeFilePickerCustomTypes.GetCustomTypes();
if( customTypes != null )
{
string plistPath = Path.Combine( buildPath, "Info.plist" );
PlistDocument plist = new PlistDocument();
plist.ReadFromString( File.ReadAllText( plistPath ) );
PlistElementDict rootDict = plist.root;
for( int i = 0; i < customTypes.Length; i++ )
{
NativeFilePickerCustomTypes.TypeHolder customType = customTypes[i];
PlistElementArray customTypesArray = GetCustomTypesArray( rootDict, customType.isExported );
// Don't allow duplicate entries
RemoveCustomTypeIfExists( customTypesArray, customType.identifier );
PlistElementDict customTypeDict = customTypesArray.AddDict();
customTypeDict.SetString( "UTTypeIdentifier", customType.identifier );
customTypeDict.SetString( "UTTypeDescription", customType.description );
PlistElementArray conformsTo = customTypeDict.CreateArray( "UTTypeConformsTo" );
for( int j = 0; j < customType.conformsTo.Length; j++ )
conformsTo.AddString( customType.conformsTo[j] );
PlistElementDict tagSpecification = customTypeDict.CreateDict( "UTTypeTagSpecification" );
PlistElementArray tagExtensions = tagSpecification.CreateArray( "public.filename-extension" );
for( int j = 0; j < customType.extensions.Length; j++ )
tagExtensions.AddString( customType.extensions[j] );
}
File.WriteAllText( plistPath, plist.WriteToString() );
}
}
// Rest of the function shouldn't execute unless build post-processing is enabled
if( !Settings.Instance.AutoSetupFrameworks && !Settings.Instance.AutoSetupiCloud )
return;
if( target == BuildTarget.iOS )
{
string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath );
PBXProject pbxProject = new PBXProject();
pbxProject.ReadFromFile( pbxProjectPath );
string targetGUID = pbxProject.GetUnityFrameworkTargetGuid();
if( Settings.Instance.AutoSetupFrameworks )
{
pbxProject.AddFrameworkToProject( targetGUID, "MobileCoreServices.framework", false );
pbxProject.AddFrameworkToProject( targetGUID, "CloudKit.framework", false );
}
File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
if( Settings.Instance.AutoSetupiCloud )
{
ProjectCapabilityManager manager = new ProjectCapabilityManager( pbxProjectPath, "iCloud.entitlements", "Unity-iPhone" );
manager.AddiCloud( false, true, false, true, null );
manager.WriteToFile();
}
}
}
// Adding PRODUCT_BUNDLE_IDENTIFIER if not exists (if another plugin also fills this value, we must not touch it)
[PostProcessBuild( 99 )]
public static void OnPostprocessBuild2( BuildTarget target, string buildPath )
{
if( !Settings.Instance.AutoSetupFrameworks && !Settings.Instance.AutoSetupiCloud )
return;
if( target == BuildTarget.iOS )
{
string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath );
PBXProject pbxProject = new PBXProject();
pbxProject.ReadFromFile( pbxProjectPath );
string targetGUID = pbxProject.GetUnityFrameworkTargetGuid();
if( string.IsNullOrEmpty( pbxProject.GetBuildPropertyForAnyConfig( targetGUID, "PRODUCT_BUNDLE_IDENTIFIER" ) ) )
pbxProject.AddBuildProperty( targetGUID, "PRODUCT_BUNDLE_IDENTIFIER", PlayerSettings.applicationIdentifier );
File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
}
}
private static PlistElementArray GetCustomTypesArray( PlistElementDict rootDict, bool isExported )
{
string key = isExported ? "UTExportedTypeDeclarations" : "UTImportedTypeDeclarations";
PlistElementArray result = rootDict[key] as PlistElementArray;
if( result == null )
result = rootDict.CreateArray( key );
return result;
}
private static void RemoveCustomTypeIfExists( PlistElementArray customTypesArray, string UTI )
{
List<PlistElement> values = customTypesArray.values;
if( values == null )
return;
for( int i = values.Count - 1; i >= 0; i-- )
{
PlistElementDict exportedType = values[i] as PlistElementDict;
if( exportedType != null )
{
PlistElementString exportedTypeID = exportedType["UTTypeIdentifier"] as PlistElementString;
if( exportedTypeID != null && exportedTypeID.value == UTI )
values.RemoveAt( i );
}
}
}
#pragma warning restore 0162
#endif
}
}
@@ -1,3 +0,0 @@
{
"name": "NativeFilePicker.Runtime"
}
@@ -1,14 +0,0 @@
fileFormatVersion: 2
guid: 250e096504da3aa4f851a4df12262950
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/NativeFilePicker.Runtime.asmdef
uploadId: 757770
@@ -1,411 +0,0 @@
using System;
using System.IO;
using UnityEngine;
using System.Threading.Tasks;
#if UNITY_ANDROID || UNITY_IOS
using NativeFilePickerNamespace;
#endif
public static class NativeFilePicker
{
public delegate void PermissionCallback( Permission permission );
public delegate void FilePickedCallback( string path );
public delegate void MultipleFilesPickedCallback( string[] paths );
public delegate void FilesExportedCallback( bool success );
public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
#region Platform Specific Elements
#if !UNITY_EDITOR && UNITY_ANDROID
private static AndroidJavaClass m_ajc = null;
private static AndroidJavaClass AJC
{
get
{
if( m_ajc == null )
m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeFilePicker" );
return m_ajc;
}
}
private static AndroidJavaObject m_context = null;
private static AndroidJavaObject Context
{
get
{
if( m_context == null )
{
using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
{
m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
}
}
return m_context;
}
}
#elif !UNITY_EDITOR && UNITY_IOS
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeFilePicker_CanPickMultipleFiles();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeFilePicker_ConvertExtensionToUTI( string extension );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeFilePicker_PickFile( string[] UTIs, int UTIsCount );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeFilePicker_PickMultipleFiles( string[] UTIs, int UTIsCount );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeFilePicker_ExportFiles( string[] files, int filesCount );
#endif
#endregion
#if !UNITY_EDITOR && UNITY_ANDROID
private static string m_selectedFilePath = null;
private static string SelectedFilePath
{
get
{
if( m_selectedFilePath == null )
{
m_selectedFilePath = Path.Combine( Application.temporaryCachePath, "pickedFile" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_selectedFilePath;
}
}
#endif
#region Runtime Permissions
public static bool CheckPermission( bool readPermissionOnly = false )
{
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<int>( "CheckPermission", Context, readPermissionOnly ) == 1;
#else
return true;
#endif
}
public static void RequestPermissionAsync( PermissionCallback callback, bool readPermissionOnly = false )
{
#if !UNITY_EDITOR && UNITY_ANDROID
FPPermissionCallbackAndroid nativeCallback = new( callback );
AJC.CallStatic( "RequestPermission", Context, nativeCallback, readPermissionOnly );
#else
callback( Permission.Granted );
#endif
}
public static Task<Permission> RequestPermissionAsync( bool readPermissionOnly = false )
{
TaskCompletionSource<Permission> tcs = new TaskCompletionSource<Permission>();
RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), readPermissionOnly );
return tcs.Task;
}
public static void OpenSettings()
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "OpenSettings", Context );
#endif
}
#endregion
#region Helper Functions
public static bool CanPickMultipleFiles()
{
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<bool>( "CanPickMultipleFiles" );
#elif !UNITY_EDITOR && UNITY_IOS
return _NativeFilePicker_CanPickMultipleFiles() == 1;
#else
return false;
#endif
}
public static bool CanExportFiles()
{
#if UNITY_EDITOR
return true;
#elif UNITY_ANDROID
return AJC.CallStatic<bool>( "CanExportFiles" );
#elif UNITY_IOS
return true;
#else
return false;
#endif
}
public static bool CanExportMultipleFiles()
{
#if UNITY_EDITOR
return true;
#elif UNITY_ANDROID
return AJC.CallStatic<bool>( "CanExportMultipleFiles" );
#elif UNITY_IOS
return _NativeFilePicker_CanPickMultipleFiles() == 1;
#else
return false;
#endif
}
public static bool IsFilePickerBusy()
{
#if !UNITY_EDITOR && UNITY_IOS
return FPResultCallbackiOS.IsBusy;
#else
return false;
#endif
}
public static string ConvertExtensionToFileType( string extension )
{
if( string.IsNullOrEmpty( extension ) )
return null;
if( extension.IndexOf( '*' ) >= 0 )
{
// So many users try to do this that it's now necessary to throw an exception for this particular scenario
throw new ArgumentException( "See: https://github.com/yasirkula/UnityNativeFilePicker#faq" );
}
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<string>( "GetMimeTypeFromExtension", extension.ToLowerInvariant() );
#elif !UNITY_EDITOR && UNITY_IOS
return _NativeFilePicker_ConvertExtensionToUTI( extension.ToLowerInvariant() );
#else
return extension;
#endif
}
#endregion
#region Import Functions
public static void PickFile( FilePickedCallback callback, params string[] allowedFileTypes )
{
// If no file type is specified, allow all file types
if( allowedFileTypes == null || allowedFileTypes.Length == 0 )
{
#if UNITY_ANDROID
allowedFileTypes = new string[] { "*/*" };
#else
allowedFileTypes = new string[] { "public.item", "public.content" };
#endif
}
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsFilePickerBusy() )
{
callback?.Invoke( null );
return;
}
#if UNITY_EDITOR
// Accept Android and iOS UTIs when possible, for user's convenience
string[] editorFilters = new string[allowedFileTypes.Length * 2];
for( int i = 0; i < allowedFileTypes.Length; i++ )
{
if( allowedFileTypes[i].IndexOf( '*' ) >= 0 )
{
if( allowedFileTypes[i] == "image/*" )
{
editorFilters[i * 2] = "Image files";
editorFilters[i * 2 + 1] = "png,jpg,jpeg";
}
else if( allowedFileTypes[i] == "video/*" )
{
editorFilters[i * 2] = "Video files";
editorFilters[i * 2 + 1] = "mp4,mov,webm,avi";
}
else if( allowedFileTypes[i] == "audio/*" )
{
editorFilters[i * 2] = "Audio files";
editorFilters[i * 2 + 1] = "mp3,wav,aac,flac";
}
else
{
editorFilters[i * 2] = "All files";
editorFilters[i * 2 + 1] = "*";
}
}
else
{
editorFilters[i * 2] = allowedFileTypes[i];
if( allowedFileTypes[i].IndexOf( '/' ) >= 0 ) // Android UTIs like 'image/png'
editorFilters[i * 2 + 1] = allowedFileTypes[i].Substring( allowedFileTypes[i].IndexOf( '/' ) + 1 );
else if( allowedFileTypes[i].StartsWith( "public." ) ) // iOS UTIs like 'public.png'
editorFilters[i * 2 + 1] = allowedFileTypes[i].Substring( 7 );
else if( allowedFileTypes[i].IndexOf( '.' ) == 0 ) // Extensions starting with period like '.png'
editorFilters[i * 2 + 1] = allowedFileTypes[i].Substring( 1 );
else
editorFilters[i * 2 + 1] = allowedFileTypes[i];
}
}
string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters );
if( callback != null )
callback( pickedFile != "" ? pickedFile : null );
#elif UNITY_ANDROID
AJC.CallStatic( "PickFiles", Context, new FPResultCallbackAndroid( callback, null, null ), false, SelectedFilePath, allowedFileTypes, "" );
#elif UNITY_IOS
FPResultCallbackiOS.Initialize( callback, null, null );
_NativeFilePicker_PickFile( allowedFileTypes, allowedFileTypes.Length );
#else
if( callback != null )
callback( null );
#endif
}, true );
}
public static void PickMultipleFiles( MultipleFilesPickedCallback callback, params string[] allowedFileTypes )
{
// If no file type is specified, allow all file types
if( allowedFileTypes == null || allowedFileTypes.Length == 0 )
{
#if UNITY_ANDROID
allowedFileTypes = new string[] { "*/*" };
#else
allowedFileTypes = new string[] { "public.item", "public.content" };
#endif
}
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsFilePickerBusy() )
{
callback?.Invoke( null );
return;
}
if( CanPickMultipleFiles() )
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "PickFiles", Context, new FPResultCallbackAndroid( null, callback, null ), true, SelectedFilePath, allowedFileTypes, "" );
#elif !UNITY_EDITOR && UNITY_IOS
FPResultCallbackiOS.Initialize( null, callback, null );
_NativeFilePicker_PickMultipleFiles( allowedFileTypes, allowedFileTypes.Length );
#endif
}
else if( callback != null )
callback( null );
}, true );
}
#endregion
#region Export Functions
public static void ExportFile( string filePath, FilesExportedCallback callback = null )
{
if( string.IsNullOrEmpty( filePath ) )
throw new ArgumentException( "Parameter 'filePath' is null or empty!" );
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsFilePickerBusy() )
{
callback?.Invoke( false );
return;
}
if( CanExportFiles() )
{
#if UNITY_EDITOR
string extension = Path.GetExtension( filePath );
if( extension == null )
extension = "";
else if( extension.IndexOf( '.' ) == 0 )
extension = extension.Substring( 1 );
string destination = UnityEditor.EditorUtility.SaveFilePanel( "Select destination", Path.GetDirectoryName( filePath ), Path.GetFileName( filePath ), extension );
if( string.IsNullOrEmpty( destination ) )
{
if( callback != null )
callback( false );
}
else
{
try
{
File.Copy( filePath, destination, true );
if( callback != null )
callback( true );
}
catch( Exception e )
{
Debug.LogException( e );
if( callback != null )
callback( false );
}
}
#elif UNITY_ANDROID
AJC.CallStatic( "ExportFiles", Context, new FPResultCallbackAndroid( null, null, callback ), new string[1] { filePath }, 1 );
#elif UNITY_IOS
FPResultCallbackiOS.Initialize( null, null, callback );
_NativeFilePicker_ExportFiles( new string[1] { filePath }, 1 );
#endif
}
else if( callback != null )
callback( false );
}, false );
}
public static void ExportMultipleFiles( string[] filePaths, FilesExportedCallback callback = null )
{
if( filePaths == null || filePaths.Length == 0 )
throw new ArgumentException( "Parameter 'filePaths' is null or empty!" );
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsFilePickerBusy() )
{
callback?.Invoke( false );
return;
}
if( CanExportMultipleFiles() )
{
#if UNITY_EDITOR
string destination = UnityEditor.EditorUtility.OpenFolderPanel( "Select destination", Path.GetDirectoryName( filePaths[0] ), "" );
if( string.IsNullOrEmpty( destination ) )
{
if( callback != null )
callback( false );
}
else
{
try
{
for( int i = 0; i < filePaths.Length; i++ )
File.Copy( filePaths[i], Path.Combine( destination, Path.GetFileName( filePaths[i] ) ), true );
if( callback != null )
callback( true );
}
catch( Exception e )
{
Debug.LogException( e );
if( callback != null )
callback( false );
}
}
#elif UNITY_ANDROID
AJC.CallStatic( "ExportFiles", Context, new FPResultCallbackAndroid( null, null, callback ), filePaths, filePaths.Length );
#elif UNITY_IOS
FPResultCallbackiOS.Initialize( null, null, callback );
_NativeFilePicker_ExportFiles( filePaths, filePaths.Length );
#endif
}
else if( callback != null )
callback( false );
}, false );
}
#endregion
}
@@ -1,6 +0,0 @@
= Native File Picker for Android & iOS (v1.4.1) =
Documentation: https://github.com/yasirkula/UnityNativeFilePicker
FAQ: https://github.com/yasirkula/UnityNativeFilePicker#faq
Example code: https://github.com/yasirkula/UnityNativeFilePicker#example-code
E-mail: yasirkula@gmail.com
@@ -1,15 +0,0 @@
fileFormatVersion: 2
guid: a0357092d6d35434c8b136abfe2b6260
timeCreated: 1563308465
licenseType: Store
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 173238
packageName: Native File Picker for Android & iOS
packageVersion: 1.4.1
assetPath: Assets/Plugins/NativeFilePicker/README.txt
uploadId: 757770
@@ -1,155 +0,0 @@
#if UNITY_EDITOR || UNITY_IOS
using UnityEngine;
namespace NativeFilePickerNamespace
{
public class FPResultCallbackiOS : MonoBehaviour
{
private static FPResultCallbackiOS instance;
private NativeFilePicker.FilePickedCallback pickCallback;
private NativeFilePicker.MultipleFilesPickedCallback pickCallbackMultiple;
private NativeFilePicker.FilesExportedCallback exportCallback;
private float nextBusyCheckTime;
public static bool IsBusy { get; private set; }
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeFilePicker_IsFilePickerBusy();
public static void Initialize( NativeFilePicker.FilePickedCallback pickCallback, NativeFilePicker.MultipleFilesPickedCallback pickCallbackMultiple, NativeFilePicker.FilesExportedCallback exportCallback )
{
if( IsBusy )
return;
if( instance == null )
{
instance = new GameObject( "FPResultCallbackiOS" ).AddComponent<FPResultCallbackiOS>();
DontDestroyOnLoad( instance.gameObject );
}
instance.pickCallback = pickCallback;
instance.pickCallbackMultiple = pickCallbackMultiple;
instance.exportCallback = exportCallback;
instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
IsBusy = true;
}
private void Update()
{
if( IsBusy )
{
if( Time.realtimeSinceStartup >= nextBusyCheckTime )
{
nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
if( _NativeFilePicker_IsFilePickerBusy() == 0 )
OnOperationCancelled( null );
}
}
}
[UnityEngine.Scripting.Preserve]
public void OnFilePicked( string path )
{
IsBusy = false;
if( string.IsNullOrEmpty( path ) )
path = null;
NativeFilePicker.FilePickedCallback _pickCallback = pickCallback;
pickCallback = null;
if( _pickCallback != null )
_pickCallback( path );
}
[UnityEngine.Scripting.Preserve]
public void OnMultipleFilesPicked( string paths )
{
IsBusy = false;
string[] _paths = SplitPaths( paths );
if( _paths != null && _paths.Length == 0 )
_paths = null;
NativeFilePicker.MultipleFilesPickedCallback _pickCallbackMultiple = pickCallbackMultiple;
pickCallbackMultiple = null;
if( _pickCallbackMultiple != null )
_pickCallbackMultiple( _paths );
}
[UnityEngine.Scripting.Preserve]
public void OnFilesExported( string message )
{
IsBusy = false;
NativeFilePicker.FilesExportedCallback _exportCallback = exportCallback;
exportCallback = null;
if( _exportCallback != null )
_exportCallback( message == "1" );
}
[UnityEngine.Scripting.Preserve]
public void OnOperationCancelled( string message )
{
IsBusy = false;
NativeFilePicker.FilePickedCallback _pickCallback = pickCallback;
NativeFilePicker.MultipleFilesPickedCallback _pickCallbackMultiple = pickCallbackMultiple;
NativeFilePicker.FilesExportedCallback _exportCallback = exportCallback;
pickCallback = null;
pickCallbackMultiple = null;
exportCallback = null;
if( _pickCallback != null )
_pickCallback( null );
if( _pickCallbackMultiple != null )
_pickCallbackMultiple( null );
if( _exportCallback != null )
_exportCallback( false );
}
private string[] SplitPaths( string paths )
{
string[] result = null;
if( !string.IsNullOrEmpty( paths ) )
{
string[] pathsSplit = paths.Split( '>' );
int validPathCount = 0;
for( int i = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPathCount++;
}
if( validPathCount == 0 )
pathsSplit = new string[0];
else if( validPathCount != pathsSplit.Length )
{
string[] validPaths = new string[validPathCount];
for( int i = 0, j = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPaths[j++] = pathsSplit[i];
}
pathsSplit = validPaths;
}
result = pathsSplit;
}
return result;
}
}
}
#endif
@@ -1,213 +0,0 @@
#import <MobileCoreServices/MobileCoreServices.h>
extern UIViewController* UnityGetGLViewController();
@interface UNativeFilePicker:NSObject
+ (void)pickFiles:(BOOL)allowMultipleSelection withUTIs:(NSArray<NSString *> *)allowedUTIs;
+ (void)exportFiles:(NSArray<NSURL *> *)paths;
+ (int)canPickMultipleFiles;
+ (int)isFilePickerBusy;
+ (char *)convertExtensionToUTI:(NSString *)extension;
@end
@implementation UNativeFilePicker
static UIDocumentPickerViewController *filePicker;
static BOOL pickingMultipleFiles;
static int filePickerState = 0; // 0 -> none, 1 -> showing, 2 -> finished
+ (void)pickFiles:(BOOL)allowMultipleSelection withUTIs:(NSArray<NSString *> *)allowedUTIs
{
filePicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:allowedUTIs inMode:UIDocumentPickerModeImport];
filePicker.delegate = (id) self;
if( @available(iOS 11.0, *) )
filePicker.allowsMultipleSelection = allowMultipleSelection;
// Show file extensions if possible
if( @available(iOS 13.0, *) )
filePicker.shouldShowFileExtensions = YES;
pickingMultipleFiles = allowMultipleSelection;
filePickerState = 1;
[UnityGetGLViewController() presentViewController:filePicker animated:NO completion:^{ filePickerState = 0; }];
}
+ (void)exportFiles:(NSArray<NSURL *> *)paths
{
if( paths != nil && [paths count] > 0 )
{
if ([paths count] > 1 && [self canPickMultipleFiles] == 1)
filePicker = [[UIDocumentPickerViewController alloc] initWithURLs:paths inMode:UIDocumentPickerModeExportToService];
else
filePicker = [[UIDocumentPickerViewController alloc] initWithURL:paths[0] inMode:UIDocumentPickerModeExportToService];
filePicker.delegate = (id) self;
// Show file extensions if possible
if( @available(iOS 13.0, *) )
filePicker.shouldShowFileExtensions = YES;
filePickerState = 1;
[UnityGetGLViewController() presentViewController:filePicker animated:NO completion:^{ filePickerState = 0; }];
}
}
+ (int)canPickMultipleFiles
{
if( @available(iOS 11.0, *) )
return 1;
return 0;
}
+ (int)isFilePickerBusy
{
if( filePickerState == 2 )
return 1;
if( filePicker != nil )
{
if( filePickerState == 1 || [filePicker presentingViewController] == UnityGetGLViewController() )
return 1;
else
{
filePicker = nil;
return 0;
}
}
else
return 0;
}
// Credit: https://lists.apple.com/archives/cocoa-dev/2012/Jan/msg00052.html
+ (char *)convertExtensionToUTI:(NSString *)extension
{
CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (__bridge CFStringRef) extension, NULL );
char *result = [self getCString:(__bridge NSString *) fileUTI];
CFRelease( fileUTI );
return result;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
{
[self documentPickerCompleted:controller documents:@[url]];
}
#pragma clang diagnostic pop
+ (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
[self documentPickerCompleted:controller documents:urls];
}
+ (void)documentPickerCompleted:(UIDocumentPickerViewController *)controller documents:(NSArray<NSURL *> *)urls
{
filePicker = nil;
filePickerState = 2;
if( controller.documentPickerMode == UIDocumentPickerModeImport )
{
if( !pickingMultipleFiles || [urls count] <= 1 )
{
const char* filePath;
if( [urls count] == 0 )
filePath = "";
else
filePath = [self getCString:[urls[0] path]];
if( pickingMultipleFiles )
UnitySendMessage( "FPResultCallbackiOS", "OnMultipleFilesPicked", filePath );
else
UnitySendMessage( "FPResultCallbackiOS", "OnFilePicked", filePath );
}
else
{
NSMutableArray<NSString *> *filePaths = [NSMutableArray arrayWithCapacity:[urls count]];
for( int i = 0; i < [urls count]; i++ )
[filePaths addObject:[urls[i] path]];
UnitySendMessage( "FPResultCallbackiOS", "OnMultipleFilesPicked", [self getCString:[filePaths componentsJoinedByString:@">"]] );
}
}
else if( controller.documentPickerMode == UIDocumentPickerModeExportToService )
{
if( [urls count] > 0 )
UnitySendMessage( "FPResultCallbackiOS", "OnFilesExported", "1" );
else
UnitySendMessage( "FPResultCallbackiOS", "OnFilesExported", "0" );
}
[controller dismissViewControllerAnimated:NO completion:nil];
}
+ (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller
{
filePicker = nil;
UnitySendMessage( "FPResultCallbackiOS", "OnOperationCancelled", "" );
[controller dismissViewControllerAnimated:NO completion:nil];
}
// Credit: https://stackoverflow.com/a/37052118/2373034
+ (char *)getCString:(NSString *)source
{
if( source == nil )
source = @"";
const char *sourceUTF8 = [source UTF8String];
char *result = (char*) malloc( strlen( sourceUTF8 ) + 1 );
strcpy(result, sourceUTF8);
return result;
}
@end
extern "C" void _NativeFilePicker_PickFile( const char* UTIs[], int UTIsCount )
{
NSMutableArray<NSString *> *allowedUTIs = [NSMutableArray arrayWithCapacity:UTIsCount];
for( int i = 0; i < UTIsCount; i++ )
[allowedUTIs addObject:[NSString stringWithUTF8String:UTIs[i]]];
[UNativeFilePicker pickFiles:NO withUTIs:allowedUTIs];
}
extern "C" void _NativeFilePicker_PickMultipleFiles( const char* UTIs[], int UTIsCount )
{
NSMutableArray<NSString *> *allowedUTIs = [NSMutableArray arrayWithCapacity:UTIsCount];
for( int i = 0; i < UTIsCount; i++ )
[allowedUTIs addObject:[NSString stringWithUTF8String:UTIs[i]]];
[UNativeFilePicker pickFiles:YES withUTIs:allowedUTIs];
}
extern "C" void _NativeFilePicker_ExportFiles( const char* files[], int filesCount )
{
NSMutableArray<NSURL *> *paths = [NSMutableArray arrayWithCapacity:filesCount];
for( int i = 0; i < filesCount; i++ )
{
NSString *filePath = [NSString stringWithUTF8String:files[i]];
[paths addObject:[NSURL fileURLWithPath:filePath]];
}
[UNativeFilePicker exportFiles:paths];
}
extern "C" int _NativeFilePicker_CanPickMultipleFiles()
{
return [UNativeFilePicker canPickMultipleFiles];
}
extern "C" int _NativeFilePicker_IsFilePickerBusy()
{
return [UNativeFilePicker isFilePickerBusy];
}
extern "C" char* _NativeFilePicker_ConvertExtensionToUTI( const char* extension )
{
return [UNativeFilePicker convertExtensionToUTI:[NSString stringWithUTF8String:extension]];
}
+9
View File
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 5e05ed2bddbccb94e9650efb5742e452
folderAsset: yes
timeCreated: 1518877529
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 0a607dcda26e7614f86300c6ca717295
folderAsset: yes
timeCreated: 1498722617
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
#if UNITY_EDITOR || UNITY_ANDROID
using System;
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGCallbackHelper : MonoBehaviour
{
private bool autoDestroyWithCallback;
private Action mainThreadAction = null;
public static NGCallbackHelper Create( bool autoDestroyWithCallback )
{
NGCallbackHelper result = new GameObject( "NGCallbackHelper" ).AddComponent<NGCallbackHelper>();
result.autoDestroyWithCallback = autoDestroyWithCallback;
DontDestroyOnLoad( result.gameObject );
return result;
}
public void CallOnMainThread( Action function )
{
lock( this )
{
mainThreadAction += function;
}
}
private void Update()
{
if( mainThreadAction != null )
{
try
{
Action temp;
lock( this )
{
temp = mainThreadAction;
mainThreadAction = null;
}
temp();
}
finally
{
if( autoDestroyWithCallback )
Destroy( gameObject );
}
}
}
}
}
#endif
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: 2d517fd0f2f85f24698df2775bee58e9
timeCreated: 1544889149
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs
uploadId: 860494
@@ -0,0 +1,62 @@
#if UNITY_EDITOR || UNITY_ANDROID
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGMediaReceiveCallbackAndroid : AndroidJavaProxy
{
private readonly NativeGallery.MediaPickCallback callback;
private readonly NativeGallery.MediaPickMultipleCallback callbackMultiple;
private readonly NGCallbackHelper callbackHelper;
public NGMediaReceiveCallbackAndroid( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple ) : base( "com.yasirkula.unity.NativeGalleryMediaReceiver" )
{
this.callback = callback;
this.callbackMultiple = callbackMultiple;
callbackHelper = NGCallbackHelper.Create( true );
}
[UnityEngine.Scripting.Preserve]
public void OnMediaReceived( string path )
{
callbackHelper.CallOnMainThread( () => callback( !string.IsNullOrEmpty( path ) ? path : null ) );
}
[UnityEngine.Scripting.Preserve]
public void OnMultipleMediaReceived( string paths )
{
string[] result = null;
if( !string.IsNullOrEmpty( paths ) )
{
string[] pathsSplit = paths.Split( '>' );
int validPathCount = 0;
for( int i = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPathCount++;
}
if( validPathCount == 0 )
pathsSplit = new string[0];
else if( validPathCount != pathsSplit.Length )
{
string[] validPaths = new string[validPathCount];
for( int i = 0, j = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPaths[j++] = pathsSplit[i];
}
pathsSplit = validPaths;
}
result = pathsSplit;
}
callbackHelper.CallOnMainThread( () => callbackMultiple( ( result != null && result.Length > 0 ) ? result : null ) );
}
}
}
#endif
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: 4c18d702b07a63945968db47201b95c9
timeCreated: 1519060539
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs
uploadId: 860494
@@ -0,0 +1,24 @@
#if UNITY_EDITOR || UNITY_ANDROID
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGPermissionCallbackAndroid : AndroidJavaProxy
{
private readonly NativeGallery.PermissionCallback callback;
private readonly NGCallbackHelper callbackHelper;
public NGPermissionCallbackAndroid( NativeGallery.PermissionCallback callback ) : base( "com.yasirkula.unity.NativeGalleryPermissionReceiver" )
{
this.callback = callback;
callbackHelper = NGCallbackHelper.Create( true );
}
[UnityEngine.Scripting.Preserve]
public void OnPermissionResult( int result )
{
callbackHelper.CallOnMainThread( () => callback( (NativeGallery.Permission) result ) );
}
}
}
#endif
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: a07afac614af1294d8e72a3c083be028
timeCreated: 1519060539
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs
uploadId: 860494
@@ -0,0 +1,40 @@
fileFormatVersion: 2
guid: db4d55e1212537e4baa84cac66eb6645
timeCreated: 1569764737
licenseType: Store
PluginImporter:
serializedVersion: 2
iconMap: {}
executionOrder: {}
isPreloaded: 0
isOverridable: 0
platformData:
data:
first:
Android: Android
second:
enabled: 1
settings: {}
data:
first:
Any:
second:
enabled: 0
settings: {}
data:
first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/Android/NativeGallery.aar
uploadId: 860494
+9
View File
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 19fc6b8ce781591438a952d8aa9104f8
folderAsset: yes
timeCreated: 1521452097
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,119 @@
using System.IO;
using UnityEditor;
using UnityEngine;
#if UNITY_IOS
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
#endif
namespace NativeGalleryNamespace
{
[System.Serializable]
public class Settings
{
private const string SAVE_PATH = "ProjectSettings/NativeGallery.json";
public bool AutomatedSetup = true;
public string PhotoLibraryUsageDescription = "The app requires access to Photos to interact with it.";
public string PhotoLibraryAdditionsUsageDescription = "The app requires access to Photos to save media to it.";
public bool DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = true; // See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/
private static Settings m_instance = null;
public static Settings Instance
{
get
{
if( m_instance == null )
{
try
{
if( File.Exists( SAVE_PATH ) )
m_instance = JsonUtility.FromJson<Settings>( File.ReadAllText( SAVE_PATH ) );
else
m_instance = new Settings();
}
catch( System.Exception e )
{
Debug.LogException( e );
m_instance = new Settings();
}
}
return m_instance;
}
}
public void Save()
{
File.WriteAllText( SAVE_PATH, JsonUtility.ToJson( this, true ) );
}
[SettingsProvider]
public static SettingsProvider CreatePreferencesGUI()
{
return new SettingsProvider( "Project/yasirkula/Native Gallery", SettingsScope.Project )
{
guiHandler = ( searchContext ) => PreferencesGUI(),
keywords = new System.Collections.Generic.HashSet<string>() { "Native", "Gallery", "Android", "iOS" }
};
}
public static void PreferencesGUI()
{
EditorGUI.BeginChangeCheck();
Instance.AutomatedSetup = EditorGUILayout.Toggle( "Automated Setup", Instance.AutomatedSetup );
EditorGUI.BeginDisabledGroup( !Instance.AutomatedSetup );
Instance.PhotoLibraryUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Usage Description", Instance.PhotoLibraryUsageDescription );
Instance.PhotoLibraryAdditionsUsageDescription = EditorGUILayout.DelayedTextField( "Photo Library Additions Usage Description", Instance.PhotoLibraryAdditionsUsageDescription );
Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 = EditorGUILayout.Toggle( new GUIContent( "Don't Ask Limited Photos Permission Automatically", "See: https://mackuba.eu/2020/07/07/photo-library-changes-ios-14/. It's recommended to keep this setting enabled" ), Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 );
EditorGUI.EndDisabledGroup();
if( EditorGUI.EndChangeCheck() )
Instance.Save();
}
}
public class NGPostProcessBuild
{
#if UNITY_IOS
[PostProcessBuild( 1 )]
public static void OnPostprocessBuild( BuildTarget target, string buildPath )
{
if( !Settings.Instance.AutomatedSetup )
return;
if( target == BuildTarget.iOS )
{
string pbxProjectPath = PBXProject.GetPBXProjectPath( buildPath );
string plistPath = Path.Combine( buildPath, "Info.plist" );
PBXProject pbxProject = new PBXProject();
pbxProject.ReadFromFile( pbxProjectPath );
string targetGUID = pbxProject.GetUnityFrameworkTargetGuid();
pbxProject.AddFrameworkToProject( targetGUID, "PhotosUI.framework", true );
pbxProject.AddFrameworkToProject( targetGUID, "Photos.framework", false );
pbxProject.AddFrameworkToProject( targetGUID, "MobileCoreServices.framework", false );
pbxProject.AddFrameworkToProject( targetGUID, "ImageIO.framework", false );
File.WriteAllText( pbxProjectPath, pbxProject.WriteToString() );
PlistDocument plist = new PlistDocument();
plist.ReadFromString( File.ReadAllText( plistPath ) );
PlistElementDict rootDict = plist.root;
if( !string.IsNullOrEmpty( Settings.Instance.PhotoLibraryUsageDescription ) )
rootDict.SetString( "NSPhotoLibraryUsageDescription", Settings.Instance.PhotoLibraryUsageDescription );
if( !string.IsNullOrEmpty( Settings.Instance.PhotoLibraryAdditionsUsageDescription ) )
rootDict.SetString( "NSPhotoLibraryAddUsageDescription", Settings.Instance.PhotoLibraryAdditionsUsageDescription );
if( Settings.Instance.DontAskLimitedPhotosPermissionAutomaticallyOnIos14 )
rootDict.SetBoolean( "PHPhotoLibraryPreventAutomaticLimitedAccessAlert", true );
File.WriteAllText( plistPath, plist.WriteToString() );
}
}
#endif
}
}
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: dff1540cf22bfb749a2422f445cf9427
timeCreated: 1521452119
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs
uploadId: 860494
@@ -0,0 +1,15 @@
{
"name": "NativeGallery.Editor",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 3dffc8e654f00c545a82d0a5274d51eb
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef
uploadId: 860494
@@ -0,0 +1,3 @@
{
"name": "NativeGallery.Runtime"
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 6e5063adab271564ba0098a06a8cebda
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef
uploadId: 860494
@@ -0,0 +1,988 @@
using System;
using System.Globalization;
using System.IO;
using UnityEngine;
using System.Threading.Tasks;
using UnityEngine.Networking;
#if UNITY_ANDROID || UNITY_IOS
using NativeGalleryNamespace;
#endif
using Object = UnityEngine.Object;
public static class NativeGallery
{
public struct ImageProperties
{
public readonly int width;
public readonly int height;
public readonly string mimeType;
public readonly ImageOrientation orientation;
public ImageProperties( int width, int height, string mimeType, ImageOrientation orientation )
{
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.orientation = orientation;
}
}
public struct VideoProperties
{
public readonly int width;
public readonly int height;
public readonly long duration;
public readonly float rotation;
public VideoProperties( int width, int height, long duration, float rotation )
{
this.width = width;
this.height = height;
this.duration = duration;
this.rotation = rotation;
}
}
public enum PermissionType { Read = 0, Write = 1 };
public enum Permission { Denied = 0, Granted = 1, ShouldAsk = 2 };
[Flags]
public enum MediaType { Image = 1, Video = 2, Audio = 4 };
// EXIF orientation: http://sylvana.net/jpegcrop/exif_orientation.html (indices are reordered)
public enum ImageOrientation { Unknown = -1, Normal = 0, Rotate90 = 1, Rotate180 = 2, Rotate270 = 3, FlipHorizontal = 4, Transpose = 5, FlipVertical = 6, Transverse = 7 };
public delegate void PermissionCallback( Permission permission );
public delegate void MediaSaveCallback( bool success, string path );
public delegate void MediaPickCallback( string path );
public delegate void MediaPickMultipleCallback( string[] paths );
#region Platform Specific Elements
#if !UNITY_EDITOR && UNITY_ANDROID
private static AndroidJavaClass m_ajc = null;
private static AndroidJavaClass AJC
{
get
{
if( m_ajc == null )
m_ajc = new AndroidJavaClass( "com.yasirkula.unity.NativeGallery" );
return m_ajc;
}
}
private static AndroidJavaObject m_context = null;
private static AndroidJavaObject Context
{
get
{
if( m_context == null )
{
using( AndroidJavaObject unityClass = new AndroidJavaClass( "com.unity3d.player.UnityPlayer" ) )
{
m_context = unityClass.GetStatic<AndroidJavaObject>( "currentActivity" );
}
}
return m_context;
}
}
#elif !UNITY_EDITOR && UNITY_IOS
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_ShowLimitedLibraryPicker();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_OpenSettings();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeGallery_CanPickMultipleMedia();
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeGallery_GetMediaTypeFromExtension( string extension );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_ImageWriteToAlbum( string path, string album, int permissionFreeMode );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_VideoWriteToAlbum( string path, string album, int permissionFreeMode );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern void _NativeGallery_PickMedia( string mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeGallery_GetImageProperties( string path );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeGallery_GetVideoProperties( string path );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeGallery_GetVideoThumbnail( string path, string thumbnailSavePath, int maxSize, double captureTimeInSeconds );
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern string _NativeGallery_LoadImageAtPath( string path, string temporaryFilePath, int maxSize );
#endif
#if !UNITY_EDITOR && ( UNITY_ANDROID || UNITY_IOS )
private static string m_temporaryImagePath = null;
private static string TemporaryImagePath
{
get
{
if( m_temporaryImagePath == null )
{
m_temporaryImagePath = Path.Combine( Application.temporaryCachePath, "tmpImg" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_temporaryImagePath;
}
}
private static string m_selectedMediaPath = null;
private static string SelectedMediaPath
{
get
{
if( m_selectedMediaPath == null )
{
m_selectedMediaPath = Path.Combine( Application.temporaryCachePath, "pickedMedia" );
Directory.CreateDirectory( Application.temporaryCachePath );
}
return m_selectedMediaPath;
}
}
#endif
#endregion
#region Runtime Permissions
// PermissionFreeMode was initially planned to be a toggleable setting on iOS but it has its own issues when set to false, so its value is forced to true.
// These issues are:
// - Presented permission dialog will have a "Select Photos" option on iOS 14+ but clicking it will freeze and eventually crash the app (I'm guessing that
// this is caused by how permissions are handled synchronously in NativeGallery)
// - While saving images/videos to Photos, iOS 14+ users would see the "Select Photos" option (which is irrelevant in this context, hence confusing) and
// the user must grant full Photos access in order to save the image/video to a custom album
// The only downside of having PermissionFreeMode = true is that, on iOS 14+, images/videos will be saved to the default Photos album rather than the
// provided custom album
private const bool PermissionFreeMode = true;
public static bool CheckPermission( PermissionType permissionType, MediaType mediaTypes )
{
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<int>( "CheckPermission", Context, permissionType == PermissionType.Read, (int) mediaTypes ) == 1;
#elif !UNITY_EDITOR && UNITY_IOS
return ProcessPermission( (Permission) _NativeGallery_CheckPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 ) ) == Permission.Granted;
#else
return true;
#endif
}
public static void RequestPermissionAsync( PermissionCallback callback, PermissionType permissionType, MediaType mediaTypes )
{
#if !UNITY_EDITOR && UNITY_ANDROID
NGPermissionCallbackAndroid nativeCallback = new( callback );
AJC.CallStatic( "RequestPermission", Context, nativeCallback, permissionType == PermissionType.Read, (int) mediaTypes );
#elif !UNITY_EDITOR && UNITY_IOS
NGPermissionCallbackiOS.Initialize( ( result ) => callback( ProcessPermission( result ) ) );
_NativeGallery_RequestPermission( permissionType == PermissionType.Read ? 1 : 0, PermissionFreeMode ? 1 : 0 );
#else
callback( Permission.Granted );
#endif
}
public static Task<Permission> RequestPermissionAsync( PermissionType permissionType, MediaType mediaTypes )
{
TaskCompletionSource<Permission> tcs = new TaskCompletionSource<Permission>();
RequestPermissionAsync( ( permission ) => tcs.SetResult( permission ), permissionType, mediaTypes );
return tcs.Task;
}
private static Permission ProcessPermission( Permission permission )
{
// result == 3: LimitedAccess permission on iOS, no need to handle it when PermissionFreeMode is set to true
return ( PermissionFreeMode && (int) permission == 3 ) ? Permission.Granted : permission;
}
// This function isn't needed when PermissionFreeMode is set to true
private static void TryExtendLimitedAccessPermission()
{
if( IsMediaPickerBusy() )
return;
#if !UNITY_EDITOR && UNITY_IOS
_NativeGallery_ShowLimitedLibraryPicker();
#endif
}
public static void OpenSettings()
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "OpenSettings", Context );
#elif !UNITY_EDITOR && UNITY_IOS
_NativeGallery_OpenSettings();
#endif
}
#endregion
#region Save Functions
public static void SaveImageToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
{
SaveToGallery( mediaBytes, album, filename, MediaType.Image, callback );
}
public static void SaveImageToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
{
SaveToGallery( existingMediaPath, album, filename, MediaType.Image, callback );
}
public static void SaveImageToGallery( Texture2D image, string album, string filename, MediaSaveCallback callback = null )
{
if( image == null )
throw new ArgumentException( "Parameter 'image' is null!" );
if( filename.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) || filename.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) )
SaveToGallery( GetTextureBytes( image, true ), album, filename, MediaType.Image, callback );
else if( filename.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) )
SaveToGallery( GetTextureBytes( image, false ), album, filename, MediaType.Image, callback );
else
SaveToGallery( GetTextureBytes( image, false ), album, filename + ".png", MediaType.Image, callback );
}
public static void SaveVideoToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
{
SaveToGallery( mediaBytes, album, filename, MediaType.Video, callback );
}
public static void SaveVideoToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
{
SaveToGallery( existingMediaPath, album, filename, MediaType.Video, callback );
}
private static void SaveAudioToGallery( byte[] mediaBytes, string album, string filename, MediaSaveCallback callback = null )
{
SaveToGallery( mediaBytes, album, filename, MediaType.Audio, callback );
}
private static void SaveAudioToGallery( string existingMediaPath, string album, string filename, MediaSaveCallback callback = null )
{
SaveToGallery( existingMediaPath, album, filename, MediaType.Audio, callback );
}
#endregion
#region Load Functions
public static bool CanSelectMultipleFilesFromGallery()
{
#if !UNITY_EDITOR && UNITY_ANDROID
return AJC.CallStatic<bool>( "CanSelectMultipleMedia" );
#elif !UNITY_EDITOR && UNITY_IOS
return _NativeGallery_CanPickMultipleMedia() == 1;
#else
return false;
#endif
}
public static bool CanSelectMultipleMediaTypesFromGallery()
{
#if UNITY_EDITOR
return true;
#elif UNITY_ANDROID
return AJC.CallStatic<bool>( "CanSelectMultipleMediaTypes" );
#elif UNITY_IOS
return true;
#else
return false;
#endif
}
public static void GetImageFromGallery( MediaPickCallback callback, string title = "", string mime = "image/*" )
{
GetMediaFromGallery( callback, MediaType.Image, mime, title );
}
public static void GetVideoFromGallery( MediaPickCallback callback, string title = "", string mime = "video/*" )
{
GetMediaFromGallery( callback, MediaType.Video, mime, title );
}
public static void GetAudioFromGallery( MediaPickCallback callback, string title = "", string mime = "audio/*" )
{
GetMediaFromGallery( callback, MediaType.Audio, mime, title );
}
public static void GetMixedMediaFromGallery( MediaPickCallback callback, MediaType mediaTypes, string title = "" )
{
GetMediaFromGallery( callback, mediaTypes, "*/*", title );
}
public static void GetImagesFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "image/*" )
{
GetMultipleMediaFromGallery( callback, MediaType.Image, mime, title );
}
public static void GetVideosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "video/*" )
{
GetMultipleMediaFromGallery( callback, MediaType.Video, mime, title );
}
public static void GetAudiosFromGallery( MediaPickMultipleCallback callback, string title = "", string mime = "audio/*" )
{
GetMultipleMediaFromGallery( callback, MediaType.Audio, mime, title );
}
public static void GetMixedMediasFromGallery( MediaPickMultipleCallback callback, MediaType mediaTypes, string title = "" )
{
GetMultipleMediaFromGallery( callback, mediaTypes, "*/*", title );
}
public static bool IsMediaPickerBusy()
{
#if !UNITY_EDITOR && UNITY_IOS
return NGMediaReceiveCallbackiOS.IsBusy;
#else
return false;
#endif
}
public static MediaType GetMediaTypeOfFile( string path )
{
if( string.IsNullOrEmpty( path ) )
return (MediaType) 0;
string extension = Path.GetExtension( path );
if( string.IsNullOrEmpty( extension ) )
return (MediaType) 0;
if( extension[0] == '.' )
{
if( extension.Length == 1 )
return (MediaType) 0;
extension = extension.Substring( 1 );
}
#if UNITY_EDITOR
extension = extension.ToLowerInvariant();
if( extension == "png" || extension == "jpg" || extension == "jpeg" || extension == "gif" || extension == "bmp" || extension == "tiff" )
return MediaType.Image;
else if( extension == "mp4" || extension == "mov" || extension == "wav" || extension == "avi" )
return MediaType.Video;
else if( extension == "mp3" || extension == "aac" || extension == "flac" )
return MediaType.Audio;
return (MediaType) 0;
#elif UNITY_ANDROID
string mime = AJC.CallStatic<string>( "GetMimeTypeFromExtension", extension.ToLowerInvariant() );
if( string.IsNullOrEmpty( mime ) )
return (MediaType) 0;
else if( mime.StartsWith( "image/" ) )
return MediaType.Image;
else if( mime.StartsWith( "video/" ) )
return MediaType.Video;
else if( mime.StartsWith( "audio/" ) )
return MediaType.Audio;
else
return (MediaType) 0;
#elif UNITY_IOS
return (MediaType) _NativeGallery_GetMediaTypeFromExtension( extension.ToLowerInvariant() );
#else
return (MediaType) 0;
#endif
}
#endregion
#region Internal Functions
private static void SaveToGallery( byte[] mediaBytes, string album, string filename, MediaType mediaType, MediaSaveCallback callback )
{
if( mediaBytes == null || mediaBytes.Length == 0 )
throw new ArgumentException( "Parameter 'mediaBytes' is null or empty!" );
if( album == null || album.Length == 0 )
throw new ArgumentException( "Parameter 'album' is null or empty!" );
if( filename == null || filename.Length == 0 )
throw new ArgumentException( "Parameter 'filename' is null or empty!" );
if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )
Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted )
{
callback?.Invoke( false, null );
return;
}
string path = GetTemporarySavePath( filename );
#if UNITY_EDITOR
Debug.Log( "SaveToGallery called successfully in the Editor" );
#else
File.WriteAllBytes( path, mediaBytes );
#endif
SaveToGalleryInternal( path, album, mediaType, callback );
}, PermissionType.Write, mediaType );
}
private static void SaveToGallery( string existingMediaPath, string album, string filename, MediaType mediaType, MediaSaveCallback callback )
{
if( !File.Exists( existingMediaPath ) )
throw new FileNotFoundException( "File not found at " + existingMediaPath );
if( album == null || album.Length == 0 )
throw new ArgumentException( "Parameter 'album' is null or empty!" );
if( filename == null || filename.Length == 0 )
throw new ArgumentException( "Parameter 'filename' is null or empty!" );
if( string.IsNullOrEmpty( Path.GetExtension( filename ) ) )
{
string originalExtension = Path.GetExtension( existingMediaPath );
if( string.IsNullOrEmpty( originalExtension ) )
Debug.LogWarning( "'filename' doesn't have an extension, this might result in unexpected behaviour!" );
else
filename += originalExtension;
}
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted )
{
callback?.Invoke( false, null );
return;
}
string path = GetTemporarySavePath( filename );
#if UNITY_EDITOR
Debug.Log( "SaveToGallery called successfully in the Editor" );
#else
File.Copy( existingMediaPath, path, true );
#endif
SaveToGalleryInternal( path, album, mediaType, callback );
}, PermissionType.Write, mediaType );
}
private static void SaveToGalleryInternal( string path, string album, MediaType mediaType, MediaSaveCallback callback )
{
#if !UNITY_EDITOR && UNITY_ANDROID
string savePath = AJC.CallStatic<string>( "SaveMedia", Context, (int) mediaType, path, album );
File.Delete( path );
if( callback != null )
callback( !string.IsNullOrEmpty( savePath ), savePath );
#elif !UNITY_EDITOR && UNITY_IOS
if( mediaType == MediaType.Audio )
{
Debug.LogError( "Saving audio files is not supported on iOS" );
if( callback != null )
callback( false, null );
return;
}
Debug.Log( "Saving to Pictures: " + Path.GetFileName( path ) );
NGMediaSaveCallbackiOS.Initialize( callback );
if( mediaType == MediaType.Image )
_NativeGallery_ImageWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );
else if( mediaType == MediaType.Video )
_NativeGallery_VideoWriteToAlbum( path, album, PermissionFreeMode ? 1 : 0 );
#else
if( callback != null )
callback( true, null );
#endif
}
private static string GetTemporarySavePath( string filename )
{
string saveDir = Path.Combine( Application.persistentDataPath, "NGallery" );
Directory.CreateDirectory( saveDir );
#if !UNITY_EDITOR && UNITY_IOS
// Ensure a unique temporary filename on iOS:
// iOS internally copies images/videos to Photos directory of the system,
// but the process is async. The redundant file is deleted by objective-c code
// automatically after the media is saved but while it is being saved, the file
// should NOT be overwritten. Therefore, always ensure a unique filename on iOS
string path = Path.Combine( saveDir, filename );
if( File.Exists( path ) )
{
int fileIndex = 0;
string filenameWithoutExtension = Path.GetFileNameWithoutExtension( filename );
string extension = Path.GetExtension( filename );
do
{
path = Path.Combine( saveDir, string.Concat( filenameWithoutExtension, ++fileIndex, extension ) );
} while( File.Exists( path ) );
}
return path;
#else
return Path.Combine( saveDir, filename );
#endif
}
private static void GetMediaFromGallery( MediaPickCallback callback, MediaType mediaType, string mime, string title )
{
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsMediaPickerBusy() )
{
callback?.Invoke( null );
return;
}
#if UNITY_EDITOR
System.Collections.Generic.List<string> editorFilters = new System.Collections.Generic.List<string>( 4 );
if( ( mediaType & MediaType.Image ) == MediaType.Image )
{
editorFilters.Add( "Image files" );
editorFilters.Add( "png,jpg,jpeg" );
}
if( ( mediaType & MediaType.Video ) == MediaType.Video )
{
editorFilters.Add( "Video files" );
editorFilters.Add( "mp4,mov,webm,avi" );
}
if( ( mediaType & MediaType.Audio ) == MediaType.Audio )
{
editorFilters.Add( "Audio files" );
editorFilters.Add( "mp3,wav,aac,flac" );
}
editorFilters.Add( "All files" );
editorFilters.Add( "*" );
string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters.ToArray() );
if( callback != null )
callback( pickedFile != "" ? pickedFile : null );
#elif UNITY_ANDROID
AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, SelectedMediaPath, mime, title );
#elif UNITY_IOS
if( mediaType == MediaType.Audio )
{
Debug.LogError( "Picking audio files is not supported on iOS" );
if( callback != null ) // Selecting audio files is not supported on iOS
callback( null );
}
else
{
NGMediaReceiveCallbackiOS.Initialize( callback, null );
_NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 1 );
}
#else
if( callback != null )
callback( null );
#endif
}, PermissionType.Read, mediaType );
}
private static void GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title )
{
RequestPermissionAsync( ( permission ) =>
{
if( permission != Permission.Granted || IsMediaPickerBusy() )
{
callback?.Invoke( null );
return;
}
if( CanSelectMultipleFilesFromGallery() )
{
#if !UNITY_EDITOR && UNITY_ANDROID
AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, SelectedMediaPath, mime, title );
#elif !UNITY_EDITOR && UNITY_IOS
if( mediaType == MediaType.Audio )
{
Debug.LogError( "Picking audio files is not supported on iOS" );
if( callback != null ) // Selecting audio files is not supported on iOS
callback( null );
}
else
{
NGMediaReceiveCallbackiOS.Initialize( null, callback );
_NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 0 );
}
#else
if( callback != null )
callback( null );
#endif
}
else if( callback != null )
callback( null );
}, PermissionType.Read, mediaType );
}
private static byte[] GetTextureBytes( Texture2D texture, bool isJpeg )
{
try
{
return isJpeg ? texture.EncodeToJPG( 100 ) : texture.EncodeToPNG();
}
catch( UnityException )
{
return GetTextureBytesFromCopy( texture, isJpeg );
}
catch( ArgumentException )
{
return GetTextureBytesFromCopy( texture, isJpeg );
}
catch
{
return null;
}
}
private static byte[] GetTextureBytesFromCopy( Texture2D texture, bool isJpeg )
{
// Texture is marked as non-readable, create a readable copy and save it instead
Debug.LogWarning( "Saving non-readable textures is slower than saving readable textures" );
Texture2D sourceTexReadable = null;
RenderTexture rt = RenderTexture.GetTemporary( texture.width, texture.height );
RenderTexture activeRT = RenderTexture.active;
try
{
Graphics.Blit( texture, rt );
RenderTexture.active = rt;
sourceTexReadable = new Texture2D( texture.width, texture.height, isJpeg ? TextureFormat.RGB24 : TextureFormat.RGBA32, false );
sourceTexReadable.ReadPixels( new Rect( 0, 0, texture.width, texture.height ), 0, 0, false );
sourceTexReadable.Apply( false, false );
}
catch( Exception e )
{
Debug.LogException( e );
Object.DestroyImmediate( sourceTexReadable );
return null;
}
finally
{
RenderTexture.active = activeRT;
RenderTexture.ReleaseTemporary( rt );
}
try
{
return isJpeg ? sourceTexReadable.EncodeToJPG( 100 ) : sourceTexReadable.EncodeToPNG();
}
catch( Exception e )
{
Debug.LogException( e );
return null;
}
finally
{
Object.DestroyImmediate( sourceTexReadable );
}
}
#if UNITY_ANDROID
private static async Task<T> TryCallNativeAndroidFunctionOnSeparateThread<T>( Func<T> function )
{
T result = default( T );
bool hasResult = false;
await Task.Run( () =>
{
if( AndroidJNI.AttachCurrentThread() != 0 )
Debug.LogWarning( "Couldn't attach JNI thread, calling native function on the main thread" );
else
{
try
{
result = function();
hasResult = true;
}
finally
{
AndroidJNI.DetachCurrentThread();
}
}
} );
return hasResult ? result : function();
}
#endif
#endregion
#region Utility Functions
public static Texture2D LoadImageAtPath( string imagePath, int maxSize = -1, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
{
if( string.IsNullOrEmpty( imagePath ) )
throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string loadPath = AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, TemporaryImagePath, maxSize );
#elif !UNITY_EDITOR && UNITY_IOS
string loadPath = _NativeGallery_LoadImageAtPath( imagePath, TemporaryImagePath, maxSize );
#else
string loadPath = imagePath;
#endif
string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
Texture2D result = new Texture2D( 2, 2, format, generateMipmaps, linearColorSpace );
try
{
if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
{
Debug.LogWarning( "Couldn't load image at path: " + loadPath );
Object.DestroyImmediate( result );
return null;
}
}
catch( Exception e )
{
Debug.LogException( e );
Object.DestroyImmediate( result );
return null;
}
finally
{
if( loadPath != imagePath )
{
try
{
File.Delete( loadPath );
}
catch { }
}
}
return result;
}
public static async Task<Texture2D> LoadImageAtPathAsync( string imagePath, int maxSize = -1, bool markTextureNonReadable = true )
{
if( string.IsNullOrEmpty( imagePath ) )
throw new ArgumentException( "Parameter 'imagePath' is null or empty!" );
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string loadPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "LoadImageAtPath", Context, imagePath, temporaryImagePath, maxSize ) );
#elif !UNITY_EDITOR && UNITY_IOS
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string loadPath = await Task.Run( () => _NativeGallery_LoadImageAtPath( imagePath, temporaryImagePath, maxSize ) );
#else
string loadPath = imagePath;
#endif
Texture2D result = null;
using( UnityWebRequest www = UnityWebRequestTexture.GetTexture( "file://" + loadPath, markTextureNonReadable ) )
{
UnityWebRequestAsyncOperation asyncOperation = www.SendWebRequest();
while( !asyncOperation.isDone )
await Task.Yield();
if( www.result != UnityWebRequest.Result.Success )
Debug.LogWarning( "Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error );
else
result = DownloadHandlerTexture.GetContent( www );
}
if( !result ) // Fallback to Texture2D.LoadImage if something goes wrong
{
string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
TextureFormat format = ( extension == ".jpg" || extension == ".jpeg" ) ? TextureFormat.RGB24 : TextureFormat.RGBA32;
result = new Texture2D( 2, 2, format, true, false );
try
{
if( !result.LoadImage( File.ReadAllBytes( loadPath ), markTextureNonReadable ) )
{
Debug.LogWarning( "Couldn't load image at path: " + loadPath );
Object.DestroyImmediate( result );
return null;
}
}
catch( Exception e )
{
Debug.LogException( e );
Object.DestroyImmediate( result );
return null;
}
finally
{
if( loadPath != imagePath )
{
try
{
File.Delete( loadPath );
}
catch { }
}
}
}
return result;
}
public static Texture2D GetVideoThumbnail( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true, bool generateMipmaps = true, bool linearColorSpace = false )
{
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string thumbnailPath = AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, TemporaryImagePath + ".png", false, maxSize, captureTimeInSeconds );
#elif !UNITY_EDITOR && UNITY_IOS
string thumbnailPath = _NativeGallery_GetVideoThumbnail( videoPath, TemporaryImagePath + ".png", maxSize, captureTimeInSeconds );
#else
string thumbnailPath = null;
#endif
if( !string.IsNullOrEmpty( thumbnailPath ) )
return LoadImageAtPath( thumbnailPath, maxSize, markTextureNonReadable, generateMipmaps, linearColorSpace );
else
return null;
}
public static async Task<Texture2D> GetVideoThumbnailAsync( string videoPath, int maxSize = -1, double captureTimeInSeconds = -1.0, bool markTextureNonReadable = true )
{
if( maxSize <= 0 )
maxSize = SystemInfo.maxTextureSize;
#if !UNITY_EDITOR && UNITY_ANDROID
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string thumbnailPath = await TryCallNativeAndroidFunctionOnSeparateThread( () => AJC.CallStatic<string>( "GetVideoThumbnail", Context, videoPath, temporaryImagePath + ".png", false, maxSize, captureTimeInSeconds ) );
#elif !UNITY_EDITOR && UNITY_IOS
string temporaryImagePath = TemporaryImagePath; // Must be accessed from main thread
string thumbnailPath = await Task.Run( () => _NativeGallery_GetVideoThumbnail( videoPath, temporaryImagePath + ".png", maxSize, captureTimeInSeconds ) );
#else
string thumbnailPath = null;
#endif
if( !string.IsNullOrEmpty( thumbnailPath ) )
return await LoadImageAtPathAsync( thumbnailPath, maxSize, markTextureNonReadable );
else
return null;
}
public static ImageProperties GetImageProperties( string imagePath )
{
if( !File.Exists( imagePath ) )
throw new FileNotFoundException( "File not found at " + imagePath );
#if !UNITY_EDITOR && UNITY_ANDROID
string value = AJC.CallStatic<string>( "GetImageProperties", Context, imagePath );
#elif !UNITY_EDITOR && UNITY_IOS
string value = _NativeGallery_GetImageProperties( imagePath );
#else
string value = null;
#endif
int width = 0, height = 0;
string mimeType = null;
ImageOrientation orientation = ImageOrientation.Unknown;
if( !string.IsNullOrEmpty( value ) )
{
string[] properties = value.Split( '>' );
if( properties != null && properties.Length >= 4 )
{
if( !int.TryParse( properties[0].Trim(), out width ) )
width = 0;
if( !int.TryParse( properties[1].Trim(), out height ) )
height = 0;
mimeType = properties[2].Trim();
if( mimeType.Length == 0 )
{
string extension = Path.GetExtension( imagePath ).ToLowerInvariant();
if( extension == ".png" )
mimeType = "image/png";
else if( extension == ".jpg" || extension == ".jpeg" )
mimeType = "image/jpeg";
else if( extension == ".gif" )
mimeType = "image/gif";
else if( extension == ".bmp" )
mimeType = "image/bmp";
else
mimeType = null;
}
int orientationInt;
if( int.TryParse( properties[3].Trim(), out orientationInt ) )
orientation = (ImageOrientation) orientationInt;
}
}
return new ImageProperties( width, height, mimeType, orientation );
}
public static VideoProperties GetVideoProperties( string videoPath )
{
if( !File.Exists( videoPath ) )
throw new FileNotFoundException( "File not found at " + videoPath );
#if !UNITY_EDITOR && UNITY_ANDROID
string value = AJC.CallStatic<string>( "GetVideoProperties", Context, videoPath );
#elif !UNITY_EDITOR && UNITY_IOS
string value = _NativeGallery_GetVideoProperties( videoPath );
#else
string value = null;
#endif
int width = 0, height = 0;
long duration = 0L;
float rotation = 0f;
if( !string.IsNullOrEmpty( value ) )
{
string[] properties = value.Split( '>' );
if( properties != null && properties.Length >= 4 )
{
if( !int.TryParse( properties[0].Trim(), out width ) )
width = 0;
if( !int.TryParse( properties[1].Trim(), out height ) )
height = 0;
if( !long.TryParse( properties[2].Trim(), out duration ) )
duration = 0L;
if( !float.TryParse( properties[3].Trim().Replace( ',', '.' ), NumberStyles.Float, CultureInfo.InvariantCulture, out rotation ) )
rotation = 0f;
}
}
if( rotation == -90f )
rotation = 270f;
return new VideoProperties( width, height, duration, rotation );
}
#endregion
}
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: ce1403606c3629046a0147d3e705f7cc
timeCreated: 1498722610
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/NativeGallery.cs
uploadId: 860494
+6
View File
@@ -0,0 +1,6 @@
= Native Gallery for Android & iOS (v1.9.2) =
Documentation: https://github.com/yasirkula/UnityNativeGallery
FAQ: https://github.com/yasirkula/UnityNativeGallery#faq
Example code: https://github.com/yasirkula/UnityNativeGallery#example-code
E-mail: yasirkula@gmail.com
@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: be769f45b807c40459e5bafb18e887d6
timeCreated: 1563308465
licenseType: Store
TextScriptImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/README.txt
uploadId: 860494
+9
View File
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 9c623599351a41a4c84c20f73c9d8976
folderAsset: yes
timeCreated: 1498722622
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,132 @@
#if UNITY_EDITOR || UNITY_IOS
using UnityEngine;
namespace NativeGalleryNamespace
{
public class NGMediaReceiveCallbackiOS : MonoBehaviour
{
private static NGMediaReceiveCallbackiOS instance;
private NativeGallery.MediaPickCallback callback;
private NativeGallery.MediaPickMultipleCallback callbackMultiple;
private float nextBusyCheckTime;
public static bool IsBusy { get; private set; }
[System.Runtime.InteropServices.DllImport( "__Internal" )]
private static extern int _NativeGallery_IsMediaPickerBusy();
public static void Initialize( NativeGallery.MediaPickCallback callback, NativeGallery.MediaPickMultipleCallback callbackMultiple )
{
if( IsBusy )
return;
if( instance == null )
{
instance = new GameObject( "NGMediaReceiveCallbackiOS" ).AddComponent<NGMediaReceiveCallbackiOS>();
DontDestroyOnLoad( instance.gameObject );
}
instance.callback = callback;
instance.callbackMultiple = callbackMultiple;
instance.nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
IsBusy = true;
}
private void Update()
{
if( IsBusy )
{
if( Time.realtimeSinceStartup >= nextBusyCheckTime )
{
nextBusyCheckTime = Time.realtimeSinceStartup + 1f;
if( _NativeGallery_IsMediaPickerBusy() == 0 )
{
IsBusy = false;
NativeGallery.MediaPickCallback _callback = callback;
callback = null;
NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple;
callbackMultiple = null;
if( _callback != null )
_callback( null );
if( _callbackMultiple != null )
_callbackMultiple( null );
}
}
}
}
[UnityEngine.Scripting.Preserve]
public void OnMediaReceived( string path )
{
IsBusy = false;
if( string.IsNullOrEmpty( path ) )
path = null;
NativeGallery.MediaPickCallback _callback = callback;
callback = null;
if( _callback != null )
_callback( path );
}
[UnityEngine.Scripting.Preserve]
public void OnMultipleMediaReceived( string paths )
{
IsBusy = false;
string[] _paths = SplitPaths( paths );
if( _paths != null && _paths.Length == 0 )
_paths = null;
NativeGallery.MediaPickMultipleCallback _callbackMultiple = callbackMultiple;
callbackMultiple = null;
if( _callbackMultiple != null )
_callbackMultiple( _paths );
}
private string[] SplitPaths( string paths )
{
string[] result = null;
if( !string.IsNullOrEmpty( paths ) )
{
string[] pathsSplit = paths.Split( '>' );
int validPathCount = 0;
for( int i = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPathCount++;
}
if( validPathCount == 0 )
pathsSplit = new string[0];
else if( validPathCount != pathsSplit.Length )
{
string[] validPaths = new string[validPathCount];
for( int i = 0, j = 0; i < pathsSplit.Length; i++ )
{
if( !string.IsNullOrEmpty( pathsSplit[i] ) )
validPaths[j++] = pathsSplit[i];
}
pathsSplit = validPaths;
}
result = pathsSplit;
}
return result;
}
}
}
#endif
@@ -0,0 +1,19 @@
fileFormatVersion: 2
guid: 71fb861c149c2d1428544c601e52a33c
timeCreated: 1519060539
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 112630
packageName: Native Gallery for Android & iOS
packageVersion: 1.9.2
assetPath: Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs
uploadId: 860494
Loaded 100 of 218 files, more files were not shown because too many files have changed in this diff. Show more