feat(admin): full legacy controller set in scene-inputs editor
Build backend images / build content-svc (push) Failing after 2m19s
Build backend images / build file-svc (push) Failing after 1m18s
Build backend images / build gateway (push) Failing after 2m38s
Build backend images / build identity-svc (push) Failing after 6m44s
Build backend images / build notification-svc (push) Failing after 1m0s
Build backend images / build render-svc (push) Failing after 58s
Build backend images / build studio-svc (push) Failing after 59s
Build backend images / build content-svc (push) Failing after 2m19s
Build backend images / build file-svc (push) Failing after 1m18s
Build backend images / build gateway (push) Failing after 2m38s
Build backend images / build identity-svc (push) Failing after 6m44s
Build backend images / build notification-svc (push) Failing after 1m0s
Build backend images / build render-svc (push) Failing after 58s
Build backend images / build studio-svc (push) Failing after 59s
The V2 scene-inputs editor only exposed ~15 of the content model's ~40 fields. Restore full parity with the legacy admin controller. content-svc: - SaveContentElementRequest + ContentElementResponse widened to the complete field set (text/font, direction/RTL, media, advanced, DP) - ApplyElement / ToElementResponse map every field 1:1 (Enum.TryParse for JustifyKind + AiInputType) frontend (SceneInputsEditor): - common fields up top; an "advanced" toggle reveals grouped sections: Text and Font, Direction (RTL/LTR), Media, Advanced, Design-Presets (DP) - editing an element loads the full field set; rows show font/hidden badges - nullable numbers sent as null, enums as named values (snake_case body) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ContentElementType>(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<JustifyKind>(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<AiInputType>(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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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<string, string | number | boolean | null>;
|
||||
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<ContentElement[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const empty = { key: "", title: "", type: "Text", default_value: "", hint: "", position_in_container: 0 };
|
||||
const [draft, setDraft] = useState<typeof empty>({ ...empty });
|
||||
const [draft, setDraft] = useState<ElDraft>({ ...emptyEl });
|
||||
const [editId, setEditId] = useState<string | null>(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<string, unknown>)[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) => (
|
||||
<div className={span === 2 ? "sm:col-span-2" : span === 3 ? "sm:col-span-3" : ""}>
|
||||
<label className={lbl}>{label}</label>
|
||||
<input className={inp} dir={ltr ? "ltr" : undefined} value={String(draft[k] ?? "")}
|
||||
onChange={(e) => set({ [k]: e.target.value })} />
|
||||
</div>
|
||||
);
|
||||
const N = (label: string, k: string) => (
|
||||
<div>
|
||||
<label className={lbl}>{label}</label>
|
||||
<input className={inp} type="number"
|
||||
value={draft[k] === null || draft[k] === undefined ? "" : String(draft[k])}
|
||||
onChange={(e) => set({ [k]: e.target.value === "" ? null : Number(e.target.value) })} />
|
||||
</div>
|
||||
);
|
||||
const Bx = (label: string, k: string) => (
|
||||
<label className="flex items-center gap-2 py-1 text-[11px] text-gray-300">
|
||||
<input type="checkbox" checked={Boolean(draft[k])} onChange={(e) => set({ [k]: e.target.checked })} />
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
const Sel = (label: string, k: string, opts: string[]) => (
|
||||
<div>
|
||||
<label className={lbl}>{label}</label>
|
||||
<select className={inp} value={String(draft[k] ?? "")} onChange={(e) => set({ [k]: e.target.value })}>
|
||||
{opts.map((o) => <option key={o} value={o}>{o}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
const Group = (title: string) => (
|
||||
<p className="sm:col-span-3 mt-1 border-t border-[#1e2235] pt-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">{title}</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-[11px] font-medium text-gray-400">
|
||||
ورودیهای قابل ویرایش این صحنه (متن، تصویر، رنگ …)
|
||||
</p>
|
||||
<p className="text-[11px] font-medium text-gray-400">ورودیهای قابل ویرایش این صحنه (متن، تصویر، رنگ …)</p>
|
||||
{loading ? (
|
||||
<p className="text-[11px] text-gray-600">در حال بارگذاری…</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{items.length === 0 && (
|
||||
<p className="text-[11px] text-gray-600">هنوز ورودیای تعریف نشده است.</p>
|
||||
)}
|
||||
{items.length === 0 && <p className="text-[11px] text-gray-600">هنوز ورودیای تعریف نشده است.</p>}
|
||||
{items.map((el) => (
|
||||
<div
|
||||
key={el.id}
|
||||
className="flex flex-wrap items-center gap-2 rounded border border-[#1e2235] bg-[#070811] px-2 py-1.5"
|
||||
>
|
||||
<span className="rounded bg-violet-500/15 px-1.5 py-0.5 text-[10px] text-violet-300">
|
||||
{el.type}
|
||||
</span>
|
||||
<div key={el.id} className="flex flex-wrap items-center gap-2 rounded border border-[#1e2235] bg-[#070811] px-2 py-1.5">
|
||||
<span className="rounded bg-violet-500/15 px-1.5 py-0.5 text-[10px] text-violet-300">{el.type}</span>
|
||||
<span className="flex-1 truncate text-[11px] text-gray-200">
|
||||
{el.title}{" "}
|
||||
<span className="text-gray-600" dir="ltr">({el.key})</span>
|
||||
{el.title} <span className="text-gray-600" dir="ltr">({el.key})</span>
|
||||
</span>
|
||||
{el.default_value && (
|
||||
<span className="max-w-[120px] truncate text-[10px] text-gray-500" dir="auto">
|
||||
{el.default_value}
|
||||
</span>
|
||||
)}
|
||||
{el.default_value && <span className="max-w-[120px] truncate text-[10px] text-gray-500" dir="auto">{el.default_value}</span>}
|
||||
{el.is_font_changeable && <span className="rounded bg-blue-500/15 px-1 text-[9px] text-blue-300">فونت</span>}
|
||||
{el.is_hidden && <span className="rounded bg-gray-500/15 px-1 text-[9px] text-gray-400">مخفی</span>}
|
||||
<span className="text-[10px] text-gray-600">#{el.position_in_container}</span>
|
||||
<button
|
||||
className={ghost}
|
||||
onClick={() => {
|
||||
setEditId(el.id);
|
||||
setDraft({
|
||||
key: el.key,
|
||||
title: el.title,
|
||||
type: el.type,
|
||||
default_value: el.default_value ?? "",
|
||||
hint: el.hint ?? "",
|
||||
position_in_container: el.position_in_container,
|
||||
});
|
||||
}}
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
<button className={del} onClick={() => remove(el)}>
|
||||
حذف
|
||||
</button>
|
||||
<button className={ghost} onClick={() => startEdit(el)}>ویرایش</button>
|
||||
<button className={del} onClick={() => remove(el)}>حذف</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add / edit input */}
|
||||
{/* Add / edit input — full controller set */}
|
||||
<div className="rounded border border-dashed border-[#262b40] p-2">
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
<div>
|
||||
<label className={lbl}>کلید (یکتا)</label>
|
||||
<input className={inp} dir="ltr" value={draft.key}
|
||||
onChange={(e) => setDraft({ ...draft, key: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>عنوان</label>
|
||||
<input className={inp} value={draft.title}
|
||||
onChange={(e) => setDraft({ ...draft, title: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>نوع</label>
|
||||
<select className={inp} value={draft.type}
|
||||
onChange={(e) => setDraft({ ...draft, type: e.target.value })}>
|
||||
{ELEMENT_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<label className={lbl}>مقدار پیشفرض</label>
|
||||
<input className={inp} value={draft.default_value}
|
||||
onChange={(e) => setDraft({ ...draft, default_value: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>ترتیب</label>
|
||||
<input className={inp} type="number" value={draft.position_in_container}
|
||||
onChange={(e) => setDraft({ ...draft, position_in_container: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div className="sm:col-span-3">
|
||||
<label className={lbl}>راهنما (اختیاری)</label>
|
||||
<input className={inp} value={draft.hint}
|
||||
onChange={(e) => setDraft({ ...draft, hint: e.target.value })} />
|
||||
</div>
|
||||
{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")}
|
||||
</>)}
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<button className={btn} onClick={submit} disabled={busy || !draft.key || !draft.title}>
|
||||
{busy ? "…" : editId ? "ذخیرهٔ تغییرات" : "+ افزودن ورودی"}
|
||||
</button>
|
||||
{editId && (
|
||||
<button className={ghost} onClick={() => { setEditId(null); setDraft({ ...empty }); }}>
|
||||
انصراف
|
||||
</button>
|
||||
<button className={ghost} onClick={() => { setEditId(null); setDraft({ ...emptyEl }); }}>انصراف</button>
|
||||
)}
|
||||
<button type="button" className="ms-auto text-[11px] text-indigo-300 hover:underline" onClick={() => setAdvanced((v) => !v)}>
|
||||
{advanced ? "پنهان کردن تنظیمات پیشرفته ▲" : "تنظیمات پیشرفته (فونت، جهت، رسانه، DP …) ▼"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user