using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class SongDetailPanel : MonoBehaviour { [Header("곡 정보")] [SerializeField] private TMP_Text titleText; [SerializeField] private TMP_Text artistText; [SerializeField] private TMP_Text infoText; [Header("난이도 버튼")] [SerializeField] private Button btnNormal; [SerializeField] private Button btnHard; [SerializeField] private Button btnExpert; [SerializeField] private Button btnExpertPlus; [Header("액션 버튼")] [SerializeField] private Button downloadButton; [SerializeField] private Button deleteButton; [SerializeField] private Button playButton; [SerializeField] private Button closeButton; [Header("진행률")] [SerializeField] private GameObject progressGroup; [SerializeField] private Slider progressSlider; [SerializeField] private TMP_Text progressText; [Header("씬 이름")] [SerializeField] private string gameSceneName = "Game"; private static readonly Color NeonBg = new Color(0.05f, 0.82f, 0.95f, 0.42f); private static readonly Color DarkButtonBg = new Color(0.09f, 0.22f, 0.27f, 0.66f); private static readonly Color DisabledBg = new Color(0.06f, 0.12f, 0.15f, 0.48f); private static readonly Color DangerBg = new Color(0.52f, 0.16f, 0.22f, 0.72f); private static readonly Color ButtonText = new Color(0.92f, 1.0f, 1.0f, 1.0f); private static readonly Color MutedText = new Color(0.66f, 0.80f, 0.84f, 0.76f); private static readonly Color NeonOutline = new Color(0.25f, 0.96f, 1.0f, 0.42f); private SongInfo currentSong; private string selectedDifficulty; private DownloadManager downloadManager; private SongSelectManager selectManager; private MarqueeText titleMarquee; private MarqueeText artistMarquee; private readonly (string key, Func btn)[] diffSlots = { ("normal", p => p.btnNormal), ("hard", p => p.btnHard), ("expert", p => p.btnExpert), ("expertplus", p => p.btnExpertPlus), }; private void Awake() { HideDifficultyLabel(); titleMarquee = ConfigureMarqueeText(titleText, 5.0f, 7.2f); artistMarquee = ConfigureMarqueeText(artistText, 3.4f, 4.4f); ConfigureOneLineText(infoText, 3.2f, 4.2f, TextAlignmentOptions.MidlineLeft); ConfigureButtonText(btnNormal, 3.2f, 4.0f); ConfigureButtonText(btnHard, 3.2f, 4.0f); ConfigureButtonText(btnExpert, 3.2f, 4.0f); ConfigureButtonText(btnExpertPlus, 3.0f, 3.8f); ConfigureButtonText(downloadButton, 3.5f, 4.4f); ConfigureButtonText(deleteButton, 3.5f, 4.4f); ConfigureButtonText(playButton, 3.5f, 4.4f); ConfigureButtonText(closeButton, 5.2f, 6.4f); } // ── Public API ─────────────────────────────────────────── public void Show(SongInfo song, DownloadManager dm, SongSelectManager sm) { currentSong = song; downloadManager = dm; selectManager = sm; selectedDifficulty = null; titleText.text = song.title; artistText.text = song.artist; infoText.text = song.duration > 0 ? $"BPM {Mathf.RoundToInt(song.bpm)} {FormatDuration(song.duration)}" : $"BPM {Mathf.RoundToInt(song.bpm)}"; titleMarquee?.Refresh(); artistMarquee?.Refresh(); RefreshUI(); } // ── UI 갱신 ────────────────────────────────────────────── private void RefreshUI() { bool downloaded = SongLibrary.Instance.IsSongDownloaded(currentSong.id); foreach (var (key, getBtn) in diffSlots) { Button btn = getBtn(this); bool exists = currentSong.difficulties.Get(key) != null; btn.interactable = downloaded && exists; btn.onClick.RemoveAllListeners(); if (downloaded && exists) { string captured = key; btn.onClick.AddListener(() => SelectDifficulty(captured)); } } UpdateDiffColors(); downloadButton.gameObject.SetActive(!downloaded); deleteButton.gameObject.SetActive(downloaded); downloadButton.interactable = !downloaded; deleteButton.interactable = downloaded; playButton.interactable = downloaded && selectedDifficulty != null; progressGroup.SetActive(false); UpdateActionButtonStyles(downloaded); downloadButton.onClick.RemoveAllListeners(); downloadButton.onClick.AddListener(OnDownloadClicked); deleteButton.onClick.RemoveAllListeners(); deleteButton.onClick.AddListener(OnDeleteClicked); playButton.onClick.RemoveAllListeners(); playButton.onClick.AddListener(OnPlayClicked); if (closeButton != null) { closeButton.onClick.RemoveAllListeners(); closeButton.onClick.AddListener(() => gameObject.SetActive(false)); } } private void SelectDifficulty(string difficulty) { selectedDifficulty = difficulty; playButton.interactable = true; UpdateDiffColors(); UpdateActionButtonStyles(true); } private void UpdateDiffColors() { foreach (var (key, getBtn) in diffSlots) { Button btn = getBtn(this); bool selected = key == selectedDifficulty; ApplyButtonStyle(btn, selected ? NeonBg : DarkButtonBg, selected, btn.interactable, false); } } // ── 다운로드 ────────────────────────────────────────────── private void OnDownloadClicked() { StartCoroutine(DownloadAllCoroutine()); } private IEnumerator DownloadAllCoroutine() { var diffs = new List(); foreach (var (key, _) in diffSlots) if (currentSong.difficulties.Get(key) != null) diffs.Add(key); if (diffs.Count == 0) yield break; SetInteractable(false); progressGroup.SetActive(true); downloadButton.gameObject.SetActive(false); deleteButton.gameObject.SetActive(false); playButton.gameObject.SetActive(false); int totalSteps = diffs.Count; int doneSteps = 0; bool failed = false; foreach (string diff in diffs) { bool stepDone = false; downloadManager.DownloadSong( currentSong, diff, onProgress: p => { float overall = (doneSteps + p) / totalSteps; progressSlider.value = overall; progressText.text = $"{diffs[Mathf.Min(doneSteps, diffs.Count - 1)].ToUpper()} {(int)(overall * 100)}%"; }, onComplete: () => { SongLibrary.Instance.MarkDownloaded(currentSong.id, diff); doneSteps++; stepDone = true; }, onError: err => { Debug.LogError($"[SongDetailPanel] {err}"); failed = true; stepDone = true; }); yield return new WaitUntil(() => stepDone); if (failed) break; } SetInteractable(true); progressGroup.SetActive(false); playButton.gameObject.SetActive(true); selectManager.RefreshCards(); RefreshUI(); if (!failed) Debug.Log($"[SongDetailPanel] '{currentSong.title}' 전체 다운로드 완료"); } // ── 삭제 ───────────────────────────────────────────────── private void OnDeleteClicked() { downloadManager.DeleteSong(currentSong.id); SongLibrary.Instance.MarkSongRemoved(currentSong.id); selectedDifficulty = null; selectManager.RefreshCards(); RefreshUI(); } // ── 플레이 ─────────────────────────────────────────────── private void OnPlayClicked() { GameSession.SelectedSong = currentSong; GameSession.SelectedDifficulty = selectedDifficulty; SceneManager.LoadScene(gameSceneName); } // ── 유틸 ───────────────────────────────────────────────── private void SetInteractable(bool value) { downloadButton.interactable = value; deleteButton.interactable = value; playButton.interactable = value && selectedDifficulty != null; foreach (var (_, getBtn) in diffSlots) getBtn(this).interactable = value; } private static string FormatDuration(int seconds) => $"{seconds / 60}:{seconds % 60:D2}"; private void HideDifficultyLabel() { Transform label = transform.Find("LblDifficulty"); if (label != null) label.gameObject.SetActive(false); } private void UpdateActionButtonStyles(bool downloaded) { ApplyButtonStyle(downloadButton, NeonBg, true, !downloaded, false); ApplyButtonStyle(deleteButton, DangerBg, true, downloaded, true); ApplyButtonStyle(playButton, NeonBg, true, playButton.interactable, false); ApplyButtonStyle(closeButton, DarkButtonBg, false, true, false); } private static void ApplyButtonStyle(Button btn, Color activeBg, bool outlined, bool enabled, bool danger) { if (btn == null) return; Color bg = enabled ? activeBg : DisabledBg; if (btn.targetGraphic is Image img) img.color = bg; var colors = btn.colors; colors.normalColor = bg; colors.highlightedColor = enabled ? (danger ? new Color(0.72f, 0.23f, 0.30f, 0.86f) : new Color(0.10f, 0.95f, 1.0f, 0.58f)) : DisabledBg; colors.pressedColor = enabled ? (danger ? new Color(0.42f, 0.10f, 0.15f, 0.92f) : new Color(0.02f, 0.58f, 0.72f, 0.80f)) : DisabledBg; colors.selectedColor = colors.highlightedColor; colors.disabledColor = DisabledBg; colors.fadeDuration = 0.08f; btn.colors = colors; TMP_Text label = btn.GetComponentInChildren(); if (label != null) label.color = enabled ? ButtonText : MutedText; Outline outline = btn.GetComponent() ?? btn.gameObject.AddComponent(); outline.enabled = outlined && enabled; outline.effectColor = danger ? new Color(1.0f, 0.35f, 0.42f, 0.34f) : NeonOutline; outline.effectDistance = new Vector2(0.0f, -0.28f); } private static MarqueeText ConfigureMarqueeText(TMP_Text text, float minSize, float maxSize) { if (text == null) return null; RectTransform textRect = text.rectTransform; Transform originalParent = textRect.parent; int siblingIndex = textRect.GetSiblingIndex(); string maskName = $"{text.name}Mask"; Transform existingMask = originalParent != null ? originalParent.Find(maskName) : null; RectTransform maskRect; if (existingMask != null) { maskRect = existingMask as RectTransform; if (textRect.parent != existingMask) textRect.SetParent(existingMask, false); } else { var mask = new GameObject(maskName); mask.transform.SetParent(originalParent, false); mask.transform.SetSiblingIndex(siblingIndex); maskRect = mask.AddComponent(); maskRect.anchorMin = textRect.anchorMin; maskRect.anchorMax = textRect.anchorMax; maskRect.pivot = textRect.pivot; maskRect.anchoredPosition = textRect.anchoredPosition; maskRect.sizeDelta = textRect.sizeDelta; maskRect.localRotation = textRect.localRotation; maskRect.localScale = textRect.localScale; mask.AddComponent(); textRect.SetParent(mask.transform, false); } textRect.anchorMin = new Vector2(0f, 0f); textRect.anchorMax = new Vector2(0f, 1f); textRect.pivot = new Vector2(0f, 0.5f); textRect.anchoredPosition = Vector2.zero; textRect.localRotation = Quaternion.identity; textRect.localScale = Vector3.one; textRect.sizeDelta = new Vector2(260.0f, 0f); ConfigureOneLineText(text, minSize, maxSize, TextAlignmentOptions.MidlineLeft); text.overflowMode = TextOverflowModes.Overflow; text.raycastTarget = false; MarqueeText marquee = text.GetComponent() ?? text.gameObject.AddComponent(); marquee.speed = 9f; marquee.pauseStart = 1.25f; marquee.pauseEnd = 0.8f; return marquee; } private static void ConfigureButtonText(Button btn, float minSize, float maxSize) { if (btn == null) return; TMP_Text label = btn.GetComponentInChildren(); ConfigureOneLineText(label, minSize, maxSize, TextAlignmentOptions.Center); if (label != null) label.raycastTarget = false; } private static void ConfigureOneLineText(TMP_Text text, float minSize, float maxSize, TextAlignmentOptions alignment) { if (text == null) return; text.enableAutoSizing = true; text.fontSizeMin = minSize; text.fontSizeMax = maxSize; text.alignment = alignment; text.overflowMode = TextOverflowModes.Ellipsis; text.textWrappingMode = TextWrappingModes.NoWrap; } }