359 lines
12 KiB
C#
359 lines
12 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using Newtonsoft.Json;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.Networking;
|
|
|
|
namespace AlicizaX.PackageManager.Editor
|
|
{
|
|
public enum PackageState
|
|
{
|
|
UnInstall,
|
|
InstallLocal,
|
|
Install,
|
|
Update
|
|
}
|
|
|
|
[Serializable]
|
|
public class RepositoryPackageData
|
|
{
|
|
public string name;
|
|
public string description;
|
|
public string cloneUrl;
|
|
public string htmlUrl;
|
|
public string updatedAt;
|
|
public string defaultBranch;
|
|
public string apiUrl;
|
|
public string remoteHead;
|
|
public string ownerName;
|
|
|
|
public string displayName;
|
|
public string version;
|
|
public string unityVersion;
|
|
public string category;
|
|
public string packageDescription;
|
|
public string authorName;
|
|
|
|
public string ManifestVersion;
|
|
public string InstalledVersion;
|
|
public string InstalledHash;
|
|
public string InstalledSource;
|
|
|
|
public PackageState PackageState;
|
|
}
|
|
|
|
[Serializable]
|
|
public class RepoApiResponse
|
|
{
|
|
public bool ok;
|
|
public List<RepoItem> data;
|
|
}
|
|
|
|
[Serializable]
|
|
public class RepoItem
|
|
{
|
|
public string name;
|
|
public string description;
|
|
public string clone_url;
|
|
public string html_url;
|
|
public string updated_at;
|
|
public string default_branch;
|
|
public string url;
|
|
public Owner owner;
|
|
|
|
[Serializable]
|
|
public class Owner
|
|
{
|
|
public string login;
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class RepoBranchResponse
|
|
{
|
|
public string name;
|
|
public BranchCommit commit;
|
|
|
|
[Serializable]
|
|
public class BranchCommit
|
|
{
|
|
public string id;
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class PackageJsonContentResponse
|
|
{
|
|
public string content;
|
|
public string encoding;
|
|
}
|
|
|
|
[Serializable]
|
|
public class PackageJsonResponse
|
|
{
|
|
public string name;
|
|
public string displayName;
|
|
public string category;
|
|
public string description;
|
|
public string version;
|
|
public string unity;
|
|
public Author author;
|
|
|
|
[Serializable]
|
|
public class Author
|
|
{
|
|
public string name;
|
|
}
|
|
}
|
|
|
|
internal sealed class PackageManagerCoroutines : MonoBehaviour
|
|
{
|
|
public static PackageManagerCoroutines Coroutines;
|
|
|
|
public static void CreateCoroutines()
|
|
{
|
|
if (Coroutines != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var gameObject = new GameObject("PackageManagerCoroutines");
|
|
gameObject.hideFlags = HideFlags.HideAndDontSave;
|
|
Coroutines = gameObject.AddComponent<PackageManagerCoroutines>();
|
|
}
|
|
|
|
public static void DestroyCoroutines()
|
|
{
|
|
if (Coroutines == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Coroutines.StopAllCoroutines();
|
|
DestroyImmediate(Coroutines.gameObject);
|
|
Coroutines = null;
|
|
}
|
|
}
|
|
|
|
public static class RepositoryDataFetcher
|
|
{
|
|
private const string BaseApiUrl = "http://101.34.252.46:3000/api/v1/repos/search?q=unity&topic=false";
|
|
private const string PackageJsonPathTemplate = "/contents/package.json?ref={0}";
|
|
private const string BranchPathTemplate = "/branches/{0}";
|
|
private const string LastUpdateEditorPrefsKey = "PackageUpdateDate";
|
|
|
|
public static void FetchRepositoryData(Action<List<RepositoryPackageData>> callback)
|
|
{
|
|
PackageManagerCoroutines.CreateCoroutines();
|
|
PackageManagerCoroutines.Coroutines.StartCoroutine(FetchDataRoutine(callback));
|
|
}
|
|
|
|
public static string GetLastRefreshTime()
|
|
{
|
|
return EditorPrefs.GetString(LastUpdateEditorPrefsKey, string.Empty);
|
|
}
|
|
|
|
private static IEnumerator FetchDataRoutine(Action<List<RepositoryPackageData>> callback)
|
|
{
|
|
List<RepositoryPackageData> results = null;
|
|
Exception fatalError = null;
|
|
|
|
using (var request = CreateWebRequest(BaseApiUrl))
|
|
{
|
|
yield return request.SendWebRequest();
|
|
|
|
if (!TryValidateRequest(request, "Repository request failed"))
|
|
{
|
|
fatalError = new Exception(request.error);
|
|
}
|
|
else
|
|
{
|
|
RepoApiResponse response = null;
|
|
try
|
|
{
|
|
response = JsonConvert.DeserializeObject<RepoApiResponse>(request.downloadHandler.text);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
fatalError = e;
|
|
}
|
|
|
|
if (fatalError == null)
|
|
{
|
|
if (response?.ok != true || response.data == null)
|
|
{
|
|
fatalError = new Exception("Repository response is invalid.");
|
|
}
|
|
else
|
|
{
|
|
results = new List<RepositoryPackageData>(response.data.Count);
|
|
foreach (var repo in response.data)
|
|
{
|
|
var data = CreateBaseRepositoryData(repo);
|
|
yield return FetchBranchHeadRoutine(data);
|
|
yield return FetchPackageInfoRoutine(data);
|
|
results.Add(data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fatalError != null)
|
|
{
|
|
Debug.LogError($"Fetch repository data failed: {fatalError.Message}");
|
|
callback?.Invoke(new List<RepositoryPackageData>());
|
|
}
|
|
else
|
|
{
|
|
EditorPrefs.SetString(LastUpdateEditorPrefsKey, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
|
callback?.Invoke(results ?? new List<RepositoryPackageData>());
|
|
}
|
|
|
|
PackageManagerCoroutines.DestroyCoroutines();
|
|
}
|
|
|
|
private static IEnumerator FetchBranchHeadRoutine(RepositoryPackageData packageData)
|
|
{
|
|
if (string.IsNullOrEmpty(packageData.apiUrl) || string.IsNullOrEmpty(packageData.defaultBranch))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var branchUrl = packageData.apiUrl + string.Format(BranchPathTemplate, UnityWebRequest.EscapeURL(packageData.defaultBranch));
|
|
using (var request = CreateWebRequest(branchUrl))
|
|
{
|
|
yield return request.SendWebRequest();
|
|
if (!TryValidateRequest(request, $"Branch request failed for {packageData.name}"))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
try
|
|
{
|
|
var branch = JsonConvert.DeserializeObject<RepoBranchResponse>(request.downloadHandler.text);
|
|
packageData.remoteHead = branch?.commit?.id;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"Branch response parse failed for {packageData.name}: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IEnumerator FetchPackageInfoRoutine(RepositoryPackageData packageData)
|
|
{
|
|
if (string.IsNullOrEmpty(packageData.apiUrl) || string.IsNullOrEmpty(packageData.defaultBranch))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var packageUrl = packageData.apiUrl + string.Format(PackageJsonPathTemplate, UnityWebRequest.EscapeURL(packageData.defaultBranch));
|
|
using (var request = CreateWebRequest(packageUrl))
|
|
{
|
|
yield return request.SendWebRequest();
|
|
if (!TryValidateRequest(request, $"Package request failed for {packageData.name}"))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
try
|
|
{
|
|
var contentResponse = JsonConvert.DeserializeObject<PackageJsonContentResponse>(request.downloadHandler.text);
|
|
var packageJson = DecodeContent(contentResponse);
|
|
if (string.IsNullOrEmpty(packageJson))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
var packageInfo = JsonConvert.DeserializeObject<PackageJsonResponse>(packageJson);
|
|
UpdateWithPackageInfo(packageData, packageInfo);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"Package json parse failed for {packageData.name}: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static UnityWebRequest CreateWebRequest(string url)
|
|
{
|
|
var request = UnityWebRequest.Get(url);
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
request.timeout = 20;
|
|
request.SetRequestHeader("accept", "application/json");
|
|
return request;
|
|
}
|
|
|
|
private static bool TryValidateRequest(UnityWebRequest request, string errorMessage)
|
|
{
|
|
if (request.result == UnityWebRequest.Result.Success)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Debug.LogError($"{errorMessage}: {request.error}");
|
|
return false;
|
|
}
|
|
|
|
private static RepositoryPackageData CreateBaseRepositoryData(RepoItem repo)
|
|
{
|
|
return new RepositoryPackageData
|
|
{
|
|
name = repo.name,
|
|
description = repo.description,
|
|
cloneUrl = repo.clone_url,
|
|
htmlUrl = repo.html_url,
|
|
updatedAt = repo.updated_at,
|
|
defaultBranch = string.IsNullOrEmpty(repo.default_branch) ? "main" : repo.default_branch,
|
|
apiUrl = repo.url,
|
|
ownerName = repo.owner?.login
|
|
};
|
|
}
|
|
|
|
private static void UpdateWithPackageInfo(RepositoryPackageData data, PackageJsonResponse package)
|
|
{
|
|
if (package == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
data.name = string.IsNullOrEmpty(package.name) ? data.name : package.name;
|
|
data.displayName = string.IsNullOrEmpty(package.displayName) ? data.name : package.displayName;
|
|
data.version = package.version;
|
|
data.unityVersion = package.unity;
|
|
data.category = package.category;
|
|
data.packageDescription = package.description;
|
|
data.authorName = package.author?.name;
|
|
}
|
|
|
|
private static string DecodeContent(PackageJsonContentResponse contentResponse)
|
|
{
|
|
if (contentResponse == null || string.IsNullOrEmpty(contentResponse.content))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!string.Equals(contentResponse.encoding, "base64", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return contentResponse.content;
|
|
}
|
|
|
|
try
|
|
{
|
|
var normalized = contentResponse.content.Replace("\n", string.Empty).Replace("\r", string.Empty);
|
|
var bytes = Convert.FromBase64String(normalized);
|
|
return Encoding.UTF8.GetString(bytes);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"Decode package json content failed: {e.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|