#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 _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("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 ready = bundle.Packages.Count(IsInstalledAtLeast); var button = new Button(() => BeginInstall(bundle.InstallRoots)); if (ready == bundle.Packages.Count) { button.text = "已完成"; button.SetEnabled(false); } else { button.text = ready > 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($"已就绪 {ready}/{bundle.Packages.Count}"); progress.style.marginTop = 8f; progress.style.color = ready == 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 = IsInstalledAtLeast(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 { switch (ShrinkPackageVersion.Classify(installed.version, package.Version)) { case ShrinkPackageVersionRelation.Equal: button.text = "已安装 " + installed.version; button.SetEnabled(false); break; case ShrinkPackageVersionRelation.Newer: button.text = "较新版本 " + installed.version; button.SetEnabled(false); break; case ShrinkPackageVersionRelation.Older: button.text = "升级 " + installed.version + " → " + package.Version; break; default: button.text = installed.version + " → " + package.Version; break; } } 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 IsInstalledAtLeast(ShrinkSdkPackageEntry package) => _installed.TryGetValue(package.PackageName, out var installed) && ShrinkPackageVersion.Classify(installed.version, package.Version) is ShrinkPackageVersionRelation.Equal or ShrinkPackageVersionRelation.Newer; private string GetInstalledStatus(ShrinkSdkPackageEntry package) { if (!_installed.TryGetValue(package.PackageName, out var installed)) return "未安装 · " + package.Version; return ShrinkPackageVersion.Classify(installed.version, package.Version) switch { ShrinkPackageVersionRelation.Equal => "已安装 " + installed.version, ShrinkPackageVersionRelation.Newer => "已安装较新版本 " + installed.version, ShrinkPackageVersionRelation.Older => "可升级 " + installed.version + " → " + package.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 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