mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-19 01:44:00 +02:00
fix custom gradients in audiomotion visualizer
This commit is contained in:
+72
-134
@@ -1003,25 +1003,29 @@ const GeneralSettings = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type CustomGradient = {
|
type CustomGradient = {
|
||||||
colorStops: (string | { color: string; level?: number; pos?: number })[];
|
colorStops: StoredColorStop[];
|
||||||
dir?: string;
|
dir?: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StoredColorStop = {
|
||||||
|
color: string;
|
||||||
|
level?: number;
|
||||||
|
levelEnabled?: boolean;
|
||||||
|
pos?: number;
|
||||||
|
positionEnabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const CustomGradientsManager = () => {
|
const CustomGradientsManager = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
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 [newGradient, setNewGradient] = useState<CustomGradient>({
|
const [newGradient, setNewGradient] = useState<CustomGradient>({
|
||||||
colorStops: ['#ff0000'],
|
colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
|
||||||
dir: 'v',
|
dir: 'v',
|
||||||
name: '',
|
name: '',
|
||||||
});
|
});
|
||||||
// Track which checkboxes are enabled for each color stop
|
|
||||||
const [colorStopOptions, setColorStopOptions] = useState<
|
|
||||||
Array<{ enableLevel: boolean; enablePos: boolean }>
|
|
||||||
>([{ enableLevel: false, enablePos: false }]);
|
|
||||||
|
|
||||||
const customGradients = visualizer.audiomotionanalyzer.customGradients || [];
|
const customGradients = visualizer.audiomotionanalyzer.customGradients || [];
|
||||||
|
|
||||||
@@ -1030,8 +1034,19 @@ const CustomGradientsManager = () => {
|
|||||||
|
|
||||||
const updatedGradients = [...customGradients, newGradient];
|
const updatedGradients = [...customGradients, newGradient];
|
||||||
updateProperty('customGradients', updatedGradients);
|
updateProperty('customGradients', updatedGradients);
|
||||||
setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' });
|
setNewGradient({
|
||||||
setColorStopOptions([{ enableLevel: false, enablePos: false }]);
|
colorStops: [
|
||||||
|
{
|
||||||
|
color: '#ff0000',
|
||||||
|
level: 0,
|
||||||
|
levelEnabled: false,
|
||||||
|
pos: 0,
|
||||||
|
positionEnabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dir: 'v',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
setIsAdding(false);
|
setIsAdding(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1043,12 +1058,6 @@ const CustomGradientsManager = () => {
|
|||||||
const handleEditGradient = (index: number) => {
|
const handleEditGradient = (index: number) => {
|
||||||
const gradient = customGradients[index];
|
const gradient = customGradients[index];
|
||||||
setNewGradient(gradient);
|
setNewGradient(gradient);
|
||||||
// Initialize checkbox states based on existing color stops
|
|
||||||
const options = gradient.colorStops.map((stop) => ({
|
|
||||||
enableLevel: typeof stop !== 'string' && stop.level !== undefined,
|
|
||||||
enablePos: typeof stop !== 'string' && stop.pos !== undefined,
|
|
||||||
}));
|
|
||||||
setColorStopOptions(options);
|
|
||||||
setEditingIndex(index);
|
setEditingIndex(index);
|
||||||
setIsAdding(true);
|
setIsAdding(true);
|
||||||
};
|
};
|
||||||
@@ -1059,15 +1068,21 @@ const CustomGradientsManager = () => {
|
|||||||
const updatedGradients = [...customGradients];
|
const updatedGradients = [...customGradients];
|
||||||
updatedGradients[editingIndex] = newGradient;
|
updatedGradients[editingIndex] = newGradient;
|
||||||
updateProperty('customGradients', updatedGradients);
|
updateProperty('customGradients', updatedGradients);
|
||||||
setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' });
|
setNewGradient({
|
||||||
setColorStopOptions([{ enableLevel: false, enablePos: false }]);
|
colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
|
||||||
|
dir: 'v',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
setEditingIndex(null);
|
setEditingIndex(null);
|
||||||
setIsAdding(false);
|
setIsAdding(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' });
|
setNewGradient({
|
||||||
setColorStopOptions([{ enableLevel: false, enablePos: false }]);
|
colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
|
||||||
|
dir: 'v',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
setEditingIndex(null);
|
setEditingIndex(null);
|
||||||
setIsAdding(false);
|
setIsAdding(false);
|
||||||
};
|
};
|
||||||
@@ -1075,9 +1090,11 @@ const CustomGradientsManager = () => {
|
|||||||
const handleAddColorStop = () => {
|
const handleAddColorStop = () => {
|
||||||
setNewGradient({
|
setNewGradient({
|
||||||
...newGradient,
|
...newGradient,
|
||||||
colorStops: [...newGradient.colorStops, '#00ff00'],
|
colorStops: [
|
||||||
|
...newGradient.colorStops,
|
||||||
|
{ color: '#00ff00', levelEnabled: false, positionEnabled: false },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
setColorStopOptions([...colorStopOptions, { enableLevel: false, enablePos: false }]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveColorStop = (index: number) => {
|
const handleRemoveColorStop = (index: number) => {
|
||||||
@@ -1086,33 +1103,16 @@ const CustomGradientsManager = () => {
|
|||||||
...newGradient,
|
...newGradient,
|
||||||
colorStops: newGradient.colorStops.filter((_, i) => i !== index),
|
colorStops: newGradient.colorStops.filter((_, i) => i !== index),
|
||||||
});
|
});
|
||||||
setColorStopOptions(colorStopOptions.filter((_, i) => i !== index));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleColorStopChange = (index: number, color: string) => {
|
const handleColorStopChange = (index: number, color: string) => {
|
||||||
const updatedColorStops = [...newGradient.colorStops];
|
const updatedColorStops = [...newGradient.colorStops];
|
||||||
const currentStop = updatedColorStops[index];
|
const currentStop = updatedColorStops[index];
|
||||||
const options = colorStopOptions[index];
|
|
||||||
|
|
||||||
// If neither checkbox is enabled, store as string
|
updatedColorStops[index] = {
|
||||||
if (!options.enablePos && !options.enableLevel) {
|
...currentStop,
|
||||||
updatedColorStops[index] = color;
|
color,
|
||||||
} else {
|
};
|
||||||
// Otherwise, store as object with enabled properties
|
|
||||||
updatedColorStops[index] = {
|
|
||||||
color,
|
|
||||||
...(options.enablePos &&
|
|
||||||
typeof currentStop !== 'string' &&
|
|
||||||
currentStop.pos !== undefined
|
|
||||||
? { pos: currentStop.pos }
|
|
||||||
: {}),
|
|
||||||
...(options.enableLevel &&
|
|
||||||
typeof currentStop !== 'string' &&
|
|
||||||
currentStop.level !== undefined
|
|
||||||
? { level: currentStop.level }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
||||||
};
|
};
|
||||||
@@ -1121,18 +1121,10 @@ const CustomGradientsManager = () => {
|
|||||||
const updatedColorStops = [...newGradient.colorStops];
|
const updatedColorStops = [...newGradient.colorStops];
|
||||||
const currentStop = updatedColorStops[index];
|
const currentStop = updatedColorStops[index];
|
||||||
const posValue = typeof pos === 'number' ? pos : parseFloat(pos) || undefined;
|
const posValue = typeof pos === 'number' ? pos : parseFloat(pos) || undefined;
|
||||||
const options = colorStopOptions[index];
|
|
||||||
|
|
||||||
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
|
|
||||||
|
|
||||||
updatedColorStops[index] = {
|
updatedColorStops[index] = {
|
||||||
color,
|
...currentStop,
|
||||||
...(options.enablePos && posValue !== undefined ? { pos: posValue } : {}),
|
...(currentStop.positionEnabled && posValue !== undefined ? { pos: posValue } : {}),
|
||||||
...(options.enableLevel &&
|
|
||||||
typeof currentStop !== 'string' &&
|
|
||||||
currentStop.level !== undefined
|
|
||||||
? { level: currentStop.level }
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
||||||
@@ -1142,87 +1134,43 @@ const CustomGradientsManager = () => {
|
|||||||
const updatedColorStops = [...newGradient.colorStops];
|
const updatedColorStops = [...newGradient.colorStops];
|
||||||
const currentStop = updatedColorStops[index];
|
const currentStop = updatedColorStops[index];
|
||||||
const levelValue = typeof level === 'number' ? level : parseFloat(level) || undefined;
|
const levelValue = typeof level === 'number' ? level : parseFloat(level) || undefined;
|
||||||
const options = colorStopOptions[index];
|
|
||||||
|
|
||||||
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
|
|
||||||
|
|
||||||
updatedColorStops[index] = {
|
updatedColorStops[index] = {
|
||||||
color,
|
...currentStop,
|
||||||
...(options.enablePos &&
|
...(currentStop.levelEnabled && levelValue !== undefined ? { level: levelValue } : {}),
|
||||||
typeof currentStop !== 'string' &&
|
|
||||||
currentStop.pos !== undefined
|
|
||||||
? { pos: currentStop.pos }
|
|
||||||
: {}),
|
|
||||||
...(options.enableLevel && levelValue !== undefined ? { level: levelValue } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTogglePos = (index: number, enabled: boolean) => {
|
const handleTogglePos = (index: number, enabled: boolean) => {
|
||||||
const updatedOptions = [...colorStopOptions];
|
const updatedColorStops = [...newGradient.colorStops];
|
||||||
updatedOptions[index] = { ...updatedOptions[index], enablePos: enabled };
|
const currentStop = updatedColorStops[index];
|
||||||
setColorStopOptions(updatedOptions);
|
|
||||||
|
|
||||||
// If both are now disabled, convert to string
|
updatedColorStops[index] = {
|
||||||
if (!enabled && !updatedOptions[index].enableLevel) {
|
...currentStop,
|
||||||
const updatedColorStops = [...newGradient.colorStops];
|
positionEnabled: enabled,
|
||||||
const currentStop = updatedColorStops[index];
|
// Remove pos if disabling
|
||||||
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
|
...(enabled && currentStop.pos !== undefined ? { pos: currentStop.pos } : {}),
|
||||||
updatedColorStops[index] = color;
|
...(!enabled ? { pos: undefined } : {}),
|
||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
};
|
||||||
} else {
|
|
||||||
// Otherwise, ensure it's an object
|
|
||||||
const updatedColorStops = [...newGradient.colorStops];
|
|
||||||
const currentStop = updatedColorStops[index];
|
|
||||||
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
|
|
||||||
|
|
||||||
updatedColorStops[index] = {
|
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
||||||
color,
|
|
||||||
...(enabled && typeof currentStop !== 'string' && currentStop.pos !== undefined
|
|
||||||
? { pos: currentStop.pos }
|
|
||||||
: {}),
|
|
||||||
...(updatedOptions[index].enableLevel &&
|
|
||||||
typeof currentStop !== 'string' &&
|
|
||||||
currentStop.level !== undefined
|
|
||||||
? { level: currentStop.level }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleLevel = (index: number, enabled: boolean) => {
|
const handleToggleLevel = (index: number, enabled: boolean) => {
|
||||||
const updatedOptions = [...colorStopOptions];
|
const updatedColorStops = [...newGradient.colorStops];
|
||||||
updatedOptions[index] = { ...updatedOptions[index], enableLevel: enabled };
|
const currentStop = updatedColorStops[index];
|
||||||
setColorStopOptions(updatedOptions);
|
|
||||||
|
|
||||||
// If both are now disabled, convert to string
|
updatedColorStops[index] = {
|
||||||
if (!enabled && !updatedOptions[index].enablePos) {
|
...currentStop,
|
||||||
const updatedColorStops = [...newGradient.colorStops];
|
levelEnabled: enabled,
|
||||||
const currentStop = updatedColorStops[index];
|
// Remove level if disabling
|
||||||
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
|
...(enabled && currentStop.level !== undefined ? { level: currentStop.level } : {}),
|
||||||
updatedColorStops[index] = color;
|
...(!enabled ? { level: undefined } : {}),
|
||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
};
|
||||||
} else {
|
|
||||||
// Otherwise, ensure it's an object
|
|
||||||
const updatedColorStops = [...newGradient.colorStops];
|
|
||||||
const currentStop = updatedColorStops[index];
|
|
||||||
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
|
|
||||||
|
|
||||||
updatedColorStops[index] = {
|
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
||||||
color,
|
|
||||||
...(updatedOptions[index].enablePos &&
|
|
||||||
typeof currentStop !== 'string' &&
|
|
||||||
currentStop.pos !== undefined
|
|
||||||
? { pos: currentStop.pos }
|
|
||||||
: {}),
|
|
||||||
...(enabled && typeof currentStop !== 'string' && currentStop.level !== undefined
|
|
||||||
? { level: currentStop.level }
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1311,10 +1259,6 @@ const CustomGradientsManager = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
{newGradient.colorStops.map((stop, index) => {
|
{newGradient.colorStops.map((stop, index) => {
|
||||||
const options = colorStopOptions[index] || {
|
|
||||||
enableLevel: false,
|
|
||||||
enablePos: false,
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<Group grow key={index}>
|
<Group grow key={index}>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
@@ -1323,20 +1267,18 @@ const CustomGradientsManager = () => {
|
|||||||
handleColorStopChange(index, color)
|
handleColorStopChange(index, color)
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
value={typeof stop === 'string' ? stop : stop.color}
|
value={stop.color}
|
||||||
/>
|
/>
|
||||||
<VisualizerSlider
|
<VisualizerSlider
|
||||||
defaultValue={
|
defaultValue={stop.pos}
|
||||||
typeof stop === 'string' ? undefined : stop.pos
|
disabled={!stop.positionEnabled}
|
||||||
}
|
|
||||||
disabled={!options.enablePos}
|
|
||||||
label={
|
label={
|
||||||
<Group
|
<Group
|
||||||
gap="xs"
|
gap="xs"
|
||||||
style={{ alignItems: 'center' }}
|
style={{ alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={options.enablePos}
|
checked={stop.positionEnabled || false}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleTogglePos(
|
handleTogglePos(
|
||||||
index,
|
index,
|
||||||
@@ -1358,19 +1300,15 @@ const CustomGradientsManager = () => {
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
/>
|
/>
|
||||||
<VisualizerSlider
|
<VisualizerSlider
|
||||||
defaultValue={
|
defaultValue={stop.level}
|
||||||
typeof stop === 'string'
|
disabled={!stop.levelEnabled}
|
||||||
? undefined
|
|
||||||
: stop.level
|
|
||||||
}
|
|
||||||
disabled={!options.enableLevel}
|
|
||||||
label={
|
label={
|
||||||
<Group
|
<Group
|
||||||
gap="xs"
|
gap="xs"
|
||||||
style={{ alignItems: 'center' }}
|
style={{ alignItems: 'center' }}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={options.enableLevel}
|
checked={stop.levelEnabled || false}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleToggleLevel(
|
handleToggleLevel(
|
||||||
index,
|
index,
|
||||||
|
|||||||
@@ -117,6 +117,50 @@ const VisualizerInner = () => {
|
|||||||
};
|
};
|
||||||
}, [visualizer, gradientsRegistered, isCustomGradient]);
|
}, [visualizer, gradientsRegistered, isCustomGradient]);
|
||||||
|
|
||||||
|
const transformGradientForVisualizer = useCallback(
|
||||||
|
(gradient: {
|
||||||
|
colorStops: Array<{
|
||||||
|
color: string;
|
||||||
|
level?: number;
|
||||||
|
levelEnabled?: boolean;
|
||||||
|
pos?: number;
|
||||||
|
positionEnabled?: boolean;
|
||||||
|
}>;
|
||||||
|
dir?: string;
|
||||||
|
}): {
|
||||||
|
colorStops: (string | { color: string; level?: number; pos?: number })[];
|
||||||
|
dir?: string;
|
||||||
|
} => {
|
||||||
|
const transformedColorStops = gradient.colorStops.map((stop) => {
|
||||||
|
// If neither position nor level is enabled, return just the color string
|
||||||
|
if (!stop.positionEnabled && !stop.levelEnabled) {
|
||||||
|
return stop.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, return an object with only enabled properties
|
||||||
|
const transformedStop: { color: string; level?: number; pos?: number } = {
|
||||||
|
color: stop.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stop.positionEnabled && stop.pos !== undefined) {
|
||||||
|
transformedStop.pos = stop.pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stop.levelEnabled && stop.level !== undefined) {
|
||||||
|
transformedStop.level = stop.level;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedStop;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
colorStops: transformedColorStops,
|
||||||
|
...(gradient.dir ? { dir: gradient.dir } : {}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const registerCustomGradients = useCallback(
|
const registerCustomGradients = useCallback(
|
||||||
(audioMotionInstance: AudioMotionAnalyzer) => {
|
(audioMotionInstance: AudioMotionAnalyzer) => {
|
||||||
if (visualizer.type !== 'audiomotionanalyzer') {
|
if (visualizer.type !== 'audiomotionanalyzer') {
|
||||||
@@ -127,18 +171,8 @@ const VisualizerInner = () => {
|
|||||||
|
|
||||||
customGradients.forEach((gradient) => {
|
customGradients.forEach((gradient) => {
|
||||||
try {
|
try {
|
||||||
const gradientConfig: {
|
const gradientConfig = transformGradientForVisualizer(gradient);
|
||||||
colorStops: (string | { color: string; level?: number; pos?: number })[];
|
|
||||||
dir?: string;
|
|
||||||
} = {
|
|
||||||
colorStops: gradient.colorStops,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (gradient.dir) {
|
|
||||||
gradientConfig.dir = gradient.dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type assertion needed as TypeScript definitions may be incomplete
|
|
||||||
audioMotionInstance.registerGradient(gradient.name, gradientConfig as any);
|
audioMotionInstance.registerGradient(gradient.name, gradientConfig as any);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to register gradient "${gradient.name}":`, error);
|
console.error(`Failed to register gradient "${gradient.name}":`, error);
|
||||||
@@ -148,7 +182,7 @@ const VisualizerInner = () => {
|
|||||||
// Mark gradients as registered
|
// Mark gradients as registered
|
||||||
setGradientsRegistered(true);
|
setGradientsRegistered(true);
|
||||||
},
|
},
|
||||||
[visualizer],
|
[visualizer, transformGradientForVisualizer],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -243,13 +243,13 @@ const AudioMotionAnalyzerSettingsSchema = z.object({
|
|||||||
customGradients: z.array(
|
customGradients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
colorStops: z.array(
|
colorStops: z.array(
|
||||||
z.string().or(
|
z.object({
|
||||||
z.object({
|
color: z.string(),
|
||||||
color: z.string(),
|
level: z.number().min(0).max(1).optional(),
|
||||||
level: z.number().min(0).max(1).optional(),
|
levelEnabled: z.boolean().optional(),
|
||||||
pos: z.number().min(0).max(1).optional(),
|
pos: z.number().min(0).max(1).optional(),
|
||||||
}),
|
positionEnabled: z.boolean().optional(),
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
dir: z.string().optional(),
|
dir: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user