Files
Installer/Editor/ShrinkSdkInstallerWindow.cs
T
cneicy 96480dd488
Publish UPM package / publish (push) Successful in 2s
feat(installer): rebuild package manager with ui toolkit
2026-08-26 12:48:35 +08:00

471 lines
18 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 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);
var title = new Label("ShrinkSDK 包管理");
title.style.fontSize = 20f;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
header.Add(title);
var description = new Label("以 Context 为组合基础,按用途安装固定版本的模块与集成包。");
description.style.marginTop = 4f;
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.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;
private void Update()
{
if (_resolvePending && EditorApplication.timeSinceStartup - _resolveRequestedAt >= 0.5d)
{
_resolvePending = false;
SetStatus("正在等待依赖解析并刷新安装状态。", HelpBoxMessageType.Info);
StartListRequest();
}
if (_listRequest == null || !_listRequest.IsCompleted)
return;
var list = _listRequest;
_listRequest = null;
if (list.Status == StatusCode.Success)
{
_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
{
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();
_resolvePending = true;
_resolveRequestedAt = EditorApplication.timeSinceStartup;
SetStatus(status, HelpBoxMessageType.Info);
RenderPage();
}
private void RefreshPackages()
{
if (IsBusy)
return;
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