This commit is contained in:
陈思海 2026-04-17 14:21:41 +08:00
parent 9fb89cde6f
commit ff93b80408
15 changed files with 1096 additions and 492 deletions

View File

@ -1,136 +1,318 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
namespace AlicizaX.PackageManager.Editor
{
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using Newtonsoft.Json.Linq;
internal enum InstalledSource
{
None,
Git,
Embedded,
LocalPath,
Registry,
BuiltIn,
Unknown
}
internal sealed class LocalPackageInfo
{
public string Name;
public string ManifestVersion;
public string LockVersion;
public string ResolvedHash;
public string InstalledVersion;
public InstalledSource Source;
public bool IsInstalled;
}
public static class PackageManagerCheckTool
{
private static Dictionary<string, string> cachedManifestDependencies;
private static ListRequest listRequest;
private const string ManifestPath = "Packages/manifest.json";
private const string PackagesLockPath = "Packages/packages-lock.json";
private static readonly Dictionary<string, string> InstalledPackageVersions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private static Dictionary<string, string> s_manifestDependencies;
private static Dictionary<string, LockDependency> s_lockDependencies;
private static ListRequest s_listRequest;
private static Action<IReadOnlyDictionary<string, string>> s_listCallback;
[Serializable]
public class ManifestDependencies
private sealed class ManifestFile
{
public Dictionary<string, string> dependencies = new Dictionary<string, string>();
}
[Serializable]
private sealed class PackagesLockFile
{
public Dictionary<string, LockDependency> dependencies = new Dictionary<string, LockDependency>();
}
[Serializable]
private sealed class LockDependency
{
public string version;
public string source;
public string hash;
}
public static void ValidatePackageStates(List<RepositoryPackageData> packages)
{
var manifest = ReadManifestFile();
var installedPackages = GetInstalledPackagesFromPM();
if (packages == null)
{
return;
}
RefreshLocalCache();
foreach (var package in packages)
{
// 检查是否在manifest中存在
if (manifest.dependencies.TryGetValue(package.name, out var gitUrl))
{
// 检查PackageCache目录
var cachePath = FindInPackageCache(package.name);
if (!string.IsNullOrEmpty(cachePath))
{
var localTime = Directory.GetLastWriteTime(cachePath);
if (DateTime.TryParse(package.updatedAt, out var remoteTime))
{
package.PackageState = remoteTime > localTime ? PackageState.Update : PackageState.Install;
}
else
{
package.PackageState = PackageState.Install;
}
}
else
{
package.PackageState = PackageState.Install;
}
}
else
{
// 检查Packages目录
var packagePath = Path.GetFullPath($"Packages/{package.name}");
package.PackageState = Directory.Exists(packagePath) ? PackageState.InstallLocal : PackageState.UnInstall;
}
var localPackage = GetLocalPackageInfo(package.name);
ApplyLocalState(package, localPackage);
}
}
private static string FindInPackageCache(string packageName)
public static void RefreshInstalledPackages(Action<IReadOnlyDictionary<string, string>> callback)
{
var cacheRoot = Path.GetFullPath("Library/PackageCache");
if (!Directory.Exists(cacheRoot)) return null;
return Path.Combine(cacheRoot, packageName);
}
private static ManifestDependencies ReadManifestFile()
{
var manifestPath = Path.GetFullPath("Packages/manifest.json");
var json = File.ReadAllText(manifestPath);
return JsonConvert.DeserializeObject<ManifestDependencies>(json);
}
public static void InstallPackage(string gitUrl)
{
Client.Add(gitUrl);
}
public static void UpdatePackage(string packageName)
{
Client.Add($"git:{packageName}");
}
public static void UninstallPackage(string packageName)
{
Client.Remove(packageName);
}
private static List<string> GetInstalledPackagesFromPM()
{
var packages = new List<string>();
listRequest = Client.List();
EditorApplication.CallbackFunction checkProgress = null;
checkProgress = () =>
if (s_listRequest != null && !s_listRequest.IsCompleted)
{
if (listRequest.IsCompleted)
{
if (listRequest.Status == StatusCode.Success)
{
foreach (var package in listRequest.Result)
{
packages.Add(package.name);
}
}
return;
}
EditorApplication.update -= checkProgress;
}
s_listCallback = callback;
s_listRequest = Client.List(true, false);
EditorApplication.update -= OnListRequestProgress;
EditorApplication.update += OnListRequestProgress;
}
public static void InstallPackage(string packageIdentifier, Action<bool, string> callback = null)
{
PackageOperationRunner.RunAdd(packageIdentifier, callback);
}
public static void UpdatePackage(RepositoryPackageData package, Action<bool, string> callback = null)
{
if (package == null)
{
callback?.Invoke(false, "Package data is null.");
return;
}
PackageOperationRunner.RunAdd(package.cloneUrl, callback);
}
public static void UninstallPackage(string packageName, Action<bool, string> callback = null)
{
PackageOperationRunner.RunRemove(packageName, callback);
}
internal static LocalPackageInfo GetLocalPackageInfo(string packageName)
{
RefreshLocalCache();
var info = new LocalPackageInfo
{
Name = packageName,
Source = InstalledSource.None
};
EditorApplication.update += checkProgress;
return packages;
if (string.IsNullOrEmpty(packageName))
{
return info;
}
if (s_manifestDependencies != null && s_manifestDependencies.TryGetValue(packageName, out var manifestVersion))
{
info.ManifestVersion = manifestVersion;
}
if (s_lockDependencies != null && s_lockDependencies.TryGetValue(packageName, out var lockDependency))
{
info.LockVersion = lockDependency.version;
info.ResolvedHash = lockDependency.hash;
info.Source = ParseSource(lockDependency.source);
info.IsInstalled = info.Source != InstalledSource.None;
}
if (InstalledPackageVersions.TryGetValue(packageName, out var installedVersion))
{
info.InstalledVersion = installedVersion;
info.IsInstalled = true;
}
if (info.Source == InstalledSource.None && !string.IsNullOrEmpty(info.ManifestVersion))
{
info.Source = GuessSourceFromManifest(info.ManifestVersion);
info.IsInstalled = info.Source == InstalledSource.Embedded || info.Source == InstalledSource.LocalPath;
}
return info;
}
// 辅助方法从Git URL提取包名
public static string ExtractPackageNameFromUrl(string gitUrl)
public static bool HasInstalledPackageVersion(string packageName, out string version)
{
return InstalledPackageVersions.TryGetValue(packageName, out version);
}
private static void OnListRequestProgress()
{
if (s_listRequest == null || !s_listRequest.IsCompleted)
{
return;
}
EditorApplication.update -= OnListRequestProgress;
InstalledPackageVersions.Clear();
if (s_listRequest.Status == StatusCode.Success)
{
foreach (var package in s_listRequest.Result)
{
InstalledPackageVersions[package.name] = package.version;
}
}
var callback = s_listCallback;
s_listCallback = null;
s_listRequest = null;
callback?.Invoke(InstalledPackageVersions);
}
private static void RefreshLocalCache()
{
s_manifestDependencies = ReadManifestDependencies();
s_lockDependencies = ReadPackagesLockDependencies();
}
private static void ApplyLocalState(RepositoryPackageData package, LocalPackageInfo localPackage)
{
package.ManifestVersion = localPackage.ManifestVersion;
package.InstalledVersion = !string.IsNullOrEmpty(localPackage.InstalledVersion)
? localPackage.InstalledVersion
: localPackage.LockVersion;
package.InstalledHash = localPackage.ResolvedHash;
package.InstalledSource = localPackage.Source.ToString();
switch (localPackage.Source)
{
case InstalledSource.Embedded:
case InstalledSource.LocalPath:
package.PackageState = PackageState.InstallLocal;
break;
case InstalledSource.Git:
package.PackageState = HasRemoteUpdate(package, localPackage)
? PackageState.Update
: PackageState.Install;
break;
case InstalledSource.Registry:
case InstalledSource.BuiltIn:
case InstalledSource.Unknown:
package.PackageState = localPackage.IsInstalled ? PackageState.Install : PackageState.UnInstall;
break;
default:
package.PackageState = localPackage.IsInstalled ? PackageState.Install : PackageState.UnInstall;
break;
}
}
private static bool HasRemoteUpdate(RepositoryPackageData package, LocalPackageInfo localPackage)
{
if (string.IsNullOrEmpty(localPackage.ResolvedHash) || string.IsNullOrEmpty(package.remoteHead))
{
return false;
}
return !string.Equals(localPackage.ResolvedHash, package.remoteHead, StringComparison.OrdinalIgnoreCase);
}
private static Dictionary<string, string> ReadManifestDependencies()
{
var path = Path.GetFullPath(ManifestPath);
if (!File.Exists(path))
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
try
{
Uri uri = new Uri(gitUrl);
string path = uri.AbsolutePath;
int startIndex = path.LastIndexOf('/') + 1;
string name = path.Substring(startIndex);
return name.Replace(".git", "").Replace("_", ".").ToLower();
var json = File.ReadAllText(path);
var manifest = JsonConvert.DeserializeObject<ManifestFile>(json);
return manifest?.dependencies != null
? new Dictionary<string, string>(manifest.dependencies, StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
catch
catch (Exception e)
{
return Path.GetFileNameWithoutExtension(gitUrl);
UnityEngine.Debug.LogError($"Read manifest failed: {e.Message}");
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
private static Dictionary<string, LockDependency> ReadPackagesLockDependencies()
{
var path = Path.GetFullPath(PackagesLockPath);
if (!File.Exists(path))
{
return new Dictionary<string, LockDependency>(StringComparer.OrdinalIgnoreCase);
}
try
{
var json = File.ReadAllText(path);
var packagesLock = JsonConvert.DeserializeObject<PackagesLockFile>(json);
return packagesLock?.dependencies != null
? new Dictionary<string, LockDependency>(packagesLock.dependencies, StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, LockDependency>(StringComparer.OrdinalIgnoreCase);
}
catch (Exception e)
{
UnityEngine.Debug.LogError($"Read packages-lock failed: {e.Message}");
return new Dictionary<string, LockDependency>(StringComparer.OrdinalIgnoreCase);
}
}
private static InstalledSource ParseSource(string source)
{
return source switch
{
"git" => InstalledSource.Git,
"embedded" => InstalledSource.Embedded,
"local" => InstalledSource.LocalPath,
"registry" => InstalledSource.Registry,
"builtin" => InstalledSource.BuiltIn,
_ => string.IsNullOrEmpty(source) ? InstalledSource.None : InstalledSource.Unknown
};
}
private static InstalledSource GuessSourceFromManifest(string manifestVersion)
{
if (string.IsNullOrEmpty(manifestVersion))
{
return InstalledSource.None;
}
if (manifestVersion.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
{
return InstalledSource.LocalPath;
}
if (manifestVersion.EndsWith(".git", StringComparison.OrdinalIgnoreCase) ||
manifestVersion.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
manifestVersion.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
manifestVersion.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase) ||
manifestVersion.StartsWith("git@", StringComparison.OrdinalIgnoreCase))
{
return InstalledSource.Git;
}
return InstalledSource.Registry;
}
}
}

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 491930ef63864219a4944a44ed820823
timeCreated: 1740016880
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,97 @@
namespace AlicizaX.PackageManager.Editor
{
using System;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
internal static class PackageOperationRunner
{
private static AddRequest s_addRequest;
private static RemoveRequest s_removeRequest;
private static Action<bool, string> s_callback;
private static string s_operationName;
public static bool IsBusy => s_addRequest != null || s_removeRequest != null;
public static void RunAdd(string packageIdentifier, Action<bool, string> callback)
{
if (IsBusy)
{
callback?.Invoke(false, "Another package operation is in progress.");
return;
}
s_operationName = $"Add {packageIdentifier}";
s_callback = callback;
s_addRequest = Client.Add(packageIdentifier);
EditorApplication.update -= Poll;
EditorApplication.update += Poll;
}
public static void RunRemove(string packageName, Action<bool, string> callback)
{
if (IsBusy)
{
callback?.Invoke(false, "Another package operation is in progress.");
return;
}
s_operationName = $"Remove {packageName}";
s_callback = callback;
s_removeRequest = Client.Remove(packageName);
EditorApplication.update -= Poll;
EditorApplication.update += Poll;
}
private static void Poll()
{
if (s_addRequest != null)
{
if (!s_addRequest.IsCompleted)
{
return;
}
var success = s_addRequest.Status == StatusCode.Success;
var message = success ? s_addRequest.Result?.name : s_addRequest.Error?.message;
s_addRequest = null;
Complete(success, message);
return;
}
if (s_removeRequest != null)
{
if (!s_removeRequest.IsCompleted)
{
return;
}
var success = s_removeRequest.Status == StatusCode.Success;
var message = success ? s_removeRequest.PackageIdOrName : s_removeRequest.Error?.message;
s_removeRequest = null;
Complete(success, message);
return;
}
EditorApplication.update -= Poll;
}
private static void Complete(bool success, string message)
{
EditorApplication.update -= Poll;
var callback = s_callback;
var operationName = s_operationName;
s_callback = null;
s_operationName = null;
if (!success)
{
UnityEngine.Debug.LogError($"{operationName} failed: {message}");
}
callback?.Invoke(success, message);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 049bb98ebc317284ab6077f713e50236
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,11 +1,11 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System.Text;
using Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
namespace AlicizaX.PackageManager.Editor
{
@ -20,20 +20,28 @@ namespace AlicizaX.PackageManager.Editor
[Serializable]
public class RepositoryPackageData
{
// Repository info
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;
// Package info
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;
}
@ -52,6 +60,8 @@ namespace AlicizaX.PackageManager.Editor
public string clone_url;
public string html_url;
public string updated_at;
public string default_branch;
public string url;
public Owner owner;
[Serializable]
@ -61,6 +71,26 @@ namespace AlicizaX.PackageManager.Editor
}
}
[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
{
@ -79,119 +109,194 @@ namespace AlicizaX.PackageManager.Editor
}
}
internal class PackageManagerCoroutines : MonoBehaviour
internal sealed class PackageManagerCoroutines : MonoBehaviour
{
public static PackageManagerCoroutines Coroutines;
public static void CreateCoroutines()
{
if (Coroutines == null)
if (Coroutines != null)
{
Coroutines = new GameObject("Coroutines").AddComponent<PackageManagerCoroutines>();
return;
}
var gameObject = new GameObject("PackageManagerCoroutines");
gameObject.hideFlags = HideFlags.HideAndDontSave;
Coroutines = gameObject.AddComponent<PackageManagerCoroutines>();
}
public static void DestroyCoroutines()
{
if (Coroutines != null)
if (Coroutines == null)
{
Coroutines.StopAllCoroutines();
DestroyImmediate(Coroutines.gameObject);
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 PackageJsonPath = "/raw/branch/main/package.json";
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();
EditorPrefs.SetString("PackageUpdateDate", DateTime.Now.ToString());
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 (!HandleRequestError(request, "Repository request failed"))
if (!TryValidateRequest(request, "Repository request failed"))
{
callback?.Invoke(null);
yield break;
fatalError = new Exception(request.error);
}
var repoResponse = JsonConvert.DeserializeObject<RepoApiResponse>(request.downloadHandler.text);
if (!repoResponse.ok || repoResponse.data == null)
else
{
Debug.LogError("Invalid repository response data");
callback?.Invoke(null);
yield break;
}
var results = new List<RepositoryPackageData>();
var pendingRequests = new Queue<RepoItem>(repoResponse.data);
// 第二阶段逐个获取package.json信息
while (pendingRequests.Count > 0)
{
var repo = pendingRequests.Dequeue();
var packageData = CreateBaseRepositoryData(repo);
var packageUrl = repo.html_url + PackageJsonPath;
using (var packageRequest = CreateWebRequest(packageUrl))
RepoApiResponse response = null;
try
{
yield return packageRequest.SendWebRequest();
response = JsonConvert.DeserializeObject<RepoApiResponse>(request.downloadHandler.text);
}
catch (Exception e)
{
fatalError = e;
}
if (HandleRequestError(packageRequest, $"Package request failed for {repo.name}"))
if (fatalError == null)
{
if (response?.ok != true || response.data == null)
{
try
fatalError = new Exception("Repository response is invalid.");
}
else
{
results = new List<RepositoryPackageData>(response.data.Count);
foreach (var repo in response.data)
{
var packageInfo = JsonConvert.DeserializeObject<PackageJsonResponse>(
packageRequest.downloadHandler.text);
UpdateWithPackageInfo(ref packageData, packageInfo);
}
catch (Exception e)
{
Debug.LogError($"JSON parse error for {repo.name}: {e.Message}");
var data = CreateBaseRepositoryData(repo);
yield return FetchBranchHeadRoutine(data);
yield return FetchPackageInfoRoutine(data);
results.Add(data);
}
}
results.Add(packageData);
}
}
}
callback?.Invoke(results);
PackageManagerCoroutines.DestroyCoroutines();
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 = new UnityWebRequest(url, "GET");
var request = UnityWebRequest.Get(url);
request.downloadHandler = new DownloadHandlerBuffer();
request.timeout = 20;
request.SetRequestHeader("accept", "application/json");
return request;
}
private static bool HandleRequestError(UnityWebRequest request, string errorMessage)
private static bool TryValidateRequest(UnityWebRequest request, string errorMessage)
{
if (request.result != UnityWebRequest.Result.Success)
if (request.result == UnityWebRequest.Result.Success)
{
Debug.LogError($"{errorMessage}: {request.error}");
return false;
return true;
}
return true;
Debug.LogError($"{errorMessage}: {request.error}");
return false;
}
private static RepositoryPackageData CreateBaseRepositoryData(RepoItem repo)
@ -202,23 +307,51 @@ namespace AlicizaX.PackageManager.Editor
description = repo.description,
cloneUrl = repo.clone_url,
htmlUrl = repo.html_url,
updatedAt = repo.updated_at
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(ref RepositoryPackageData data, PackageJsonResponse package)
private static void UpdateWithPackageInfo(RepositoryPackageData data, PackageJsonResponse package)
{
if (package == null) return;
data.name = package.name;
data.displayName = package.displayName;
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;
}
if (package.author != null)
private static string DecodeContent(PackageJsonContentResponse contentResponse)
{
if (contentResponse == null || string.IsNullOrEmpty(contentResponse.content))
{
data.authorName = package.author.name;
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;
}
}
}

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
guid: 311855e386934def95705701938ac3f5
timeCreated: 1739878350
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,116 +1,144 @@
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace AlicizaX.PackageManager.Editor
{
/// <summary>
/// 更新包帮助类
/// </summary>
internal static class UpdateAllPackageHelper
{
private static AddRequest _addRequest;
private static readonly Queue<RepositoryPackageData> PackagesToUpdate = new Queue<RepositoryPackageData>();
private static int _allPackagesCount = 0;
private static int _updatingPackagesIndex = 0;
private static FrameworkPackageManagerWindow s_window;
private static int s_totalCount;
private static int s_currentIndex;
public static void UpdatePackages()
public static void UpdatePackages(FrameworkPackageManagerWindow window)
{
var result = EditorUtility.DisplayDialog("更新包提示", "是否更新所有包?\n 更新完成之后可能需要重启Unity", "是", "否");
if (result)
if (window == null)
{
UpdatePackagesFromManifest(FrameworkPackageManagerWindow.Instance.RepositoryPackageDatas);
return;
}
}
/// <summary>
/// 更新指定包
/// </summary>
/// <param name="packageName">包名</param>
/// <param name="packageUrl">包地址</param>
public static void UpdatePackages(RepositoryPackageData repositoryPackageData)
{
PackagesToUpdate.Enqueue(repositoryPackageData);
}
private static void UpdatePackagesFromManifest(List<RepositoryPackageData> RepositoryPackageDatas)
{
foreach (var package in RepositoryPackageDatas)
if (PackageOperationRunner.IsBusy)
{
string packageUrl = package.cloneUrl;
if (packageUrl.EndsWith(".git") && package.PackageState == PackageState.Update)
EditorUtility.DisplayDialog("更新包", "当前有其他包操作正在执行。", "确定");
return;
}
var updatablePackages = CollectUpdatablePackages(window.RepositoryPackageDatas);
if (updatablePackages.Count == 0)
{
EditorUtility.DisplayDialog("更新包", "当前没有可更新的包。", "确定");
return;
}
var confirm = EditorUtility.DisplayDialog(
"更新包提示",
$"检测到 {updatablePackages.Count} 个包可更新,是否继续?\n更新完成后建议重新打开 Unity。",
"是",
"否");
if (!confirm)
{
return;
}
s_window = window;
PackagesToUpdate.Clear();
foreach (var package in updatablePackages)
{
PackagesToUpdate.Enqueue(package);
}
s_totalCount = PackagesToUpdate.Count;
s_currentIndex = 0;
UpdateNextPackage();
}
public static void UpdatePackage(FrameworkPackageManagerWindow window, RepositoryPackageData package)
{
if (window == null || package == null)
{
return;
}
if (PackageOperationRunner.IsBusy)
{
EditorUtility.DisplayDialog("更新包", "当前有其他包操作正在执行。", "确定");
return;
}
if (package.PackageState != PackageState.Update)
{
EditorUtility.DisplayDialog("更新包", "当前包没有检测到远端更新。", "确定");
return;
}
s_window = window;
PackagesToUpdate.Clear();
PackagesToUpdate.Enqueue(package);
s_totalCount = 1;
s_currentIndex = 0;
UpdateNextPackage();
}
private static List<RepositoryPackageData> CollectUpdatablePackages(List<RepositoryPackageData> repositoryPackageDatas)
{
var result = new List<RepositoryPackageData>();
if (repositoryPackageDatas == null)
{
return result;
}
foreach (var package in repositoryPackageDatas)
{
if (package == null || package.PackageState != PackageState.Update)
{
UpdatePackages(package);
continue;
}
if (string.IsNullOrEmpty(package.cloneUrl))
{
continue;
}
result.Add(package);
}
StartUpdate();
}
/// <summary>
/// 开始更新
/// </summary>
public static void StartUpdate()
{
_allPackagesCount = PackagesToUpdate.Count;
_updatingPackagesIndex = 0;
if (PackagesToUpdate.Count > 0)
{
UpdateNextPackage();
}
else
{
UnityEngine.Debug.Log("No packages to update.");
}
return result;
}
private static void UpdateNextPackage()
{
if (PackagesToUpdate.Count > 0)
{
_updatingPackagesIndex++;
var repositoryPackageData = PackagesToUpdate.Dequeue();
_addRequest = Client.Add(repositoryPackageData.cloneUrl);
EditorApplication.update += UpdatingProgressHandler;
var isCancelableProgressBar = EditorUtility.DisplayCancelableProgressBar("正在更新包", $"{_updatingPackagesIndex}/{_allPackagesCount} ({repositoryPackageData.name})", (float)_updatingPackagesIndex / _allPackagesCount);
if (isCancelableProgressBar)
{
EditorUtility.DisplayProgressBar("正在取消更新", "请等待...", 0.5f);
PackagesToUpdate.Clear();
EditorUtility.ClearProgressBar();
EditorApplication.update -= UpdatingProgressHandler;
AssetDatabase.Refresh();
}
}
else
if (PackagesToUpdate.Count == 0)
{
EditorUtility.ClearProgressBar();
UnityEngine.Debug.Log("All packages updated.");
AssetDatabase.Refresh();
s_window?.Refresh();
return;
}
}
private static void UpdatingProgressHandler()
{
if (_addRequest.IsCompleted)
s_currentIndex++;
var package = PackagesToUpdate.Dequeue();
var canceled = EditorUtility.DisplayCancelableProgressBar(
"正在更新包",
$"{s_currentIndex}/{s_totalCount} {package.name}",
(float)s_currentIndex / s_totalCount);
if (canceled)
{
if (_addRequest.Status == StatusCode.Success)
PackagesToUpdate.Clear();
EditorUtility.ClearProgressBar();
return;
}
PackageManagerCheckTool.UpdatePackage(package, (success, _) =>
{
if (!success)
{
FrameworkPackageManagerWindow.Instance.RefreshPackage(_addRequest.Result.name);
UnityEngine.Debug.Log($"Updated package: {_addRequest.Result.name}");
}
else if (_addRequest.Status >= StatusCode.Failure)
{
UnityEngine.Debug.LogError($"Failed to update package: {_addRequest.Error.message}");
EditorUtility.ClearProgressBar();
return;
}
EditorApplication.update -= UpdatingProgressHandler;
UpdateNextPackage();
}
});
}
}
}

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: f5dac61f7c2046f1a7dcd221b36c67cb
timeCreated: 1740027829
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,6 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
@ -12,90 +11,142 @@ namespace AlicizaX.PackageManager.Editor
internal class FrameworkPackageManagerWindow : EditorWindow
{
private const string VisualAssetPath = "Packages/com.alicizax.unity.packagemanager/Editor/PackageManager/Res/AlicizaXPackageManagerWindow.uxml";
public List<RepositoryPackageData> RepositoryPackageDatas = new();
public static FrameworkPackageManagerWindow Instance;
public List<RepositoryPackageData> RepositoryPackageDatas { get; private set; } = new List<RepositoryPackageData>();
private PackageListView packageListView;
private PackageInfoView packageInfoView;
private PackageBottomStateBar bottomStateBar;
private bool isRefreshing;
public FrameworkPackageManagerWindow()
{
Instance = this;
}
public void RefreshPackage(string packageName)
{
var package = RepositoryPackageDatas.Find(t => t.name.Equals(packageName));
if (package != null)
{
packageListView.itemsSource = RepositoryPackageDatas;
packageListView.Rebuild();
}
}
private void RefreshPackageData(Action callBack)
{
RepositoryDataFetcher.FetchRepositoryData((datas) =>
{
PackageManagerCheckTool.ValidatePackageStates(datas);
RepositoryPackageDatas = datas;
callBack?.Invoke();
});
}
[MenuItem("Tools/AlicizaX/PackageManager")]
[MenuItem("AlicizaX/PackageManager")]
internal static void Open()
{
GetWindow<FrameworkPackageManagerWindow>().Show();
}
public void Refresh()
{
if (isRefreshing)
{
return;
}
isRefreshing = true;
SetRefreshState("Refreshing packages...");
RepositoryDataFetcher.FetchRepositoryData(datas =>
{
RepositoryPackageDatas = datas ?? new List<RepositoryPackageData>();
PackageManagerCheckTool.RefreshInstalledPackages(_ =>
{
PackageManagerCheckTool.ValidatePackageStates(RepositoryPackageDatas);
RebuildList();
isRefreshing = false;
SetRefreshState($"Last refresh: {RepositoryDataFetcher.GetLastRefreshTime()}");
});
});
}
public void RefreshPackage(string packageName)
{
if (string.IsNullOrEmpty(packageName))
{
return;
}
var package = RepositoryPackageDatas.FirstOrDefault(t => string.Equals(t.name, packageName, StringComparison.OrdinalIgnoreCase));
if (package == null)
{
return;
}
PackageManagerCheckTool.ValidatePackageStates(new List<RepositoryPackageData> { package });
RebuildList();
if (packageListView.selectedItem == package)
{
packageInfoView.RefreshView(package);
}
}
public bool IsBusy()
{
return isRefreshing || PackageOperationRunner.IsBusy;
}
private void OnDestroy()
{
if (Instance == this)
{
Instance = null;
}
PackageManagerCoroutines.DestroyCoroutines();
}
private void CreateGUI()
{
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(VisualAssetPath);
visualTree.CloneTree(rootVisualElement);
CreateLeftList();
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(VisualAssetPath);
visualTree?.CloneTree(rootVisualElement);
CreateLayout();
Refresh();
}
private void Refresh()
{
RefreshPackageData(() =>
{
packageListView.itemsSource = RepositoryPackageDatas;
packageListView.Rebuild();
});
}
private PackageListView packageListView;
private PackageInfoView packageInfoView;
private void CreateLeftList()
private void CreateLayout()
{
var leftContainer = rootVisualElement.Q<VisualElement>("LeftContainer");
leftContainer.style.backgroundColor = new Color(0.1686275f, 0.1606275f, 0.1686275f);
packageListView = new PackageListView();
packageListView.OnSelectionChangedEvent += OnPackageSelectionChanged;
leftContainer.Add(packageListView);
packageListView.OnSelectionChangedEvent += PackageListViewOnOnSelectionChangedEvent;
leftContainer.Add(new PackageBottomStateBar());
bottomStateBar = new PackageBottomStateBar(this);
leftContainer.Add(bottomStateBar);
var rightContainer = rootVisualElement.Q<VisualElement>("RightContainer");
packageInfoView = new PackageInfoView();
packageInfoView = new PackageInfoView(this);
rightContainer.Add(packageInfoView);
packageListView.itemsSource = RepositoryPackageDatas;
Refresh();
}
private void PackageListViewOnOnSelectionChangedEvent(IEnumerable<RepositoryPackageData> obj)
private void RebuildList()
{
packageInfoView.RefreshView(obj.First());
packageListView.itemsSource = RepositoryPackageDatas;
packageListView.Rebuild();
if (RepositoryPackageDatas.Count > 0 && packageListView.selectedIndex < 0)
{
packageListView.SetSelection(0);
packageInfoView.RefreshView(RepositoryPackageDatas[0]);
}
else if (packageListView.selectedItem is RepositoryPackageData selected)
{
packageInfoView.RefreshView(selected);
}
else
{
packageInfoView.RefreshView(null);
}
}
private void OnPackageSelectionChanged(IEnumerable<RepositoryPackageData> selectedItems)
{
packageInfoView.RefreshView(selectedItems.FirstOrDefault());
}
private void SetRefreshState(string text)
{
bottomStateBar?.SetStateText(text);
}
}
}

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
guid: 29f884ddb4ba425fbb082406fe4da3cf
timeCreated: 1739944505
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,4 +1,4 @@
using UnityEditor;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
@ -6,40 +6,82 @@ namespace AlicizaX.PackageManager.Editor
{
internal class PackageBottomStateBar : VisualElement
{
private readonly Label stateLabel;
private readonly Button btnRefresh;
private readonly FrameworkPackageManagerWindow window;
private readonly Button refreshButton;
private readonly Button updateAllButton;
private readonly VisualElement spacer;
public PackageBottomStateBar()
public PackageBottomStateBar(FrameworkPackageManagerWindow window)
{
this.window = window;
style.flexDirection = FlexDirection.Row;
style.flexShrink = 1;
style.justifyContent = Justify.FlexStart;
style.justifyContent = Justify.FlexEnd;
style.alignItems = Align.Center;
style.height = Length.Percent(5);
style.backgroundColor = new Color(0.2509804f, 0.2509804f, 0.2509804f);
style.borderTopWidth = 1;
style.paddingLeft = 4;
style.paddingRight = 4;
stateLabel = new Label();
stateLabel.name = "stateLabel";
spacer = new VisualElement();
spacer.style.flexGrow = 1;
Add(spacer);
stateLabel.text = EditorPrefs.GetString("PackageUpdateDate", "");
stateLabel.style.alignSelf = Align.Center;
stateLabel.style.marginLeft = new Length(2, LengthUnit.Pixel);
stateLabel.style.flexGrow = 1; // 让 Label 占据剩余空间
Add(stateLabel);
refreshButton = CreateIconButton("btnRefresh", "d_Refresh", "Refresh", OnRefreshClicked);
Add(refreshButton);
btnRefresh = new Button(OnBtnRefreshClick);
btnRefresh.name = "btnRefresh";
btnRefresh.style.maxHeight = 30;
Image icon = new Image();
icon.image = EditorGUIUtility.IconContent("d_Refresh").image;
btnRefresh.Add(icon);
Add(btnRefresh);
updateAllButton = CreateIconButton("btnUpdateAll", "Update-Available", "Update All", OnUpdateAllClicked);
updateAllButton.style.marginLeft = 6;
Add(updateAllButton);
}
private void OnBtnRefreshClick()
public void SetStateText(string text)
{
UpdateAllPackageHelper.UpdatePackages();
}
private static Button CreateIconButton(string name, string iconName, string tooltip, System.Action clickAction)
{
var button = new Button(clickAction)
{
name = name,
tooltip = tooltip
};
button.style.width = 26;
button.style.height = 22;
button.style.paddingLeft = 0;
button.style.paddingRight = 0;
button.style.paddingTop = 0;
button.style.paddingBottom = 0;
button.style.unityTextAlign = TextAnchor.MiddleCenter;
var icon = new Image
{
image = EditorGUIUtility.IconContent(iconName).image
};
icon.style.width = 16;
icon.style.height = 16;
icon.style.alignSelf = Align.Center;
button.Add(icon);
return button;
}
private void OnRefreshClicked()
{
if (window.IsBusy())
{
return;
}
window.Refresh();
}
private void OnUpdateAllClicked()
{
UpdateAllPackageHelper.UpdatePackages(window);
}
}
}

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: ef4c9b64d076487cae6a6e58ab137fb4
timeCreated: 1739950003
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -25,7 +25,7 @@ namespace AlicizaX.PackageManager.Editor
public void RefreshDataSource(RepositoryPackageData packageData)
{
_repositoryPackageData = packageData;
m_NameLabel.text = packageData.displayName;
m_NameLabel.text = string.IsNullOrEmpty(packageData.displayName) ? packageData.name : packageData.displayName;
if (packageData.PackageState == PackageState.Update)
{
@ -40,7 +40,18 @@ namespace AlicizaX.PackageManager.Editor
m_StateIcon.image = EditorGUIUtility.IconContent("d_CreateAddNew").image;
}
m_VersionLabel.text = packageData.PackageState == PackageState.InstallLocal ? "[Local]" : packageData.version;
if (packageData.PackageState == PackageState.InstallLocal)
{
m_VersionLabel.text = "[Local]";
}
else if (packageData.PackageState == PackageState.Update)
{
m_VersionLabel.text = $"{packageData.version} -> {ShortHash(packageData.remoteHead)}";
}
else
{
m_VersionLabel.text = packageData.version;
}
}
private void BuildMainItem()
@ -112,5 +123,15 @@ namespace AlicizaX.PackageManager.Editor
: Color.clear;
}
}
private static string ShortHash(string hash)
{
if (string.IsNullOrEmpty(hash))
{
return "HEAD";
}
return hash.Length <= 6 ? hash : hash.Substring(0, 6);
}
}
}

View File

@ -1,208 +1,199 @@
using UnityEngine;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace AlicizaX.PackageManager.Editor
{
internal class PackageInfoView : VisualElement
{
private VisualElement _mainContainer;
private VisualElement _headContainer;
private Label _displayName;
private Label _versionWithDate;
private Label _authorLabel;
private Label _packageNameLabel;
private Label _projectDescription;
private Label _btnLinkHtml;
private readonly FrameworkPackageManagerWindow window;
private VisualElement mainContainer;
private Label displayNameLabel;
private Label versionLabel;
private Label authorLabel;
private Label packageNameLabel;
private Label descriptionLabel;
private Label sourceLinkLabel;
private Label installedStateLabel;
private VisualElement packageBtnContainer;
private Button updateButton;
private Button installButton;
private Button removeButton;
private RepositoryPackageData selectedPackage;
private Button _btnUpdate;
private Button _btnInstall;
private Button _btnRemove;
private RepositoryPackageData _selectRepositoryPackageData;
public PackageInfoView()
public PackageInfoView(FrameworkPackageManagerWindow window)
{
this.window = window;
style.flexShrink = 1;
style.height = Length.Percent(100);
_mainContainer = new VisualElement()
{
name = "MainContainer"
};
_mainContainer.style.flexShrink = 1;
_mainContainer.style.height = Length.Percent(100);
mainContainer = new VisualElement { name = "MainContainer" };
mainContainer.style.flexShrink = 1;
mainContainer.style.height = Length.Percent(100);
mainContainer.style.paddingLeft = 10;
mainContainer.style.paddingTop = 10;
displayNameLabel = CreateLabel("DisplayName", 19, FontStyle.Bold);
versionLabel = CreateLabel("VersionWithDate", 13, FontStyle.Bold);
authorLabel = CreateLabel("AuthorLabel", 12, FontStyle.Italic);
packageNameLabel = CreateLabel("PackageName", 12, FontStyle.Italic);
installedStateLabel = CreateLabel("InstalledState", 12, FontStyle.Normal);
descriptionLabel = CreateLabel("ProjectDescription", 12, FontStyle.Normal);
_headContainer = new VisualElement()
{
name = "HeadContainer"
};
_headContainer.style.flexShrink = 1;
_headContainer.style.height = Length.Percent(35);
_headContainer.style.paddingLeft = 10;
_headContainer.style.paddingTop = 10;
sourceLinkLabel = CreateLabel("BtnLink", 12, FontStyle.Normal);
sourceLinkLabel.text = "Source Code";
sourceLinkLabel.style.color = new Color(0.2980392f, 0.498f, 1f);
sourceLinkLabel.RegisterCallback<ClickEvent>(OnSourceClick);
_mainContainer.Add(_headContainer);
var buttonContainer = new VisualElement { name = "PackageBtnContainer" };
buttonContainer.style.flexDirection = FlexDirection.Row;
buttonContainer.style.paddingTop = 10;
installButton = CreateButton("BtnInstall", "Install", OnInstallClick);
updateButton = CreateButton("BtnUpdate", "Update", OnUpdateClick);
removeButton = CreateButton("BtnRemove", "Remove", OnRemoveClick);
_displayName = new Label()
{
name = "DisplayName"
};
_displayName.style.fontSize = 19;
_displayName.style.unityFontStyleAndWeight = FontStyle.Bold;
buttonContainer.Add(installButton);
buttonContainer.Add(updateButton);
buttonContainer.Add(removeButton);
_versionWithDate = new Label()
{
name = "VersionWithDate"
};
_versionWithDate.style.fontSize = 13;
_versionWithDate.style.unityFontStyleAndWeight = FontStyle.Bold;
_versionWithDate.style.paddingTop = 5;
mainContainer.Add(displayNameLabel);
mainContainer.Add(versionLabel);
mainContainer.Add(authorLabel);
mainContainer.Add(packageNameLabel);
mainContainer.Add(installedStateLabel);
mainContainer.Add(descriptionLabel);
mainContainer.Add(sourceLinkLabel);
mainContainer.Add(buttonContainer);
_authorLabel = new Label()
{
name = "AuthorLabel"
};
_authorLabel.style.fontSize = 12;
_authorLabel.style.paddingTop = 5;
_authorLabel.style.unityFontStyleAndWeight = FontStyle.Italic;
_packageNameLabel = new Label()
{
name = "PackageName"
};
_packageNameLabel.style.fontSize = 12;
_packageNameLabel.style.paddingTop = 5;
_packageNameLabel.style.unityFontStyleAndWeight = FontStyle.Italic;
_projectDescription = new Label()
{
name = "ProjectDescription"
};
_projectDescription.style.fontSize = 12;
_projectDescription.style.paddingTop = 5;
_btnLinkHtml = new Label()
{
name = "BtnLink"
};
_btnLinkHtml.text = "Source Code";
_btnLinkHtml.style.fontSize = 12;
_btnLinkHtml.style.paddingTop = 5;
_btnLinkHtml.style.marginLeft = 0;
_btnLinkHtml.style.paddingLeft = 0;
_btnLinkHtml.style.flexGrow = 0;
_btnLinkHtml.style.minWidth = new Length(20, LengthUnit.Pixel);
_btnLinkHtml.style.maxWidth = new Length(300, LengthUnit.Pixel);
_btnLinkHtml.style.color = new Color(0.2980392f, 0.498f, 1f);
_btnLinkHtml.style.backgroundColor = Color.clear;
_btnLinkHtml.RegisterCallback<ClickEvent>(OnBtnLinkClick);
packageBtnContainer = new VisualElement()
{
name = "PackageBtnContainer"
};
packageBtnContainer.style.flexShrink = 1;
packageBtnContainer.style.height = Length.Percent(100);
packageBtnContainer.style.flexDirection = FlexDirection.Row;
packageBtnContainer.style.paddingTop = 10;
_headContainer.Add(_displayName);
_headContainer.Add(_versionWithDate);
_headContainer.Add(_authorLabel);
_headContainer.Add(_packageNameLabel);
_headContainer.Add(_projectDescription);
_headContainer.Add(_btnLinkHtml);
_headContainer.Add(packageBtnContainer);
_btnUpdate = new Button(OnBtnUpdate)
{
name = "BtnUpdate"
};
_btnUpdate.text = "Update";
_btnUpdate.style.width = new Length(90, LengthUnit.Pixel);
_btnUpdate.style.height = new Length(24, LengthUnit.Pixel);
_btnUpdate.style.marginLeft = 0;
_btnInstall = new Button(OnBtnInstall)
{
name = "BtnInstall"
};
_btnInstall.text = "Install";
_btnInstall.style.width = new Length(90, LengthUnit.Pixel);
_btnInstall.style.height = new Length(24, LengthUnit.Pixel);
_btnInstall.style.marginLeft = 0;
_btnRemove = new Button(OnBtnRemove)
{
name = "BtnRemove"
};
_btnRemove.text = "Remove";
_btnRemove.style.width = new Length(90, LengthUnit.Pixel);
_btnRemove.style.height = new Length(24, LengthUnit.Pixel);
_btnRemove.style.marginLeft = 0;
packageBtnContainer.Add(_btnInstall);
packageBtnContainer.Add(_btnUpdate);
packageBtnContainer.Add(_btnRemove);
Add(_mainContainer);
RefreshView(_selectRepositoryPackageData);
Add(mainContainer);
RefreshView(null);
}
public void RefreshView(RepositoryPackageData repositoryPackageData)
{
selectedPackage = repositoryPackageData;
if (repositoryPackageData == null)
{
_mainContainer.visible = false;
mainContainer.visible = false;
return;
}
_selectRepositoryPackageData = repositoryPackageData;
mainContainer.visible = true;
displayNameLabel.text = string.IsNullOrEmpty(repositoryPackageData.displayName)
? repositoryPackageData.name
: repositoryPackageData.displayName;
versionLabel.text = $"Remote: {repositoryPackageData.version} HEAD: {ShortHash(repositoryPackageData.remoteHead)}";
authorLabel.text = $"Author: {repositoryPackageData.authorName}";
packageNameLabel.text = repositoryPackageData.name;
installedStateLabel.text = BuildInstalledState(repositoryPackageData);
descriptionLabel.text = string.IsNullOrEmpty(repositoryPackageData.packageDescription)
? "Description: N/A"
: repositoryPackageData.packageDescription;
_mainContainer.visible = true;
_displayName.text = repositoryPackageData.displayName;
_versionWithDate.text = $"{repositoryPackageData.version}· {repositoryPackageData.updatedAt}";
_authorLabel.text = $"Author: {repositoryPackageData.authorName}";
_packageNameLabel.text = repositoryPackageData.name;
_projectDescription.text = string.IsNullOrEmpty(repositoryPackageData.description) ? "Description:TODO" : repositoryPackageData.description;
_btnInstall.style.display = repositoryPackageData.PackageState == PackageState.UnInstall ? DisplayStyle.Flex : DisplayStyle.None;
_btnUpdate.style.display = repositoryPackageData.PackageState == PackageState.Update ? DisplayStyle.Flex : DisplayStyle.None;
_btnRemove.style.display = repositoryPackageData.PackageState == PackageState.Update || repositoryPackageData.PackageState == PackageState.Install ? DisplayStyle.Flex : DisplayStyle.None;
installButton.style.display = repositoryPackageData.PackageState == PackageState.UnInstall ? DisplayStyle.Flex : DisplayStyle.None;
updateButton.style.display = repositoryPackageData.PackageState == PackageState.Update ? DisplayStyle.Flex : DisplayStyle.None;
removeButton.style.display = repositoryPackageData.PackageState == PackageState.Install || repositoryPackageData.PackageState == PackageState.Update
? DisplayStyle.Flex
: DisplayStyle.None;
}
private void OnBtnLinkClick(ClickEvent e)
private void OnSourceClick(ClickEvent evt)
{
Application.OpenURL(_selectRepositoryPackageData.htmlUrl);
if (selectedPackage == null || string.IsNullOrEmpty(selectedPackage.htmlUrl))
{
return;
}
Application.OpenURL(selectedPackage.htmlUrl);
}
private void OnBtnUpdate()
private void OnInstallClick()
{
PackageManagerCheckTool.InstallPackage(_selectRepositoryPackageData.cloneUrl);
if (selectedPackage == null)
{
return;
}
PackageManagerCheckTool.InstallPackage(selectedPackage.cloneUrl, (success, _) =>
{
if (success)
{
window.Refresh();
}
});
}
private void OnBtnInstall()
private void OnUpdateClick()
{
PackageManagerCheckTool.InstallPackage(_selectRepositoryPackageData.cloneUrl);
if (selectedPackage == null)
{
return;
}
UpdateAllPackageHelper.UpdatePackage(window, selectedPackage);
}
private void OnBtnRemove()
private void OnRemoveClick()
{
PackageManagerCheckTool.UninstallPackage(_selectRepositoryPackageData.name);
if (selectedPackage == null)
{
return;
}
var confirm = EditorUtility.DisplayDialog("Remove Package", $"Remove {selectedPackage.name}?", "Yes", "No");
if (!confirm)
{
return;
}
PackageManagerCheckTool.UninstallPackage(selectedPackage.name, (success, _) =>
{
if (success)
{
window.Refresh();
}
});
}
private static Label CreateLabel(string name, int fontSize, FontStyle fontStyle)
{
var label = new Label { name = name };
label.style.fontSize = fontSize;
label.style.unityFontStyleAndWeight = fontStyle;
label.style.paddingTop = 5;
return label;
}
private static Button CreateButton(string name, string text, System.Action clickAction)
{
var button = new Button(clickAction) { name = name, text = text };
button.style.width = 90;
button.style.height = 24;
button.style.marginLeft = 0;
button.style.marginRight = 4;
return button;
}
private static string BuildInstalledState(RepositoryPackageData package)
{
var installedVersion = string.IsNullOrEmpty(package.InstalledVersion) ? "N/A" : package.InstalledVersion;
var installedHash = string.IsNullOrEmpty(package.InstalledHash) ? "N/A" : ShortHash(package.InstalledHash);
var source = string.IsNullOrEmpty(package.InstalledSource) ? "None" : package.InstalledSource;
return $"Installed: {installedVersion} Source: {source} Local HEAD: {installedHash}";
}
private static string ShortHash(string hash)
{
if (string.IsNullOrEmpty(hash))
{
return "N/A";
}
return hash.Length <= 8 ? hash : hash.Substring(0, 8);
}
}
}

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
fileFormatVersion: 2
guid: 76b5dc77258547288eeaed68ca86a596
timeCreated: 1739957408
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: