Add lower-corner layouts, custom blur backgrounds, and classic blur fill.

Ship composition families, optional background images for lower-corner templates, and an optional blurred cover fill for classic letterbox.
This commit is contained in:
Atakan Doğan Özban
2026-08-07 00:51:37 +02:00
parent 03634d5100
commit 848607f9e0
25 changed files with 1081 additions and 122 deletions
+30 -1
View File
@@ -8,7 +8,7 @@ import { audioTagsToMetadata } from "@/lib/audio-tags";
import { resolveYouTubeTitle } from "@/lib/titles";
import { SONG_TITLE_MAX } from "@/lib/layout";
import type { ItemMetadata } from "@/lib/types";
import { DEFAULT_LAYOUT, type LayoutSettings } from "@/lib/layout";
import { DEFAULT_LAYOUT, isLowerCornerTemplate, type LayoutSettings } from "@/lib/layout";
import { DEFAULT_WATERMARK, type WatermarkSettings } from "@/lib/watermark";
import { CategorySelect } from "./CategorySelect";
import { LayoutStudio } from "./LayoutStudio";
@@ -65,6 +65,8 @@ export function UploadForm() {
const [jobWatermark, setJobWatermark] = useState<WatermarkSettings>({ ...DEFAULT_WATERMARK });
const [jobLayout, setJobLayout] = useState<LayoutSettings>({ ...DEFAULT_LAYOUT });
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
const [backgroundImagePath, setBackgroundImagePath] = useState<string | null>(null);
const [backgroundPreviewUrl, setBackgroundPreviewUrl] = useState<string | null>(null);
const [quota, setQuota] = useState<{
maxBatchSize: number;
used: number;
@@ -89,6 +91,7 @@ export function UploadForm() {
return () => {
if (imagePreviewUrl) URL.revokeObjectURL(imagePreviewUrl);
if (logoPreviewUrl) URL.revokeObjectURL(logoPreviewUrl);
if (backgroundPreviewUrl) URL.revokeObjectURL(backgroundPreviewUrl);
audioItems.forEach((a) => {
if (a.itemImagePreview) URL.revokeObjectURL(a.itemImagePreview);
});
@@ -258,6 +261,21 @@ export function UploadForm() {
return uploaded.path;
}
async function handleBackgroundUpload(file: File) {
const preview = URL.createObjectURL(file);
if (backgroundPreviewUrl) URL.revokeObjectURL(backgroundPreviewUrl);
setBackgroundPreviewUrl(preview);
const uploaded = await uploadFile(file, "image");
setBackgroundImagePath(uploaded.path);
return uploaded.path;
}
function clearBackgroundImage() {
if (backgroundPreviewUrl) URL.revokeObjectURL(backgroundPreviewUrl);
setBackgroundPreviewUrl(null);
setBackgroundImagePath(null);
}
function applyWatermarkToAll(next: WatermarkSettings) {
const normalized = { ...DEFAULT_WATERMARK, ...next };
setJobWatermark(normalized);
@@ -276,6 +294,9 @@ export function UploadForm() {
function applyLayoutToAll(next: LayoutSettings) {
const normalized = { ...DEFAULT_LAYOUT, ...next };
setJobLayout(normalized);
if (!isLowerCornerTemplate(normalized.template)) {
clearBackgroundImage();
}
setAudioItems((prev) =>
prev.map((item) => ({
...item,
@@ -371,6 +392,10 @@ export function UploadForm() {
includeWatermark: effectiveWatermark.mode !== "none",
watermark: effectiveWatermark,
layout: jobLayout,
backgroundImagePath:
isLowerCornerTemplate(jobLayout.template) && backgroundImagePath
? backgroundImagePath
: null,
},
})),
}),
@@ -620,6 +645,7 @@ export function UploadForm() {
previewImageUrl={
audioItems.find((a) => a.itemImagePreview)?.itemImagePreview || imagePreviewUrl
}
previewBackgroundUrl={backgroundPreviewUrl}
songTitle={
audioItems[0]?.metadata.songTitle ||
audioItems[0]?.metadata.title ||
@@ -633,6 +659,9 @@ export function UploadForm() {
onWatermarkChange={applyWatermarkToAll}
onUploadLogo={handleLogoUpload}
onUploadFont={handleFontUpload}
onUploadBackground={handleBackgroundUpload}
onClearBackground={clearBackgroundImage}
hasCustomBackground={Boolean(backgroundImagePath)}
logoPreviewUrl={logoPreviewUrl}
/>