feat(content): scenes + shared-colors + colour-presets endpoints
Build backend images / build content-svc (push) Failing after 2m53s
Build backend images / build file-svc (push) Failing after 3m55s
Build backend images / build gateway (push) Failing after 53s
Build backend images / build identity-svc (push) Failing after 3m26s
Build backend images / build notification-svc (push) Failing after 3m5s
Build backend images / build render-svc (push) Failing after 46s
Build backend images / build studio-svc (push) Failing after 2m22s

Completes the content backend for the studio building blocks (all project-scoped):
- GET /v1/scenes?project_id= + POST/PUT/DELETE (scene metadata CRUD)
- GET /v1/shared-colors?project_id= + POST/PUT/DELETE
- GET /v1/color-presets?project_id= + POST/PUT/DELETE (palette + items)
SceneColorService + DTOs; reads open, writes [Authorize(Roles=Admin)].
Gateway routes /v1/{scenes,shared-colors,color-presets}/* → content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 15:35:32 +03:30
parent 9a1d60e9d0
commit e7cdf35b65
5 changed files with 331 additions and 0 deletions
@@ -0,0 +1,190 @@
using FlatRender.ContentSvc.Domain.Entities;
using FlatRender.ContentSvc.Domain.Enums;
using FlatRender.ContentSvc.Infrastructure.Data;
using FlatRender.ContentSvc.Models;
using Microsoft.EntityFrameworkCore;
namespace FlatRender.ContentSvc.Application.Services;
/// <summary>CRUD for project-scoped scenes, shared colors and colour presets.</summary>
public class SceneColorService(ContentDbContext db)
{
// ── Scenes ──────────────────────────────────────────────────────────────
public async Task<List<SceneResponse>> GetScenesAsync(Guid projectId) =>
await db.Scenes.Where(s => s.ProjectId == projectId && s.DeletedAt == null)
.OrderBy(s => s.Sort)
.Select(s => ToSceneResponse(s)).ToListAsync();
public async Task<SceneResponse> CreateSceneAsync(CreateSceneRequest req)
{
var scene = new Scene
{
ProjectId = req.ProjectId,
Key = req.Key,
Title = req.Title,
LocalizedTitle = req.LocalizedTitle,
SceneType = ParseSceneKind(req.SceneType),
Image = req.Image,
Demo = req.Demo,
SceneColorSvg = req.SceneColorSvg,
SnapshotUrl = req.SnapshotUrl,
GenerateKf = req.GenerateKf,
DefaultDurationSec = req.DefaultDurationSec,
MinDurationSec = req.MinDurationSec,
MaxDurationSec = req.MaxDurationSec,
OverlapAtEndSec = req.OverlapAtEndSec,
CanHandleDuration = req.CanHandleDuration,
ManualColorSelection = req.ManualColorSelection,
Sort = req.Sort,
IsActive = req.IsActive,
};
db.Scenes.Add(scene);
await db.SaveChangesAsync();
return ToSceneResponse(scene);
}
public async Task<SceneResponse> UpdateSceneAsync(Guid id, UpdateSceneRequest req)
{
var scene = await db.Scenes.FindAsync(id) ?? throw new KeyNotFoundException($"Scene {id} not found");
scene.Key = req.Key;
scene.Title = req.Title;
scene.LocalizedTitle = req.LocalizedTitle;
scene.SceneType = ParseSceneKind(req.SceneType);
scene.Image = req.Image;
scene.Demo = req.Demo;
scene.SceneColorSvg = req.SceneColorSvg;
scene.SnapshotUrl = req.SnapshotUrl;
scene.GenerateKf = req.GenerateKf;
scene.DefaultDurationSec = req.DefaultDurationSec;
scene.MinDurationSec = req.MinDurationSec;
scene.MaxDurationSec = req.MaxDurationSec;
scene.OverlapAtEndSec = req.OverlapAtEndSec;
scene.CanHandleDuration = req.CanHandleDuration;
scene.ManualColorSelection = req.ManualColorSelection;
scene.Sort = req.Sort;
scene.IsActive = req.IsActive;
scene.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return ToSceneResponse(scene);
}
public async Task DeleteSceneAsync(Guid id)
{
var scene = await db.Scenes.FindAsync(id) ?? throw new KeyNotFoundException($"Scene {id} not found");
scene.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
// ── Shared colours ──────────────────────────────────────────────────────
public async Task<List<SharedColorResponse>> GetSharedColorsAsync(Guid projectId) =>
await db.SharedColors.Where(c => c.ProjectId == projectId).OrderBy(c => c.Sort)
.Select(c => new SharedColorResponse(c.Id, c.ProjectId, c.ElementKey, c.Title, c.Icon,
c.AttrValue.ToString(), c.DefaultColor, c.Sort)).ToListAsync();
public async Task<SharedColorResponse> CreateSharedColorAsync(SaveSharedColorRequest req)
{
var c = new SharedColor
{
ProjectId = req.ProjectId,
ElementKey = req.ElementKey,
Title = req.Title,
Icon = req.Icon,
AttrValue = ParseAttrValue(req.AttrValue),
DefaultColor = req.DefaultColor,
Sort = req.Sort,
};
db.SharedColors.Add(c);
await db.SaveChangesAsync();
return new SharedColorResponse(c.Id, c.ProjectId, c.ElementKey, c.Title, c.Icon, c.AttrValue.ToString(), c.DefaultColor, c.Sort);
}
public async Task<SharedColorResponse> UpdateSharedColorAsync(Guid id, SaveSharedColorRequest req)
{
var c = await db.SharedColors.FindAsync(id) ?? throw new KeyNotFoundException($"Color {id} not found");
c.ElementKey = req.ElementKey;
c.Title = req.Title;
c.Icon = req.Icon;
c.AttrValue = ParseAttrValue(req.AttrValue);
c.DefaultColor = req.DefaultColor;
c.Sort = req.Sort;
c.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return new SharedColorResponse(c.Id, c.ProjectId, c.ElementKey, c.Title, c.Icon, c.AttrValue.ToString(), c.DefaultColor, c.Sort);
}
public async Task DeleteSharedColorAsync(Guid id)
{
var c = await db.SharedColors.FindAsync(id) ?? throw new KeyNotFoundException($"Color {id} not found");
db.SharedColors.Remove(c);
await db.SaveChangesAsync();
}
// ── Colour presets (palettes) ───────────────────────────────────────────
public async Task<List<ColorPresetResponse>> GetColorPresetsAsync(Guid projectId) =>
await db.SharedColorPresets.Where(p => p.ProjectId == projectId).OrderBy(p => p.Sort)
.Select(p => new ColorPresetResponse(p.Id, p.ProjectId, p.Name, p.Sort,
p.Items.OrderBy(i => i.Sort).Select(i => new ColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList()))
.ToListAsync();
public async Task<ColorPresetResponse> CreateColorPresetAsync(SaveColorPresetRequest req)
{
var preset = new SharedColorPreset
{
ProjectId = req.ProjectId,
Name = req.Name,
Sort = req.Sort,
Items = (req.Items ?? []).Select(i => new SharedColorPresetItem
{
ElementKey = i.ElementKey, Value = i.Value, Sort = i.Sort,
}).ToList(),
};
db.SharedColorPresets.Add(preset);
await db.SaveChangesAsync();
return await GetPresetAsync(preset.Id);
}
public async Task<ColorPresetResponse> UpdateColorPresetAsync(Guid id, SaveColorPresetRequest req)
{
var preset = await db.SharedColorPresets.Include(p => p.Items).FirstOrDefaultAsync(p => p.Id == id)
?? throw new KeyNotFoundException($"Preset {id} not found");
preset.Name = req.Name;
preset.Sort = req.Sort;
db.SharedColorPresetItems.RemoveRange(preset.Items);
preset.Items = (req.Items ?? []).Select(i => new SharedColorPresetItem
{
PresetId = preset.Id, ElementKey = i.ElementKey, Value = i.Value, Sort = i.Sort,
}).ToList();
await db.SaveChangesAsync();
return await GetPresetAsync(preset.Id);
}
public async Task DeleteColorPresetAsync(Guid id)
{
var preset = await db.SharedColorPresets.FindAsync(id) ?? throw new KeyNotFoundException($"Preset {id} not found");
db.SharedColorPresets.Remove(preset);
await db.SaveChangesAsync();
}
private async Task<ColorPresetResponse> GetPresetAsync(Guid id) =>
await db.SharedColorPresets.Where(p => p.Id == id)
.Select(p => new ColorPresetResponse(p.Id, p.ProjectId, p.Name, p.Sort,
p.Items.OrderBy(i => i.Sort).Select(i => new ColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList()))
.FirstAsync();
// ── helpers ─────────────────────────────────────────────────────────────
private static SceneResponse ToSceneResponse(Scene s) => new(
s.Id, s.ProjectId, s.Key, s.Title, s.LocalizedTitle, s.SceneType.ToString(),
s.Image, s.Demo, s.SceneColorSvg, s.SnapshotUrl, s.GenerateKf,
s.DefaultDurationSec, s.MinDurationSec, s.MaxDurationSec, s.OverlapAtEndSec,
s.CanHandleDuration, s.ManualColorSelection, s.Sort, s.IsActive);
private static SceneKind ParseSceneKind(string? v) =>
Enum.TryParse<SceneKind>(v, true, out var k) ? k : SceneKind.Normal;
private static AttrValueKind ParseAttrValue(string? v) =>
Enum.TryParse<AttrValueKind>(v, true, out var a) ? a : AttrValueKind.fill;
}