feat(canvas): add persistent node favorites with toolbar star and glow
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
normalizeColorAdjustData,
|
||||
type ColorAdjustData,
|
||||
} from "@/lib/image-pipeline/adjustment-types";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { COLOR_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -53,10 +54,13 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
|
||||
const [presetSelection, setPresetSelection] = useState("custom");
|
||||
const normalizeData = useCallback(
|
||||
(value: unknown) =>
|
||||
normalizeColorAdjustData({
|
||||
...cloneAdjustmentData(DEFAULT_COLOR_ADJUST_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
preserveNodeFavorite(
|
||||
normalizeColorAdjustData({
|
||||
...cloneAdjustmentData(DEFAULT_COLOR_ADJUST_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
value,
|
||||
) as ColorAdjustData,
|
||||
[],
|
||||
);
|
||||
const { localData, applyLocalData, updateLocalData } = useNodeLocalData<ColorAdjustData>({
|
||||
@@ -67,7 +71,7 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps
|
||||
onSave: (next) =>
|
||||
queueNodeDataUpdate({
|
||||
nodeId: id as Id<"nodes">,
|
||||
data: next,
|
||||
data: preserveNodeFavorite(next, data),
|
||||
}),
|
||||
debugLabel: "color-adjust",
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type CropNodeData,
|
||||
type CropResizeMode,
|
||||
} from "@/lib/image-pipeline/crop-node-data";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
@@ -188,7 +189,11 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
|
||||
const { queueNodeDataUpdate } = useCanvasSync();
|
||||
const graph = useCanvasGraph();
|
||||
|
||||
const normalizeData = useCallback((value: unknown) => normalizeCropNodeData(value), []);
|
||||
const normalizeData = useCallback(
|
||||
(value: unknown) =>
|
||||
preserveNodeFavorite(normalizeCropNodeData(value), value) as CropNodeData,
|
||||
[],
|
||||
);
|
||||
const previewAreaRef = useRef<HTMLDivElement | null>(null);
|
||||
const interactionRef = useRef<CropInteractionState | null>(null);
|
||||
const { localData, updateLocalData } = useNodeLocalData<CropNodeData>({
|
||||
@@ -199,7 +204,7 @@ export default function CropNode({ id, data, selected, width }: NodeProps<CropNo
|
||||
onSave: (next) =>
|
||||
queueNodeDataUpdate({
|
||||
nodeId: id as Id<"nodes">,
|
||||
data: next,
|
||||
data: preserveNodeFavorite(next, data),
|
||||
}),
|
||||
debugLabel: "crop",
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
normalizeCurvesData,
|
||||
type CurvesData,
|
||||
} from "@/lib/image-pipeline/adjustment-types";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { CURVE_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -53,10 +54,13 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
|
||||
const [presetSelection, setPresetSelection] = useState("custom");
|
||||
const normalizeData = useCallback(
|
||||
(value: unknown) =>
|
||||
normalizeCurvesData({
|
||||
...cloneAdjustmentData(DEFAULT_CURVES_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
preserveNodeFavorite(
|
||||
normalizeCurvesData({
|
||||
...cloneAdjustmentData(DEFAULT_CURVES_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
value,
|
||||
) as CurvesData,
|
||||
[],
|
||||
);
|
||||
const { localData, applyLocalData, updateLocalData } = useNodeLocalData<CurvesData>({
|
||||
@@ -67,7 +71,7 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps<Curv
|
||||
onSave: (next) =>
|
||||
queueNodeDataUpdate({
|
||||
nodeId: id as Id<"nodes">,
|
||||
data: next,
|
||||
data: preserveNodeFavorite(next, data),
|
||||
}),
|
||||
debugLabel: "curves",
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
normalizeDetailAdjustData,
|
||||
type DetailAdjustData,
|
||||
} from "@/lib/image-pipeline/adjustment-types";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { DETAIL_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -53,10 +54,13 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
|
||||
const [presetSelection, setPresetSelection] = useState("custom");
|
||||
const normalizeData = useCallback(
|
||||
(value: unknown) =>
|
||||
normalizeDetailAdjustData({
|
||||
...cloneAdjustmentData(DEFAULT_DETAIL_ADJUST_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
preserveNodeFavorite(
|
||||
normalizeDetailAdjustData({
|
||||
...cloneAdjustmentData(DEFAULT_DETAIL_ADJUST_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
value,
|
||||
) as DetailAdjustData,
|
||||
[],
|
||||
);
|
||||
const { localData, applyLocalData, updateLocalData } = useNodeLocalData<DetailAdjustData>({
|
||||
@@ -67,7 +71,7 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp
|
||||
onSave: (next) =>
|
||||
queueNodeDataUpdate({
|
||||
nodeId: id as Id<"nodes">,
|
||||
data: next,
|
||||
data: preserveNodeFavorite(next, data),
|
||||
}),
|
||||
debugLabel: "detail-adjust",
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
createCompressedImagePreview,
|
||||
getImageDimensions,
|
||||
} from "@/components/canvas/canvas-media-utils";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
|
||||
const ALLOWED_IMAGE_TYPES = new Set([
|
||||
"image/png",
|
||||
@@ -302,13 +303,16 @@ export default function ImageNode({
|
||||
|
||||
await queueNodeDataUpdate({
|
||||
nodeId: id as Id<"nodes">,
|
||||
data: {
|
||||
storageId,
|
||||
...(previewUpload ?? {}),
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
||||
},
|
||||
data: preserveNodeFavorite(
|
||||
{
|
||||
storageId,
|
||||
...(previewUpload ?? {}),
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
||||
},
|
||||
data,
|
||||
),
|
||||
});
|
||||
|
||||
if (dimensions) {
|
||||
@@ -354,6 +358,7 @@ export default function ImageNode({
|
||||
}
|
||||
},
|
||||
[
|
||||
data,
|
||||
generateUploadUrl,
|
||||
id,
|
||||
isUploading,
|
||||
@@ -377,16 +382,19 @@ export default function ImageNode({
|
||||
try {
|
||||
await queueNodeDataUpdate({
|
||||
nodeId: id as Id<"nodes">,
|
||||
data: {
|
||||
storageId: item.storageId,
|
||||
previewStorageId: item.previewStorageId,
|
||||
filename: item.filename,
|
||||
mimeType: item.mimeType,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
previewWidth: item.previewWidth,
|
||||
previewHeight: item.previewHeight,
|
||||
},
|
||||
data: preserveNodeFavorite(
|
||||
{
|
||||
storageId: item.storageId,
|
||||
previewStorageId: item.previewStorageId,
|
||||
filename: item.filename,
|
||||
mimeType: item.mimeType,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
previewWidth: item.previewWidth,
|
||||
previewHeight: item.previewHeight,
|
||||
},
|
||||
data,
|
||||
),
|
||||
});
|
||||
setMediaLibraryPhase("syncing");
|
||||
|
||||
@@ -414,7 +422,7 @@ export default function ImageNode({
|
||||
);
|
||||
}
|
||||
},
|
||||
[id, isNodeLoading, queueNodeDataUpdate, queueNodeResize, t],
|
||||
[data, id, isNodeLoading, queueNodeDataUpdate, queueNodeResize, t],
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
normalizeLightAdjustData,
|
||||
type LightAdjustData,
|
||||
} from "@/lib/image-pipeline/adjustment-types";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import { LIGHT_PRESETS } from "@/lib/image-pipeline/presets";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -53,10 +54,13 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
|
||||
const [presetSelection, setPresetSelection] = useState("custom");
|
||||
const normalizeData = useCallback(
|
||||
(value: unknown) =>
|
||||
normalizeLightAdjustData({
|
||||
...cloneAdjustmentData(DEFAULT_LIGHT_ADJUST_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
preserveNodeFavorite(
|
||||
normalizeLightAdjustData({
|
||||
...cloneAdjustmentData(DEFAULT_LIGHT_ADJUST_DATA),
|
||||
...(value as Record<string, unknown>),
|
||||
}),
|
||||
value,
|
||||
) as LightAdjustData,
|
||||
[],
|
||||
);
|
||||
const { localData, applyLocalData, updateLocalData } = useNodeLocalData<LightAdjustData>({
|
||||
@@ -67,7 +71,7 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps
|
||||
onSave: (next) =>
|
||||
queueNodeDataUpdate({
|
||||
nodeId: id as Id<"nodes">,
|
||||
data: next,
|
||||
data: preserveNodeFavorite(next, data),
|
||||
}),
|
||||
debugLabel: "light-adjust",
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
isPipelineAbortError,
|
||||
renderFullWithWorkerFallback,
|
||||
} from "@/lib/image-pipeline/worker-client";
|
||||
import { preserveNodeFavorite } from "@/lib/canvas-node-favorite";
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
@@ -105,6 +106,7 @@ type PersistedRenderData = {
|
||||
lastUploadFilename?: string;
|
||||
lastUploadError?: string;
|
||||
lastUploadErrorHash?: string;
|
||||
isFavorite?: true;
|
||||
};
|
||||
|
||||
const DEFAULT_OUTPUT_RESOLUTION: RenderResolutionOption = "original";
|
||||
@@ -348,7 +350,7 @@ function sanitizeRenderData(data: RenderNodeData): PersistedRenderData {
|
||||
next.lastUploadErrorHash = data.lastUploadErrorHash;
|
||||
}
|
||||
|
||||
return next;
|
||||
return preserveNodeFavorite(next, data) as PersistedRenderData;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number | undefined): string {
|
||||
@@ -496,6 +498,7 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
);
|
||||
|
||||
const steps = renderPreviewInput.steps;
|
||||
const hasCropStep = useMemo(() => steps.some((step) => step.type === "crop"), [steps]);
|
||||
const previewDebounceMs = shouldFastPathPreviewPipeline(
|
||||
steps,
|
||||
graph.previewNodeDataOverrides,
|
||||
@@ -592,6 +595,15 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
});
|
||||
|
||||
const targetAspectRatio = useMemo(() => {
|
||||
if (
|
||||
hasCropStep &&
|
||||
typeof previewAspectRatio === "number" &&
|
||||
Number.isFinite(previewAspectRatio) &&
|
||||
previewAspectRatio > 0
|
||||
) {
|
||||
return previewAspectRatio;
|
||||
}
|
||||
|
||||
const sourceAspectRatio = resolveSourceAspectRatio(sourceNode);
|
||||
if (sourceAspectRatio && Number.isFinite(sourceAspectRatio) && sourceAspectRatio > 0) {
|
||||
return sourceAspectRatio;
|
||||
@@ -606,7 +618,7 @@ export default function RenderNode({ id, data, selected, width, height }: NodePr
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [previewAspectRatio, sourceNode]);
|
||||
}, [hasCropStep, previewAspectRatio, sourceNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSource || targetAspectRatio === null) {
|
||||
|
||||
Reference in New Issue
Block a user