image save/load

This commit is contained in:
Savya Bikram Shah
2026-05-29 14:24:36 +05:45
parent 774a7159d3
commit b78678a6d8
31 changed files with 493 additions and 80 deletions

View File

@@ -17,16 +17,21 @@ namespace Darkmatter.Features.AppBoot.Flow
private readonly AppBootSceneRefs _sceneRefs;
private readonly ISceneService _sceneService;
private readonly IEventBus _eventBus;
private readonly IProgressionSystem _progression;
public AppBootFlow(AppBootSceneRefs sceneRefs, ISceneService sceneService, IEventBus eventBus)
public AppBootFlow(AppBootSceneRefs sceneRefs, ISceneService sceneService, IEventBus eventBus,
IProgressionSystem progression)
{
_sceneRefs = sceneRefs;
_sceneService = sceneService;
_eventBus = eventBus;
_progression = progression;
}
public async UniTask StartAsync(CancellationToken cancellation = default)
{
await _progression.LoadAsync();
var tcs = new UniTaskCompletionSource();
var player = _sceneRefs.IntroVideoPlayer;

View File

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

View File

@@ -0,0 +1,19 @@
{
"name": "Features.Capture",
"rootNamespace": "Darkmatter.Features.Capture",
"references": [
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:f51ebe6a0ceec4240a699833d6309b23"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 80056ede5198b460198933cb79d694ff
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,24 @@
using Darkmatter.Core.Contracts.Features.Capture;
using Darkmatter.Features.Capture.UI;
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Darkmatter.Features.Capture
{
public class CaptureFeatureModule : MonoBehaviour, IModule
{
[SerializeField, Range(0.1f, 2f)] private float captureScale = 1f;
[SerializeField] private CaptureButtonView captureButtonView;
public void Register(IContainerBuilder builder)
{
builder.RegisterInstance(new CaptureConfig(captureScale));
builder.Register<ICaptureFeature, CaptureSystem>(Lifetime.Singleton);
if (captureButtonView != null)
builder.RegisterEntryPoint<CaptureButtonPresenter>().WithParameter(captureButtonView);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9b7df1c2a732341d3b123b5e7ae5d7b7

View File

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

View File

@@ -0,0 +1,15 @@
using System;
namespace Darkmatter.Features.Capture
{
[Serializable]
public struct CaptureConfig
{
public float CaptureScale { get; }
public CaptureConfig(float captureScale)
{
CaptureScale = captureScale;
}
}
}

View File

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

View File

@@ -0,0 +1,50 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Features.Capture;
using Darkmatter.Core.Contracts.Features.GameplayFlow;
using Darkmatter.Core.Contracts.Services.Capture;
using Darkmatter.Core.Contracts.Services.Gallery;
using UnityEngine;
namespace Darkmatter.Features.Capture
{
public class CaptureSystem : ICaptureFeature
{
private readonly ICaptureService _captureService;
private readonly IGalleryService _galleryService;
private readonly IGameplaySceneRefs _refs;
private readonly CaptureConfig _config;
public CaptureSystem(
ICaptureService captureService,
IGalleryService galleryService,
IGameplaySceneRefs refs,
CaptureConfig config)
{
_captureService = captureService;
_galleryService = galleryService;
_refs = refs;
_config = config;
}
public async UniTask<byte[]> CapturePngAsync(bool saveToGallery = false, CancellationToken ct = default)
{
var png = await _captureService.CapturePngAsync(_refs.PaperRoot.gameObject, _config.CaptureScale, ct);
if (!saveToGallery || png == null || png.Length == 0) return png;
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false);
try
{
if (tex.LoadImage(png))
await _galleryService.SaveImageAsync(tex,
$"colorbook_{DateTime.UtcNow:yyyyMMdd_HHmmss}.png", ct);
}
finally
{
UnityEngine.Object.Destroy(tex);
}
return png;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 76bbd68f070e24d6a86130ec07c237b8

View File

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

View File

@@ -0,0 +1,44 @@
using System;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Features.Capture;
using VContainer.Unity;
namespace Darkmatter.Features.Capture.UI
{
public class CaptureButtonPresenter : IStartable, IDisposable
{
private readonly CaptureButtonView _view;
private readonly ICaptureFeature _capture;
public CaptureButtonPresenter(CaptureButtonView view, ICaptureFeature capture)
{
_view = view;
_capture = capture;
}
public void Start()
{
_view.OnCaptureClicked += HandleCaptureClicked;
}
private void HandleCaptureClicked() => CaptureAsync().Forget();
private async UniTaskVoid CaptureAsync()
{
_view.SetInteractable(false);
try
{
await _capture.CapturePngAsync(saveToGallery: true);
}
finally
{
_view.SetInteractable(true);
}
}
public void Dispose()
{
_view.OnCaptureClicked -= HandleCaptureClicked;
}
}
}

View File

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

View File

@@ -0,0 +1,29 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace Darkmatter.Features.Capture.UI
{
public class CaptureButtonView : MonoBehaviour
{
[SerializeField] private Button captureButton;
public event Action OnCaptureClicked;
private void Start()
{
if (captureButton != null)
captureButton.onClick.AddListener(() => OnCaptureClicked?.Invoke());
}
public void SetInteractable(bool value)
{
if (captureButton != null) captureButton.interactable = value;
}
private void OnDestroy()
{
if (captureButton != null) captureButton.onClick.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f0946b153da9431eb6c66bda432901f

View File

@@ -27,6 +27,7 @@ public class ColoringController : IColoringController, IDisposable
private readonly IAssetProviderService _assetProviderService;
private readonly IUndoStack _history;
private readonly IGameplaySceneRefs _refs;
private readonly ColorPaletteHolderView _paletteHolder;
private GameObject _colorInstance;
private GameObject _colorButtonPrefab;
@@ -39,7 +40,8 @@ public class ColoringController : IColoringController, IDisposable
IEventBus bus,
IAssetProviderService assetProviderService,
IUndoStack history,
IGameplaySceneRefs refs)
IGameplaySceneRefs refs,
ColorPaletteHolderView paletteHolder)
{
_repository = repository;
_buttonFactory = buttonFactory;
@@ -47,12 +49,14 @@ public class ColoringController : IColoringController, IDisposable
_assetProviderService = assetProviderService;
_history = history;
_refs = refs;
_paletteHolder = paletteHolder;
}
public async UniTask InitializeRegionsAsync(IDrawingTemplate template,
IReadOnlyDictionary<string, Color> savedColors, CancellationToken ct)
{
Clear();
_paletteHolder.Show();
await TryLoadColorButtonPrefabAsync(ct);
ct.ThrowIfCancellationRequested();
await TryLoadColorPaletteAsync(template, ct);

View File

@@ -27,6 +27,7 @@ namespace Darkmatter.Features.Coloring.UI
_color = color;
_repository = repository;
_sfx = sfx;
if (_button == null) _button = GetComponent<Button>();
foreach (var swatch in swatches)
{
swatch.color = color;

View File

@@ -34,6 +34,7 @@ namespace Darkmatter.Features.Coloring.UI
public void OnPointerClick(PointerEventData eventData)
{
Debug.Log($"[ColorRegion] clicked '{RegionId}' on '{gameObject.name}'");
OnRegionClicked?.Invoke();
}

View File

@@ -52,11 +52,18 @@ namespace Darkmatter.Features.DrawingTemplates.Systems
_initialized = true;
}
public UniTask<Sprite> GetThumbnailAsync(string id)
public async UniTask<Sprite> GetThumbnailAsync(string id)
{
if (!_byId.TryGetValue(id, out var t))
throw new KeyNotFoundException($"Template '{id}' not in catalog. Did InitializeAsync run?");
return UniTask.FromResult(t.DefaultThumbnail);
var savedTex = await _progression.GetCachedThumbnailAsync(id);
if (savedTex != null)
{
var rect = new Rect(0, 0, savedTex.width, savedTex.height);
return Sprite.Create(savedTex, rect, new Vector2(0.5f, 0.5f));
}
return t.DefaultThumbnail;
}
public UniTask<IDrawingTemplate> LoadAsync(string id)

View File

@@ -2,13 +2,13 @@ using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Features.Capture;
using Darkmatter.Core.Contracts.Features.Coloring;
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
using Darkmatter.Core.Contracts.Features.GameplayFlow;
using Darkmatter.Core.Contracts.Features.Loading;
using Darkmatter.Core.Contracts.Features.Progression;
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
using Darkmatter.Core.Contracts.Services.Capture;
using Darkmatter.Core.Contracts.Services.Gallery;
using Darkmatter.Core.Contracts.Services.Scenes;
using Darkmatter.Core.Data.Dynamic.Features.Progression;
@@ -31,8 +31,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private readonly IShapeBuilderController _shapeBuilder;
private readonly IColoringController _coloring;
private readonly ISceneService _scenes;
private readonly ICaptureService _captureService;
private readonly IGameplaySceneRefs _refs;
private readonly ICaptureFeature _capture;
private readonly ILoadingScreen _loadingScreen;
private readonly IEventBus _bus;
@@ -51,8 +50,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
IShapeBuilderController shapeBuilder,
IColoringController coloring,
ISceneService scenes,
ICaptureService captureService,
IGameplaySceneRefs refs,
ICaptureFeature capture,
ILoadingScreen loadingScreen,
IEventBus bus)
{
@@ -61,9 +59,8 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_shapeBuilder = shapeBuilder;
_coloring = coloring;
_scenes = scenes;
_captureService = captureService;
_capture = capture;
_loadingScreen = loadingScreen;
_refs = refs;
_bus = bus;
}
@@ -89,6 +86,9 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
Application.quitting += OnAppQuitting;
Application.focusChanged += OnAppFocusChanged;
_loadingScreen.SetProgress(1f);
if (_phase == DrawingPhase.Coloring)
{
@@ -105,7 +105,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public async UniTask BackAsync(CancellationToken ct)
{
await SaveCurrentAsync(captureThumbnail: true, CancellationToken.None);
await SaveCurrentAsync(CancellationToken.None);
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(GameScene.Colorbook, progress: null, cancellationToken: ct);
@@ -113,14 +113,13 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public async UniTask SaveAsync(CancellationToken ct)
{
// Explicit user-pressed save — capture thumbnail + (planned) native gallery export.
await SaveCurrentAsync(captureThumbnail: true, ct);
// TODO: route through ICaptureService + IGalleryService once those exist.
await SaveCurrentAsync(ct);
await _capture.CapturePngAsync(saveToGallery: true, ct);
}
public async UniTask NextAsync(CancellationToken ct)
{
await SaveCurrentAsync(captureThumbnail: true, ct);
await SaveCurrentAsync(ct);
_progression.MarkCompleted(_templateId);
var nextId = _catalog.GetNextTemplate(_templateId);
@@ -139,7 +138,14 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public void OnApplicationPaused()
{
// Fire-and-forget — pause window is small; PlayerPrefs write is sync under the hood.
SaveCurrentAsync(captureThumbnail: false, CancellationToken.None).Forget();
SaveCurrentAsync(CancellationToken.None).Forget();
}
private void OnAppQuitting() => OnApplicationPaused();
private void OnAppFocusChanged(bool focused)
{
if (!focused) OnApplicationPaused();
}
private void OnShapeAssembled(ShapeAssembledSignal signal)
@@ -157,7 +163,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
await _coloring.InitializeRegionsAsync(_template, savedColors, _scopeCts.Token);
_shapeBuilder.DespawnDrawing();
await SaveCurrentAsync(captureThumbnail: true, ct);
await SaveCurrentAsync(ct);
}
private void OnColorApplied(ColorAppliedSignal _)
@@ -174,7 +180,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
try
{
await UniTask.Delay(AutosaveDebounceMs, cancellationToken: ct);
await SaveCurrentAsync(captureThumbnail: false, ct);
await SaveCurrentAsync(ct);
}
catch (OperationCanceledException)
{
@@ -182,7 +188,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
}
}
private async UniTask SaveCurrentAsync(bool captureThumbnail, CancellationToken ct)
private async UniTask SaveCurrentAsync(CancellationToken ct)
{
if (string.IsNullOrEmpty(_templateId)) return;
@@ -198,18 +204,14 @@ namespace Darkmatter.Features.GameplayFlow.Systems
phase = _phase,
snappedPieces = snappedIds,
regionColors = regionEntries,
hasThumbnail = captureThumbnail || (existing?.hasThumbnail ?? false),
hasThumbnail = true,
hasBeenCompleted = existing?.hasBeenCompleted ?? false,
completionCount = existing?.completionCount ?? 0,
UpdatedUtc = DateTime.UtcNow,
FirstCompletedUtc = existing?.FirstCompletedUtc,
};
byte[] thumbnailPng = null;
if (captureThumbnail)
{
thumbnailPng = await _captureService.CapturePngAsync(_refs.PaperRoot.gameObject, 10, ct);
}
var thumbnailPng = await _capture.CapturePngAsync(saveToGallery: false, ct);
await _progression.SaveProgressAsync(progress, thumbnailPng);
}
@@ -226,6 +228,9 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public void Dispose()
{
Application.quitting -= OnAppQuitting;
Application.focusChanged -= OnAppFocusChanged;
_assembledSub?.Dispose();
_colorAppliedSub?.Dispose();
_autosaveCts?.Cancel();