From d21928da53ebb44528da2a1609cee0cbeb595c28 Mon Sep 17 00:00:00 2001 From: KoalaTrapLord Date: Wed, 18 Mar 2026 16:57:01 -0400 Subject: [PATCH] Add PreconController.cs - precon browser scene (U12) --- .../Assets/Scripts/UI/PreconController.cs | 403 ++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 unity-client/Assets/Scripts/UI/PreconController.cs diff --git a/unity-client/Assets/Scripts/UI/PreconController.cs b/unity-client/Assets/Scripts/UI/PreconController.cs new file mode 100644 index 0000000..cd4d75b --- /dev/null +++ b/unity-client/Assets/Scripts/UI/PreconController.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.SceneManagement; +using TMPro; +using Newtonsoft.Json; +using CommanderAILab.Models; +using CommanderAILab.Services; + +namespace CommanderAILab.UI +{ + /// + /// Precon browser scene — browse, search, and install preconstructed Commander decks. + /// Grid display with card images, deck details panel, install/install-all buttons, + /// and refresh from GitHub functionality. + /// + public class PreconController : MonoBehaviour + { + // -- Header —————————————————————————————————— + [Header("Navigation")] + [SerializeField] private Button backButton; + + [Header("Search & Filter")] + [SerializeField] private TMP_InputField searchInput; + [SerializeField] private TMP_Dropdown colorFilterDropdown; + [SerializeField] private Button searchButton; + [SerializeField] private Button clearButton; + + [Header("Deck Grid")] + [SerializeField] private Transform deckGridParent; + [SerializeField] private GameObject deckCardPrefab; + [SerializeField] private ScrollRect gridScrollRect; + + [Header("Deck Detail Panel")] + [SerializeField] private GameObject detailPanel; + [SerializeField] private Image detailCommanderImage; + [SerializeField] private TMP_Text detailDeckName; + [SerializeField] private TMP_Text detailColorIdentity; + [SerializeField] private TMP_Text detailCardCount; + [SerializeField] private TMP_Text detailThemeText; + [SerializeField] private Button detailInstallButton; + [SerializeField] private Button detailCloseButton; + + [Header("Batch Actions")] + [SerializeField] private Button installAllButton; + [SerializeField] private Button refreshButton; + [SerializeField] private TMP_Text statusText; + + [Header("Loading & Error")] + [SerializeField] private GameObject loadingSpinner; + [SerializeField] private GameObject errorPanel; + [SerializeField] private TMP_Text errorText; + [SerializeField] private Button retryButton; + + // -- State —————————————————————————————————— + private List _allPrecons = new(); + private List _filteredPrecons = new(); + private PreconDeck _selectedPrecon; + private bool _isLoading = false; + private HashSet _installedDecks = new(); + + // -- Serializable response wrappers ——————————————————— + [Serializable] + private class PreconListResponse + { + public List precons; + } + + [Serializable] + public class PreconDeck + { + public string fileName; + public string name; + public string commander; + public string colorIdentity; + public int cardCount; + public string theme; + public string imageUrl; + } + + [Serializable] + private class InstallRequest + { + public string fileName; + } + + [Serializable] + private class InstallResponse + { + public bool installed; + public string deckName; + public string destination; + public string message; + } + + [Serializable] + private class BatchInstallRequest + { + public List fileNames; + } + + [Serializable] + private class BatchInstallResponse + { + public List results; + } + + [Serializable] + private class BatchInstallResult + { + public string fileName; + public bool installed; + public string deckName; + public string error; + } + + [Serializable] + private class RefreshResponse + { + public string message; + public int total; + } + + // ======================================================== + // Lifecycle + // ======================================================== + + private void Start() + { + backButton.onClick.AddListener(OnBack); + searchButton.onClick.AddListener(OnSearch); + clearButton.onClick.AddListener(OnClear); + retryButton.onClick.AddListener(LoadPrecons); + installAllButton.onClick.AddListener(OnInstallAll); + refreshButton.onClick.AddListener(OnRefresh); + detailInstallButton.onClick.AddListener(OnInstallSelected); + detailCloseButton.onClick.AddListener(() => detailPanel.SetActive(false)); + + if (searchInput != null) + searchInput.onSubmit.AddListener(_ => OnSearch()); + + detailPanel.SetActive(false); + errorPanel.SetActive(false); + + LoadPrecons(); + } + + // ======================================================== + // Data Loading + // ======================================================== + + private void LoadPrecons() + { + if (_isLoading) return; + _isLoading = true; + loadingSpinner.SetActive(true); + errorPanel.SetActive(false); + statusText.text = "Loading precons..."; + + ApiClient.Instance.Get( + "/api/lab/precons", + OnPreconsLoaded, + OnError + ); + } + + private void OnPreconsLoaded(PreconListResponse response) + { + _isLoading = false; + loadingSpinner.SetActive(false); + + _allPrecons = response.precons ?? new List(); + statusText.text = $"{_allPrecons.Count} precon deck(s) available"; + + ApplyFilter(); + } + + // ======================================================== + // Search & Filter + // ======================================================== + + private void OnSearch() + { + ApplyFilter(); + } + + private void OnClear() + { + searchInput.text = ""; + if (colorFilterDropdown != null) + colorFilterDropdown.value = 0; + ApplyFilter(); + } + + private void ApplyFilter() + { + string query = searchInput.text.Trim().ToLowerInvariant(); + string colorFilter = ""; + if (colorFilterDropdown != null && colorFilterDropdown.value > 0) + colorFilter = colorFilterDropdown.options[colorFilterDropdown.value].text; + + _filteredPrecons = _allPrecons.FindAll(p => + { + bool matchesSearch = string.IsNullOrEmpty(query) + || (p.name != null && p.name.ToLowerInvariant().Contains(query)) + || (p.commander != null && p.commander.ToLowerInvariant().Contains(query)) + || (p.theme != null && p.theme.ToLowerInvariant().Contains(query)); + + bool matchesColor = string.IsNullOrEmpty(colorFilter) + || (p.colorIdentity != null && p.colorIdentity.Contains(colorFilter)); + + return matchesSearch && matchesColor; + }); + + RenderGrid(); + } + + // ======================================================== + // Grid Rendering + // ======================================================== + + private void RenderGrid() + { + // Clear existing children + for (int i = deckGridParent.childCount - 1; i >= 0; i--) + Destroy(deckGridParent.GetChild(i).gameObject); + + foreach (var precon in _filteredPrecons) + { + var card = Instantiate(deckCardPrefab, deckGridParent); + var texts = card.GetComponentsInChildren(true); + if (texts.Length > 0) texts[0].text = precon.name ?? precon.fileName; + if (texts.Length > 1) texts[1].text = precon.commander ?? ""; + if (texts.Length > 2) texts[2].text = precon.colorIdentity ?? ""; + + // Installed badge + bool installed = _installedDecks.Contains(precon.fileName); + var badge = card.transform.Find("InstalledBadge"); + if (badge != null) badge.gameObject.SetActive(installed); + + // Click to show detail + var btn = card.GetComponent