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 [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
||||||
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
||||||
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
||||||
|
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
||||||
const [editNameInput, setEditNameInput] = useState('');
|
const [editNameInput, setEditNameInput] = useState('');
|
||||||
|
|
||||||
// ── Context Menu & Clipboard ──
|
// ── Context Menu & Clipboard ──
|
||||||
@@ -3248,24 +3249,26 @@ const App = () => {
|
|||||||
setIsMandatoryLogin(false);
|
setIsMandatoryLogin(false);
|
||||||
setAuthModalOpen(false);
|
setAuthModalOpen(false);
|
||||||
loadPendingSfsProject();
|
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 () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await window.SonicAPI.getPreferences();
|
const data = await window.SonicAPI.getPreferences();
|
||||||
if (data && data.preferences) {
|
if (data && data.preferences) loadPrefs(data.preferences);
|
||||||
const p = data.preferences;
|
else { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
|
||||||
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
} catch (e) { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); }
|
||||||
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 */ }
|
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4638,10 +4641,10 @@ const App = () => {
|
|||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const targetTrack = tracks.find(t => t.id === targetTrackId);
|
const targetTrack = tracks.find(t => t.id === targetTrackId);
|
||||||
const newClip = {
|
const newClip = {
|
||||||
id: 'clip_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
|
id: nextClipId(),
|
||||||
startTime: pasteTime,
|
startTime: pasteTime,
|
||||||
buffer: clipBuffer,
|
buffer: clipBuffer,
|
||||||
name: name || 'Pasted Clip'
|
name: (name || 'Pasted Clip').replace(/\.\w+$/, '') + ' (Pasted)'
|
||||||
};
|
};
|
||||||
if (targetTrack) {
|
if (targetTrack) {
|
||||||
setTracks(p => p.map(t => {
|
setTracks(p => p.map(t => {
|
||||||
@@ -5218,6 +5221,16 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updatedTime >= maxDurationRef.current) {
|
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();
|
stopAllPlayback();
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
return;
|
return;
|
||||||
@@ -5559,6 +5572,9 @@ const App = () => {
|
|||||||
hoveredTrackIdRef.current = hoveredTrackId;
|
hoveredTrackIdRef.current = hoveredTrackId;
|
||||||
const captureTrackSnapshotRef = useRef(null);
|
const captureTrackSnapshotRef = useRef(null);
|
||||||
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
||||||
|
let clipSeqCounter = 0;
|
||||||
|
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
||||||
|
|
||||||
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
|
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
|
||||||
const track = tracks.find(t => t.id === trackId);
|
const track = tracks.find(t => t.id === trackId);
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
@@ -5571,48 +5587,27 @@ const App = () => {
|
|||||||
const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
|
const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
|
||||||
if (!clip) return;
|
if (!clip) return;
|
||||||
const beforeSnap = captureTrackSnapshot(trackId);
|
const beforeSnap = captureTrackSnapshot(trackId);
|
||||||
let targetClipId = clip.id;
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
const cloneId = clip.id + '_copy_' + Date.now();
|
setDraggedClip({
|
||||||
const clone = {
|
trackId, clipId: clip.id, clickOffset,
|
||||||
...clip,
|
buffer: clip.buffer, name: clip.name, beforeSnap,
|
||||||
id: cloneId,
|
isDuplicate: true,
|
||||||
name: clip.name + ' (Copy)'
|
origTrackId: trackId, origClipId: clip.id
|
||||||
};
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!track.clips || track.clips.length === 0) {
|
||||||
setTracks(prev => prev.map(t => {
|
setTracks(prev => prev.map(t => {
|
||||||
if (t.id === trackId) {
|
if (t.id === trackId) {
|
||||||
const newClips = [...existingClips, clone];
|
return { ...t, clips: existingClips };
|
||||||
return {
|
|
||||||
...t,
|
|
||||||
clips: newClips,
|
|
||||||
buffer: newClips[0].buffer,
|
|
||||||
startTime: newClips[0].startTime,
|
|
||||||
name: newClips[0].name
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return t;
|
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({
|
setDraggedClip({
|
||||||
trackId: trackId,
|
trackId, clipId: clip.id, clickOffset,
|
||||||
clipId: targetClipId,
|
buffer: clip.buffer, name: clip.name, beforeSnap,
|
||||||
clickOffset: clickOffset,
|
isDuplicate: false
|
||||||
buffer: clip.buffer,
|
|
||||||
name: clip.name,
|
|
||||||
beforeSnap: beforeSnap
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const stretchedClipRef = useRef(null);
|
const stretchedClipRef = useRef(null);
|
||||||
@@ -5769,10 +5764,22 @@ const App = () => {
|
|||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
const drag = draggedClipRef.current;
|
const drag = draggedClipRef.current;
|
||||||
if (!drag) return;
|
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);
|
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);
|
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('mousemove', handleMouseMove);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
@@ -6074,6 +6081,13 @@ const App = () => {
|
|||||||
return next;
|
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) ──
|
// ── Load File on Track (with server upload) ──
|
||||||
const loadFileOnTrack = async (trackId, file) => {
|
const loadFileOnTrack = async (trackId, file) => {
|
||||||
@@ -7238,7 +7252,7 @@ const App = () => {
|
|||||||
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
|
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
|
||||||
const data = buffer.getChannelData(0);
|
const data = buffer.getChannelData(0);
|
||||||
for (let i = 0; i < data.length; i++) data[i] = 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 => {
|
setTracks(prev => prev.map(t => {
|
||||||
if (t.id !== trackId) return 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 }] : []);
|
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
|
panelPositions, rightSidebarWidth, mediaExplorerHeight, selectedProviderId
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser) return;
|
const prefs = prefsRef.current;
|
||||||
|
localStorage.setItem('sonic_preferences', JSON.stringify(prefs));
|
||||||
|
if (!currentUser || currentUser === 'cached') return;
|
||||||
const timer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
try { await window.SonicAPI.savePreferences(prefsRef.current); } catch (e) {}
|
try { await window.SonicAPI.savePreferences(prefs); } catch (e) {}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
|
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
|
||||||
@@ -8165,7 +8181,7 @@ const App = () => {
|
|||||||
}), /*#__PURE__*/React.createElement("button", {
|
}), /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => setIsLoopingSelection(prev => !prev),
|
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'}`,
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
@@ -8955,7 +8971,33 @@ const App = () => {
|
|||||||
setSelectionEnd(end);
|
setSelectionEnd(end);
|
||||||
showToast(`Selected: ${c.name || track.name}`, 'info');
|
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"
|
className: "flex items-center gap-1"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
|
|||||||
@@ -3074,6 +3074,7 @@ const App = () => {
|
|||||||
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
const [selectedClipId, setSelectedClipId] = useState(null); // { trackId, clipId }
|
||||||
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
const [stretchedClip, setStretchedClip] = useState(null); // { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap }
|
||||||
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited
|
||||||
|
const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId }
|
||||||
const [editNameInput, setEditNameInput] = useState('');
|
const [editNameInput, setEditNameInput] = useState('');
|
||||||
|
|
||||||
// ── Context Menu & Clipboard ──
|
// ── Context Menu & Clipboard ──
|
||||||
@@ -3250,24 +3251,31 @@ const App = () => {
|
|||||||
setIsMandatoryLogin(false);
|
setIsMandatoryLogin(false);
|
||||||
setAuthModalOpen(false);
|
setAuthModalOpen(false);
|
||||||
loadPendingSfsProject();
|
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 () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await window.SonicAPI.getPreferences();
|
const data = await window.SonicAPI.getPreferences();
|
||||||
if (data && data.preferences) {
|
if (data && data.preferences) loadPrefs(data.preferences);else {
|
||||||
const p = data.preferences;
|
const cached = localStorage.getItem('sonic_preferences');
|
||||||
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
if (cached) loadPrefs(JSON.parse(cached));
|
||||||
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 */}
|
} catch (e) {
|
||||||
|
const cached = localStorage.getItem('sonic_preferences');
|
||||||
|
if (cached) loadPrefs(JSON.parse(cached));
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4639,10 +4647,10 @@ const App = () => {
|
|||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const targetTrack = tracks.find(t => t.id === targetTrackId);
|
const targetTrack = tracks.find(t => t.id === targetTrackId);
|
||||||
const newClip = {
|
const newClip = {
|
||||||
id: 'clip_' + Date.now() + '_' + Math.floor(Math.random() * 1000),
|
id: nextClipId(),
|
||||||
startTime: pasteTime,
|
startTime: pasteTime,
|
||||||
buffer: clipBuffer,
|
buffer: clipBuffer,
|
||||||
name: name || 'Pasted Clip'
|
name: (name || 'Pasted Clip').replace(/\.\w+$/, '') + ' (Pasted)'
|
||||||
};
|
};
|
||||||
if (targetTrack) {
|
if (targetTrack) {
|
||||||
setTracks(p => p.map(t => {
|
setTracks(p => p.map(t => {
|
||||||
@@ -5219,6 +5227,16 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updatedTime >= maxDurationRef.current) {
|
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();
|
stopAllPlayback();
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
return;
|
return;
|
||||||
@@ -5560,6 +5578,8 @@ const App = () => {
|
|||||||
hoveredTrackIdRef.current = hoveredTrackId;
|
hoveredTrackIdRef.current = hoveredTrackId;
|
||||||
const captureTrackSnapshotRef = useRef(null);
|
const captureTrackSnapshotRef = useRef(null);
|
||||||
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
captureTrackSnapshotRef.current = captureTrackSnapshot;
|
||||||
|
let clipSeqCounter = 0;
|
||||||
|
const nextClipId = () => `clip_${Date.now()}_${++clipSeqCounter}`;
|
||||||
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
|
const handleClipDragStart = (trackId, clipId, clickOffset, isDuplicate = false) => {
|
||||||
const track = tracks.find(t => t.id === trackId);
|
const track = tracks.find(t => t.id === trackId);
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
@@ -5572,48 +5592,39 @@ const App = () => {
|
|||||||
const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
|
const clip = existingClips.find(c => c.id === clipId || clipId === 'default' && c.id === 'default_' + track.id);
|
||||||
if (!clip) return;
|
if (!clip) return;
|
||||||
const beforeSnap = captureTrackSnapshot(trackId);
|
const beforeSnap = captureTrackSnapshot(trackId);
|
||||||
let targetClipId = clip.id;
|
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
const cloneId = clip.id + '_copy_' + Date.now();
|
setDraggedClip({
|
||||||
const clone = {
|
trackId,
|
||||||
...clip,
|
clipId: clip.id,
|
||||||
id: cloneId,
|
clickOffset,
|
||||||
name: clip.name + ' (Copy)'
|
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 => {
|
setTracks(prev => prev.map(t => {
|
||||||
if (t.id === trackId) {
|
if (t.id === trackId) {
|
||||||
const newClips = [...existingClips, clone];
|
|
||||||
return {
|
return {
|
||||||
...t,
|
...t,
|
||||||
clips: newClips,
|
clips: existingClips
|
||||||
buffer: newClips[0].buffer,
|
|
||||||
startTime: newClips[0].startTime,
|
|
||||||
name: newClips[0].name
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return t;
|
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({
|
setDraggedClip({
|
||||||
trackId: trackId,
|
trackId,
|
||||||
clipId: targetClipId,
|
clipId: clip.id,
|
||||||
clickOffset: clickOffset,
|
clickOffset,
|
||||||
buffer: clip.buffer,
|
buffer: clip.buffer,
|
||||||
name: clip.name,
|
name: clip.name,
|
||||||
beforeSnap: beforeSnap
|
beforeSnap,
|
||||||
|
isDuplicate: false
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const stretchedClipRef = useRef(null);
|
const stretchedClipRef = useRef(null);
|
||||||
@@ -5770,10 +5781,40 @@ const App = () => {
|
|||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
const drag = draggedClipRef.current;
|
const drag = draggedClipRef.current;
|
||||||
if (!drag) return;
|
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);
|
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);
|
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('mousemove', handleMouseMove);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
@@ -6075,6 +6116,19 @@ const App = () => {
|
|||||||
return next;
|
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) ──
|
// ── Load File on Track (with server upload) ──
|
||||||
const loadFileOnTrack = async (trackId, file) => {
|
const loadFileOnTrack = async (trackId, file) => {
|
||||||
@@ -7296,7 +7350,7 @@ const App = () => {
|
|||||||
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
|
const buffer = ctx.createBuffer(1, Math.floor(sr * duration), sr);
|
||||||
const data = buffer.getChannelData(0);
|
const data = buffer.getChannelData(0);
|
||||||
for (let i = 0; i < data.length; i++) data[i] = 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 => {
|
setTracks(prev => prev.map(t => {
|
||||||
if (t.id !== trackId) return t;
|
if (t.id !== trackId) return t;
|
||||||
const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
|
const clips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{
|
||||||
@@ -7764,10 +7818,12 @@ const App = () => {
|
|||||||
selectedProviderId
|
selectedProviderId
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser) return;
|
const prefs = prefsRef.current;
|
||||||
|
localStorage.setItem('sonic_preferences', JSON.stringify(prefs));
|
||||||
|
if (!currentUser || currentUser === 'cached') return;
|
||||||
const timer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await window.SonicAPI.savePreferences(prefsRef.current);
|
await window.SonicAPI.savePreferences(prefs);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
@@ -8382,7 +8438,7 @@ const App = () => {
|
|||||||
}), /*#__PURE__*/React.createElement("button", {
|
}), /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => setIsLoopingSelection(prev => !prev),
|
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'}`,
|
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", {
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
className: "inline-flex items-center shrink-0"
|
className: "inline-flex items-center shrink-0"
|
||||||
}, /*#__PURE__*/React.createElement("i", {
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
@@ -9178,7 +9234,39 @@ const App = () => {
|
|||||||
setSelectionEnd(end);
|
setSelectionEnd(end);
|
||||||
showToast(`Selected: ${c.name || track.name}`, 'info');
|
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"
|
className: "flex items-center gap-1"
|
||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user