playback 기능 추가

This commit is contained in:
logonkhi
2025-07-22 19:58:14 +09:00
parent cf97c6b61b
commit 4d29143d47
124 changed files with 15059 additions and 628 deletions

View File

@@ -0,0 +1,43 @@
using UnityEngine;
using UVC.Data;
using UVC.Factory.Playback.UI;
using UVC.UI.Commands;
using UVC.UI.Loading;
using UVC.UI.Modal;
namespace UVC.Factory.Playback
{
public class PlaybackCommand : ICommand
{
public async void Execute(object parameter = null)
{
var modalContent = new ModalContent("Prefabs/Factory/Playback/UIPlaybackListModal")
{
Title = "Playback List",
ConfirmButtonText = "Play",
ShowCancelButton = false
};
object result = await UVC.UI.Modal.Modal.Open<bool>(modalContent);
Debug.Log($"PlaybackCommand result:{result}");
if (result != null)
{
UIPlaybackListItemData data = (UIPlaybackListItemData)result;
Debug.Log($"PlaybackCommand data:{data}");
UIPlayback.Instance.Show();
DataRepository.Instance.MqttReceiver.Stop();
await UIPlayback.Instance.SetData(data.date, data.time, data.sqlFileName);
}
else
{
UILoading.Show();
PlaybackService.Instance.Stop();
UILoading.Hide();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 071f03b6e7e6d0342a5fe94183a1db70

View File

@@ -0,0 +1,107 @@
#nullable enable
using Best.HTTP;
using Cysharp.Threading.Tasks;
using SampleProject.Config;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.Network;
namespace UVC.Factory.Playback
{
public class PlaybackRepository
{
private PlaybackSQLiteService? sqliteService = null;
public async UniTask<Dictionary<string, Dictionary<string, string>>?> RequestPlaybackDateList()
{
//local
//string path = System.IO.Path.Combine(Application.streamingAssetsPath, "playbackList.json");
//string json = System.IO.File.ReadAllText(path);
//HttpResponseModel<Dictionary<string, Dictionary<string, string>>> localData = JsonHelper.FromJson<HttpResponseModel<Dictionary<string, Dictionary<string, string>>>>(json);
//return new EntityWithState<Dictionary<string, Dictionary<string, string>>>(APIState.Loaded, localData.data);
try
{
return await UniTask.RunOnThreadPool<Dictionary<string, Dictionary<string, string>>?>(async () =>
{
var response = await HttpRequester.RequestGet<HttpResponseModel<Dictionary<string, Dictionary<string, string>>>>($"{Constants.PlaybackDomain}/playback/list");
if (response.message.ToLower() == "success")
{
return new Dictionary<string, Dictionary<string, string>>(response.data);
}
return null;
});
}
catch (Exception e)
{
Debug.Log($"Exception {e.ToString()}");
return null;
}
}
public HTTPRequest? DownloadPlaybackData(string fileName, string savePath, Action<long, long> OnProgress, Action OnComplete, Action<string> OnError)
{
try
{
return HttpRequester.Download($"{Constants.PlaybackDomain}/playback/{fileName}", savePath, OnComplete, OnProgress, OnError);
}
catch (Exception e)
{
Debug.Log($"Exception {e.ToString()}");
return null;
}
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// </summary>
/// <param name="date"></param>
/// <param name="sqlFileName"></param>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecondAsync(string date, string sqlFileName, string selectTime, int second, bool orderAsc = true, int limit = 0)
{
validationSqliteService(date, sqlFileName);
return await sqliteService!.SelectBySecond(selectTime, second, orderAsc, limit);
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// </summary>
/// <param name="date"></param>
/// <param name="sqlFileName"></param>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecondBaseInfo(string date, string sqlFileName, string selectTime, int second = 59, bool orderAsc = true, int limit = 1)
{
validationSqliteService(date, sqlFileName);
return await sqliteService!.SelectBySecondBaseInfo(selectTime, second, orderAsc, limit);
}
private void validationSqliteService(string date, string sqlFileName)
{
if (sqliteService == null) sqliteService = new PlaybackSQLiteService();
if (sqliteService.Connected)
{
if (sqliteService.Date != date || sqliteService.SqliteFileName != sqlFileName)
{
sqliteService.CloseDB();
}
}
sqliteService.Connect(date, sqlFileName);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 131e22b5d8e3f214cafd3ad0848fce28

View File

@@ -0,0 +1,164 @@
using Cysharp.Threading.Tasks;
using SQLite4Unity3d;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using UVC.Util;
namespace UVC.Factory
{
public class PlaybackSQLiteService
{
//#region Singleton
//private static readonly SQLiteService instance = new SQLiteService();
//public static SQLiteService Instance => instance;
//static SQLiteService() { }
//#endregion
private SQLiteConnection dbConnection;
public bool Connected { get => dbConnection != null; }
private string date;
public string Date { get => date; }
private string sqliteFileName;
public string SqliteFileName { get => sqliteFileName; }
public void Connect(string date, string sqliteFileName)
{
this.date = date;
this.sqliteFileName = sqliteFileName;
dbConnection = new SQLiteConnection(Path.Combine(Application.streamingAssetsPath, "playback", date, sqliteFileName));
}
public void CloseDB()
{
dbConnection.Close();
dbConnection = null;
}
/// <summary>
/// 추가하기
/// </summary>
/// <param name="data"></param>
/// <param name="timeStamp">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="temp"></param>
/// <returns>데이터베이스에서 추가된 행 수</returns>
public int Insert(string data, string timeStamp, string temp = null)
{
var query = $"INSERT INTO realTime (data, timestamp, temp) VALUES ('{data}', '{timeStamp}', " + (temp == null ? "null" : "'" + temp + "'") + ");";
int changedRowLen = dbConnection.Execute(query);
return changedRowLen;
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// second > 0 : selectTime <= data < selectTime + second
/// second < 0 : selectTime + second < data <= selectTime
/// </summary>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
///
readonly string[] queryParts =
{
"SELECT * FROM realTime WHERE ",
"timestamp >= '",
"' AND timestamp < '",
"timestamp <= '",
"' AND timestamp > '",
" ORDER BY timestamp ",
" LIMIT ",
};
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecond(string selectTime, int second, bool orderAsc = true, int limit = 0)
{
List<PlaybackSQLiteDataEntity> result = await UniTask.RunOnThreadPool(() =>
{
DateTime date = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
string targetTime = DateTimeUtil.FormatTime(date);
//Debug.Log($"SelectBySecondBaseInfo {selectTime} {second} {targetTime} {date}");
queryBuilder.Append(queryParts[0]);
//second가 selectTime 보다 더 미래면
if (second > 0)
{
queryBuilder.Append($"{queryParts[1]}{selectTime}{queryParts[2]}{targetTime}'");
}
else
{
//second가 selectTime 보다 더 과거면
queryBuilder.Append($"{queryParts[3]}{selectTime}{queryParts[4]}{targetTime}'");
}
queryBuilder.Append($"{queryParts[5]}{(orderAsc ? "asc" : "desc")}");
if (limit > 0)
queryBuilder.Append($"{queryParts[6]}{limit}");
queryBuilder.Append(";");
//Debug.Log($"SelectBySecond {query}");
var query = queryBuilder.ToString();
queryBuilder.Clear();
return dbConnection.Query<PlaybackSQLiteDataEntity>(query);
});
return result;
}
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// second > 0 : selectTime <= data < selectTime + second
/// second < 0 : selectTime + second < data <= selectTime
/// </summary>
/// <param name="selectTime">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
///
StringBuilder queryBuilder = new();
public async UniTask<List<PlaybackSQLiteDataEntity>> SelectBySecondBaseInfo(string selectTime, int second, bool orderAsc = false, int limit = 1)
{
List<PlaybackSQLiteDataEntity> result = await UniTask.RunOnThreadPool(() =>
{
DateTime date = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
string targetTime = DateTimeUtil.FormatTime(date);
//Debug.Log($"SelectBySecondBaseInfo {selectTime} {second} {targetTime} {date}");
queryBuilder.Append($"SELECT * FROM baseInfo WHERE ");
//second가 selectTime 보다 더 미래면
if (second > 0)
{
queryBuilder.Append($"timestamp >= '{selectTime}' AND timestamp < '{targetTime}'");
}
else
{
//second가 selectTime 보다 더 과거면
queryBuilder.Append($"timestamp <= '{selectTime}' AND timestamp > '{targetTime}'");
}
queryBuilder.Append($" ORDER BY timestamp {(orderAsc ? "asc" : "desc")}");
if (limit > 0) queryBuilder.Append($" LIMIT {limit}");
queryBuilder.Append(";");
//Debug.Log($"SelectBySecondBaseInfo {query}");
var query = queryBuilder.ToString();
queryBuilder.Clear();
return dbConnection.Query<PlaybackSQLiteDataEntity>(query);
});
return result;
}
}
[System.Serializable]
public class PlaybackSQLiteDataEntity
{
public string data { get; set; }
[PrimaryKey]
public string timestamp { get; set; }
public DateTime timestampHungary { get => DateTimeUtil.UtcStringToKoreaDateTime(timestamp); }
public string temp { get; set; }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 10c4a6d7e00d6e04eb5efd504572f689

View File

@@ -0,0 +1,207 @@
#nullable enable
using Best.HTTP;
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UVC.Data;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Json;
using UVC.Util;
namespace UVC.Factory.Playback
{
public class PlaybackService
{
#region Singleton
private static readonly PlaybackService instance = new PlaybackService(new PlaybackRepository());
public static PlaybackService Instance => instance;
static PlaybackService() { }
#endregion
private readonly PlaybackRepository repository;
private string date;
private string time;
private string fileName;
public Action OnStopPlayback;
public PlaybackService(PlaybackRepository repository)
{
this.repository = repository;
}
public async UniTask<Dictionary<string, Dictionary<string, string>>?> RequestDataAsync()
{
Dictionary<string, Dictionary<string, string>>? data = await repository.RequestPlaybackDateList();
return data;
}
public async UniTask DispatchBaseInfoData(string date, string time, string fileName, string minute = "00", string second = "00")
{
Debug.Log($"DispatchBaseInfoData {date} {time} {minute} {second} {fileName}");
//헝가리 시간임
this.date = date;
this.time = time;
this.fileName = fileName;
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:{minute}:{second}.000Z");
string formatTime = DateTimeUtil.FormatTime(dateTime);
//baseInfo 가져오기
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondBaseInfo(date, fileName, formatTime);
if (list.Count > 0)
{
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.setSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
await HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="second">0 ~ 3600</param>
public async UniTask DispatchRealTimeData(int second, int speed)
{
int newSecond = second;
if (newSecond > 36000) newSecond = 36000;
//utc 시간으로 변환
DateTime dateTime = DateTimeUtil.UtcParse($"{date}T{int.Parse(time).ToString("00")}:00:00.000Z").AddSeconds(newSecond);//.Add(-DateTimeUtil.UtcKoreaGap);
string formatTime = DateTimeUtil.FormatTime(dateTime);
List<PlaybackSQLiteDataEntity> list = await repository.SelectBySecondAsync(date, fileName, formatTime, 1);
if (list.Count > 0)
{
HttpRequestConfig httpRequestConfig = new HttpRequestConfig("");
httpRequestConfig.setSplitResponseByKey(true);
httpRequestConfig.AddSplitConfig("AGV", DataMapperValidator.Get("AGV"));
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
await HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
}
public void Stop()
{
OnStopPlayback?.Invoke();
}
public HTTPRequest? ReadyData(string date, string time, string fileName, Action<long, long, float> OnProgress, Action<string> OnComplete)
{
//date : "2024-12-05"
//fileName : "2024-12-05_0.sqlite.7z"
string playbackPath = Path.Combine(Application.streamingAssetsPath, "playback");
string tempPath = Path.Combine(playbackPath, "temp");//한국 시간으로 변경하기 때문에 임시 폴더 만들어서 압축 해제 후 이동
string datePath = Path.Combine(playbackPath, date);
var fileNameArr = fileName.Split(".");
string zipFilePath = Path.Combine(datePath, fileName);
string sqlFilePath = Path.Combine(datePath, fileNameArr[0] + ".sqlite");
DateTime utcDateTime = DateTimeUtil.Parse(fileNameArr[0], "yyyy-MM-dd_H").Add(-DateTimeUtil.UtcKoreaGap);
string utcDatePath = Path.Combine(playbackPath, utcDateTime.ToString("yyyy-MM-dd"));
string utcFileName = utcDateTime.ToString("yyyy-MM-dd_H") + "." + fileNameArr[1] + "." + fileNameArr[2];
var utcFileNameArr = utcFileName.Split(".");
string utcZipFilePath = Path.Combine(tempPath, utcFileName);
string utcSqlFilePath = Path.Combine(tempPath, utcFileNameArr[0] + ".sqlite");
if (!Directory.Exists(playbackPath)) Directory.CreateDirectory(playbackPath);
if (!Directory.Exists(datePath)) Directory.CreateDirectory(datePath);
if (!Directory.Exists(utcDatePath)) Directory.CreateDirectory(utcDatePath);
if (!Directory.Exists(tempPath)) Directory.CreateDirectory(tempPath);
if (File.Exists(sqlFilePath))
{
Debug.Log($"ONREADY SQP FILE");
if (OnProgress != null) OnProgress.Invoke(100, 100, 1.0f);
if (OnComplete != null) OnComplete.Invoke(null);
return null;
}
else
{
long downloadTotal = 0;
return repository.DownloadPlaybackData(utcFileName, utcZipFilePath,
(long progress, long length) =>
{
float percent = (float)progress / (float)length;
Debug.Log($"DownloadPlaybackData OnProgress:{percent}");
if (OnProgress != null) OnProgress.Invoke(progress, length, percent / 2);
downloadTotal = length;
}, async () =>
{
await UniTask.Delay(500);
Debug.Log($"DownloadPlaybackData OnComplete");
if (File.Exists(utcZipFilePath))
{
if (OnProgress != null) OnProgress.Invoke(50, 100, 0.5f);
//압축해제 후
var zipper = new Zipper();
string errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
{
if (OnProgress != null)
{
OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
if (0.5f + percent / 2 > 100f)
{
Debug.Log($" DownloadReadData :{downloadTotal + read} , DownloadTotalData :{downloadTotal + total} ,DownloadPlaybackData OnProgress:{percent}");
}
}
});
Debug.Log($"zipper1 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
////파일 접근 문제면 2회 0.5초 후에 다시 실행.
//if (errorMessage == "Could not open input(7z) file")
//{
// await UniTask.Delay(500);
// errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
// {
// if (OnProgress != null) OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
// });
// Debug.Log($"zipper2 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
//if (errorMessage == "Could not open input(7z) file")
//{
// await UniTask.Delay(500);
// errorMessage = await zipper.Decompress(utcZipFilePath, tempPath, (long read, long total, float percent) =>
// {
// if (OnProgress != null) OnProgress.Invoke(downloadTotal + read, downloadTotal + total, 0.5f + percent / 2);
// });
// Debug.Log($"zipper3 errorMessage:{errorMessage} utcSqlFilePath:{utcSqlFilePath} sqlFilePath:{sqlFilePath} utcZipFilePath:{utcZipFilePath}");
//}
//압축해제 한 파일 이동
if (File.Exists(utcSqlFilePath))
{
//동일한 파일명이 있을경우 제거후 다시
File.Copy(utcSqlFilePath, sqlFilePath);
File.Delete(utcSqlFilePath);
}
//zip 파일 삭제
File.Delete(utcZipFilePath);
if (OnComplete != null) OnComplete.Invoke(errorMessage);
}
},
(string? error) =>
{
Debug.Log($"DownloadPlaybackData OnError:{error}");
if (OnComplete != null) OnComplete.Invoke(error);
});
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: deed16ffd893d334b937460dfaa00de3

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 866839c3051b5dc46bb738a62bef8fb3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,265 @@
using Cysharp.Threading.Tasks;
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extension;
using UVC.UI;
using UVC.UI.Loading;
namespace UVC.Factory.Playback.UI
{
public class UIPlayback : MonoBehaviour
{
private static UIPlayback instance;
public static UIPlayback Instance
{
get
{
if (instance == null) instance = CreateUIPlayBack();
return instance;
}
}
private static UIPlayback CreateUIPlayBack()
{
GameObject prefab = Resources.Load<GameObject>("Prefabs/Factory/Playback/UIPlayback");
GameObject go = GameObject.Instantiate(prefab);
return go.GetComponent<UIPlayback>();
}
[SerializeField]
[Tooltip("종료 버튼")]
private Button exitButton;
[SerializeField]
[Tooltip("종료 버튼")]
private TextMeshProUGUI dateTimeTxt0;
[SerializeField]
[Tooltip("종료 버튼")]
private TextMeshProUGUI dateTimeTxt1;
[SerializeField]
[Tooltip("play 버튼")]
private Button playButton;
[SerializeField]
[Tooltip("play 버튼 이미지")]
private Image playButtonImage;
[SerializeField]
[Tooltip("play 버튼 이미지 Sprite")]
private Sprite playButtonImagePlay;
[SerializeField]
[Tooltip("play 버튼 Puase 이미지 Sprite")]
private Sprite playButtonImagePause;
[SerializeField]
[Tooltip("Speed Slider")]
private UISliderWithLabel sliderSpeed;
[SerializeField]
[Tooltip("투명 조절 Slider")]
private SliderWithEvent opacitySlider;
[SerializeField]
[Tooltip("Progress Bar")]
private UIPlaybackProgressBar progressBar;
[SerializeField]
private CanvasGroup canvasGroup;
private bool isPlaying = false;
private bool preparingData = false;
private string date;
private string time;
private string fileName;
private bool isTick = false;
private bool IsTick
{
get => isTick;
set
{
if (isTick != value)
{
var temp = isTick;
isTick = value;
if (!temp && value) OnTimer().Forget();
}
}
}
private void Init()
{
exitButton.onClick.AddListener(OnClickExit);
playButton.onClick.AddListener(OnClickPlay);
progressBar.OnChangeValue += OnChangeProgress;
sliderSpeed.OnChangeValue += OnChangeSpeed;
opacitySlider.onValueChanged.AddListener(OnValueChangedOpcity);
}
private void OnDestroy()
{
exitButton.onClick.RemoveListener(OnClickExit);
playButton.onClick.RemoveListener(OnClickPlay);
progressBar.OnChangeValue = null;
sliderSpeed.OnChangeValue = null;
opacitySlider.onValueChanged.RemoveListener(OnValueChangedOpcity);
if (isPlaying) IsTick = false;
}
public void Show()
{
if (playButton == null) Init();
gameObject.SetActive(true);
if (transform.parent == null)
{
var canvases = GameObject.FindObjectsByType<Canvas>(FindObjectsSortMode.None);
foreach (var canvas in canvases)
{
if (canvas.name == "ModalCanvas")
{
transform.SetParent(canvas.transform, false);
break;
}
}
}
}
public void Hide()
{
Time.timeScale = 1;
IsTick = false;
gameObject.SetActive(false);
}
private void OnClickExit()
{
UILoading.Show();
isPlaying = false;
UpdatePlayState();
Hide();
PlaybackService.Instance.Stop();
UILoading.Hide();
}
private void OnClickPlay()
{
isPlaying = !isPlaying;
UpdatePlayState();
}
private void OnChangeProgress(int newValue)
{
ChangePlayTime().Forget();
}
private void OnChangeSpeed(int newValue)
{
if (isPlaying)
{
if (Time.timeScale != sliderSpeed.Value) Time.timeScale = sliderSpeed.Value;
}
else
{
Time.timeScale = 1;
}
}
private void OnValueChangedOpcity(float newValue)
{
canvasGroup.alpha = opacitySlider.value;
}
private async UniTaskVoid ChangePlayTime()
{
bool tempIsPlaing = isPlaying;
isPlaying = false;
int newSecond = (int)progressBar.Value;
if (newSecond == progressBar.MaxValue)
{
newSecond -= 60;
progressBar.Value = newSecond;
}
preparingData = true;
progressBar.Interactable = !preparingData;
IsTick = false;
UILoading.Show();
UpdatePlayState();
await UniTask.WaitForSeconds(0.5f);
int minute = (int)newSecond / 60;
int seconds = (int)newSecond % 60;
await PlaybackService.Instance.DispatchBaseInfoData(date, time, fileName, minute.ToString("00"), seconds.ToString("00"));
preparingData = false;
progressBar.Interactable = !preparingData;
if (isPlaying != tempIsPlaing)
{
isPlaying = tempIsPlaing;
UpdatePlayState();
}
UILoading.Hide();
await UniTask.WaitForSeconds(0.5f);
}
public async UniTask SetData(string date, string time, string fileName)
{
Init();
this.date = date;
this.time = time;
this.fileName = fileName;
Debug.Log($"UIPlayback SetData {date} {time}");
int timeInt = int.Parse(time);
dateTimeTxt0.text = dateTimeTxt1.text = date.Substring(2).Replace("-", ".");
progressBar.Init(timeInt);
sliderSpeed.Init();
Time.timeScale = 1;
canvasGroup.alpha = opacitySlider.value = 1;
preparingData = true;
progressBar.Interactable = !preparingData;
isPlaying = false;
UpdatePlayState();
//UILoading.Show();
await UniTask.WaitForSeconds(0.5f);
await PlaybackService.Instance.DispatchBaseInfoData(date, time, fileName);
preparingData = false;
progressBar.Interactable = !preparingData;
//UILoading.Hide();
await UniTask.WaitForSeconds(0.5f);
}
private void UpdatePlayState()
{
playButton.enabled = false;
progressBar.enabled = false;
playButtonImage.sprite = isPlaying ? playButtonImagePause : playButtonImagePlay;
IsTick = isPlaying;
progressBar.enabled = true;
playButton.enabled = true;
OnChangeSpeed(sliderSpeed.Value);
}
private async UniTaskVoid OnTimer()
{
if (progressBar.Value == progressBar.MaxValue)
{
if (isPlaying) OnClickPlay();
return;
}
progressBar.Value += 1;
//PlaybackService.Instance.DispatchingTimelineEvent = false;
PlaybackService.Instance.DispatchRealTimeData(progressBar.Value, sliderSpeed.Value).Forget();
if (isTick)
{
//PlaybackService.Instance.DispatchingTimelineEvent = true;
await UniTask.Delay(TimeSpan.FromMilliseconds(1000 / sliderSpeed.Value));
OnTimer().Forget();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6ca072637b87cb24eba398b90a1d49e9

View File

@@ -0,0 +1,311 @@
using Best.HTTP;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UVC.Extension;
using UVC.Pool;
using UVC.UI.Modal;
using UVC.Util;
namespace UVC.Factory.Playback.UI
{
public enum UIPlaybackListItemStatus
{
Default,
Downloading,
Downloaded
}
public class UIPlaybackListItemData
{
public string date = "";
public string time = "";
public string zipFileName = "";
public string sqlFileName = "";
public UIPlaybackListItemStatus status = UIPlaybackListItemStatus.Default;
public override string ToString()
{
return $"date:{date}, time:{time}, zipFileName:{zipFileName}, sqlFileName:{sqlFileName}, status:{status}";
}
}
public class UIPlaybackListItem : UnityEngine.MonoBehaviour, IPointerClickHandler
{
protected static ItemPool<UIPlaybackListItem> pool;
public static readonly string PrefabPath = "Prefabs/Factory/Playback/UIPlaybackListItem";
public static UIPlaybackListItem CreateFromPool(Transform parent)
{
if (pool == null)
{
GameObject prefab = Resources.Load<GameObject>(PrefabPath);
pool = new ItemPool<UIPlaybackListItem>(prefab, parent, null);
}
return pool.GetItem(true, parent);
}
public static List<UIPlaybackListItem> ActiveItems
{
get
{
if (pool != null) return (List<UIPlaybackListItem>)pool.ActiveItems;
return new List<UIPlaybackListItem>();
}
}
public static List<UIPlaybackListItem> DownloadingItems
{
get
{
return ActiveItems.Where(x => x.Status == UIPlaybackListItemStatus.Downloading).ToList();
}
}
public static void ReleaseAll()
{
if (pool != null)
{
foreach (var item in pool.ActiveItems) item.Release();
pool.ReturnAll();
}
}
public static void ClearAll()
{
if (pool != null)
{
foreach (var item in pool.ActiveItems) item.Release();
pool.ReturnAll();
pool.ClearRecycledItems();
}
}
[SerializeField]
private Image loadingImage;
[SerializeField]
private Image selectedImage;
[SerializeField]
private Button downloadButton;
[SerializeField]
private Button stopButton;
[SerializeField]
private Button deleteButton;
[SerializeField]
private TextMeshProUGUI downloadText;
[SerializeField]
private TextMeshProUGUI text;
public Action<UIPlaybackListItemData, bool> OnSelect { get; set; }
public Action<UIPlaybackListItemData, UIPlaybackListItemStatus> OnChangeStatus { get; set; }
private UIPlaybackListItemData data;
public UIPlaybackListItemData Data { get => data; }
public UIPlaybackListItemStatus Status
{
get => data.status;
set
{
if (data.status != value)
{
data.status = value;
ChangeStatus();
if (OnChangeStatus != null) OnChangeStatus.Invoke(Data, value);
}
}
}
private bool selected = false;
public bool Selected
{
get => selected;
set
{
if (selected != value)
{
selected = value;
selectedImage.gameObject.SetActive(selected);
}
}
}
private HTTPRequest? downloadRequest;
public void Init(UIPlaybackListItemData data)
{
transform.localScale = Vector3.one;
selected = false;
selectedImage.gameObject.SetActive(selected);
this.data = data;
text.text = $"{data.time} ~ {int.Parse(data.time) + 1} Hour";
downloadButton.onClick.AddListener(onClickDownload);
stopButton.onClick.AddListener(onClickStop);
deleteButton.onClick.AddListener(onClickDelete);
ChangeStatus();
}
private void onClickDownload()
{
if (Status == UIPlaybackListItemStatus.Downloading) return;
long freeSize = DriveUtil.GetDriveGigaBytes();
Debug.Log($"여유 공간 freeSize:{freeSize} {10 * (DownloadingItems.Count + 1)} {freeSize < 10 * (DownloadingItems.Count + 1)}");
if (freeSize < 10 * (DownloadingItems.Count + 1))
{
Toast.Show("Playback 하나당 10기가바이트의 용량이 필요합니다. 용량을 확보하고 다운로드 하세요.\nEach Playback requires a capacity of 10 gigabytes. Secure the capacity and download it.");
return;
}
if (DownloadingItems.Count >= 2)
{
Toast.Show("한번에 총 2개 아이템만 다운로드가 가능합니다.\nOnly 2 items can be downloaded at a time.");
//await UIAlertManager.Instance.ShowAlert("Downloading", "한번에 총 2개 아이템만 다운로드가 가능합니다.\nOnly 3 items can be downloaded at a time.");
return;
}
Status = UIPlaybackListItemStatus.Downloading;
downloadRequest = PlaybackService.Instance.ReadyData(data.date, data.time, data.zipFileName,
(long read, long total, float percent) =>
{
Debug.Log($"ReadyData {percent}");
downloadText.text = $"{read.ToSizeSuffix()}/{total.ToSizeSuffix()} ({(int)(percent * 100)}%)";
loadingImage.fillAmount = percent;
},
(string errorMessage) =>
{
Debug.Log($"Complete Data errorMessage:{errorMessage}");
if (downloadRequest != null) downloadRequest.Clear();
downloadRequest = null;
if (errorMessage != null)
{
if (errorMessage.ToLower().Contains("abort")) errorMessage = "다운로드를 중지 했습니다.\nThe download has been stopped.";
Toast.Show(errorMessage);
//await UIAlertManager.Instance.ShowAlert("Error", errorMessage);
deleteFile();
Status = UIPlaybackListItemStatus.Default;
}
else
{
Status = UIPlaybackListItemStatus.Downloaded;
}
});
}
private void deleteFile()
{
try
{
string playbackPath = Path.Combine(Application.streamingAssetsPath, "playback");
string tempPath = Path.Combine(playbackPath, "temp");
string datePath = Path.Combine(playbackPath, data.date);
var fileNameArr = data.zipFileName.Split('.');
DateTime utcDateTime = DateTimeUtil.Parse(fileNameArr[0], "yyyy-MM-dd_H");
string utcSqlFileName = utcDateTime.ToString("yyyy-MM-dd_H") + "." + fileNameArr[1];
string utcZipFileName = utcSqlFileName + "." + fileNameArr[2];
string utcZipFilePath = Path.Combine(datePath, utcZipFileName);
string utcSqlFilePath = Path.Combine(datePath, utcSqlFileName);
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
if (File.Exists(utcZipFilePath)) File.Delete(utcZipFilePath);
if (File.Exists(utcSqlFilePath)) File.Delete(utcSqlFilePath);
// if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true);
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
private void onClickStop()
{
if (downloadRequest != null && Status == UIPlaybackListItemStatus.Downloading)
{
downloadRequest.Abort();
downloadRequest.Clear();
downloadRequest = null;
deleteFile();
Status = UIPlaybackListItemStatus.Default;
}
}
private void onClickDelete()
{
deleteFile();
Status = UIPlaybackListItemStatus.Default;
}
public void OnPointerClick(PointerEventData eventData)
{
Selected = !selected;
if (OnSelect != null) OnSelect.Invoke(data, Selected);
}
public void Release()
{
OnDestroy();
}
private void OnDestroy()
{
downloadButton.onClick.RemoveListener(onClickDownload);
stopButton.onClick.RemoveListener(onClickStop);
deleteButton.onClick.RemoveListener(onClickDelete);
OnSelect = null;
OnChangeStatus = null;
transform.SetParent(null);
}
private void ChangeStatus()
{
downloadText.text = "";
if (data.status == UIPlaybackListItemStatus.Default)
{
loadingImage.gameObject.SetActive(false);
downloadButton.gameObject.SetActive(true);
stopButton.gameObject.SetActive(false);
deleteButton.gameObject.SetActive(false);
downloadText.gameObject.SetActive(false);
}
else if (data.status == UIPlaybackListItemStatus.Downloading)
{
loadingImage.gameObject.SetActive(true);
loadingImage.SetAlpha(0.2f);
loadingImage.fillAmount = 0;
downloadButton.gameObject.SetActive(false);
stopButton.gameObject.SetActive(true);
deleteButton.gameObject.SetActive(false);
downloadText.gameObject.SetActive(true);
}
else if (data.status == UIPlaybackListItemStatus.Downloaded)
{
loadingImage.gameObject.SetActive(true);
loadingImage.SetAlpha(1f);
loadingImage.fillAmount = 1;
downloadButton.gameObject.SetActive(false);
stopButton.gameObject.SetActive(false);
deleteButton.gameObject.SetActive(true);
downloadText.gameObject.SetActive(false);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e157d53f2eb114b4b94e5b0d1c7eeaa9

View File

@@ -0,0 +1,319 @@
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.Extension;
using UVC.Log;
using UVC.UI;
using UVC.UI.Modal;
using UVC.Util;
namespace UVC.Factory.Playback.UI
{
[RequireComponent(typeof(CanvasGroup))]
public class UIPlaybackListModal : ModalView
{
public static new readonly string PrefabPath = "Prefabs/Factory/Playback/UIPlaybackListModal";
public static UIPlaybackListModal CreateFromPrefab(Transform parent = null)
{
GameObject prefab = Resources.Load(PrefabPath, typeof(GameObject)) as GameObject;
GameObject go = UnityEngine.Object.Instantiate(prefab);
UIPlaybackListModal modal = go.GetComponent<UIPlaybackListModal>();
return modal;
}
private Dictionary<string, List<UIPlaybackListItemData>> data;
public bool IsOkable => (selectedItem != null && selectedItem.status == UIPlaybackListItemStatus.Downloaded && UIPlaybackListItem.DownloadingItems.Count == 0);
private UIPlaybackListItemData? selectedItem;
[SerializeField]
private TMP_Dropdown dropdownDate;
[SerializeField]
private ScrollRect scrollRectTime;
public void Init()
{
initContent();
}
public override async UniTask OnOpen(ModalContent content)
{
await base.OnOpen(content); // 부모의 OnOpen을 먼저 호출해서 기본 UI를 설정해요.
}
public override object GetResult()
{
if (data != null) data.Clear();
data = null;
return selectedItem;
}
public override async UniTask OnClose(ModalContent content)
{
await base.OnClose(content);
}
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);
}
protected void OnDestroy()
{
dropdownDate.onValueChanged.RemoveListener(DropDownDateChanged);
UIPlaybackListItem.ClearAll();
}
public void LocalSetData()
{
Dictionary<string, List<UIPlaybackListItemData>> newData = new Dictionary<string, List<UIPlaybackListItemData>>();
var dateList = new List<TMP_Dropdown.OptionData>();
string playbackPath = Path.Combine(Application.streamingAssetsPath, "playback");
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;
}
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 = Path.Combine(Application.streamingAssetsPath, "playback");
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;
}
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.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();
}
//scroll move to top
scrollRectTime.normalizedPosition = new Vector2(0, 1);
}
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();
}
private void OnItemChangeStatus(UIPlaybackListItemData data, UIPlaybackListItemStatus status)
{
bool enable = UIPlaybackListItem.DownloadingItems.Count == 0;
dropdownDate.interactable = enable;
}
private void updateButtonStatus()
{
confirmButton.interactable = IsOkable;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f7fd94cb2e9eab84ea0e85244ba1a656

View File

@@ -0,0 +1,142 @@
using System;
using TMPro;
using UnityEngine;
using UVC.Extension;
using UVC.UI;
namespace UVC.Factory.Playback.UI
{
public class UIPlaybackProgressBar : MonoBehaviour
{
private TextMeshProUGUI playTimeTxt;
private TextMeshProUGUI totalTimeTxt;
private SliderWithEvent progressBar;
private float progressBarPrevValue = 0;
private CanvasGroup canvasGroup;
private int time;
public Action<int> OnChangeValue { get; set; }
public int Value
{
get
{
return (int)progressBar.value;
}
set
{
if (progressBar.value != value)
{
progressBar.value = value;
UpdateTimeText();
progressBarPrevValue = value;
}
}
}
public int MaxValue
{
get => (int)progressBar.maxValue;
}
public bool Interactable
{
get => canvasGroup.interactable;
set => canvasGroup.interactable = value;
}
private void OnDestroy()
{
progressBar.onValueChanged.RemoveListener(OnChangeSlider);
progressBar.OnClickAction = null;
progressBar.OnDragAction = null;
progressBar.OnEndDragAction = null;
}
public void Init(int time)
{
this.time = time;
canvasGroup = GetComponent<CanvasGroup>();
playTimeTxt = transform.FindChildren("PlayTimeTxt").GetComponent<TextMeshProUGUI>();
totalTimeTxt = transform.FindChildren("TotalTimeTxt").GetComponent<TextMeshProUGUI>();
progressBar = GetComponentInChildren<SliderWithEvent>();
progressBar.onValueChanged.AddListener(OnChangeSlider);
progressBar.OnClickAction += OnClickProgressBar;
progressBar.OnDragAction += OnDragProgressBar;
progressBar.OnEndDragAction += OnEndDragProgressBar;
playTimeTxt.text = $"{time.ToString("00")}:00:00";
totalTimeTxt.text = $"{(time + 1).ToString("00")}:00:00";
progressBar.value = 0;
Interactable = true;
}
private void OnChangeSlider(float newValue)
{
if (!Interactable)
{
progressBar.value = progressBarPrevValue;
UpdateTimeText();
return;
}
}
private void OnClickProgressBar()
{
float snapedValue = SnapProgressBarValue();
progressBar.value = snapedValue;
UpdateTimeText();
if (progressBarPrevValue != snapedValue)
{
progressBarPrevValue = snapedValue;
if (OnChangeValue != null) OnChangeValue.Invoke((int)snapedValue);
}
}
private void OnDragProgressBar()
{
float snapedValue = SnapProgressBarValue();
progressBar.value = snapedValue;
UpdateTimeText();
}
private void OnEndDragProgressBar()
{
float snapedValue = SnapProgressBarValue();
progressBar.value = snapedValue;
UpdateTimeText();
if (progressBarPrevValue != snapedValue)
{
progressBarPrevValue = snapedValue;
if (OnChangeValue != null) OnChangeValue.Invoke((int)snapedValue);
}
}
private float SnapProgressBarValue()
{
float value = progressBar.value;
float interval = 60f;
value = Mathf.Round(value / interval) * interval;
return value;
}
private void UpdateTimeText()
{
int minute = (int)progressBar.value / 60;
int second = (int)progressBar.value % 60;
string timeStr = time.ToString("00");
if (progressBar.value == 3600)
{
timeStr = (time + 1).ToString("00");
minute = 0;
}
playTimeTxt.text = $"{timeStr}:{minute.ToString("00")}:{second.ToString("00")}";
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bce52072cdea6144b98e9297b89d6558