diff --git a/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs b/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs index 8a5dd0d..20df3bf 100644 --- a/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs +++ b/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs @@ -211,25 +211,60 @@ public class SceneColorService(ContentDbContext db) { e.Key = req.Key; e.Title = req.Title; + e.LocalizedTitle = req.LocalizedTitle; e.Hint = req.Hint; e.Type = Enum.TryParse(req.Type, true, out var t) ? t : ContentElementType.Text; e.DefaultValue = req.DefaultValue; e.PositionInContainer = req.PositionInContainer; + // text + font e.IsTextBox = req.IsTextBox; e.MaxSize = req.MaxSize; e.FontSize = req.FontSize; + e.DefaultFontSize = req.DefaultFontSize; + e.FontFace = req.FontFace; + e.FontFaceName = req.FontFaceName; + e.DefaultFontFace = req.DefaultFontFace; e.IsFontChangeable = req.IsFontChangeable; e.IsFontSizeChangeable = req.IsFontSizeChangeable; + if (req.Justify != null && Enum.TryParse(req.Justify, true, out var j)) e.Justify = j; + e.CanJustify = req.CanJustify; + // direction / RTL + e.DirectionLayerKey = req.DirectionLayerKey; + e.DirectionLayerValue = req.DirectionLayerValue; + // media e.VideoSupport = req.VideoSupport; e.Width = req.Width; e.Height = req.Height; e.Thumbnail = req.Thumbnail; + e.MinDurationSec = req.MinDurationSec; + e.MaxDurationSec = req.MaxDurationSec; + // advanced + e.MappedList = req.MappedList; + e.CounterMode = req.CounterMode; + if (req.AiInputType != null && Enum.TryParse(req.AiInputType, true, out var ai)) e.AiInputType = ai; + e.IsHidden = req.IsHidden; + e.IsFocused = req.IsFocused; + e.OpacityControllerKey = req.OpacityControllerKey; + e.VirtualCount = req.VirtualCount; + // design-preset variants + e.Dp1Image = req.Dp1Image; e.Dp1Title = req.Dp1Title; + e.Dp2Image = req.Dp2Image; e.Dp2Title = req.Dp2Title; + e.Dp3Image = req.Dp3Image; e.Dp3Title = req.Dp3Title; + e.Dp4Image = req.Dp4Image; e.Dp4Title = req.Dp4Title; } private static ContentElementResponse ToElementResponse(SceneContentElement e) => new( - e.Id, e.SceneId, e.Key, e.Title, e.Hint, e.Type.ToString(), e.DefaultValue, - e.PositionInContainer, e.IsTextBox, e.MaxSize, e.FontSize, e.IsFontChangeable, - e.IsFontSizeChangeable, e.VideoSupport, e.Width, e.Height, e.Thumbnail); + e.Id, e.SceneId, e.Key, e.Title, e.LocalizedTitle, e.Hint, e.Type.ToString(), e.DefaultValue, + e.PositionInContainer, + e.IsTextBox, e.MaxSize, e.FontSize, e.DefaultFontSize, + e.FontFace, e.FontFaceName, e.DefaultFontFace, + e.IsFontChangeable, e.IsFontSizeChangeable, e.Justify.ToString(), e.CanJustify, + e.DirectionLayerKey, e.DirectionLayerValue, + e.VideoSupport, e.Width, e.Height, e.Thumbnail, e.MinDurationSec, e.MaxDurationSec, + e.MappedList, e.CounterMode, e.AiInputType.ToString(), + e.IsHidden, e.IsFocused, e.OpacityControllerKey, e.VirtualCount, + e.Dp1Image, e.Dp1Title, e.Dp2Image, e.Dp2Title, + e.Dp3Image, e.Dp3Title, e.Dp4Image, e.Dp4Title); // ── helpers ───────────────────────────────────────────────────────────── diff --git a/services/content/FlatRender.ContentSvc/Models/SceneColor.cs b/services/content/FlatRender.ContentSvc/Models/SceneColor.cs index 8d052af..1bebcff 100644 --- a/services/content/FlatRender.ContentSvc/Models/SceneColor.cs +++ b/services/content/FlatRender.ContentSvc/Models/SceneColor.cs @@ -52,17 +52,39 @@ public record SaveColorPresetRequest( // ── Scene content elements (the editable inputs inside a scene) ─────────────── public record ContentElementResponse( - Guid Id, Guid SceneId, string Key, string Title, string? Hint, + Guid Id, Guid SceneId, string Key, string Title, string? LocalizedTitle, string? Hint, string Type, string? DefaultValue, int PositionInContainer, - bool IsTextBox, int? MaxSize, int? FontSize, bool IsFontChangeable, - bool IsFontSizeChangeable, bool VideoSupport, int? Width, int? Height, - string? Thumbnail + // text + font + bool IsTextBox, int? MaxSize, int? FontSize, int? DefaultFontSize, + string? FontFace, string? FontFaceName, string? DefaultFontFace, + bool IsFontChangeable, bool IsFontSizeChangeable, string Justify, bool CanJustify, + // direction / RTL + string? DirectionLayerKey, int DirectionLayerValue, + // media + bool VideoSupport, int? Width, int? Height, string? Thumbnail, + decimal? MinDurationSec, decimal? MaxDurationSec, + // advanced + string? MappedList, string? CounterMode, string AiInputType, + bool IsHidden, bool IsFocused, string? OpacityControllerKey, int VirtualCount, + // design-preset (dp) variants + string? Dp1Image, string? Dp1Title, string? Dp2Image, string? Dp2Title, + string? Dp3Image, string? Dp3Title, string? Dp4Image, string? Dp4Title ); public record SaveContentElementRequest( - Guid SceneId, string Key, string Title, string? Hint, - string Type, string? DefaultValue, int PositionInContainer, - bool IsTextBox, int? MaxSize, int? FontSize, - bool IsFontChangeable, bool IsFontSizeChangeable, - bool VideoSupport, int? Width, int? Height, string? Thumbnail + Guid SceneId, string Key, string Title, string Type, + string? LocalizedTitle = null, string? Hint = null, string? DefaultValue = null, + int PositionInContainer = 0, + bool IsTextBox = false, int? MaxSize = null, int? FontSize = null, int? DefaultFontSize = null, + string? FontFace = null, string? FontFaceName = null, string? DefaultFontFace = null, + bool IsFontChangeable = false, bool IsFontSizeChangeable = false, + string? Justify = null, bool CanJustify = true, + string? DirectionLayerKey = null, int DirectionLayerValue = 0, + bool VideoSupport = false, int? Width = null, int? Height = null, string? Thumbnail = null, + decimal? MinDurationSec = null, decimal? MaxDurationSec = null, + string? MappedList = null, string? CounterMode = null, string? AiInputType = null, + bool IsHidden = false, bool IsFocused = false, string? OpacityControllerKey = null, + int VirtualCount = 1, + string? Dp1Image = null, string? Dp1Title = null, string? Dp2Image = null, string? Dp2Title = null, + string? Dp3Image = null, string? Dp3Title = null, string? Dp4Image = null, string? Dp4Title = null ); diff --git a/src/components/admin/SceneColorEditor.tsx b/src/components/admin/SceneColorEditor.tsx index bfc773d..bdf8b94 100644 --- a/src/components/admin/SceneColorEditor.tsx +++ b/src/components/admin/SceneColorEditor.tsx @@ -722,20 +722,39 @@ function PresetsTab({ // ── Scene inputs (content elements) ─────────────────────────────────────────── interface ContentElement { - id: string; - scene_id: string; - key: string; - title: string; - hint?: string | null; - type: string; - default_value?: string | null; - position_in_container: number; + id: string; scene_id: string; key: string; title: string; localized_title?: string | null; + hint?: string | null; type: string; default_value?: string | null; position_in_container: number; + is_text_box: boolean; max_size?: number | null; font_size?: number | null; default_font_size?: number | null; + font_face?: string | null; font_face_name?: string | null; default_font_face?: string | null; + is_font_changeable: boolean; is_font_size_changeable: boolean; justify: string; can_justify: boolean; + direction_layer_key?: string | null; direction_layer_value: number; + video_support: boolean; width?: number | null; height?: number | null; thumbnail?: string | null; + min_duration_sec?: number | null; max_duration_sec?: number | null; + mapped_list?: string | null; counter_mode?: string | null; ai_input_type: string; + is_hidden: boolean; is_focused: boolean; opacity_controller_key?: string | null; virtual_count: number; + dp1_image?: string | null; dp1_title?: string | null; dp2_image?: string | null; dp2_title?: string | null; + dp3_image?: string | null; dp3_title?: string | null; dp4_image?: string | null; dp4_title?: string | null; } const ELEMENT_TYPES = [ "Text", "TextArea", "Media", "Audio", "Voiceover", "CheckBox", "DropDown", "Fill", "Color", "Number", "Date", "Toggle", "Slider", "Counter", "Hidden", ]; +const JUSTIFY_KINDS = ["LEFT_JUSTIFY", "CENTER_JUSTIFY", "RIGHT_JUSTIFY", "FULL_JUSTIFY"]; +const AI_INPUT_TYPES = ["None", "TitleSuggest", "BodySuggest", "TranslateRtl", "TranslateLtr", "RemoveBG", "UpscaleImage", "TTS"]; + +type ElDraft = Record; +const emptyEl: ElDraft = { + key: "", title: "", localized_title: "", type: "Text", default_value: "", hint: "", position_in_container: 0, + is_text_box: false, max_size: null, font_size: null, default_font_size: null, + font_face: "", font_face_name: "", default_font_face: "", is_font_changeable: false, is_font_size_changeable: false, + justify: "CENTER_JUSTIFY", can_justify: true, + direction_layer_key: "", direction_layer_value: 0, + video_support: false, width: null, height: null, thumbnail: "", min_duration_sec: null, max_duration_sec: null, + mapped_list: "", counter_mode: "", ai_input_type: "None", is_hidden: false, is_focused: false, + opacity_controller_key: "", virtual_count: 1, + dp1_image: "", dp1_title: "", dp2_image: "", dp2_title: "", dp3_image: "", dp3_title: "", dp4_image: "", dp4_title: "", +}; export function SceneInputsEditor({ sceneId, @@ -746,17 +765,15 @@ export function SceneInputsEditor({ }) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); - const empty = { key: "", title: "", type: "Text", default_value: "", hint: "", position_in_container: 0 }; - const [draft, setDraft] = useState({ ...empty }); + const [draft, setDraft] = useState({ ...emptyEl }); const [editId, setEditId] = useState(null); const [busy, setBusy] = useState(false); + const [advanced, setAdvanced] = useState(false); const reload = useCallback(async () => { setLoading(true); try { - const r = await fetch(`/api/admin/resource/scene-elements?scene_id=${sceneId}`, { - cache: "no-store", - }).then((x) => x.json()); + const r = await fetch(`/api/admin/resource/scene-elements?scene_id=${sceneId}`, { cache: "no-store" }).then((x) => x.json()); setItems(Array.isArray(r) ? r : (r?.items ?? [])); } catch { setError("بارگذاری ورودی‌ها ناموفق بود"); @@ -766,30 +783,22 @@ export function SceneInputsEditor({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [sceneId]); - useEffect(() => { - reload(); - }, [reload]); + useEffect(() => { reload(); }, [reload]); + + const set = (patch: ElDraft) => setDraft((d) => ({ ...d, ...patch })); const submit = async () => { setBusy(true); setError(null); - const body = { scene_id: sceneId, is_text_box: draft.type === "TextArea", ...draft }; - const url = editId - ? `/api/admin/resource/scene-elements/${editId}` - : `/api/admin/resource/scene-elements`; + const url = editId ? `/api/admin/resource/scene-elements/${editId}` : `/api/admin/resource/scene-elements`; const res = await fetch(url, { method: editId ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), + body: JSON.stringify({ ...draft, scene_id: sceneId }), }); setBusy(false); - if (res.ok) { - setDraft({ ...empty }); - setEditId(null); - reload(); - } else { - setError("ذخیرهٔ ورودی ناموفق بود"); - } + if (res.ok) { setDraft({ ...emptyEl }); setEditId(null); reload(); } + else setError("ذخیرهٔ ورودی ناموفق بود"); }; const remove = async (el: ContentElement) => { @@ -798,105 +807,140 @@ export function SceneInputsEditor({ if (res.ok) reload(); }; + const startEdit = (el: ContentElement) => { + setEditId(el.id); + const next: ElDraft = { ...emptyEl }; + for (const k of Object.keys(emptyEl)) { + const v = (el as unknown as Record)[k]; + if (v !== undefined && v !== null) next[k] = v as string | number | boolean; + } + setDraft(next); + setAdvanced(true); + }; + + // ── field render helpers (typed, no `any`) ────────────────────────────────── + const T = (label: string, k: string, span?: number, ltr?: boolean) => ( +
+ + set({ [k]: e.target.value })} /> +
+ ); + const N = (label: string, k: string) => ( +
+ + set({ [k]: e.target.value === "" ? null : Number(e.target.value) })} /> +
+ ); + const Bx = (label: string, k: string) => ( + + ); + const Sel = (label: string, k: string, opts: string[]) => ( +
+ + +
+ ); + const Group = (title: string) => ( +

{title}

+ ); + return (
-

- ورودی‌های قابل ویرایش این صحنه (متن، تصویر، رنگ …) -

+

ورودی‌های قابل ویرایش این صحنه (متن، تصویر، رنگ …)

{loading ? (

در حال بارگذاری…

) : (
- {items.length === 0 && ( -

هنوز ورودی‌ای تعریف نشده است.

- )} + {items.length === 0 &&

هنوز ورودی‌ای تعریف نشده است.

} {items.map((el) => ( -
- - {el.type} - +
+ {el.type} - {el.title}{" "} - ({el.key}) + {el.title} ({el.key}) - {el.default_value && ( - - {el.default_value} - - )} + {el.default_value && {el.default_value}} + {el.is_font_changeable && فونت} + {el.is_hidden && مخفی} #{el.position_in_container} - - + +
))}
)} - {/* Add / edit input */} + {/* Add / edit input — full controller set */}
-
- - setDraft({ ...draft, key: e.target.value })} /> -
-
- - setDraft({ ...draft, title: e.target.value })} /> -
-
- - -
-
- - setDraft({ ...draft, default_value: e.target.value })} /> -
-
- - setDraft({ ...draft, position_in_container: Number(e.target.value) })} /> -
-
- - setDraft({ ...draft, hint: e.target.value })} /> -
+ {T("کلید (یکتا)", "key", undefined, true)} + {T("عنوان", "title")} + {Sel("نوع", "type", ELEMENT_TYPES)} + {T("مقدار پیش‌فرض", "default_value", 2)} + {N("ترتیب", "position_in_container")} + {T("عنوان محلی (FA/EN JSON)", "localized_title", 2)} + {T("راهنما", "hint")} + + {advanced && (<> + {Group("متن و فونت")} + {Bx("جعبهٔ متن (TextBox)", "is_text_box")} + {Bx("فونت قابل تغییر", "is_font_changeable")} + {Bx("اندازهٔ فونت قابل تغییر", "is_font_size_changeable")} + {N("حداکثر طول متن", "max_size")} + {N("اندازهٔ فونت", "font_size")} + {N("اندازهٔ فونت پیش‌فرض", "default_font_size")} + {T("فونت (face)", "font_face", undefined, true)} + {T("نام فونت", "font_face_name")} + {T("فونت پیش‌فرض", "default_font_face", undefined, true)} + {Sel("چینش متن", "justify", JUSTIFY_KINDS)} + {Bx("چینش قابل تغییر", "can_justify")} + + {Group("جهت (RTL/LTR)")} + {T("کلید لایهٔ جهت", "direction_layer_key", undefined, true)} + {N("مقدار جهت", "direction_layer_value")} + + {Group("رسانه")} + {Bx("پشتیبانی ویدیو", "video_support")} + {N("عرض", "width")} + {N("ارتفاع", "height")} + {N("حداقل مدت (ثانیه)", "min_duration_sec")} + {N("حداکثر مدت (ثانیه)", "max_duration_sec")} + {T("تصویر بندانگشتی", "thumbnail", 3)} + + {Group("پیشرفته")} + {Sel("نوع ورودی هوش مصنوعی", "ai_input_type", AI_INPUT_TYPES)} + {T("حالت شمارنده", "counter_mode")} + {N("تعداد مجازی (Virtual)", "virtual_count")} + {Bx("مخفی", "is_hidden")} + {Bx("فوکوس", "is_focused")} + {T("کلید کنترل شفافیت", "opacity_controller_key", undefined, true)} + {T("لیست نگاشت (JSON)", "mapped_list", 3)} + + {Group("حالت‌های طراحی (DP)")} + {T("DP1 تصویر", "dp1_image")} {T("DP1 عنوان", "dp1_title")} + {T("DP2 تصویر", "dp2_image")} {T("DP2 عنوان", "dp2_title")} + {T("DP3 تصویر", "dp3_image")} {T("DP3 عنوان", "dp3_title")} + {T("DP4 تصویر", "dp4_image")} {T("DP4 عنوان", "dp4_title")} + )}
-
+ +
{editId && ( - + )} +