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 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(); } 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> callback) { PackageManagerCoroutines.CreateCoroutines(); PackageManagerCoroutines.Coroutines.StartCoroutine(FetchDataRoutine(callback)); } public static string GetLastRefreshTime() { return EditorPrefs.GetString(LastUpdateEditorPrefsKey, string.Empty); } private static IEnumerator FetchDataRoutine(Action> callback) { List 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(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(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()); } else { EditorPrefs.SetString(LastUpdateEditorPrefsKey, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); callback?.Invoke(results ?? new List()); } 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(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(request.downloadHandler.text); var packageJson = DecodeContent(contentResponse); if (string.IsNullOrEmpty(packageJson)) { yield break; } var packageInfo = JsonConvert.DeserializeObject(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; } } } }