using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; namespace Amanotes { internal class BuildTab { private readonly List builds = new List(); private bool needsRefresh = true; private Vector2 scrollPos; private int selectedAssetIndex; private int selectedBuildInfoIndex; private bool assetsExpanded = true; private bool infosExpanded = true; private readonly Color rowBg = new Color(0.22f, 0.22f, 0.22f, 1f); private readonly Color headerBg = new Color(0.19f, 0.19f, 0.19f, 1f); private readonly Color separatorColor = new Color(0.1f, 0.1f, 0.1f, 1f); private GUIStyle _sRowLabel, _sRowLabelDim, _sHeader, _sSmallBtn, _sVersion, _sTag; private GUIStyle sRowLabel => _sRowLabel ??= new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Clip, wordWrap = false, padding = new RectOffset(4, 0, 0, 0) }; private GUIStyle sRowLabelDim => _sRowLabelDim ??= new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft, clipping = TextClipping.Clip, wordWrap = false, padding = new RectOffset(4, 0, 0, 0), normal = { textColor = new Color(0.55f, 0.55f, 0.55f) } }; private GUIStyle sVersion => _sVersion ??= new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleRight, padding = new RectOffset(0, 0, 0, 0), normal = { textColor = new Color(0.7f, 0.7f, 0.7f) } }; private GUIStyle sHeader { get { if (_sHeader != null) return _sHeader; _sHeader = new GUIStyle(EditorStyles.foldout) { fontSize = 14 }; _sHeader.normal.textColor = Color.white; _sHeader.onNormal.textColor = Color.white; _sHeader.focused.textColor = Color.white; _sHeader.onFocused.textColor = Color.white; _sHeader.active.textColor = Color.white; _sHeader.onActive.textColor = Color.white; _sHeader.hover.textColor = Color.white; _sHeader.onHover.textColor = Color.white; return _sHeader; } } private GUIStyle sSmallBtn { get { if (_sSmallBtn != null) return _sSmallBtn; _sSmallBtn = new GUIStyle(GUI.skin.button) { fontSize = 9, alignment = TextAnchor.MiddleCenter, padding = new RectOffset(4, 4, 0, 0), margin = new RectOffset(0, 0, 0, 0), overflow = new RectOffset(0, 0, 0, 0) }; return _sSmallBtn; } } private GUIStyle sTag { get { if (_sTag != null) return _sTag; _sTag = new GUIStyle(EditorStyles.miniLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 9, border = new RectOffset(1, 1, 1, 1), padding = new RectOffset(4, 4, 1, 1), normal = { textColor = new Color(0.65f, 0.65f, 0.65f) } }; return _sTag; } } internal void Refresh() => needsRefresh = true; private void EnsureData() { if (!needsRefresh && builds.Count > 0) return; needsRefresh = false; builds.Clear(); var guids = AssetDatabase.FindAssets("t:LocalBuild"); for (int i = 0; i < guids.Length; i++) { var path = AssetDatabase.GUIDToAssetPath(guids[i]); var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) builds.Add(asset); } if (selectedAssetIndex >= builds.Count) selectedAssetIndex = 0; } internal void OnGUI() { EnsureData(); if (builds.Count == 0) { EditorGUILayout.HelpBox("No LocalBuild assets found.\nCreate one via Assets > Create > AmaDev > Local Build Config.", MessageType.Info); if (GUILayout.Button("Create Local Build Config")) CreateNewBuildAsset(); return; } if (EditorApplication.isCompiling) { EditorGUILayout.HelpBox("Compiling...", MessageType.Info); return; } // Asset selector header DrawAssetSelector(); if (selectedAssetIndex >= builds.Count) return; var build = builds[selectedAssetIndex]; if (build == null) { needsRefresh = true; return; } scrollPos = EditorGUILayout.BeginScrollView(scrollPos); // Build infos list DrawBuildInfoList(build); // Inline editor for selected build info if (build.listBuild.Count > 0) { GUILayout.Space(4); DrawBuildInfoEditor(build); } EditorGUILayout.EndScrollView(); GUILayout.FlexibleSpace(); DrawFooter(build); } private void DrawAssetSelector() { var headerRect = GUILayoutUtility.GetRect(0, 26f, GUILayout.ExpandWidth(true)); EditorGUI.DrawRect(headerRect, headerBg); EditorGUI.DrawRect(new Rect(headerRect.x, headerRect.y, headerRect.width, 1), separatorColor); // + button and Duplicate button on the right const float btnW = 20f; const float dupW = 50f; const float btnH = 16f; float bx = headerRect.xMax - 6; bx -= btnW; var addRect = new Rect(bx, headerRect.y + (26f - btnH) * 0.5f, btnW, btnH); if (GUI.Button(addRect, "+", sSmallBtn)) CreateNewBuildAsset(); bx -= dupW + 2; var dupRect = new Rect(bx, headerRect.y + (26f - btnH) * 0.5f, dupW, btnH); if (GUI.Button(dupRect, "Dup", sSmallBtn) && builds.Count > 0 && selectedAssetIndex < builds.Count) DuplicateBuildAsset(builds[selectedAssetIndex]); var foldRect = new Rect(headerRect.x + 4, headerRect.y, bx - headerRect.x - 8, headerRect.height); assetsExpanded = EditorGUI.Foldout(foldRect, assetsExpanded, $"Build Configs ({builds.Count})", true, sHeader); if (!assetsExpanded) return; for (int i = 0; i < builds.Count; i++) DrawAssetRow(builds[i], i); } private void DrawAssetRow(Core.LocalBuild build, int index) { const float rowH = 26f; const float pad = 4f; const float tagH = 14f; const float selectW = 36f; var rect = GUILayoutUtility.GetRect(0, rowH, GUILayout.ExpandWidth(true)); if (Event.current.type == EventType.Repaint) { var bg = index == selectedAssetIndex ? new Color(0.25f, 0.35f, 0.5f, 1f) : rowBg; EditorGUI.DrawRect(rect, bg); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), new Color(0.18f, 0.18f, 0.18f, 0.5f)); } float x = rect.x + pad + 4; // Ping button (click name to select in project) var assetPath = AssetDatabase.GetAssetPath(build); var assetName = build.name; float rightW = selectW + pad; float nameW = rect.xMax - pad - rightW - x; if (nameW < 40) nameW = 40; var nameRect = new Rect(x, rect.y, nameW, rowH); var style = index == selectedAssetIndex ? sRowLabel : sRowLabelDim; if (Event.current.type == EventType.Repaint) style.Draw(nameRect, new GUIContent(assetName, assetPath), false, false, false, false); // Click row to select if (GUI.Button(new Rect(rect.x, rect.y, rect.width - selectW - pad * 2, rowH), GUIContent.none, GUIStyle.none)) { selectedAssetIndex = index; selectedBuildInfoIndex = 0; } // Ping button float rx = rect.xMax - pad - selectW; var pingRect = new Rect(rx, rect.y + (rowH - tagH - 2) * 0.5f, selectW, tagH + 2); if (GUI.Button(pingRect, "Ping", sSmallBtn)) EditorGUIUtility.PingObject(build); } private void DrawBuildInfoList(Core.LocalBuild build) { var headerRect = GUILayoutUtility.GetRect(0, 26f, GUILayout.ExpandWidth(true)); EditorGUI.DrawRect(headerRect, headerBg); EditorGUI.DrawRect(new Rect(headerRect.x, headerRect.y, headerRect.width, 1), separatorColor); // Add build info button const float btnW = 20f; const float btnH = 16f; float bx = headerRect.xMax - 6 - btnW; var addRect = new Rect(bx, headerRect.y + (26f - btnH) * 0.5f, btnW, btnH); if (GUI.Button(addRect, "+", sSmallBtn)) { build.listBuild.Add(new Core.LocalBuild.BuildInfo { target = BuildTarget.Android, productName = PlayerSettings.productName, buildVersion = PlayerSettings.bundleVersion, buildNumber = 1 }.Read()); EditorUtility.SetDirty(build); } var foldRect = new Rect(headerRect.x + 4, headerRect.y, bx - headerRect.x - 8, headerRect.height); infosExpanded = EditorGUI.Foldout(foldRect, infosExpanded, $"Build Targets ({build.listBuild.Count})", true, sHeader); if (!infosExpanded) return; for (int i = 0; i < build.listBuild.Count; i++) DrawBuildInfoRow(build, i); } private void DrawBuildInfoRow(Core.LocalBuild build, int index) { var info = build.listBuild[index]; const float rowH = 28f; const float pad = 4f; const float tagH = 14f; const float verW = 52f; const float closeW = 14f; var rect = GUILayoutUtility.GetRect(0, rowH, GUILayout.ExpandWidth(true)); if (Event.current.type == EventType.Repaint) { var bg = index == selectedBuildInfoIndex ? new Color(0.25f, 0.35f, 0.5f, 1f) : rowBg; EditorGUI.DrawRect(rect, bg); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), new Color(0.18f, 0.18f, 0.18f, 0.5f)); } float x = rect.x + pad; // Delete button var oldCloseColor = GUI.color; GUI.color = new Color(1f, 1f, 1f, 0.35f); if (GUI.Button(new Rect(x, rect.y, closeW, rowH), "\u00D7", EditorStyles.label)) { if (EditorUtility.DisplayDialog("Remove Build Info", $"Remove {info.target} build info?", "Remove", "Cancel")) { build.listBuild.RemoveAt(index); EditorUtility.SetDirty(build); if (selectedBuildInfoIndex >= build.listBuild.Count) selectedBuildInfoIndex = Mathf.Max(0, build.listBuild.Count - 1); return; } } GUI.color = oldCloseColor; x += closeW + 2; // Platform tag var platformTag = info.target.ToString(); if (platformTag.Length > 7) platformTag = platformTag.Substring(0, 7); float tagW = sTag.CalcSize(new GUIContent(platformTag)).x + 8; if (tagW < 42) tagW = 42; float rightW = tagW + pad + verW + pad; float nameW = rect.xMax - pad - rightW - x; if (nameW < 40) nameW = 40; // Name var displayName = !string.IsNullOrEmpty(info.productName) ? info.productName : info.projectId ?? "(unnamed)"; var nameRect = new Rect(x, rect.y, nameW, rowH); var nameStyle = index == selectedBuildInfoIndex ? sRowLabel : sRowLabelDim; if (Event.current.type == EventType.Repaint) nameStyle.Draw(nameRect, new GUIContent(displayName, info.buildName), false, false, false, false); // Click to select if (GUI.Button(new Rect(rect.x, rect.y, rect.width - tagW - verW - pad * 3, rowH), GUIContent.none, GUIStyle.none)) { selectedBuildInfoIndex = index; } // Platform tag on right float rx = rect.xMax - pad - tagW; var tagRect = new Rect(rx, rect.y + (rowH - tagH) * 0.5f, tagW, tagH); DrawTag(tagRect, platformTag); // Version rx -= verW + pad; var verRect = new Rect(rx, rect.y, verW, rowH); if (Event.current.type == EventType.Repaint) sVersion.Draw(verRect, new GUIContent($"v{info.buildVersion}"), false, false, false, false); } private void DrawTag(Rect rect, string label) { if (Event.current.type != EventType.Repaint) return; var borderColor = new Color(0.5f, 0.5f, 0.5f, 0.6f); EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1), borderColor); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1, rect.width, 1), borderColor); EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1, rect.height), borderColor); EditorGUI.DrawRect(new Rect(rect.xMax - 1, rect.y, 1, rect.height), borderColor); sTag.Draw(rect, new GUIContent(label), false, false, false, false); } private void DrawBuildInfoEditor(Core.LocalBuild build) { if (selectedBuildInfoIndex >= build.listBuild.Count) return; var info = build.listBuild[selectedBuildInfoIndex]; // Separator var sepRect = GUILayoutUtility.GetRect(0, 26f, GUILayout.ExpandWidth(true)); EditorGUI.DrawRect(sepRect, headerBg); EditorGUI.DrawRect(new Rect(sepRect.x, sepRect.y, sepRect.width, 1), separatorColor); GUI.Label(new Rect(sepRect.x + 8, sepRect.y, sepRect.width - 16, sepRect.height), $"\u25B6 {info.target} — {info.buildName}", EditorStyles.boldLabel); GUILayout.Space(2); // Inline fields EditorGUI.BeginChangeCheck(); info.projectId = EditorGUILayout.TextField("Project ID", info.projectId); info.productName = EditorGUILayout.TextField("Product Name", info.productName); info.packageName = EditorGUILayout.TextField("Package Name", info.packageName); info.target = (BuildTarget)EditorGUILayout.EnumPopup("Target", info.target); info.buildVersion = EditorGUILayout.TextField("Version", info.buildVersion); info.buildNumber = EditorGUILayout.IntField("Build Number", info.buildNumber); info.buildIcon = (Texture2D)EditorGUILayout.ObjectField("Build Icon", info.buildIcon, typeof(Texture2D), false); if (EditorGUI.EndChangeCheck()) EditorUtility.SetDirty(build); GUILayout.Space(4); // Action buttons GUILayout.BeginHorizontal(); if (GUILayout.Button("Read", GUILayout.Height(22))) { info.Read(); EditorUtility.SetDirty(build); } if (GUILayout.Button("Write", GUILayout.Height(22))) info.Write(); if (GUILayout.Button("v+", GUILayout.Height(22))) { info.IncreaseVersion(); EditorUtility.SetDirty(build); } GUILayout.EndHorizontal(); } private void DrawFooter(Core.LocalBuild build) { var sep = GUILayoutUtility.GetRect(0, 1, GUILayout.ExpandWidth(true)); EditorGUI.DrawRect(sep, new Color(0.3f, 0.3f, 0.3f, 1f)); GUILayout.BeginHorizontal(); if (GUILayout.Button("Refresh", GUILayout.Height(26))) needsRefresh = true; if (build.listBuild.Count > 0 && selectedBuildInfoIndex < build.listBuild.Count) { if (GUILayout.Button("Dry Run", GUILayout.Height(26))) { build.DryRun(build.listBuild[selectedBuildInfoIndex]); } var oldBg = GUI.backgroundColor; GUI.backgroundColor = new Color(0.3f, 0.7f, 1f, 1f); if (GUILayout.Button("Build", GUILayout.Height(26))) { var info = build.listBuild[selectedBuildInfoIndex]; info.enable = true; build.buildRQ = Core.LocalBuild.BuildRequestTS.Generate(); build.ProcessBuild(); GUIUtility.ExitGUI(); } GUI.backgroundColor = oldBg; } GUILayout.EndHorizontal(); GUILayout.Space(2); } private void CreateNewBuildAsset() { var folder = "Assets"; var path = AssetDatabase.GenerateUniqueAssetPath($"{folder}/LocalBuild.asset"); var asset = ScriptableObject.CreateInstance(); asset.listBuild = new System.Collections.Generic.List { new Core.LocalBuild.BuildInfo { target = BuildTarget.Android }.Read(), new Core.LocalBuild.BuildInfo { target = BuildTarget.iOS }.Read() }; AssetDatabase.CreateAsset(asset, path); AssetDatabase.SaveAssets(); needsRefresh = true; selectedAssetIndex = builds.Count; // will point to the new one after refresh } private void DuplicateBuildAsset(Core.LocalBuild source) { var sourcePath = AssetDatabase.GetAssetPath(source); if (string.IsNullOrEmpty(sourcePath)) return; var dir = Path.GetDirectoryName(sourcePath); var name = Path.GetFileNameWithoutExtension(sourcePath); var newPath = AssetDatabase.GenerateUniqueAssetPath(Path.Combine(dir, $"{name}.asset")); AssetDatabase.CopyAsset(sourcePath, newPath); AssetDatabase.SaveAssets(); needsRefresh = true; } } }