Files
flatrender/services/studio/FlatRender.StudioSvc/Application/Services/StudioService.cs
T
soroush.asadi c0d04fa855 feat(studio+render): wire theme picker → saved_shared_colors → FlexStory render
Closes the theme→render gap: the studio theme picker now actually drives a
FlexStory render's colours. GetFlexStoryProps reads saved_shared_colors by
element_key (accentColor/secondaryColor/backgroundColor/textColor), but the studio
only wrote the theme into scene_data — so the picker never reached the MP4.

- studio-svc: UpdateSharedColorsAsync upserts saved_shared_colors by (project,
  element_key) + PATCH /v1/saved-projects/{id}/shared-colors endpoint +
  UpdateColorsRequest/UpdateColorItem. Mirrors UpdateSceneContentsAsync. (dotnet
  build: 0 errors.)
- gateway already wildcard-routes /v1/saved-projects/*path → studio-svc (no change).
- Next: /api/projects/[id]/colors route → gateway; project-api patchProjectColors
  + themeColorsFromSceneData (maps scene_data sceneAccentColor… → the colorSchema
  keys); performSave best-effort pushes the 4 colours alongside contents.

Chain: theme picker → store → scene_data → performSave → patchProjectColors →
gateway → studio-svc upsert → saved_shared_colors → GetFlexStoryProps → render.
Verified: Next build + dotnet build both clean; theme presets render cohesively
across all 6 (incl. dark Midnight). End-to-end studio→render needs the live stack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:04:47 +03:30

532 lines
27 KiB
C#

using System.Text.Json;
using FlatRender.StudioSvc.Domain.Entities;
using FlatRender.StudioSvc.Domain.Enums;
using FlatRender.StudioSvc.Infrastructure.Data;
using FlatRender.StudioSvc.Models.Requests;
using FlatRender.StudioSvc.Models.Responses;
using Microsoft.EntityFrameworkCore;
namespace FlatRender.StudioSvc.Application.Services;
public class StudioService(StudioDbContext db)
{
public async Task<PagedResponse<SavedProjectSummaryResponse>> ListProjectsAsync(
Guid userId, SavedProjectListRequest req)
{
var q = db.SavedProjects.Where(x => x.UserId == userId);
if (!string.IsNullOrWhiteSpace(req.Q))
q = q.Where(x => EF.Functions.ILike(x.Name, $"%{req.Q}%"));
if (!string.IsNullOrWhiteSpace(req.Type) &&
Enum.TryParse<SavedProjectType>(req.Type, true, out var t))
q = q.Where(x => x.Type == t);
var total = await q.LongCountAsync();
var items = await q
.OrderByDescending(x => x.LastEditDate)
.Skip((req.Page - 1) * req.PageSize)
.Take(req.PageSize)
.ToListAsync();
return new PagedResponse<SavedProjectSummaryResponse>(
items.Select(MapSummary),
new PaginationMeta(req.Page, req.PageSize, total,
(int)Math.Ceiling((double)total / req.PageSize)));
}
public async Task<SavedProjectFullResponse> GetProjectAsync(Guid id, Guid userId)
{
var p = await LoadProjectQuery()
.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId)
?? throw new KeyNotFoundException($"Project {id} not found");
return MapFull(p);
}
/// <summary>Internal: load project without user-ownership check (for service-to-service calls).</summary>
public async Task<SavedProjectFullResponse> GetProjectForRenderAsync(Guid id)
{
var p = await LoadProjectQuery()
.FirstOrDefaultAsync(x => x.Id == id)
?? throw new KeyNotFoundException($"Project {id} not found");
return MapFull(p);
}
private IQueryable<Domain.Entities.SavedProject> LoadProjectQuery() =>
db.SavedProjects
.Include(x => x.Scenes.OrderBy(s => s.Sort))
.ThenInclude(s => s.Contents.OrderBy(c => c.Sort))
.Include(x => x.Scenes).ThenInclude(s => s.Colors.OrderBy(c => c.Sort))
.Include(x => x.Scenes).ThenInclude(s => s.ColorPresets.OrderBy(c => c.Sort))
.ThenInclude(cp => cp.Items.OrderBy(i => i.Sort))
.Include(x => x.Scenes).ThenInclude(s => s.Characters)
.ThenInclude(ch => ch.Controllers.OrderBy(c => c.Sort))
.Include(x => x.SharedColors.OrderBy(c => c.Sort))
.Include(x => x.SharedColorPresets.OrderBy(c => c.Sort))
.ThenInclude(cp => cp.Items.OrderBy(i => i.Sort))
.Include(x => x.SharedLayers.OrderBy(l => l.Sort));
public async Task<SavedProjectFullResponse> CreateProjectAsync(
Guid userId, Guid tenantId, CreateSavedProjectRequest req)
{
var project = new SavedProject
{
TenantId = tenantId,
UserId = userId,
OriginalProjectId = req.OriginalProjectId,
OriginalProjectName = req.Name ?? $"Project {DateTime.UtcNow:yyyy-MM-dd}",
Name = req.Name ?? $"Project {DateTime.UtcNow:yyyy-MM-dd}",
SelectedPresetStoryId = req.PresetStoryId,
ProjectDurationSec = 0,
};
await using var tx = await db.Database.BeginTransactionAsync();
db.SavedProjects.Add(project);
await db.SaveChangesAsync();
// Deep-copy the template's scene graph (scenes + content/colour elements) from
// the content schema so the new project opens editable in the studio. Same DB,
// so this is one atomic cross-schema copy.
if (req.CopyDefaultValues && req.OriginalProjectId != Guid.Empty)
await CopyTemplateGraphAsync(project.Id, req.OriginalProjectId);
// Pre-fill from the chosen preset story (premade example video): overlay its
// saved values onto the freshly-copied scene contents + colours.
if (req.PresetStoryId.HasValue)
await ApplyPresetValuesAsync(project.Id, req.PresetStoryId.Value);
await tx.CommitAsync();
return await GetProjectAsync(project.Id, userId);
}
/// <summary>
/// Applies an admin-authored preset story's filled values onto a freshly-created
/// project. The preset stores them in content.preset_stories.scenes_spa as
/// <c>{ "values": { contentKey: value }, "colors": { elementKey: hex } }</c>.
/// Content/colour keys are globally unique (AE naming convention), so matching by
/// key alone reaches the right scene element. Runs inside the create transaction.
/// </summary>
private async Task ApplyPresetValuesAsync(Guid savedProjectId, Guid presetStoryId)
{
var spa = await db.Database
.SqlQuery<string?>($@"SELECT scenes_spa AS ""Value"" FROM content.preset_stories
WHERE id = {presetStoryId} AND deleted_at IS NULL")
.FirstOrDefaultAsync();
if (string.IsNullOrWhiteSpace(spa)) return;
JsonElement root;
try { root = JsonDocument.Parse(spa).RootElement; }
catch { return; } // malformed scenes_spa → skip pre-fill, leave defaults
if (root.ValueKind != JsonValueKind.Object) return;
// content element values → saved_scene_contents.value (match by key)
if (root.TryGetProperty("values", out var vals) && vals.ValueKind == JsonValueKind.Object)
{
var valuesJson = vals.GetRawText();
await db.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE studio.saved_scene_contents c
SET value = pv.value, updated_at = now()
FROM json_each_text({valuesJson}::json) AS pv(key, value)
WHERE c.key = pv.key
AND c.saved_scene_id IN (
SELECT id FROM studio.saved_scenes WHERE saved_project_id = {savedProjectId});");
}
// colour values → shared + per-scene colours (match by element_key)
if (root.TryGetProperty("colors", out var cols) && cols.ValueKind == JsonValueKind.Object)
{
var colorsJson = cols.GetRawText();
await db.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE studio.saved_shared_colors sc
SET value = pc.value, is_selected = true
FROM json_each_text({colorsJson}::json) AS pc(key, value)
WHERE sc.element_key = pc.key AND sc.saved_project_id = {savedProjectId};");
await db.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE studio.saved_scene_colors c
SET value = pc.value, is_selected = true
FROM json_each_text({colorsJson}::json) AS pc(key, value)
WHERE c.element_key = pc.key
AND c.saved_scene_id IN (
SELECT id FROM studio.saved_scenes WHERE saved_project_id = {savedProjectId});");
}
}
/// <summary>
/// Copies a content template project's scenes, content elements and colour elements
/// into a freshly-created editable project. Runs inside the caller's transaction;
/// the temp scene-id map drops on commit. Same Postgres DB → atomic cross-schema copy.
/// </summary>
private async Task CopyTemplateGraphAsync(Guid savedProjectId, Guid originalProjectId)
{
// 1. scenes → capture old→new id mapping in a temp table
await db.Database.ExecuteSqlRawAsync(@"
CREATE TEMP TABLE _scene_map ON COMMIT DROP AS
WITH ins AS (
INSERT INTO studio.saved_scenes
(saved_project_id, original_scene_id, key, title, image, demo,
scene_color_svg, scene_type, sort, scene_length_sec, min_duration_sec,
max_duration_sec, overlap_at_end_sec, can_handle_duration,
manual_color_selection, created_at, updated_at)
SELECT {0}, s.id, s.key, s.title, s.image, s.demo, s.scene_color_svg,
s.scene_type::text, s.sort, COALESCE(s.default_duration_sec, 0),
s.min_duration_sec, s.max_duration_sec, s.overlap_at_end_sec,
s.can_handle_duration, s.manual_color_selection, now(), now()
FROM content.scenes s
WHERE s.project_id = {1} AND s.deleted_at IS NULL AND s.is_active = true
RETURNING id AS new_id, original_scene_id AS old_id
)
SELECT new_id, old_id FROM ins;",
savedProjectId, originalProjectId);
// 2. content elements — incl. repeater children flattened via repeater_item_key
await db.Database.ExecuteSqlRawAsync(@"
INSERT INTO studio.saved_scene_contents
(saved_scene_id, key, title, localized_title, hint, type, value,
font_face, font_face_name, font_size, default_font_size, default_font_face,
justify, position_in_container, direction_layer_value, is_text_box,
ai_input_type, mapped_list, thumbnail, sort, repeater_item_key,
repeater_index, status, created_at, updated_at)
SELECT sm.new_id, ce.key, ce.title, ce.localized_title, ce.hint, ce.type::text,
ce.default_value, ce.font_face, ce.font_face_name, ce.font_size,
ce.default_font_size, ce.default_font_face, ce.justify::text,
ce.position_in_container, ce.direction_layer_value, ce.is_text_box,
ce.ai_input_type::text, ce.mapped_list, ce.thumbnail, ce.sort,
ri.repeat_item_key, ri.sort, 'default', now(), now()
FROM content.scene_content_elements ce
JOIN _scene_map sm ON sm.old_id = ce.scene_id
LEFT JOIN content.repeater_items ri ON ri.id = ce.repeater_item_id;");
// 3. colour elements
await db.Database.ExecuteSqlRawAsync(@"
INSERT INTO studio.saved_scene_colors
(saved_scene_id, element_key, title, icon, attr_value, value, is_selected, sort)
SELECT sm.new_id, ce.element_key, ce.title, ce.icon, ce.attr_value::text,
COALESCE(ce.default_color, ''), false, ce.sort
FROM content.scene_color_elements ce
JOIN _scene_map sm ON sm.old_id = ce.scene_id;");
// 3b. characters — studio's `key` is a uuid; store the original character id
await db.Database.ExecuteSqlRawAsync(@"
INSERT INTO studio.saved_scene_characters (saved_scene_id, key, name, icon)
SELECT sm.new_id, ch.id, ch.name, ch.icon
FROM content.scene_characters ch
JOIN _scene_map sm ON sm.old_id = ch.scene_id;");
// 3c. character controllers (correlate the new character by scene + original id)
await db.Database.ExecuteSqlRawAsync(@"
INSERT INTO studio.saved_scene_character_controllers
(saved_scene_character_id, name, key, value, sort)
SELECT sc.id, cc.name, cc.key, cc.default_value, cc.sort
FROM content.scene_character_controllers cc
JOIN content.scene_characters cch ON cch.id = cc.scene_character_id
JOIN _scene_map sm ON sm.old_id = cch.scene_id
JOIN studio.saved_scene_characters sc
ON sc.saved_scene_id = sm.new_id AND sc.key = cch.id;");
// 3d. colour presets
await db.Database.ExecuteSqlRawAsync(@"
INSERT INTO studio.saved_scene_color_presets (saved_scene_id, is_selected, sort)
SELECT sm.new_id, false, cp.sort
FROM content.scene_color_presets cp
JOIN _scene_map sm ON sm.old_id = cp.scene_id;");
// 3e. colour preset items (correlate the new preset by scene + sort)
await db.Database.ExecuteSqlRawAsync(@"
INSERT INTO studio.saved_scene_color_preset_items (preset_id, element_key, value, sort)
SELECT sp.id, ci.element_key, ci.value, ci.sort
FROM content.scene_color_preset_items ci
JOIN content.scene_color_presets cp ON cp.id = ci.preset_id
JOIN _scene_map sm ON sm.old_id = cp.scene_id
JOIN studio.saved_scene_color_presets sp
ON sp.saved_scene_id = sm.new_id AND sp.sort = cp.sort;");
// 4. shared (project-level) colours — frshare frd_* controls
await db.Database.ExecuteSqlRawAsync(@"
INSERT INTO studio.saved_shared_colors
(saved_project_id, element_key, title, icon, attr_value, value, is_selected, sort)
SELECT {0}, sc.element_key, sc.title, sc.icon, sc.attr_value::text,
COALESCE(sc.default_color, ''), false, sc.sort
FROM content.shared_colors sc
WHERE sc.project_id = {1};",
savedProjectId, originalProjectId);
// 5. carry the template's render mode (FIX/FLEXIBLE/…) so the studio knows whether
// adding scenes is allowed, plus a few useful project defaults.
await db.Database.ExecuteSqlRawAsync(@"
UPDATE studio.saved_projects sp
SET choose_mode = COALESCE((SELECT p.choose_mode::text FROM content.projects p WHERE p.id = {1}), sp.choose_mode)
WHERE sp.id = {0};",
savedProjectId, originalProjectId);
}
public async Task<SavedProjectFullResponse> UpdateProjectAsync(
Guid id, Guid userId, UpdateSavedProjectRequest req)
{
var project = await db.SavedProjects
.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId)
?? throw new KeyNotFoundException($"Project {id} not found");
if (req.Name != null) project.Name = req.Name;
if (req.Image != null) project.Image = req.Image;
if (req.Type != null && Enum.TryParse<SavedProjectType>(req.Type, true, out var t))
project.Type = t;
if (req.MusicFileId.HasValue) project.MusicFileId = req.MusicFileId;
if (req.MusicTrackId.HasValue) project.MusicTrackId = req.MusicTrackId;
if (req.MusicVolume.HasValue) project.MusicVolume = req.MusicVolume.Value;
if (req.VoiceoverFileId.HasValue) project.VoiceoverFileId = req.VoiceoverFileId;
if (req.VoiceoverVolume.HasValue) project.VoiceoverVolume = req.VoiceoverVolume.Value;
if (req.VoiceoverRecordedInBrowser.HasValue)
project.VoiceoverRecordedInBrowser = req.VoiceoverRecordedInBrowser.Value;
if (req.SfxVolume.HasValue) project.SfxVolume = req.SfxVolume.Value;
if (req.SfxEnabled.HasValue) project.SfxEnabled = req.SfxEnabled.Value;
if (req.AudioVisualizerMusicUrl != null) project.AudioVisualizerMusicUrl = req.AudioVisualizerMusicUrl;
if (req.AudioVisualizerDurationSec.HasValue)
project.AudioVisualizerDurationSec = req.AudioVisualizerDurationSec;
if (req.ManualColorPicker.HasValue) project.ManualColorPicker = req.ManualColorPicker.Value;
if (req.SelectedPresetStoryId.HasValue) project.SelectedPresetStoryId = req.SelectedPresetStoryId;
if (req.LastEditStep != null) project.LastEditStep = req.LastEditStep;
if (req.EditState != null) project.EditState = req.EditState;
project.LastEditDate = DateTime.UtcNow;
project.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return await GetProjectAsync(id, userId);
}
/// <summary>
/// Update individual scene-input values by content key (the studio editor's live
/// edits). Writes to saved_scene_contents.value so the render binder uses them.
/// </summary>
public async Task<int> UpdateSceneContentsAsync(Guid projectId, Guid userId, List<UpdateContentItem> items)
{
var owns = await db.SavedProjects.AnyAsync(x => x.Id == projectId && x.UserId == userId);
if (!owns) throw new KeyNotFoundException($"Project {projectId} not found");
var updated = 0;
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.Key)) continue;
// ExecuteSqlInterpolated maps C# null → typed SQL NULL (raw DBNull params throw).
updated += await db.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE studio.saved_scene_contents c
SET value = {item.Value}, value_file_id = COALESCE({item.ValueFileId}, c.value_file_id), updated_at = now()
FROM studio.saved_scenes s
WHERE c.saved_scene_id = s.id AND s.saved_project_id = {projectId} AND c.key = {item.Key}");
}
await db.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE studio.saved_projects SET last_edit_date = now(), updated_at = now() WHERE id = {projectId}");
return updated;
}
/// <summary>
/// Update the project's theme colours by element key (accentColor/secondaryColor/
/// backgroundColor/textColor). Upserts saved_shared_colors so the FlexStory render
/// binder (GetFlexStoryProps) reads the studio theme picker's colours.
/// </summary>
public async Task<int> UpdateSharedColorsAsync(Guid projectId, Guid userId, List<UpdateColorItem> items)
{
var owns = await db.SavedProjects.AnyAsync(x => x.Id == projectId && x.UserId == userId);
if (!owns) throw new KeyNotFoundException($"Project {projectId} not found");
var updated = 0;
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.Key) || string.IsNullOrWhiteSpace(item.Value)) continue;
// Upsert by (project, element_key): the row usually exists (copied from the
// template's shared colours); insert if a FlexStory key is missing.
var n = await db.Database.ExecuteSqlInterpolatedAsync($@"
UPDATE studio.saved_shared_colors
SET value = {item.Value}
WHERE saved_project_id = {projectId} AND element_key = {item.Key}");
if (n == 0)
{
n = await db.Database.ExecuteSqlInterpolatedAsync($@"
INSERT INTO studio.saved_shared_colors
(saved_project_id, element_key, attr_value, value, is_selected, sort)
VALUES ({projectId}, {item.Key}, 'fill', {item.Value}, true, 0)");
}
updated += n;
}
await db.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE studio.saved_projects SET last_edit_date = now(), updated_at = now() WHERE id = {projectId}");
return updated;
}
public async Task DeleteProjectAsync(Guid id, Guid userId)
{
var project = await db.SavedProjects
.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId)
?? throw new KeyNotFoundException($"Project {id} not found");
project.DeletedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
}
/// <summary>
/// Full scene save — replaces all scene data atomically.
/// Called by the studio editor on every save checkpoint.
/// </summary>
public async Task<SavedProjectFullResponse> SaveScenesAsync(
Guid projectId, Guid userId, List<SaveSceneRequest> scenes)
{
var project = await db.SavedProjects
.FirstOrDefaultAsync(x => x.Id == projectId && x.UserId == userId)
?? throw new KeyNotFoundException($"Project {projectId} not found");
// Delete existing scenes (cascade deletes children)
var existing = await db.SavedScenes
.Where(x => x.SavedProjectId == projectId).ToListAsync();
db.SavedScenes.RemoveRange(existing);
foreach (var (sceneReq, idx) in scenes.Select((s, i) => (s, i)))
{
var scene = new SavedScene
{
SavedProjectId = projectId,
OriginalSceneId = sceneReq.OriginalSceneId,
Key = sceneReq.Key,
Title = sceneReq.Title,
Image = sceneReq.Image,
SceneColorSvg = sceneReq.SceneColorSvg,
SceneType = sceneReq.SceneType,
Sort = sceneReq.Sort,
SceneLengthSec = sceneReq.SceneLengthSec,
MinDurationSec = sceneReq.MinDurationSec,
MaxDurationSec = sceneReq.MaxDurationSec,
OverlapAtEndSec = sceneReq.OverlapAtEndSec,
CanHandleDuration = sceneReq.CanHandleDuration,
ManualColorSelection = sceneReq.ManualColorSelection,
};
foreach (var c in sceneReq.Contents)
scene.Contents.Add(MapContentEntity(c));
foreach (var c in sceneReq.Colors)
scene.Colors.Add(new SavedSceneColor
{
ElementKey = c.ElementKey, Title = c.Title, Icon = c.Icon,
AttrValue = c.AttrValue, Value = c.Value, IsSelected = c.IsSelected, Sort = c.Sort
});
db.SavedScenes.Add(scene);
}
// Sync shared colors
var sharedColors = scenes.SelectMany(s => s.SharedColors).DistinctBy(x => x.ElementKey).ToList();
if (sharedColors.Count > 0)
{
var existingSC = await db.SavedSharedColors
.Where(x => x.SavedProjectId == projectId).ToListAsync();
db.SavedSharedColors.RemoveRange(existingSC);
foreach (var c in sharedColors)
db.SavedSharedColors.Add(new SavedSharedColor
{
SavedProjectId = projectId, ElementKey = c.ElementKey, Title = c.Title,
Icon = c.Icon, AttrValue = c.AttrValue, Value = c.Value,
IsSelected = c.IsSelected, Sort = c.Sort
});
}
// Sync shared layers
var sharedLayers = scenes.SelectMany(s => s.SharedLayers).DistinctBy(x => x.Key).ToList();
if (sharedLayers.Count > 0)
{
var existingSL = await db.SavedSharedLayers
.Where(x => x.SavedProjectId == projectId).ToListAsync();
db.SavedSharedLayers.RemoveRange(existingSL);
foreach (var l in sharedLayers)
db.SavedSharedLayers.Add(MapSharedLayerEntity(projectId, l));
}
project.LastEditDate = DateTime.UtcNow;
project.UpdatedAt = DateTime.UtcNow;
// Recalc total duration
project.ProjectDurationSec = scenes.Sum(s => s.SceneLengthSec - s.OverlapAtEndSec);
await db.SaveChangesAsync();
return await GetProjectAsync(projectId, userId);
}
// ── Mappers ───────────────────────────────────────────────────────────────
private static SavedProjectSummaryResponse MapSummary(SavedProject p) => new(
p.Id, p.UserId, p.OriginalProjectId, p.OriginalProjectName, p.OriginalContainerSlug,
p.Name, p.Image, p.Type.ToString(), p.Resolution, p.ChooseMode,
p.ProjectDurationSec, p.LastEditDate, p.CreatedAt);
private static SavedProjectFullResponse MapFull(SavedProject p) => new(
p.Id, p.UserId, p.OriginalProjectId, p.OriginalProjectName, p.OriginalContainerSlug,
p.Name, p.Image, p.Type.ToString(), p.FrameRate, p.ProjectDurationSec,
p.Resolution, p.ChooseMode, p.VipFactor,
p.MusicFileId, p.MusicTrackId, p.MusicVolume,
p.VoiceoverFileId, p.VoiceoverVolume, p.VoiceoverRecordedInBrowser,
p.SfxVolume, p.SfxEnabled, p.AudioVisualizerMusicUrl, p.AudioVisualizerDurationSec,
p.ManualColorPicker, p.SelectedPresetStoryId,
p.LastEditStep, p.EditState, p.LastEditDate, p.CreatedAt,
p.Scenes.Select(MapSceneResponse).ToList(),
p.SharedColors.Select(MapSharedColorResponse).ToList(),
p.SharedColorPresets.Select(MapSharedColorPresetResponse).ToList(),
p.SharedLayers.Select(MapSharedLayerResponse).ToList()
);
private static SavedSceneResponse MapSceneResponse(SavedScene s) => new(
s.Id, s.OriginalSceneId, s.Key, s.Title, s.Image, s.SceneType,
s.Sort, s.SceneLengthSec, s.MinDurationSec, s.MaxDurationSec,
s.OverlapAtEndSec, s.CanHandleDuration, s.ManualColorSelection, s.SelectedColorPresetId,
s.Contents.Select(MapContentResponse).ToList(),
s.Colors.Select(c => new SavedSceneColorResponse(c.Id, c.ElementKey, c.Title, c.Icon, c.AttrValue, c.Value, c.IsSelected, c.Sort)).ToList(),
s.ColorPresets.Select(cp => new SavedSceneColorPresetResponse(cp.Id, cp.IsSelected, cp.Sort,
cp.Items.Select(i => new SavedColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList()
)).ToList(),
s.Characters.Select(ch => new SavedSceneCharacterResponse(ch.Id, ch.Key, ch.Name, ch.Icon,
ch.Controllers.Select(c => new SavedCharacterControllerResponse(c.Id, c.Name, c.Key, c.Value, c.Sort)).ToList()
)).ToList()
);
private static SavedSceneContentResponse MapContentResponse(SavedSceneContent c) => new(
c.Id, c.Key, c.Title, c.Type, c.Value, c.ValueFileId, c.FileUrlCached,
c.FontFace, c.FontSize, c.Justify, c.PositionInContainer, c.DirectionLayerValue,
c.IsTextBox, c.AiInputType, c.SelectedDp, c.RepeaterItemKey, c.RepeaterIndex,
c.IsFocused, c.MappedList, c.Sort);
private static SavedSharedColorResponse MapSharedColorResponse(SavedSharedColor c) =>
new(c.Id, c.ElementKey, c.Title, c.Icon, c.AttrValue, c.Value, c.IsSelected, c.Sort);
private static SavedSharedColorPresetResponse MapSharedColorPresetResponse(SavedSharedColorPreset cp) =>
new(cp.Id, cp.Name, cp.IsSelected, cp.Sort,
cp.Items.Select(i => new SavedColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList());
private static SavedSharedLayerResponse MapSharedLayerResponse(SavedSharedLayer l) => new(
l.Id, l.Key, l.Title, l.Type, l.Value, l.ValueFileId, l.FileUrlCached,
l.FontFace, l.FontSize, l.Justify, l.PositionInContainer, l.DirectionLayerValue,
l.IsTextBox, l.AiInputType, l.MappedList, l.Width, l.Height,
l.IsFocused, l.IsFontChangeable, l.IsFontSizeChangeable, l.Sort);
private static SavedSceneContent MapContentEntity(SaveSceneContentRequest c) => new()
{
Key = c.Key, Title = c.Title, Type = c.Type, Value = c.Value,
ValueFileId = c.ValueFileId, InsertedFileType = c.InsertedFileType,
FontFace = c.FontFace, FontFaceName = c.FontFaceName, FontSize = c.FontSize,
DefaultFontSize = c.DefaultFontSize, DefaultFontFace = c.DefaultFontFace,
Justify = c.Justify, PositionInContainer = c.PositionInContainer,
DirectionLayerValue = c.DirectionLayerValue, IsTextBox = c.IsTextBox,
AiInputType = c.AiInputType, SelectedDp = c.SelectedDp,
RepeaterItemKey = c.RepeaterItemKey, RepeaterIndex = c.RepeaterIndex,
IsFocused = c.IsFocused, MappedList = c.MappedList, Thumbnail = c.Thumbnail, Sort = c.Sort
};
private static SavedSharedLayer MapSharedLayerEntity(Guid projectId, SaveSharedLayerRequest l) => new()
{
SavedProjectId = projectId, Key = l.Key, Title = l.Title, Type = l.Type,
Value = l.Value, ValueFileId = l.ValueFileId, FontFace = l.FontFace,
FontFaceName = l.FontFaceName, FontSize = l.FontSize, Justify = l.Justify,
PositionInContainer = l.PositionInContainer, DirectionLayerValue = l.DirectionLayerValue,
IsTextBox = l.IsTextBox, AiInputType = l.AiInputType, MappedList = l.MappedList,
Thumbnail = l.Thumbnail, Width = l.Width, Height = l.Height, IsFocused = l.IsFocused,
IsFontChangeable = l.IsFontChangeable, IsFontSizeChangeable = l.IsFontSizeChangeable, Sort = l.Sort
};
}