feat: enhance mobile responsiveness with collapsible controls and automation/effects bars
Added comprehensive mobile support for Phase 15 (Polish & Optimization): **Mobile Layout Enhancements:** - Track controls now collapsible on mobile with two states: - Collapsed: minimal controls with expand chevron, R/M/S buttons, horizontal level meter - Expanded: full height fader, pan control, all buttons - Track collapse buttons added to mobile view (left chevron for track collapse, right chevron for control collapse) - Master controls collapse button hidden on desktop (lg:hidden) - Automation and effects bars now available on mobile layout - Both bars collapsible with eye/eye-off icons, horizontally scrollable when zoomed - Mobile vertical stacking: controls → waveform → automation → effects per track **Bug Fixes:** - Fixed track controls and waveform container height matching on desktop - Fixed Modal component prop: isOpen → open in all dialog components - Fixed TypeScript null check for audioBuffer.duration - Fixed keyboard shortcut category: 'help' → 'view' **Technical Improvements:** - Consistent height calculation using trackHeight variable - Proper responsive breakpoints with Tailwind (sm:640px, lg:1024px) - Progressive disclosure pattern for mobile controls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,16 +4,9 @@ import * as React from 'react';
|
||||
import { Music, Plus, Upload, Trash2, Settings, Download, FolderOpen } from 'lucide-react';
|
||||
import { PlaybackControls } from './PlaybackControls';
|
||||
import { MasterControls } from '@/components/controls/MasterControls';
|
||||
import { FrequencyAnalyzer } from '@/components/analysis/FrequencyAnalyzer';
|
||||
import { Spectrogram } from '@/components/analysis/Spectrogram';
|
||||
import { PhaseCorrelationMeter } from '@/components/analysis/PhaseCorrelationMeter';
|
||||
import { LUFSMeter } from '@/components/analysis/LUFSMeter';
|
||||
import { AudioStatistics } from '@/components/analysis/AudioStatistics';
|
||||
import { ThemeToggle } from '@/components/layout/ThemeToggle';
|
||||
import { CommandPalette } from '@/components/ui/CommandPalette';
|
||||
import { GlobalSettingsDialog } from '@/components/settings/GlobalSettingsDialog';
|
||||
import { ExportDialog, type ExportSettings } from '@/components/dialogs/ExportDialog';
|
||||
import { ProjectsDialog } from '@/components/dialogs/ProjectsDialog';
|
||||
import { BrowserCompatDialog } from '@/components/dialogs/BrowserCompatDialog';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import type { CommandAction } from '@/components/ui/CommandPalette';
|
||||
import { useMultiTrack } from '@/lib/hooks/useMultiTrack';
|
||||
@@ -21,7 +14,21 @@ import { useMultiTrackPlayer } from '@/lib/hooks/useMultiTrackPlayer';
|
||||
import { useEffectChain } from '@/lib/hooks/useEffectChain';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { TrackList } from '@/components/tracks/TrackList';
|
||||
import { ImportTrackDialog } from '@/components/tracks/ImportTrackDialog';
|
||||
import type { ExportSettings } from '@/components/dialogs/ExportDialog';
|
||||
|
||||
// Lazy load dialogs for better performance
|
||||
const GlobalSettingsDialog = React.lazy(() => import('@/components/settings/GlobalSettingsDialog').then(m => ({ default: m.GlobalSettingsDialog })));
|
||||
const ExportDialog = React.lazy(() => import('@/components/dialogs/ExportDialog').then(m => ({ default: m.ExportDialog })));
|
||||
const ProjectsDialog = React.lazy(() => import('@/components/dialogs/ProjectsDialog').then(m => ({ default: m.ProjectsDialog })));
|
||||
const ImportTrackDialog = React.lazy(() => import('@/components/tracks/ImportTrackDialog').then(m => ({ default: m.ImportTrackDialog })));
|
||||
const KeyboardShortcutsDialog = React.lazy(() => import('@/components/dialogs/KeyboardShortcutsDialog').then(m => ({ default: m.KeyboardShortcutsDialog })));
|
||||
|
||||
// Lazy load analysis components (shown conditionally based on analyzerView)
|
||||
const FrequencyAnalyzer = React.lazy(() => import('@/components/analysis/FrequencyAnalyzer').then(m => ({ default: m.FrequencyAnalyzer })));
|
||||
const Spectrogram = React.lazy(() => import('@/components/analysis/Spectrogram').then(m => ({ default: m.Spectrogram })));
|
||||
const PhaseCorrelationMeter = React.lazy(() => import('@/components/analysis/PhaseCorrelationMeter').then(m => ({ default: m.PhaseCorrelationMeter })));
|
||||
const LUFSMeter = React.lazy(() => import('@/components/analysis/LUFSMeter').then(m => ({ default: m.LUFSMeter })));
|
||||
const AudioStatistics = React.lazy(() => import('@/components/analysis/AudioStatistics').then(m => ({ default: m.AudioStatistics })));
|
||||
import { formatDuration } from '@/lib/audio/decoder';
|
||||
import { useHistory } from '@/lib/hooks/useHistory';
|
||||
import { useRecording } from '@/lib/hooks/useRecording';
|
||||
@@ -48,6 +55,7 @@ import {
|
||||
type ProjectMetadata,
|
||||
} from '@/lib/storage/projects';
|
||||
import { getAudioContext } from '@/lib/audio/context';
|
||||
import { checkBrowserCompatibility } from '@/lib/utils/browser-compat';
|
||||
|
||||
export function AudioEditor() {
|
||||
// Settings hook
|
||||
@@ -66,6 +74,7 @@ export function AudioEditor() {
|
||||
const [masterVolume, setMasterVolume] = React.useState(0.8);
|
||||
const [masterPan, setMasterPan] = React.useState(0);
|
||||
const [isMasterMuted, setIsMasterMuted] = React.useState(false);
|
||||
const [masterControlsCollapsed, setMasterControlsCollapsed] = React.useState(false);
|
||||
const [clipboard, setClipboard] = React.useState<AudioBuffer | null>(null);
|
||||
const [recordingTrackId, setRecordingTrackId] = React.useState<string | null>(null);
|
||||
const [punchInEnabled, setPunchInEnabled] = React.useState(false);
|
||||
@@ -87,6 +96,12 @@ export function AudioEditor() {
|
||||
const [projects, setProjects] = React.useState<ProjectMetadata[]>([]);
|
||||
const [currentProjectId, setCurrentProjectId] = React.useState<string | null>(null);
|
||||
const [currentProjectName, setCurrentProjectName] = React.useState('Untitled Project');
|
||||
const [browserCompatDialogOpen, setBrowserCompatDialogOpen] = React.useState(false);
|
||||
const [shortcutsDialogOpen, setShortcutsDialogOpen] = React.useState(false);
|
||||
const [browserCompatInfo, setBrowserCompatInfo] = React.useState<{
|
||||
missingFeatures: string[];
|
||||
warnings: string[];
|
||||
}>({ missingFeatures: [], warnings: [] });
|
||||
|
||||
const { addToast } = useToast();
|
||||
|
||||
@@ -633,6 +648,9 @@ export function AudioEditor() {
|
||||
);
|
||||
setClipboard(extracted);
|
||||
|
||||
// Ensure the track is selected so paste works
|
||||
setSelectedTrackId(track.id);
|
||||
|
||||
// Execute cut command
|
||||
const command = createMultiTrackCutCommand(
|
||||
track.id,
|
||||
@@ -664,6 +682,9 @@ export function AudioEditor() {
|
||||
);
|
||||
setClipboard(extracted);
|
||||
|
||||
// Ensure the track is selected so paste works
|
||||
setSelectedTrackId(track.id);
|
||||
|
||||
// Execute copy command (doesn't modify buffer, just for undo history)
|
||||
const command = createMultiTrackCopyCommand(
|
||||
track.id,
|
||||
@@ -684,7 +705,25 @@ export function AudioEditor() {
|
||||
}, [tracks, executeCommand, updateTrack, addToast]);
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
if (!clipboard || !selectedTrackId) return;
|
||||
if (!clipboard) {
|
||||
addToast({
|
||||
title: 'Nothing to Paste',
|
||||
description: 'Clipboard is empty. Copy or cut a selection first.',
|
||||
variant: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedTrackId) {
|
||||
addToast({
|
||||
title: 'No Track Selected',
|
||||
description: 'Select a track to paste into.',
|
||||
variant: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const track = tracks.find((t) => t.id === selectedTrackId);
|
||||
if (!track) return;
|
||||
@@ -1056,6 +1095,16 @@ export function AudioEditor() {
|
||||
}
|
||||
}, [loadTracks, addToast]);
|
||||
|
||||
// Check browser compatibility on mount
|
||||
React.useEffect(() => {
|
||||
const { isSupported, missingFeatures, warnings } = checkBrowserCompatibility();
|
||||
|
||||
if (!isSupported || warnings.length > 0) {
|
||||
setBrowserCompatInfo({ missingFeatures, warnings });
|
||||
setBrowserCompatDialogOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto-load last project on mount
|
||||
const [hasAutoLoaded, setHasAutoLoaded] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
@@ -1266,6 +1315,80 @@ export function AudioEditor() {
|
||||
category: 'playback',
|
||||
action: stop,
|
||||
},
|
||||
// Edit
|
||||
{
|
||||
id: 'undo',
|
||||
label: 'Undo',
|
||||
description: 'Undo last action',
|
||||
shortcut: 'Ctrl+Z',
|
||||
category: 'edit',
|
||||
action: undo,
|
||||
},
|
||||
{
|
||||
id: 'redo',
|
||||
label: 'Redo',
|
||||
description: 'Redo last undone action',
|
||||
shortcut: 'Ctrl+Shift+Z',
|
||||
category: 'edit',
|
||||
action: redo,
|
||||
},
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
description: 'Cut selection to clipboard',
|
||||
shortcut: 'Ctrl+X',
|
||||
category: 'edit',
|
||||
action: handleCut,
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
description: 'Copy selection to clipboard',
|
||||
shortcut: 'Ctrl+C',
|
||||
category: 'edit',
|
||||
action: handleCopy,
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
description: 'Paste clipboard content',
|
||||
shortcut: 'Ctrl+V',
|
||||
category: 'edit',
|
||||
action: handlePaste,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: 'Delete',
|
||||
description: 'Delete selection',
|
||||
shortcut: 'Delete',
|
||||
category: 'edit',
|
||||
action: handleDelete,
|
||||
},
|
||||
{
|
||||
id: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
description: 'Duplicate selection',
|
||||
shortcut: 'Ctrl+D',
|
||||
category: 'edit',
|
||||
action: handleDuplicate,
|
||||
},
|
||||
{
|
||||
id: 'select-all',
|
||||
label: 'Select All',
|
||||
description: 'Select all content on current track',
|
||||
shortcut: 'Ctrl+A',
|
||||
category: 'edit',
|
||||
action: () => {
|
||||
if (selectedTrackId) {
|
||||
const track = tracks.find(t => t.id === selectedTrackId);
|
||||
if (track?.audioBuffer) {
|
||||
updateTrack(selectedTrackId, {
|
||||
selection: { start: 0, end: track.audioBuffer.duration }
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
// Project
|
||||
{
|
||||
id: 'save-project',
|
||||
@@ -1287,6 +1410,7 @@ export function AudioEditor() {
|
||||
id: 'zoom-in',
|
||||
label: 'Zoom In',
|
||||
description: 'Zoom in on waveforms',
|
||||
shortcut: 'Ctrl++',
|
||||
category: 'view',
|
||||
action: handleZoomIn,
|
||||
},
|
||||
@@ -1294,6 +1418,7 @@ export function AudioEditor() {
|
||||
id: 'zoom-out',
|
||||
label: 'Zoom Out',
|
||||
description: 'Zoom out on waveforms',
|
||||
shortcut: 'Ctrl+-',
|
||||
category: 'view',
|
||||
action: handleZoomOut,
|
||||
},
|
||||
@@ -1301,9 +1426,59 @@ export function AudioEditor() {
|
||||
id: 'fit-to-view',
|
||||
label: 'Fit to View',
|
||||
description: 'Reset zoom to fit all tracks',
|
||||
shortcut: 'Ctrl+0',
|
||||
category: 'view',
|
||||
action: handleFitToView,
|
||||
},
|
||||
// Navigation
|
||||
{
|
||||
id: 'seek-start',
|
||||
label: 'Go to Start',
|
||||
description: 'Seek to beginning',
|
||||
shortcut: 'Home',
|
||||
category: 'playback',
|
||||
action: () => seek(0),
|
||||
},
|
||||
{
|
||||
id: 'seek-end',
|
||||
label: 'Go to End',
|
||||
description: 'Seek to end',
|
||||
shortcut: 'End',
|
||||
category: 'playback',
|
||||
action: () => seek(duration),
|
||||
},
|
||||
{
|
||||
id: 'seek-backward',
|
||||
label: 'Seek Backward',
|
||||
description: 'Seek backward 1 second',
|
||||
shortcut: '←',
|
||||
category: 'playback',
|
||||
action: () => seek(Math.max(0, currentTime - 1)),
|
||||
},
|
||||
{
|
||||
id: 'seek-forward',
|
||||
label: 'Seek Forward',
|
||||
description: 'Seek forward 1 second',
|
||||
shortcut: '→',
|
||||
category: 'playback',
|
||||
action: () => seek(Math.min(duration, currentTime + 1)),
|
||||
},
|
||||
{
|
||||
id: 'seek-backward-5s',
|
||||
label: 'Seek Backward 5s',
|
||||
description: 'Seek backward 5 seconds',
|
||||
shortcut: 'Ctrl+←',
|
||||
category: 'playback',
|
||||
action: () => seek(Math.max(0, currentTime - 5)),
|
||||
},
|
||||
{
|
||||
id: 'seek-forward-5s',
|
||||
label: 'Seek Forward 5s',
|
||||
description: 'Seek forward 5 seconds',
|
||||
shortcut: 'Ctrl+→',
|
||||
category: 'playback',
|
||||
action: () => seek(Math.min(duration, currentTime + 5)),
|
||||
},
|
||||
// Tracks
|
||||
{
|
||||
id: 'add-track',
|
||||
@@ -1326,9 +1501,18 @@ export function AudioEditor() {
|
||||
category: 'tracks',
|
||||
action: handleClearTracks,
|
||||
},
|
||||
// Help
|
||||
{
|
||||
id: 'keyboard-shortcuts',
|
||||
label: 'Keyboard Shortcuts',
|
||||
description: 'View all keyboard shortcuts',
|
||||
shortcut: '?',
|
||||
category: 'view',
|
||||
action: () => setShortcutsDialogOpen(true),
|
||||
},
|
||||
];
|
||||
return actions;
|
||||
}, [play, pause, stop, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack]);
|
||||
}, [play, pause, stop, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, handleOpenProjectsDialog, handleZoomIn, handleZoomOut, handleFitToView, handleImportTracks, handleClearTracks, addTrack, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, setShortcutsDialogOpen]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
React.useEffect(() => {
|
||||
@@ -1398,6 +1582,27 @@ export function AudioEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd++: Zoom In (also accepts Ctrl+=)
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === '=' || e.key === '+')) {
|
||||
e.preventDefault();
|
||||
handleZoomIn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+-: Zoom Out
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === '-') {
|
||||
e.preventDefault();
|
||||
handleZoomOut();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd+0: Fit to View
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === '0') {
|
||||
e.preventDefault();
|
||||
handleFitToView();
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete or Backspace: Delete selection
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
@@ -1457,7 +1662,16 @@ export function AudioEditor() {
|
||||
// Ctrl+A: Select All (select all content on current track)
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
||||
e.preventDefault();
|
||||
if (selectedTrackId) {
|
||||
if (!selectedTrackId) {
|
||||
// If no track selected, try to select the first track with audio
|
||||
const trackWithAudio = tracks.find(t => t.audioBuffer);
|
||||
if (trackWithAudio && trackWithAudio.audioBuffer) {
|
||||
setSelectedTrackId(trackWithAudio.id);
|
||||
updateTrack(trackWithAudio.id, {
|
||||
selection: { start: 0, end: trackWithAudio.audioBuffer.duration }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const track = tracks.find(t => t.id === selectedTrackId);
|
||||
if (track?.audioBuffer) {
|
||||
updateTrack(selectedTrackId, {
|
||||
@@ -1468,45 +1682,31 @@ export function AudioEditor() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Plus/Equals: Zoom in
|
||||
if ((e.ctrlKey || e.metaKey) && (e.key === '+' || e.key === '=')) {
|
||||
// ?: Open keyboard shortcuts help
|
||||
if (e.key === '?' && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
handleZoomIn();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+Minus: Zoom out
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === '-') {
|
||||
e.preventDefault();
|
||||
handleZoomOut();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+0: Fit to view
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === '0') {
|
||||
e.preventDefault();
|
||||
handleFitToView();
|
||||
setShortcutsDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, handleZoomIn, handleZoomOut, handleFitToView]);
|
||||
}, [togglePlayPause, canUndo, canRedo, undo, redo, handleCut, handleCopy, handlePaste, handleDelete, handleDuplicate, handleSaveProject, seek, duration, currentTime, selectedTrackId, tracks, updateTrack, handleZoomIn, handleZoomOut, handleFitToView, setSelectedTrackId, setShortcutsDialogOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Compact Header */}
|
||||
<header className="flex items-center justify-between px-4 py-2 border-b border-border bg-card flex-shrink-0 gap-4">
|
||||
<header className="flex items-center justify-between px-2 sm:px-4 py-2 border-b border-border bg-card flex-shrink-0 gap-2 sm:gap-4">
|
||||
{/* Left: Logo */}
|
||||
<div className="flex items-center gap-4 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Music className="h-5 w-5 text-primary" />
|
||||
<h1 className="text-lg font-bold text-foreground">Audio UI</h1>
|
||||
<h1 className="text-base sm:text-lg font-bold text-foreground hidden sm:block">Audio UI</h1>
|
||||
</div>
|
||||
|
||||
{/* Project Name */}
|
||||
<div className="flex items-center gap-2 border-l border-border pl-4">
|
||||
<div className="hidden md:flex items-center gap-2 border-l border-border pl-4">
|
||||
<input
|
||||
type="text"
|
||||
value={currentProjectName}
|
||||
@@ -1518,27 +1718,31 @@ export function AudioEditor() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Track Actions */}
|
||||
<div className="flex items-center gap-2 border-l border-border pl-4">
|
||||
<Button variant="outline" size="sm" onClick={handleOpenProjectsDialog}>
|
||||
{/* Track Actions - Compact on mobile */}
|
||||
<div className="flex items-center gap-1 sm:gap-2 border-l border-border pl-2 sm:pl-4">
|
||||
<Button variant="outline" size="sm" onClick={handleOpenProjectsDialog} className="hidden sm:flex">
|
||||
<FolderOpen className="h-4 w-4 mr-1.5" />
|
||||
Projects
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleOpenProjectsDialog} className="sm:hidden px-2">
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={() => addTrack()}>
|
||||
<Plus className="h-4 w-4 mr-1.5" />
|
||||
Add Track
|
||||
<Plus className="h-4 w-4 sm:mr-1.5" />
|
||||
<span className="hidden sm:inline">Add Track</span>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleImportTracks}>
|
||||
<Upload className="h-4 w-4 mr-1.5" />
|
||||
Import
|
||||
<Upload className="h-4 w-4 sm:mr-1.5" />
|
||||
<span className="hidden sm:inline">Import</span>
|
||||
</Button>
|
||||
{tracks.length > 0 && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => setExportDialogOpen(true)}>
|
||||
<Button variant="outline" size="sm" onClick={() => setExportDialogOpen(true)} className="hidden md:flex">
|
||||
<Download className="h-4 w-4 mr-1.5" />
|
||||
Export
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleClearTracks}>
|
||||
<Button variant="outline" size="sm" onClick={handleClearTracks} className="hidden lg:flex">
|
||||
<Trash2 className="h-4 w-4 mr-1.5 text-destructive" />
|
||||
Clear All
|
||||
</Button>
|
||||
@@ -1591,8 +1795,8 @@ export function AudioEditor() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right Sidebar - Master Controls & Analyzers */}
|
||||
<aside className="flex-shrink-0 border-l border-border bg-card flex flex-col pt-5 px-4 pb-4 gap-4 w-60">
|
||||
{/* Right Sidebar - Master Controls & Analyzers - Hidden on mobile */}
|
||||
<aside className="hidden lg:flex flex-shrink-0 border-l border-border bg-card flex-col pt-5 px-4 pb-4 gap-4 w-60">
|
||||
{/* Master Controls */}
|
||||
<div className="flex items-center justify-center">
|
||||
<MasterControls
|
||||
@@ -1602,6 +1806,7 @@ export function AudioEditor() {
|
||||
rmsLevel={masterRmsLevel}
|
||||
isClipping={masterIsClipping}
|
||||
isMuted={isMasterMuted}
|
||||
collapsed={masterControlsCollapsed}
|
||||
onVolumeChange={setMasterVolume}
|
||||
onPanChange={setMasterPan}
|
||||
onMuteToggle={() => {
|
||||
@@ -1614,6 +1819,7 @@ export function AudioEditor() {
|
||||
}
|
||||
}}
|
||||
onResetClip={resetClipIndicator}
|
||||
onToggleCollapse={() => setMasterControlsCollapsed(!masterControlsCollapsed)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1681,89 +1887,162 @@ export function AudioEditor() {
|
||||
{/* Analyzer Display */}
|
||||
<div className="flex-1 min-h-[360px] flex items-start justify-center">
|
||||
<div className="w-[178px]">
|
||||
{analyzerView === 'frequency' && <FrequencyAnalyzer analyserNode={masterAnalyser} />}
|
||||
{analyzerView === 'spectrogram' && settings.performance.enableSpectrogram && <Spectrogram analyserNode={masterAnalyser} />}
|
||||
{analyzerView === 'phase' && <PhaseCorrelationMeter analyserNode={masterAnalyser} />}
|
||||
{analyzerView === 'lufs' && <LUFSMeter analyserNode={masterAnalyser} />}
|
||||
{analyzerView === 'stats' && <AudioStatistics tracks={tracks} />}
|
||||
{analyzerView === 'frequency' && (
|
||||
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
|
||||
<FrequencyAnalyzer analyserNode={masterAnalyser} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{analyzerView === 'spectrogram' && settings.performance.enableSpectrogram && (
|
||||
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
|
||||
<Spectrogram analyserNode={masterAnalyser} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{analyzerView === 'phase' && (
|
||||
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
|
||||
<PhaseCorrelationMeter analyserNode={masterAnalyser} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{analyzerView === 'lufs' && (
|
||||
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
|
||||
<LUFSMeter analyserNode={masterAnalyser} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{analyzerView === 'stats' && (
|
||||
<React.Suspense fallback={<div className="h-[300px] flex items-center justify-center text-muted-foreground">Loading...</div>}>
|
||||
<AudioStatistics tracks={tracks} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Transport Controls */}
|
||||
<div className="border-t border-border bg-card p-3 flex justify-center">
|
||||
<PlaybackControls
|
||||
isPlaying={isPlaying}
|
||||
isPaused={!isPlaying}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
volume={masterVolume}
|
||||
onPlay={play}
|
||||
onPause={pause}
|
||||
onStop={stop}
|
||||
onSeek={seek}
|
||||
onVolumeChange={setMasterVolume}
|
||||
currentTimeFormatted={formatDuration(currentTime)}
|
||||
durationFormatted={formatDuration(duration)}
|
||||
isRecording={recordingState.isRecording}
|
||||
onStartRecording={handleStartRecording}
|
||||
onStopRecording={handleStopRecording}
|
||||
punchInEnabled={punchInEnabled}
|
||||
punchInTime={punchInTime}
|
||||
punchOutTime={punchOutTime}
|
||||
onPunchInEnabledChange={setPunchInEnabled}
|
||||
onPunchInTimeChange={setPunchInTime}
|
||||
onPunchOutTimeChange={setPunchOutTime}
|
||||
overdubEnabled={overdubEnabled}
|
||||
onOverdubEnabledChange={setOverdubEnabled}
|
||||
/>
|
||||
{/* Bottom Bar - Stacked on mobile (Master then Transport), Side-by-side on desktop */}
|
||||
<div className="border-t border-border bg-card p-3 flex flex-col lg:flex-row gap-3">
|
||||
{/* Master Controls - Mobile only (hidden on desktop where sidebar shows master) */}
|
||||
<div className="lg:hidden flex justify-center border-b border-border pb-3 lg:border-b-0 lg:pb-0">
|
||||
<MasterControls
|
||||
volume={masterVolume}
|
||||
pan={masterPan}
|
||||
peakLevel={masterPeakLevel}
|
||||
rmsLevel={masterRmsLevel}
|
||||
isClipping={masterIsClipping}
|
||||
isMuted={isMasterMuted}
|
||||
collapsed={masterControlsCollapsed}
|
||||
onVolumeChange={setMasterVolume}
|
||||
onPanChange={setMasterPan}
|
||||
onMuteToggle={() => {
|
||||
if (isMasterMuted) {
|
||||
setMasterVolume(0.8);
|
||||
setIsMasterMuted(false);
|
||||
} else {
|
||||
setMasterVolume(0);
|
||||
setIsMasterMuted(true);
|
||||
}
|
||||
}}
|
||||
onResetClip={resetClipIndicator}
|
||||
onToggleCollapse={() => setMasterControlsCollapsed(!masterControlsCollapsed)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Playback Controls - Bottom on mobile, centered on desktop */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
<PlaybackControls
|
||||
isPlaying={isPlaying}
|
||||
isPaused={!isPlaying}
|
||||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
volume={masterVolume}
|
||||
onPlay={play}
|
||||
onPause={pause}
|
||||
onStop={stop}
|
||||
onSeek={seek}
|
||||
onVolumeChange={setMasterVolume}
|
||||
currentTimeFormatted={formatDuration(currentTime)}
|
||||
durationFormatted={formatDuration(duration)}
|
||||
isRecording={recordingState.isRecording}
|
||||
onStartRecording={handleStartRecording}
|
||||
onStopRecording={handleStopRecording}
|
||||
punchInEnabled={punchInEnabled}
|
||||
punchInTime={punchInTime}
|
||||
punchOutTime={punchOutTime}
|
||||
onPunchInEnabledChange={setPunchInEnabled}
|
||||
onPunchInTimeChange={setPunchInTime}
|
||||
onPunchOutTimeChange={setPunchOutTime}
|
||||
overdubEnabled={overdubEnabled}
|
||||
onOverdubEnabledChange={setOverdubEnabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Track Dialog */}
|
||||
<ImportTrackDialog
|
||||
open={importDialogOpen}
|
||||
onClose={() => setImportDialogOpen(false)}
|
||||
onImportTrack={handleImportTrack}
|
||||
/>
|
||||
<React.Suspense fallback={null}>
|
||||
<ImportTrackDialog
|
||||
open={importDialogOpen}
|
||||
onClose={() => setImportDialogOpen(false)}
|
||||
onImportTrack={handleImportTrack}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
{/* Global Settings Dialog */}
|
||||
<GlobalSettingsDialog
|
||||
open={settingsDialogOpen}
|
||||
onClose={() => setSettingsDialogOpen(false)}
|
||||
recordingSettings={recordingSettings}
|
||||
onInputGainChange={setInputGain}
|
||||
onRecordMonoChange={setRecordMono}
|
||||
onSampleRateChange={setSampleRate}
|
||||
settings={settings}
|
||||
onAudioSettingsChange={updateAudioSettings}
|
||||
onUISettingsChange={updateUISettings}
|
||||
onEditorSettingsChange={updateEditorSettings}
|
||||
onPerformanceSettingsChange={updatePerformanceSettings}
|
||||
onResetCategory={resetCategory}
|
||||
/>
|
||||
<React.Suspense fallback={null}>
|
||||
<GlobalSettingsDialog
|
||||
open={settingsDialogOpen}
|
||||
onClose={() => setSettingsDialogOpen(false)}
|
||||
recordingSettings={recordingSettings}
|
||||
onInputGainChange={setInputGain}
|
||||
onRecordMonoChange={setRecordMono}
|
||||
onSampleRateChange={setSampleRate}
|
||||
settings={settings}
|
||||
onAudioSettingsChange={updateAudioSettings}
|
||||
onUISettingsChange={updateUISettings}
|
||||
onEditorSettingsChange={updateEditorSettings}
|
||||
onPerformanceSettingsChange={updatePerformanceSettings}
|
||||
onResetCategory={resetCategory}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
{/* Export Dialog */}
|
||||
<ExportDialog
|
||||
open={exportDialogOpen}
|
||||
onClose={() => setExportDialogOpen(false)}
|
||||
onExport={handleExport}
|
||||
isExporting={isExporting}
|
||||
hasSelection={tracks.some(t => t.selection !== null)}
|
||||
/>
|
||||
<React.Suspense fallback={null}>
|
||||
<ExportDialog
|
||||
open={exportDialogOpen}
|
||||
onClose={() => setExportDialogOpen(false)}
|
||||
onExport={handleExport}
|
||||
isExporting={isExporting}
|
||||
hasSelection={tracks.some(t => t.selection !== null)}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
{/* Projects Dialog */}
|
||||
<ProjectsDialog
|
||||
open={projectsDialogOpen}
|
||||
onClose={() => setProjectsDialogOpen(false)}
|
||||
projects={projects}
|
||||
onNewProject={handleNewProject}
|
||||
onLoadProject={handleLoadProject}
|
||||
onDeleteProject={handleDeleteProject}
|
||||
onDuplicateProject={handleDuplicateProject}
|
||||
onExportProject={handleExportProject}
|
||||
onImportProject={handleImportProject}
|
||||
<React.Suspense fallback={null}>
|
||||
<ProjectsDialog
|
||||
open={projectsDialogOpen}
|
||||
onClose={() => setProjectsDialogOpen(false)}
|
||||
projects={projects}
|
||||
onNewProject={handleNewProject}
|
||||
onLoadProject={handleLoadProject}
|
||||
onDeleteProject={handleDeleteProject}
|
||||
onDuplicateProject={handleDuplicateProject}
|
||||
onExportProject={handleExportProject}
|
||||
onImportProject={handleImportProject}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
{/* Browser Compatibility Dialog */}
|
||||
<BrowserCompatDialog
|
||||
open={browserCompatDialogOpen}
|
||||
missingFeatures={browserCompatInfo.missingFeatures}
|
||||
warnings={browserCompatInfo.warnings}
|
||||
onClose={() => setBrowserCompatDialogOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Keyboard Shortcuts Dialog */}
|
||||
<React.Suspense fallback={null}>
|
||||
<KeyboardShortcutsDialog
|
||||
open={shortcutsDialogOpen}
|
||||
onClose={() => setShortcutsDialogOpen(false)}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user