using FlatRender.ContentSvc.Domain.Entities; using FlatRender.ContentSvc.Domain.Enums; using FlatRender.ContentSvc.Infrastructure.Data; using FlatRender.ContentSvc.Models.Requests; using FlatRender.ContentSvc.Models.Responses; using Microsoft.EntityFrameworkCore; namespace FlatRender.ContentSvc.Application.Services; public class TemplateService(ContentDbContext db) { public async Task> GetContainersAsync(ContainerListRequest req) { var q = db.ProjectContainers .Include(x => x.ContainerCategories).ThenInclude(x => x.Category) .Include(x => x.ContainerTags).ThenInclude(x => x.Tag) .AsQueryable(); if (!string.IsNullOrWhiteSpace(req.Search)) q = q.Where(x => EF.Functions.ILike(x.Name, $"%{req.Search}%") || EF.Functions.ILike(x.Slug, $"%{req.Search}%")); if (req.CategoryId.HasValue) q = q.Where(x => x.ContainerCategories.Any(cc => cc.CategoryId == req.CategoryId)); if (!string.IsNullOrWhiteSpace(req.TagSlug)) q = q.Where(x => x.ContainerTags.Any(ct => ct.Tag.Slug == req.TagSlug)); if (req.IsPublished.HasValue) q = q.Where(x => x.IsPublished == req.IsPublished); if (req.IsPremium.HasValue) q = q.Where(x => x.IsPremium == req.IsPremium); if (!string.IsNullOrWhiteSpace(req.Mode)) { if (Enum.TryParse(req.Mode, true, out var mode)) q = q.Where(x => x.PrimaryMode == mode); } q = req.Sort switch { "sort_asc" => q.OrderBy(x => x.Sort), "name_asc" => q.OrderBy(x => x.Name), "view_count_desc" => q.OrderByDescending(x => x.ViewCount), _ => q.OrderByDescending(x => x.SortDate) }; var total = await q.LongCountAsync(); var items = await q.Skip((req.Page - 1) * req.PageSize).Take(req.PageSize).ToListAsync(); return new PagedResponse( items.Select(MapContainerSummary), new PaginationMeta(req.Page, req.PageSize, total, (int)Math.Ceiling((double)total / req.PageSize)) ); } public async Task GetContainerBySlugAsync(string slug) { var container = await db.ProjectContainers .Include(x => x.ContainerCategories).ThenInclude(x => x.Category) .Include(x => x.ContainerTags).ThenInclude(x => x.Tag) .Include(x => x.Projects.Where(p => p.DeletedAt == null)) .FirstOrDefaultAsync(x => x.Slug == slug) ?? throw new KeyNotFoundException($"Container '{slug}' not found"); return MapContainerDetail(container); } public async Task GetContainerByIdAsync(Guid id) { var container = await db.ProjectContainers .Include(x => x.ContainerCategories).ThenInclude(x => x.Category) .Include(x => x.ContainerTags).ThenInclude(x => x.Tag) .Include(x => x.Projects.Where(p => p.DeletedAt == null)) .FirstOrDefaultAsync(x => x.Id == id) ?? throw new KeyNotFoundException($"Container {id} not found"); return MapContainerDetail(container); } public async Task CreateContainerAsync(CreateContainerRequest req) { if (!Enum.TryParse(req.PrimaryMode, true, out var mode)) throw new ArgumentException($"Invalid PrimaryMode: {req.PrimaryMode}"); var container = new ProjectContainer { Slug = req.Slug, Name = req.Name, Description = req.Description, Keywords = req.Keywords, NewsText = req.NewsText, Image = req.Image, Demo = req.Demo, FullDemo = req.FullDemo, MiniDemo = req.MiniDemo, DemoScriptTag = req.DemoScriptTag, IsPublished = req.IsPublished, IsPremium = req.IsPremium, IsMockup = req.IsMockup, PrimaryMode = mode, Sort = req.Sort, SortDate = DateTime.UtcNow }; db.ProjectContainers.Add(container); await db.SaveChangesAsync(); await SyncContainerCategoriesAsync(container.Id, req.CategoryIds); await SyncContainerTagsAsync(container.Id, req.TagIds); return await GetContainerByIdAsync(container.Id); } public async Task UpdateContainerAsync(Guid id, UpdateContainerRequest req) { var container = await db.ProjectContainers.FindAsync(id) ?? throw new KeyNotFoundException($"Container {id} not found"); if (!Enum.TryParse(req.PrimaryMode, true, out var mode)) throw new ArgumentException($"Invalid PrimaryMode: {req.PrimaryMode}"); container.Slug = req.Slug; container.Name = req.Name; container.Description = req.Description; container.Keywords = req.Keywords; container.NewsText = req.NewsText; container.Image = req.Image; container.Demo = req.Demo; container.FullDemo = req.FullDemo; container.MiniDemo = req.MiniDemo; container.DemoScriptTag = req.DemoScriptTag; container.IsPublished = req.IsPublished; container.IsPremium = req.IsPremium; container.IsMockup = req.IsMockup; container.PrimaryMode = mode; container.Sort = req.Sort; container.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); await SyncContainerCategoriesAsync(id, req.CategoryIds); await SyncContainerTagsAsync(id, req.TagIds); return await GetContainerByIdAsync(id); } public async Task DeleteContainerAsync(Guid id) { var container = await db.ProjectContainers.FindAsync(id) ?? throw new KeyNotFoundException($"Container {id} not found"); container.DeletedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task GetProjectDetailAsync(Guid id) { var project = await db.Projects .Include(x => x.Scenes.Where(s => s.DeletedAt == null)).ThenInclude(x => x.RepeaterItems) .Include(x => x.Scenes).ThenInclude(x => x.ContentElements) .Include(x => x.Scenes).ThenInclude(x => x.ColorElements) .Include(x => x.Scenes).ThenInclude(x => x.ColorPresets).ThenInclude(x => x.Items) .Include(x => x.Scenes).ThenInclude(x => x.Characters).ThenInclude(x => x.Controllers).ThenInclude(x => x.Options) .Include(x => x.SharedColors) .Include(x => x.SharedLayers) .FirstOrDefaultAsync(x => x.Id == id) ?? throw new KeyNotFoundException($"Project {id} not found"); return MapProjectDetail(project); } public async Task CreateProjectAsync(CreateProjectRequest req) { if (!Enum.TryParse(req.ChooseMode, true, out var chooseMode)) throw new ArgumentException($"Invalid ChooseMode: {req.ChooseMode}"); if (!Enum.TryParse(req.Resolution, true, out var resolution)) throw new ArgumentException($"Invalid Resolution: {req.Resolution}"); var project = new Project { ContainerId = req.ContainerId, ProjectServerId = req.ProjectServerId, Name = req.Name, Description = req.Description, Image = req.Image, FullDemo = req.FullDemo, DemoScriptTag = req.DemoScriptTag, DownloadLink = req.DownloadLink, Folder = req.Folder, OriginalWidth = req.OriginalWidth, OriginalHeight = req.OriginalHeight, Aspect = req.Aspect, ProjectDurationSec = req.ProjectDurationSec, MinDurationSec = req.MinDurationSec, MaxDurationSec = req.MaxDurationSec, FreeFps = req.FreeFps, ChooseMode = chooseMode, Resolution = resolution, VipFactor = req.VipFactor, RenderAepComp = req.RenderAepComp, IsPublished = req.IsPublished, Sort = req.Sort }; db.Projects.Add(project); await db.SaveChangesAsync(); return await GetProjectDetailAsync(project.Id); } public async Task UpdateProjectAsync(Guid id, UpdateProjectRequest req) { var project = await db.Projects.FindAsync(id) ?? throw new KeyNotFoundException($"Project {id} not found"); if (!Enum.TryParse(req.ChooseMode, true, out var chooseMode)) throw new ArgumentException($"Invalid ChooseMode: {req.ChooseMode}"); if (!Enum.TryParse(req.Resolution, true, out var resolution)) throw new ArgumentException($"Invalid Resolution: {req.Resolution}"); project.Name = req.Name; project.Description = req.Description; project.Image = req.Image; project.FullDemo = req.FullDemo; project.DemoScriptTag = req.DemoScriptTag; project.DownloadLink = req.DownloadLink; project.Folder = req.Folder; project.OriginalWidth = req.OriginalWidth; project.OriginalHeight = req.OriginalHeight; project.Aspect = req.Aspect; project.ProjectDurationSec = req.ProjectDurationSec; project.MinDurationSec = req.MinDurationSec; project.MaxDurationSec = req.MaxDurationSec; project.FreeFps = req.FreeFps; project.ChooseMode = chooseMode; project.Resolution = resolution; project.VipFactor = req.VipFactor; project.RenderAepComp = req.RenderAepComp; project.SharedLayerImage = req.SharedLayerImage; project.SharedColorsSvg = req.SharedColorsSvg; project.SharedColorPresetsSvg = req.SharedColorPresetsSvg; project.IsPublished = req.IsPublished; project.Sort = req.Sort; project.UpdatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); return await GetProjectDetailAsync(project.Id); } public async Task DeleteProjectAsync(Guid id) { var project = await db.Projects.FindAsync(id) ?? throw new KeyNotFoundException($"Project {id} not found"); project.DeletedAt = DateTime.UtcNow; await db.SaveChangesAsync(); } public async Task IncrementContainerViewAsync(Guid id) { await db.ProjectContainers .Where(x => x.Id == id) .ExecuteUpdateAsync(s => s.SetProperty(x => x.ViewCount, x => x.ViewCount + 1)); } // ── Helpers ─────────────────────────────────────────────────────────────── private async Task SyncContainerCategoriesAsync(Guid containerId, List categoryIds) { var existing = await db.ContainerCategories.Where(x => x.ContainerId == containerId).ToListAsync(); db.ContainerCategories.RemoveRange(existing); for (int i = 0; i < categoryIds.Count; i++) db.ContainerCategories.Add(new ContainerCategory { ContainerId = containerId, CategoryId = categoryIds[i], Sort = i }); await db.SaveChangesAsync(); } private async Task SyncContainerTagsAsync(Guid containerId, List tagIds) { var existing = await db.ContainerTags.Where(x => x.ContainerId == containerId).ToListAsync(); db.ContainerTags.RemoveRange(existing); foreach (var tagId in tagIds) db.ContainerTags.Add(new ContainerTag { ContainerId = containerId, TagId = tagId }); await db.SaveChangesAsync(); } private static ContainerSummaryResponse MapContainerSummary(ProjectContainer c) => new( c.Id, c.Slug, c.Name, c.Description, c.Image, c.Demo, c.MiniDemo, c.IsPublished, c.IsPremium, c.IsMockup, c.PrimaryMode.ToString(), c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate, c.ContainerCategories.Select(cc => cc.Category.Slug).ToList(), c.ContainerTags.Select(ct => ct.Tag.Name).ToList() ); private static ContainerDetailResponse MapContainerDetail(ProjectContainer c) => new( c.Id, c.Slug, c.Name, c.Description, c.Keywords, c.NewsText, c.Image, c.Demo, c.FullDemo, c.MiniDemo, c.DemoScriptTag, c.IsPublished, c.IsPremium, c.IsMockup, c.PrimaryMode.ToString(), c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate, c.Projects.Select(MapProject).ToList(), c.ContainerCategories.Select(cc => MapCategoryFlat(cc.Category)).ToList(), c.ContainerTags.Select(ct => new TagResponse(ct.Tag.Id, ct.Tag.Name, ct.Tag.LatinName, ct.Tag.Slug, ct.Tag.AppliesToMode, ct.Tag.IsActive)).ToList() ); private static CategoryResponse MapCategoryFlat(Category c) => new( c.Id, c.ParentId, c.Name, c.Slug, c.Description, c.ImageUrl, c.Icon, c.IsActive, c.Sort, [] ); private static ProjectResponse MapProject(Project p) => new( p.Id, p.ContainerId, p.Name, p.Image, p.FullDemo, p.OriginalWidth, p.OriginalHeight, p.Aspect, p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec, p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(), p.IsPublished, p.Sort ); private static ProjectDetailResponse MapProjectDetail(Project p) => new( p.Id, p.ContainerId, p.Name, p.Description, p.Image, p.FullDemo, p.DemoScriptTag, p.DownloadLink, p.OriginalWidth, p.OriginalHeight, p.Aspect, p.ProjectDurationSec, p.MinDurationSec, p.MaxDurationSec, p.FreeFps, p.ChooseMode.ToString(), p.Resolution.ToString(), p.VipFactor, p.RenderAepComp, p.SharedLayerImage, p.IsPublished, p.Sort, p.Scenes.Select(MapScene).ToList(), p.SharedColors.Select(sc => new SharedColorResponse(sc.Id, sc.ElementKey, sc.Title, sc.Icon, sc.AttrValue.ToString(), sc.DefaultColor, sc.Sort)).ToList(), p.SharedLayers.Select(MapSharedLayer).ToList() ); private static SceneResponse MapScene(Scene s) => new( s.Id, s.ProjectId, s.Key, s.Title, s.LocalizedTitle, s.SceneType.ToString(), s.Image, s.Demo, s.SnapshotUrl, s.GenerateKf, s.DefaultDurationSec, s.MinDurationSec, s.MaxDurationSec, s.OverlapAtEndSec, s.CanHandleDuration, s.ManualColorSelection, s.Sort, s.IsActive ); private static SharedLayerResponse MapSharedLayer(SharedLayer sl) => new( sl.Id, sl.Key, sl.Title, sl.LocalizedTitle, sl.Hint, sl.Type.ToString(), sl.DefaultValue, sl.FontId, sl.FontFace, sl.FontSize, sl.IsFontChangeable, sl.IsFontSizeChangeable, sl.Justify.ToString(), sl.CanJustify, sl.PositionInContainer, sl.IsTextBox, sl.MaxSize, sl.VideoSupport, sl.MinDurationSec, sl.MaxDurationSec, sl.Width, sl.Height, sl.MappedList, sl.AiInputType.ToString(), sl.IsHidden, sl.IsFocused, sl.VirtualCount, sl.Sort ); }