diff --git a/lua_probject/base_project/Game/Controller/LoddyController.lua b/lua_probject/base_project/Game/Controller/LoddyController.lua
index 36d8ed37..6cac7487 100644
--- a/lua_probject/base_project/Game/Controller/LoddyController.lua
+++ b/lua_probject/base_project/Game/Controller/LoddyController.lua
@@ -530,3 +530,13 @@ function M:FG_Get_Diamond_Msg(callback)
callback(res)
end)
end
+
+-- 设置支付图片
+function M:FG_Set_Pay_Code(paycode,callback)
+ local _data = {}
+ _data.pay_code = paycode
+ local _client = ControllerManager.WebClient
+ _client:send(Protocol.WEB_SEY_PAY_CODE, _data, function(res)
+ callback(res)
+ end)
+end
\ No newline at end of file
diff --git a/lua_probject/base_project/Game/Controller/LoginController.lua b/lua_probject/base_project/Game/Controller/LoginController.lua
index 11a817fe..1af3753e 100644
--- a/lua_probject/base_project/Game/Controller/LoginController.lua
+++ b/lua_probject/base_project/Game/Controller/LoginController.lua
@@ -17,9 +17,9 @@ local _LocalConfigAllGame = {
-- 90,
22,
55,
- 66,90
- ,91,93
- ,92
+ 66, 90
+, 91, 93
+, 92
}
@@ -55,6 +55,7 @@ local function __Login(cmd, _data, callBack)
user.group_id = account["groupId"]
user.type = account["type"] --为1的时候是代理,为0的时候是普通玩家
user.agent = account["mng"]
+ user.pay_code = account.pay_code
user.real_info = account.real_info
user.phone = account.phone
@@ -62,6 +63,7 @@ local function __Login(cmd, _data, callBack)
user.games = FilterGame(data.games)
user.havaPsw = data.havaPassword
user.currenIp = data.ip
+
if Application.platform == RuntimePlatform.WindowsPlayer or Application.platform == RuntimePlatform.WindowsEditor then
--GameApplication.Instance.printLog = true
else
diff --git a/lua_probject/base_project/Game/Protocol.lua b/lua_probject/base_project/Game/Protocol.lua
index c2ae0f6c..0ed64391 100644
--- a/lua_probject/base_project/Game/Protocol.lua
+++ b/lua_probject/base_project/Game/Protocol.lua
@@ -57,6 +57,8 @@ Protocol = {
WEB_LOGIN_Phone = "acc/phone_login",
-- 获取自己房卡记录
WEB_Get_Diamond_Mssages = "acc/get_messages",
+ -- 设置自己支付图片
+ WEB_SEY_PAY_CODE = "acc/set_pay_code",
----index----
-- 获取公告
WEB_UPDATE_NOTICE = "index/get_notice",
diff --git a/lua_probject/base_project/Game/View/FamilyViewZuo.lua b/lua_probject/base_project/Game/View/FamilyViewZuo.lua
index e50c9b1f..d3d58ec4 100644
--- a/lua_probject/base_project/Game/View/FamilyViewZuo.lua
+++ b/lua_probject/base_project/Game/View/FamilyViewZuo.lua
@@ -344,6 +344,11 @@ end
--点击桌子进入游戏
function M:ClickJoinRoom(context, room, selectSeat)
+
+ if room.maxPlayers == 2 and (not DataManager.SelfUser.pay_code or #DataManager.SelfUser.pay_code < 6) then
+ ViewUtil:ErrorTip("没有绑定二维码不能进入双人游戏")
+ return
+ end
local group = DataManager.CurrenGroup
local roomCtr = ControllerManager.GetController(RoomController)
roomCtr:PublicJoinRoom(
diff --git a/lua_probject/base_project/Game/View/Lobby/LobbyPlayerInfoView.lua b/lua_probject/base_project/Game/View/Lobby/LobbyPlayerInfoView.lua
index f14a04ca..3ad58fd8 100644
--- a/lua_probject/base_project/Game/View/Lobby/LobbyPlayerInfoView.lua
+++ b/lua_probject/base_project/Game/View/Lobby/LobbyPlayerInfoView.lua
@@ -117,11 +117,40 @@ function M:init(url)
local btn_changeInfo = self._view:GetChild('btn_changeInfo')
btn_changeInfo:GetController('type').selectedIndex = pswType
btn_changeInfo.onClick:Set(function()
- local passwordUpdateView = PasswordUpdateView.new(pswType, function(res)
- pswType = DataManager.SelfUser.havaPsw and 1 or 0
- btn_changeInfo:GetController('type').selectedIndex = pswType
+ -- local passwordUpdateView = PasswordUpdateView.new(pswType, function(res)
+ -- pswType = DataManager.SelfUser.havaPsw and 1 or 0
+ -- btn_changeInfo:GetController('type').selectedIndex = pswType
+ -- end)
+ -- passwordUpdateView:Show()
+
+ GameApplication.Instance:PickAndUploadImage(function(ok, jsonStr)
+ print("PickAndUpload ok=", ok, jsonStr)
+
+ if not ok then
+ local err = json.decode(jsonStr)
+ print("err=", err.err, err.http, err.resp)
+ return
+ end
+
+ local r = json.decode(jsonStr)
+
+ -- 服务器返回在 r.serverResp
+ -- 你的 success 格式: {code=1,msg="ok",data={url=...,fullurl=...}}
+ local srv = r.serverResp
+ local data = srv.data
+
+ print("local=", r.localPath)
+ print("uploaded url=", data.url) --实战用
+ print("uploaded fullurl=", data.fullurl) --测试用
+
+ -- 这里 fullurl 就能直接用于显示/下载/存数据库
+ local loddyCtr1 = ControllerManager.GetController(LoddyController)
+ loddyCtr1:FG_Set_Pay_Code(data.fullurl,function(res)
+ if res.ReturnCode == 0 then
+
+ end
+ end)
end)
- passwordUpdateView:Show()
end)
local loddyCtr1 = ControllerManager.GetController(LoddyController)
diff --git a/lua_probject/base_project/Game/View/LobbyView.lua b/lua_probject/base_project/Game/View/LobbyView.lua
index 7930f85d..1d0fd247 100644
--- a/lua_probject/base_project/Game/View/LobbyView.lua
+++ b/lua_probject/base_project/Game/View/LobbyView.lua
@@ -154,11 +154,12 @@ function M:InitView(url)
self._flag_loadImageSucces = true
end)
- -- btn_head.onClick:Set(function()
- -- local lobbyPlayerInfoView = LobbyPlayerInfoView.new(DataManager.SelfUser, function()
- -- ImageLoad.Load(DataManager.SelfUser.head_url, btn_head._iconObject)
- -- end)
- -- end)
+ --临时开去测试设置支付图片(全部样式没有修改)
+ btn_head.onClick:Set(function()
+ local lobbyPlayerInfoView = LobbyPlayerInfoView.new(DataManager.SelfUser, function()
+ ImageLoad.Load(DataManager.SelfUser.head_url, btn_head._iconObject)
+ end)
+ end)
diff --git a/wb_new_ui/assets/Common/component/comp_payTips.xml b/wb_new_ui/assets/Common/component/comp_payTips.xml
new file mode 100644
index 00000000..53b08a3f
--- /dev/null
+++ b/wb_new_ui/assets/Common/component/comp_payTips.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wb_new_ui/assets/Common/package.xml b/wb_new_ui/assets/Common/package.xml
index d134d928..1897a43b 100644
--- a/wb_new_ui/assets/Common/package.xml
+++ b/wb_new_ui/assets/Common/package.xml
@@ -2359,6 +2359,7 @@
+
diff --git a/wb_new_ui/assets/Family/FamilyPayRecord/Compenent/Child_PlayerManagerChild.xml b/wb_new_ui/assets/Family/FamilyPayRecord/Compenent/Child_PlayerManagerChild.xml
index 00e54890..7cdaef80 100644
--- a/wb_new_ui/assets/Family/FamilyPayRecord/Compenent/Child_PlayerManagerChild.xml
+++ b/wb_new_ui/assets/Family/FamilyPayRecord/Compenent/Child_PlayerManagerChild.xml
@@ -1,25 +1,29 @@
-
+
-
+
-
+
-
-
+
+
-
+
+
+
+
+
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0.png
index 21fbd701..1fd78169 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_1.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_1.png
index a00a0a72..6f9c42ff 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_1.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_1.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_2.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_2.png
index 4b9b63f1..172fe03c 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_2.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_2.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_3.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_3.png
index ba808d5c..a0b5064d 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_3.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_3.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_4.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_4.png
index 1aa782c4..0ff19cae 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_4.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_4.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_5.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_5.png
index a1a60354..8ec693df 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_5.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_5.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_6.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_6.png
index d75fabd6..930f5d09 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_6.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas0_6.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_ej1ibt7d7s.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_ej1ibt7d7s.png
index fd28632f..f2f922ab 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_ej1ibt7d7s.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_ej1ibt7d7s.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jo5dbt7d93.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jo5dbt7d93.png
index 63d7c2a6..59872510 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jo5dbt7d93.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jo5dbt7d93.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyd.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyd.png
index 9518bc00..8d4fade0 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyd.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyd.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyi.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyi.png
index cf4d1099..520349a6 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyi.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyi.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyj.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyj.png
index 775343e4..f22d6a47 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyj.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyj.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyk.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyk.png
index b13058a1..308b9f09 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyk.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyk.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyl.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyl.png
index 70acf85b..11ba0b7a 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyl.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyl.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyp.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyp.png
index 8f121560..97aed3e1 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyp.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_jrro7cyp.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_wbhh7d6a.png b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_wbhh7d6a.png
index f8feb3b7..2f05203e 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_wbhh7d6a.png and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_atlas_wbhh7d6a.png differ
diff --git a/wb_unity_pro/Assets/ART/base/Family/ui/Family_fui.bytes b/wb_unity_pro/Assets/ART/base/Family/ui/Family_fui.bytes
index b693b6ed..d62d2646 100644
Binary files a/wb_unity_pro/Assets/ART/base/Family/ui/Family_fui.bytes and b/wb_unity_pro/Assets/ART/base/Family/ui/Family_fui.bytes differ
diff --git a/wb_unity_pro/Assets/Plugins/Android/network.jar b/wb_unity_pro/Assets/Plugins/Android/wb_android.jar
similarity index 88%
rename from wb_unity_pro/Assets/Plugins/Android/network.jar
rename to wb_unity_pro/Assets/Plugins/Android/wb_android.jar
index f6c386f0..a6b5d05f 100644
Binary files a/wb_unity_pro/Assets/Plugins/Android/network.jar and b/wb_unity_pro/Assets/Plugins/Android/wb_android.jar differ
diff --git a/wb_unity_pro/Assets/Plugins/Android/network.jar.meta b/wb_unity_pro/Assets/Plugins/Android/wb_android.jar.meta
similarity index 93%
rename from wb_unity_pro/Assets/Plugins/Android/network.jar.meta
rename to wb_unity_pro/Assets/Plugins/Android/wb_android.jar.meta
index d0d0971b..298152cf 100644
--- a/wb_unity_pro/Assets/Plugins/Android/network.jar.meta
+++ b/wb_unity_pro/Assets/Plugins/Android/wb_android.jar.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: c30b7448361d11948bf212d4bae1edb7
+guid: 5e9e9d6ba28c83c47b5120bb03e6a2d1
PluginImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery.meta b/wb_unity_pro/Assets/Plugins/NativeGallery.meta
new file mode 100644
index 00000000..c3fe3101
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 5e05ed2bddbccb94e9650efb5742e452
+folderAsset: yes
+timeCreated: 1518877529
+licenseType: Free
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Android.meta
new file mode 100644
index 00000000..4d679dbc
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/Android.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 0a607dcda26e7614f86300c6ca717295
+folderAsset: yes
+timeCreated: 1498722617
+licenseType: Pro
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs
new file mode 100644
index 00000000..2cef7e43
--- /dev/null
+++ b/wb_unity_pro/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();
+ 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
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGCallbackHelper.cs.meta
new file mode 100644
index 00000000..3509799e
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs
new file mode 100644
index 00000000..b8e975d2
--- /dev/null
+++ b/wb_unity_pro/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
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGMediaReceiveCallbackAndroid.cs.meta
new file mode 100644
index 00000000..f0bec22d
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs
new file mode 100644
index 00000000..0cb2e837
--- /dev/null
+++ b/wb_unity_pro/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
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NGPermissionCallbackAndroid.cs.meta
new file mode 100644
index 00000000..3f523aee
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NativeGallery.aar b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NativeGallery.aar
new file mode 100644
index 00000000..b78fa449
Binary files /dev/null and b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NativeGallery.aar differ
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Android/NativeGallery.aar.meta
new file mode 100644
index 00000000..7d90a7c4
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Editor.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Editor.meta
new file mode 100644
index 00000000..5d79ce2f
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/Editor.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 19fc6b8ce781591438a952d8aa9104f8
+folderAsset: yes
+timeCreated: 1521452097
+licenseType: Free
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs
new file mode 100644
index 00000000..ea337252
--- /dev/null
+++ b/wb_unity_pro/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( 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() { "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
+ }
+}
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NGPostProcessBuild.cs.meta
new file mode 100644
index 00000000..15c37deb
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef b/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef
new file mode 100644
index 00000000..d129dd2d
--- /dev/null
+++ b/wb_unity_pro/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
+}
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta
new file mode 100644
index 00000000..46835096
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/Editor/NativeGallery.Editor.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3dffc8e654f00c545a82d0a5274d51eb
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef b/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef
new file mode 100644
index 00000000..c1fb4a75
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef
@@ -0,0 +1,3 @@
+{
+ "name": "NativeGallery.Runtime"
+}
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta
new file mode 100644
index 00000000..097882be
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.Runtime.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 6e5063adab271564ba0098a06a8cebda
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.cs
new file mode 100644
index 00000000..9951d9f4
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.cs
@@ -0,0 +1,995 @@
+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( "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( "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 NGPermissionCallbackAndroid( 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 RequestPermissionAsync( PermissionType permissionType, MediaType mediaTypes )
+ {
+ TaskCompletionSource tcs = new TaskCompletionSource();
+ 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( "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( "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( "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( "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 editorFilters = new System.Collections.Generic.List( 4 );
+
+ if( ( mediaType & MediaType.Image ) == MediaType.Image )
+ {
+ editorFilters.Add( "Image files" );
+ editorFilters.Add( "png,jpg,jpeg" );
+ }
+
+ if( ( mediaType & MediaType.Video ) == MediaType.Video )
+ {
+ editorFilters.Add( "Video files" );
+ editorFilters.Add( "mp4,mov,webm,avi" );
+ }
+
+ if( ( mediaType & MediaType.Audio ) == MediaType.Audio )
+ {
+ editorFilters.Add( "Audio files" );
+ editorFilters.Add( "mp3,wav,aac,flac" );
+ }
+
+ editorFilters.Add( "All files" );
+ editorFilters.Add( "*" );
+
+ string pickedFile = UnityEditor.EditorUtility.OpenFilePanelWithFilters( "Select file", "", editorFilters.ToArray() );
+
+ if( callback != null )
+ callback( pickedFile != "" ? pickedFile : null );
+#elif UNITY_ANDROID
+ AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( callback, null ), (int) mediaType, false, SelectedMediaPath, mime, title );
+#elif UNITY_IOS
+ if( mediaType == MediaType.Audio )
+ {
+ Debug.LogError( "Picking audio files is not supported on iOS" );
+
+ if( callback != null ) // Selecting audio files is not supported on iOS
+ callback( null );
+ }
+ else
+ {
+ NGMediaReceiveCallbackiOS.Initialize( callback, null );
+ _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 1 );
+ }
+#else
+ if( callback != null )
+ callback( null );
+#endif
+ }, PermissionType.Read, mediaType );
+ }
+
+ private static void GetMultipleMediaFromGallery( MediaPickMultipleCallback callback, MediaType mediaType, string mime, string title )
+ {
+ RequestPermissionAsync( ( permission ) =>
+ {
+ if( permission != Permission.Granted || IsMediaPickerBusy() )
+ {
+ callback?.Invoke( null );
+ return;
+ }
+
+ if( CanSelectMultipleFilesFromGallery() )
+ {
+#if !UNITY_EDITOR && UNITY_ANDROID
+ AJC.CallStatic( "PickMedia", Context, new NGMediaReceiveCallbackAndroid( null, callback ), (int) mediaType, true, SelectedMediaPath, mime, title );
+#elif !UNITY_EDITOR && UNITY_IOS
+ if( mediaType == MediaType.Audio )
+ {
+ Debug.LogError( "Picking audio files is not supported on iOS" );
+
+ if( callback != null ) // Selecting audio files is not supported on iOS
+ callback( null );
+ }
+ else
+ {
+ NGMediaReceiveCallbackiOS.Initialize( null, callback );
+ _NativeGallery_PickMedia( SelectedMediaPath, (int) ( mediaType & ~MediaType.Audio ), PermissionFreeMode ? 1 : 0, 0 );
+ }
+#else
+ if( callback != null )
+ callback( null );
+#endif
+ }
+ else if( callback != null )
+ callback( null );
+ }, PermissionType.Read, mediaType );
+ }
+
+ private static byte[] GetTextureBytes( Texture2D texture, bool isJpeg )
+ {
+ try
+ {
+ return isJpeg ? texture.EncodeToJPG( 100 ) : texture.EncodeToPNG();
+ }
+ catch( UnityException )
+ {
+ return GetTextureBytesFromCopy( texture, isJpeg );
+ }
+ catch( ArgumentException )
+ {
+ return GetTextureBytesFromCopy( texture, isJpeg );
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static byte[] GetTextureBytesFromCopy( Texture2D texture, bool isJpeg )
+ {
+ // Texture is marked as non-readable, create a readable copy and save it instead
+ Debug.LogWarning( "Saving non-readable textures is slower than saving readable textures" );
+
+ Texture2D sourceTexReadable = null;
+ RenderTexture rt = RenderTexture.GetTemporary( texture.width, texture.height );
+ RenderTexture activeRT = RenderTexture.active;
+
+ try
+ {
+ Graphics.Blit( texture, rt );
+ RenderTexture.active = rt;
+
+ sourceTexReadable = new Texture2D( texture.width, texture.height, isJpeg ? TextureFormat.RGB24 : TextureFormat.RGBA32, false );
+ sourceTexReadable.ReadPixels( new Rect( 0, 0, texture.width, texture.height ), 0, 0, false );
+ sourceTexReadable.Apply( false, false );
+ }
+ catch( Exception e )
+ {
+ Debug.LogException( e );
+
+ Object.DestroyImmediate( sourceTexReadable );
+ return null;
+ }
+ finally
+ {
+ RenderTexture.active = activeRT;
+ RenderTexture.ReleaseTemporary( rt );
+ }
+
+ try
+ {
+ return isJpeg ? sourceTexReadable.EncodeToJPG( 100 ) : sourceTexReadable.EncodeToPNG();
+ }
+ catch( Exception e )
+ {
+ Debug.LogException( e );
+ return null;
+ }
+ finally
+ {
+ Object.DestroyImmediate( sourceTexReadable );
+ }
+ }
+
+#if UNITY_ANDROID
+ private static async Task TryCallNativeAndroidFunctionOnSeparateThread( Func 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( "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 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( "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 UNITY_2020_2_OR_NEWER
+ 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 );
+#else
+ if (www.isNetworkError || www.isHttpError)
+ Debug.LogWarning("Couldn't use UnityWebRequest to load image, falling back to LoadImage: " + www.error);
+ else
+ result = DownloadHandlerTexture.GetContent(www);
+#endif
+ }
+
+ 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( "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 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( "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( "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( "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
+}
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/NativeGallery.cs.meta
new file mode 100644
index 00000000..230d7e60
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/README.txt b/wb_unity_pro/Assets/Plugins/NativeGallery/README.txt
new file mode 100644
index 00000000..b66b32e8
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/README.txt
@@ -0,0 +1,6 @@
+= Native Gallery for Android & iOS (v1.9.2) =
+
+Documentation: https://github.com/yasirkula/UnityNativeGallery
+FAQ: https://github.com/yasirkula/UnityNativeGallery#faq
+Example code: https://github.com/yasirkula/UnityNativeGallery#example-code
+E-mail: yasirkula@gmail.com
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/README.txt.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/README.txt.meta
new file mode 100644
index 00000000..1bdea0b5
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/README.txt.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: be769f45b807c40459e5bafb18e887d6
+timeCreated: 1563308465
+licenseType: Free
+TextScriptImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS.meta
new file mode 100644
index 00000000..6db8dc89
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 9c623599351a41a4c84c20f73c9d8976
+folderAsset: yes
+timeCreated: 1498722622
+licenseType: Pro
+DefaultImporter:
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs
new file mode 100644
index 00000000..e30081fa
--- /dev/null
+++ b/wb_unity_pro/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();
+ 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
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaReceiveCallbackiOS.cs.meta
new file mode 100644
index 00000000..00b8b23a
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs
new file mode 100644
index 00000000..222965b1
--- /dev/null
+++ b/wb_unity_pro/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();
+ 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
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGMediaSaveCallbackiOS.cs.meta
new file mode 100644
index 00000000..734c853a
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs
new file mode 100644
index 00000000..1936f76d
--- /dev/null
+++ b/wb_unity_pro/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();
+ 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
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NGPermissionCallbackiOS.cs.meta
new file mode 100644
index 00000000..ea8489a2
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm
new file mode 100644
index 00000000..d301101b
--- /dev/null
+++ b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm
@@ -0,0 +1,1251 @@
+#import
+#import
+#import
+#import
+#import
+#import
+
+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 *)results API_AVAILABLE(ios(14))
+{
+ imagePickerNew = nil;
+ imagePickerState = 2;
+
+ [picker dismissViewControllerAnimated:NO completion:nil];
+
+ if( results != nil && [results count] > 0 )
+ {
+ NSMutableArray *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 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 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 object, NSError *error )
+ {
+ if( object != nil && [object isKindOfClass:[PHLivePhoto class]] )
+ {
+ // Extract image data from live photo
+ // Credit: https://stackoverflow.com/a/41341675/2373034
+ NSArray* 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* 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];
+}
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta b/wb_unity_pro/Assets/Plugins/NativeGallery/iOS/NativeGallery.mm.meta
new file mode 100644
index 00000000..dd3697a2
--- /dev/null
+++ b/wb_unity_pro/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:
diff --git a/wb_unity_pro/Assets/Scripts/GameApplication.cs b/wb_unity_pro/Assets/Scripts/GameApplication.cs
index ee809dc3..b4b07941 100644
--- a/wb_unity_pro/Assets/Scripts/GameApplication.cs
+++ b/wb_unity_pro/Assets/Scripts/GameApplication.cs
@@ -36,7 +36,12 @@ public class GameApplication : MonoBehaviour
ExceptionReporter _ex_reporter=null;
private LuaFunction _share_callback;
private LuaFunction _wxlogin_callback;
-
+
+ // === Gallery / Pick Image ===
+ private LuaFunction _pick_image_callback;
+ private int _pick_max_size = 1024; // 长边最大像素
+ private int _pick_jpg_quality = 80; // JPG质量 1~100
+
void Awake()
{
TaurusUnity.init();
@@ -330,6 +335,7 @@ public class GameApplication : MonoBehaviour
}
[DllImport("native-lib")]
private static extern IntPtr getAppIdCSharp();
+
public void WXLogin(LuaFunction callback)
{
if (!buildApp) return;
@@ -557,4 +563,130 @@ public class GameApplication : MonoBehaviour
{
Application.Quit();
}
+
+ private const string UPLOAD_IMAGE_URL = "http://8.138.255.70:9898/api/common/uploadimage";
+
+ /// Lua: GameApplication.Instance:PickAndUploadImage(function(ok, json) end)
+ /// 返回 json:
+ /// - 选图信息: localPath,width,height,mime,orientation
+ /// - 上传结果: url, fullurl, size, ext, serverName
+ public void PickAndUploadImage(LuaFunction cb)
+ {
+#if UNITY_EDITOR
+ SafeLuaCallback(cb, false, "{\"err\":\"UNITY_EDITOR\"}");
+ return;
+#else
+ // 1) 先选图
+ try
+ {
+ NativeGallery.RequestPermissionAsync((perm) =>
+ {
+ if (perm != NativeGallery.Permission.Granted)
+ {
+ SafeLuaCallback(cb, false, "{\"err\":\"PERMISSION_DENIED\"}");
+ return;
+ }
+
+ NativeGallery.GetImageFromGallery((path) =>
+ {
+ if (string.IsNullOrEmpty(path) || !File.Exists(path))
+ {
+ SafeLuaCallback(cb, false, "{\"err\":\"CANCEL_OR_NOT_FOUND\"}");
+ return;
+ }
+
+ // 2) 拿图片属性(可选)
+ NativeGallery.ImageProperties prop;
+ try { prop = NativeGallery.GetImageProperties(path); }
+ catch { prop = new NativeGallery.ImageProperties(0, 0, "", NativeGallery.ImageOrientation.Unknown); }
+
+ // 3) 上传
+ StartCoroutine(CoUploadPickedImage(path, prop, cb));
+ }, "请选择一张图片", "image/*");
+
+ }, NativeGallery.PermissionType.Read, NativeGallery.MediaType.Image);
+ }
+ catch (Exception e)
+ {
+ SafeLuaCallback(cb, false, "{\"err\":\"EXCEPTION\",\"msg\":\"" + EscapeJson(e.ToString()) + "\"}");
+ }
+#endif
+ }
+
+ private IEnumerator CoUploadPickedImage(string localPath, NativeGallery.ImageProperties prop, LuaFunction cb)
+ {
+ byte[] bytes;
+ try { bytes = File.ReadAllBytes(localPath); }
+ catch (Exception e)
+ {
+ SafeLuaCallback(cb, false, "{\"err\":\"READ_FILE_FAIL\",\"msg\":\"" + EscapeJson(e.Message) + "\"}");
+ yield break;
+ }
+
+ string fileName = Path.GetFileName(localPath);
+ if (string.IsNullOrEmpty(Path.GetExtension(fileName)))
+ fileName += ".jpg"; // 防御:没后缀就补一个
+
+ WWWForm form = new WWWForm();
+ form.AddBinaryData("file", bytes, fileName, "application/octet-stream"); // 服务端字段名是 file
+
+ using (UnityWebRequest req = UnityWebRequest.Post(UPLOAD_IMAGE_URL, form))
+ {
+ req.timeout = 60;
+ yield return req.SendWebRequest();
+
+#if UNITY_2020_2_OR_NEWER
+ bool ok = req.result == UnityWebRequest.Result.Success;
+#else
+ bool ok = !(req.isNetworkError || req.isHttpError);
+#endif
+ string body = req.downloadHandler != null ? req.downloadHandler.text : "";
+
+ if (!ok)
+ {
+ string errJson = "{"
+ + "\"err\":\"UPLOAD_FAIL\","
+ + "\"http\":\"" + EscapeJson(req.error ?? "") + "\","
+ + "\"resp\":\"" + EscapeJson(body) + "\""
+ + "}";
+ SafeLuaCallback(cb, false, errJson);
+ yield break;
+ }
+
+ // 把选图信息 + 服务器返回一起打包给 Lua(不解析服务端 JSON,直接原样塞回,Lua 自己 decode)
+ string resultJson = "{"
+ + "\"localPath\":\"" + EscapeJson(localPath) + "\","
+ + "\"width\":" + prop.width + ","
+ + "\"height\":" + prop.height + ","
+ + "\"mime\":\"" + EscapeJson(prop.mimeType ?? "") + "\","
+ + "\"orientation\":" + (int)prop.orientation + ","
+ + "\"serverResp\":" + SafeJsonObject(body)
+ + "}";
+
+ SafeLuaCallback(cb, true, resultJson);
+ }
+ }
+
+ private void SafeLuaCallback(LuaFunction fn, bool ok, string json)
+ {
+ if (fn == null) return;
+ try { fn.Call(ok, json); }
+ catch (Exception e) { Debug.LogException(e); }
+ finally { fn.Dispose(); }
+ }
+
+ private string EscapeJson(string s)
+ {
+ if (string.IsNullOrEmpty(s)) return "";
+ return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
+ }
+
+ // 服务端返回一般是 {"code":1,...},这里确保拼接时是合法 json
+ private string SafeJsonObject(string s)
+ {
+ if (string.IsNullOrEmpty(s)) return "{}";
+ s = s.Trim();
+ if (s.StartsWith("{") && s.EndsWith("}")) return s; // 看起来像对象
+ return "{\"raw\":\"" + EscapeJson(s) + "\"}";
+ }
}
\ No newline at end of file
diff --git a/wb_unity_pro/Assets/Scripts/Platform/PlatformAndroid.cs b/wb_unity_pro/Assets/Scripts/Platform/PlatformAndroid.cs
index 12dd3e52..020f3920 100644
--- a/wb_unity_pro/Assets/Scripts/Platform/PlatformAndroid.cs
+++ b/wb_unity_pro/Assets/Scripts/Platform/PlatformAndroid.cs
@@ -78,6 +78,7 @@ public class PlatformAndroid :MonoBehaviour
return _activity.Call("__getRoomID");
}
+
#endif
diff --git a/wb_unity_pro/Assets/Source/Generate/GameApplicationWrap.cs b/wb_unity_pro/Assets/Source/Generate/GameApplicationWrap.cs
index ca648d1c..3e83bdd8 100644
--- a/wb_unity_pro/Assets/Source/Generate/GameApplicationWrap.cs
+++ b/wb_unity_pro/Assets/Source/Generate/GameApplicationWrap.cs
@@ -24,6 +24,7 @@ public class GameApplicationWrap
L.RegFunction("GetPublicIP", GetPublicIP);
L.RegFunction("InitBaiduMap", InitBaiduMap);
L.RegFunction("QuitGameOnUnity", QuitGameOnUnity);
+ L.RegFunction("PickAndUploadImage", PickAndUploadImage);
L.RegFunction("__eq", op_Equality);
L.RegFunction("__tostring", ToLua.op_ToString);
L.RegVar("Instance", get_Instance, set_Instance);
@@ -401,6 +402,23 @@ public class GameApplicationWrap
}
}
+ [MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
+ static int PickAndUploadImage(IntPtr L)
+ {
+ try
+ {
+ ToLua.CheckArgsCount(L, 2);
+ GameApplication obj = (GameApplication)ToLua.CheckObject(L, 1);
+ LuaFunction arg0 = ToLua.CheckLuaFunction(L, 2);
+ obj.PickAndUploadImage(arg0);
+ return 0;
+ }
+ catch (Exception e)
+ {
+ return LuaDLL.toluaL_exception(L, e);
+ }
+ }
+
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int op_Equality(IntPtr L)
{
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack.byte b/wb_unity_pro/Assets/StreamingAssets/Pack.byte
index 5244f10d..691765c0 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack.byte and b/wb_unity_pro/Assets/StreamingAssets/Pack.byte differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/base/family/5c934681ea6462867ebfd8485f3e87d6 b/wb_unity_pro/Assets/StreamingAssets/Pack/base/family/5c934681ea6462867ebfd8485f3e87d6
index c2377f24..cf56d784 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/base/family/5c934681ea6462867ebfd8485f3e87d6 and b/wb_unity_pro/Assets/StreamingAssets/Pack/base/family/5c934681ea6462867ebfd8485f3e87d6 differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoddyController.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoddyController.lua
index b0df3d91..fee9410f 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoddyController.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoddyController.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoginController.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoginController.lua
index efe738b6..b2f18588 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoginController.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/LoginController.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/NewGroupController.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/NewGroupController.lua
index 99811a7b..0404e51a 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/NewGroupController.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Controller/NewGroupController.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Protocol.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Protocol.lua
index d17de7de..de8b9e08 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Protocol.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/Protocol.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyViewZuo.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyViewZuo.lua
index 860c7bf1..a3d96459 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyViewZuo.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyViewZuo.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyManagerTable.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyManagerTable.lua
index 9cd29d17..265e4bf9 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyManagerTable.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyManagerTable.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyPayRecordView.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyPayRecordView.lua
new file mode 100644
index 00000000..af207248
Binary files /dev/null and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyPayRecordView.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyPayRecordView.lua.meta b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyPayRecordView.lua.meta
new file mode 100644
index 00000000..4f49a488
--- /dev/null
+++ b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/FamilyPayRecordView.lua.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c87c4fe551df54b4ba4a9526ac652ded
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerChild_GamePlayView.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerChild_GamePlayView.lua
index 157113e6..0b05078a 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerChild_GamePlayView.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerChild_GamePlayView.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerRecordChild_LeaderboardView.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerRecordChild_LeaderboardView.lua
new file mode 100644
index 00000000..296a3180
Binary files /dev/null and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerRecordChild_LeaderboardView.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerRecordChild_LeaderboardView.lua.meta b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerRecordChild_LeaderboardView.lua.meta
new file mode 100644
index 00000000..a91de1cf
--- /dev/null
+++ b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/FamilyZuo/ManagerRecordChild_LeaderboardView.lua.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: ae3d47c06054bdb4d865c060f9310804
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/Lobby/LobbyPlayerInfoView.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/Lobby/LobbyPlayerInfoView.lua
index b14e668b..34ddd52e 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/Lobby/LobbyPlayerInfoView.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/Lobby/LobbyPlayerInfoView.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/LobbyView.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/LobbyView.lua
index 6d67f0b4..1f36708c 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/LobbyView.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/Game/View/LobbyView.lua differ
diff --git a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/extend/poker/duoduo/EXPlayerSelfPokerInfoView.lua b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/extend/poker/duoduo/EXPlayerSelfPokerInfoView.lua
index ad5d4516..5c8d7fdd 100644
Binary files a/wb_unity_pro/Assets/StreamingAssets/Pack/lua/extend/poker/duoduo/EXPlayerSelfPokerInfoView.lua and b/wb_unity_pro/Assets/StreamingAssets/Pack/lua/extend/poker/duoduo/EXPlayerSelfPokerInfoView.lua differ
diff --git a/wb_unity_pro/Pack/Android/base/base_script/asset_pack1.0.4.bytes b/wb_unity_pro/Pack/Android/base/base_script/asset_pack1.0.4.bytes
index c6459316..94605701 100644
Binary files a/wb_unity_pro/Pack/Android/base/base_script/asset_pack1.0.4.bytes and b/wb_unity_pro/Pack/Android/base/base_script/asset_pack1.0.4.bytes differ
diff --git a/wb_unity_pro/Pack/Android/base/family/asset_pack1.0.4.bytes b/wb_unity_pro/Pack/Android/base/family/asset_pack1.0.4.bytes
index 9cd49ada..b05077f6 100644
Binary files a/wb_unity_pro/Pack/Android/base/family/asset_pack1.0.4.bytes and b/wb_unity_pro/Pack/Android/base/family/asset_pack1.0.4.bytes differ
diff --git a/wb_unity_pro/Pack/Android/extend/poker/duoduo/asset_pack1.0.2.bytes b/wb_unity_pro/Pack/Android/extend/poker/duoduo/asset_pack1.0.2.bytes
index f02a8a3a..8902147c 100644
Binary files a/wb_unity_pro/Pack/Android/extend/poker/duoduo/asset_pack1.0.2.bytes and b/wb_unity_pro/Pack/Android/extend/poker/duoduo/asset_pack1.0.2.bytes differ
diff --git a/wb_unity_pro/Pack/config/asset_config1.0.4.json b/wb_unity_pro/Pack/config/asset_config1.0.4.json
index 91acd411..0f186371 100644
--- a/wb_unity_pro/Pack/config/asset_config1.0.4.json
+++ b/wb_unity_pro/Pack/config/asset_config1.0.4.json
@@ -1 +1 @@
-[{"bundle":"base/base_script","lua_path":"/tolua_project,/base_project,/main_project","check":true,"size":2010011,"name":"base_script","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/common","check":true,"size":16793054,"name":"common","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/login","check":true,"size":602811,"name":"login","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/lobby","check":true,"size":12116548,"name":"lobby","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/family","check":true,"size":5047323,"name":"family","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/chat","check":true,"size":602524,"name":"chat","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/newgroup","check":true,"size":1941001,"name":"newgroup","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/rank","check":true,"size":165367,"name":"rank","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_majiang","check":true,"size":11706906,"name":"main_majiang","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_poker","check":true,"size":8010045,"name":"main_poker","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_zipai","check":true,"size":9126303,"name":"main_zipai","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/static","check":true,"size":35706766,"name":"static","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/embed","check":true,"size":898919,"name":"embed","ver":"1.0.4","version":"1.0.4","is_res":true},{"bundle":"base/main_pokemajiang","check":true,"size":6192705,"name":"main_pokemajiang","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_zipaimajiang","check":true,"size":8984762,"name":"main_zipaimajiang","ver":"1.0.4","version":"1.0.4"}]
\ No newline at end of file
+[{"bundle":"base/base_script","lua_path":"/tolua_project,/base_project,/main_project","check":true,"size":2018202,"name":"base_script","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/common","check":true,"size":16793054,"name":"common","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/login","check":true,"size":602811,"name":"login","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/lobby","check":true,"size":12116548,"name":"lobby","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/family","check":true,"size":5073460,"name":"family","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/chat","check":true,"size":602524,"name":"chat","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/newgroup","check":true,"size":1941001,"name":"newgroup","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/rank","check":true,"size":165367,"name":"rank","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_majiang","check":true,"size":11706906,"name":"main_majiang","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_poker","check":true,"size":8010045,"name":"main_poker","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_zipai","check":true,"size":9126303,"name":"main_zipai","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/static","check":true,"size":35706766,"name":"static","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/embed","check":true,"size":898919,"name":"embed","ver":"1.0.4","version":"1.0.4","is_res":true},{"bundle":"base/main_pokemajiang","check":true,"size":6192705,"name":"main_pokemajiang","ver":"1.0.4","version":"1.0.4"},{"bundle":"base/main_zipaimajiang","check":true,"size":8984762,"name":"main_zipaimajiang","ver":"1.0.4","version":"1.0.4"}]
\ No newline at end of file
diff --git a/wb_unity_pro/Pack_All/Android/base/base_script/asset_pack1.0.4.bytes b/wb_unity_pro/Pack_All/Android/base/base_script/asset_pack1.0.4.bytes
index c6459316..94605701 100644
Binary files a/wb_unity_pro/Pack_All/Android/base/base_script/asset_pack1.0.4.bytes and b/wb_unity_pro/Pack_All/Android/base/base_script/asset_pack1.0.4.bytes differ
diff --git a/wb_unity_pro/Pack_All/Android/base/family/asset_pack1.0.4.bytes b/wb_unity_pro/Pack_All/Android/base/family/asset_pack1.0.4.bytes
index 9cd49ada..b05077f6 100644
Binary files a/wb_unity_pro/Pack_All/Android/base/family/asset_pack1.0.4.bytes and b/wb_unity_pro/Pack_All/Android/base/family/asset_pack1.0.4.bytes differ
diff --git a/wb_unity_pro/Pack_All/Android/extend/poker/duoduo/asset_pack1.0.2.bytes b/wb_unity_pro/Pack_All/Android/extend/poker/duoduo/asset_pack1.0.2.bytes
index f02a8a3a..8902147c 100644
Binary files a/wb_unity_pro/Pack_All/Android/extend/poker/duoduo/asset_pack1.0.2.bytes and b/wb_unity_pro/Pack_All/Android/extend/poker/duoduo/asset_pack1.0.2.bytes differ