mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
support copy / paste audiomotionanalyzer gradients
This commit is contained in:
@@ -1155,6 +1155,8 @@
|
|||||||
"position": "Position",
|
"position": "Position",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
"pasteGradient": "Paste Gradient",
|
||||||
|
"pasteGradientPlaceholder": "Paste gradient JSON here...",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
"builtIn": "Built-in",
|
"builtIn": "Built-in",
|
||||||
"colors": "Colors",
|
"colors": "Colors",
|
||||||
|
|||||||
+160
-21
@@ -801,6 +801,7 @@ const PresetSettings = () => {
|
|||||||
minRows={5}
|
minRows={5}
|
||||||
onChange={(e) => setPasteValue(e.currentTarget.value)}
|
onChange={(e) => setPasteValue(e.currentTarget.value)}
|
||||||
placeholder={t('visualizer.pasteConfigurationPlaceholder')}
|
placeholder={t('visualizer.pasteConfigurationPlaceholder')}
|
||||||
|
spellCheck={false}
|
||||||
value={pasteValue}
|
value={pasteValue}
|
||||||
/>
|
/>
|
||||||
<Group>
|
<Group>
|
||||||
@@ -1002,6 +1003,8 @@ const CustomGradientsManager = () => {
|
|||||||
const { updateProperty, visualizer } = useUpdateAudioMotionAnalyzer();
|
const { updateProperty, visualizer } = useUpdateAudioMotionAnalyzer();
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [editingIndex, setEditingIndex] = useState<null | number>(null);
|
const [editingIndex, setEditingIndex] = useState<null | number>(null);
|
||||||
|
const [isPasting, setIsPasting] = useState(false);
|
||||||
|
const [pasteValue, setPasteValue] = useState('');
|
||||||
const [newGradient, setNewGradient] = useState<CustomGradient>({
|
const [newGradient, setNewGradient] = useState<CustomGradient>({
|
||||||
colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
|
colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
|
||||||
dir: 'v',
|
dir: 'v',
|
||||||
@@ -1010,6 +1013,33 @@ const CustomGradientsManager = () => {
|
|||||||
|
|
||||||
const customGradients = visualizer.audiomotionanalyzer.customGradients || [];
|
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 = () => {
|
const handleAddGradient = () => {
|
||||||
if (!newGradient.name.trim()) return;
|
if (!newGradient.name.trim()) return;
|
||||||
|
|
||||||
@@ -1154,6 +1184,76 @@ const CustomGradientsManager = () => {
|
|||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
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 (
|
return (
|
||||||
<Fieldset
|
<Fieldset
|
||||||
legend={
|
legend={
|
||||||
@@ -1176,32 +1276,71 @@ const CustomGradientsManager = () => {
|
|||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{customGradients.map((gradient, index) => (
|
{customGradients.map((gradient, index) => (
|
||||||
<Group grow key={index}>
|
<Group grow key={index}>
|
||||||
<Text size="sm" style={{ flex: 1 }}>
|
<Group grow>
|
||||||
{gradient.name}
|
<Text size="sm">{gradient.name}</Text>
|
||||||
</Text>
|
</Group>
|
||||||
<Button
|
<Group justify="flex-end">
|
||||||
onClick={() => handleEditGradient(index)}
|
<Button
|
||||||
size="xs"
|
onClick={() => handleCopyGradient(gradient)}
|
||||||
variant="default"
|
size="xs"
|
||||||
>
|
variant="subtle"
|
||||||
{t('common.edit', { postProcess: 'titleCase' })}
|
>
|
||||||
</Button>
|
{t('visualizer.copyConfiguration')}
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => handleDeleteGradient(index)}
|
<Button
|
||||||
size="xs"
|
onClick={() => handleEditGradient(index)}
|
||||||
variant="subtle"
|
size="xs"
|
||||||
>
|
variant="default"
|
||||||
{t('common.delete', { postProcess: 'titleCase' })}
|
>
|
||||||
</Button>
|
{t('common.edit', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleDeleteGradient(index)}
|
||||||
|
size="xs"
|
||||||
|
variant="state-error"
|
||||||
|
>
|
||||||
|
{t('common.delete', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isAdding ? (
|
{!isAdding && !isPasting ? (
|
||||||
<Button onClick={() => setIsAdding(true)} size="sm" variant="outline">
|
<Group>
|
||||||
{t('visualizer.addCustomGradient')}
|
<Button onClick={handleStartAdding} size="sm" variant="outline">
|
||||||
</Button>
|
{t('visualizer.addCustomGradient')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setIsPasting(true)} size="sm" variant="outline">
|
||||||
|
{t('visualizer.pasteGradient', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
) : isPasting ? (
|
||||||
|
<Stack>
|
||||||
|
<Textarea
|
||||||
|
autosize
|
||||||
|
label={t('visualizer.pasteGradient', { postProcess: 'titleCase' })}
|
||||||
|
maxRows={10}
|
||||||
|
minRows={5}
|
||||||
|
onChange={(e) => setPasteValue(e.currentTarget.value)}
|
||||||
|
placeholder={t('visualizer.pasteGradientPlaceholder')}
|
||||||
|
spellCheck={false}
|
||||||
|
value={pasteValue}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
<Button onClick={() => setIsPasting(false)} variant="subtle">
|
||||||
|
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!pasteValue.trim()}
|
||||||
|
onClick={handlePasteGradient}
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{t('common.add', { postProcess: 'titleCase' })}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|||||||
Reference in New Issue
Block a user