Просмотр исходного кода

1.优化轨迹,修改轨迹问题。2.选择自定义头像。3.修改优化蓝牙名称选择连接

slambb 2 месяцев назад
Родитель
Сommit
f250793bb0
54 измененных файлов с 4596 добавлено и 639 удалено
  1. BIN
      Assets/AddressableAssetsData/Android/addressables_content_state.bin
  2. 1 1
      Assets/AddressableAssetsData/link.xml.meta
  3. 2 2
      Assets/BowArrow/DoubleScene/Scripts/BLEView.cs
  4. 147 14
      Assets/BowArrow/Scenes/Game.unity
  5. 90 42
      Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs
  6. 3 3
      Assets/BowArrow/Scripts/CommonConfig.cs
  7. 8 0
      Assets/BowArrow/Scripts/Components/TextAutoLanguage2/Resources/TextAutoLanguage2/cn.json
  8. 7 0
      Assets/BowArrow/Scripts/Components/TextAutoLanguage2/Resources/TextAutoLanguage2/en.json
  9. 12 7
      Assets/BowArrow/Scripts/Game/ArmBow.cs
  10. 174 51
      Assets/BowArrow/Scripts/Game/Arrow.cs
  11. 77 95
      Assets/BowArrow/Scripts/Game/ArrowCamera.cs
  12. 23 3
      Assets/BowArrow/Scripts/Game/CrossHair.cs
  13. 1 1
      Assets/BowArrow/Scripts/GameMode/GameModeTest.cs
  14. 28 0
      Assets/BowArrow/Scripts/Network/HttpController/LoginController.cs
  15. BIN
      Assets/BowArrow/Textures/Me/addIcon.png
  16. 135 0
      Assets/BowArrow/Textures/Me/addIcon.png.meta
  17. BIN
      Assets/BowArrow/Textures/Me/addImage.png
  18. 135 0
      Assets/BowArrow/Textures/Me/addImage.png.meta
  19. 9 0
      Assets/Plugins/NativeGallery.meta
  20. 9 0
      Assets/Plugins/NativeGallery/Android.meta
  21. 52 0
      Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs
  22. 12 0
      Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta
  23. 62 0
      Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs
  24. 12 0
      Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta
  25. 24 0
      Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs
  26. 12 0
      Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta
  27. BIN
      Assets/Plugins/NativeGallery/Android/NativeGallery.aar
  28. 33 0
      Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta
  29. 9 0
      Assets/Plugins/NativeGallery/Editor.meta
  30. 119 0
      Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs
  31. 12 0
      Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta
  32. 15 0
      Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef
  33. 7 0
      Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta
  34. 3 0
      Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef
  35. 7 0
      Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta
  36. 988 0
      Assets/Plugins/NativeGallery/NativeGallery.cs
  37. 12 0
      Assets/Plugins/NativeGallery/NativeGallery.cs.meta
  38. 6 0
      Assets/Plugins/NativeGallery/README.txt
  39. 8 0
      Assets/Plugins/NativeGallery/README.txt.meta
  40. 9 0
      Assets/Plugins/NativeGallery/iOS.meta
  41. 132 0
      Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs
  42. 12 0
      Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta
  43. 45 0
      Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs
  44. 12 0
      Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta
  45. 35 0
      Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs
  46. 12 0
      Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta
  47. 1251 0
      Assets/Plugins/NativeGallery/iOS/NativeGallery.mm
  48. 33 0
      Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta
  49. 368 0
      Assets/SmartBow/Resources/Common/Avatar.prefab
  50. 7 0
      Assets/SmartBow/Resources/Common/Avatar.prefab.meta
  51. 200 367
      Assets/SmartBow/Resources/SmartBow/Prefabs/Views/Home/PersonalView.prefab
  52. 142 16
      Assets/SmartBow/Scripts/Views/PersonalViewParts/BoxUserSettings.cs
  53. 84 37
      Assets/SmartBow/SmartBowSDK/BleWinHelper.cs
  54. BIN
      Assets/SmartBow/SmartBowSDK/SmartBowSDK.dll

BIN
Assets/AddressableAssetsData/Android/addressables_content_state.bin


+ 1 - 1
Assets/AddressableAssetsData/link.xml.meta

@@ -1,5 +1,5 @@
 fileFormatVersion: 2
 fileFormatVersion: 2
-guid: ec74b621f31942e41aa8765c58abe78f
+guid: 29ca3a6a3e6275f499cfa15d6b52fa69
 TextScriptImporter:
 TextScriptImporter:
   externalObjects: {}
   externalObjects: {}
   userData: 
   userData: 

+ 2 - 2
Assets/BowArrow/DoubleScene/Scripts/BLEView.cs

@@ -89,8 +89,8 @@ public class BLEView : MonoBehaviour
                 };
                 };
             }
             }
         };
         };
-        if (playerIndex == 0 && BluetoothWindows.IsWindows())
-            BleWinHelper.RegisterTo(smartBowHelper.gameObject, smartBowHelper.CreateBluetoothWindows());
+        //if (playerIndex == 0 && BluetoothWindows.IsWindows())
+        //    BleWinHelper.RegisterTo(smartBowHelper.gameObject, smartBowHelper.CreateBluetoothWindows());
 
 
         btnConnect.onClick.AddListener(OnClick_Connect);
         btnConnect.onClick.AddListener(OnClick_Connect);
         btnGyr.onClick.AddListener(OnClick_CalibrateGyr);
         btnGyr.onClick.AddListener(OnClick_CalibrateGyr);

+ 147 - 14
Assets/BowArrow/Scenes/Game.unity

@@ -2816,7 +2816,7 @@ Transform:
   m_PrefabAsset: {fileID: 0}
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 234983879}
   m_GameObject: {fileID: 234983879}
   m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
   m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
-  m_LocalPosition: {x: -0, y: -0, z: 0.78}
+  m_LocalPosition: {x: 0, y: 0, z: 0.78}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_ConstrainProportionsScale: 0
   m_ConstrainProportionsScale: 0
   m_Children:
   m_Children:
@@ -2836,6 +2836,8 @@ GameObject:
   serializedVersion: 6
   serializedVersion: 6
   m_Component:
   m_Component:
   - component: {fileID: 237700704}
   - component: {fileID: 237700704}
+  - component: {fileID: 237700706}
+  - component: {fileID: 237700705}
   m_Layer: 0
   m_Layer: 0
   m_Name: Arrow
   m_Name: Arrow
   m_TagString: Untagged
   m_TagString: Untagged
@@ -2862,6 +2864,118 @@ Transform:
   m_Father: {fileID: 2036668599}
   m_Father: {fileID: 2036668599}
   m_RootOrder: 1
   m_RootOrder: 1
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &237700705
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 237700703}
+  m_Enabled: 0
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 62db7f6d1b6dc4e419b9b1b07c69a8a4, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  pointSpacing: 0.1
+--- !u!120 &237700706
+LineRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 237700703}
+  m_Enabled: 0
+  m_CastShadows: 1
+  m_ReceiveShadows: 1
+  m_DynamicOccludee: 1
+  m_StaticShadowCaster: 0
+  m_MotionVectors: 0
+  m_LightProbeUsage: 0
+  m_ReflectionProbeUsage: 0
+  m_RayTracingMode: 0
+  m_RayTraceProcedural: 0
+  m_RenderingLayerMask: 1
+  m_RendererPriority: 0
+  m_Materials:
+  - {fileID: 0}
+  m_StaticBatchInfo:
+    firstSubMesh: 0
+    subMeshCount: 0
+  m_StaticBatchRoot: {fileID: 0}
+  m_ProbeAnchor: {fileID: 0}
+  m_LightProbeVolumeOverride: {fileID: 0}
+  m_ScaleInLightmap: 1
+  m_ReceiveGI: 1
+  m_PreserveUVs: 0
+  m_IgnoreNormalsForChartDetection: 0
+  m_ImportantGI: 0
+  m_StitchLightmapSeams: 1
+  m_SelectedEditorRenderState: 3
+  m_MinimumChartSize: 4
+  m_AutoUVMaxDistance: 0.5
+  m_AutoUVMaxAngle: 89
+  m_LightmapParameters: {fileID: 0}
+  m_SortingLayerID: 0
+  m_SortingLayer: 0
+  m_SortingOrder: 0
+  m_Positions:
+  - {x: 0, y: 0, z: 0}
+  - {x: 0, y: 0, z: 1}
+  m_Parameters:
+    serializedVersion: 3
+    widthMultiplier: 1
+    widthCurve:
+      serializedVersion: 2
+      m_Curve:
+      - serializedVersion: 3
+        time: 0
+        value: 1
+        inSlope: 0
+        outSlope: 0
+        tangentMode: 0
+        weightedMode: 0
+        inWeight: 0.33333334
+        outWeight: 0.33333334
+      m_PreInfinity: 2
+      m_PostInfinity: 2
+      m_RotationOrder: 4
+    colorGradient:
+      serializedVersion: 2
+      key0: {r: 1, g: 1, b: 1, a: 1}
+      key1: {r: 1, g: 1, b: 1, a: 1}
+      key2: {r: 0, g: 0, b: 0, a: 0}
+      key3: {r: 0, g: 0, b: 0, a: 0}
+      key4: {r: 0, g: 0, b: 0, a: 0}
+      key5: {r: 0, g: 0, b: 0, a: 0}
+      key6: {r: 0, g: 0, b: 0, a: 0}
+      key7: {r: 0, g: 0, b: 0, a: 0}
+      ctime0: 0
+      ctime1: 65535
+      ctime2: 0
+      ctime3: 0
+      ctime4: 0
+      ctime5: 0
+      ctime6: 0
+      ctime7: 0
+      atime0: 0
+      atime1: 65535
+      atime2: 0
+      atime3: 0
+      atime4: 0
+      atime5: 0
+      atime6: 0
+      atime7: 0
+      m_Mode: 0
+      m_NumColorKeys: 2
+      m_NumAlphaKeys: 2
+    numCornerVertices: 0
+    numCapVertices: 0
+    alignment: 0
+    textureMode: 0
+    shadowBias: 0.5
+    generateLightingData: 0
+  m_UseWorldSpace: 1
+  m_Loop: 0
 --- !u!1001 &237896599
 --- !u!1001 &237896599
 PrefabInstance:
 PrefabInstance:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -9952,6 +10066,10 @@ PrefabInstance:
   m_Modification:
   m_Modification:
     m_TransformParent: {fileID: 237700704}
     m_TransformParent: {fileID: 237700704}
     m_Modifications:
     m_Modifications:
+    - target: {fileID: 269162862552698132, guid: 02d4cf543ee8d8e4093d8275bdc59b29, type: 3}
+      propertyPath: InitialModule.maxNumParticles
+      value: 6000
+      objectReference: {fileID: 0}
     - target: {fileID: 1805953886975959927, guid: 02d4cf543ee8d8e4093d8275bdc59b29, type: 3}
     - target: {fileID: 1805953886975959927, guid: 02d4cf543ee8d8e4093d8275bdc59b29, type: 3}
       propertyPath: m_RootOrder
       propertyPath: m_RootOrder
       value: 2
       value: 2
@@ -12563,7 +12681,7 @@ RectTransform:
   m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
   m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
   m_LocalPosition: {x: 0, y: 0, z: 0}
   m_LocalPosition: {x: 0, y: 0, z: 0}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_LocalScale: {x: 1, y: 1, z: 1}
-  m_ConstrainProportionsScale: 0
+  m_ConstrainProportionsScale: 1
   m_Children:
   m_Children:
   - {fileID: 1211575576}
   - {fileID: 1211575576}
   m_Father: {fileID: 2099761839}
   m_Father: {fileID: 2099761839}
@@ -15688,20 +15806,20 @@ MonoBehaviour:
     - serializedVersion: 3
     - serializedVersion: 3
       time: 0
       time: 0
       value: 0
       value: 0
-      inSlope: 0
-      outSlope: 0
+      inSlope: 0.5143808
+      outSlope: 0.5143808
       tangentMode: 0
       tangentMode: 0
       weightedMode: 0
       weightedMode: 0
       inWeight: 0.33333334
       inWeight: 0.33333334
-      outWeight: 0.33333334
+      outWeight: 0.07392563
     - serializedVersion: 3
     - serializedVersion: 3
-      time: 0.7341202
-      value: 0.31326506
-      inSlope: 1.2260199
-      outSlope: 1.2260199
+      time: 0.7001091
+      value: 0.50243723
+      inSlope: 1.342917
+      outSlope: 1.342917
       tangentMode: 0
       tangentMode: 0
       weightedMode: 0
       weightedMode: 0
-      inWeight: 0.052017
+      inWeight: 0.33333334
       outWeight: 0.33333334
       outWeight: 0.33333334
     - serializedVersion: 3
     - serializedVersion: 3
       time: 1
       time: 1
@@ -15740,9 +15858,9 @@ MonoBehaviour:
     m_PostInfinity: 2
     m_PostInfinity: 2
     m_RotationOrder: 4
     m_RotationOrder: 4
   slowFactor: 0.4
   slowFactor: 0.4
+  notSlowFactor: 0.8
   offsetFromArrow: {x: 0, y: 0.75, z: -2.5}
   offsetFromArrow: {x: 0, y: 0.75, z: -2.5}
-  offsetFromArrowTemp: {x: 0, y: 0.75, z: -2.5}
-  stopDistanceToTarget: 4
+  offsetFromArrowEnd: {x: 0, y: 0.75, z: -0.8}
 --- !u!114 &1423259624
 --- !u!114 &1423259624
 MonoBehaviour:
 MonoBehaviour:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -18860,6 +18978,7 @@ GameObject:
   - component: {fileID: 1626185853}
   - component: {fileID: 1626185853}
   - component: {fileID: 1626185856}
   - component: {fileID: 1626185856}
   - component: {fileID: 1626185855}
   - component: {fileID: 1626185855}
+  - component: {fileID: 1626185857}
   m_Layer: 0
   m_Layer: 0
   m_Name: 005 (2)
   m_Name: 005 (2)
   m_TagString: Untagged
   m_TagString: Untagged
@@ -18932,6 +19051,20 @@ MeshFilter:
   m_PrefabAsset: {fileID: 0}
   m_PrefabAsset: {fileID: 0}
   m_GameObject: {fileID: 1626185852}
   m_GameObject: {fileID: 1626185852}
   m_Mesh: {fileID: -4115292678144379797, guid: 70c4aa998c9ff8c4390f580f22b28a03, type: 3}
   m_Mesh: {fileID: -4115292678144379797, guid: 70c4aa998c9ff8c4390f580f22b28a03, type: 3}
+--- !u!64 &1626185857
+MeshCollider:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1626185852}
+  m_Material: {fileID: 0}
+  m_IsTrigger: 0
+  m_Enabled: 1
+  serializedVersion: 4
+  m_Convex: 0
+  m_CookingOptions: 30
+  m_Mesh: {fileID: -4115292678144379797, guid: 70c4aa998c9ff8c4390f580f22b28a03, type: 3}
 --- !u!1 &1637292225
 --- !u!1 &1637292225
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -22102,11 +22235,11 @@ PrefabInstance:
       objectReference: {fileID: 0}
       objectReference: {fileID: 0}
     - target: {fileID: 3202296430119542852, guid: f4eac784f6b9a464dbbbd2bb0ed3d3fe, type: 3}
     - target: {fileID: 3202296430119542852, guid: f4eac784f6b9a464dbbbd2bb0ed3d3fe, type: 3}
       propertyPath: m_LocalPosition.x
       propertyPath: m_LocalPosition.x
-      value: -0
+      value: 0
       objectReference: {fileID: 0}
       objectReference: {fileID: 0}
     - target: {fileID: 3202296430119542852, guid: f4eac784f6b9a464dbbbd2bb0ed3d3fe, type: 3}
     - target: {fileID: 3202296430119542852, guid: f4eac784f6b9a464dbbbd2bb0ed3d3fe, type: 3}
       propertyPath: m_LocalPosition.y
       propertyPath: m_LocalPosition.y
-      value: -0
+      value: 0
       objectReference: {fileID: 0}
       objectReference: {fileID: 0}
     - target: {fileID: 3202296430119542852, guid: f4eac784f6b9a464dbbbd2bb0ed3d3fe, type: 3}
     - target: {fileID: 3202296430119542852, guid: f4eac784f6b9a464dbbbd2bb0ed3d3fe, type: 3}
       propertyPath: m_LocalPosition.z
       propertyPath: m_LocalPosition.z

+ 90 - 42
Assets/BowArrow/Scripts/Bluetooth/BluetoothAim.cs

@@ -7,6 +7,7 @@ using DG.Tweening;
 using SmartBowSDK;
 using SmartBowSDK;
 using System.Collections;
 using System.Collections;
 using UnityEngine.SceneManagement;
 using UnityEngine.SceneManagement;
+using BleWinHelper = SmartBowSDK_BleWinHelper.BleWinHelper;
 
 
 /* 蓝牙瞄准模块 */
 /* 蓝牙瞄准模块 */
 /* 管理1p和2p蓝牙连接,脚本外调用逻辑如果同时连接1p和2p设备,以前的逻辑还是以1p为主,如果只连2p,则使用2p数据*/
 /* 管理1p和2p蓝牙连接,脚本外调用逻辑如果同时连接1p和2p设备,以前的逻辑还是以1p为主,如果只连2p,则使用2p数据*/
@@ -18,10 +19,12 @@ public class BluetoothAim : MonoBehaviour
     #endregion
     #endregion
 
 
     #region 弓箭蓝牙模块基本信息
     #region 弓箭蓝牙模块基本信息
+    readonly string targetDeviceNameAxis = "Bbow_20210501 | ARTEMIS | HOUYI";
     readonly string targetDeviceName = "Bbow_20210501 | ARTEMIS Pro";//HOUYI Pro
     readonly string targetDeviceName = "Bbow_20210501 | ARTEMIS Pro";//HOUYI Pro
     readonly string targetDeviceNameHOUYIPro = "HOUYI Pro";
     readonly string targetDeviceNameHOUYIPro = "HOUYI Pro";
-    readonly string targetDeviceNameGun = "Pistol | Pistol M9 | Bbow_20210501 | " +
-        "Pistol M17 | Rifle M416";
+    readonly string targetDeviceNameGun = "Pistol | Pistol M9 | Bbow_20210501";
+    readonly string targetDeviceNameGun_M17 =  "Pistol M17";
+    readonly string targetDeviceNameGun_M416 = "Rifle M416";
     string targetDeviceService
     string targetDeviceService
     {
     {
         get
         get
@@ -366,49 +369,71 @@ public class BluetoothAim : MonoBehaviour
                 foreach (BluetoothDevice device in nearbyDevices)
                 foreach (BluetoothDevice device in nearbyDevices)
                 {
                 {
                     Log("发现设备 " + device.DeviceName);
                     Log("发现设备 " + device.DeviceName);
+                    //if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.HOUYIPRO)
+                    //{   //需要判断是否是红外弓箭
+                    //    //targetDeviceNameHOUYIPro targetDeviceNameHOUYIPro{ HOUYI Pro }
+                    //    if (targetDeviceNameHOUYIPro.Contains(device.DeviceName))
+                    //    {
+                    //        BLEConnectByName(device.DeviceName);
+                    //        Log("匹配HOUYIPRO设备 " + device.DeviceName);
+                    //        return;
+                    //    }
+
+                    //}
+                    //else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.ARTEMISPRO)
+                    //{   //需要判断是否是ARTEMISPro弓箭
+                    //    if (targetDeviceName.Contains(device.DeviceName))
+                    //    {
+                    //        BLEConnectByName(device.DeviceName);
+                    //        Log("匹配ARTEMISPro设备 " + device.DeviceName);
+                    //        return;
+                    //    }
+
+                    //}
+                    //else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.Gun ||
+                    //        AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.PistolM17 ||
+                    //        AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.RifleM416)
+                    //{
+                    //    //需要判断是否是枪
+                    //    if (targetDeviceNameGun.Contains(device.DeviceName))
+                    //    {
+                    //        BLEConnectByName(device.DeviceName);
+                    //        Log("匹配枪设备 " + device.DeviceName);
+                    //        return;
+                    //    }
+
+                    //}
                     if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.HOUYIPRO)
                     if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.HOUYIPRO)
                     {   //需要判断是否是红外弓箭
                     {   //需要判断是否是红外弓箭
-                        //targetDeviceNameHOUYIPro targetDeviceNameHOUYIPro{ HOUYI Pro }
-                        if (targetDeviceNameHOUYIPro.Contains(device.DeviceName))
-                        {
-                            BLEConnectByName(device.DeviceName);
-                            Log("匹配HOUYIPRO设备 " + device.DeviceName);
-                            return;
-                        }
-
+                        if (TryConnectDevice(targetDeviceNameHOUYIPro, device.DeviceName, "HOUYIPRO")) return;
                     }
                     }
                     else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.ARTEMISPRO)
                     else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.ARTEMISPRO)
                     {   //需要判断是否是ARTEMISPro弓箭
                     {   //需要判断是否是ARTEMISPro弓箭
-                        if (targetDeviceName.Contains(device.DeviceName))
-                        {
-                            BLEConnectByName(device.DeviceName);
-                            Log("匹配ARTEMISPro设备 " + device.DeviceName);
-                            return;
-                        }
-
+                        if (TryConnectDevice(targetDeviceName, device.DeviceName, "ARTEMISPRO")) return;
                     }
                     }
-                    else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.Gun ||
-                            AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.PistolM17 ||
-                            AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.RifleM416)
+                    else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.Gun)
                     {
                     {
-                        //需要判断是否是枪
-                        if (targetDeviceNameGun.Contains(device.DeviceName))
-                        {
-                            BLEConnectByName(device.DeviceName);
-                            Log("匹配枪设备 " + device.DeviceName);
-                            return;
-                        }
-
+                        if (TryConnectDevice(targetDeviceNameGun, device.DeviceName, "Pistol")) return;
+                    }
+                    else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.PistolM17)
+                    {
+                        if (TryConnectDevice(targetDeviceNameGun_M17, device.DeviceName, "PistolM17")) return;
+                    }
+                    else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.RifleM416)
+                    {
+                        if (TryConnectDevice(targetDeviceNameGun_M416, device.DeviceName, "RifleM416")) return;
                     }
                     }
                     else
                     else
                     {   //其余的九轴连接
                     {   //其余的九轴连接
                         //不允许匹配HOUYIPRO,因为这个没有九轴 targetDeviceName { Bbow_20210501 }
                         //不允许匹配HOUYIPRO,因为这个没有九轴 targetDeviceName { Bbow_20210501 }
-                        if (targetDeviceName.Contains(device.DeviceName))
-                        {
-                            BLEConnectByName(device.DeviceName);
-                            Log("匹配普通设备设备 " + device.DeviceName);
-                            return;
-                        }
+                        //if (targetDeviceName.Contains(device.DeviceName))
+                        //{
+                        //    BLEConnectByName(device.DeviceName);
+                        //    Log("匹配普通设备设备 " + device.DeviceName);
+                        //    return;
+                        //}
+                        if (TryConnectDevice(targetDeviceNameAxis, device.DeviceName, device.DeviceName)) return;
+                        
                     }
                     }
 
 
                 }
                 }
@@ -437,6 +462,29 @@ public class BluetoothAim : MonoBehaviour
             HandleConnectException(TextAutoLanguage2.GetTextByKey("ble-please-open-ble"));
             HandleConnectException(TextAutoLanguage2.GetTextByKey("ble-please-open-ble"));
         }
         }
     }
     }
+    /// <summary>
+    ///  精确匹配,大小写敏感,多设备名字用 " | " 分隔
+    /// </summary>
+    /// <param name="filterNames">过滤字符串</param>
+    /// <param name="deviceName">真实连接名字</param>
+    /// <param name="deviceTypeName">日志名字</param>
+    /// <returns></returns>
+    private bool TryConnectDevice(string filterNames, string deviceName, string deviceTypeName)
+    {
+        if (string.IsNullOrWhiteSpace(filterNames)) return false;
+
+        string[] filterArray = filterNames.Split('|'); // 支持多个名字
+        foreach (var f in filterArray)
+        {
+            if (string.Equals(f.Trim(), deviceName.Trim(), StringComparison.OrdinalIgnoreCase))
+            {
+                BLEConnectByName(deviceName);
+                Log($"匹配设备 {deviceTypeName}: {deviceName}");
+                return true;
+            }
+        }
+        return false;
+    }
     void BLEConnectByName(string name)
     void BLEConnectByName(string name)
     {
     {
         deviceName = name;
         deviceName = name;
@@ -933,14 +981,14 @@ public class BluetoothAim : MonoBehaviour
 
 
         };
         };
 
 
-#if UNITY_STANDALONE_WIN || UNITY_EDITOR
-        //用户2window连接 BluetoothWindows.IsWindows() &&
-        if (currentBLEPlayer == BluetoothPlayer.SECONDE_PLAYER)
-        {
-            Debug.Log("SECONDE_PLAYER BleWinHelper.RegisterTo");
-            BleWinHelper com = BleWinHelper.RegisterTo(smartBowHelper2P.gameObject, smartBowHelper2P.CreateBluetoothWindows(), "2P");
-        }
-#endif
+//#if UNITY_STANDALONE_WIN || UNITY_EDITOR
+//        //用户2window连接 BluetoothWindows.IsWindows() &&
+//        if (currentBLEPlayer == BluetoothPlayer.SECONDE_PLAYER)
+//        {
+//            Debug.Log("SECONDE_PLAYER BleWinHelper.RegisterTo");
+//            BleWinHelper com = BleWinHelper.RegisterTo(smartBowHelper2P.gameObject, smartBowHelper2P.CreateBluetoothWindows(), "2P");
+//        }
+//#endif
 
 
     }
     }
     void OnShot2P(float speed)
     void OnShot2P(float speed)

+ 3 - 3
Assets/BowArrow/Scripts/CommonConfig.cs

@@ -80,7 +80,7 @@ public class CommonConfig
 
 
 
 
     public enum ServerType { LocalTest, Produce, Test };
     public enum ServerType { LocalTest, Produce, Test };
-    private static ServerType serverType = ServerType.Produce;
+    private static ServerType serverType = ServerType.Test;
     //网关服务器访问地址
     //网关服务器访问地址
     public static string gateServerURL
     public static string gateServerURL
     {
     {
@@ -98,9 +98,9 @@ public class CommonConfig
             {
             {
               //  Debug.Log("****************** 测试服务器 ********************");
               //  Debug.Log("****************** 测试服务器 ********************");
                 //测试服务器
                 //测试服务器
-                return "http://www.b-beng.com/SmartBowBusinessServer";
+                return "http://8.155.53.54:11432/SmartBowBusinessServer";
             }
             }
-            return "http://192.168.0.108:11432/SmartBowBusinessServer";
+            return "http://192.168.0.105:11432/SmartBowBusinessServer";
         }
         }
     }
     }
     //业务服务端WS访问地址
     //业务服务端WS访问地址

+ 8 - 0
Assets/BowArrow/Scripts/Components/TextAutoLanguage2/Resources/TextAutoLanguage2/cn.json

@@ -15,6 +15,8 @@
   "common_return": "返回",
   "common_return": "返回",
   "common_send": "发送",
   "common_send": "发送",
   "common_code": "验证码",
   "common_code": "验证码",
+  "common_update_successful": "更新成功!",
+  "common_update_failed": "更新失败!",
 
 
   "modal-confirm-default": "知道了",
   "modal-confirm-default": "知道了",
 
 
@@ -64,6 +66,12 @@
   "me_phone_placeholder": "点击绑定您的手机号",
   "me_phone_placeholder": "点击绑定您的手机号",
   "me_askSave": "您修改了信息,是否保存?",
   "me_askSave": "您修改了信息,是否保存?",
 
 
+  /**
+  头像
+  **/
+  "avatar_path_error": "读取图片异常,请重试!",
+
+
   "course_btn-see": "查看",
   "course_btn-see": "查看",
   "course_title_0": "新手指导",
   "course_title_0": "新手指导",
   "course_title_1": "弓的介绍及护具佩戴",
   "course_title_1": "弓的介绍及护具佩戴",

+ 7 - 0
Assets/BowArrow/Scripts/Components/TextAutoLanguage2/Resources/TextAutoLanguage2/en.json

@@ -15,6 +15,8 @@
   "common_return": "Return",
   "common_return": "Return",
   "common_send": "Send",
   "common_send": "Send",
   "common_code": "Verification code",
   "common_code": "Verification code",
+  "common_update_successful": "upload successful!",
+  "common_update_failed": "upload failed!",
 
 
   "modal-confirm-default": "OK",
   "modal-confirm-default": "OK",
 
 
@@ -65,6 +67,11 @@
   "me_phone_placeholder": "Click to bind your mobile number",
   "me_phone_placeholder": "Click to bind your mobile number",
   "me_askSave": "You have modified the information, do you want to save it?",
   "me_askSave": "You have modified the information, do you want to save it?",
 
 
+  /**
+  头像
+  **/
+  "avatar_path_error": "Abnormal image reading, please try again!",
+
   "course_btn-see": "see",
   "course_btn-see": "see",
   "course_title_0": "Novice guidance",
   "course_title_0": "Novice guidance",
   "course_title_1": "Introduction of bow and equipment",
   "course_title_1": "Introduction of bow and equipment",

+ 12 - 7
Assets/BowArrow/Scripts/Game/ArmBow.cs

@@ -33,7 +33,9 @@ public class ArmBow : MonoBehaviour
     [Tooltip("弓箭飞行速度")]
     [Tooltip("弓箭飞行速度")]
     [SerializeField]
     [SerializeField]
     public float slowFactor = 0.3f;
     public float slowFactor = 0.3f;
-
+    [Tooltip("弓箭未射中靶子的时候飞行速度")]
+    [SerializeField]
+    public float notSlowFactor = 0.8f;
     /// <summary>
     /// <summary>
     /// 相机相对箭的偏移量
     /// 相机相对箭的偏移量
     /// x = 左右偏移(负数表示往左,正数往右)
     /// x = 左右偏移(负数表示往左,正数往右)
@@ -42,13 +44,14 @@ public class ArmBow : MonoBehaviour
     /// </summary>
     /// </summary>
     [Tooltip("相机相对箭的偏移量(Y 是目标靶叠加值 目标Y + Y = 相机高度)")]
     [Tooltip("相机相对箭的偏移量(Y 是目标靶叠加值 目标Y + Y = 相机高度)")]
     [SerializeField]
     [SerializeField]
-    public Vector3 offsetFromArrow = new Vector3(0, 0.75f, -2.5f);
-    [Tooltip("没有射中目标靶的时候,给定一个跟随差值")]
+    public Vector3 offsetFromArrow = new Vector3(0, 0.8f, -2.5f);
+    [Tooltip("到达终点时候的最终值")]
     [SerializeField]
     [SerializeField]
-    public Vector3 offsetFromArrowTemp = new Vector3(0, 0.75f, -2.5f);
-    [Tooltip("相机与目标之间的最小停止距离")]
-    [SerializeField]
-    public float stopDistanceToTarget = 4f; // 相机与目标之间的最小停止距离(XZ 平面)
+    public Vector3 offsetFromArrowEnd = new Vector3(0, 0.8f, -2.5f);
+
+    //[Tooltip("相机与目标之间的最小停止距离")]
+    //[SerializeField]
+    //public float stopDistanceToTarget = 4f; // 相机与目标之间的最小停止距离(XZ 平面)
 
 
 
 
     BowCamera _bowCamera;
     BowCamera _bowCamera;
@@ -264,6 +267,7 @@ public class ArmBow : MonoBehaviour
         Quaternion oldCameraRotation = BowCamera.ins.transform.rotation;
         Quaternion oldCameraRotation = BowCamera.ins.transform.rotation;
         BowCamera.ins.transform.rotation = absolute_rotation;
         BowCamera.ins.transform.rotation = absolute_rotation;
         RaycastHit absoluteRay = CrossHair.ins.GetRaycastHit();
         RaycastHit absoluteRay = CrossHair.ins.GetRaycastHit();
+        RaycastHit absoluteRayDef = CrossHair.ins.GetRaycastDefault();
         BowCamera.ins.transform.rotation = oldCameraRotation;
         BowCamera.ins.transform.rotation = oldCameraRotation;
         
         
         Vector3 shootOutPosition = this.bowCamera.transform.position;
         Vector3 shootOutPosition = this.bowCamera.transform.position;
@@ -279,6 +283,7 @@ public class ArmBow : MonoBehaviour
         arrowComp.armBow = this;
         arrowComp.armBow = this;
         arrowComp.shootOutPosition = shootOutPosition;
         arrowComp.shootOutPosition = shootOutPosition;
         arrowComp.absoluteRay = absoluteRay;
         arrowComp.absoluteRay = absoluteRay;
+        arrowComp.absoluteRayDef = absoluteRayDef; 
         arrowComp.offsetAngle = GameDebug.ins ? 
         arrowComp.offsetAngle = GameDebug.ins ? 
             GameDebug.ins.GetOffsetAngle() : 
             GameDebug.ins.GetOffsetAngle() : 
             Quaternion.Angle(absolute_rotation, final_rotation);
             Quaternion.Angle(absolute_rotation, final_rotation);

+ 174 - 51
Assets/BowArrow/Scripts/Game/Arrow.cs

@@ -24,6 +24,9 @@ public class Arrow : MonoBehaviour
     [NonSerialized] public Vector3 shootOutPosition;
     [NonSerialized] public Vector3 shootOutPosition;
     //绝对射线
     //绝对射线
     [NonSerialized] public RaycastHit absoluteRay;
     [NonSerialized] public RaycastHit absoluteRay;
+    //默认层级的射线对象
+    [NonSerialized] public RaycastHit absoluteRayDef;
+
     //制作误差偏移角(强行制造误差,为了增加游戏难度,该偏移角的大小根据难度而定)
     //制作误差偏移角(强行制造误差,为了增加游戏难度,该偏移角的大小根据难度而定)
     [NonSerialized] public float offsetAngle;
     [NonSerialized] public float offsetAngle;
     //误差偏移后的欧拉角
     //误差偏移后的欧拉角
@@ -71,6 +74,8 @@ public class Arrow : MonoBehaviour
 
 
     void Start()
     void Start()
     {
     {
+        //speed = 23;
+
         mySpeed = speed;
         mySpeed = speed;
 
 
         Billboard.ins?.SetArrowSpeed(speed);
         Billboard.ins?.SetArrowSpeed(speed);
@@ -111,8 +116,16 @@ public class Arrow : MonoBehaviour
                 }
                 }
             }
             }
         }
         }
-
-        SetUpBeforFly();
+        //新的靶子场景不用这个SetUpBeforFly
+        if (isInGameScene)
+        {
+            this.InitFlyLogic_Parabola();
+        }
+        else 
+        {
+            SetUpBeforFly();
+        }
+         
 
 
         if (GlobalData.MyDeviceMode == DeviceMode.Archery)
         if (GlobalData.MyDeviceMode == DeviceMode.Archery)
         {
         {
@@ -202,10 +215,12 @@ public class Arrow : MonoBehaviour
         }
         }
         else
         else
         {
         {
+            //以前默认的计算
             finalPoint = this.transform.position + this.transform.forward * 100;
             finalPoint = this.transform.position + this.transform.forward * 100;
         }
         }
 
 
         parabolaAngleInRadian = finalAngleX / 180 * Mathf.PI;
         parabolaAngleInRadian = finalAngleX / 180 * Mathf.PI;
+
     }
     }
 
 
     /*
     /*
@@ -285,7 +300,7 @@ public class Arrow : MonoBehaviour
         float b = deltaX;
         float b = deltaX;
         float c = a - deltaY;
         float c = a - deltaY;
         hasParabolaAngle = Mathf.Pow(b, 2) - 4 * a * c >= 0;
         hasParabolaAngle = Mathf.Pow(b, 2) - 4 * a * c >= 0;
-        Debug.Log("是否有弧度解:" + hasParabolaAngle);
+        //Debug.Log("是否有弧度解:" + hasParabolaAngle);
         if (hasParabolaAngle)
         if (hasParabolaAngle)
         {
         {
             float res1 = (-b + Mathf.Pow(Mathf.Pow(b, 2) - 4 * a * c, 0.5f)) / (2 * a);
             float res1 = (-b + Mathf.Pow(Mathf.Pow(b, 2) - 4 * a * c, 0.5f)) / (2 * a);
@@ -295,7 +310,7 @@ public class Arrow : MonoBehaviour
             float angle1 = Mathf.Atan(res1); // 弧度
             float angle1 = Mathf.Atan(res1); // 弧度
             float angle2 = Mathf.Atan(res2);
             float angle2 = Mathf.Atan(res2);
 
 
-            // 这里决定是取低角度还是高角度
+            // 这里决定是取低角度还是高角度
             parabolaAngleInRadian = Mathf.Min(angle1, angle2);
             parabolaAngleInRadian = Mathf.Min(angle1, angle2);
              //parabolaAngleInRadian = Mathf.Max(angle1, angle2); // 高抛解
              //parabolaAngleInRadian = Mathf.Max(angle1, angle2); // 高抛解
         }
         }
@@ -304,7 +319,7 @@ public class Arrow : MonoBehaviour
     {
     {
         if (!isHit && flyTime >= 0)
         if (!isHit && flyTime >= 0)
         {
         {
-            flyTime += Time.deltaTime * currentSlowFactor;
+            flyTime += Time.deltaTime;
             if (flyTime > 14)
             if (flyTime > 14)
             {
             {
                 Debug.Log("flyTime:"+ flyTime);
                 Debug.Log("flyTime:"+ flyTime);
@@ -317,7 +332,7 @@ public class Arrow : MonoBehaviour
     void Update()
     void Update()
     {
     {
         if (isInGameScene)
         if (isInGameScene)
-            UpdateFlyLogic_New();
+            UpdateFlyLogic_Parabola();
         else
         else
             UpdateFlyLogic();
             UpdateFlyLogic();
 
 
@@ -393,64 +408,166 @@ public class Arrow : MonoBehaviour
             if (ArrowTraceDebug.ins) ArrowTraceDebug.ins.OnArrowUpdate(this);
             if (ArrowTraceDebug.ins) ArrowTraceDebug.ins.OnArrowUpdate(this);
         }
         }
     }
     }
-    /** 经典射击里面的 新飞行帧逻辑(运动学) */
-    void UpdateFlyLogic_New()
+    #region 新飞行帧逻辑 (非运动学)
+    /** 经典射击里面的 新飞行帧逻辑(非运动学) */
+    public float minFlyDuration = 0.2f;  // 最短飞行时间,受slowFactor影响
+    public float maxFlyDuration = 2.0f;  // 最长飞行时间,受slowFactor影响
+    public float maxAirTime = 3f;  // 最大允许飞行时间,超过这个时间即加上重力
+    public float minHeight = 0.3f;       // 最近时的最小弧度
+    public float maxHeightFactor = 1f;   // 最远时的最大弧度倍数
+
+    public float flyDuration;            // 自动计算后的飞行时长
+    public float maxHeight;              // 自动计算后的抛物线高度
+    private float flyTimer = 0f;
+    public float arrowLength = 1.7f;     // 箭长度
+    private bool isFalling = false;
+    /// <summary>
+    /// 初始化飞行参数(在发射时调用一次)
+    /// </summary>
+    public void InitFlyLogic_Parabola()
+    {
+        //获取终点
+        if (absoluteRay.transform)
+        {
+            if (Math.Abs(offsetAngle) < 0.001)
+            {
+                canPerfectHit = true;
+            }
+            if (rayHitTargetBody && Mathf.RoundToInt(rayHitTargetBody.GetDistance()) >= 50 && UnityEngine.Random.value < 0.5)
+            {
+                canUseSideCamera = true;
+            }
+            finalPoint = absoluteRay.point;
+        }
+        else
+        {
+            if (absoluteRayDef.transform)
+            {
+                finalPoint = absoluteRayDef.point;
+            }
+            else
+            {
+                //如果没有给个以前默认的计算
+                finalPoint = this.transform.position + this.transform.forward * 100;
+            }
+        }
+
+        // 目标距离
+        float dist = Vector3.Distance(shootOutPosition, finalPoint);
+
+        // 自动计算飞行时长
+        //flyDuration = Mathf.Clamp(dist / mySpeed, minFlyDuration, maxFlyDuration);
+        //Debug.Log("dist / mySpeed:" + (dist / mySpeed));
+        // 超长飞行时间 → 把终点调整到靶子前面,让箭自然落地
+        float flyTime = dist / mySpeed;
+        if (flyTime <= maxFlyDuration)
+        {
+            // 正常情况
+            flyDuration = flyTime;
+        }
+        else
+        {
+            // 超过最大飞行时间 → 缩短水平距离
+            flyDuration = maxFlyDuration;
+            // 缩短比例 = maxFlyDuration / flyTime
+            float ratio = maxFlyDuration / flyTime;
+            // 新终点 = 起点 + ratio * (原终点 - 起点)
+            this.finalPoint = shootOutPosition + (finalPoint - shootOutPosition) * ratio;
+            this.isFalling = true;
+            //
+            currentSlowFactor = ArmBow.ins.notSlowFactor;
+
+            //这里飞不到的时候给个false,反正弓箭最后跳回瞄准点
+            //OnHitAnyInFlyLogic 之后
+            canPerfectHit = false;
+            //给个加速卡提示?
+            AimLoadChecker.ins?.ShowOutTip();
+        }
+        // 自动计算抛物线弧度(远的高一点,近的低一点)
+        maxHeight = Mathf.Lerp(minHeight, maxHeightFactor, dist / 70f); // 假设70米是最大距离
+
+        flyTimer = 0f;
+        isHit = false;
+
+        // 起始位置
+        transform.position = shootOutPosition;
+    }
+
+    /// <summary>
+    /// 带抛物线的飞行逻辑
+    /// </summary>
+    void UpdateFlyLogic_Parabola()
     {
     {
         if (isHit) return;
         if (isHit) return;
-  
-        // 飞行进度 (0 = 刚射出, 1 = 到达目标)
-        float totalDistance = Vector3.Distance(shootOutPosition, finalPoint);
-        float currentDistance = Vector3.Distance(transform.position, shootOutPosition);
-        float progress = Mathf.Clamp01(currentDistance / totalDistance);
-
-        // 从曲线获取速度倍率
-        float speedMultiplier = ArmBow.ins.customCurveArrow.Evaluate(progress);
-        float detalSlowFactor = currentSlowFactor * 0.5f;
-        float _tempSlowFactor = currentSlowFactor + ( detalSlowFactor * speedMultiplier - detalSlowFactor);
-        //Debug.Log("TempSlowFactor:"+ _tempSlowFactor);
-        logicFlyTime += Time.deltaTime * _tempSlowFactor;
-
-        float currentSpeed = mySpeed; 
-        float t = logicFlyTime;
-        // --- 初速度分解 ---
-        float vx = Mathf.Cos(parabolaAngleInRadian) * currentSpeed; // 水平速度大小
-        float vy0 = Mathf.Sin(parabolaAngleInRadian) * currentSpeed; // 初始竖直速度
-        // XZ方向向量(水平朝向目标)
-        Vector3 dirXZ = new Vector3(finalPoint.x - shootOutPosition.x, 0, finalPoint.z - shootOutPosition.z).normalized;
 
 
-        // --- 位移计算 ---
-        float dx = vx * t; // 水平位移
-        float dy = vy0 * t + 0.5f * Physics.gravity.y * t * t; // 竖直位移
+        // 时间推进(整体慢速因子)
+        flyTimer += Time.deltaTime * currentSlowFactor;
 
 
-        Vector3 nextPosition = shootOutPosition + dirXZ * dx + Vector3.up * dy;
+        // 线性进度
+        float rawProgress = Mathf.Clamp01(flyTimer / flyDuration);
 
 
-        // --- 更新位置 ---
-        Vector3 oldPosition = this.transform.position;
-        this.transform.position = nextPosition;
+        // 曲线控制速度分布(0~1)
+        float curveValue = ArmBow.ins.customCurveArrow.Evaluate(rawProgress);
 
 
-        // --- 箭头朝向 ---
-        float vy = vy0 + Physics.gravity.y * t; // 当前瞬时竖直速度
-        float angleX = Mathf.Atan2(vy, vx) * Mathf.Rad2Deg;
-        Vector3 eulerAngles = this.transform.eulerAngles;
-        eulerAngles.x = -angleX;
-        this.transform.eulerAngles = eulerAngles;
+        // 最终进度 = 曲线值(保证能跑到 1)
+        float progress = Mathf.Clamp01(curveValue);
 
 
-        // --- 碰撞检测 ---
-        float deltaDistance = Vector3.Distance(oldPosition, nextPosition);
-        Ray ray = new Ray(oldPosition, nextPosition - oldPosition);
-        if (Physics.Raycast(ray, out RaycastHit raycastHit, deltaDistance))
+        // --- 水平位置插值 ---
+        Vector3 horizontalPos = Vector3.Lerp(shootOutPosition, finalPoint, progress);
+
+        // --- 抛物线高度 ---
+        float parabola = 4 * progress * (1 - progress); // 标准0~1抛物线
+        float heightOffset = parabola * maxHeight;
+
+        Vector3 nextPos = new Vector3(
+          horizontalPos.x,
+          Mathf.Lerp(shootOutPosition.y, finalPoint.y, progress) + heightOffset,
+          horizontalPos.z
+         );
+
+        // 移动
+        Vector3 oldPos = transform.position;
+
+        // --- 最终位置 ---
+        if (this.isFalling && progress == 1)
         {
         {
-            this.transform.position = raycastHit.point;
+            //这个状态到了,加个持续飞行,知道碰撞到停止?
+            // 当前状态:到达缩短终点,但需要持续飞行
+            Vector3 forwardMove = transform.forward * mySpeed *2  * Time.deltaTime * currentSlowFactor;
+            nextPos = transform.position + forwardMove;
+        }
+
+        transform.position = nextPos;
+
+        // 箭头朝向(指向移动方向)
+        Vector3 moveDir = (nextPos - oldPos).normalized;
+        if (moveDir.sqrMagnitude > 0.0001f)
+            transform.forward = moveDir;
+
+        // 箭尖位置(保证碰撞从箭尖检测)
+        Vector3 tipOffset = transform.forward * arrowLength * 0.5f;
+        Vector3 oldTip = oldPos + tipOffset;
+        Vector3 newTip = nextPos + tipOffset;
+
+        // 轨迹调试线
+        Debug.DrawLine(shootOutPosition, nextPos, Color.cyan, 2f);
+        Debug.DrawLine(oldTip, newTip, Color.green, 0.1f);
+
+        // 碰撞检测
+        if (Physics.Linecast(oldTip, newTip, out RaycastHit hit))
+        {
+            transform.position = hit.point - tipOffset; // 调整箭模型,使箭尖贴在命中点
+            Debug.DrawLine(oldTip, hit.point, Color.red, 2f);
             if (ArrowTraceDebug.ins) ArrowTraceDebug.ins.OnArrowUpdate(this);
             if (ArrowTraceDebug.ins) ArrowTraceDebug.ins.OnArrowUpdate(this);
-            OnHitAnyInFlyLogic(raycastHit);
-            Debug.Log(raycastHit.collider.gameObject.name);
+            OnHitAnyInFlyLogic(hit);
+            this.isFalling = false;
         }
         }
         else
         else
         {
         {
             if (ArrowTraceDebug.ins) ArrowTraceDebug.ins.OnArrowUpdate(this);
             if (ArrowTraceDebug.ins) ArrowTraceDebug.ins.OnArrowUpdate(this);
         }
         }
     }
     }
-
+    #endregion
     public class HitType
     public class HitType
     {
     {
         public static int None = 0;
         public static int None = 0;
@@ -465,9 +582,15 @@ public class Arrow : MonoBehaviour
     //飞行逻辑中检测到碰撞
     //飞行逻辑中检测到碰撞
     void OnHitAnyInFlyLogic(RaycastHit raycastHit)
     void OnHitAnyInFlyLogic(RaycastHit raycastHit)
     {
     {
-        this.Head().position = raycastHit.point;
-        this.transform.SetParent(raycastHit.transform.parent);
+
         string targetName = raycastHit.transform.gameObject.name;
         string targetName = raycastHit.transform.gameObject.name;
+        this.Head().position = raycastHit.point;
+        //命中靶子的情况下不移动这个parent,不然弓箭位置旋转会被影响
+        //其他情况照常
+        if (targetName != "TargetBody") {
+            this.transform.SetParent(raycastHit.transform.parent);
+        }
+
         this.raycastHitTransform = raycastHit.transform;
         this.raycastHitTransform = raycastHit.transform;
         if (targetName == "TargetBody")
         if (targetName == "TargetBody")
         {
         {

+ 77 - 95
Assets/BowArrow/Scripts/Game/ArrowCamera.cs

@@ -303,7 +303,9 @@ class ArrowCameraTemplate_targetLock : ArrowCameraTemplate
     /// </summary>
     /// </summary>
     private  Vector3 offsetFromArrow = new Vector3(0, 0.8f, -2.2f);
     private  Vector3 offsetFromArrow = new Vector3(0, 0.8f, -2.2f);
 
 
-    private  float stopDistanceToTarget = 4f; // 相机与目标之间的最小停止距离(XZ 平面)
+    private Vector3 targetCenterPos;//目标的中心点位置,靶子的射击靶中点
+
+    //private  float stopDistanceToTarget = 4f; // 相机与目标之间的最小停止距离(XZ 平面)
 
 
     private float totalDist = 0f; // 初始相机与目标之间的水平总距离(用于计算进度)
     private float totalDist = 0f; // 初始相机与目标之间的水平总距离(用于计算进度)
 
 
@@ -311,16 +313,15 @@ class ArrowCameraTemplate_targetLock : ArrowCameraTemplate
 
 
     private float arrowSpeed = 0;
     private float arrowSpeed = 0;
 
 
-    // 调节参数
-    private const float extraSpeedMax = 100f; // 相机在箭方向上的额外冲击速度最大值(越大越“冲”)
-    private const float minLerpSpeed = 2f;    // 插值时的最小速度(进度小的时候)
-    private const float maxLerpSpeed = 10f;   // 插值时的最大速度(进度接近 1 时)
+    public float maxVerticalOffset = -2f; // 相机最多比箭低 2
 
 
     public ArrowCameraTemplate_targetLock(ArrowCamera arrowCamera, Transform target,bool followTarget = true) : base(arrowCamera)
     public ArrowCameraTemplate_targetLock(ArrowCamera arrowCamera, Transform target,bool followTarget = true) : base(arrowCamera)
     {
     {
         //在 ArmBow.ins 调试参数
         //在 ArmBow.ins 调试参数
-        offsetFromArrow = followTarget? ArmBow.ins.offsetFromArrow : ArmBow.ins.offsetFromArrowTemp;
-        stopDistanceToTarget = ArmBow.ins.stopDistanceToTarget;
+        offsetFromArrow = ArmBow.ins.offsetFromArrow;
+        //stopDistanceToTarget = ArmBow.ins.stopDistanceToTarget;
+        //靶子的位置是 y = 1.3 中心点高度
+        targetCenterPos = new Vector3(target.position.x, 1.3f, target.position.z);
 
 
         this.target = target;
         this.target = target;
     
     
@@ -337,7 +338,7 @@ class ArrowCameraTemplate_targetLock : ArrowCameraTemplate
         cameraT.position = offsetPos;
         cameraT.position = offsetPos;
 
 
         // 记录初始总路程(XZ 平面,不考虑高度差)
         // 记录初始总路程(XZ 平面,不考虑高度差)
-        totalDist = GetXZDistance(offsetPos, target.position);
+        totalDist = GetXZDistance(offsetPos, targetCenterPos);
 
 
         arrowSpeed = arrowCamera.arrow.mySpeed;
         arrowSpeed = arrowCamera.arrow.mySpeed;
         Debug.Log("当前弓箭速度:" + arrowSpeed);
         Debug.Log("当前弓箭速度:" + arrowSpeed);
@@ -354,113 +355,97 @@ class ArrowCameraTemplate_targetLock : ArrowCameraTemplate
 
 
         if (!cameraStopped)
         if (!cameraStopped)
         {
         {
-            // 计算当前相机与目标的水平距离
-            float distToTarget = GetXZDistance(cameraT.position, target.position);
-
-            if (distToTarget <= stopDistanceToTarget)
-            {
-                // 到达目标附近 -> 停止跟随
-                cameraStopped = true;
-                cameraT.SetParent(null);
-            }
-            else
+            // 计算当前相机与靶子中心的水平距离
+            float distToTarget = GetXZDistance(cameraT.position, targetCenterPos);
+        
+            if (this.isFollowTarget)
             {
             {
-                // progress = 相机已接近目标的进度(0 = 起点,1 = 停止点)
-                float progress = Mathf.Clamp01(
-                    1f - (distToTarget - stopDistanceToTarget) / (totalDist - stopDistanceToTarget)
-                );
-
-                //Vector3 arrowScreenPos = cameraT.GetComponent<Camera>().WorldToViewportPoint(arrowT.position);
-                //bool isFollow = true;
-                //// 如果箭接近屏幕边缘,就往后退
-                //if (arrowScreenPos.x < -0.13f || arrowScreenPos.x > 1f ||
-                //    arrowScreenPos.y < -0.13f || arrowScreenPos.y > 1f)
+                //if (distToTarget <= stopDistanceToTarget)
                 //{
                 //{
-                //    //UnityEditor.EditorApplication.isPaused = true;
-                //    // offsetFromArrow.z =  ArmBow.ins.offsetFromArrow.z - 2;
-                //    // 用 Lerp 平滑过渡
-                //    //float targetZ = ArmBow.ins.offsetFromArrow.z - 4f;
-                //   // offsetFromArrow.z = Mathf.Lerp(offsetFromArrow.z, targetZ, Time.deltaTime * 2f);
-
-                //    Debug.Log("箭离开屏幕!" + offsetFromArrow);
-                //    isFollow = false;
+                //    // 到达目标附近 -> 停止跟随
+                //    cameraStopped = true;
+                //    cameraT.SetParent(null);
+                //    Debug.Log("distToTarget:" + distToTarget + ",stopDistanceToTarget:"+ stopDistanceToTarget);
+                //    Debug.DrawLine(cameraT.position, targetCenterPos, Color.blue, 2f);
                 //}
                 //}
                 //else
                 //else
                 //{
                 //{
-                //    //offsetFromArrow.z = ArmBow.ins.offsetFromArrow.z;
-                //    isFollow = true;
+                //// progress = 相机已接近目标的进度(0 = 起点,1 = 停止点)
+                //float progress = Mathf.Clamp01(
+                //    1f - (distToTarget - stopDistanceToTarget) / (totalDist - stopDistanceToTarget)
+                //);
                 //}
                 //}
-
+                // progress = 相机已接近目标的进度(0 = 起点,1 = 停止点)
+                float progress = Mathf.Clamp01(
+                    1f - (distToTarget) / (totalDist)
+                );
                 // 用缓动曲线计算归一化进度(决定移动节奏)
                 // 用缓动曲线计算归一化进度(决定移动节奏)
                 float easedT = ArmBow.ins.customCurveCamera.Evaluate(progress);
                 float easedT = ArmBow.ins.customCurveCamera.Evaluate(progress);
 
 
-                // 箭位置 + 偏移(相机保持与箭的相对位置,并锁定到目标的高度)
-                Vector3 offsetPos = arrowT.position + arrowT.TransformDirection(offsetFromArrow);
-                //offsetPos.y = target.position.y + offsetFromArrow.y;
-               // offsetPos.x = target.position.x;
+                // -----------------------
+                // 插值偏移量 (从 offsetFromArrow -> offsetFromArrowEnd)
+                // -----------------------
+                Vector3 lerpedOffset = Vector3.Lerp(offsetFromArrow, ArmBow.ins.offsetFromArrowEnd, easedT);
 
 
-                if (this.isFollowTarget)
-                {
-                    // 箭的水平前进方向(忽略x, y 分量)arrowT.forward.x
-                    Vector3 arrowDirXZ = new Vector3(0f, 0f, arrowT.forward.z).normalized;
 
 
-                    // 相机额外的位移偏移量(随 easedT 增强,越靠近目标越快
-                    Vector3 extraOffset = arrowDirXZ * Mathf.Lerp(0f, extraSpeedMax, easedT) * Time.deltaTime;
+                // 箭位置 + 偏移(相机保持与箭的相对位置,并锁定到目标的高度)
+                Vector3 offsetPos = arrowT.position + arrowT.TransformDirection(lerpedOffset);
 
 
-                    // 最终目标位置 = 箭偏移位置 + 额外速度偏移
-                    Vector3 targetPos = offsetPos + extraOffset;
+                // -----------------------
+                // 修改:保持 x 对齐靶子
+                // -----------------------
+                offsetPos.x = target.position.x;
 
 
-                    // 相机向目标位置平滑插值(随曲线加速)
-                    float speedFactor = Mathf.Lerp(minLerpSpeed, maxLerpSpeed, easedT);
-                    cameraT.position = Vector3.Lerp(cameraT.position, targetPos, Time.deltaTime * speedFactor);
-                }
-                else {
-                    // 最终目标位置 = 箭偏移位置 
-                    //offsetPos.z = arrowT.position.z - 5;
-                    // 箭的水平前进方向(忽略 y 分量)arrowT.forward.x
-                    //Vector3 arrowDirXZ = new Vector3(0f, 0f, arrowT.forward.z).normalized;
+                // y 固定为靶子高度
+                offsetPos.y = target.position.y + offsetFromArrow.y;
 
 
-                    // 相机额外的位移偏移量(随 easedT 增强,越靠近目标越快)
-                    // Vector3 extraOffset = arrowDirXZ * Mathf.Lerp(0f, extraSpeedMax, easedT) * Time.deltaTime;
+                cameraT.position = offsetPos;
+            }
+            else {
+                //如果没射中目标靶,不用判断目标距离,
+                //需要判断一下射出目标高度,比如现在是固定y和目标靶的,如何飞高之后,相机不再参考目标靶高度,跟随箭飞出去,
+                //也给一个能看到的最低水平距离,比如箭飞出相机的距离是10,那就是需要相机和箭保持5的相对高度
+                // ----------------------------
+                // 自由跟随模式(非目标锁定,下方视角 + 阶段性切换)
+                // ----------------------------
 
 
-                    // 最终目标位置 = 箭偏移位置 + 额外速度偏移
-                    //Vector3 targetPos = offsetPos + extraOffset;
-                    // 相机向目标位置平滑插值(随曲线加速)
-                    //float speedFactor = Mathf.Lerp(minLerpSpeed, maxLerpSpeed, easedT);
-                    //cameraT.position = offsetPos;// Vector3.Lerp(cameraT.position, targetPos, Time.deltaTime * speedFactor);
-                    cameraT.SetParent(null);
+                Vector3 offsetPos = arrowT.position + arrowT.TransformDirection(offsetFromArrow);
 
 
-                    // 箭的前进方向(XZ 平面)Z 分量
-                    float arrowSpeedZ = arrowSpeed * arrowT.forward.z; // 箭速度 * 前方向 Z 分量
+                // 相机的目标高度,初始参考靶子,后续逐渐切换为箭下方固定距离
+                float targetY = target.position.y + offsetFromArrow.y;
 
 
-                    // 相机当前 Z 位置参考箭的 Z
-                    float targetZ = cameraT.position.z + arrowSpeedZ * Time.deltaTime;
+                // 箭当前位置的理想“下方位置”
+                float arrowFollowY = arrowT.position.y + maxVerticalOffset;
 
 
-                    // 保证相机不要超过箭本身(可选)
-                    targetZ = Mathf.Min(targetZ, arrowT.position.z);
+                // 如果相机位置比箭下方限制还高,就参考靶子高度;
+                // 一旦箭飞到比 maxVerticalOffset 更高的地方,相机高度固定在箭下方 maxVerticalOffset。
+                if (targetY > arrowFollowY)
+                    offsetPos.y = targetY;
+                else
+                    offsetPos.y = arrowFollowY;
 
 
-                    // 目标位置 = 相机 X/Y 偏移不变,Z 方向用 targetZ
-                    Vector3 targetPos = new Vector3(
-                        target.position.x,      // X 可以锁定到目标或保持 offsetPos.x
-                        offsetPos.y,            // Y 维持箭高度偏移
-                        targetZ
-                    );
+                // 保持相机与箭的最小水平距离,避免重叠
+                float minXZDistance = 4f;
+                Vector3 flatCamPos = new Vector3(offsetPos.x, 0, offsetPos.z);
+                Vector3 flatArrowPos = new Vector3(arrowT.position.x, 0, arrowT.position.z);
+                float currentXZDist = Vector3.Distance(flatCamPos, flatArrowPos);
 
 
-                    // 直接插值平滑到目标位置(也可以加缓动)
-                    float speedFactor = 5f; // 可调节平滑程度
-                    cameraT.position = Vector3.Lerp(cameraT.position, targetPos, Time.deltaTime * speedFactor);
+                if (currentXZDist < minXZDistance)
+                {
+                    // 推开到最小水平距离
+                    Vector3 dir = (flatCamPos - flatArrowPos).normalized;
+                    flatCamPos = flatArrowPos + dir * minXZDistance;
+                    offsetPos.x = flatCamPos.x;
+                    offsetPos.z = flatCamPos.z;
+                }
 
 
+                // 更新相机位置
+                cameraT.position = offsetPos;
 
 
-                }
-                // 最后强制 y = 靶子高度,不受lerp影响
-                Vector3 fixedPos = cameraT.position;
-                fixedPos.y = target.position.y + offsetFromArrow.y;
-                cameraT.position = fixedPos;
-
-                //#if UNITY_EDITOR
-                //                Debug.Log($"progress:{progress:F2}, easedT:{easedT:F2}, speed:{speedFactor:F2}, extra:{extraOffset}");
-                //#endif
+                // 相机始终朝向箭头方向(可选,保证画面观感)
+                //cameraT.LookAt(arrowT);
             }
             }
+           
  
  
         }
         }
 
 
@@ -496,9 +481,6 @@ class ArrowCameraTemplate_targetLock : ArrowCameraTemplate
 }
 }
 
 
 
 
-
-
-
 public class ArrowCameraTemplate {
 public class ArrowCameraTemplate {
     public ArrowCamera arrowCamera;
     public ArrowCamera arrowCamera;
     public ArrowCameraTemplate(ArrowCamera arrowCamera)
     public ArrowCameraTemplate(ArrowCamera arrowCamera)

+ 23 - 3
Assets/BowArrow/Scripts/Game/CrossHair.cs

@@ -106,10 +106,30 @@ public class CrossHair : MonoBehaviour
         }
         }
         Physics.Raycast(ray.origin, ray.direction, out raycastHit, maxDistance, layerMask);
         Physics.Raycast(ray.origin, ray.direction, out raycastHit, maxDistance, layerMask);
         return raycastHit;
         return raycastHit;
-    }    
-
+    }
+    /// <summary>
+    /// 获取默认层级的
+    /// </summary>
+    /// <returns></returns>
+    public RaycastHit GetRaycastDefault()
+    {
+        float maxDistance = 100;
+        int layerMask = 1 << 0; //Default
+        RaycastHit raycastHit;
+        Ray ray;
+        if (Camera.main.name.EndsWith("Fixed"))
+        {
+            ray = new Ray(BowCamera.ins.transform.position, BowCamera.ins.transform.forward);
+        }
+        else
+        {
+            ray = Camera.main.ScreenPointToRay(this.transform.position, Camera.MonoOrStereoscopicEye.Mono);
+        }
+        Physics.Raycast(ray.origin, ray.direction, out raycastHit, maxDistance, layerMask);
+        return raycastHit;
+    }
     #region 固定相机更新
     #region 固定相机更新
-        RectTransform _parentRTF;
+    RectTransform _parentRTF;
         RectTransform parentRTF {
         RectTransform parentRTF {
             get {
             get {
                 if (!_parentRTF) {
                 if (!_parentRTF) {

+ 1 - 1
Assets/BowArrow/Scripts/GameMode/GameModeTest.cs

@@ -9,7 +9,7 @@ public class GameModeTest : GameMode {
         if (SceneManager.GetActiveScene().name == "Game") {
         if (SceneManager.GetActiveScene().name == "Game") {
             TargetBody targetBody = GameObject.Find("GameArea/TargetObject/TargetBody").GetComponent<TargetBody>();
             TargetBody targetBody = GameObject.Find("GameArea/TargetObject/TargetBody").GetComponent<TargetBody>();
             GameObject.FindObjectOfType<ArmBow>().validTargets.Add(targetBody);
             GameObject.FindObjectOfType<ArmBow>().validTargets.Add(targetBody);
-            targetBody.SetDistance(40);
+            targetBody.SetDistance(70);
         }
         }
     }
     }
     public override void HitTarget(float score) {
     public override void HitTarget(float score) {

+ 28 - 0
Assets/BowArrow/Scripts/Network/HttpController/LoginController.cs

@@ -199,6 +199,34 @@ public class LoginController : JCUnityLib.Singleton<LoginController>
             if (callback != null) callback(requestResult);
             if (callback != null) callback(requestResult);
         }
         }
     }
     }
+    /// <summary>
+    /// 上传自定义头像
+    /// </summary>
+    /// <param name="texture"></param>
+    /// <returns></returns>
+    public IEnumerator UploadAvatar(Texture2D texture, Action<RequestResult> callback)
+    {
+        string url = $"{CommonConfig.gateServerURL}/SmartBowSDK/uploadAvatar";
+        byte[] imageBytes = texture.EncodeToPNG();
+        WWWForm form = new WWWForm();
+        form.AddField("token", PlayerPrefs.GetString(LoginMgr.LoginTokenKey));
+        form.AddField("tokenType", "client");
+        form.AddBinaryData("file", imageBytes, "avatar.png", "image/png");
+
+        using (UnityWebRequest uwr = UnityWebRequest.Post(url, form))
+        {
+            yield return uwr.SendWebRequest();
+
+            RequestResult requestResult;
+            if (uwr.result == UnityWebRequest.Result.Success)
+                requestResult = JsonConvert.DeserializeObject<RequestResult>(uwr.downloadHandler.text);
+            else
+            {
+                requestResult = new RequestResult();
+            }
+            if (callback != null) callback(requestResult);
+        }
+    }
 
 
     public IEnumerator ListUsernamesByEmail(string email, long timestamp, string sign, Action<RequestResult> callback) {
     public IEnumerator ListUsernamesByEmail(string email, long timestamp, string sign, Action<RequestResult> callback) {
         string url = CommonConfig.gateServerURL + "/gameLogin/listUsernamesByEmail";
         string url = CommonConfig.gateServerURL + "/gameLogin/listUsernamesByEmail";

BIN
Assets/BowArrow/Textures/Me/addIcon.png


+ 135 - 0
Assets/BowArrow/Textures/Me/addIcon.png.meta

@@ -0,0 +1,135 @@
+fileFormatVersion: 2
+guid: a12dff2b4d0faac4fb03a516864b15d5
+TextureImporter:
+  internalIDToNameTable: []
+  externalObjects: {}
+  serializedVersion: 12
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapsPreserveCoverage: 0
+    alphaTestReferenceValue: 0.5
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  streamingMipmaps: 0
+  streamingMipmapsPriority: 0
+  vTOnly: 0
+  ignoreMasterTextureLimit: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: 1
+  maxTextureSize: 2048
+  textureSettings:
+    serializedVersion: 2
+    filterMode: 1
+    aniso: 1
+    mipBias: 0
+    wrapU: 1
+    wrapV: 1
+    wrapW: 0
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 1
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spritePixelsToUnits: 100
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spriteGenerateFallbackPhysicsShape: 1
+  alphaUsage: 1
+  alphaIsTransparency: 1
+  spriteTessellationDetail: -1
+  textureType: 8
+  textureShape: 1
+  singleChannelComponent: 0
+  flipbookRows: 1
+  flipbookColumns: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  ignorePngGamma: 0
+  applyGammaDecoding: 0
+  cookieLightType: 0
+  platformSettings:
+  - serializedVersion: 3
+    buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Standalone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: iPhone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Android
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+    physicsShape: []
+    bones: []
+    spriteID: 5e97eb03825dee720800000000000000
+    internalID: 0
+    vertices: []
+    indices: 
+    edges: []
+    weights: []
+    secondaryTextures: []
+    nameFileIdTable: {}
+  spritePackingTag: 
+  pSDRemoveMatte: 0
+  pSDShowRemoveMatteOption: 0
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/BowArrow/Textures/Me/addImage.png


+ 135 - 0
Assets/BowArrow/Textures/Me/addImage.png.meta

@@ -0,0 +1,135 @@
+fileFormatVersion: 2
+guid: 2f17576af51d0d244bf5540a3cfe19d2
+TextureImporter:
+  internalIDToNameTable: []
+  externalObjects: {}
+  serializedVersion: 12
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 0
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapsPreserveCoverage: 0
+    alphaTestReferenceValue: 0.5
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  streamingMipmaps: 0
+  streamingMipmapsPriority: 0
+  vTOnly: 0
+  ignoreMasterTextureLimit: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: 1
+  maxTextureSize: 2048
+  textureSettings:
+    serializedVersion: 2
+    filterMode: 1
+    aniso: 1
+    mipBias: 0
+    wrapU: 1
+    wrapV: 1
+    wrapW: 0
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 1
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spritePixelsToUnits: 100
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spriteGenerateFallbackPhysicsShape: 1
+  alphaUsage: 1
+  alphaIsTransparency: 1
+  spriteTessellationDetail: -1
+  textureType: 8
+  textureShape: 1
+  singleChannelComponent: 0
+  flipbookRows: 1
+  flipbookColumns: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  ignorePngGamma: 0
+  applyGammaDecoding: 0
+  cookieLightType: 0
+  platformSettings:
+  - serializedVersion: 3
+    buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Standalone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: iPhone
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  - serializedVersion: 3
+    buildTarget: Android
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+    physicsShape: []
+    bones: []
+    spriteID: 5e97eb03825dee720800000000000000
+    internalID: 0
+    vertices: []
+    indices: 
+    edges: []
+    weights: []
+    secondaryTextures: []
+    nameFileIdTable: {}
+  spritePackingTag: 
+  pSDRemoveMatte: 0
+  pSDShowRemoveMatteOption: 0
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/Plugins/NativeGallery.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 5e05ed2bddbccb94e9650efb5742e452
+folderAsset: yes
+timeCreated: 1518877529
+licenseType: Free
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/Plugins/NativeGallery/Android.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 0a607dcda26e7614f86300c6ca717295
+folderAsset: yes
+timeCreated: 1498722617
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 52 - 0
Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs

@@ -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

+ 12 - 0
Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 2d517fd0f2f85f24698df2775bee58e9
+timeCreated: 1544889149
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 62 - 0
Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs

@@ -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

+ 12 - 0
Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4c18d702b07a63945968db47201b95c9
+timeCreated: 1519060539
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 24 - 0
Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs

@@ -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

+ 12 - 0
Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: a07afac614af1294d8e72a3c083be028
+timeCreated: 1519060539
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Assets/Plugins/NativeGallery/Android/NativeGallery.aar


+ 33 - 0
Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta

@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: db4d55e1212537e4baa84cac66eb6645
+timeCreated: 1569764737
+licenseType: Free
+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: 

+ 9 - 0
Assets/Plugins/NativeGallery/Editor.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 19fc6b8ce781591438a952d8aa9104f8
+folderAsset: yes
+timeCreated: 1521452097
+licenseType: Free
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 119 - 0
Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs

@@ -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
+	}
+}

+ 12 - 0
Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: dff1540cf22bfb749a2422f445cf9427
+timeCreated: 1521452119
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 15 - 0
Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef

@@ -0,0 +1,15 @@
+{
+    "name": "NativeGallery.Editor",
+    "references": [],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3dffc8e654f00c545a82d0a5274d51eb
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 3 - 0
Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef

@@ -0,0 +1,3 @@
+{
+	"name": "NativeGallery.Runtime"
+}

+ 7 - 0
Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 6e5063adab271564ba0098a06a8cebda
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 988 - 0
Assets/Plugins/NativeGallery/NativeGallery.cs

@@ -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 );
+		}
+
+#pragma warning disable 0162
+		return null;
+#pragma warning restore 0162
+	}
+
+	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
+}

+ 12 - 0
Assets/Plugins/NativeGallery/NativeGallery.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: ce1403606c3629046a0147d3e705f7cc
+timeCreated: 1498722610
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 6 - 0
Assets/Plugins/NativeGallery/README.txt

@@ -0,0 +1,6 @@
+= Native Gallery for Android & iOS (v1.9.1) =
+
+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

+ 8 - 0
Assets/Plugins/NativeGallery/README.txt.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: be769f45b807c40459e5bafb18e887d6
+timeCreated: 1563308465
+licenseType: Free
+TextScriptImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/Plugins/NativeGallery/iOS.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 9c623599351a41a4c84c20f73c9d8976
+folderAsset: yes
+timeCreated: 1498722622
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 132 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs

@@ -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

+ 12 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 71fb861c149c2d1428544c601e52a33c
+timeCreated: 1519060539
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 45 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs

@@ -0,0 +1,45 @@
+#if UNITY_EDITOR || UNITY_IOS
+using UnityEngine;
+
+namespace NativeGalleryNamespace
+{
+	public class NGMediaSaveCallbackiOS : MonoBehaviour
+	{
+		private static NGMediaSaveCallbackiOS instance;
+		private NativeGallery.MediaSaveCallback callback;
+
+		public static void Initialize( NativeGallery.MediaSaveCallback callback )
+		{
+			if( instance == null )
+			{
+				instance = new GameObject( "NGMediaSaveCallbackiOS" ).AddComponent<NGMediaSaveCallbackiOS>();
+				DontDestroyOnLoad( instance.gameObject );
+			}
+			else if( instance.callback != null )
+				instance.callback( false, null );
+
+			instance.callback = callback;
+		}
+
+		[UnityEngine.Scripting.Preserve]
+		public void OnMediaSaveCompleted( string message )
+		{
+			NativeGallery.MediaSaveCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( true, null );
+		}
+
+		[UnityEngine.Scripting.Preserve]
+		public void OnMediaSaveFailed( string error )
+		{
+			NativeGallery.MediaSaveCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( false, null );
+		}
+	}
+}
+#endif

+ 12 - 0
Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 9cbb865d0913a0d47bb6d2eb3ad04c4f
+timeCreated: 1519060539
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 35 - 0
Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs

@@ -0,0 +1,35 @@
+#if UNITY_EDITOR || UNITY_IOS
+using UnityEngine;
+
+namespace NativeGalleryNamespace
+{
+	public class NGPermissionCallbackiOS : MonoBehaviour
+	{
+		private static NGPermissionCallbackiOS instance;
+		private NativeGallery.PermissionCallback callback;
+
+		public static void Initialize( NativeGallery.PermissionCallback callback )
+		{
+			if( instance == null )
+			{
+				instance = new GameObject( "NGPermissionCallbackiOS" ).AddComponent<NGPermissionCallbackiOS>();
+				DontDestroyOnLoad( instance.gameObject );
+			}
+			else if( instance.callback != null )
+				instance.callback( NativeGallery.Permission.ShouldAsk );
+
+			instance.callback = callback;
+		}
+
+		[UnityEngine.Scripting.Preserve]
+		public void OnPermissionRequested( string message )
+		{
+			NativeGallery.PermissionCallback _callback = callback;
+			callback = null;
+
+			if( _callback != null )
+				_callback( (NativeGallery.Permission) int.Parse( message ) );
+		}
+	}
+}
+#endif

+ 12 - 0
Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: bc6d7fa0a99114a45b1a6800097c6eb1
+timeCreated: 1519060539
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1251 - 0
Assets/Plugins/NativeGallery/iOS/NativeGallery.mm

@@ -0,0 +1,1251 @@
+#import <Foundation/Foundation.h>
+#import <Photos/Photos.h>
+#import <MobileCoreServices/UTCoreTypes.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+#import <ImageIO/ImageIO.h>
+#import <PhotosUI/PhotosUI.h>
+
+extern UIViewController* UnityGetGLViewController();
+
+#define CHECK_IOS_VERSION( version )  ([[[UIDevice currentDevice] systemVersion] compare:version options:NSNumericSearch] != NSOrderedAscending)
+
+@interface UNativeGallery:NSObject
++ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode;
++ (void)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode;
++ (void)showLimitedLibraryPicker;
++ (void)openSettings;
++ (int)canPickMultipleMedia;
++ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage permissionFreeMode:(BOOL)permissionFreeMode;
++ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit;
++ (int)isMediaPickerBusy;
++ (int)getMediaTypeFromExtension:(NSString *)extension;
++ (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 UNativeGallery
+
+static NSString *pickedMediaSavePath;
+static UIImagePickerController *imagePicker;
+API_AVAILABLE(ios(14))
+static PHPickerViewController *imagePickerNew;
+static int imagePickerState = 0; // 0 -> none, 1 -> showing (always in this state on iPad), 2 -> finished
+static BOOL simpleMediaPickMode;
+static BOOL pickingMultipleFiles = NO;
+
++ (int)checkPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode
+{
+	// On iOS 11 and later, permission isn't mandatory to fetch media from Photos
+	if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )
+		return 1;
+	
+	// Photos permissions has changed on iOS 14
+	if( @available(iOS 14.0, *) )
+	{
+		// Request ReadWrite permission in 2 cases:
+		// 1) When attempting to pick media from Photos with PHPhotoLibrary (readPermission=true and permissionFreeMode=false)
+		// 2) When attempting to write media to a specific album in Photos using PHPhotoLibrary (readPermission=false and permissionFreeMode=false)
+		PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( ( readPermission || !permissionFreeMode ) ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];
+		if( status == PHAuthorizationStatusAuthorized )
+			return 1;
+		else if( status == PHAuthorizationStatusRestricted )
+			return 3;
+		else if( status == PHAuthorizationStatusNotDetermined )
+			return 2;
+		else
+			return 0;
+	}
+	else
+	{
+		PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
+		if( status == PHAuthorizationStatusAuthorized )
+			return 1;
+		else if( status == PHAuthorizationStatusNotDetermined )
+			return 2;
+		else
+			return 0;
+	}
+}
+
++ (void)requestPermission:(BOOL)readPermission permissionFreeMode:(BOOL)permissionFreeMode
+{
+	// On iOS 11 and later, permission isn't mandatory to fetch media from Photos
+	if( readPermission && permissionFreeMode && CHECK_IOS_VERSION( @"11.0" ) )
+		UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" );
+	else if( @available(iOS 14.0, *) )
+	{
+		// Photos permissions has changed on iOS 14. There are 2 permission dialogs now:
+		// - AddOnly permission dialog: has 2 options: "Allow" and "Don't Allow". This dialog grants permission for save operations only. Unfortunately,
+		//   saving media to a custom album isn't possible with this dialog, media can only be saved to the default Photos album
+		// - ReadWrite permission dialog: has 3 options: "Allow Access to All Photos" (i.e. full permission), "Select Photos" (i.e. limited access) and
+		//   "Don't Allow". To be able to save media to a custom album, user must grant Full Photos permission. Thus, even when readPermission is false,
+		//   this dialog will be used if PermissionFreeMode is set to false. So, PermissionFreeMode determines whether or not saving to a custom album is supported
+		PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly )];
+		if( status == PHAuthorizationStatusAuthorized )
+			UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" );
+		else if( status == PHAuthorizationStatusRestricted )
+			UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "3" );
+		else if( status == PHAuthorizationStatusNotDetermined )
+		{
+			[PHPhotoLibrary requestAuthorizationForAccessLevel:( readPermission ? PHAccessLevelReadWrite : PHAccessLevelAddOnly ) handler:^( PHAuthorizationStatus status )
+			{
+				if( status == PHAuthorizationStatusAuthorized )
+					UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" );
+				else if( status == PHAuthorizationStatusRestricted )
+					UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "3" );
+				else
+					UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "0" );
+			}];
+		}
+		else
+			UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "0" );
+	}
+	else
+	{
+		// Request permission using Photos framework: https://stackoverflow.com/a/32989022/2373034
+		PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
+		if( status == PHAuthorizationStatusAuthorized )
+			UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "1" );
+		else if( status == PHAuthorizationStatusNotDetermined )
+		{
+			[PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status )
+			{
+				UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", ( status == PHAuthorizationStatusAuthorized ) ? "1" : "0" );
+			}];
+		}
+		else
+			UnitySendMessage( "NGPermissionCallbackiOS", "OnPermissionRequested", "0" );
+	}
+}
+
+// When Photos permission is set to restricted, allows user to change the permission or change the list of restricted images
+// It doesn't support a deterministic callback; for example there is a photoLibraryDidChange event but it won't be invoked if
+// user doesn't change the list of restricted images
++ (void)showLimitedLibraryPicker
+{
+	if( @available(iOS 14.0, *) )
+	{
+		PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
+		if( status == PHAuthorizationStatusNotDetermined )
+			[self requestPermission:YES permissionFreeMode:NO];
+		else if( status == PHAuthorizationStatusRestricted )
+			[[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:UnityGetGLViewController()];
+	}
+}
+
+// Credit: https://stackoverflow.com/a/25453667/2373034
++ (void)openSettings
+{
+	[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
+}
+
++ (int)canPickMultipleMedia
+{
+	if( @available(iOS 14.0, *) )
+		return 1;
+	else
+		return 0;
+}
+
+// Credit: https://stackoverflow.com/a/39909129/2373034
++ (void)saveMedia:(NSString *)path albumName:(NSString *)album isImage:(BOOL)isImage permissionFreeMode:(BOOL)permissionFreeMode
+{
+	// On iOS 14+, permission workflow has changed significantly with the addition of PHAuthorizationStatusRestricted permission. On those versions,
+	// user must grant Full Photos permission to be able to save to a custom album. Hence, there are 2 workflows:
+	// - If PermissionFreeMode is enabled, save the media directly to the default album (i.e. ignore 'album' parameter). This will present a simple
+	//   permission dialog stating "The app requires access to Photos to save media to it." and the "Selected Photos" permission won't be listed in the options
+	// - Otherwise, the more complex "The app requires access to Photos to interact with it." permission dialog will be shown and if the user grants
+	//   Full Photos permission, only then the image will be saved to the specified album. If user selects "Selected Photos" permission, default album will be
+	//   used as fallback
+	void (^saveToPhotosAlbum)() = ^void()
+	{
+		if( isImage )
+		{
+			// Try preserving image metadata (essential for animated gif images)
+			[[PHPhotoLibrary sharedPhotoLibrary] performChanges:
+			^{
+				[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];
+			}
+			completionHandler:^( BOOL success, NSError *error )
+			{
+				if( success )
+				{
+					[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+					UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+				}
+				else
+				{
+					NSLog( @"Error creating asset in default Photos album: %@", error );
+					
+					UIImage *image = [UIImage imageWithContentsOfFile:path];
+					if( image != nil )
+						UIImageWriteToSavedPhotosAlbum( image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );
+					else
+					{
+						NSLog( @"Couldn't create UIImage from file at path: %@", path );
+						[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+						UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+					}
+				}
+			}];
+		}
+		else
+		{
+			if( UIVideoAtPathIsCompatibleWithSavedPhotosAlbum( path ) )
+				UISaveVideoAtPathToSavedPhotosAlbum( path, self, @selector(video:didFinishSavingWithError:contextInfo:), (__bridge_retained void *) path );
+			else
+			{
+				NSLog( @"Video at path isn't compatible with saved photos album: %@", path );
+				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+			}
+		}
+	};
+
+	void (^saveBlock)(PHAssetCollection *assetCollection) = ^void( PHAssetCollection *assetCollection )
+	{
+		[[PHPhotoLibrary sharedPhotoLibrary] performChanges:
+		^{
+			PHAssetChangeRequest *assetChangeRequest;
+			if( isImage )
+				assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:[NSURL fileURLWithPath:path]];
+			else
+				assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:[NSURL fileURLWithPath:path]];
+			
+			PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:assetCollection];
+			[assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]];
+			
+		}
+		completionHandler:^( BOOL success, NSError *error )
+		{
+			if( success )
+			{
+				[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+				UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+			}
+			else
+			{
+				NSLog( @"Error creating asset: %@", error );
+				saveToPhotosAlbum();
+			}
+		}];
+	};
+
+	if( permissionFreeMode && CHECK_IOS_VERSION( @"14.0" ) )
+		saveToPhotosAlbum();
+	else
+	{
+		PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
+		fetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", album];
+		PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:fetchOptions];
+		if( fetchResult.count > 0 )
+			saveBlock( fetchResult.firstObject);
+		else
+		{
+			__block PHObjectPlaceholder *albumPlaceholder;
+			[[PHPhotoLibrary sharedPhotoLibrary] performChanges:
+			^{
+				PHAssetCollectionChangeRequest *changeRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:album];
+				albumPlaceholder = changeRequest.placeholderForCreatedAssetCollection;
+			}
+			completionHandler:^( BOOL success, NSError *error )
+			{
+				if( success )
+				{
+					PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumPlaceholder.localIdentifier] options:nil];
+					if( fetchResult.count > 0 )
+						saveBlock( fetchResult.firstObject);
+					else
+					{
+						NSLog( @"Error creating album: Album placeholder not found" );
+						saveToPhotosAlbum();
+					}
+				}
+				else
+				{
+					NSLog( @"Error creating album: %@", error );
+					saveToPhotosAlbum();
+				}
+			}];
+		}
+	}
+}
+
++ (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
+{
+	NSString* path = (__bridge_transfer NSString *)(contextInfo);
+	[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+
+	if( error == nil )
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+	else
+	{
+		NSLog( @"Error saving image with UIImageWriteToSavedPhotosAlbum: %@", error );
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+	}
+}
+
++ (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
+{
+	NSString* path = (__bridge_transfer NSString *)(contextInfo);
+	[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+
+	if( error == nil )
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveCompleted", "" );
+	else
+	{
+		NSLog( @"Error saving video with UISaveVideoAtPathToSavedPhotosAlbum: %@", error );
+		UnitySendMessage( "NGMediaSaveCallbackiOS", "OnMediaSaveFailed", "" );
+	}
+}
+
+// Credit: https://stackoverflow.com/a/10531752/2373034
++ (void)pickMedia:(int)mediaType savePath:(NSString *)mediaSavePath permissionFreeMode:(BOOL)permissionFreeMode selectionLimit:(int)selectionLimit
+{
+	pickedMediaSavePath = mediaSavePath;
+	imagePickerState = 1;
+	simpleMediaPickMode = permissionFreeMode && CHECK_IOS_VERSION( @"11.0" );
+	
+	if( @available(iOS 14.0, *) )
+	{
+		// PHPickerViewController is used on iOS 14
+		PHPickerConfiguration *config = simpleMediaPickMode ? [[PHPickerConfiguration alloc] init] : [[PHPickerConfiguration alloc] initWithPhotoLibrary:[PHPhotoLibrary sharedPhotoLibrary]];
+		config.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCurrent;
+		config.selectionLimit = selectionLimit;
+		pickingMultipleFiles = selectionLimit != 1;
+		
+		// mediaType is a bitmask:
+		// 1: image
+		// 2: video
+		// 4: audio (not supported)
+		if( mediaType == 1 )
+			config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], nil]];
+		else if( mediaType == 2 )
+			config.filter = [PHPickerFilter videosFilter];
+		else
+			config.filter = [PHPickerFilter anyFilterMatchingSubfilters:[NSArray arrayWithObjects:[PHPickerFilter imagesFilter], [PHPickerFilter livePhotosFilter], [PHPickerFilter videosFilter], nil]];
+		
+		imagePickerNew = [[PHPickerViewController alloc] initWithConfiguration:config];
+		imagePickerNew.delegate = (id) self;
+		[UnityGetGLViewController() presentViewController:imagePickerNew animated:YES completion:^{ imagePickerState = 0; }];
+	}
+	else
+	{
+		// UIImagePickerController is used on previous versions
+		imagePicker = [[UIImagePickerController alloc] init];
+		imagePicker.delegate = (id) self;
+		imagePicker.allowsEditing = NO;
+		imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
+		
+		// mediaType is a bitmask:
+		// 1: image
+		// 2: video
+		// 4: audio (not supported)
+		if( mediaType == 1 )
+			imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, nil];
+		else if( mediaType == 2 )
+			imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
+		else
+			imagePicker.mediaTypes = [NSArray arrayWithObjects:(NSString *)kUTTypeImage, (NSString *)kUTTypeLivePhoto, (NSString *)kUTTypeMovie, (NSString *)kUTTypeVideo, nil];
+		
+		if( mediaType != 1 )
+		{
+			// Don't compress picked videos if possible
+			imagePicker.videoExportPreset = AVAssetExportPresetPassthrough;
+		}
+		
+		UIViewController *rootViewController = UnityGetGLViewController();
+		if( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ) // iPad
+		{
+			imagePicker.modalPresentationStyle = UIModalPresentationPopover;
+			UIPopoverPresentationController *popover = imagePicker.popoverPresentationController;
+			if( popover != nil )
+			{
+				popover.sourceView = rootViewController.view;
+				popover.sourceRect = CGRectMake( rootViewController.view.frame.size.width / 2, rootViewController.view.frame.size.height / 2, 1, 1 );
+				popover.permittedArrowDirections = 0;
+			}
+		}
+
+		[rootViewController presentViewController:imagePicker animated:YES completion:^{ imagePickerState = 0; }];
+	}
+}
+
++ (int)isMediaPickerBusy
+{
+	if( imagePickerState == 2 )
+		return 1;
+	
+	if( imagePicker != nil )
+	{
+		if( imagePickerState == 1 || [imagePicker presentingViewController] == UnityGetGLViewController() )
+			return 1;
+		else
+		{
+			imagePicker = nil;
+			return 0;
+		}
+	}
+	else if( @available(iOS 14.0, *) )
+	{
+		if( imagePickerNew == nil )
+			return 0;
+		else if( imagePickerState == 1 || [imagePickerNew presentingViewController] == UnityGetGLViewController() )
+			return 1;
+		else
+		{
+			imagePickerNew = nil;
+			return 0;
+		}
+	}
+	else
+		return 0;
+}
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
++ (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
+{
+	NSString *resultPath = nil;
+	
+	if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeImage] )
+	{
+		NSLog( @"Picked an image" );
+		
+		// Try to obtain the raw data of the image (which allows picking gifs properly or preserving metadata)
+		PHAsset *asset = nil;
+		
+		// Try fetching the source image via UIImagePickerControllerImageURL
+		NSURL *mediaUrl = info[UIImagePickerControllerImageURL];
+		if( mediaUrl != nil )
+		{
+			NSString *imagePath = [mediaUrl path];
+			if( imagePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:imagePath] )
+			{
+				NSError *error;
+				NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[imagePath pathExtension]];
+				
+				if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+				{
+					if( [[NSFileManager defaultManager] copyItemAtPath:imagePath toPath:newPath error:&error] )
+					{
+						resultPath = newPath;
+						NSLog( @"Copied source image from UIImagePickerControllerImageURL" );
+					}
+					else
+						NSLog( @"Error copying image: %@", error );
+				}
+				else
+					NSLog( @"Error deleting existing image: %@", error );
+			}
+		}
+		
+		if( resultPath == nil )
+			asset = info[UIImagePickerControllerPHAsset];
+		
+		if( resultPath == nil && !simpleMediaPickMode )
+		{
+			if( asset == nil )
+			{
+				mediaUrl = info[UIImagePickerControllerReferenceURL] ?: info[UIImagePickerControllerMediaURL];
+				if( mediaUrl != nil )
+					asset = [[PHAsset fetchAssetsWithALAssetURLs:[NSArray arrayWithObject:mediaUrl] options:nil] firstObject];
+			}
+			
+			resultPath = [self trySavePHAsset:asset atIndex:1];
+		}
+		
+		if( resultPath == nil )
+		{
+			// Save image as PNG
+			UIImage *image = info[UIImagePickerControllerOriginalImage];
+			if( image != nil )
+			{
+				resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];
+				if( ![self saveImageAsPNG:image toPath:resultPath] )
+				{
+					NSLog( @"Error creating PNG image" );
+					resultPath = nil;
+				}
+			}
+			else
+				NSLog( @"Error fetching original image from picker" );
+		}
+	}
+	else if( [info[UIImagePickerControllerMediaType] isEqualToString:(NSString *)kUTTypeLivePhoto] )
+	{
+		NSLog( @"Picked a live photo" );
+		
+		// Save live photo as PNG
+		UIImage *image = info[UIImagePickerControllerOriginalImage];
+		if( image != nil )
+		{
+			resultPath = [pickedMediaSavePath stringByAppendingPathExtension:@"png"];
+			if( ![self saveImageAsPNG:image toPath:resultPath] )
+			{
+				NSLog( @"Error creating PNG image" );
+				resultPath = nil;
+			}
+		}
+		else
+			NSLog( @"Error fetching live photo's still image from picker" );
+	}
+	else
+	{
+		NSLog( @"Picked a video" );
+		
+		NSURL *mediaUrl = info[UIImagePickerControllerMediaURL] ?: info[UIImagePickerControllerReferenceURL];
+		if( mediaUrl != nil )
+		{
+			resultPath = [mediaUrl path];
+			
+			// On iOS 13, picked file becomes unreachable as soon as the UIImagePickerController disappears,
+			// in that case, copy the video to a temporary location
+			if( @available(iOS 13.0, *) )
+			{
+				NSError *error;
+				NSString *newPath = [pickedMediaSavePath stringByAppendingPathExtension:[resultPath pathExtension]];
+				
+				if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+				{
+					if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error] )
+						resultPath = newPath;
+					else
+					{
+						NSLog( @"Error copying video: %@", error );
+						resultPath = nil;
+					}
+				}
+				else
+				{
+					NSLog( @"Error deleting existing video: %@", error );
+					resultPath = nil;
+				}
+			}
+		}
+	}
+	
+	imagePicker = nil;
+	imagePickerState = 2;
+	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPath] );
+	
+	[picker dismissViewControllerAnimated:NO completion:nil];
+}
+#pragma clang diagnostic pop
+
+// Credit: https://ikyle.me/blog/2020/phpickerviewcontroller
++(void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results API_AVAILABLE(ios(14))
+{
+	imagePickerNew = nil;
+	imagePickerState = 2;
+	
+	[picker dismissViewControllerAnimated:NO completion:nil];
+	
+	if( results != nil && [results count] > 0 )
+	{
+		NSMutableArray<NSString *> *resultPaths = [NSMutableArray arrayWithCapacity:[results count]];
+		NSLock *arrayLock = [[NSLock alloc] init];
+		dispatch_group_t group = dispatch_group_create();
+		
+		for( int i = 0; i < [results count]; i++ )
+		{
+			PHPickerResult *result = results[i];
+			NSItemProvider *itemProvider = result.itemProvider;
+			NSString *assetIdentifier = result.assetIdentifier;
+			__block NSString *resultPath = nil;
+			
+			int j = i + 1;
+			
+			//NSLog( @"result: %@", result );
+			//NSLog( @"%@", result.assetIdentifier);
+			//NSLog( @"%@", result.itemProvider);
+
+			if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage] )
+			{
+				NSLog( @"Picked an image" );
+				
+				if( !simpleMediaPickMode && assetIdentifier != nil )
+				{
+					PHAsset *asset = [[PHAsset fetchAssetsWithLocalIdentifiers:[NSArray arrayWithObject:assetIdentifier] options:nil] firstObject];
+					resultPath = [self trySavePHAsset:asset atIndex:j];
+				}
+				
+				if( resultPath != nil )
+				{
+					[arrayLock lock];
+					[resultPaths addObject:resultPath];
+					[arrayLock unlock];
+				}
+				else
+				{
+					dispatch_group_enter( group );
+					
+					[itemProvider loadFileRepresentationForTypeIdentifier:(NSString *)kUTTypeImage completionHandler:^( NSURL *url, NSError *error )
+					{
+						if( url != nil )
+						{
+							// Copy the image to a temporary location because the returned image will be deleted by the OS after this callback is completed
+							resultPath = [url path];
+							NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];
+							
+							if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+							{
+								if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])
+									resultPath = newPath;
+								else
+								{
+									NSLog( @"Error copying image: %@", error );
+									resultPath = nil;
+								}
+							}
+							else
+							{
+								NSLog( @"Error deleting existing image: %@", error );
+								resultPath = nil;
+							}
+						}
+						else
+							NSLog( @"Error getting the picked image's path: %@", error );
+						
+						if( resultPath != nil )
+						{
+							[arrayLock lock];
+							[resultPaths addObject:resultPath];
+							[arrayLock unlock];
+						}
+						else
+						{
+							if( [itemProvider canLoadObjectOfClass:[UIImage class]] )
+							{
+								dispatch_group_enter( group );
+								
+								[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
+								{
+									if( object != nil && [object isKindOfClass:[UIImage class]] )
+									{
+										resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];
+										if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )
+										{
+											NSLog( @"Error creating PNG image" );
+											resultPath = nil;
+										}
+									}
+									else
+										NSLog( @"Error generating UIImage from picked image: %@", error );
+									
+									[arrayLock lock];
+									[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+									[arrayLock unlock];
+									
+									dispatch_group_leave( group );
+								}];
+							}
+							else
+							{
+								NSLog( @"Can't generate UIImage from picked image" );
+								
+								[arrayLock lock];
+								[resultPaths addObject:@""];
+								[arrayLock unlock];
+							}
+						}
+						
+						dispatch_group_leave( group );
+					}];
+				}
+			}
+			else if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeLivePhoto] )
+			{
+				NSLog( @"Picked a live photo" );
+				
+				if( [itemProvider canLoadObjectOfClass:[UIImage class]] )
+				{
+					dispatch_group_enter( group );
+					
+					[itemProvider loadObjectOfClass:[UIImage class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
+					{
+						if( object != nil && [object isKindOfClass:[UIImage class]] )
+						{
+							resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:@"png"];
+							if( ![self saveImageAsPNG:(UIImage *)object toPath:resultPath] )
+							{
+								NSLog( @"Error creating PNG image" );
+								resultPath = nil;
+							}
+						}
+						else
+							NSLog( @"Error generating UIImage from picked live photo: %@", error );
+						
+						[arrayLock lock];
+						[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+						[arrayLock unlock];
+						
+						dispatch_group_leave( group );
+					}];
+				}
+				else if( [itemProvider canLoadObjectOfClass:[PHLivePhoto class]] )
+				{
+					dispatch_group_enter( group );
+					
+					[itemProvider loadObjectOfClass:[PHLivePhoto class] completionHandler:^( __kindof id<NSItemProviderReading> object, NSError *error )
+					{
+						if( object != nil && [object isKindOfClass:[PHLivePhoto class]] )
+						{
+							// Extract image data from live photo
+							// Credit: https://stackoverflow.com/a/41341675/2373034
+							NSArray<PHAssetResource*>* livePhotoResources = [PHAssetResource assetResourcesForLivePhoto:(PHLivePhoto *)object];
+							
+							PHAssetResource *livePhotoImage = nil;
+							for( int k = 0; k < [livePhotoResources count]; k++ )
+							{
+								if( livePhotoResources[k].type == PHAssetResourceTypePhoto )
+								{
+									livePhotoImage = livePhotoResources[k];
+									break;
+								}
+							}
+							
+							if( livePhotoImage == nil )
+							{
+								NSLog( @"Error extracting image data from live photo" );
+							
+								[arrayLock lock];
+								[resultPaths addObject:@""];
+								[arrayLock unlock];
+							}
+							else
+							{
+								dispatch_group_enter( group );
+								
+								NSString *originalFilename = livePhotoImage.originalFilename;
+								if( originalFilename == nil || [originalFilename length] == 0 )
+									resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j];
+								else
+									resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[originalFilename pathExtension]];
+								
+								[[PHAssetResourceManager defaultManager] writeDataForAssetResource:livePhotoImage toFile:[NSURL fileURLWithPath:resultPath] options:nil completionHandler:^( NSError * _Nullable error2 )
+								{
+									if( error2 != nil )
+									{
+										NSLog( @"Error saving image data from live photo: %@", error2 );
+										resultPath = nil;
+									}
+									
+									[arrayLock lock];
+									[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+									[arrayLock unlock];
+									
+									dispatch_group_leave( group );
+								}];
+							}
+						}
+						else
+						{
+							NSLog( @"Error generating PHLivePhoto from picked live photo: %@", error );
+						
+							[arrayLock lock];
+							[resultPaths addObject:@""];
+							[arrayLock unlock];
+						}
+						
+						dispatch_group_leave( group );
+					}];
+				}
+				else
+				{
+					NSLog( @"Can't convert picked live photo to still image" );
+					
+					[arrayLock lock];
+					[resultPaths addObject:@""];
+					[arrayLock unlock];
+				}
+			}
+			else if( [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] || [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVideo] )
+			{
+				NSLog( @"Picked a video" );
+				
+				// Get the video file's path
+				dispatch_group_enter( group );
+				
+				[itemProvider loadFileRepresentationForTypeIdentifier:([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie] ? (NSString *)kUTTypeMovie : (NSString *)kUTTypeVideo) completionHandler:^( NSURL *url, NSError *error )
+				{
+					if( url != nil )
+					{
+						// Copy the video to a temporary location because the returned video will be deleted by the OS after this callback is completed
+						resultPath = [url path];
+						NSString *newPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, j] stringByAppendingPathExtension:[resultPath pathExtension]];
+						
+						if( ![[NSFileManager defaultManager] fileExistsAtPath:newPath] || [[NSFileManager defaultManager] removeItemAtPath:newPath error:&error] )
+						{
+							if( [[NSFileManager defaultManager] copyItemAtPath:resultPath toPath:newPath error:&error])
+								resultPath = newPath;
+							else
+							{
+								NSLog( @"Error copying video: %@", error );
+								resultPath = nil;
+							}
+						}
+						else
+						{
+							NSLog( @"Error deleting existing video: %@", error );
+							resultPath = nil;
+						}
+					}
+					else
+						NSLog( @"Error getting the picked video's path: %@", error );
+					
+					[arrayLock lock];
+					[resultPaths addObject:( resultPath != nil ? resultPath : @"" )];
+					[arrayLock unlock];
+					
+					dispatch_group_leave( group );
+				}];
+			}
+			else
+			{
+				// Unknown media type picked?
+				NSLog( @"Couldn't determine type of picked media: %@", itemProvider );
+				
+				[arrayLock lock];
+				[resultPaths addObject:@""];
+				[arrayLock unlock];
+			}
+		}
+		
+		dispatch_group_notify( group, dispatch_get_main_queue(),
+		^{
+			if( !pickingMultipleFiles )
+				UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", [self getCString:resultPaths[0]] );
+			else
+				UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", [self getCString:[resultPaths componentsJoinedByString:@">"]] );
+		});
+	}
+	else
+	{
+		NSLog( @"No media picked" );
+		
+		if( !pickingMultipleFiles )
+			UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
+		else
+			UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMultipleMediaReceived", "" );
+	}
+}
+
++ (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
+{
+	NSLog( @"UIImagePickerController cancelled" );
+
+	imagePicker = nil;
+	UnitySendMessage( "NGMediaReceiveCallbackiOS", "OnMediaReceived", "" );
+	
+	[picker dismissViewControllerAnimated:NO completion:nil];
+}
+
++ (NSString *)trySavePHAsset:(PHAsset *)asset atIndex:(int)filenameIndex
+{
+	if( asset == nil )
+		return nil;
+	
+	__block NSString *resultPath = nil;
+	
+	PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
+	options.synchronous = YES;
+	options.version = PHImageRequestOptionsVersionCurrent;
+	
+	if( @available(iOS 13.0, *) )
+	{
+		[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary *imageInfo )
+		{
+			if( imageData != nil )
+				resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];
+			else
+				NSLog( @"Couldn't fetch raw image data" );
+		}];
+	}
+	else 
+	{
+		[[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^( NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *imageInfo )
+		{
+			if( imageData != nil )
+				resultPath = [self trySaveSourceImage:imageData withInfo:imageInfo atIndex:filenameIndex];
+			else
+				NSLog( @"Couldn't fetch raw image data" );
+		}];
+	}
+	
+	return resultPath;
+}
+
++ (NSString *)trySaveSourceImage:(NSData *)imageData withInfo:(NSDictionary *)info atIndex:(int)filenameIndex
+{
+	NSString *filePath = info[@"PHImageFileURLKey"];
+	if( filePath != nil ) // filePath can actually be an NSURL, convert it to NSString
+		filePath = [NSString stringWithFormat:@"%@", filePath];
+	
+	if( filePath == nil || [filePath length] == 0 )
+	{
+		filePath = info[@"PHImageFileUTIKey"];
+		if( filePath != nil )
+			filePath = [NSString stringWithFormat:@"%@", filePath];
+	}
+	
+	NSString *resultPath;
+	if( filePath == nil || [filePath length] == 0 )
+		resultPath = [NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex];
+	else
+		resultPath = [[NSString stringWithFormat:@"%@%d", pickedMediaSavePath, filenameIndex] stringByAppendingPathExtension:[filePath pathExtension]];
+	
+	NSError *error;
+	if( ![[NSFileManager defaultManager] fileExistsAtPath:resultPath] || [[NSFileManager defaultManager] removeItemAtPath:resultPath error:&error] )
+	{
+		if( ![imageData writeToFile:resultPath atomically:YES] )
+		{
+			NSLog( @"Error copying source image to file" );
+			resultPath = nil;
+		}
+	}
+	else
+	{
+		NSLog( @"Error deleting existing image: %@", error );
+		resultPath = nil;
+	}
+	
+	return resultPath;
+}
+
+// Credit: https://lists.apple.com/archives/cocoa-dev/2012/Jan/msg00052.html
++ (int)getMediaTypeFromExtension:(NSString *)extension
+{
+	CFStringRef fileUTI = UTTypeCreatePreferredIdentifierForTag( kUTTagClassFilenameExtension, (__bridge CFStringRef) extension, NULL );
+	
+	// mediaType is a bitmask:
+	// 1: image
+	// 2: video
+	// 4: audio (not supported)
+	int result = 0;
+	if( UTTypeConformsTo( fileUTI, kUTTypeImage ) || UTTypeConformsTo( fileUTI, kUTTypeLivePhoto ) )
+		result = 1;
+	else if( UTTypeConformsTo( fileUTI, kUTTypeMovie ) || UTTypeConformsTo( fileUTI, kUTTypeVideo ) )
+		result = 2;
+	else if( UTTypeConformsTo( fileUTI, kUTTypeAudio ) )
+		result = 4;
+	
+	CFRelease( fileUTI );
+	
+	return result;
+}
+
+// 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 NativeGallery.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];
+}
+
++ (BOOL)saveImageAsPNG:(UIImage *)image toPath:(NSString *)resultPath
+{
+	return [UIImagePNGRepresentation( [self scaleImage:image maxSize:16384] ) writeToFile:resultPath atomically:YES];
+}
+
++ (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 _NativeGallery_CheckPermission( int readPermission, int permissionFreeMode )
+{
+	return [UNativeGallery checkPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" void _NativeGallery_RequestPermission( int readPermission, int permissionFreeMode )
+{
+	[UNativeGallery requestPermission:( readPermission == 1 ) permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" void _NativeGallery_ShowLimitedLibraryPicker()
+{
+	return [UNativeGallery showLimitedLibraryPicker];
+}
+
+extern "C" void _NativeGallery_OpenSettings()
+{
+	[UNativeGallery openSettings];
+}
+
+extern "C" int _NativeGallery_CanPickMultipleMedia()
+{
+	return [UNativeGallery canPickMultipleMedia];
+}
+
+extern "C" void _NativeGallery_ImageWriteToAlbum( const char* path, const char* album, int permissionFreeMode )
+{
+	[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImage:YES permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" void _NativeGallery_VideoWriteToAlbum( const char* path, const char* album, int permissionFreeMode )
+{
+	[UNativeGallery saveMedia:[NSString stringWithUTF8String:path] albumName:[NSString stringWithUTF8String:album] isImage:NO permissionFreeMode:( permissionFreeMode == 1 )];
+}
+
+extern "C" void _NativeGallery_PickMedia( const char* mediaSavePath, int mediaType, int permissionFreeMode, int selectionLimit )
+{
+	[UNativeGallery pickMedia:mediaType savePath:[NSString stringWithUTF8String:mediaSavePath] permissionFreeMode:( permissionFreeMode == 1 ) selectionLimit:selectionLimit];
+}
+
+extern "C" int _NativeGallery_IsMediaPickerBusy()
+{
+	return [UNativeGallery isMediaPickerBusy];
+}
+
+extern "C" int _NativeGallery_GetMediaTypeFromExtension( const char* extension )
+{
+	return [UNativeGallery getMediaTypeFromExtension:[NSString stringWithUTF8String:extension]];
+}
+
+extern "C" char* _NativeGallery_GetImageProperties( const char* path )
+{
+	return [UNativeGallery getImageProperties:[NSString stringWithUTF8String:path]];
+}
+
+extern "C" char* _NativeGallery_GetVideoProperties( const char* path )
+{
+	return [UNativeGallery getVideoProperties:[NSString stringWithUTF8String:path]];
+}
+
+extern "C" char* _NativeGallery_GetVideoThumbnail( const char* path, const char* thumbnailSavePath, int maxSize, double captureTimeInSeconds )
+{
+	return [UNativeGallery getVideoThumbnail:[NSString stringWithUTF8String:path] savePath:[NSString stringWithUTF8String:thumbnailSavePath] maximumSize:maxSize captureTime:captureTimeInSeconds];
+}
+
+extern "C" char* _NativeGallery_LoadImageAtPath( const char* path, const char* temporaryFilePath, int maxSize )
+{
+	return [UNativeGallery loadImageAtPath:[NSString stringWithUTF8String:path] tempFilePath:[NSString stringWithUTF8String:temporaryFilePath] maximumSize:maxSize];
+}

+ 33 - 0
Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta

@@ -0,0 +1,33 @@
+fileFormatVersion: 2
+guid: 953e0b740eb03144883db35f72cad8a6
+timeCreated: 1498722774
+licenseType: Pro
+PluginImporter:
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  isOverridable: 0
+  platformData:
+    data:
+      first:
+        Any: 
+      second:
+        enabled: 0
+        settings: {}
+    data:
+      first:
+        Editor: Editor
+      second:
+        enabled: 0
+        settings:
+          DefaultValueInitialized: true
+    data:
+      first:
+        iPhone: iOS
+      second:
+        enabled: 1
+        settings: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 368 - 0
Assets/SmartBow/Resources/Common/Avatar.prefab

@@ -0,0 +1,368 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &1878734309326330971
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2192531741856180799}
+  - component: {fileID: 4243764024293818743}
+  - component: {fileID: 4301402470131418379}
+  - component: {fileID: 2693515651924364677}
+  m_Layer: 0
+  m_Name: Avatar
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &2192531741856180799
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1878734309326330971}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8655514790091739685}
+  - {fileID: 2187136817437112070}
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 1}
+  m_AnchorMax: {x: 0, y: 1}
+  m_AnchoredPosition: {x: 255, y: -75}
+  m_SizeDelta: {x: 150, y: 150}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &4243764024293818743
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1878734309326330971}
+  m_CullTransparentMesh: 1
+--- !u!114 &4301402470131418379
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1878734309326330971}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: a7fbd6a98369f174c96aa171bcc7a84d, type: 3}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
+--- !u!114 &2693515651924364677
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1878734309326330971}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Navigation:
+    m_Mode: 3
+    m_WrapAround: 0
+    m_SelectOnUp: {fileID: 0}
+    m_SelectOnDown: {fileID: 0}
+    m_SelectOnLeft: {fileID: 0}
+    m_SelectOnRight: {fileID: 0}
+  m_Transition: 1
+  m_Colors:
+    m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+    m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+    m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+    m_ColorMultiplier: 1
+    m_FadeDuration: 0.1
+  m_SpriteState:
+    m_HighlightedSprite: {fileID: 0}
+    m_PressedSprite: {fileID: 0}
+    m_SelectedSprite: {fileID: 0}
+    m_DisabledSprite: {fileID: 0}
+  m_AnimationTriggers:
+    m_NormalTrigger: Normal
+    m_HighlightedTrigger: Highlighted
+    m_PressedTrigger: Pressed
+    m_SelectedTrigger: Selected
+    m_DisabledTrigger: Disabled
+  m_Interactable: 1
+  m_TargetGraphic: {fileID: 4301402470131418379}
+  m_OnClick:
+    m_PersistentCalls:
+      m_Calls: []
+--- !u!1 &5302563637154437518
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 4323761979427739763}
+  - component: {fileID: 5986895754064286819}
+  - component: {fileID: 6412626231820479592}
+  m_Layer: 0
+  m_Name: Sprite
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &4323761979427739763
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5302563637154437518}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 8655514790091739685}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 0}
+  m_AnchorMax: {x: 1, y: 1}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 0, y: 0}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &5986895754064286819
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5302563637154437518}
+  m_CullTransparentMesh: 0
+--- !u!114 &6412626231820479592
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 5302563637154437518}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 0
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: 80f06e979bad243429dfbced2ebf26bd, type: 3}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
+--- !u!1 &6876304773288944616
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8655514790091739685}
+  - component: {fileID: 879001932077576965}
+  - component: {fileID: 1620631368731371749}
+  - component: {fileID: 3113848077874179069}
+  m_Layer: 0
+  m_Name: Mask
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &8655514790091739685
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6876304773288944616}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 4323761979427739763}
+  m_Father: {fileID: 2192531741856180799}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 143, y: 143}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &879001932077576965
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6876304773288944616}
+  m_CullTransparentMesh: 0
+--- !u!114 &1620631368731371749
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6876304773288944616}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 0
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: a7fbd6a98369f174c96aa171bcc7a84d, type: 3}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
+--- !u!114 &3113848077874179069
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 6876304773288944616}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_ShowMaskGraphic: 0
+--- !u!1 &7751405430910567335
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 2187136817437112070}
+  - component: {fileID: 1003361219171867014}
+  - component: {fileID: 3615282788069835231}
+  m_Layer: 0
+  m_Name: Check
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &2187136817437112070
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7751405430910567335}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 2192531741856180799}
+  m_RootOrder: 1
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 1, y: 0}
+  m_AnchorMax: {x: 1, y: 0}
+  m_AnchoredPosition: {x: -15, y: 15}
+  m_SizeDelta: {x: 56, y: 55}
+  m_Pivot: {x: 1, y: 0}
+--- !u!222 &1003361219171867014
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7751405430910567335}
+  m_CullTransparentMesh: 1
+--- !u!114 &3615282788069835231
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 7751405430910567335}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 0
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: 9c4fa1f51fa64414b8d192349145d7ee, type: 3}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1

+ 7 - 0
Assets/SmartBow/Resources/Common/Avatar.prefab.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: db1bb5ff6db637e468f23ad86bbcb1d0
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 200 - 367
Assets/SmartBow/Resources/SmartBow/Prefabs/Views/Home/PersonalView.prefab

@@ -353,6 +353,82 @@ MonoBehaviour:
   m_FillOrigin: 0
   m_FillOrigin: 0
   m_UseSpriteMesh: 0
   m_UseSpriteMesh: 0
   m_PixelsPerUnitMultiplier: 1
   m_PixelsPerUnitMultiplier: 1
+--- !u!1 &1042803896396751040
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 8169161649865977945}
+  - component: {fileID: 5005035615464604689}
+  - component: {fileID: 6854989956660916494}
+  m_Layer: 0
+  m_Name: Sprite
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &8169161649865977945
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1042803896396751040}
+  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children: []
+  m_Father: {fileID: 7841574166018282395}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 87, y: 83}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &5005035615464604689
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1042803896396751040}
+  m_CullTransparentMesh: 0
+--- !u!114 &6854989956660916494
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 1042803896396751040}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 0
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: a12dff2b4d0faac4fb03a516864b15d5, type: 3}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
 --- !u!1 &1100711941473562281
 --- !u!1 &1100711941473562281
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -1168,129 +1244,6 @@ MonoBehaviour:
     m_VerticalOverflow: 1
     m_VerticalOverflow: 1
     m_LineSpacing: 1
     m_LineSpacing: 1
   m_Text: Game Record
   m_Text: Game Record
---- !u!1 &2250284125010357144
-GameObject:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  serializedVersion: 6
-  m_Component:
-  - component: {fileID: 1965108324120026620}
-  - component: {fileID: 4597303848238905012}
-  - component: {fileID: 4510813913874870984}
-  - component: {fileID: 2326476090784447046}
-  m_Layer: 0
-  m_Name: Avatar
-  m_TagString: Untagged
-  m_Icon: {fileID: 0}
-  m_NavMeshLayer: 0
-  m_StaticEditorFlags: 0
-  m_IsActive: 1
---- !u!224 &1965108324120026620
-RectTransform:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 2250284125010357144}
-  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
-  m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 1, y: 1, z: 1}
-  m_ConstrainProportionsScale: 0
-  m_Children:
-  - {fileID: 9022565260447688166}
-  - {fileID: 1977716594654642373}
-  m_Father: {fileID: 5782561019388482318}
-  m_RootOrder: 0
-  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
-  m_AnchorMin: {x: 0, y: 1}
-  m_AnchorMax: {x: 0, y: 1}
-  m_AnchoredPosition: {x: 75, y: -75}
-  m_SizeDelta: {x: 150, y: 150}
-  m_Pivot: {x: 0.5, y: 0.5}
---- !u!222 &4597303848238905012
-CanvasRenderer:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 2250284125010357144}
-  m_CullTransparentMesh: 1
---- !u!114 &4510813913874870984
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 2250284125010357144}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  m_Material: {fileID: 0}
-  m_Color: {r: 1, g: 1, b: 1, a: 1}
-  m_RaycastTarget: 1
-  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
-  m_Maskable: 1
-  m_OnCullStateChanged:
-    m_PersistentCalls:
-      m_Calls: []
-  m_Sprite: {fileID: 21300000, guid: a7fbd6a98369f174c96aa171bcc7a84d, type: 3}
-  m_Type: 0
-  m_PreserveAspect: 0
-  m_FillCenter: 1
-  m_FillMethod: 4
-  m_FillAmount: 1
-  m_FillClockwise: 1
-  m_FillOrigin: 0
-  m_UseSpriteMesh: 0
-  m_PixelsPerUnitMultiplier: 1
---- !u!114 &2326476090784447046
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 2250284125010357144}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  m_Navigation:
-    m_Mode: 3
-    m_WrapAround: 0
-    m_SelectOnUp: {fileID: 0}
-    m_SelectOnDown: {fileID: 0}
-    m_SelectOnLeft: {fileID: 0}
-    m_SelectOnRight: {fileID: 0}
-  m_Transition: 1
-  m_Colors:
-    m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
-    m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
-    m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
-    m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
-    m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
-    m_ColorMultiplier: 1
-    m_FadeDuration: 0.1
-  m_SpriteState:
-    m_HighlightedSprite: {fileID: 0}
-    m_PressedSprite: {fileID: 0}
-    m_SelectedSprite: {fileID: 0}
-    m_DisabledSprite: {fileID: 0}
-  m_AnimationTriggers:
-    m_NormalTrigger: Normal
-    m_HighlightedTrigger: Highlighted
-    m_PressedTrigger: Pressed
-    m_SelectedTrigger: Selected
-    m_DisabledTrigger: Disabled
-  m_Interactable: 1
-  m_TargetGraphic: {fileID: 4510813913874870984}
-  m_OnClick:
-    m_PersistentCalls:
-      m_Calls: []
 --- !u!1 &2615675145272488185
 --- !u!1 &2615675145272488185
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -1321,7 +1274,7 @@ RectTransform:
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_ConstrainProportionsScale: 0
   m_ConstrainProportionsScale: 0
   m_Children:
   m_Children:
-  - {fileID: 1965108324120026620}
+  - {fileID: 7841574166018282395}
   m_Father: {fileID: 777921083551667692}
   m_Father: {fileID: 777921083551667692}
   m_RootOrder: 0
   m_RootOrder: 0
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -2073,6 +2026,128 @@ MonoBehaviour:
   textKey: Personal_DeleteAccount
   textKey: Personal_DeleteAccount
   layoutRebuildObject: {fileID: 0}
   layoutRebuildObject: {fileID: 0}
   languageFontSizes: []
   languageFontSizes: []
+--- !u!1 &3622492624823877955
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 7841574166018282395}
+  - component: {fileID: 5355368796942341147}
+  - component: {fileID: 300675144348682214}
+  - component: {fileID: 4144976682513785907}
+  m_Layer: 0
+  m_Name: AddBtn
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &7841574166018282395
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3622492624823877955}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_ConstrainProportionsScale: 0
+  m_Children:
+  - {fileID: 8169161649865977945}
+  m_Father: {fileID: 5782561019388482318}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 1}
+  m_AnchorMax: {x: 0, y: 1}
+  m_AnchoredPosition: {x: 75, y: -75}
+  m_SizeDelta: {x: 150, y: 150}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &5355368796942341147
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3622492624823877955}
+  m_CullTransparentMesh: 1
+--- !u!114 &300675144348682214
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3622492624823877955}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: 2f17576af51d0d244bf5540a3cfe19d2, type: 3}
+  m_Type: 0
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 1
+--- !u!114 &4144976682513785907
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 3622492624823877955}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Navigation:
+    m_Mode: 3
+    m_WrapAround: 0
+    m_SelectOnUp: {fileID: 0}
+    m_SelectOnDown: {fileID: 0}
+    m_SelectOnLeft: {fileID: 0}
+    m_SelectOnRight: {fileID: 0}
+  m_Transition: 1
+  m_Colors:
+    m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+    m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+    m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+    m_ColorMultiplier: 1
+    m_FadeDuration: 0.1
+  m_SpriteState:
+    m_HighlightedSprite: {fileID: 0}
+    m_PressedSprite: {fileID: 0}
+    m_SelectedSprite: {fileID: 0}
+    m_DisabledSprite: {fileID: 0}
+  m_AnimationTriggers:
+    m_NormalTrigger: Normal
+    m_HighlightedTrigger: Highlighted
+    m_PressedTrigger: Pressed
+    m_SelectedTrigger: Selected
+    m_DisabledTrigger: Disabled
+  m_Interactable: 1
+  m_TargetGraphic: {fileID: 300675144348682214}
+  m_OnClick:
+    m_PersistentCalls:
+      m_Calls: []
 --- !u!1 &3785774461229767442
 --- !u!1 &3785774461229767442
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -2967,82 +3042,6 @@ MonoBehaviour:
   m_OnValueChanged:
   m_OnValueChanged:
     m_PersistentCalls:
     m_PersistentCalls:
       m_Calls: []
       m_Calls: []
---- !u!1 &5529991618292297293
-GameObject:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  serializedVersion: 6
-  m_Component:
-  - component: {fileID: 4118842059885653936}
-  - component: {fileID: 6214332434793014176}
-  - component: {fileID: 6761660121825123243}
-  m_Layer: 0
-  m_Name: Sprite
-  m_TagString: Untagged
-  m_Icon: {fileID: 0}
-  m_NavMeshLayer: 0
-  m_StaticEditorFlags: 0
-  m_IsActive: 1
---- !u!224 &4118842059885653936
-RectTransform:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 5529991618292297293}
-  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
-  m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 1, y: 1, z: 1}
-  m_ConstrainProportionsScale: 0
-  m_Children: []
-  m_Father: {fileID: 9022565260447688166}
-  m_RootOrder: 0
-  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
-  m_AnchorMin: {x: 0, y: 0}
-  m_AnchorMax: {x: 1, y: 1}
-  m_AnchoredPosition: {x: 0, y: 0}
-  m_SizeDelta: {x: 0, y: 0}
-  m_Pivot: {x: 0.5, y: 0.5}
---- !u!222 &6214332434793014176
-CanvasRenderer:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 5529991618292297293}
-  m_CullTransparentMesh: 0
---- !u!114 &6761660121825123243
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 5529991618292297293}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  m_Material: {fileID: 0}
-  m_Color: {r: 1, g: 1, b: 1, a: 1}
-  m_RaycastTarget: 0
-  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
-  m_Maskable: 1
-  m_OnCullStateChanged:
-    m_PersistentCalls:
-      m_Calls: []
-  m_Sprite: {fileID: 21300000, guid: 80f06e979bad243429dfbced2ebf26bd, type: 3}
-  m_Type: 0
-  m_PreserveAspect: 0
-  m_FillCenter: 1
-  m_FillMethod: 4
-  m_FillAmount: 1
-  m_FillClockwise: 1
-  m_FillOrigin: 0
-  m_UseSpriteMesh: 0
-  m_PixelsPerUnitMultiplier: 1
 --- !u!1 &5534863600885995390
 --- !u!1 &5534863600885995390
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -4087,97 +4086,6 @@ MonoBehaviour:
   m_FillOrigin: 0
   m_FillOrigin: 0
   m_UseSpriteMesh: 0
   m_UseSpriteMesh: 0
   m_PixelsPerUnitMultiplier: 1
   m_PixelsPerUnitMultiplier: 1
---- !u!1 &6504760650591933483
-GameObject:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  serializedVersion: 6
-  m_Component:
-  - component: {fileID: 9022565260447688166}
-  - component: {fileID: 656065406402872518}
-  - component: {fileID: 1393200254280261414}
-  - component: {fileID: 3323264890326768702}
-  m_Layer: 0
-  m_Name: Mask
-  m_TagString: Untagged
-  m_Icon: {fileID: 0}
-  m_NavMeshLayer: 0
-  m_StaticEditorFlags: 0
-  m_IsActive: 1
---- !u!224 &9022565260447688166
-RectTransform:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 6504760650591933483}
-  m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
-  m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 1, y: 1, z: 1}
-  m_ConstrainProportionsScale: 0
-  m_Children:
-  - {fileID: 4118842059885653936}
-  m_Father: {fileID: 1965108324120026620}
-  m_RootOrder: 0
-  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
-  m_AnchorMin: {x: 0.5, y: 0.5}
-  m_AnchorMax: {x: 0.5, y: 0.5}
-  m_AnchoredPosition: {x: 0, y: 0}
-  m_SizeDelta: {x: 143, y: 143}
-  m_Pivot: {x: 0.5, y: 0.5}
---- !u!222 &656065406402872518
-CanvasRenderer:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 6504760650591933483}
-  m_CullTransparentMesh: 0
---- !u!114 &1393200254280261414
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 6504760650591933483}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  m_Material: {fileID: 0}
-  m_Color: {r: 1, g: 1, b: 1, a: 1}
-  m_RaycastTarget: 0
-  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
-  m_Maskable: 1
-  m_OnCullStateChanged:
-    m_PersistentCalls:
-      m_Calls: []
-  m_Sprite: {fileID: 21300000, guid: a7fbd6a98369f174c96aa171bcc7a84d, type: 3}
-  m_Type: 0
-  m_PreserveAspect: 0
-  m_FillCenter: 1
-  m_FillMethod: 4
-  m_FillAmount: 1
-  m_FillClockwise: 1
-  m_FillOrigin: 0
-  m_UseSpriteMesh: 0
-  m_PixelsPerUnitMultiplier: 1
---- !u!114 &3323264890326768702
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 6504760650591933483}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  m_ShowMaskGraphic: 0
 --- !u!1 &6936138757148595727
 --- !u!1 &6936138757148595727
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -10296,6 +10204,7 @@ MonoBehaviour:
   - {fileID: 654430252714443831}
   - {fileID: 654430252714443831}
   datePickerPrefab: {fileID: 8259705882647745319, guid: ca1bb4036d12d3841ad1f29a75a8f2ce, type: 3}
   datePickerPrefab: {fileID: 8259705882647745319, guid: ca1bb4036d12d3841ad1f29a75a8f2ce, type: 3}
   avatarSelectView: {fileID: 3568271985103462570}
   avatarSelectView: {fileID: 3568271985103462570}
+  avatarPrefab: {fileID: 1878734309326330971, guid: db1bb5ff6db637e468f23ad86bbcb1d0, type: 3}
 --- !u!1 &7327544349476603443
 --- !u!1 &7327544349476603443
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0
@@ -14016,82 +13925,6 @@ MonoBehaviour:
   m_FillOrigin: 0
   m_FillOrigin: 0
   m_UseSpriteMesh: 0
   m_UseSpriteMesh: 0
   m_PixelsPerUnitMultiplier: 1
   m_PixelsPerUnitMultiplier: 1
---- !u!1 &7978832286902137956
-GameObject:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  serializedVersion: 6
-  m_Component:
-  - component: {fileID: 1977716594654642373}
-  - component: {fileID: 631805963919765061}
-  - component: {fileID: 3964318937217365532}
-  m_Layer: 0
-  m_Name: Check
-  m_TagString: Untagged
-  m_Icon: {fileID: 0}
-  m_NavMeshLayer: 0
-  m_StaticEditorFlags: 0
-  m_IsActive: 1
---- !u!224 &1977716594654642373
-RectTransform:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 7978832286902137956}
-  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
-  m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 1, y: 1, z: 1}
-  m_ConstrainProportionsScale: 0
-  m_Children: []
-  m_Father: {fileID: 1965108324120026620}
-  m_RootOrder: 1
-  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
-  m_AnchorMin: {x: 1, y: 0}
-  m_AnchorMax: {x: 1, y: 0}
-  m_AnchoredPosition: {x: -15, y: 15}
-  m_SizeDelta: {x: 56, y: 55}
-  m_Pivot: {x: 1, y: 0}
---- !u!222 &631805963919765061
-CanvasRenderer:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 7978832286902137956}
-  m_CullTransparentMesh: 1
---- !u!114 &3964318937217365532
-MonoBehaviour:
-  m_ObjectHideFlags: 0
-  m_CorrespondingSourceObject: {fileID: 0}
-  m_PrefabInstance: {fileID: 0}
-  m_PrefabAsset: {fileID: 0}
-  m_GameObject: {fileID: 7978832286902137956}
-  m_Enabled: 1
-  m_EditorHideFlags: 0
-  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
-  m_Name: 
-  m_EditorClassIdentifier: 
-  m_Material: {fileID: 0}
-  m_Color: {r: 1, g: 1, b: 1, a: 1}
-  m_RaycastTarget: 0
-  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
-  m_Maskable: 1
-  m_OnCullStateChanged:
-    m_PersistentCalls:
-      m_Calls: []
-  m_Sprite: {fileID: 21300000, guid: 9c4fa1f51fa64414b8d192349145d7ee, type: 3}
-  m_Type: 0
-  m_PreserveAspect: 0
-  m_FillCenter: 1
-  m_FillMethod: 4
-  m_FillAmount: 1
-  m_FillClockwise: 1
-  m_FillOrigin: 0
-  m_UseSpriteMesh: 0
-  m_PixelsPerUnitMultiplier: 1
 --- !u!1 &8026924697939995740
 --- !u!1 &8026924697939995740
 GameObject:
 GameObject:
   m_ObjectHideFlags: 0
   m_ObjectHideFlags: 0

+ 142 - 16
Assets/SmartBow/Scripts/Views/PersonalViewParts/BoxUserSettings.cs

@@ -5,6 +5,9 @@ using UnityEngine.UI;
 using JCUnityLib;
 using JCUnityLib;
 using UnityEngine.EventSystems;
 using UnityEngine.EventSystems;
 using UnityEngine.Events;
 using UnityEngine.Events;
+using static NativeGallery;
+using System;
+using Newtonsoft.Json.Linq;
 
 
 public class BoxUserSettings : MonoBehaviour
 public class BoxUserSettings : MonoBehaviour
 {
 {
@@ -211,6 +214,9 @@ public class BoxUserSettings : MonoBehaviour
 
 
     #region 头像选择
     #region 头像选择
     [SerializeField] Transform avatarSelectView;
     [SerializeField] Transform avatarSelectView;
+    [SerializeField] GameObject avatarPrefab;
+    GridLayoutGroup gridLayoutGroup;
+    GameObject customAvatarObj;
     bool avatarSelectViewInited = false;
     bool avatarSelectViewInited = false;
     int curAvatarSelectID = 0;
     int curAvatarSelectID = 0;
     public void ShowAvatarSelectView()
     public void ShowAvatarSelectView()
@@ -224,36 +230,69 @@ public class BoxUserSettings : MonoBehaviour
         else
         else
         {
         {
             avatarSelectViewInited = true;
             avatarSelectViewInited = true;
-            curAvatarSelectID = LoginMgr.myUserInfo.avatarID;
+            curAvatarSelectID = LoginMgr.myUserInfo.avatarID;//服务器带的信息是 -1是微信头像,-2是自定义。
             avatarSelectView.Find("FrameBox/BtnClose").GetComponent<Button>().onClick.AddListener(CloseAvatarSelectView);
             avatarSelectView.Find("FrameBox/BtnClose").GetComponent<Button>().onClick.AddListener(CloseAvatarSelectView);
-            GridLayoutGroup gridLayoutGroup = avatarSelectView.GetComponentInChildren<GridLayoutGroup>();
-            GameObject avatarPrefab = gridLayoutGroup.transform.Find("Avatar").gameObject;
+            if(!gridLayoutGroup) 
+                gridLayoutGroup = avatarSelectView.GetComponentInChildren<GridLayoutGroup>();
+            //打开相册
+            gridLayoutGroup.transform.Find("AddBtn").GetComponent<Button>().onClick.AddListener(OpenGallery);
+
+            //GameObject avatarPrefab = gridLayoutGroup.transform.Find("Avatar").gameObject;
+            // -1 是自定义头像
             for (int id = -1; id < RoleMgr.GetAvatarListLen(); id++)
             for (int id = -1; id < RoleMgr.GetAvatarListLen(); id++)
             {
             {
-                if (RoleMgr.IsRoleAvatar(id)) continue;
-                if (id == -1 && string.IsNullOrWhiteSpace(LoginMgr.myUserInfo.avatarUrl)) continue;
+                // 对战的头像?
+                if (RoleMgr.IsRoleAvatar(id)) continue; 
+                // 如果是第三方并且是空的(这里的id = -1 是指不是固定头像)?
+                if (id == -1 && string.IsNullOrWhiteSpace(LoginMgr.myUserInfo.avatarUrl)) continue; 
                 GameObject avatar = Instantiate(avatarPrefab, gridLayoutGroup.transform);
                 GameObject avatar = Instantiate(avatarPrefab, gridLayoutGroup.transform);
                 Image avatarImage = avatar.transform.Find("Mask/Sprite").GetComponent<Image>();
                 Image avatarImage = avatar.transform.Find("Mask/Sprite").GetComponent<Image>();
-                RoleMgr.SetAvatarToImage(avatarImage, id, LoginMgr.myUserInfo.avatarUrl);
-                avatar.transform.Find("Check").gameObject.SetActive(id == curAvatarSelectID);
-                avatar.gameObject.name = id.ToString();
+              
+                //自定义用当前id命名
                 int aid = id; //记录该值
                 int aid = id; //记录该值
+                avatar.name = id.ToString();
+                // 看看是不是自定义的url
+                if (id == -1)
+                {
+                    aid  = IsCosResource(LoginMgr.myUserInfo.avatarUrl) ? -2 : -1; 
+                    avatar.name = aid.ToString();
+                    customAvatarObj = avatar;
+                }
+
+                RoleMgr.SetAvatarToImage(avatarImage, aid, LoginMgr.myUserInfo.avatarUrl);
+                avatar.transform.Find("Check").gameObject.SetActive(aid == curAvatarSelectID);
+
                 avatar.GetComponent<Button>().onClick.AddListener(() => {
                 avatar.GetComponent<Button>().onClick.AddListener(() => {
                     AudioMgr.ins.PlayBtn();
                     AudioMgr.ins.PlayBtn();
-                    curAvatarSelectID = aid;
-                    for (int i = 0; i < gridLayoutGroup.transform.childCount; i++)
-                    {
-                        Transform item = gridLayoutGroup.transform.GetChild(i);
-                        int theAvatarID = int.Parse(item.gameObject.name);
-                        item.Find("Check").gameObject.SetActive(theAvatarID == curAvatarSelectID);
-                    }
+                    updateCheck(aid);
+                    //curAvatarSelectID = aid;
+                    //for (int i = 0; i < gridLayoutGroup.transform.childCount; i++)
+                    //{
+                    //    Transform item = gridLayoutGroup.transform.GetChild(i);
+                    //    if (item.gameObject.name == "AddBtn") continue;
+                    //    int theAvatarID = int.Parse(item.gameObject.name);
+                    //    item.Find("Check").gameObject.SetActive(theAvatarID == curAvatarSelectID);
+                    //}
                 });
                 });
             }
             }
-            Destroy(avatarPrefab);
+           // Destroy(avatarPrefab);
             LayoutRebuilder.ForceRebuildLayoutImmediate(gridLayoutGroup.transform.parent.GetComponent<RectTransform>());
             LayoutRebuilder.ForceRebuildLayoutImmediate(gridLayoutGroup.transform.parent.GetComponent<RectTransform>());
         }
         }
     }
     }
+    // 腾讯云 COS 域名后缀
+    private const string COS_DOMAIN_SUFFIX = ".myqcloud.com";
 
 
+    /// <summary>
+    /// 判断是否是 COS 资源
+    /// </summary>
+    public static bool IsCosResource(string url)
+    {
+        if (string.IsNullOrEmpty(url))
+            return false;
+
+        // 简单字符串匹配 host 是否包含 cos 域名
+        return url.IndexOf(COS_DOMAIN_SUFFIX, StringComparison.OrdinalIgnoreCase) >= 0;
+    }
     void CloseAvatarSelectView()
     void CloseAvatarSelectView()
     {
     {
         AudioMgr.ins.PlayBtn();
         AudioMgr.ins.PlayBtn();
@@ -267,6 +306,93 @@ public class BoxUserSettings : MonoBehaviour
             LoginMgr.myUserInfo.Save();
             LoginMgr.myUserInfo.Save();
         }
         }
     }
     }
+    /// <summary>
+    /// 刷新列表选择的check,和当前的选择id
+    /// </summary>
+    /// <param name="aid"></param>
+    void updateCheck(int aid) {
+        curAvatarSelectID = aid;
+        if (!gridLayoutGroup) 
+            gridLayoutGroup = avatarSelectView.GetComponentInChildren<GridLayoutGroup>();
+
+        for (int i = 0; i < gridLayoutGroup.transform.childCount; i++)
+        {
+            Transform item = gridLayoutGroup.transform.GetChild(i);
+            if (item.gameObject.name == "AddBtn") continue;
+            int theAvatarID = int.Parse(item.gameObject.name);
+            item.Find("Check").gameObject.SetActive(theAvatarID == curAvatarSelectID);
+        }
+    }
+    /// <summary>
+    /// 添加新的自定义头像
+    /// </summary>
+    public void OpenGallery()
+    {
+        // 打开相册选择图片
+        NativeGallery.GetImageFromGallery((path) =>
+        {
+            HandleImagePicked(path);
+        }, "Select Avatar", "image/*");
+    }
+
+    //如果要用摄像机,需要另外导入 NativeCamera 插件
+    //public void OpenCamera()
+    //{
+    //    // 打开相机拍照
+    //    //NativeCamera.TakePicture((path) =>
+    //    //{
+    //    //    HandleImagePicked(path);
+    //    //}, maxSize: 256); // maxSize 可限制分辨率,避免太大
+    //}
+
+    private void HandleImagePicked(string path)
+    {
+        if (string.IsNullOrEmpty(path))
+            return;
+
+        Texture2D texture = NativeGallery.LoadImageAtPath(path, 100, false);
+        if (texture == null)
+        {
+            Debug.LogError("加载图片失败: " + path);
+            PopupMgr.ins.ShowTip(TextAutoLanguage2.GetTextByKey("avatar_path_error"));
+            return;
+        }
+
+        // 上传
+        CoroutineStarter.Start(LoginController.Instance.UploadAvatar(texture, (res) =>
+        {
+            if (res.code == 0)
+            {
+                JObject obj = res.data as JObject;
+                int avatarID = obj.Value<int>("avatarID");
+                string avatarUrl = obj.Value<string>("avatarUrl");
+
+                LoginMgr.myUserInfo.avatarID = avatarID;
+                LoginMgr.myUserInfo.avatarUrl = avatarUrl;
+
+                // 更新头像列表
+                if (!customAvatarObj)
+                {
+                    customAvatarObj = Instantiate(avatarPrefab, gridLayoutGroup.transform);
+                    customAvatarObj.name = avatarID.ToString();
+                    customAvatarObj.transform.SetSiblingIndex(1);
+                }
+
+                Image avatarImage = customAvatarObj.transform.Find("Mask/Sprite").GetComponent<Image>();
+                RoleMgr.SetAvatarToImage(avatarImage, avatarID, avatarUrl);
+
+                updateCheck(avatarID);
+                RenderAvatar();
+                //PopupMgr.ins.ShowTip(TextAutoLanguage2.GetTextByKey("common_update_successful"));
+            }
+            else
+            {
+                Debug.LogError("上传头像失败:" + res.msg);
+                PopupMgr.ins.ShowTip(TextAutoLanguage2.GetTextByKey("common_update_failed"));
+            }
+        }));
+    }
+
     #endregion
     #endregion
 
 
 
 

+ 84 - 37
Assets/SmartBow/SmartBowSDK/BleWinHelper.cs

@@ -4,8 +4,9 @@ using System.Collections.Generic;
 using System.Text;
 using System.Text;
 using UnityEngine;
 using UnityEngine;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
+using SmartBowSDK;
 
 
-namespace SmartBowSDK
+namespace SmartBowSDK_BleWinHelper
 {
 {
     /// <summary>
     /// <summary>
     /// Windows连接BluetoothLE
     /// Windows连接BluetoothLE
@@ -28,9 +29,12 @@ namespace SmartBowSDK
             if (bDebug)Debug.Log(LogTag + text);
             if (bDebug)Debug.Log(LogTag + text);
         }
         }
 
 
+        private string targetDeviceNameAxis = "Bbow_20210501 | ARTEMIS | HOUYI";
         private string targetDeviceName = "Bbow_20210501 | ARTEMIS Pro";
         private string targetDeviceName = "Bbow_20210501 | ARTEMIS Pro";
         private string targetDeviceNameHOUYIPro = "HOUYI Pro | Bbow_20210501";
         private string targetDeviceNameHOUYIPro = "HOUYI Pro | Bbow_20210501";
         private string targetDeviceNameGun = "Pistol | Pistol M9 | Bbow_20210501";
         private string targetDeviceNameGun = "Pistol | Pistol M9 | Bbow_20210501";
+        private string targetDeviceNameGun_M17 = "Pistol M17";
+        private string targetDeviceNameGun_M416 = "Rifle M416";
         private string targetService = "{0000fff0-0000-1000-8000-00805f9b34fb}";
         private string targetService = "{0000fff0-0000-1000-8000-00805f9b34fb}";
         private string targetCharacteristicsNotify = "{0000fff1-0000-1000-8000-00805f9b34fb}";
         private string targetCharacteristicsNotify = "{0000fff1-0000-1000-8000-00805f9b34fb}";
         private string targetCharacteristicsWrite = "{0000fff2-0000-1000-8000-00805f9b34fb}";
         private string targetCharacteristicsWrite = "{0000fff2-0000-1000-8000-00805f9b34fb}";
@@ -96,7 +100,7 @@ namespace SmartBowSDK
                     bluetoothWindows.OnCharacteristicChanged?.Invoke(deviceID, bytes);
                     bluetoothWindows.OnCharacteristicChanged?.Invoke(deviceID, bytes);
                 }
                 }
             };
             };
-
+            //bleWinHelper.bDebug = true;
             return bleWinHelper;
             return bleWinHelper;
         }
         }
 
 
@@ -145,48 +149,73 @@ namespace SmartBowSDK
                         if (res.isConnectableUpdated)
                         if (res.isConnectableUpdated)
                             deviceList[res.id]["isConnectable"] = res.isConnectable.ToString();
                             deviceList[res.id]["isConnectable"] = res.isConnectable.ToString();
 
 
-                        //deviceList[res.id]["name"] == targetDeviceName
-                        //if (targetDeviceName.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                        //{
-                        //    selectedDeviceId = res.id;
-                        //    StopDeviceScan();
-                        //}
-
-                        if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.HOUYIPRO)
-                        {   //需要判断是否是红外弓箭
-                            if (targetDeviceNameHOUYIPro.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                            {
-                                selectedDeviceId = res.id;
-                                StopDeviceScan();
-                            }
+                        // 获取当前设备信息
+                        string deviceName = deviceList[res.id]["name"];
+                        bool isConnectable = res.isConnectable || deviceList[res.id]["isConnectable"] == "True";
 
 
+                        var type = (AimDeviceType)AimHandler.ins.aimDeviceInfo.type;
+                        //Debug.Log("type:" + type);
+                        if (type == AimDeviceType.HOUYIPRO)
+                        {
+                            TrySelectDevice(targetDeviceNameHOUYIPro, deviceName, isConnectable, "HOUYIPro", res.id);
                         }
                         }
-                        else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.ARTEMISPRO)
-                        {   //需要判断是否是ARTEMISPRO弓箭
-                            if (targetDeviceName.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                            {
-                                selectedDeviceId = res.id;
-                                StopDeviceScan();
-                            }
-
+                        else if (type == AimDeviceType.ARTEMISPRO)
+                        {
+                            TrySelectDevice(targetDeviceName, deviceName, isConnectable, "ARTEMISPRO", res.id);
                         }
                         }
-                        else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.Gun|| AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.PistolM17|| AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.RifleM416) 
+                        else if (type == AimDeviceType.Gun)
                         {
                         {
-                            //需要判断是否是枪
-                            if (targetDeviceNameGun.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                            {
-                                selectedDeviceId = res.id;
-                                StopDeviceScan();
-                            }
+                            //Debug.Log(targetDeviceNameGun + "-----:" + deviceName);
+                            TrySelectDevice(targetDeviceNameGun, deviceName, isConnectable, "Pistol", res.id);
+                        }
+                        else if (type == AimDeviceType.PistolM17)
+                        {
+                            TrySelectDevice(targetDeviceNameGun_M17, deviceName, isConnectable, "PistolM17", res.id);
+                        }
+                        else if (type == AimDeviceType.RifleM416)
+                        {
+                            TrySelectDevice(targetDeviceNameGun_M416, deviceName, isConnectable, "RifleM416", res.id);
                         }
                         }
                         else
                         else
-                        {   //其余的九轴连接
-                            if (targetDeviceName.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
-                            {
-                                selectedDeviceId = res.id;
-                                StopDeviceScan();
-                            }
+                        {
+                            //其余的九轴连接
+                            TrySelectDevice(targetDeviceNameAxis, deviceName, isConnectable, deviceName, res.id);
                         }
                         }
+
+                        //if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.HOUYIPRO)
+                        //{   //需要判断是否是红外弓箭
+                        //    if (targetDeviceNameHOUYIPro.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
+                        //    {
+                        //        selectedDeviceId = res.id;
+                        //        StopDeviceScan();
+                        //    }
+                        //}
+                        //else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.ARTEMISPRO)
+                        //{   //需要判断是否是ARTEMISPRO弓箭
+                        //    if (targetDeviceName.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
+                        //    {
+                        //        selectedDeviceId = res.id;
+                        //        StopDeviceScan();
+                        //    }
+
+                        //}
+                        //else if (AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.Gun|| AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.PistolM17|| AimHandler.ins.aimDeviceInfo.type == (int)AimDeviceType.RifleM416) 
+                        //{
+                        //    //需要判断是否是枪
+                        //    if (targetDeviceNameGun.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
+                        //    {
+                        //        selectedDeviceId = res.id;
+                        //        StopDeviceScan();
+                        //    }
+                        //}
+                        //else
+                        //{   //其余的九轴连接
+                        //    if (targetDeviceName.Contains(deviceList[res.id]["name"]) && deviceList[res.id]["isConnectable"] == "True")
+                        //    {
+                        //        selectedDeviceId = res.id;
+                        //        StopDeviceScan();
+                        //    }
+                        //}
                     }
                     }
                     else if (status == BleApi.ScanStatus.FINISHED)
                     else if (status == BleApi.ScanStatus.FINISHED)
                     {
                     {
@@ -268,6 +297,24 @@ namespace SmartBowSDK
                 }
                 }
             }
             }
         }
         }
+        private bool TrySelectDevice(string filterNames, string deviceName, bool isConnectable, string typeName, string deviceId)
+        {
+            //Log($"匹配设备 [{isConnectable}] :{string.IsNullOrWhiteSpace(filterNames)}");
+            if (!isConnectable || string.IsNullOrWhiteSpace(filterNames)) return false;
+
+            string[] filterArray = filterNames.Split('|'); // 支持多个名字
+            foreach (var f in filterArray)
+            {
+                if (string.Equals(f.Trim(), deviceName.Trim(), StringComparison.OrdinalIgnoreCase))
+                {
+                    selectedDeviceId = deviceId;
+                    StopDeviceScan();
+                    Log($"匹配设备 [{typeName}] :{deviceName}");
+                    return true;
+                }
+            }
+            return false;
+        }
 
 
         void LateUpdate()
         void LateUpdate()
         {
         {

BIN
Assets/SmartBow/SmartBowSDK/SmartBowSDK.dll