fix: sửa UX các tools của AI
This commit is contained in:
+100
-58
@@ -3074,6 +3074,7 @@ const App = () => {
|
||||
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
||||
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
||||
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
||||
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
||||
const [editNameInput, setEditNameInput] = useState('');
|
||||
|
||||
// ── Context Menu & Clipboard ──
|
||||
@@ -3248,24 +3249,26 @@ const App = () => {
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
loadPendingSfsProject();
|
||||
const loadPrefs = (p) => {
|
||||
if (!p) return;
|
||||
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
||||
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
|
||||
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
|
||||
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
|
||||
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
|
||||
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
|
||||
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
|
||||
if (p.panelPositions) setPanelPositions(p.panelPositions);
|
||||
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
|
||||
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
|
||||
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
|
||||
};
|
||||
(async () => {
|
||||
try {
|
||||
const data = await window.SonicAPI.getPreferences();
|
||||
if (data && data.preferences) {
|
||||
const p = data.preferences;
|
||||
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
||||
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
|
||||
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
|
||||
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
|
||||
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
|
||||
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
|
||||
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
|
||||
if (p.panelPositions) setPanelPositions(p.panelPositions);
|
||||
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
|
||||
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
|
||||
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
|
||||
}
|
||||
} catch (e) { /* preferences not available */ }
|
||||
if (data && data.preferences) loadPrefs(data.preferences);
|
||||
else { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
|
||||
} catch (e) { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
|
||||
})();
|
||||
}
|
||||
};
|
||||
@@ -4638,10 +4641,10 @@ const App = () => {
|
||||
const ctx = getAudioContext();
|
||||
const targetTrack = tracks.find(t => t.id === targetTrackId);
|
||||
const newClip = {
|
||||
id: 'clip_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
|
||||
id: nextClipId(),
|
||||
startTime: pasteTime,
|
||||
buffer: clipBuffer,
|
||||
name: name || 'Pasted Clip'
|
||||
name: (name || 'Pasted Clip').replace(/\.\w+$/, '') + ' (Pasted)'
|
||||
};
|
||||
if (targetTrack) {
|
||||
setTracks(p => p.map(t => {
|
||||
@@ -5218,6 +5221,16 @@ const App = () => {
|
||||
}
|
||||
}
|
||||
if (updatedTime >= maxDurationRef.current) {
|
||||
if (isLoopingSelection) {
|
||||
stopAllPlayback();
|
||||
startOffsetTimeRef.current = 0;
|
||||
startAudioTimeRef.current = context.currentTime;
|
||||
startTrackPlayback(0);
|
||||
setCurrentTime(0);
|
||||
setIsPlaying(true);
|
||||
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
|
||||
return;
|
||||
}
|
||||
stopAllPlayback();
|
||||
setCurrentTime(0);
|
||||
return;
|
||||
@@ -5559,6 +5572,9 @@ const App = () => {
|
||||
hoveredTrackIdRef.current = hoveredTrackId;
|
||||
const captureTrackSnapshotRef = useRef(null);
|
||||
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
||||
let clipSeqCounter = 0;
|
||||
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
||||
|
||||
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
|
||||
const track = tracks.find(t => t.id === trackId);
|
||||
if (!track) return;
|
||||
@@ -5571,48 +5587,27 @@ const App = () => {
|
||||
const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
|
||||
if (!clip) return;
|
||||
const beforeSnap = captureTrackSnapshot(trackId);
|
||||
let targetClipId = clip.id;
|
||||
if (isDuplicate) {
|
||||
const cloneId = clip.id + '_copy_' + Date.now();
|
||||
const clone = {
|
||||
...clip,
|
||||
id: cloneId,
|
||||
name: clip.name + ' (Copy)'
|
||||
};
|
||||
setDraggedClip({
|
||||
trackId, clipId: clip.id, clickOffset,
|
||||
buffer: clip.buffer, name: clip.name, beforeSnap,
|
||||
isDuplicate: true,
|
||||
origTrackId: trackId, origClipId: clip.id
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!track.clips || track.clips.length === 0) {
|
||||
setTracks(prev => prev.map(t => {
|
||||
if (t.id === trackId) {
|
||||
const newClips = [...existingClips, clone];
|
||||
return {
|
||||
...t,
|
||||
clips: newClips,
|
||||
buffer: newClips[0].buffer,
|
||||
startTime: newClips[0].startTime,
|
||||
name: newClips[0].name
|
||||
};
|
||||
return { ...t, clips: existingClips };
|
||||
}
|
||||
return t;
|
||||
}));
|
||||
targetClipId = cloneId;
|
||||
} else {
|
||||
if (!track.clips || track.clips.length === 0) {
|
||||
setTracks(prev => prev.map(t => {
|
||||
if (t.id === trackId) {
|
||||
return {
|
||||
...t,
|
||||
clips: existingClips
|
||||
};
|
||||
}
|
||||
return t;
|
||||
}));
|
||||
}
|
||||
}
|
||||
setDraggedClip({
|
||||
trackId: trackId,
|
||||
clipId: targetClipId,
|
||||
clickOffset: clickOffset,
|
||||
buffer: clip.buffer,
|
||||
name: clip.name,
|
||||
beforeSnap: beforeSnap
|
||||
trackId, clipId: clip.id, clickOffset,
|
||||
buffer: clip.buffer, name: clip.name, beforeSnap,
|
||||
isDuplicate: false
|
||||
});
|
||||
};
|
||||
const stretchedClipRef = useRef(null);
|
||||
@@ -5769,10 +5764,22 @@ const App = () => {
|
||||
const handleMouseUp = () => {
|
||||
const drag = draggedClipRef.current;
|
||||
if (!drag) return;
|
||||
if (drag.isDuplicate) {
|
||||
const cloneId = nextClipId();
|
||||
const clone = { id: cloneId, buffer: drag.buffer, startTime: 0, name: drag.name.replace(/\.\w+$/, '') + ' (Copy)' };
|
||||
setTracks(prev => prev.map(t => {
|
||||
if (t.id === drag.trackId) {
|
||||
const clips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []);
|
||||
return { ...t, clips: [...clips, clone], buffer: clips.length > 0 ? clips[0].buffer : drag.buffer };
|
||||
}
|
||||
return t;
|
||||
}));
|
||||
setDraggedClip({ ...drag, clipId: cloneId, isDuplicate: false });
|
||||
}
|
||||
const afterSnap = captureTrackSnapshotRef.current(drag.trackId);
|
||||
pushAction('MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
|
||||
pushAction(drag.isDuplicate ? 'DUPLICATE_CLIP' : 'MOVE_CLIP', drag.trackId, drag.beforeSnap, afterSnap);
|
||||
setDraggedClip(null);
|
||||
showToast('Đã di chuyển clip.', 'success');
|
||||
showToast(drag.isDuplicate ? 'Đã sao chép clip.' : 'Đã di chuyển clip.', 'success');
|
||||
};
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
@@ -6074,6 +6081,13 @@ const App = () => {
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const updateClipName = (trackId, clipId, newName) => {
|
||||
setTracks(prev => prev.map(t => {
|
||||
if (t.id !== trackId) return t;
|
||||
const clips = t.clips && t.clips.length > 0 ? t.clips : [];
|
||||
return { ...t, clips: clips.map(c => c.id === clipId ? { ...c, name: newName } : c) };
|
||||
}));
|
||||
};
|
||||
|
||||
// ── Load File on Track (with server upload) ──
|
||||
const loadFileOnTrack = async (trackId, file) => {
|
||||
@@ -7238,7 +7252,7 @@ const App = () => {
|
||||
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < data.length; i++) data[i] = 0;
|
||||
const clipId = 'clip_' + Date.now();
|
||||
const clipId = nextClipId();
|
||||
setTracks(prev => prev.map(t => {
|
||||
if (t.id !== trackId) return t;
|
||||
const clips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []);
|
||||
@@ -7547,9 +7561,11 @@ const App = () => {
|
||||
panelPositions, rightSidebarWidth, mediaExplorerHeight, selectedProviderId
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
const prefs = prefsRef.current;
|
||||
localStorage.setItem('sonic_preferences', JSON.stringify(prefs));
|
||||
if (!currentUser || currentUser === 'cached') return;
|
||||
const timer = setTimeout(async () => {
|
||||
try { await window.SonicAPI.savePreferences(prefsRef.current); } catch (e) {}
|
||||
try { await window.SonicAPI.savePreferences(prefs); } catch (e) {}
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
|
||||
@@ -8165,7 +8181,7 @@ const App = () => {
|
||||
}), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => setIsLoopingSelection(prev => !prev),
|
||||
className: `w-7 h-7 flex items-center justify-center rounded border transition ${isLoopingSelection ? 'bg-amber-600 text-black border-amber-500 hover:bg-amber-500' : 'bg-zinc-800 text-zinc-200 border-zinc-700 hover:bg-zinc-700'}`,
|
||||
title: "Bật/Tắt Lặp vùng chọn"
|
||||
title: isLoopingSelection ? (selLeft !== null && selRight !== null ? "Loop vùng chọn" : "Loop timeline") : "Bật loop"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "inline-flex items-center shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
@@ -8955,7 +8971,33 @@ const App = () => {
|
||||
setSelectionEnd(end);
|
||||
showToast(`Selected: ${c.name || track.name}`, 'info');
|
||||
}
|
||||
}, c.name || track.name))), /*#__PURE__*/React.createElement("div", {
|
||||
}, c.name || track.name), editingClipName && editingClipName.trackId === track.id && editingClipName.clipId === c.id ? /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
value: editNameInput,
|
||||
autoFocus: true,
|
||||
onChange: e => setEditNameInput(e.target.value),
|
||||
onBlur: () => {
|
||||
if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim());
|
||||
setEditingClipName(null);
|
||||
},
|
||||
onKeyDown: e => {
|
||||
if (e.key === 'Enter') { if (editNameInput.trim()) updateClipName(track.id, c.id, editNameInput.trim()); setEditingClipName(null); }
|
||||
if (e.key === 'Escape') setEditingClipName(null);
|
||||
},
|
||||
onClick: e => e.stopPropagation(),
|
||||
className: "w-20 text-[9px] font-mono bg-black text-cyan-300 border border-cyan-500 rounded px-0.5 py-0 outline-none"
|
||||
}) : /*#__PURE__*/React.createElement("button", {
|
||||
className: "text-[9px] text-zinc-600 hover:text-cyan-400 ml-0.5 shrink-0",
|
||||
title: "Sửa tên clip",
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
setEditingClipName({ trackId: track.id, clipId: c.id });
|
||||
setEditNameInput(c.name || track.name);
|
||||
}
|
||||
}, /*#__PURE__*/React.createElement("i", {
|
||||
"data-lucide": "pencil",
|
||||
className: "w-2.5 h-2.5"
|
||||
})))), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-1"
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
onClick: e => {
|
||||
|
||||
Reference in New Issue
Block a user