64ef3d64ec
- MarqueeText: scrolling title for long song names (RectMask2D clipped) - SongDetailPanel: difficulty selected = green (img.color direct set) - SongSelectManager: ALL/OWNED tab active state via img.color - Card layout: DestroyImmediate + correct rebuild order to fix zero-size title bug - NanumGothic SDF fallback configured on LiberationSans for Korean support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
229 lines
8.3 KiB
C#
229 lines
8.3 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
public class SongSelectManager : MonoBehaviour
|
|
{
|
|
[SerializeField] private Button tabAllBtn;
|
|
[SerializeField] private Button tabOwnedBtn;
|
|
[SerializeField] private RectTransform cardContainer;
|
|
[SerializeField] private SongDetailPanel detailPanel;
|
|
[SerializeField] private DownloadManager downloadManager;
|
|
[SerializeField] private GameObject loadingOverlay;
|
|
[SerializeField] private GameObject errorOverlay;
|
|
[SerializeField] private TMP_Text errorText;
|
|
|
|
|
|
private static readonly Color TabActive = new Color(1f, 1f, 1f, 0.45f);
|
|
private static readonly Color TabInactive = new Color(1f, 1f, 1f, 0.12f);
|
|
|
|
private static string CachePath =>
|
|
Path.Combine(Application.persistentDataPath, "songs_cache.json");
|
|
|
|
private List<SongInfo> allSongs = new List<SongInfo>();
|
|
private bool showingOwned = false;
|
|
private TMP_FontAsset _cardFont;
|
|
|
|
private void Start()
|
|
{
|
|
// NanumGothic SDF를 직접 로드 — Resources 경로에 있어야 함
|
|
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/NanumGothic SDF");
|
|
if (_cardFont == null)
|
|
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
|
|
|
tabAllBtn .onClick.AddListener(() => SwitchTab(false));
|
|
tabOwnedBtn.onClick.AddListener(() => SwitchTab(true));
|
|
detailPanel.gameObject.SetActive(false);
|
|
SetTabVisual(false);
|
|
FetchSongs();
|
|
}
|
|
|
|
private void SwitchTab(bool owned)
|
|
{
|
|
showingOwned = owned;
|
|
SetTabVisual(owned);
|
|
RefreshCards();
|
|
}
|
|
|
|
private void SetTabVisual(bool owned)
|
|
{
|
|
ApplyTabColor(tabAllBtn, owned ? TabInactive : TabActive);
|
|
ApplyTabColor(tabOwnedBtn, owned ? TabActive : TabInactive);
|
|
}
|
|
|
|
private static void ApplyTabColor(Button btn, Color c)
|
|
{
|
|
if (btn.targetGraphic is Image img)
|
|
img.color = c;
|
|
var colors = btn.colors;
|
|
colors.normalColor = Color.white;
|
|
btn.colors = colors;
|
|
}
|
|
|
|
private void FetchSongs()
|
|
{
|
|
loadingOverlay.SetActive(true);
|
|
errorOverlay .SetActive(false);
|
|
|
|
downloadManager.FetchSongsList(
|
|
onSuccess: list =>
|
|
{
|
|
allSongs = list.songs;
|
|
SaveCache(list);
|
|
SongLibrary.Instance.ValidateWithFileSystem(downloadManager, list.songs);
|
|
loadingOverlay.SetActive(false);
|
|
RefreshCards();
|
|
},
|
|
onError: _ =>
|
|
{
|
|
SongsList cached = LoadCache();
|
|
loadingOverlay.SetActive(false);
|
|
if (cached != null)
|
|
{
|
|
allSongs = cached.songs;
|
|
RefreshCards();
|
|
}
|
|
else
|
|
{
|
|
errorOverlay.SetActive(true);
|
|
errorText.text = "Failed to connect to server\nPlease check your internet connection";
|
|
}
|
|
});
|
|
}
|
|
|
|
public void RefreshCards()
|
|
{
|
|
// DestroyImmediate to avoid deferred-destroy interfering with layout
|
|
for (int i = cardContainer.childCount - 1; i >= 0; i--)
|
|
DestroyImmediate(cardContainer.GetChild(i).gameObject);
|
|
|
|
List<SongInfo> songs = showingOwned
|
|
? allSongs.FindAll(s => SongLibrary.Instance.IsSongDownloaded(s.id))
|
|
: allSongs;
|
|
|
|
foreach (SongInfo song in songs)
|
|
SpawnCard(song);
|
|
|
|
// Order matters: layout first → card gets size → then canvas update → anchored children recalculate
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(cardContainer);
|
|
Canvas.ForceUpdateCanvases();
|
|
}
|
|
|
|
private void SpawnCard(SongInfo song)
|
|
{
|
|
bool downloaded = SongLibrary.Instance.IsSongDownloaded(song.id);
|
|
|
|
var card = new GameObject(song.title);
|
|
card.transform.SetParent(cardContainer, false);
|
|
|
|
var le = card.AddComponent<LayoutElement>();
|
|
le.preferredHeight = 13f;
|
|
le.flexibleWidth = 1f;
|
|
|
|
var bg = card.AddComponent<Image>();
|
|
bg.color = new Color(1f, 1f, 1f, 0.06f);
|
|
|
|
var btn = card.AddComponent<Button>();
|
|
btn.targetGraphic = bg;
|
|
var bc = btn.colors;
|
|
bc.normalColor = new Color(1f, 1f, 1f, 0.06f);
|
|
bc.highlightedColor = new Color(0.4f, 0.75f, 1f, 0.25f);
|
|
bc.pressedColor = new Color(0.3f, 0.60f, 0.9f, 0.45f);
|
|
bc.fadeDuration = 0.1f;
|
|
btn.colors = bc;
|
|
|
|
// Title — RectMask2D 컨테이너 안에서 마퀴 스크롤
|
|
var titleMask = new GameObject("TitleMask");
|
|
titleMask.transform.SetParent(card.transform, false);
|
|
var tmr = titleMask.AddComponent<RectTransform>();
|
|
tmr.anchorMin = new Vector2(0f, 0.5f);
|
|
tmr.anchorMax = new Vector2(1f, 1f);
|
|
tmr.offsetMin = new Vector2(5f, 0f);
|
|
tmr.offsetMax = new Vector2(downloaded ? -20f : -3f, 0f);
|
|
titleMask.AddComponent<RectMask2D>();
|
|
|
|
var titleGO = new GameObject("Title");
|
|
titleGO.transform.SetParent(titleMask.transform, false);
|
|
var tr = titleGO.AddComponent<RectTransform>();
|
|
tr.anchorMin = new Vector2(0f, 0f);
|
|
tr.anchorMax = new Vector2(0f, 1f);
|
|
tr.pivot = new Vector2(0f, 0.5f);
|
|
tr.anchoredPosition = Vector2.zero;
|
|
tr.sizeDelta = new Vector2(500f, 0f);
|
|
var tTmp = titleGO.AddComponent<TextMeshProUGUI>();
|
|
if (_cardFont != null) tTmp.font = _cardFont;
|
|
tTmp.text = song.title;
|
|
tTmp.fontSize = 5f;
|
|
tTmp.color = Color.white;
|
|
tTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
|
tTmp.overflowMode = TextOverflowModes.Overflow;
|
|
tTmp.enableWordWrapping = false;
|
|
titleGO.AddComponent<MarqueeText>();
|
|
|
|
// Artist
|
|
var artistGO = new GameObject("Artist");
|
|
artistGO.transform.SetParent(card.transform, false);
|
|
var ar = artistGO.AddComponent<RectTransform>();
|
|
ar.anchorMin = new Vector2(0f, 0f);
|
|
ar.anchorMax = new Vector2(1f, 0.5f);
|
|
ar.offsetMin = new Vector2(5f, 1f);
|
|
ar.offsetMax = new Vector2(-3f, 0f);
|
|
var aTmp = artistGO.AddComponent<TextMeshProUGUI>();
|
|
if (_cardFont != null) aTmp.font = _cardFont;
|
|
aTmp.text = song.artist;
|
|
aTmp.fontSize = 4f;
|
|
aTmp.color = new Color(1f, 1f, 1f, 0.6f);
|
|
aTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
|
|
|
// Downloaded badge
|
|
if (downloaded)
|
|
{
|
|
var badge = new GameObject("Badge");
|
|
badge.transform.SetParent(card.transform, false);
|
|
var br = badge.AddComponent<RectTransform>();
|
|
br.anchorMin = new Vector2(1f, 0.5f);
|
|
br.anchorMax = new Vector2(1f, 0.5f);
|
|
br.pivot = new Vector2(1f, 0.5f);
|
|
br.anchoredPosition = new Vector2(-3f, 0f);
|
|
br.sizeDelta = new Vector2(14f, 5.5f);
|
|
badge.AddComponent<Image>().color = new Color(0.2f, 0.78f, 0.4f, 0.85f);
|
|
|
|
var bl = new GameObject("Text");
|
|
bl.transform.SetParent(badge.transform, false);
|
|
var blr = bl.AddComponent<RectTransform>();
|
|
blr.anchorMin = Vector2.zero;
|
|
blr.anchorMax = Vector2.one;
|
|
blr.offsetMin = blr.offsetMax = Vector2.zero;
|
|
var blTmp = bl.AddComponent<TextMeshProUGUI>();
|
|
blTmp.text = "OWNED";
|
|
blTmp.fontSize = 3.5f;
|
|
blTmp.color = Color.white;
|
|
blTmp.alignment = TextAlignmentOptions.Center;
|
|
}
|
|
|
|
SongInfo captured = song;
|
|
btn.onClick.AddListener(() => OnCardClicked(captured));
|
|
}
|
|
|
|
private void OnCardClicked(SongInfo song)
|
|
{
|
|
detailPanel.gameObject.SetActive(true);
|
|
detailPanel.Show(song, downloadManager, this);
|
|
}
|
|
|
|
private static void SaveCache(SongsList list)
|
|
{
|
|
try { File.WriteAllText(CachePath, JsonUtility.ToJson(list, true)); }
|
|
catch { }
|
|
}
|
|
|
|
private static SongsList LoadCache()
|
|
{
|
|
if (!File.Exists(CachePath)) return null;
|
|
try { return JsonUtility.FromJson<SongsList>(File.ReadAllText(CachePath)); }
|
|
catch { return null; }
|
|
}
|
|
}
|