diff --git a/components/canvas/canvas.tsx b/components/canvas/canvas.tsx index 7b27e73..97a6b45 100644 --- a/components/canvas/canvas.tsx +++ b/components/canvas/canvas.tsx @@ -336,7 +336,11 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { const pendingResizeAfterCreateRef = useRef( new Map(), ); + const pendingDataAfterCreateRef = useRef(new Map()); const resolvedRealIdByClientRequestRef = useRef(new Map>()); + const pendingCreatePromiseByClientRequestRef = useRef( + new Map>>(), + ); const pendingEdgeSplitByClientRequestRef = useRef( new Map(), ); @@ -580,6 +584,29 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { const isSyncOnline = isBrowserOnline === true && connectionState.isWebSocketConnected === true; + const trackPendingNodeCreate = useCallback( + ( + clientRequestId: string, + createPromise: Promise>, + ): Promise> => { + const trackedPromise = createPromise + .then((realId) => { + resolvedRealIdByClientRequestRef.current.set(clientRequestId, realId); + return realId; + }) + .finally(() => { + pendingCreatePromiseByClientRequestRef.current.delete(clientRequestId); + }); + + pendingCreatePromiseByClientRequestRef.current.set( + clientRequestId, + trackedPromise, + ); + return trackedPromise; + }, + [], + ); + useEffect(() => { const handleOnline = () => setIsBrowserOnline(true); const handleOffline = () => setIsBrowserOnline(false); @@ -763,6 +790,8 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { pendingMoveAfterCreateRef.current.delete(args.clientRequestId); pendingResizeAfterCreateRef.current.delete(args.clientRequestId); + pendingDataAfterCreateRef.current.delete(args.clientRequestId); + pendingCreatePromiseByClientRequestRef.current.delete(args.clientRequestId); pendingEdgeSplitByClientRequestRef.current.delete(args.clientRequestId); pendingConnectionCreatesRef.current.delete(args.clientRequestId); resolvedRealIdByClientRequestRef.current.delete(args.clientRequestId); @@ -848,24 +877,28 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { const payload = { ...args, clientRequestId }; if (isSyncOnline) { - return await createNode(payload); + return await trackPendingNodeCreate(clientRequestId, createNode(payload)); } const optimisticNodeId = addOptimisticNodeLocally(payload); await enqueueSyncMutationRef.current("createNode", payload); return optimisticNodeId; }, - [addOptimisticNodeLocally, createNode, isSyncOnline], + [addOptimisticNodeLocally, createNode, isSyncOnline, trackPendingNodeCreate], ); const runCreateNodeWithEdgeFromSourceOnlineOnly = useCallback( async (args: Parameters[0]) => { const clientRequestId = args.clientRequestId ?? crypto.randomUUID(); const payload = { ...args, clientRequestId }; + const sourceNodeId = payload.sourceNodeId as string; pendingConnectionCreatesRef.current.add(clientRequestId); - if (isSyncOnline) { - return await createNodeWithEdgeFromSource(payload); + if (isSyncOnline && !isOptimisticNodeId(sourceNodeId)) { + return await trackPendingNodeCreate( + clientRequestId, + createNodeWithEdgeFromSource(payload), + ); } const optimisticNodeId = addOptimisticNodeLocally(payload); @@ -876,6 +909,24 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { sourceHandle: payload.sourceHandle, targetHandle: payload.targetHandle, }); + + if (isSyncOnline) { + try { + const realId = await trackPendingNodeCreate(clientRequestId, createNodeWithEdgeFromSourceRaw({ + ...payload, + })); + await remapOptimisticNodeLocally(clientRequestId, realId); + return realId; + } catch (error) { + removeOptimisticCreateLocally({ + clientRequestId, + removeNode: true, + removeEdge: true, + }); + throw error; + } + } + await enqueueSyncMutationRef.current( "createNodeWithEdgeFromSource", payload, @@ -886,7 +937,11 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { addOptimisticEdgeLocally, addOptimisticNodeLocally, createNodeWithEdgeFromSource, + createNodeWithEdgeFromSourceRaw, isSyncOnline, + remapOptimisticNodeLocally, + removeOptimisticCreateLocally, + trackPendingNodeCreate, ], ); @@ -894,10 +949,14 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { async (args: Parameters[0]) => { const clientRequestId = args.clientRequestId ?? crypto.randomUUID(); const payload = { ...args, clientRequestId }; + const targetNodeId = payload.targetNodeId as string; pendingConnectionCreatesRef.current.add(clientRequestId); - if (isSyncOnline) { - return await createNodeWithEdgeToTarget(payload); + if (isSyncOnline && !isOptimisticNodeId(targetNodeId)) { + return await trackPendingNodeCreate( + clientRequestId, + createNodeWithEdgeToTarget(payload), + ); } const optimisticNodeId = addOptimisticNodeLocally(payload); @@ -908,6 +967,24 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { sourceHandle: payload.sourceHandle, targetHandle: payload.targetHandle, }); + + if (isSyncOnline) { + try { + const realId = await trackPendingNodeCreate(clientRequestId, createNodeWithEdgeToTargetRaw({ + ...payload, + })); + await remapOptimisticNodeLocally(clientRequestId, realId); + return realId; + } catch (error) { + removeOptimisticCreateLocally({ + clientRequestId, + removeNode: true, + removeEdge: true, + }); + throw error; + } + } + await enqueueSyncMutationRef.current("createNodeWithEdgeToTarget", payload); return optimisticNodeId; }, @@ -915,7 +992,11 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { addOptimisticEdgeLocally, addOptimisticNodeLocally, createNodeWithEdgeToTarget, + createNodeWithEdgeToTargetRaw, isSyncOnline, + remapOptimisticNodeLocally, + removeOptimisticCreateLocally, + trackPendingNodeCreate, ], ); @@ -1287,6 +1368,19 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { [enqueueSyncMutation], ); + const flushPendingDataForClientRequest = useCallback( + async (clientRequestId: string, realId: Id<"nodes">): Promise => { + if (!pendingDataAfterCreateRef.current.has(clientRequestId)) return; + const pendingData = pendingDataAfterCreateRef.current.get(clientRequestId); + pendingDataAfterCreateRef.current.delete(clientRequestId); + await enqueueSyncMutation("updateData", { + nodeId: realId, + data: pendingData, + }); + }, + [enqueueSyncMutation], + ); + const runResizeNodeMutation = useCallback( async (args: { nodeId: Id<"nodes">; width: number; height: number }) => { const rawNodeId = args.nodeId as string; @@ -1336,9 +1430,44 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { const runUpdateNodeDataMutation = useCallback( async (args: { nodeId: Id<"nodes">; data: unknown }) => { - await enqueueSyncMutation("updateData", args); + const rawNodeId = args.nodeId as string; + if (!isOptimisticNodeId(rawNodeId)) { + await enqueueSyncMutation("updateData", args); + return; + } + + if (!isSyncOnline) { + await enqueueSyncMutation("updateData", args); + return; + } + + const clientRequestId = clientRequestIdFromOptimisticNodeId(rawNodeId); + const resolvedRealId = clientRequestId + ? resolvedRealIdByClientRequestRef.current.get(clientRequestId) + : undefined; + + if (resolvedRealId) { + await enqueueSyncMutation("updateData", { + nodeId: resolvedRealId, + data: args.data, + }); + return; + } + + if (clientRequestId) { + pendingDataAfterCreateRef.current.set(clientRequestId, args.data); + } + + if (process.env.NODE_ENV !== "production") { + console.info("[Canvas sync debug] deferred updateData for optimistic node", { + nodeId: rawNodeId, + clientRequestId, + resolvedRealId: resolvedRealId ?? null, + hasData: args.data !== undefined, + }); + } }, - [enqueueSyncMutation], + [enqueueSyncMutation, isSyncOnline], ); const runBatchRemoveNodesMutation = useCallback( @@ -1590,6 +1719,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { ); pendingMoveAfterCreateRef.current.delete(clientRequestId); pendingResizeAfterCreateRef.current.delete(clientRequestId); + pendingDataAfterCreateRef.current.delete(clientRequestId); pendingEdgeSplitByClientRequestRef.current.delete(clientRequestId); pendingConnectionCreatesRef.current.delete(clientRequestId); resolvedRealIdByClientRequestRef.current.delete(clientRequestId); @@ -1642,6 +1772,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { }); } await flushPendingResizeForClientRequest(clientRequestId, realId); + await flushPendingDataForClientRequest(clientRequestId, realId); return; } @@ -1663,11 +1794,13 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { positionY: pendingMove.positionY, }); await flushPendingResizeForClientRequest(clientRequestId, realId); + await flushPendingDataForClientRequest(clientRequestId, realId); return; } resolvedRealIdByClientRequestRef.current.set(clientRequestId, realId); await flushPendingResizeForClientRequest(clientRequestId, realId); + await flushPendingDataForClientRequest(clientRequestId, realId); return; } @@ -1700,6 +1833,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { error: String(error), }); } + await flushPendingDataForClientRequest(clientRequestId, r); } else { pendingLocalPositionUntilConvexMatchesRef.current.set(r as string, { x: p.positionX, @@ -1710,11 +1844,13 @@ function CanvasInner({ canvasId }: CanvasInnerProps) { positionX: p.positionX, positionY: p.positionY, }); + await flushPendingDataForClientRequest(clientRequestId, r); } }, [ canvasId, runBatchRemoveNodesMutation, + flushPendingDataForClientRequest, flushPendingResizeForClientRequest, runMoveNodeMutation, runSplitEdgeAtExistingNodeMutation, diff --git a/components/canvas/nodes/color-adjust-node.tsx b/components/canvas/nodes/color-adjust-node.tsx index ca38ea5..9fcff79 100644 --- a/components/canvas/nodes/color-adjust-node.tsx +++ b/components/canvas/nodes/color-adjust-node.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; import { useMutation } from "convex/react"; +import { useTranslations } from "next-intl"; import { Palette } from "lucide-react"; import { api } from "@/convex/_generated/api"; @@ -41,6 +42,9 @@ type PresetDoc = { }; export default function ColorAdjustNode({ id, data, selected, width }: NodeProps) { + const tCommon = useTranslations("common"); + const tNodes = useTranslations("nodes"); + const tToasts = useTranslations("toasts"); const { queueNodeDataUpdate } = useCanvasSync(); const savePreset = useMutation(api.presets.save); const userPresets = (useAuthQuery(api.presets.list, { nodeType: "color-adjust" }) ?? []) as PresetDoc[]; @@ -88,48 +92,48 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps () => [ { id: "hue", - label: "Hue", + label: tNodes("adjustments.colorAdjust.sliders.hue"), min: -180, max: 180, value: DEFAULT_COLOR_ADJUST_DATA.hsl.hue, }, { id: "saturation", - label: "Saturation", + label: tNodes("adjustments.colorAdjust.sliders.saturation"), min: -100, max: 100, value: DEFAULT_COLOR_ADJUST_DATA.hsl.saturation, }, { id: "luminance", - label: "Luminance", + label: tNodes("adjustments.colorAdjust.sliders.luminance"), min: -100, max: 100, value: DEFAULT_COLOR_ADJUST_DATA.hsl.luminance, }, { id: "temperature", - label: "Temperature", + label: tNodes("adjustments.colorAdjust.sliders.temperature"), min: -100, max: 100, value: DEFAULT_COLOR_ADJUST_DATA.temperature, }, { id: "tint", - label: "Tint", + label: tNodes("adjustments.colorAdjust.sliders.tint"), min: -100, max: 100, value: DEFAULT_COLOR_ADJUST_DATA.tint, }, { id: "vibrance", - label: "Vibrance", + label: tNodes("adjustments.colorAdjust.sliders.vibrance"), min: -100, max: 100, value: DEFAULT_COLOR_ADJUST_DATA.vibrance, }, ], - [], + [tNodes], ); const sliderValues = useMemo( () => [ @@ -179,14 +183,14 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps }; const handleSavePreset = async () => { - const name = window.prompt("Preset-Name"); + const name = window.prompt(tNodes("adjustments.common.presetNamePrompt")); if (!name) return; await savePreset({ name, nodeType: "color-adjust", params: localData, }); - toast.success("Preset gespeichert"); + toast.success(tToasts("canvas.adjustmentPresetSaved")); }; return ( @@ -206,24 +210,24 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
- Farbe + {tNodes("adjustments.colorAdjust.title")}
- + - Custom + {tNodes("custom")} {builtinOptions.map(([name]) => ( - Built-in: {name} + {tNodes("adjustments.common.builtinPresetLabel", { name })} ))} {userPresets.map((preset) => ( - User: {preset.name} + {tNodes("adjustments.common.userPresetLabel", { name: preset.name })} ))} @@ -207,7 +211,7 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps - Save + {tCommon("save")}
@@ -226,6 +230,7 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps { const valueById = new Map(values.map((entry) => [entry.id, entry.value])); updateData((current) => ({ @@ -239,11 +244,6 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps { - if (actionId === "apply") { - queueSave(); - } - }} />
diff --git a/components/canvas/nodes/detail-adjust-node.tsx b/components/canvas/nodes/detail-adjust-node.tsx index 2d92c75..8659903 100644 --- a/components/canvas/nodes/detail-adjust-node.tsx +++ b/components/canvas/nodes/detail-adjust-node.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; import { useMutation } from "convex/react"; +import { useTranslations } from "next-intl"; import { Focus } from "lucide-react"; import { api } from "@/convex/_generated/api"; @@ -41,6 +42,9 @@ type PresetDoc = { }; export default function DetailAdjustNode({ id, data, selected, width }: NodeProps) { + const tCommon = useTranslations("common"); + const tNodes = useTranslations("nodes"); + const tToasts = useTranslations("toasts"); const { queueNodeDataUpdate } = useCanvasSync(); const savePreset = useMutation(api.presets.save); const userPresets = (useAuthQuery(api.presets.list, { nodeType: "detail-adjust" }) ?? []) as PresetDoc[]; @@ -88,14 +92,14 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp () => [ { id: "sharpen-amount", - label: "Sharpen", + label: tNodes("adjustments.detailAdjust.sliders.sharpen"), min: 0, max: 500, value: DEFAULT_DETAIL_ADJUST_DATA.sharpen.amount, }, { id: "sharpen-radius", - label: "Radius", + label: tNodes("adjustments.detailAdjust.sliders.radius"), min: 0.5, max: 5, step: 0.01, @@ -104,41 +108,41 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp }, { id: "sharpen-threshold", - label: "Threshold", + label: tNodes("adjustments.detailAdjust.sliders.threshold"), min: 0, max: 255, value: DEFAULT_DETAIL_ADJUST_DATA.sharpen.threshold, }, { id: "clarity", - label: "Clarity", + label: tNodes("adjustments.detailAdjust.sliders.clarity"), min: -100, max: 100, value: DEFAULT_DETAIL_ADJUST_DATA.clarity, }, { id: "denoise-luminance", - label: "Denoise Luma", + label: tNodes("adjustments.detailAdjust.sliders.denoiseLuma"), min: 0, max: 100, value: DEFAULT_DETAIL_ADJUST_DATA.denoise.luminance, }, { id: "denoise-color", - label: "Denoise Color", + label: tNodes("adjustments.detailAdjust.sliders.denoiseColor"), min: 0, max: 100, value: DEFAULT_DETAIL_ADJUST_DATA.denoise.color, }, { id: "grain-amount", - label: "Grain", + label: tNodes("adjustments.detailAdjust.sliders.grain"), min: 0, max: 100, value: DEFAULT_DETAIL_ADJUST_DATA.grain.amount, }, ], - [], + [tNodes], ); const sliderValues = useMemo( () => [ @@ -190,14 +194,14 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp }; const handleSavePreset = async () => { - const name = window.prompt("Preset-Name"); + const name = window.prompt(tNodes("adjustments.common.presetNamePrompt")); if (!name) return; await savePreset({ name, nodeType: "detail-adjust", params: localData, }); - toast.success("Preset gespeichert"); + toast.success(tToasts("canvas.adjustmentPresetSaved")); }; return ( @@ -217,24 +221,24 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
- Detail + {tNodes("adjustments.detailAdjust.title")}
- + - Custom + {tNodes("custom")} {builtinOptions.map(([name]) => ( - Built-in: {name} + {tNodes("adjustments.common.builtinPresetLabel", { name })} ))} {userPresets.map((preset) => ( - User: {preset.name} + {tNodes("adjustments.common.userPresetLabel", { name: preset.name })} ))} @@ -257,7 +261,7 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps void handleSavePreset(); }} > - Save + {tCommon("save")}
@@ -276,6 +280,7 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps fillClassName="bg-amber-500/35 dark:bg-amber-400/35" handleClassName="bg-amber-500 dark:bg-amber-400" trackClassName="bg-amber-500/10 dark:bg-amber-500/15" + actions={[{ id: "reset", label: tCommon("reset") }]} onChange={(values) => { const valueById = new Map(values.map((entry) => [entry.id, entry.value])); updateData((current) => ({ @@ -294,11 +299,6 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps preset: null, })); }} - onAction={async (actionId) => { - if (actionId === "apply") { - queueSave(); - } - }} />
diff --git a/convex/nodes.ts b/convex/nodes.ts index bee3f84..6b20166 100644 --- a/convex/nodes.ts +++ b/convex/nodes.ts @@ -46,6 +46,14 @@ type NodeCreateMutationName = | "nodes.createWithEdgeFromSource" | "nodes.createWithEdgeToTarget"; +const OPTIMISTIC_NODE_PREFIX = "optimistic_"; +const NODE_CREATE_MUTATIONS: NodeCreateMutationName[] = [ + "nodes.create", + "nodes.createWithEdgeSplit", + "nodes.createWithEdgeFromSource", + "nodes.createWithEdgeToTarget", +]; + const DISALLOWED_ADJUSTMENT_DATA_KEYS = [ "blob", "blobUrl", @@ -491,6 +499,42 @@ async function rememberIdempotentNodeCreateResult( }); } +function getClientRequestIdFromOptimisticNodeId(nodeId: string): string | null { + if (!nodeId.startsWith(OPTIMISTIC_NODE_PREFIX)) { + return null; + } + const clientRequestId = nodeId.slice(OPTIMISTIC_NODE_PREFIX.length); + return clientRequestId.length > 0 ? clientRequestId : null; +} + +async function resolveNodeReferenceForWrite( + ctx: MutationCtx, + args: { + userId: string; + canvasId: Id<"canvases">; + nodeId: string; + }, +): Promise> { + const clientRequestId = getClientRequestIdFromOptimisticNodeId(args.nodeId); + if (!clientRequestId) { + return args.nodeId as Id<"nodes">; + } + + for (const mutation of NODE_CREATE_MUTATIONS) { + const resolvedNodeId = await getIdempotentNodeCreateResult(ctx, { + userId: args.userId, + mutation, + clientRequestId, + canvasId: args.canvasId, + }); + if (resolvedNodeId) { + return resolvedNodeId; + } + } + + throw new Error(`Referenced node not found for optimistic id ${args.nodeId}`); +} + // ============================================================================ // Queries // ============================================================================ @@ -923,7 +967,7 @@ export const createWithEdgeFromSource = mutation({ parentId: v.optional(v.id("nodes")), zIndex: v.optional(v.number()), clientRequestId: v.optional(v.string()), - sourceNodeId: v.id("nodes"), + sourceNodeId: v.string(), sourceHandle: v.optional(v.string()), targetHandle: v.optional(v.string()), }, @@ -941,7 +985,12 @@ export const createWithEdgeFromSource = mutation({ return existingNodeId; } - const source = await ctx.db.get(args.sourceNodeId); + const sourceNodeId = await resolveNodeReferenceForWrite(ctx, { + userId: user.userId, + canvasId: args.canvasId, + nodeId: args.sourceNodeId, + }); + const source = await ctx.db.get(sourceNodeId); if (!source || source.canvasId !== args.canvasId) { throw new Error("Source node not found"); } @@ -973,7 +1022,7 @@ export const createWithEdgeFromSource = mutation({ await ctx.db.insert("edges", { canvasId: args.canvasId, - sourceNodeId: args.sourceNodeId, + sourceNodeId, targetNodeId: nodeId, sourceHandle: args.sourceHandle, targetHandle: args.targetHandle, @@ -1008,7 +1057,7 @@ export const createWithEdgeToTarget = mutation({ parentId: v.optional(v.id("nodes")), zIndex: v.optional(v.number()), clientRequestId: v.optional(v.string()), - targetNodeId: v.id("nodes"), + targetNodeId: v.string(), sourceHandle: v.optional(v.string()), targetHandle: v.optional(v.string()), }, @@ -1026,7 +1075,12 @@ export const createWithEdgeToTarget = mutation({ return existingNodeId; } - const target = await ctx.db.get(args.targetNodeId); + const targetNodeId = await resolveNodeReferenceForWrite(ctx, { + userId: user.userId, + canvasId: args.canvasId, + nodeId: args.targetNodeId, + }); + const target = await ctx.db.get(targetNodeId); if (!target || target.canvasId !== args.canvasId) { throw new Error("Target node not found"); } @@ -1034,7 +1088,7 @@ export const createWithEdgeToTarget = mutation({ await assertConnectionPolicyForTypes(ctx, { sourceType: args.type, targetType: target.type, - targetNodeId: args.targetNodeId, + targetNodeId, }); const normalizedData = normalizeNodeDataForWrite(args.type, args.data); @@ -1056,7 +1110,7 @@ export const createWithEdgeToTarget = mutation({ await ctx.db.insert("edges", { canvasId: args.canvasId, sourceNodeId: nodeId, - targetNodeId: args.targetNodeId, + targetNodeId, sourceHandle: args.sourceHandle, targetHandle: args.targetHandle, }); diff --git a/messages/de.json b/messages/de.json index 116b893..5034155 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1,6 +1,7 @@ { "common": { "save": "Speichern", + "reset": "Zurücksetzen", "cancel": "Abbrechen", "delete": "Löschen", "close": "Schließen", @@ -55,6 +56,58 @@ "compareProcessed": "Verarbeitet", "custom": "Benutzerdefiniert", "customize": "Anpassen", + "adjustments": { + "common": { + "presetPlaceholder": "Voreinstellung", + "presetNamePrompt": "Name der Voreinstellung", + "builtinPresetLabel": "Standard: {name}", + "userPresetLabel": "Benutzer: {name}" + }, + "curves": { + "title": "Kurven", + "sliders": { + "blackPoint": "Schwarzpunkt", + "whitePoint": "Weißpunkt", + "gamma": "Gamma" + } + }, + "colorAdjust": { + "title": "Farbe", + "sliders": { + "hue": "Farbton", + "saturation": "Sättigung", + "luminance": "Luminanz", + "temperature": "Temperatur", + "tint": "Tönung", + "vibrance": "Dynamik" + } + }, + "lightAdjust": { + "title": "Licht", + "sliders": { + "brightness": "Helligkeit", + "contrast": "Kontrast", + "exposure": "Belichtung", + "highlights": "Lichter", + "shadows": "Schatten", + "whites": "Weißtöne", + "blacks": "Schwärzen", + "vignette": "Vignette" + } + }, + "detailAdjust": { + "title": "Details", + "sliders": { + "sharpen": "Schärfen", + "radius": "Radius", + "threshold": "Schwellwert", + "clarity": "Klarheit", + "denoiseLuma": "Entrauschen Luma", + "denoiseColor": "Entrauschen Farbe", + "grain": "Körnung" + } + } + }, "prompts": { "prompt1": "Prompt 1", "prompt2": "Prompt 2", @@ -121,6 +174,7 @@ "toasts": { "canvas": { "imageUploaded": "Bild hochgeladen", + "adjustmentPresetSaved": "Voreinstellung gespeichert", "uploadFailed": "Upload fehlgeschlagen", "uploadFormatError": "Format „{format}“ wird nicht unterstützt. Erlaubt: PNG, JPG, WebP.", "uploadSizeError": "Maximale Dateigröße: {maxMb} MB.", diff --git a/messages/en.json b/messages/en.json index b79e06a..a1fdd79 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,6 +1,7 @@ { "common": { "save": "Save", + "reset": "Reset", "cancel": "Cancel", "delete": "Delete", "close": "Close", @@ -55,6 +56,58 @@ "compareProcessed": "Processed", "custom": "Custom", "customize": "Customize", + "adjustments": { + "common": { + "presetPlaceholder": "Preset", + "presetNamePrompt": "Preset name", + "builtinPresetLabel": "Built-in: {name}", + "userPresetLabel": "User: {name}" + }, + "curves": { + "title": "Curves", + "sliders": { + "blackPoint": "Black Point", + "whitePoint": "White Point", + "gamma": "Gamma" + } + }, + "colorAdjust": { + "title": "Color", + "sliders": { + "hue": "Hue", + "saturation": "Saturation", + "luminance": "Luminance", + "temperature": "Temperature", + "tint": "Tint", + "vibrance": "Vibrance" + } + }, + "lightAdjust": { + "title": "Light", + "sliders": { + "brightness": "Brightness", + "contrast": "Contrast", + "exposure": "Exposure", + "highlights": "Highlights", + "shadows": "Shadows", + "whites": "Whites", + "blacks": "Blacks", + "vignette": "Vignette" + } + }, + "detailAdjust": { + "title": "Detail", + "sliders": { + "sharpen": "Sharpen", + "radius": "Radius", + "threshold": "Threshold", + "clarity": "Clarity", + "denoiseLuma": "Denoise Luma", + "denoiseColor": "Denoise Color", + "grain": "Grain" + } + } + }, "prompts": { "prompt1": "Prompt 1", "prompt2": "Prompt 2", @@ -121,6 +174,7 @@ "toasts": { "canvas": { "imageUploaded": "Image uploaded", + "adjustmentPresetSaved": "Preset saved", "uploadFailed": "Upload failed", "uploadFormatError": "Format \"{format}\" is not supported. Allowed: PNG, JPG, WebP.", "uploadSizeError": "Maximum file size: {maxMb} MB.",