diff --git a/Assets/Scenes/MiniGames/StatueDecoration.unity b/Assets/Scenes/MiniGames/StatueDecoration.unity
index 07df1172..d0a924dd 100644
--- a/Assets/Scenes/MiniGames/StatueDecoration.unity
+++ b/Assets/Scenes/MiniGames/StatueDecoration.unity
@@ -1560,6 +1560,53 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1685271989
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1685271991}
+ - component: {fileID: 1685271990}
+ m_Layer: 0
+ m_Name: TestController
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1685271990
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1685271989}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: deab1758ddef4bdea0e2c50554eaf568, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: AppleHillsScripts::Minigames.StatueDressup.Controllers.PhotoCaptureTestController
+ captureArea: {fileID: 65358845}
+ captureButton: {fileID: 37633367}
+ hideTheseObjects: []
+--- !u!4 &1685271991
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1685271989}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -29.79184, y: 0.56528, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2071711337
GameObject:
m_ObjectHideFlags: 0
@@ -1691,3 +1738,4 @@ SceneRoots:
- {fileID: 1217454518}
- {fileID: 1126329096}
- {fileID: 483064112}
+ - {fileID: 1685271991}
diff --git a/Assets/Scripts/AppleHillsScripts.asmdef b/Assets/Scripts/AppleHillsScripts.asmdef
index 2de6f9e7..07854c9f 100644
--- a/Assets/Scripts/AppleHillsScripts.asmdef
+++ b/Assets/Scripts/AppleHillsScripts.asmdef
@@ -11,7 +11,9 @@
"OptimizedRope",
"AudioSourceEvents",
"NewAssembly",
- "Unity.Cinemachine"
+ "Unity.Cinemachine",
+ "ScreenshotHelper",
+ "SwanDevCommon"
],
"includePlatforms": [],
"excludePlatforms": [],
diff --git a/Assets/Scripts/Core/SaveLoad/AppleMachine.cs b/Assets/Scripts/Core/SaveLoad/AppleMachine.cs
index 6859cdd3..dabcd5ce 100644
--- a/Assets/Scripts/Core/SaveLoad/AppleMachine.cs
+++ b/Assets/Scripts/Core/SaveLoad/AppleMachine.cs
@@ -26,9 +26,9 @@ namespace Core.SaveLoad
/// Has this state machine been restored from save data?
///
public bool HasBeenRestored { get; private set; }
-
+
// Override ChangeState to call OnEnterState on SaveableState components
- public new GameObject ChangeState(GameObject state)
+ public new void ChangeState(GameObject state)
{
var result = base.ChangeState(state);
@@ -41,11 +41,9 @@ namespace Core.SaveLoad
saveableState.OnEnterState();
}
}
-
- return result;
}
- public new GameObject ChangeState(string state)
+ public new void ChangeState(string state)
{
var result = base.ChangeState(state);
@@ -58,11 +56,9 @@ namespace Core.SaveLoad
saveableState.OnEnterState();
}
}
-
- return result;
}
- public new GameObject ChangeState(int childIndex)
+ public new void ChangeState(int childIndex)
{
var result = base.ChangeState(childIndex);
@@ -75,8 +71,6 @@ namespace Core.SaveLoad
saveableState.OnEnterState();
}
}
-
- return result;
}
private void Start()
diff --git a/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoCaptureTestController.cs b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoCaptureTestController.cs
new file mode 100644
index 00000000..9f177755
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoCaptureTestController.cs
@@ -0,0 +1,93 @@
+using Core;
+using Minigames.StatueDressup.Utils;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace Minigames.StatueDressup.Controllers
+{
+ ///
+ /// Minimal test for photo capture - just capture and save to disk
+ ///
+ public class PhotoCaptureTestController : MonoBehaviour
+ {
+ [Header("Required")]
+ [SerializeField] private RectTransform captureArea;
+ [SerializeField] private Button captureButton;
+
+ [Header("Optional - UI to hide during capture")]
+ [SerializeField] private GameObject[] hideTheseObjects;
+
+ private void Start()
+ {
+ if (captureButton != null)
+ {
+ captureButton.onClick.AddListener(OnCaptureClicked);
+ }
+
+ Debug.Log($"[PhotoCaptureTest] Ready. Photo save path: {StatuePhotoManager.GetPhotoDirectory()}");
+ }
+
+ private void OnCaptureClicked()
+ {
+ if (captureArea == null)
+ {
+ Debug.LogError("[PhotoCaptureTest] Capture Area not assigned!");
+ return;
+ }
+
+ Debug.Log("[PhotoCaptureTest] Starting capture...");
+ StartCoroutine(CaptureCoroutine());
+ }
+
+ private System.Collections.IEnumerator CaptureCoroutine()
+ {
+ // Hide UI
+ foreach (var obj in hideTheseObjects)
+ if (obj != null) obj.SetActive(false);
+
+ yield return new WaitForEndOfFrame();
+
+ // Capture
+ bool done = false;
+ Texture2D photo = null;
+
+ StatuePhotoManager.CaptureAreaPhoto(captureArea, (texture) => {
+ photo = texture;
+ done = true;
+ });
+
+ yield return new WaitUntil(() => done);
+
+ // Restore UI
+ foreach (var obj in hideTheseObjects)
+ if (obj != null) obj.SetActive(true);
+
+ // Save
+ if (photo != null)
+ {
+ string photoId = StatuePhotoManager.SavePhoto(photo, 0);
+
+ if (!string.IsNullOrEmpty(photoId))
+ {
+ string path = $"{StatuePhotoManager.GetPhotoDirectory()}/{photoId}.png";
+ Debug.Log($"[PhotoCaptureTest] ✅ SUCCESS! Photo saved: {path}");
+ Debug.Log($"[PhotoCaptureTest] Photo size: {photo.width}x{photo.height}");
+ }
+ else
+ {
+ Debug.LogError("[PhotoCaptureTest] ❌ Failed to save photo");
+ }
+ }
+ else
+ {
+ Debug.LogError("[PhotoCaptureTest] ❌ Failed to capture photo");
+ }
+ }
+
+ private void OnDestroy()
+ {
+ if (captureButton != null)
+ captureButton.onClick.RemoveListener(OnCaptureClicked);
+ }
+ }
+}
diff --git a/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoCaptureTestController.cs.meta b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoCaptureTestController.cs.meta
new file mode 100644
index 00000000..26f394e5
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoCaptureTestController.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: deab1758ddef4bdea0e2c50554eaf568
+timeCreated: 1764077035
\ No newline at end of file
diff --git a/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoGridItem.cs b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoGridItem.cs
new file mode 100644
index 00000000..240ede7a
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoGridItem.cs
@@ -0,0 +1,79 @@
+using Core;
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEngine.EventSystems;
+
+namespace Minigames.StatueDressup.Controllers
+{
+ ///
+ /// Individual photo thumbnail in the gallery grid.
+ /// Handles click to show enlarged view.
+ ///
+ public class PhotoGridItem : MonoBehaviour, IPointerClickHandler
+ {
+ [Header("References")]
+ [SerializeField] private Image thumbnailImage;
+ [SerializeField] private GameObject loadingIndicator;
+
+ private string _photoId;
+ private StatuePhotoGalleryController _galleryController;
+
+ ///
+ /// Initialize grid item with photo ID
+ ///
+ public void Initialize(string photoId, StatuePhotoGalleryController controller)
+ {
+ _photoId = photoId;
+ _galleryController = controller;
+
+ // Show loading state
+ if (loadingIndicator != null)
+ loadingIndicator.SetActive(true);
+
+ if (thumbnailImage != null)
+ thumbnailImage.enabled = false;
+ }
+
+ ///
+ /// Set the thumbnail texture
+ ///
+ public void SetThumbnail(Texture2D thumbnail)
+ {
+ if (thumbnail == null)
+ {
+ Logging.Warning($"[PhotoGridItem] Null thumbnail for photo: {_photoId}");
+ return;
+ }
+
+ // Create sprite from thumbnail
+ Sprite thumbnailSprite = Sprite.Create(
+ thumbnail,
+ new Rect(0, 0, thumbnail.width, thumbnail.height),
+ new Vector2(0.5f, 0.5f)
+ );
+
+ if (thumbnailImage != null)
+ {
+ thumbnailImage.sprite = thumbnailSprite;
+ thumbnailImage.enabled = true;
+ }
+
+ // Hide loading indicator
+ if (loadingIndicator != null)
+ loadingIndicator.SetActive(false);
+ }
+
+ ///
+ /// Handle click to show enlarged view
+ ///
+ public void OnPointerClick(PointerEventData eventData)
+ {
+ if (_galleryController != null && !string.IsNullOrEmpty(_photoId))
+ {
+ Logging.Debug($"[PhotoGridItem] Clicked: {_photoId}");
+ _galleryController.ShowEnlargedView(_photoId);
+ }
+ }
+ }
+}
+
diff --git a/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoGridItem.cs.meta b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoGridItem.cs.meta
new file mode 100644
index 00000000..a02c349b
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Controllers/PhotoGridItem.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: acd8d97ee2f744d984a9507e75309be0
+timeCreated: 1764065014
\ No newline at end of file
diff --git a/Assets/Scripts/Minigames/StatueDressup/Controllers/StatueDecorationController.cs b/Assets/Scripts/Minigames/StatueDressup/Controllers/StatueDecorationController.cs
index abd31db8..fd624a9a 100644
--- a/Assets/Scripts/Minigames/StatueDressup/Controllers/StatueDecorationController.cs
+++ b/Assets/Scripts/Minigames/StatueDressup/Controllers/StatueDecorationController.cs
@@ -129,82 +129,61 @@ namespace Minigames.StatueDressup.Controllers
{
yield return new WaitForEndOfFrame();
- // Capture the photo area
- Texture2D photo = CaptureScreenshotArea();
+ // Capture using Screenshot Helper via StatuePhotoManager
+ bool captureComplete = false;
+ Texture2D capturedPhoto = null;
- if (photo != null)
+ Utils.StatuePhotoManager.CaptureAreaPhoto(
+ photoArea,
+ (Texture2D texture) =>
+ {
+ capturedPhoto = texture;
+ captureComplete = true;
+ },
+ Camera.main
+ );
+
+ // Wait for capture to complete
+ yield return new WaitUntil(() => captureComplete);
+
+ if (capturedPhoto != null)
{
- // Save photo to album
- SavePhotoToAlbum(photo);
+ // Save photo with StatuePhotoManager
+ int decorationCount = _placedDecorations.Count;
+ string photoId = Utils.StatuePhotoManager.SavePhoto(capturedPhoto, decorationCount);
- // Award cards
- AwardCards();
-
- // Update town icon
- UpdateTownIcon(photo);
-
- // Show completion feedback
- ShowCompletionFeedback();
-
- _minigameCompleted = true;
+ if (!string.IsNullOrEmpty(photoId))
+ {
+ Logging.Debug($"[StatueDecorationController] Photo saved: {photoId}");
+
+ // Award cards
+ AwardCards();
+
+ // Update town icon
+ UpdateTownIcon(capturedPhoto);
+
+ // Show completion feedback
+ ShowCompletionFeedback();
+
+ _minigameCompleted = true;
+ }
+ else
+ {
+ Logging.Error("[StatueDecorationController] Failed to save photo!");
+ DebugUIMessage.Show("Failed to save photo!", Color.red);
+ }
+ }
+ else
+ {
+ Logging.Error("[StatueDecorationController] Failed to capture photo!");
+ DebugUIMessage.Show("Failed to capture photo!", Color.red);
}
// Restore UI
HideUIForPhoto(false);
}
- ///
- /// Capture screenshot of specific area
- ///
- private Texture2D CaptureScreenshotArea()
- {
- if (photoArea == null)
- {
- Logging.Warning("[StatueDecorationController] No photo area specified, capturing full screen");
-
- // Capture full screen
- Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);
- screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
- screenshot.Apply();
- return screenshot;
- }
-
- // Get world corners of the rect
- Vector3[] corners = new Vector3[4];
- photoArea.GetWorldCorners(corners);
-
- // Convert to screen space
- Vector2 min = RectTransformUtility.WorldToScreenPoint(Camera.main, corners[0]);
- Vector2 max = RectTransformUtility.WorldToScreenPoint(Camera.main, corners[2]);
-
- int width = (int)(max.x - min.x);
- int height = (int)(max.y - min.y);
-
- Logging.Debug($"[StatueDecorationController] Capturing area: {width}x{height} at ({min.x}, {min.y})");
-
- // Capture the specified area
- Texture2D areaScreenshot = new Texture2D(width, height, TextureFormat.RGB24, false);
- areaScreenshot.ReadPixels(new Rect(min.x, min.y, width, height), 0, 0);
- areaScreenshot.Apply();
-
- return areaScreenshot;
- }
-
- ///
- /// Save photo to card album
- ///
- private void SavePhotoToAlbum(Texture2D photo)
- {
- // TODO: Integrate with existing album save system
- // For now, save to PlayerPrefs as base64
- byte[] bytes = photo.EncodeToPNG();
- string base64 = System.Convert.ToBase64String(bytes);
- string saveKey = _settings?.PhotoSaveKey ?? photoSaveKey;
- PlayerPrefs.SetString(saveKey, base64);
- PlayerPrefs.Save();
-
- Logging.Debug("[StatueDecorationController] Photo saved to album");
- }
+
///
/// Award Blokkemon cards to player
diff --git a/Assets/Scripts/Minigames/StatueDressup/Controllers/StatuePhotoGalleryController.cs b/Assets/Scripts/Minigames/StatueDressup/Controllers/StatuePhotoGalleryController.cs
new file mode 100644
index 00000000..271093e3
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Controllers/StatuePhotoGalleryController.cs
@@ -0,0 +1,394 @@
+using System.Collections;
+using System.Collections.Generic;
+using Core;
+using Core.Lifecycle;
+using Minigames.StatueDressup.Utils;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace Minigames.StatueDressup.Controllers
+{
+ ///
+ /// Manages photo gallery display with optimized memory usage.
+ /// Loads photos in pages and caches thumbnails to avoid loading 1000+ photos at once.
+ /// Supports grid view with thumbnail preview and enlarged view on selection.
+ ///
+ public class StatuePhotoGalleryController : ManagedBehaviour
+ {
+ [Header("Gallery UI")]
+ [SerializeField] private Transform gridContainer;
+ [SerializeField] private PhotoGridItem gridItemPrefab;
+ [SerializeField] private ScrollRect scrollRect;
+
+ [Header("Enlarged View")]
+ [SerializeField] private GameObject enlargedViewPanel;
+ [SerializeField] private Image enlargedPhotoImage;
+ [SerializeField] private Button closeEnlargedButton;
+ [SerializeField] private Button deletePhotoButton;
+ [SerializeField] private Text photoInfoText;
+
+ [Header("Pagination")]
+ [SerializeField] private Button loadMoreButton;
+ [SerializeField] private Text statusText;
+
+ [Header("Settings")]
+ [SerializeField] private int itemsPerPage = 20;
+ [SerializeField] private int thumbnailSize = 256;
+ [SerializeField] private int maxCachedThumbnails = 50; // Keep recent thumbnails in memory
+
+ private int _currentPage = 0;
+ private List _allPhotoIds = new List();
+ private Dictionary _activeGridItems = new Dictionary();
+ private Dictionary _thumbnailCache = new Dictionary();
+ private Queue _thumbnailCacheOrder = new Queue();
+ private string _currentEnlargedPhotoId = null;
+ private Texture2D _currentEnlargedTexture = null;
+
+ internal override void OnManagedStart()
+ {
+ base.OnManagedStart();
+
+ // Setup buttons
+ if (closeEnlargedButton != null)
+ closeEnlargedButton.onClick.AddListener(CloseEnlargedView);
+
+ if (deletePhotoButton != null)
+ deletePhotoButton.onClick.AddListener(DeleteCurrentPhoto);
+
+ if (loadMoreButton != null)
+ loadMoreButton.onClick.AddListener(LoadNextPage);
+
+ // Hide enlarged view initially
+ if (enlargedViewPanel != null)
+ enlargedViewPanel.SetActive(false);
+
+ // Load first page
+ RefreshGallery();
+ }
+
+ ///
+ /// Refresh the entire gallery from scratch
+ ///
+ public void RefreshGallery()
+ {
+ // Clear existing items
+ ClearGallery();
+
+ // Get all photo IDs
+ _allPhotoIds = StatuePhotoManager.GetAllPhotoIds();
+ _currentPage = 0;
+
+ Logging.Debug($"[StatuePhotoGalleryController] Gallery refreshed: {_allPhotoIds.Count} photos");
+
+ // Load first page
+ LoadNextPage();
+ }
+
+ ///
+ /// Load next page of photos
+ ///
+ private void LoadNextPage()
+ {
+ List pagePhotoIds = StatuePhotoManager.GetPhotoIdsPage(_currentPage, itemsPerPage);
+
+ if (pagePhotoIds.Count == 0)
+ {
+ if (loadMoreButton != null)
+ loadMoreButton.gameObject.SetActive(false);
+
+ UpdateStatusText($"All photos loaded ({_allPhotoIds.Count} total)");
+ return;
+ }
+
+ Logging.Debug($"[StatuePhotoGalleryController] Loading page {_currentPage}: {pagePhotoIds.Count} items");
+
+ // Spawn grid items for this page
+ foreach (string photoId in pagePhotoIds)
+ {
+ SpawnGridItem(photoId);
+ }
+
+ _currentPage++;
+
+ // Update UI state
+ bool hasMore = _currentPage * itemsPerPage < _allPhotoIds.Count;
+ if (loadMoreButton != null)
+ loadMoreButton.gameObject.SetActive(hasMore);
+
+ UpdateStatusText($"Showing {_activeGridItems.Count} of {_allPhotoIds.Count} photos");
+ }
+
+ ///
+ /// Spawn a grid item for a photo
+ ///
+ private void SpawnGridItem(string photoId)
+ {
+ if (_activeGridItems.ContainsKey(photoId))
+ {
+ Logging.Warning($"[StatuePhotoGalleryController] Grid item already exists: {photoId}");
+ return;
+ }
+
+ PhotoGridItem gridItem = Instantiate(gridItemPrefab, gridContainer);
+ gridItem.Initialize(photoId, this);
+
+ _activeGridItems[photoId] = gridItem;
+
+ // Load thumbnail asynchronously
+ StartCoroutine(LoadThumbnailAsync(photoId, gridItem));
+ }
+
+ ///
+ /// Load thumbnail for grid item (async to avoid frame hitches)
+ ///
+ private IEnumerator LoadThumbnailAsync(string photoId, PhotoGridItem gridItem)
+ {
+ // Check cache first
+ if (_thumbnailCache.TryGetValue(photoId, out Texture2D cachedThumbnail))
+ {
+ gridItem.SetThumbnail(cachedThumbnail);
+ yield break;
+ }
+
+ // Yield to avoid loading all thumbnails in one frame
+ yield return null;
+
+ // Load full photo
+ Texture2D fullPhoto = StatuePhotoManager.LoadPhoto(photoId);
+
+ if (fullPhoto == null)
+ {
+ Logging.Warning($"[StatuePhotoGalleryController] Failed to load photo: {photoId}");
+ yield break;
+ }
+
+ // Create thumbnail
+ Texture2D thumbnail = StatuePhotoManager.CreateThumbnail(fullPhoto, thumbnailSize);
+
+ // Destroy full photo immediately (we only need thumbnail)
+ Destroy(fullPhoto);
+
+ // Cache thumbnail
+ CacheThumbnail(photoId, thumbnail);
+
+ // Set on grid item
+ if (gridItem != null)
+ {
+ gridItem.SetThumbnail(thumbnail);
+ }
+ }
+
+ ///
+ /// Cache thumbnail with LRU eviction
+ ///
+ private void CacheThumbnail(string photoId, Texture2D thumbnail)
+ {
+ // Add to cache
+ _thumbnailCache[photoId] = thumbnail;
+ _thumbnailCacheOrder.Enqueue(photoId);
+
+ // Evict oldest if over limit
+ while (_thumbnailCache.Count > maxCachedThumbnails && _thumbnailCacheOrder.Count > 0)
+ {
+ string oldestId = _thumbnailCacheOrder.Dequeue();
+
+ if (_thumbnailCache.TryGetValue(oldestId, out Texture2D oldThumbnail))
+ {
+ Destroy(oldThumbnail);
+ _thumbnailCache.Remove(oldestId);
+ Logging.Debug($"[StatuePhotoGalleryController] Evicted thumbnail from cache: {oldestId}");
+ }
+ }
+ }
+
+ ///
+ /// Show enlarged view of a photo (called by PhotoGridItem)
+ ///
+ public void ShowEnlargedView(string photoId)
+ {
+ if (enlargedViewPanel == null || enlargedPhotoImage == null)
+ {
+ Logging.Warning("[StatuePhotoGalleryController] Enlarged view UI not configured");
+ return;
+ }
+
+ Logging.Debug($"[StatuePhotoGalleryController] Showing enlarged view: {photoId}");
+
+ // Clear previous enlarged texture
+ if (_currentEnlargedTexture != null)
+ {
+ Destroy(_currentEnlargedTexture);
+ _currentEnlargedTexture = null;
+ }
+
+ // Load full-size photo
+ _currentEnlargedTexture = StatuePhotoManager.LoadPhoto(photoId);
+
+ if (_currentEnlargedTexture == null)
+ {
+ Logging.Error($"[StatuePhotoGalleryController] Failed to load enlarged photo: {photoId}");
+ return;
+ }
+
+ // Create sprite from texture
+ Sprite enlargedSprite = Sprite.Create(
+ _currentEnlargedTexture,
+ new Rect(0, 0, _currentEnlargedTexture.width, _currentEnlargedTexture.height),
+ new Vector2(0.5f, 0.5f)
+ );
+
+ enlargedPhotoImage.sprite = enlargedSprite;
+ _currentEnlargedPhotoId = photoId;
+
+ // Update photo info
+ UpdatePhotoInfo(photoId);
+
+ // Show panel
+ enlargedViewPanel.SetActive(true);
+ }
+
+ ///
+ /// Close enlarged view
+ ///
+ private void CloseEnlargedView()
+ {
+ if (enlargedViewPanel != null)
+ enlargedViewPanel.SetActive(false);
+
+ // Clean up texture
+ if (_currentEnlargedTexture != null)
+ {
+ Destroy(_currentEnlargedTexture);
+ _currentEnlargedTexture = null;
+ }
+
+ _currentEnlargedPhotoId = null;
+ }
+
+ ///
+ /// Delete currently viewed photo
+ ///
+ private void DeleteCurrentPhoto()
+ {
+ if (string.IsNullOrEmpty(_currentEnlargedPhotoId))
+ {
+ Logging.Warning("[StatuePhotoGalleryController] No photo selected for deletion");
+ return;
+ }
+
+ string photoIdToDelete = _currentEnlargedPhotoId;
+
+ // Close enlarged view first
+ CloseEnlargedView();
+
+ // Delete photo
+ bool deleted = StatuePhotoManager.DeletePhoto(photoIdToDelete);
+
+ if (deleted)
+ {
+ // Remove from grid
+ if (_activeGridItems.TryGetValue(photoIdToDelete, out PhotoGridItem gridItem))
+ {
+ Destroy(gridItem.gameObject);
+ _activeGridItems.Remove(photoIdToDelete);
+ }
+
+ // Remove from cache
+ if (_thumbnailCache.TryGetValue(photoIdToDelete, out Texture2D thumbnail))
+ {
+ Destroy(thumbnail);
+ _thumbnailCache.Remove(photoIdToDelete);
+ }
+
+ // Refresh photo list
+ _allPhotoIds.Remove(photoIdToDelete);
+
+ UpdateStatusText($"Photo deleted. {_allPhotoIds.Count} photos remaining");
+
+ Logging.Debug($"[StatuePhotoGalleryController] Photo deleted: {photoIdToDelete}");
+ }
+ }
+
+ ///
+ /// Update photo info text in enlarged view
+ ///
+ private void UpdatePhotoInfo(string photoId)
+ {
+ if (photoInfoText == null) return;
+
+ StatuePhotoManager.PhotoMetadata metadata = StatuePhotoManager.GetPhotoMetadata(photoId);
+
+ if (metadata != null)
+ {
+ System.DateTime timestamp = System.DateTime.Parse(metadata.timestamp);
+ string dateStr = timestamp.ToString("MMM dd, yyyy hh:mm tt");
+
+ float fileSizeMB = metadata.fileSizeBytes / (1024f * 1024f);
+
+ photoInfoText.text = $"Date: {dateStr}\n" +
+ $"Decorations: {metadata.decorationCount}\n" +
+ $"Size: {fileSizeMB:F2} MB";
+ }
+ else
+ {
+ photoInfoText.text = "Photo information unavailable";
+ }
+ }
+
+ ///
+ /// Update status text
+ ///
+ private void UpdateStatusText(string message)
+ {
+ if (statusText != null)
+ statusText.text = message;
+
+ Logging.Debug($"[StatuePhotoGalleryController] Status: {message}");
+ }
+
+ ///
+ /// Clear all grid items and cached data
+ ///
+ private void ClearGallery()
+ {
+ // Destroy grid items
+ foreach (var gridItem in _activeGridItems.Values)
+ {
+ if (gridItem != null)
+ Destroy(gridItem.gameObject);
+ }
+ _activeGridItems.Clear();
+
+ // Clear thumbnail cache
+ foreach (var thumbnail in _thumbnailCache.Values)
+ {
+ if (thumbnail != null)
+ Destroy(thumbnail);
+ }
+ _thumbnailCache.Clear();
+ _thumbnailCacheOrder.Clear();
+
+ Logging.Debug("[StatuePhotoGalleryController] Gallery cleared");
+ }
+
+ internal override void OnManagedDestroy()
+ {
+ base.OnManagedDestroy();
+
+ // Cleanup
+ ClearGallery();
+ CloseEnlargedView();
+
+ // Unsubscribe buttons
+ if (closeEnlargedButton != null)
+ closeEnlargedButton.onClick.RemoveListener(CloseEnlargedView);
+
+ if (deletePhotoButton != null)
+ deletePhotoButton.onClick.RemoveListener(DeleteCurrentPhoto);
+
+ if (loadMoreButton != null)
+ loadMoreButton.onClick.RemoveListener(LoadNextPage);
+ }
+ }
+}
+
diff --git a/Assets/Scripts/Minigames/StatueDressup/Controllers/StatuePhotoGalleryController.cs.meta b/Assets/Scripts/Minigames/StatueDressup/Controllers/StatuePhotoGalleryController.cs.meta
new file mode 100644
index 00000000..a56e8706
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Controllers/StatuePhotoGalleryController.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: a7339274a0c54f8c9134942f84d47140
+timeCreated: 1764065004
\ No newline at end of file
diff --git a/Assets/Scripts/Minigames/StatueDressup/Utils/StatuePhotoManager.cs b/Assets/Scripts/Minigames/StatueDressup/Utils/StatuePhotoManager.cs
new file mode 100644
index 00000000..74de576d
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Utils/StatuePhotoManager.cs
@@ -0,0 +1,399 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Core;
+using UnityEngine;
+using SDev;
+
+namespace Minigames.StatueDressup.Utils
+{
+ ///
+ /// Manages statue photo capture, storage, and retrieval.
+ /// Supports area-limited screenshots, persistent file storage (mobile + PC),
+ /// and optimized gallery loading with pagination.
+ ///
+ public static class StatuePhotoManager
+ {
+ private const string PHOTO_FOLDER = "StatuePhotos";
+ private const string PHOTO_PREFIX = "MrCementStatue_";
+ private const string METADATA_KEY_PREFIX = "StatuePhoto_Meta_";
+ private const string PHOTO_INDEX_KEY = "StatuePhoto_Index";
+
+ ///
+ /// Photo metadata stored in PlayerPrefs
+ ///
+ [System.Serializable]
+ public class PhotoMetadata
+ {
+ public string photoId;
+ public string timestamp;
+ public int decorationCount;
+ public long fileSizeBytes;
+ }
+
+ #region Capture
+
+ ///
+ /// Capture a specific area of the screen using Screenshot Helper
+ ///
+ /// RectTransform defining the capture region
+ /// Callback with captured Texture2D
+ /// Camera used for coordinate conversion (null = Camera.main)
+ public static void CaptureAreaPhoto(RectTransform captureArea, Action onComplete, Camera mainCamera = null)
+ {
+ if (captureArea == null)
+ {
+ Logging.Error("[StatuePhotoManager] CaptureArea RectTransform is null!");
+ onComplete?.Invoke(null);
+ return;
+ }
+
+ if (mainCamera == null) mainCamera = Camera.main;
+
+ // Get screen rect from RectTransform
+ Rect screenRect = GetScreenRectFromRectTransform(captureArea, mainCamera);
+
+ Logging.Debug($"[StatuePhotoManager] Capturing area: pos={screenRect.position}, size={screenRect.size}");
+
+ // Use Screenshot Helper's Capture method
+ ScreenshotHelper.Instance.Capture(
+ screenRect.position,
+ screenRect.size,
+ (Texture2D texture) =>
+ {
+ if (texture != null)
+ {
+ Logging.Debug($"[StatuePhotoManager] Photo captured: {texture.width}x{texture.height}");
+ onComplete?.Invoke(texture);
+ }
+ else
+ {
+ Logging.Error("[StatuePhotoManager] Screenshot Helper returned null texture!");
+ onComplete?.Invoke(null);
+ }
+ }
+ );
+ }
+
+ ///
+ /// Convert RectTransform world corners to screen space rect
+ ///
+ private static Rect GetScreenRectFromRectTransform(RectTransform rectTransform, Camera camera)
+ {
+ Vector3[] corners = new Vector3[4];
+ rectTransform.GetWorldCorners(corners);
+
+ Vector2 min = RectTransformUtility.WorldToScreenPoint(camera, corners[0]);
+ Vector2 max = RectTransformUtility.WorldToScreenPoint(camera, corners[2]);
+
+ // Ensure positive dimensions
+ float width = Mathf.Abs(max.x - min.x);
+ float height = Mathf.Abs(max.y - min.y);
+
+ return new Rect(min.x, min.y, width, height);
+ }
+
+ #endregion
+
+ #region Save/Load
+
+ ///
+ /// Save photo to persistent storage with metadata
+ ///
+ /// Texture2D to save
+ /// Number of decorations placed (for metadata)
+ /// Photo ID if successful, null if failed
+ public static string SavePhoto(Texture2D photo, int decorationCount = 0)
+ {
+ if (photo == null)
+ {
+ Logging.Error("[StatuePhotoManager] Cannot save null photo");
+ return null;
+ }
+
+ try
+ {
+ // Generate unique photo ID
+ string photoId = $"{PHOTO_PREFIX}{DateTime.Now.Ticks}";
+
+ // Save texture using FileSaveUtil
+ string savedPath = FileSaveUtil.Instance.SaveTextureAsPNG(
+ photo,
+ FileSaveUtil.AppPath.PersistentDataPath,
+ PHOTO_FOLDER,
+ photoId
+ );
+
+ // Calculate file size
+ FileInfo fileInfo = new FileInfo(savedPath);
+ long fileSize = fileInfo.Exists ? fileInfo.Length : 0;
+
+ // Save metadata
+ PhotoMetadata metadata = new PhotoMetadata
+ {
+ photoId = photoId,
+ timestamp = DateTime.Now.ToString("o"),
+ decorationCount = decorationCount,
+ fileSizeBytes = fileSize
+ };
+
+ SaveMetadata(metadata);
+ AddToPhotoIndex(photoId);
+
+ Logging.Debug($"[StatuePhotoManager] Photo saved: {savedPath} ({fileSize} bytes)");
+ return photoId;
+ }
+ catch (Exception e)
+ {
+ Logging.Error($"[StatuePhotoManager] Failed to save photo: {e.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Load photo texture from storage
+ ///
+ public static Texture2D LoadPhoto(string photoId)
+ {
+ if (string.IsNullOrEmpty(photoId))
+ {
+ Logging.Warning("[StatuePhotoManager] PhotoId is null or empty");
+ return null;
+ }
+
+ try
+ {
+ string filePath = GetPhotoFilePath(photoId);
+
+ if (!File.Exists(filePath))
+ {
+ Logging.Warning($"[StatuePhotoManager] 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($"[StatuePhotoManager] Photo loaded: {photoId} ({texture.width}x{texture.height})");
+ return texture;
+ }
+ else
+ {
+ Logging.Error($"[StatuePhotoManager] Failed to decode image: {photoId}");
+ UnityEngine.Object.Destroy(texture);
+ return null;
+ }
+ }
+ catch (Exception e)
+ {
+ Logging.Error($"[StatuePhotoManager] Failed to load photo {photoId}: {e.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Delete photo and its metadata
+ ///
+ public static bool DeletePhoto(string photoId)
+ {
+ if (string.IsNullOrEmpty(photoId)) return false;
+
+ try
+ {
+ string filePath = GetPhotoFilePath(photoId);
+
+ if (File.Exists(filePath))
+ {
+ File.Delete(filePath);
+ }
+
+ DeleteMetadata(photoId);
+ RemoveFromPhotoIndex(photoId);
+
+ Logging.Debug($"[StatuePhotoManager] Photo deleted: {photoId}");
+ return true;
+ }
+ catch (Exception e)
+ {
+ Logging.Error($"[StatuePhotoManager] Failed to delete photo {photoId}: {e.Message}");
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Gallery Support
+
+ ///
+ /// Get all photo IDs sorted by timestamp (newest first)
+ ///
+ public static List GetAllPhotoIds()
+ {
+ string indexJson = PlayerPrefs.GetString(PHOTO_INDEX_KEY, "[]");
+ List photoIds = JsonUtility.FromJson(WrapJsonArray(indexJson))?.ids ?? new List();
+
+ // Sort by timestamp descending (newest first)
+ photoIds.Sort((a, b) =>
+ {
+ PhotoMetadata metaA = LoadMetadata(a);
+ PhotoMetadata metaB = LoadMetadata(b);
+
+ DateTime dateA = DateTime.Parse(metaA?.timestamp ?? DateTime.MinValue.ToString("o"));
+ DateTime dateB = DateTime.Parse(metaB?.timestamp ?? DateTime.MinValue.ToString("o"));
+
+ return dateB.CompareTo(dateA);
+ });
+
+ return photoIds;
+ }
+
+ ///
+ /// Get paginated photo IDs for optimized gallery loading
+ ///
+ /// Page number (0-indexed)
+ /// Number of items per page
+ public static List GetPhotoIdsPage(int page, int pageSize)
+ {
+ List allIds = GetAllPhotoIds();
+ int startIndex = page * pageSize;
+
+ if (startIndex >= allIds.Count) return new List();
+
+ int count = Mathf.Min(pageSize, allIds.Count - startIndex);
+ return allIds.GetRange(startIndex, count);
+ }
+
+ ///
+ /// Get total number of saved photos
+ ///
+ public static int GetPhotoCount()
+ {
+ return GetAllPhotoIds().Count;
+ }
+
+ ///
+ /// Get latest photo ID (most recent)
+ ///
+ public static string GetLatestPhotoId()
+ {
+ List allIds = GetAllPhotoIds();
+ return allIds.Count > 0 ? allIds[0] : null;
+ }
+
+ ///
+ /// Load photo metadata
+ ///
+ public static PhotoMetadata GetPhotoMetadata(string photoId)
+ {
+ return LoadMetadata(photoId);
+ }
+
+ ///
+ /// Create thumbnail from full-size photo (for gallery preview)
+ ///
+ 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;
+ }
+
+ #endregion
+
+ #region Internal Helpers
+
+ public static string GetPhotoDirectory()
+ {
+ return Path.Combine(Application.persistentDataPath, PHOTO_FOLDER);
+ }
+
+ private static string GetPhotoFilePath(string photoId)
+ {
+ return Path.Combine(GetPhotoDirectory(), $"{photoId}.png");
+ }
+
+ private static void SaveMetadata(PhotoMetadata metadata)
+ {
+ string json = JsonUtility.ToJson(metadata);
+ PlayerPrefs.SetString(METADATA_KEY_PREFIX + metadata.photoId, json);
+ PlayerPrefs.Save();
+ }
+
+ private static PhotoMetadata LoadMetadata(string photoId)
+ {
+ string json = PlayerPrefs.GetString(METADATA_KEY_PREFIX + photoId, null);
+ return string.IsNullOrEmpty(json) ? null : JsonUtility.FromJson(json);
+ }
+
+ private static void DeleteMetadata(string photoId)
+ {
+ PlayerPrefs.DeleteKey(METADATA_KEY_PREFIX + photoId);
+ PlayerPrefs.Save();
+ }
+
+ private static void AddToPhotoIndex(string photoId)
+ {
+ List photoIds = GetAllPhotoIds();
+ if (!photoIds.Contains(photoId))
+ {
+ photoIds.Add(photoId);
+ SavePhotoIndex(photoIds);
+ }
+ }
+
+ private static void RemoveFromPhotoIndex(string photoId)
+ {
+ List photoIds = GetAllPhotoIds();
+ if (photoIds.Remove(photoId))
+ {
+ SavePhotoIndex(photoIds);
+ }
+ }
+
+ private static void SavePhotoIndex(List photoIds)
+ {
+ string json = JsonUtility.ToJson(new PhotoIdList { ids = photoIds });
+ PlayerPrefs.SetString(PHOTO_INDEX_KEY, json);
+ PlayerPrefs.Save();
+ }
+
+ private static string WrapJsonArray(string json)
+ {
+ if (json.StartsWith("[")) return "{\"ids\":" + json + "}";
+ return json;
+ }
+
+ [System.Serializable]
+ private class PhotoIdList
+ {
+ public List ids = new List();
+ }
+
+ #endregion
+ }
+}
+
diff --git a/Assets/Scripts/Minigames/StatueDressup/Utils/StatuePhotoManager.cs.meta b/Assets/Scripts/Minigames/StatueDressup/Utils/StatuePhotoManager.cs.meta
new file mode 100644
index 00000000..e1e7f0e0
--- /dev/null
+++ b/Assets/Scripts/Minigames/StatueDressup/Utils/StatuePhotoManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7f3e9a2b4c5d6e7f8a9b0c1d2e3f4a5b
\ No newline at end of file