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 { workshop as workshopApi } from '../services/api.js';
|
||||
import { useAuth } from '../services/AuthContext.jsx';
|
||||
import { state, deserialize } from '../engine/state.js';
|
||||
import { serialize } from '../engine/state.js';
|
||||
import { rebuildGraph } from '../engine/audioEngine.js';
|
||||
import { state, deserialize, emit } from '../engine/state.js';
|
||||
import { stopAudio, rebuildGraph } from '../engine/audioEngine.js';
|
||||
import { getPresets } from '../engine/presets.js';
|
||||
|
||||
const TAGS = ['ambient', 'bass', 'drums', 'pad', 'lead', 'fx', 'chiptune', 'experimental'];
|
||||
|
||||
@@ -11,17 +11,27 @@ function ShareModal({ onClose, onShared }) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
const [selectedPreset, setSelectedPreset] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const presets = getPresets();
|
||||
|
||||
const handleShare = async () => {
|
||||
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);
|
||||
setError('');
|
||||
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({
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
@@ -38,10 +48,36 @@ function ShareModal({ onClose, onShared }) {
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<input className="auth-input" placeholder="Nombre de tu patch"
|
||||
value={title} onChange={e => setTitle(e.target.value)} />
|
||||
@@ -65,7 +101,8 @@ function ShareModal({ onClose, onShared }) {
|
||||
|
||||
{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'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -132,11 +169,18 @@ export default function Workshop({ onSwitchToSandbox, onSwitchToGame }) {
|
||||
useEffect(() => { loadPatches(); }, [loadPatches]);
|
||||
|
||||
const handleLoad = (patch) => {
|
||||
if (patch.data) {
|
||||
deserialize(patch.data);
|
||||
if (state.isRunning) rebuildGraph();
|
||||
onSwitchToSandbox?.();
|
||||
}
|
||||
if (!patch.data) return;
|
||||
|
||||
// Stop audio, clean state, then load
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user