497 lines
19 KiB
C#
497 lines
19 KiB
C#
#if UNITY_EDITOR
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.PackageManager;
|
|
using UnityEditor.PackageManager.Requests;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace ShrinkSDK.Installer
|
|
{
|
|
internal sealed class ShrinkSdkInstallerWindow : EditorWindow
|
|
{
|
|
private readonly Dictionary<string, UnityEditor.PackageManager.PackageInfo> _installed =
|
|
new(StringComparer.Ordinal);
|
|
private ListRequest? _listRequest;
|
|
private bool _resolvePending;
|
|
private double _resolveRequestedAt;
|
|
private bool _listRetryPending;
|
|
private double _nextListRetryAt;
|
|
private int _listRetryCount;
|
|
private HelpBox? _statusBox;
|
|
private Label? _registryState;
|
|
private VisualElement? _page;
|
|
private ToolbarToggle? _bundleTab;
|
|
private ToolbarToggle? _packageTab;
|
|
private string _searchText = string.Empty;
|
|
private bool _showBundles = true;
|
|
|
|
[MenuItem("ShrinkSDK/包管理")]
|
|
private static void Open()
|
|
{
|
|
var window = GetWindow<ShrinkSdkInstallerWindow>("ShrinkSDK 包管理");
|
|
window.minSize = new Vector2(820f, 560f);
|
|
window.Show();
|
|
}
|
|
|
|
public void CreateGUI()
|
|
{
|
|
var root = rootVisualElement;
|
|
root.Clear();
|
|
root.style.flexDirection = FlexDirection.Column;
|
|
root.Add(BuildHeader());
|
|
root.Add(BuildToolbar());
|
|
|
|
_page = new ScrollView(ScrollViewMode.Vertical);
|
|
_page.style.flexGrow = 1f;
|
|
_page.style.paddingLeft = 16f;
|
|
_page.style.paddingRight = 16f;
|
|
_page.style.paddingTop = 12f;
|
|
_page.style.paddingBottom = 16f;
|
|
root.Add(_page);
|
|
|
|
_statusBox = new HelpBox("正在读取已安装包……", HelpBoxMessageType.Info);
|
|
_statusBox.style.marginLeft = 16f;
|
|
_statusBox.style.marginRight = 16f;
|
|
_statusBox.style.marginBottom = 12f;
|
|
root.Add(_statusBox);
|
|
|
|
RenderPage();
|
|
RefreshPackages();
|
|
}
|
|
|
|
private VisualElement BuildHeader()
|
|
{
|
|
var header = new VisualElement();
|
|
header.style.paddingLeft = 16f;
|
|
header.style.paddingRight = 16f;
|
|
header.style.paddingTop = 14f;
|
|
header.style.paddingBottom = 10f;
|
|
header.style.borderBottomWidth = 1f;
|
|
header.style.borderBottomColor = new Color(0.25f, 0.25f, 0.25f);
|
|
header.style.minHeight = 104f;
|
|
|
|
var title = new Label("ShrinkSDK 包管理");
|
|
title.style.fontSize = 20f;
|
|
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
title.style.height = 28f;
|
|
title.style.unityTextAlign = TextAnchor.MiddleLeft;
|
|
header.Add(title);
|
|
|
|
var description = new Label("以 Context 为组合基础,按用途安装固定版本的模块与集成包。");
|
|
description.style.marginTop = 4f;
|
|
description.style.minHeight = 20f;
|
|
description.style.whiteSpace = WhiteSpace.Normal;
|
|
header.Add(description);
|
|
|
|
var registryRow = new VisualElement();
|
|
registryRow.style.flexDirection = FlexDirection.Row;
|
|
registryRow.style.alignItems = Align.Center;
|
|
registryRow.style.marginTop = 10f;
|
|
registryRow.style.minHeight = 22f;
|
|
registryRow.Add(new Label("软件源:"));
|
|
|
|
_registryState = new Label();
|
|
_registryState.style.flexGrow = 1f;
|
|
_registryState.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
registryRow.Add(_registryState);
|
|
|
|
var configure = new Button(ConfigureRegistry) { text = "配置软件源" };
|
|
configure.style.width = 110f;
|
|
registryRow.Add(configure);
|
|
header.Add(registryRow);
|
|
|
|
RefreshRegistryState();
|
|
return header;
|
|
}
|
|
|
|
private VisualElement BuildToolbar()
|
|
{
|
|
var toolbar = new Toolbar();
|
|
_bundleTab = new ToolbarToggle { text = "推荐组合", value = true };
|
|
_packageTab = new ToolbarToggle { text = "全部包" };
|
|
_bundleTab.RegisterValueChangedCallback(evt =>
|
|
{
|
|
if (!evt.newValue)
|
|
return;
|
|
_showBundles = true;
|
|
_packageTab?.SetValueWithoutNotify(false);
|
|
RenderPage();
|
|
});
|
|
_packageTab.RegisterValueChangedCallback(evt =>
|
|
{
|
|
if (!evt.newValue)
|
|
return;
|
|
_showBundles = false;
|
|
_bundleTab?.SetValueWithoutNotify(false);
|
|
RenderPage();
|
|
});
|
|
toolbar.Add(_bundleTab);
|
|
toolbar.Add(_packageTab);
|
|
toolbar.Add(new ToolbarSpacer { flex = true });
|
|
|
|
var search = new ToolbarSearchField();
|
|
search.style.width = 230f;
|
|
search.RegisterValueChangedCallback(evt =>
|
|
{
|
|
_searchText = evt.newValue?.Trim() ?? string.Empty;
|
|
RenderPage();
|
|
});
|
|
toolbar.Add(search);
|
|
toolbar.Add(new ToolbarButton(RefreshPackages) { text = "刷新状态" });
|
|
return toolbar;
|
|
}
|
|
|
|
private void RenderPage()
|
|
{
|
|
if (_page == null)
|
|
return;
|
|
_page.Clear();
|
|
if (_showBundles)
|
|
RenderBundles(_page);
|
|
else
|
|
RenderPackages(_page);
|
|
}
|
|
|
|
private void RenderBundles(VisualElement parent)
|
|
{
|
|
foreach (var bundle in ShrinkSdkPackageCatalog.Bundles.Where(MatchesSearch))
|
|
{
|
|
var card = CreateCard();
|
|
var titleRow = new VisualElement();
|
|
titleRow.style.flexDirection = FlexDirection.Row;
|
|
titleRow.style.alignItems = Align.Center;
|
|
var title = new Label(bundle.DisplayName + (bundle.Recommended ? " · 推荐" : string.Empty));
|
|
title.style.fontSize = 15f;
|
|
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
title.style.flexGrow = 1f;
|
|
titleRow.Add(title);
|
|
|
|
var exact = bundle.Packages.Count(IsExactInstalled);
|
|
var button = new Button(() => BeginInstall(bundle.InstallRoots));
|
|
if (exact == bundle.Packages.Count)
|
|
{
|
|
button.text = "已完成";
|
|
button.SetEnabled(false);
|
|
}
|
|
else
|
|
{
|
|
button.text = exact > 0 ? "补齐组合" : "安装组合";
|
|
button.SetEnabled(!IsBusy);
|
|
}
|
|
button.style.width = 100f;
|
|
titleRow.Add(button);
|
|
card.Add(titleRow);
|
|
|
|
var summary = new Label(bundle.Summary);
|
|
summary.style.whiteSpace = WhiteSpace.Normal;
|
|
summary.style.marginTop = 5f;
|
|
card.Add(summary);
|
|
|
|
var progress = new Label($"已就绪 {exact}/{bundle.Packages.Count}");
|
|
progress.style.marginTop = 8f;
|
|
progress.style.color = exact == bundle.Packages.Count
|
|
? new Color(0.35f, 0.75f, 0.45f)
|
|
: new Color(0.65f, 0.65f, 0.65f);
|
|
card.Add(progress);
|
|
|
|
var contents = new Foldout { text = "包含的包", value = false };
|
|
contents.style.marginTop = 4f;
|
|
foreach (var package in bundle.Packages)
|
|
contents.Add(CreateCompactPackageRow(package));
|
|
card.Add(contents);
|
|
parent.Add(card);
|
|
}
|
|
}
|
|
|
|
private void RenderPackages(VisualElement parent)
|
|
{
|
|
var packages = ShrinkSdkPackageCatalog.Packages.Where(MatchesSearch).ToArray();
|
|
foreach (var group in packages.GroupBy(entry => entry.Layer))
|
|
{
|
|
var heading = new Label(GetLayerName(group.Key));
|
|
heading.style.fontSize = 15f;
|
|
heading.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
heading.style.marginTop = 8f;
|
|
heading.style.marginBottom = 4f;
|
|
parent.Add(heading);
|
|
foreach (var package in group)
|
|
parent.Add(CreatePackageCard(package));
|
|
}
|
|
}
|
|
|
|
private VisualElement CreatePackageCard(ShrinkSdkPackageEntry package)
|
|
{
|
|
var card = CreateCard();
|
|
var row = new VisualElement();
|
|
row.style.flexDirection = FlexDirection.Row;
|
|
row.style.alignItems = Align.Center;
|
|
var text = new VisualElement();
|
|
text.style.flexGrow = 1f;
|
|
var title = new Label(package.DisplayName);
|
|
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
text.Add(title);
|
|
var id = new Label(package.PackageName + " · 固定版本 " + package.Version);
|
|
id.style.fontSize = 10f;
|
|
id.style.color = new Color(0.58f, 0.58f, 0.58f);
|
|
id.style.marginTop = 2f;
|
|
text.Add(id);
|
|
var summary = new Label(package.Summary);
|
|
summary.style.whiteSpace = WhiteSpace.Normal;
|
|
summary.style.marginTop = 4f;
|
|
text.Add(summary);
|
|
row.Add(text);
|
|
|
|
var action = CreatePackageAction(package);
|
|
action.style.width = 118f;
|
|
action.style.marginLeft = 12f;
|
|
row.Add(action);
|
|
card.Add(row);
|
|
return card;
|
|
}
|
|
|
|
private VisualElement CreateCompactPackageRow(ShrinkSdkPackageEntry package)
|
|
{
|
|
var row = new VisualElement();
|
|
row.style.flexDirection = FlexDirection.Row;
|
|
row.style.alignItems = Align.Center;
|
|
row.style.marginTop = 3f;
|
|
var name = new Label(package.DisplayName);
|
|
name.style.flexGrow = 1f;
|
|
row.Add(name);
|
|
var status = new Label(GetInstalledStatus(package));
|
|
status.style.fontSize = 10f;
|
|
status.style.color = IsExactInstalled(package)
|
|
? new Color(0.35f, 0.75f, 0.45f)
|
|
: new Color(0.65f, 0.65f, 0.65f);
|
|
row.Add(status);
|
|
return row;
|
|
}
|
|
|
|
private Button CreatePackageAction(ShrinkSdkPackageEntry package)
|
|
{
|
|
var button = new Button(() => BeginInstall(new[] { package }));
|
|
if (!_installed.TryGetValue(package.PackageName, out var installed))
|
|
button.text = "安装 " + package.Version;
|
|
else if (string.Equals(installed.version, package.Version, StringComparison.Ordinal))
|
|
{
|
|
button.text = "已安装 " + installed.version;
|
|
button.SetEnabled(false);
|
|
}
|
|
else
|
|
button.text = installed.version + " → " + package.Version;
|
|
|
|
if (IsBusy)
|
|
button.SetEnabled(false);
|
|
return button;
|
|
}
|
|
|
|
private static VisualElement CreateCard()
|
|
{
|
|
var card = new VisualElement();
|
|
card.style.marginBottom = 8f;
|
|
card.style.paddingLeft = 12f;
|
|
card.style.paddingRight = 12f;
|
|
card.style.paddingTop = 10f;
|
|
card.style.paddingBottom = 10f;
|
|
card.style.borderLeftWidth = 1f;
|
|
card.style.borderRightWidth = 1f;
|
|
card.style.borderTopWidth = 1f;
|
|
card.style.borderBottomWidth = 1f;
|
|
card.style.borderLeftColor = new Color(0.28f, 0.28f, 0.28f);
|
|
card.style.borderRightColor = new Color(0.28f, 0.28f, 0.28f);
|
|
card.style.borderTopColor = new Color(0.28f, 0.28f, 0.28f);
|
|
card.style.borderBottomColor = new Color(0.28f, 0.28f, 0.28f);
|
|
card.style.borderTopLeftRadius = 4f;
|
|
card.style.borderTopRightRadius = 4f;
|
|
card.style.borderBottomLeftRadius = 4f;
|
|
card.style.borderBottomRightRadius = 4f;
|
|
return card;
|
|
}
|
|
|
|
private bool MatchesSearch(ShrinkSdkPackageBundle bundle) =>
|
|
string.IsNullOrEmpty(_searchText) ||
|
|
bundle.DisplayName.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
bundle.Summary.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
bundle.Packages.Any(MatchesSearch);
|
|
|
|
private bool MatchesSearch(ShrinkSdkPackageEntry package) =>
|
|
string.IsNullOrEmpty(_searchText) ||
|
|
package.DisplayName.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
package.Summary.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
package.PackageName.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
|
|
private bool IsExactInstalled(ShrinkSdkPackageEntry package) =>
|
|
_installed.TryGetValue(package.PackageName, out var installed) &&
|
|
string.Equals(installed.version, package.Version, StringComparison.Ordinal);
|
|
|
|
private string GetInstalledStatus(ShrinkSdkPackageEntry package)
|
|
{
|
|
if (!_installed.TryGetValue(package.PackageName, out var installed))
|
|
return "未安装 · " + package.Version;
|
|
return string.Equals(installed.version, package.Version, StringComparison.Ordinal)
|
|
? "已安装 " + installed.version
|
|
: "已安装 " + installed.version + " · 固定 " + package.Version;
|
|
}
|
|
|
|
private bool IsBusy => _listRequest != null || _resolvePending || _listRetryPending;
|
|
|
|
private void Update()
|
|
{
|
|
if (_resolvePending && EditorApplication.timeSinceStartup - _resolveRequestedAt >= 0.5d)
|
|
{
|
|
_resolvePending = false;
|
|
SetStatus("正在等待依赖解析并刷新安装状态。", HelpBoxMessageType.Info);
|
|
StartListRequest();
|
|
}
|
|
|
|
if (_listRetryPending && EditorApplication.timeSinceStartup >= _nextListRetryAt)
|
|
{
|
|
_listRetryPending = false;
|
|
StartListRequest();
|
|
}
|
|
|
|
if (_listRequest == null || !_listRequest.IsCompleted)
|
|
return;
|
|
|
|
var list = _listRequest;
|
|
_listRequest = null;
|
|
if (list.Status == StatusCode.Success)
|
|
{
|
|
_listRetryCount = 0;
|
|
_installed.Clear();
|
|
foreach (var package in list.Result)
|
|
_installed[package.name] = package;
|
|
var count = _installed.Keys.Count(name => name.StartsWith("com.cneicy.", StringComparison.Ordinal));
|
|
SetStatus("状态已刷新:当前项目共解析到 " + count + " 个 ShrinkSDK 包。", HelpBoxMessageType.Info);
|
|
}
|
|
else if (string.IsNullOrWhiteSpace(list.Error?.message) && _listRetryCount < 4)
|
|
{
|
|
_listRetryCount++;
|
|
_listRetryPending = true;
|
|
_nextListRetryAt = EditorApplication.timeSinceStartup + 1d;
|
|
SetStatus("Unity 正在完成依赖解析,稍后自动重试安装状态(" + _listRetryCount + "/4)。",
|
|
HelpBoxMessageType.Info);
|
|
}
|
|
else
|
|
{
|
|
SetStatus("读取安装状态失败:" + GetRequestError(list.Error), HelpBoxMessageType.Warning);
|
|
}
|
|
RenderPage();
|
|
}
|
|
|
|
private void ConfigureRegistry()
|
|
{
|
|
if (IsBusy)
|
|
return;
|
|
try
|
|
{
|
|
ShrinkSdkManifestStore.EnsureShrinkSdkRegistry();
|
|
RefreshRegistryState();
|
|
BeginResolve("软件源已写入,Unity 正在解析依赖。");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
SetStatus(exception.Message, HelpBoxMessageType.Error);
|
|
}
|
|
}
|
|
|
|
private void BeginInstall(IEnumerable<ShrinkSdkPackageEntry> packages)
|
|
{
|
|
if (IsBusy)
|
|
return;
|
|
try
|
|
{
|
|
var roots = ShrinkSdkPackageCatalog.DistinctInstallRoots(packages);
|
|
ShrinkSdkManifestStore.EnsurePackageInstallation(roots);
|
|
RefreshRegistryState();
|
|
BeginResolve("已写入 " + roots.Count + " 个固定版本入口,Unity 正在安装并解析依赖。");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
SetStatus(exception.Message, HelpBoxMessageType.Error);
|
|
}
|
|
}
|
|
|
|
private void BeginResolve(string status)
|
|
{
|
|
_listRequest = null;
|
|
Client.Resolve();
|
|
_listRetryPending = false;
|
|
_listRetryCount = 0;
|
|
_resolvePending = true;
|
|
_resolveRequestedAt = EditorApplication.timeSinceStartup;
|
|
SetStatus(status, HelpBoxMessageType.Info);
|
|
RenderPage();
|
|
}
|
|
|
|
private void RefreshPackages()
|
|
{
|
|
if (IsBusy)
|
|
return;
|
|
_listRetryCount = 0;
|
|
StartListRequest();
|
|
SetStatus("正在读取当前项目的直接与间接依赖。", HelpBoxMessageType.Info);
|
|
RenderPage();
|
|
}
|
|
|
|
private void StartListRequest()
|
|
{
|
|
if (_listRequest == null)
|
|
_listRequest = Client.List(true, true);
|
|
}
|
|
|
|
private void RefreshRegistryState()
|
|
{
|
|
if (_registryState == null)
|
|
return;
|
|
try
|
|
{
|
|
var configured = ShrinkSdkManifestStore.HasShrinkSdkRegistry();
|
|
_registryState.text = configured ? "已配置 · " + ShrinkSdkPackageCatalog.RegistryUrl : "未配置";
|
|
_registryState.style.color = configured
|
|
? new Color(0.35f, 0.75f, 0.45f)
|
|
: new Color(0.85f, 0.55f, 0.25f);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_registryState.text = "无法读取 Packages/manifest.json";
|
|
_registryState.style.color = new Color(0.85f, 0.35f, 0.3f);
|
|
SetStatus(exception.Message, HelpBoxMessageType.Error);
|
|
}
|
|
}
|
|
|
|
private void SetStatus(string message, HelpBoxMessageType type)
|
|
{
|
|
if (_statusBox == null)
|
|
return;
|
|
_statusBox.text = message;
|
|
_statusBox.messageType = type;
|
|
}
|
|
|
|
private static string GetRequestError(Error? error)
|
|
{
|
|
var message = error?.message;
|
|
return string.IsNullOrWhiteSpace(message)
|
|
? "Unity Package Manager 未返回详细原因,请在依赖解析结束后刷新状态。"
|
|
: message!;
|
|
}
|
|
|
|
private static string GetLayerName(ShrinkSdkPackageLayer layer) => layer switch
|
|
{
|
|
ShrinkSdkPackageLayer.Context => "Context 组合基础",
|
|
ShrinkSdkPackageLayer.Host => "应用与组合根",
|
|
ShrinkSdkPackageLayer.Feature => "独立功能模块",
|
|
ShrinkSdkPackageLayer.Integration => "模块集成",
|
|
ShrinkSdkPackageLayer.Tooling => "编译与工具基础",
|
|
_ => layer.ToString()
|
|
};
|
|
}
|
|
}
|
|
#endif
|