Files
XRLib/Assets/Scripts/Factory/Playback/UI/UIPlaybackListModal.cs
2025-12-12 19:28:20 +09:00

395 lines
16 KiB
C#

#nullable enable
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Log;
using UVC.UI.Modal;
using UVC.Util;
namespace UVC.Factory.Playback.UI
{
/// <summary>
/// 📅 재생 목록(Playback List)을 날짜별로 보여주고, 원하는 시간대의 파일을 선택할 수 있는 모달 창입니다.
/// - 날짜 드롭다운, 시간별 리스트, 다운로드 상태, 선택/확인 버튼 등을 제공합니다.
///
/// <para>샘플 사용법:</para>
/// <code>
/// // 1. 모달을 띄우는 코드 예시
/// var content = new ModalContent(UIPlaybackListModal.PrefabPath)
/// {
/// Title = "Playback List",
/// ConfirmButtonText = "Play",
/// ShowCancelButton = false
/// };
/// // 모달을 열고, 사용자가 선택한 결과를 기다립니다.
/// var result = await Modal.Open<UIPlaybackListItemData>(content);
/// if (result != null)
/// {
/// Debug.Log($"선택된 파일: {result.date} {result.time}시");
/// }
/// </code>
/// </summary>
[RequireComponent(typeof(CanvasGroup))]
public class UIPlaybackListModal : ModalView
{
/// <summary>
/// 이 모달의 프리팹 경로(Resources 폴더 기준)
/// </summary>
public static readonly string PrefabPath = "Factory/Prefabs/Playback/UIPlaybackListModal";
/// <summary>
/// 프리팹에서 모달을 생성합니다.
/// </summary>
/// <param name="parent">부모 Transform (생략 가능)</param>
/// <returns>UIPlaybackListModal 인스턴스</returns>
/// <example>
/// var modal = UIPlaybackListModal.CreateFromPrefab(parentTransform);
/// </example>
public static UIPlaybackListModal CreateFromPrefab(Transform parent = null)
{
GameObject prefab = Resources.Load<GameObject>(PrefabPath);
GameObject go = UnityEngine.Object.Instantiate(prefab);
UIPlaybackListModal modal = go.GetComponent<UIPlaybackListModal>();
return modal;
}
// 날짜별로 시간대 파일 목록을 저장하는 딕셔너리
private Dictionary<string, List<UIPlaybackListItemData>>? data;
/// <summary>
/// '확인' 버튼을 누를 수 있는지 여부
/// (다운로드 완료된 항목이 선택되어 있고, 현재 다운로드 중인 항목이 없을 때만 true)
/// </summary>
public bool IsOkable => (selectedItem != null && selectedItem.status == UIPlaybackListItemStatus.Downloaded && UIPlaybackListItem.DownloadingItems.Count == 0);
// 현재 선택된 아이템 데이터
private UIPlaybackListItemData? selectedItem = null;
[SerializeField]
private TMP_Dropdown dropdownDate; // 날짜 선택 드롭다운
[SerializeField]
private ScrollRect scrollRectTime; // 시간대 리스트 스크롤 영역
// 모달이 닫힐 때 반환할 결과 데이터
private UIPlaybackListItemData? resultData = null;
/// <summary>
/// 모달이 열릴 때 호출됩니다. (비동기)
/// </summary>
/// <param name="content">모달에 표시할 내용/설정</param>
public override async UniTask OnOpen(ModalContent content)
{
await base.OnOpen(content); // 부모의 OnOpen을 먼저 호출해서 기본 UI를 설정해요.
initContent();
}
/// <summary>
/// 모달이 닫힐 때 결과로 반환할 데이터를 돌려줍니다.
/// </summary>
/// <returns>선택된 UIPlaybackListItemData 또는 null</returns>
public override UniTask<object?> GetResult()
{
if (data != null) data.Clear();
data = null;
return UniTask.FromResult<object?>(resultData);
}
/// <summary>
/// 모달이 닫힐 때 호출됩니다.
/// </summary>
public override async UniTask OnClose(ModalContent content)
{
await base.OnClose(content);
}
/// <summary>
/// 모달의 내용을 초기화합니다. (날짜/시간대 데이터 불러오기)
/// </summary>
private async void initContent()
{
confirmButton.interactable = false;
// 서버에서 날짜/시간대 목록을 받아옵니다.
Dictionary<string, Dictionary<string, string>>? data = await PlaybackService.Instance.RequestDataAsync();
dropdownDate.onValueChanged.AddListener(DropDownDateChanged);
LocalSetData(); // 로컬 파일 기준으로 먼저 리스트를 만듭니다.
if (data != null) SetData(data); // 서버 데이터가 있으면 반영합니다.
}
/// <summary>
/// 오브젝트가 파괴될 때 이벤트 해제 및 정리
/// </summary>
protected void OnDestroy()
{
dropdownDate.onValueChanged.RemoveListener(DropDownDateChanged);
UIPlaybackListItem.ClearAll();
}
/// <summary>
/// 로컬 저장소에서 날짜/시간대 파일 목록을 읽어와 리스트를 만듭니다.
/// </summary>
public void LocalSetData()
{
Dictionary<string, List<UIPlaybackListItemData>> newData = new Dictionary<string, List<UIPlaybackListItemData>>();
var dateList = new List<TMP_Dropdown.OptionData>();
string playbackPath = PlaybackService.PlaybackFolderPath;
DirectoryInfo di = new DirectoryInfo(playbackPath);
if (di.Exists)
{
var childDi = di.EnumerateDirectories();
foreach (var child in childDi)
{
string date = child.Name;
//Debug.Log($"date:{date}");
var childFiles = child.EnumerateFiles();
if (childFiles.Count() > 0 && !newData.ContainsKey(date)) newData[date] = new List<UIPlaybackListItemData>();
foreach (var childFile in childFiles)
{
//남은게 있다면 삭제
if (childFile.Extension == ".zip" || childFile.Extension == ".7z")
{
try
{
Debug.Log($"delete zip file:{childFile.FullName}");
childFile.Delete();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
if (childFile.Extension != ".sqlite") continue;
var sqlFileName = childFile.Name;
string time = sqlFileName.Split("_")[1].Replace(".sqlite", "");
newData[date].Add(new UIPlaybackListItemData()
{
date = date,
time = time,
zipFileName = $"{sqlFileName}.7z",
sqlFileName = sqlFileName,
status = UIPlaybackListItemStatus.Downloaded,
});
//fileName.Split(".")[0] + ".sqlite"
}
if (newData.ContainsKey(date) && newData[date].Count() > 0) dateList.Add(new TMP_Dropdown.OptionData(date));
}
}
this.data = newData;
dateList.Sort((b, a) => a.text.CompareTo(b.text));
dropdownDate.options = dateList;
dropdownDate.value = -1;
}
/// <summary>
/// 서버에서 받은 날짜/시간대 데이터를 리스트에 반영합니다.
/// </summary>
/// <param name="data">서버에서 받은 데이터</param>
public void SetData(Dictionary<string, Dictionary<string, string>> data)
{
//{
// "2024-12-05": {
// "0": "2024-12-05_0.sqlite.7z",
// "1": "2024-12-05_1.sqlite.7z",
// "2": "2024-12-05_2.sqlite.7z",
// }
//}
Dictionary<string, Dictionary<string, string>> dataList = new Dictionary<string, Dictionary<string, string>>();
// 날짜/시간대 정규화
foreach (var keyPair in data)
{
if (!dataList.ContainsKey(keyPair.Key)) dataList[keyPair.Key] = new Dictionary<string, string>();
foreach (var keyPair2 in keyPair.Value)
{
var arr = keyPair2.Value.Split(".");
string timeStr = arr[0];
DateTime dateTime = DateTimeUtil.Parse(timeStr, "yyyy-MM-dd_H");
string dateStr = dateTime.ToString("yyyy-MM-dd");
string dateKey = dateTime.Hour.ToString();
string dateValue = dateTime.ToString("yyyy-MM-dd_H") + "." + arr[1] + "." + arr[2];
ULog.Debug($"dateTime:{dateTime}, timeStr:{timeStr} dateValue:{dateValue}");
if (!dataList.ContainsKey(dateStr)) dataList[dateStr] = new Dictionary<string, string>();
if (!dataList[dateStr].ContainsKey(dateKey)) dataList[dateStr][dateKey] = dateValue;
}
}
foreach (var item in dataList.ToList())
{
if (item.Key.Count() == 0) dataList.Remove(item.Key);
}
Dictionary<string, List<UIPlaybackListItemData>> newData = new Dictionary<string, List<UIPlaybackListItemData>>();
var dateList = new List<TMP_Dropdown.OptionData>();
//로컬에 저장 되 있는데 sqlite 파일 찾아서 추가
string playbackPath = PlaybackService.PlaybackFolderPath;
DirectoryInfo di = new DirectoryInfo(playbackPath);
if (di.Exists)
{
var childDi = di.EnumerateDirectories();
foreach (var child in childDi)
{
string date = child.Name;
//Debug.Log($"date:{date}");
var childFiles = child.EnumerateFiles();
if (childFiles.Count() > 0 && !newData.ContainsKey(date)) newData[date] = new List<UIPlaybackListItemData>();
foreach (var childFile in childFiles)
{
//남은게 있다면 삭제
if (childFile.Extension == ".zip" || childFile.Extension == ".7z")
{
try
{
Debug.Log($"delete zip file:{childFile.FullName}");
childFile.Delete();
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
if (childFile.Extension != ".sqlite") continue;
var sqlFileName = childFile.Name;
string time = sqlFileName.Split("_")[1].Replace(".sqlite", "");
newData[date].Add(new UIPlaybackListItemData()
{
date = date,
time = time,
zipFileName = $"{sqlFileName}.7z",
sqlFileName = sqlFileName,
status = UIPlaybackListItemStatus.Downloaded,
});
//fileName.Split(".")[0] + ".sqlite"
}
if (newData.ContainsKey(date) && newData[date].Count() > 0) dateList.Add(new TMP_Dropdown.OptionData(date));
}
}
// 서버 데이터 기준으로 추가
foreach (var keyPair in dataList)
{
string date = keyPair.Key;
if (dateList.FindIndex(o => o.text == date) == -1) dateList.Add(new TMP_Dropdown.OptionData(date));
if (!newData.ContainsKey(date)) newData[date] = new List<UIPlaybackListItemData>();
foreach (var keyPair2 in keyPair.Value)
{
var zipFileName = keyPair2.Value;
var sqlFileName = zipFileName.Replace(".zip", "").Replace(".7z", "");
if (newData[date].FindIndex(item => item.sqlFileName == sqlFileName) == -1)
{
newData[date].Add(new UIPlaybackListItemData()
{
date = date,
time = keyPair2.Key,
zipFileName = zipFileName,
sqlFileName = sqlFileName,
status = UIPlaybackListItemStatus.Default,
});
}
}
// 시간 오름차순 정렬
if (newData.ContainsKey(date)) newData[date].Sort((a, b) => int.Parse(a.time) - int.Parse(b.time));
}
this.data = newData;
dateList.Sort((b, a) => a.text.CompareTo(b.text));
dropdownDate.options = dateList;
dropdownDate.value = -1;
}
/// <summary>
/// 날짜 드롭다운이 변경될 때 호출됩니다.
/// </summary>
/// <param name="value">선택된 인덱스</param>
private void DropDownDateChanged(int value)
{
// 선택된 날짜의 시간대 리스트를 갱신
//dropdownDatePlaceHolder.gameObject.SetActive(value == -1);
string key = dropdownDate.options[dropdownDate.value].text;
Debug.Log($"DropDownDateChanged dropdownDate.value:{dropdownDate.value} value:{value} key:{key}");
if (data != null && data.ContainsKey(key))
{
if (UIPlaybackListItem.ActiveItems.Count > 0) UIPlaybackListItem.ReleaseAll();
List<UIPlaybackListItemData> itemList = data[key];
foreach (var itemData in itemList)
{
UIPlaybackListItem item = UIPlaybackListItem.CreateFromPool(scrollRectTime.content);
item.Init(itemData);
item.OnSelect += OnItemSelect;
item.OnChangeStatus += OnItemChangeStatus;
}
selectedItem = null;
updateButtonStatus();
}
// 스크롤을 맨 위로 이동
scrollRectTime.normalizedPosition = new Vector2(0, 1);
}
/// <summary>
/// 시간대 아이템이 선택될 때 호출됩니다.
/// </summary>
/// <param name="data">선택된 아이템 데이터</param>
/// <param name="selected">선택 여부</param>
private void OnItemSelect(UIPlaybackListItemData data, bool selected)
{
if (selected)
{
List<UIPlaybackListItem> itemList = UIPlaybackListItem.ActiveItems;
foreach (var item in itemList)
{
if (item.Data.sqlFileName != data.sqlFileName)
{
item.Selected = false;
}
}
selectedItem = data;
}
else
{
selectedItem = null;
}
updateButtonStatus();
}
/// <summary>
/// 아이템의 다운로드 상태가 변경될 때 호출됩니다.
/// </summary>
private void OnItemChangeStatus(UIPlaybackListItemData data, UIPlaybackListItemStatus status)
{
bool enable = UIPlaybackListItem.DownloadingItems.Count == 0;
dropdownDate.interactable = enable;
}
/// <summary>
/// '확인' 버튼의 활성화 상태를 갱신합니다.
/// </summary>
private void updateButtonStatus()
{
confirmButton.interactable = IsOkable;
}
/// <summary>
/// '확인' 버튼 클릭 시 호출됩니다.
/// </summary>
public override void OnConfirmButtonClicked()
{
resultData = selectedItem;
}
}
}