fix: Workshop share from saved presets + clean load
Share: - Share modal now shows user's saved presets to pick from - No longer grabs live canvas (which had serialization issues) - Auto-fills title from preset name - Shows module/wire count per preset Load: - Stops audio before loading (prevents ghost sounds) - Deep clones patch data (prevents reference issues) - Calls deserialize → rebuildGraph → emit in correct order - Switches to Sandbox after loading Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { workshop as workshopApi } from '../services/api.js';
|
import { workshop as workshopApi } from '../services/api.js';
|
||||||
import { useAuth } from '../services/AuthContext.jsx';
|
import { useAuth } from '../services/AuthContext.jsx';
|
||||||
import { state, deserialize } from '../engine/state.js';
|
import { state, deserialize, emit } from '../engine/state.js';
|
||||||
import { serialize } from '../engine/state.js';
|
import { stopAudio, rebuildGraph } from '../engine/audioEngine.js';
|
||||||
import { rebuildGraph } from '../engine/audioEngine.js';
|
import { getPresets } from '../engine/presets.js';
|
||||||
|
|
||||||
const TAGS = ['ambient', 'bass', 'drums', 'pad', 'lead', 'fx', 'chiptune', 'experimental'];
|
const TAGS = ['ambient', 'bass', 'drums', 'pad', 'lead', 'fx', 'chiptune', 'experimental'];
|
||||||
|
|
||||||
@@ -11,17 +11,27 @@ function ShareModal({ onClose, onShared }) {
|
|||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [selectedTags, setSelectedTags] = useState([]);
|
const [selectedTags, setSelectedTags] = useState([]);
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const presets = getPresets();
|
||||||
|
|
||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
if (!title.trim()) { setError('Titulo requerido'); return; }
|
if (!title.trim()) { setError('Titulo requerido'); return; }
|
||||||
if (state.modules.length === 0) { setError('No hay modulos en el canvas'); return; }
|
if (!selectedPreset) { setError('Selecciona un preset para compartir'); return; }
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const patchData = serialize();
|
// Use the preset data directly (already serialized correctly)
|
||||||
|
const patchData = {
|
||||||
|
modules: selectedPreset.modules || [],
|
||||||
|
connections: selectedPreset.connections || [],
|
||||||
|
camera: selectedPreset.camera || { camX: 0, camY: 0, zoom: 1 },
|
||||||
|
masterVolume: selectedPreset.masterVolume ?? -6,
|
||||||
|
};
|
||||||
|
|
||||||
await workshopApi.share({
|
await workshopApi.share({
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
description: description.trim(),
|
description: description.trim(),
|
||||||
@@ -38,10 +48,36 @@ function ShareModal({ onClose, onShared }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="auth-overlay" onClick={onClose}>
|
<div className="auth-overlay" onClick={onClose}>
|
||||||
<div className="auth-card" onClick={e => e.stopPropagation()} style={{ gap: 14 }}>
|
<div className="auth-card" onClick={e => e.stopPropagation()} style={{ gap: 14, maxHeight: '80vh', overflow: 'auto' }}>
|
||||||
<h2 style={{ fontSize: 18, fontWeight: 700, color: 'var(--text)', margin: 0 }}>Compartir Patch</h2>
|
<h2 style={{ fontSize: 18, fontWeight: 700, color: 'var(--text)', margin: 0 }}>Compartir Patch</h2>
|
||||||
|
|
||||||
<div className="auth-form" style={{ gap: 10 }}>
|
<div className="auth-form" style={{ gap: 10 }}>
|
||||||
|
<label className="auth-label">SELECCIONA UN PRESET</label>
|
||||||
|
{presets.length === 0 ? (
|
||||||
|
<p style={{ fontSize: 12, color: 'var(--text2)' }}>
|
||||||
|
No tienes presets guardados. Ve al Sandbox, crea algo y guardalo con "Save" primero.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 150, overflowY: 'auto' }}>
|
||||||
|
{presets.map((p, i) => (
|
||||||
|
<button key={i} type="button"
|
||||||
|
style={{
|
||||||
|
padding: '10px 12px', background: selectedPreset === p ? 'var(--surface2)' : 'var(--bg)',
|
||||||
|
border: `1px solid ${selectedPreset === p ? 'var(--accent)' : 'var(--border)'}`,
|
||||||
|
borderRadius: 6, cursor: 'pointer', textAlign: 'left',
|
||||||
|
color: 'var(--text)', fontSize: 13, fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
onClick={() => { setSelectedPreset(p); if (!title) setTitle(p.name || ''); }}
|
||||||
|
>
|
||||||
|
<strong>{p.name}</strong>
|
||||||
|
<span style={{ color: 'var(--text2)', fontSize: 11, marginLeft: 8 }}>
|
||||||
|
{p.modules?.length || 0} modules · {p.connections?.length || 0} wires
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<label className="auth-label">TITULO</label>
|
<label className="auth-label">TITULO</label>
|
||||||
<input className="auth-input" placeholder="Nombre de tu patch"
|
<input className="auth-input" placeholder="Nombre de tu patch"
|
||||||
value={title} onChange={e => setTitle(e.target.value)} />
|
value={title} onChange={e => setTitle(e.target.value)} />
|
||||||
@@ -65,7 +101,8 @@ function ShareModal({ onClose, onShared }) {
|
|||||||
|
|
||||||
{error && <div className="auth-error">{error}</div>}
|
{error && <div className="auth-error">{error}</div>}
|
||||||
|
|
||||||
<button className="auth-submit" onClick={handleShare} disabled={loading}>
|
<button className="auth-submit" onClick={handleShare}
|
||||||
|
disabled={loading || presets.length === 0}>
|
||||||
{loading ? 'Compartiendo...' : 'Compartir'}
|
{loading ? 'Compartiendo...' : 'Compartir'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,11 +169,18 @@ export default function Workshop({ onSwitchToSandbox, onSwitchToGame }) {
|
|||||||
useEffect(() => { loadPatches(); }, [loadPatches]);
|
useEffect(() => { loadPatches(); }, [loadPatches]);
|
||||||
|
|
||||||
const handleLoad = (patch) => {
|
const handleLoad = (patch) => {
|
||||||
if (patch.data) {
|
if (!patch.data) return;
|
||||||
deserialize(patch.data);
|
|
||||||
if (state.isRunning) rebuildGraph();
|
// Stop audio, clean state, then load
|
||||||
onSwitchToSandbox?.();
|
if (state.isRunning) stopAudio();
|
||||||
}
|
|
||||||
|
// Deep clone to avoid reference issues
|
||||||
|
const cleanData = JSON.parse(JSON.stringify(patch.data));
|
||||||
|
deserialize(cleanData);
|
||||||
|
rebuildGraph();
|
||||||
|
emit();
|
||||||
|
|
||||||
|
onSwitchToSandbox?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLike = async (patchId) => {
|
const handleLike = async (patchId) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user