Files
AppleHillsProduction/Assets/Scripts/Utils/PhotoManager.cs
2025-12-15 15:24:17 +01:00

553 lines
20 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Core;
using UnityEngine;
namespace Utils
{
/// <summary>
/// Generalized photo capture, storage, and retrieval manager.
/// Supports multiple capture types (minigames, screenshots, etc.) with type-based configuration.
/// Uses disk-only storage - no PlayerPrefs indexing (simplified for reliability).
/// </summary>
public static class PhotoManager
{
private const string RootCapturesFolder = "Captures";
#region Plug-and-Play Coroutine
/// <summary>
/// Capture and save photo in one coroutine call. Handles UI hiding, capture, save, and restoration.
/// </summary>
/// <param name="captureType">Type of capture (determines folder/prefix)</param>
/// <param name="captureArea">RectTransform defining the capture region</param>
/// <param name="uiToHide">Optional UI elements to hide during capture</param>
/// <param name="onSuccess">Callback with photoId on success</param>
/// <param name="onFailure">Callback with error message on failure</param>
/// <param name="metadata">Generic metadata (decoration count, score, etc.)</param>
/// <param name="mainCamera">Camera for coordinate conversion (null = Camera.main)</param>
/// <param name="clampToScreenBounds">If true, clamps capture area to visible screen</param>
public static IEnumerator CaptureAndSaveCoroutine(
CaptureType captureType,
RectTransform captureArea,
GameObject[] uiToHide = null,
Action<string> onSuccess = null,
Action<string> onFailure = null,
int metadata = 0,
Camera mainCamera = null,
bool clampToScreenBounds = true)
{
if (captureArea == null)
{
string error = "[PhotoManager] CaptureArea RectTransform is null!";
Logging.Error(error);
onFailure?.Invoke(error);
yield break;
}
// Hide UI elements
if (uiToHide != null)
{
foreach (var obj in uiToHide)
{
if (obj != null) obj.SetActive(false);
}
}
// Wait for UI to hide
yield return new WaitForEndOfFrame();
// Capture photo
bool captureComplete = false;
Texture2D capturedPhoto = null;
CaptureAreaPhoto(captureArea, (texture) =>
{
capturedPhoto = texture;
captureComplete = true;
}, mainCamera, clampToScreenBounds);
// Wait for capture to complete
yield return new WaitUntil(() => captureComplete);
// Restore UI elements
if (uiToHide != null)
{
foreach (var obj in uiToHide)
{
if (obj != null) obj.SetActive(true);
}
}
// Save photo
if (capturedPhoto != null)
{
string photoId = SavePhoto(captureType, capturedPhoto, metadata);
if (!string.IsNullOrEmpty(photoId))
{
Logging.Debug($"[PhotoManager] Photo saved successfully: {photoId}");
onSuccess?.Invoke(photoId);
}
else
{
string error = "[PhotoManager] Failed to save photo!";
Logging.Error(error);
onFailure?.Invoke(error);
}
}
else
{
string error = "[PhotoManager] Photo capture returned null!";
Logging.Error(error);
onFailure?.Invoke(error);
}
}
#endregion
#region Capture
/// <summary>
/// Capture a specific area of the screen using Screenshot Helper
/// </summary>
public static void CaptureAreaPhoto(
RectTransform captureArea,
Action<Texture2D> onComplete,
Camera mainCamera = null,
bool clampToScreenBounds = true)
{
if (captureArea == null)
{
Logging.Error("[PhotoManager] CaptureArea RectTransform is null!");
onComplete?.Invoke(null);
return;
}
if (mainCamera == null) mainCamera = Camera.main;
// Use ScreenSpaceUtility to convert RectTransform to screen rect
Rect screenRect = ScreenSpaceUtility.RectTransformToScreenRect(
captureArea,
mainCamera,
clampToScreenBounds,
returnCenterPosition: true
);
Logging.Debug($"[PhotoManager] Capturing area: pos={screenRect.position}, size={screenRect.size}");
// Use Screenshot Helper's Capture method
ScreenshotHelper.Instance.Capture(
screenRect.position,
screenRect.size,
(texture) =>
{
if (texture != null)
{
Logging.Debug($"[PhotoManager] Photo captured: {texture.width}x{texture.height}");
onComplete?.Invoke(texture);
}
else
{
Logging.Error("[PhotoManager] Screenshot Helper returned null texture!");
onComplete?.Invoke(null);
}
}
);
}
#endregion
#region Save/Load
/// <summary>
/// Save photo to persistent storage with metadata
/// </summary>
public static string SavePhoto(CaptureType captureType, Texture2D photo, int metadata = 0)
{
if (photo == null)
{
Logging.Error("[PhotoManager] Cannot save null photo");
return null;
}
try
{
CaptureConfig config = PhotoCaptureConfigs.GetConfig(captureType);
// Generate unique photo filename using timestamp
string photoId = $"{config.photoPrefix}{DateTime.Now.Ticks}";
// Get capture directory for this type
string captureDirectory = GetCaptureDirectory(captureType);
// Ensure directory exists
if (!Directory.Exists(captureDirectory))
{
Directory.CreateDirectory(captureDirectory);
}
// Save PNG to disk (that's it - no PlayerPrefs!)
string filePath = Path.Combine(captureDirectory, $"{photoId}.png");
byte[] pngData = photo.EncodeToPNG();
File.WriteAllBytes(filePath, pngData);
Logging.Debug($"[PhotoManager] Photo saved: {filePath}");
return photoId;
}
catch (Exception e)
{
Logging.Error($"[PhotoManager] Failed to save photo: {e.Message}");
return null;
}
}
/// <summary>
/// Load photo texture from storage
/// </summary>
public static Texture2D LoadPhoto(CaptureType captureType, string photoId)
{
if (string.IsNullOrEmpty(photoId))
{
Logging.Warning("[PhotoManager] PhotoId is null or empty");
return null;
}
try
{
string filePath = GetPhotoFilePath(captureType, photoId);
if (!File.Exists(filePath))
{
Logging.Warning($"[PhotoManager] Photo not found: {filePath}");
return null;
}
byte[] fileData = File.ReadAllBytes(filePath);
Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
if (texture.LoadImage(fileData))
{
Logging.Debug($"[PhotoManager] Photo loaded: {photoId} ({texture.width}x{texture.height})");
return texture;
}
else
{
Logging.Error($"[PhotoManager] Failed to decode image: {photoId}");
UnityEngine.Object.Destroy(texture);
return null;
}
}
catch (Exception e)
{
Logging.Error($"[PhotoManager] Failed to load photo {photoId}: {e.Message}");
return null;
}
}
/// <summary>
/// Load multiple photos (most recent first)
/// </summary>
public static List<Texture2D> LoadPhotos(CaptureType captureType, int count)
{
List<string> photoIds = GetPhotoIds(captureType, count);
List<Texture2D> photos = new List<Texture2D>();
foreach (string photoId in photoIds)
{
Texture2D photo = LoadPhoto(captureType, photoId);
if (photo != null)
{
photos.Add(photo);
}
}
return photos;
}
/// <summary>
/// Load all photos for a capture type
/// </summary>
public static List<Texture2D> LoadAllPhotos(CaptureType captureType)
{
return LoadPhotos(captureType, -1);
}
/// <summary>
/// Delete photo and its associated files
/// </summary>
public static bool DeletePhoto(CaptureType captureType, string photoId)
{
if (string.IsNullOrEmpty(photoId)) return false;
try
{
// Delete main photo
string filePath = GetPhotoFilePath(captureType, photoId);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
// Delete decoration metadata if exists
string decorationPath = GetDecorationMetadataPath(captureType, photoId);
if (File.Exists(decorationPath))
{
File.Delete(decorationPath);
}
Logging.Debug($"[PhotoManager] Photo deleted: {photoId}");
return true;
}
catch (Exception e)
{
Logging.Error($"[PhotoManager] Failed to delete photo {photoId}: {e.Message}");
return false;
}
}
#endregion
#region Retrieval & Queries
/// <summary>
/// Get photo filenames sorted by newest first (scans disk)
/// </summary>
/// <param name="captureType">Type of capture</param>
/// <param name="count">Number of filenames to return (-1 = all)</param>
public static List<string> GetPhotoIds(CaptureType captureType, int count = -1)
{
string directory = GetCaptureDirectory(captureType);
if (!Directory.Exists(directory))
{
return new List<string>();
}
try
{
// Get all PNG files, sorted by creation time (newest first)
var files = Directory.GetFiles(directory, "*.png")
.Select(f => new FileInfo(f))
.OrderByDescending(f => f.CreationTime)
.Select(f => Path.GetFileNameWithoutExtension(f.Name));
// Return top X or all
return (count > 0 ? files.Take(count) : files).ToList();
}
catch (Exception e)
{
Logging.Error($"[PhotoManager] Failed to scan directory {directory}: {e.Message}");
return new List<string>();
}
}
/// <summary>
/// Get all photo filenames sorted by newest first
/// </summary>
public static List<string> GetAllPhotoIds(CaptureType captureType)
{
return GetPhotoIds(captureType, -1);
}
/// <summary>
/// Get paginated photo IDs for optimized gallery loading
/// </summary>
public static List<string> GetPhotoIdsPage(CaptureType captureType, int page, int pageSize)
{
List<string> allIds = GetAllPhotoIds(captureType);
int startIndex = page * pageSize;
if (startIndex >= allIds.Count) return new List<string>();
int count = Mathf.Min(pageSize, allIds.Count - startIndex);
return allIds.GetRange(startIndex, count);
}
/// <summary>
/// Get total number of saved photos
/// </summary>
public static int GetPhotoCount(CaptureType captureType)
{
return GetAllPhotoIds(captureType).Count;
}
/// <summary>
/// Get latest photo filename (most recent)
/// </summary>
public static string GetLatestPhotoId(CaptureType captureType)
{
List<string> allIds = GetPhotoIds(captureType, 1);
return allIds.Count > 0 ? allIds[0] : null;
}
#endregion
#region Utility Methods
/// <summary>
/// Create thumbnail from full-size photo (for gallery preview)
/// </summary>
public static Texture2D CreateThumbnail(Texture2D fullSizePhoto, int maxSize = 256)
{
if (fullSizePhoto == null) return null;
int width = fullSizePhoto.width;
int height = fullSizePhoto.height;
// Calculate thumbnail size maintaining aspect ratio
float scale = Mathf.Min((float)maxSize / width, (float)maxSize / height);
int thumbWidth = Mathf.RoundToInt(width * scale);
int thumbHeight = Mathf.RoundToInt(height * scale);
// Create thumbnail using bilinear filtering
RenderTexture rt = RenderTexture.GetTemporary(thumbWidth, thumbHeight, 0, RenderTextureFormat.ARGB32);
RenderTexture.active = rt;
Graphics.Blit(fullSizePhoto, rt);
Texture2D thumbnail = new Texture2D(thumbWidth, thumbHeight, TextureFormat.RGB24, false);
thumbnail.ReadPixels(new Rect(0, 0, thumbWidth, thumbHeight), 0, 0);
thumbnail.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
return thumbnail;
}
/// <summary>
/// Get capture directory for a specific type
/// </summary>
public static string GetCaptureDirectory(CaptureType captureType)
{
CaptureConfig config = PhotoCaptureConfigs.GetConfig(captureType);
return Path.Combine(Application.persistentDataPath, RootCapturesFolder, config.subFolder);
}
#endregion
#region Decoration Metadata
/// <summary>
/// Save decoration metadata for a photo
/// Saves both with photo ID and as "latest" for easy reference
/// </summary>
public static void SaveDecorationMetadata<T>(CaptureType captureType, string photoId, T metadata) where T : class
{
try
{
string directory = GetCaptureDirectory(captureType);
// Ensure directory exists
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string json = JsonUtility.ToJson(metadata, true);
// Save with photo ID (for loading specific photo's decorations)
string specificPath = Path.Combine(directory, $"{photoId}_decorations.json");
File.WriteAllText(specificPath, json);
// Also save as "latest" for easy reference
CaptureConfig config = PhotoCaptureConfigs.GetConfig(captureType);
string latestPath = Path.Combine(directory, $"{config.subFolder}_latest.json");
File.WriteAllText(latestPath, json);
Logging.Debug($"[PhotoManager] Decoration metadata saved: {specificPath}");
}
catch (Exception e)
{
Logging.Error($"[PhotoManager] Failed to save decoration metadata: {e.Message}");
}
}
/// <summary>
/// Load decoration metadata for a specific photo
/// </summary>
public static T LoadDecorationMetadata<T>(CaptureType captureType, string photoId) where T : class
{
try
{
string directory = GetCaptureDirectory(captureType);
string filePath = Path.Combine(directory, $"{photoId}_decorations.json");
if (!File.Exists(filePath))
{
Logging.Warning($"[PhotoManager] Decoration metadata not found: {filePath}");
return null;
}
string json = File.ReadAllText(filePath);
T metadata = JsonUtility.FromJson<T>(json);
Logging.Debug($"[PhotoManager] Decoration metadata loaded: {filePath}");
return metadata;
}
catch (Exception e)
{
Logging.Error($"[PhotoManager] Failed to load decoration metadata: {e.Message}");
return null;
}
}
/// <summary>
/// Load the latest decoration metadata (most recent capture)
/// </summary>
public static T LoadLatestDecorationMetadata<T>(CaptureType captureType) where T : class
{
try
{
CaptureConfig config = PhotoCaptureConfigs.GetConfig(captureType);
string directory = GetCaptureDirectory(captureType);
string filePath = Path.Combine(directory, $"{config.subFolder}_latest.json");
if (!File.Exists(filePath))
{
Logging.Warning($"[PhotoManager] Latest decoration metadata not found: {filePath}");
return null;
}
string json = File.ReadAllText(filePath);
T metadata = JsonUtility.FromJson<T>(json);
Logging.Debug($"[PhotoManager] Latest decoration metadata loaded: {filePath}");
return metadata;
}
catch (Exception e)
{
Logging.Error($"[PhotoManager] Failed to load latest decoration metadata: {e.Message}");
return null;
}
}
#endregion
#region Internal Helpers
private static string GetPhotoFilePath(CaptureType captureType, string photoId)
{
// Ensure .png extension
if (!photoId.EndsWith(".png"))
photoId += ".png";
return Path.Combine(GetCaptureDirectory(captureType), photoId);
}
private static string GetDecorationMetadataPath(CaptureType captureType, string photoId)
{
// Remove .png extension if present for decoration metadata filename
string baseId = photoId.Replace(".png", "");
return Path.Combine(GetCaptureDirectory(captureType), $"{baseId}_decorations.json");
}
#endregion
}
}