diff --git a/unity-client/Assets/Scripts/UI/CollectionController.cs b/unity-client/Assets/Scripts/UI/CollectionController.cs new file mode 100644 index 0000000..f020a01 --- /dev/null +++ b/unity-client/Assets/Scripts/UI/CollectionController.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.SceneManagement; +using TMPro; +using Newtonsoft.Json; +using CommanderAILab.Models; +using CommanderAILab.Services; + +namespace CommanderAILab.UI +{ + /// + /// Collection scene controller — card grid with search, filter, sort, + /// card detail panel, async image loading, pagination, and mobile support. + /// + public class CollectionController : MonoBehaviour + { + // ── Header ────────────────────────────────────────────────────────── + [Header("Navigation")] + [SerializeField] private Button backButton; + + [Header("Search & Sort")] + [SerializeField] private TMP_InputField searchInput; + [SerializeField] private TMP_Dropdown sortDropdown; + [SerializeField] private Button searchButton; + [SerializeField] private Button filterToggleButton; // mobile: open/close drawer + + [Header("Filter Sidebar")] + [SerializeField] private GameObject filterPanel; // root – toggled on mobile + [SerializeField] private TMP_Dropdown colorFilterDropdown; + [SerializeField] private TMP_Dropdown typeFilterDropdown; + [SerializeField] private TMP_Dropdown setFilterDropdown; + [SerializeField] private TMP_Dropdown rarityFilterDropdown; + [SerializeField] private TMP_Dropdown ownedFilterDropdown; // All / Owned / Missing + [SerializeField] private Button applyFilterButton; + [SerializeField] private Button clearFilterButton; + + [Header("Card Grid")] + [SerializeField] private Transform cardGridParent; // ScrollRect content + [SerializeField] private GameObject cardPrefab; // CardPrefab instance + [SerializeField] private ScrollRect gridScrollRect; + + [Header("Pagination")] + [SerializeField] private Button loadMoreButton; + [SerializeField] private TMP_Text pageInfoText; + [SerializeField] private GameObject loadingSpinner; + + [Header("Card Detail Panel")] + [SerializeField] private GameObject detailPanel; + [SerializeField] private Image detailCardFront; + [SerializeField] private Image detailCardBack; // placeholder / back face + [SerializeField] private TMP_Text detailCardName; + [SerializeField] private TMP_Text detailManaCost; + [SerializeField] private TMP_Text detailType; + [SerializeField] private TMP_Text detailRarity; + [SerializeField] private TMP_Text detailSet; + [SerializeField] private TMP_Text detailOracleText; + [SerializeField] private TMP_Text detailQuantity; + [SerializeField] private Button detailAddToDeckButton; + [SerializeField] private Button detailCloseButton; + + [Header("Error")] + [SerializeField] private GameObject errorPanel; + [SerializeField] private TMP_Text errorText; + [SerializeField] private Button retryButton; + + // ── State ──────────────────────────────────────────────────────────── + private List _cards = new(); + private CardModel _selectedCard; + private int _currentPage = 1; + private int _totalCards = 0; + private const int PageSize = 40; + private bool _isLoading = false; + private bool _filterOpen = false; + + // Current filter / sort state + private string _searchQuery = ""; + private string _colorFilter = ""; + private string _typeFilter = ""; + private string _setFilter = ""; + private string _rarityFilter = ""; + private string _ownedFilter = ""; // "all" | "owned" | "missing" + private string _sortField = "name"; + + // Pooled card tile GameObjects + private readonly List _cardTiles = new(); + + // ── Lifecycle ──────────────────────────────────────────────────────── + private void Start() + { + // Back button → MainMenu + backButton.onClick.AddListener(() => SceneManager.LoadScene("MainMenu")); + + // Search + searchButton.onClick.AddListener(OnSearchClicked); + searchInput.onSubmit.AddListener(_ => OnSearchClicked()); + + // Sort + sortDropdown.onValueChanged.AddListener(_ => RefreshFromPage1()); + + // Filter actions + applyFilterButton.onClick.AddListener(RefreshFromPage1); + clearFilterButton.onClick.AddListener(OnClearFilters); + filterToggleButton.onClick.AddListener(ToggleFilterPanel); + + // Pagination + loadMoreButton.onClick.AddListener(OnLoadMore); + loadMoreButton.gameObject.SetActive(false); + + // Detail panel + detailCloseButton.onClick.AddListener(CloseDetailPanel); + detailAddToDeckButton.onClick.AddListener(OnAddToDeck); + detailPanel.SetActive(false); + + // Error retry + retryButton.onClick.AddListener(RefreshFromPage1); + errorPanel.SetActive(false); + + // On mobile start with filter panel hidden + _filterOpen = Screen.width > 900; + filterPanel.SetActive(_filterOpen); + + // Initial load + RefreshFromPage1(); + } + + // ── Search / Filter / Sort ─────────────────────────────────────────── + private void OnSearchClicked() + { + _searchQuery = searchInput.text.Trim(); + RefreshFromPage1(); + } + + private void OnClearFilters() + { + colorFilterDropdown.value = 0; + typeFilterDropdown.value = 0; + setFilterDropdown.value = 0; + rarityFilterDropdown.value = 0; + ownedFilterDropdown.value = 0; + searchInput.text = ""; + _searchQuery = ""; + RefreshFromPage1(); + } + + private void ToggleFilterPanel() + { + _filterOpen = !_filterOpen; + filterPanel.SetActive(_filterOpen); + } + + // ── Data Loading ───────────────────────────────────────────────────── + private void RefreshFromPage1() + { + _currentPage = 1; + _cards.Clear(); + ClearCardTiles(); + LoadPage(_currentPage); + } + + private void OnLoadMore() + { + _currentPage++; + LoadPage(_currentPage); + } + + private void LoadPage(int page) + { + if (_isLoading) return; + _isLoading = true; + loadingSpinner.SetActive(true); + loadMoreButton.interactable = false; + errorPanel.SetActive(false); + + // Collect filter values from dropdowns + _colorFilter = DropdownValue(colorFilterDropdown); + _typeFilter = DropdownValue(typeFilterDropdown); + _setFilter = DropdownValue(setFilterDropdown); + _rarityFilter = DropdownValue(rarityFilterDropdown); + _ownedFilter = DropdownValue(ownedFilterDropdown); + _sortField = SortFieldFromDropdown(sortDropdown.value); + + string path = BuildQueryPath(page); + StartCoroutine(ApiClient.Instance.GetRaw(path, + json => OnPageLoaded(json, page), + errMsg => OnLoadError(errMsg))); + } + + private string BuildQueryPath(int page) + { + var sb = new StringBuilder("/api/collection?"); + sb.Append($"page={page}&per_page={PageSize}"); + if (!string.IsNullOrEmpty(_searchQuery)) + sb.Append($"&search={UnityWebRequest.EscapeURL(_searchQuery)}"); + if (!string.IsNullOrEmpty(_colorFilter)) + sb.Append($"&colors={UnityWebRequest.EscapeURL(_colorFilter)}"); + if (!string.IsNullOrEmpty(_typeFilter)) + sb.Append($"&type={UnityWebRequest.EscapeURL(_typeFilter)}"); + if (!string.IsNullOrEmpty(_setFilter)) + sb.Append($"&set={UnityWebRequest.EscapeURL(_setFilter)}"); + if (!string.IsNullOrEmpty(_rarityFilter)) + sb.Append($"&rarity={UnityWebRequest.EscapeURL(_rarityFilter)}"); + if (!string.IsNullOrEmpty(_ownedFilter) && _ownedFilter != "all") + sb.Append($"&owned={_ownedFilter}"); + if (!string.IsNullOrEmpty(_sortField)) + sb.Append($"&sort={_sortField}"); + return sb.ToString(); + } + + private void OnPageLoaded(string json, int page) + { + _isLoading = false; + loadingSpinner.SetActive(false); + + CollectionResponse response; + try { response = JsonConvert.DeserializeObject(json); } + catch (Exception e) + { + OnLoadError($"Parse error: {e.Message}"); + return; + } + + _totalCards = response.total; + _cards.AddRange(response.cards); + + UpdatePageInfo(); + SpawnCardTiles(response.cards); + + bool hasMore = _cards.Count < _totalCards; + loadMoreButton.gameObject.SetActive(hasMore); + loadMoreButton.interactable = true; + } + + private void OnLoadError(string msg) + { + _isLoading = false; + loadingSpinner.SetActive(false); + errorText.text = $"Failed to load collection:\n{msg}"; + errorPanel.SetActive(true); + Debug.LogWarning($"[CollectionController] {msg}"); + } + + // ── Card Tiles ─────────────────────────────────────────────────────── + private void SpawnCardTiles(List cards) + { + foreach (var card in cards) + { + var tile = Instantiate(cardPrefab, cardGridParent); + _cardTiles.Add(tile); + + // Set name label (assumes CardPrefab has a TMP child named "CardName") + var nameLabel = tile.GetComponentInChildren(); + if (nameLabel != null) nameLabel.text = card.name; + + // Async image load + var img = tile.GetComponentInChildren(); + if (img != null && !string.IsNullOrEmpty(card.imageUrl)) + { + ImageCache.Instance.GetSprite(card.imageUrl, sprite => + { + if (img != null) img.sprite = sprite; + }); + } + + // Click → open detail panel + var btn = tile.GetComponentInChildren