From 6ddaf0366c2a2e4d7618dc02c18fa42c6463c403 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 30 Dec 2025 15:03:05 -0800 Subject: [PATCH] support copy / paste audiomotionanalyzer gradients --- src/i18n/locales/en.json | 2 + .../visualizer-settings-form.tsx | 181 ++++++++++++++++-- 2 files changed, 162 insertions(+), 21 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index ee1a2ed43..f998e8588 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1155,6 +1155,8 @@ "position": "Position", "level": "Level", "remove": "Remove", + "pasteGradient": "Paste Gradient", + "pasteGradientPlaceholder": "Paste gradient JSON here...", "custom": "Custom", "builtIn": "Built-in", "colors": "Colors", diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx index 9109f3dca..d84e0fb06 100644 --- a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx @@ -801,6 +801,7 @@ const PresetSettings = () => { minRows={5} onChange={(e) => setPasteValue(e.currentTarget.value)} placeholder={t('visualizer.pasteConfigurationPlaceholder')} + spellCheck={false} value={pasteValue} /> @@ -1002,6 +1003,8 @@ const CustomGradientsManager = () => { const { updateProperty, visualizer } = useUpdateAudioMotionAnalyzer(); const [isAdding, setIsAdding] = useState(false); const [editingIndex, setEditingIndex] = useState(null); + const [isPasting, setIsPasting] = useState(false); + const [pasteValue, setPasteValue] = useState(''); const [newGradient, setNewGradient] = useState({ colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }], dir: 'v', @@ -1010,6 +1013,33 @@ const CustomGradientsManager = () => { const customGradients = visualizer.audiomotionanalyzer.customGradients || []; + const generateDefaultName = () => { + const existingNames = customGradients.map((g) => g.name); + const pattern = /^Custom Gradient (\d+)$/i; + const numbers = existingNames + .map((name) => { + const match = name.match(pattern); + return match ? parseInt(match[1], 10) : null; + }) + .filter((num): num is number => num !== null); + + if (numbers.length === 0) { + return 'Custom Gradient 1'; + } + + const maxNumber = Math.max(...numbers); + return `Custom Gradient ${maxNumber + 1}`; + }; + + const handleStartAdding = () => { + setNewGradient({ + colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }], + dir: 'v', + name: generateDefaultName(), + }); + setIsAdding(true); + }; + const handleAddGradient = () => { if (!newGradient.name.trim()) return; @@ -1154,6 +1184,76 @@ const CustomGradientsManager = () => { setNewGradient({ ...newGradient, colorStops: updatedColorStops }); }; + const handleCopyGradient = async (gradient: CustomGradient) => { + try { + const gradientJson = JSON.stringify(gradient, null, 2); + await navigator.clipboard.writeText(gradientJson); + toast.success({ + message: t('visualizer.configCopied'), + }); + } catch { + toast.error({ + message: t('visualizer.configCopyFailed'), + }); + } + }; + + const handlePasteGradient = () => { + if (!pasteValue.trim()) return; + + try { + const parsed = JSON.parse(pasteValue.trim()); + + // Validate that it's a valid gradient object + if ( + typeof parsed !== 'object' || + parsed === null || + Array.isArray(parsed) || + !parsed.colorStops || + !Array.isArray(parsed.colorStops) || + parsed.colorStops.length === 0 + ) { + throw new Error('Invalid gradient format'); + } + + // Generate a unique name if the pasted gradient has a name that already exists + let gradientName = parsed.name || generateDefaultName(); + const existingNames = customGradients.map((g) => g.name); + if (existingNames.includes(gradientName)) { + const pattern = /^(.+?)(\s+\((\d+)\))?$/; + const match = gradientName.match(pattern); + const baseName = match ? match[1] : gradientName; + let counter = 1; + while (existingNames.includes(`${baseName} (${counter})`)) { + counter++; + } + gradientName = `${baseName} (${counter})`; + } + + const pastedGradient: CustomGradient = { + colorStops: parsed.colorStops.map((stop: any) => ({ + color: stop.color || '#ff0000', + level: stop.level, + levelEnabled: stop.levelEnabled || false, + pos: stop.pos, + positionEnabled: stop.positionEnabled || false, + })), + dir: parsed.dir || 'v', + name: gradientName, + }; + + setNewGradient(pastedGradient); + setPasteValue(''); + setIsPasting(false); + setIsAdding(true); + setEditingIndex(null); + } catch { + toast.error({ + message: t('visualizer.configPasteFailed'), + }); + } + }; + return (
{ {customGradients.map((gradient, index) => ( - - {gradient.name} - - - + + {gradient.name} + + + + + + ))} )} - {!isAdding ? ( - + {!isAdding && !isPasting ? ( + + + + + ) : isPasting ? ( + +