feat: SongSelect panel — NAS song list with download/play in Menu.unity
- Add DownloadManager, SongLibrary, SongSelectManager, SongDetailPanel scripts - Rebuild SongSelect panel inside Menu.unity using VRBeatsKit style: left scroll list (ALL/OWNED tabs) + right detail panel (diff buttons, Download/Delete/Play) - SceneBuilder replaced: only ③ Menu — Rebuild SongSelect Panel remains - All UI text in English Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
public class DownloadManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string baseUrl = "http://whdwo798.synology.me/beatsaber";
|
||||
|
||||
private static string CacheRoot => Path.Combine(Application.temporaryCachePath, "beatsaber");
|
||||
|
||||
// ── Public API ───────────────────────────────────────────
|
||||
|
||||
public void FetchSongsList(Action<SongsList> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
StartCoroutine(GetText($"{baseUrl}/songs.json", json =>
|
||||
{
|
||||
SongsList list = JsonUtility.FromJson<SongsList>(json);
|
||||
if (list == null)
|
||||
onError?.Invoke("songs.json 파싱 실패");
|
||||
else
|
||||
onSuccess?.Invoke(list);
|
||||
}, onError));
|
||||
}
|
||||
|
||||
public void DownloadSong(SongInfo song, string difficulty,
|
||||
Action<float> onProgress, Action onComplete, Action<string> onError = null)
|
||||
{
|
||||
StartCoroutine(DownloadSongCoroutine(song, difficulty, onProgress, onComplete, onError));
|
||||
}
|
||||
|
||||
public void DeleteSong(string songId)
|
||||
{
|
||||
string dir = SongDir(songId);
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
Directory.Delete(dir, recursive: true);
|
||||
Debug.Log($"[DownloadManager] 삭제: {songId}");
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteDifficulty(SongInfo song, string difficulty)
|
||||
{
|
||||
string path = MapPath(song, difficulty);
|
||||
if (path != null && File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
public bool IsSongDownloaded(string songId)
|
||||
=> File.Exists(AudioPath(songId));
|
||||
|
||||
public bool IsDifficultyDownloaded(SongInfo song, string difficulty)
|
||||
{
|
||||
string path = MapPath(song, difficulty);
|
||||
return path != null && File.Exists(path);
|
||||
}
|
||||
|
||||
public string AudioPath(string songId)
|
||||
=> Path.Combine(SongDir(songId), $"{songId}.mp3");
|
||||
|
||||
public string MapPath(SongInfo song, string difficulty)
|
||||
{
|
||||
DifficultyInfo info = song.difficulties.Get(difficulty);
|
||||
if (info == null || string.IsNullOrEmpty(info.mapFile)) return null;
|
||||
string fileName = Path.GetFileName(info.mapFile);
|
||||
if (string.IsNullOrEmpty(fileName)) return null;
|
||||
return Path.Combine(SongDir(song.id), fileName);
|
||||
}
|
||||
|
||||
// ── 내부 구현 ─────────────────────────────────────────────
|
||||
|
||||
private IEnumerator DownloadSongCoroutine(SongInfo song, string difficulty,
|
||||
Action<float> onProgress, Action onComplete, Action<string> onError)
|
||||
{
|
||||
string songDir = Path.GetFullPath(SongDir(song.id));
|
||||
Directory.CreateDirectory(songDir);
|
||||
|
||||
// 1단계: 오디오 (70%)
|
||||
string audioPath = Path.Combine(songDir, $"{song.id}.mp3");
|
||||
if (!File.Exists(audioPath))
|
||||
{
|
||||
bool failed = false;
|
||||
yield return DownloadFile(
|
||||
$"{baseUrl}/{song.audioFile}", audioPath,
|
||||
p => onProgress?.Invoke(p * 0.7f),
|
||||
err => { onError?.Invoke(err); failed = true; });
|
||||
if (failed) yield break;
|
||||
}
|
||||
|
||||
// 2단계: 맵 파일 (30%)
|
||||
DifficultyInfo diffInfo = song.difficulties.Get(difficulty);
|
||||
if (diffInfo == null)
|
||||
{
|
||||
onError?.Invoke($"난이도 '{difficulty}' 없음");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(diffInfo.mapFile))
|
||||
{
|
||||
onError?.Invoke($"'{difficulty}' 맵 파일 정보 없음 — Creator에서 곡을 다시 생성해주세요");
|
||||
yield break;
|
||||
}
|
||||
|
||||
string mapPath = MapPath(song, difficulty);
|
||||
if (mapPath != null) mapPath = Path.GetFullPath(mapPath);
|
||||
if (mapPath == null)
|
||||
{
|
||||
onError?.Invoke($"'{difficulty}' 맵 경로 계산 실패");
|
||||
yield break;
|
||||
}
|
||||
if (!File.Exists(mapPath))
|
||||
{
|
||||
bool failed = false;
|
||||
yield return DownloadFile(
|
||||
$"{baseUrl}/{diffInfo.mapFile}", mapPath,
|
||||
p => onProgress?.Invoke(0.7f + p * 0.3f),
|
||||
err => { onError?.Invoke(err); failed = true; });
|
||||
if (failed) yield break;
|
||||
}
|
||||
|
||||
onProgress?.Invoke(1f);
|
||||
onComplete?.Invoke();
|
||||
Debug.Log($"[DownloadManager] 완료: {song.title} ({difficulty})");
|
||||
}
|
||||
|
||||
private IEnumerator DownloadFile(string url, string savePath,
|
||||
Action<float> onProgress, Action<string> onError)
|
||||
{
|
||||
using var req = UnityWebRequest.Get(url);
|
||||
req.downloadHandler = new DownloadHandlerFile(savePath);
|
||||
req.SendWebRequest();
|
||||
|
||||
while (!req.isDone)
|
||||
{
|
||||
onProgress?.Invoke(req.downloadProgress);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (req.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
if (File.Exists(savePath)) File.Delete(savePath);
|
||||
onError?.Invoke($"다운로드 실패: {url} — {req.error}");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator GetText(string url, Action<string> onSuccess, Action<string> onError)
|
||||
{
|
||||
using var req = UnityWebRequest.Get(url);
|
||||
yield return req.SendWebRequest();
|
||||
|
||||
if (req.result != UnityWebRequest.Result.Success)
|
||||
onError?.Invoke($"요청 실패: {url} — {req.error}");
|
||||
else
|
||||
onSuccess?.Invoke(req.downloadHandler.text);
|
||||
}
|
||||
|
||||
private static string SongDir(string songId)
|
||||
=> Path.Combine(CacheRoot, songId);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8efd2469f7355140ae71425ecc638e0
|
||||
@@ -0,0 +1,230 @@
|
||||
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 SelectedColor = new Color(0.4f, 0.8f, 1f);
|
||||
private static readonly Color DeselectedColor = Color.white;
|
||||
|
||||
private SongInfo currentSong;
|
||||
private string selectedDifficulty;
|
||||
private DownloadManager downloadManager;
|
||||
private SongSelectManager selectManager;
|
||||
|
||||
private readonly (string key, Func<SongDetailPanel, Button> btn)[] diffSlots =
|
||||
{
|
||||
("normal", p => p.btnNormal),
|
||||
("hard", p => p.btnHard),
|
||||
("expert", p => p.btnExpert),
|
||||
("expertplus", p => p.btnExpertPlus),
|
||||
};
|
||||
|
||||
// ── 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)}";
|
||||
|
||||
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);
|
||||
playButton.interactable = downloaded && selectedDifficulty != null;
|
||||
progressGroup.SetActive(false);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void UpdateDiffColors()
|
||||
{
|
||||
foreach (var (key, getBtn) in diffSlots)
|
||||
{
|
||||
Button btn = getBtn(this);
|
||||
var colors = btn.colors;
|
||||
colors.normalColor = (key == selectedDifficulty) ? SelectedColor : DeselectedColor;
|
||||
btn.colors = colors;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 다운로드 ──────────────────────────────────────────────
|
||||
|
||||
private void OnDownloadClicked()
|
||||
{
|
||||
StartCoroutine(DownloadAllCoroutine());
|
||||
}
|
||||
|
||||
private IEnumerator DownloadAllCoroutine()
|
||||
{
|
||||
var diffs = new List<string>();
|
||||
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}";
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d27acdc84ca9a6241894ce7ee9f3c3fa
|
||||
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
public class SongLibrary : MonoBehaviour
|
||||
{
|
||||
public static SongLibrary Instance { get; private set; }
|
||||
|
||||
private const string FileName = "song_library.json";
|
||||
private static string SavePath => Path.Combine(Application.persistentDataPath, FileName);
|
||||
|
||||
private LibraryData _data = new LibraryData();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null) { Destroy(gameObject); return; }
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Load();
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────
|
||||
|
||||
public void MarkDownloaded(string songId, string difficulty)
|
||||
{
|
||||
LibraryEntry entry = GetOrCreate(songId);
|
||||
if (!entry.difficulties.Contains(difficulty))
|
||||
entry.difficulties.Add(difficulty);
|
||||
entry.lastAccessedAt = DateTime.UtcNow.ToString("o");
|
||||
Save();
|
||||
}
|
||||
|
||||
public void MarkDifficultyRemoved(string songId, string difficulty)
|
||||
{
|
||||
LibraryEntry entry = Find(songId);
|
||||
if (entry == null) return;
|
||||
|
||||
entry.difficulties.Remove(difficulty);
|
||||
if (entry.difficulties.Count == 0)
|
||||
_data.entries.Remove(entry);
|
||||
Save();
|
||||
}
|
||||
|
||||
public void MarkSongRemoved(string songId)
|
||||
{
|
||||
_data.entries.RemoveAll(e => e.songId == songId);
|
||||
Save();
|
||||
}
|
||||
|
||||
public void TouchSong(string songId)
|
||||
{
|
||||
LibraryEntry entry = Find(songId);
|
||||
if (entry == null) return;
|
||||
entry.lastAccessedAt = DateTime.UtcNow.ToString("o");
|
||||
Save();
|
||||
}
|
||||
|
||||
public bool IsSongDownloaded(string songId)
|
||||
=> Find(songId) != null;
|
||||
|
||||
public bool IsDifficultyDownloaded(string songId, string difficulty)
|
||||
=> Find(songId)?.difficulties.Contains(difficulty) ?? false;
|
||||
|
||||
public List<LibraryEntry> GetAll()
|
||||
=> _data.entries;
|
||||
|
||||
public void ValidateWithFileSystem(DownloadManager dm, List<SongInfo> songs)
|
||||
{
|
||||
bool dirty = false;
|
||||
foreach (SongInfo song in songs)
|
||||
{
|
||||
LibraryEntry entry = Find(song.id);
|
||||
if (entry == null) continue;
|
||||
|
||||
if (!dm.IsSongDownloaded(song.id))
|
||||
{
|
||||
_data.entries.Remove(entry);
|
||||
dirty = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.difficulties.RemoveAll(d => !dm.IsDifficultyDownloaded(song, d));
|
||||
if (entry.difficulties.Count == 0)
|
||||
{
|
||||
_data.entries.Remove(entry);
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if (dirty) Save();
|
||||
}
|
||||
|
||||
// ── 내부 구현 ─────────────────────────────────────────────
|
||||
|
||||
private LibraryEntry Find(string songId)
|
||||
=> _data.entries.Find(e => e.songId == songId);
|
||||
|
||||
private LibraryEntry GetOrCreate(string songId)
|
||||
{
|
||||
LibraryEntry entry = Find(songId);
|
||||
if (entry != null) return entry;
|
||||
entry = new LibraryEntry { songId = songId };
|
||||
_data.entries.Add(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
if (!File.Exists(SavePath)) return;
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(SavePath);
|
||||
_data = JsonUtility.FromJson<LibraryData>(json) ?? new LibraryData();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[SongLibrary] 로드 실패, 초기화: {e.Message}");
|
||||
_data = new LibraryData();
|
||||
}
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
File.WriteAllText(SavePath, JsonUtility.ToJson(_data, true));
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LibraryData
|
||||
{
|
||||
public List<LibraryEntry> entries = new List<LibraryEntry>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LibraryEntry
|
||||
{
|
||||
public string songId;
|
||||
public List<string> difficulties = new List<string>();
|
||||
public string lastAccessedAt;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 765cf3a9cd9c14943be42e1cee050abd
|
||||
@@ -0,0 +1,200 @@
|
||||
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 = Color.white;
|
||||
private static readonly Color TabInactive = new Color(0.6f, 0.6f, 0.6f);
|
||||
|
||||
private static string CachePath =>
|
||||
Path.Combine(Application.persistentDataPath, "songs_cache.json");
|
||||
|
||||
private List<SongInfo> allSongs = new List<SongInfo>();
|
||||
private bool showingOwned = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
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)
|
||||
{
|
||||
var colors = btn.colors;
|
||||
colors.normalColor = c;
|
||||
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()
|
||||
{
|
||||
for (int i = cardContainer.childCount - 1; i >= 0; i--)
|
||||
Destroy(cardContainer.GetChild(i).gameObject);
|
||||
|
||||
List<SongInfo> songs = showingOwned
|
||||
? allSongs.FindAll(s => SongLibrary.Instance.IsSongDownloaded(s.id))
|
||||
: allSongs;
|
||||
|
||||
foreach (SongInfo song in songs)
|
||||
SpawnCard(song);
|
||||
}
|
||||
|
||||
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
|
||||
var titleGO = new GameObject("Title");
|
||||
titleGO.transform.SetParent(card.transform, false);
|
||||
var tr = titleGO.AddComponent<RectTransform>();
|
||||
tr.anchorMin = new Vector2(0f, 0.5f);
|
||||
tr.anchorMax = new Vector2(1f, 1f);
|
||||
tr.offsetMin = new Vector2(5f, 0f);
|
||||
tr.offsetMax = new Vector2(downloaded ? -18f : -3f, -1f);
|
||||
var tTmp = titleGO.AddComponent<TextMeshProUGUI>();
|
||||
tTmp.text = song.title;
|
||||
tTmp.fontSize = 5f;
|
||||
tTmp.color = Color.white;
|
||||
tTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
tTmp.overflowMode = TextOverflowModes.Ellipsis;
|
||||
|
||||
// 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>();
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46403458ee5537142ad1e0b2ce7d3995
|
||||
Reference in New Issue
Block a user