- Removed waveformColor from UISettings interface - Removed waveform color picker from Interface settings tab - Preserves dynamic per-track waveform coloring system - Cleaner settings UI with one less option 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
556 lines
23 KiB
TypeScript
556 lines
23 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { X, RotateCcw } from 'lucide-react';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Slider } from '@/components/ui/Slider';
|
|
import { RecordingSettings } from '@/components/recording/RecordingSettings';
|
|
import { cn } from '@/lib/utils/cn';
|
|
import type { RecordingSettings as RecordingSettingsType } from '@/lib/hooks/useRecording';
|
|
import type {
|
|
Settings,
|
|
AudioSettings,
|
|
UISettings,
|
|
EditorSettings,
|
|
PerformanceSettings,
|
|
} from '@/lib/hooks/useSettings';
|
|
|
|
export interface GlobalSettingsDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
recordingSettings: RecordingSettingsType;
|
|
onInputGainChange: (gain: number) => void;
|
|
onRecordMonoChange: (mono: boolean) => void;
|
|
onSampleRateChange: (sampleRate: number) => void;
|
|
settings: Settings;
|
|
onAudioSettingsChange: (updates: Partial<AudioSettings>) => void;
|
|
onUISettingsChange: (updates: Partial<UISettings>) => void;
|
|
onEditorSettingsChange: (updates: Partial<EditorSettings>) => void;
|
|
onPerformanceSettingsChange: (updates: Partial<PerformanceSettings>) => void;
|
|
onResetCategory: (category: 'audio' | 'ui' | 'editor' | 'performance') => void;
|
|
}
|
|
|
|
type TabType = 'recording' | 'audio' | 'editor' | 'interface' | 'performance';
|
|
|
|
export function GlobalSettingsDialog({
|
|
open,
|
|
onClose,
|
|
recordingSettings,
|
|
onInputGainChange,
|
|
onRecordMonoChange,
|
|
onSampleRateChange,
|
|
settings,
|
|
onAudioSettingsChange,
|
|
onUISettingsChange,
|
|
onEditorSettingsChange,
|
|
onPerformanceSettingsChange,
|
|
onResetCategory,
|
|
}: GlobalSettingsDialogProps) {
|
|
const [activeTab, setActiveTab] = React.useState<TabType>('recording');
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
<div
|
|
className="fixed inset-0 bg-background/80 backdrop-blur-sm z-40"
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Dialog */}
|
|
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-3xl z-50">
|
|
<div className="bg-card border border-border rounded-lg shadow-2xl overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
<h2 className="text-lg font-semibold">Settings</h2>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={onClose}
|
|
title="Close"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-border bg-muted/30 overflow-x-auto">
|
|
{[
|
|
{ id: 'recording', label: 'Recording' },
|
|
{ id: 'audio', label: 'Audio' },
|
|
{ id: 'editor', label: 'Editor' },
|
|
{ id: 'interface', label: 'Interface' },
|
|
{ id: 'performance', label: 'Performance' },
|
|
].map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as TabType)}
|
|
className={cn(
|
|
'px-6 py-3 text-sm font-medium transition-colors relative flex-shrink-0',
|
|
activeTab === tab.id
|
|
? 'text-foreground bg-card'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
)}
|
|
>
|
|
{tab.label}
|
|
{activeTab === tab.id && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 max-h-[60vh] overflow-y-auto custom-scrollbar">
|
|
{/* Recording Tab */}
|
|
{activeTab === 'recording' && (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium">Recording Settings</h3>
|
|
</div>
|
|
<RecordingSettings
|
|
settings={recordingSettings}
|
|
onInputGainChange={onInputGainChange}
|
|
onRecordMonoChange={onRecordMonoChange}
|
|
onSampleRateChange={onSampleRateChange}
|
|
className="border-0 bg-transparent p-0"
|
|
/>
|
|
|
|
<div className="pt-4 border-t border-border">
|
|
<h3 className="text-sm font-medium mb-2">Note</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
These settings apply globally to all recordings. Arm a track (red button)
|
|
to enable recording on that specific track.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Audio Tab */}
|
|
{activeTab === 'audio' && (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium">Audio Settings</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onResetCategory('audio')}
|
|
className="h-7 text-xs"
|
|
>
|
|
<RotateCcw className="h-3 w-3 mr-1" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Buffer Size */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Buffer Size</label>
|
|
<select
|
|
value={settings.audio.bufferSize}
|
|
onChange={(e) =>
|
|
onAudioSettingsChange({ bufferSize: Number(e.target.value) })
|
|
}
|
|
className="w-full px-3 py-2 bg-background border border-border rounded text-sm"
|
|
>
|
|
<option value={256}>256 samples (Low latency, higher CPU)</option>
|
|
<option value={512}>512 samples</option>
|
|
<option value={1024}>1024 samples</option>
|
|
<option value={2048}>2048 samples (Recommended)</option>
|
|
<option value={4096}>4096 samples (Low CPU)</option>
|
|
</select>
|
|
<p className="text-xs text-muted-foreground">
|
|
Smaller buffer = lower latency but higher CPU usage. Requires reload.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Sample Rate */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Default Sample Rate</label>
|
|
<select
|
|
value={settings.audio.sampleRate}
|
|
onChange={(e) =>
|
|
onAudioSettingsChange({ sampleRate: Number(e.target.value) })
|
|
}
|
|
className="w-full px-3 py-2 bg-background border border-border rounded text-sm"
|
|
>
|
|
<option value={44100}>44.1 kHz (CD Quality)</option>
|
|
<option value={48000}>48 kHz (Professional)</option>
|
|
<option value={96000}>96 kHz (Hi-Res Audio)</option>
|
|
</select>
|
|
<p className="text-xs text-muted-foreground">
|
|
Higher sample rate = better quality but larger file sizes.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Auto Normalize */}
|
|
<div className="flex items-center justify-between p-3 bg-muted/50 rounded">
|
|
<div>
|
|
<div className="text-sm font-medium">Auto-Normalize on Import</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Automatically normalize audio when importing files
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
checked={settings.audio.autoNormalizeOnImport}
|
|
onChange={(e) =>
|
|
onAudioSettingsChange({ autoNormalizeOnImport: e.target.checked })
|
|
}
|
|
className="h-4 w-4"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Editor Tab */}
|
|
{activeTab === 'editor' && (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium">Editor Settings</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onResetCategory('editor')}
|
|
className="h-7 text-xs"
|
|
>
|
|
<RotateCcw className="h-3 w-3 mr-1" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Auto-Save Interval */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium">Auto-Save Interval</label>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{settings.editor.autoSaveInterval === 0
|
|
? 'Disabled'
|
|
: `${settings.editor.autoSaveInterval}s`}
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[settings.editor.autoSaveInterval]}
|
|
onValueChange={([value]) =>
|
|
onEditorSettingsChange({ autoSaveInterval: value })
|
|
}
|
|
min={0}
|
|
max={30}
|
|
step={1}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Set to 0 to disable auto-save. Default: 3 seconds.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Undo History Limit */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium">Undo History Limit</label>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{settings.editor.undoHistoryLimit} operations
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[settings.editor.undoHistoryLimit]}
|
|
onValueChange={([value]) =>
|
|
onEditorSettingsChange({ undoHistoryLimit: value })
|
|
}
|
|
min={10}
|
|
max={200}
|
|
step={10}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Higher values use more memory. Default: 50.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Snap to Grid */}
|
|
<div className="flex items-center justify-between p-3 bg-muted/50 rounded">
|
|
<div>
|
|
<div className="text-sm font-medium">Snap to Grid</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Snap playhead and selections to grid lines
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
checked={settings.editor.snapToGrid}
|
|
onChange={(e) =>
|
|
onEditorSettingsChange({ snapToGrid: e.target.checked })
|
|
}
|
|
className="h-4 w-4"
|
|
/>
|
|
</div>
|
|
|
|
{/* Grid Resolution */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium">Grid Resolution</label>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{settings.editor.gridResolution}s
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[settings.editor.gridResolution]}
|
|
onValueChange={([value]) =>
|
|
onEditorSettingsChange({ gridResolution: value })
|
|
}
|
|
min={0.1}
|
|
max={5}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Grid spacing in seconds. Default: 1.0s.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Default Zoom */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium">Default Zoom Level</label>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{settings.editor.defaultZoom}x
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[settings.editor.defaultZoom]}
|
|
onValueChange={([value]) =>
|
|
onEditorSettingsChange({ defaultZoom: value })
|
|
}
|
|
min={1}
|
|
max={20}
|
|
step={1}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Initial zoom level when opening projects. Default: 1x.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Interface Tab */}
|
|
{activeTab === 'interface' && (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium">Interface Settings</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onResetCategory('ui')}
|
|
className="h-7 text-xs"
|
|
>
|
|
<RotateCcw className="h-3 w-3 mr-1" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Theme */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Theme</label>
|
|
<div className="flex gap-2">
|
|
{['dark', 'light', 'auto'].map((theme) => (
|
|
<button
|
|
key={theme}
|
|
onClick={() =>
|
|
onUISettingsChange({ theme: theme as 'dark' | 'light' | 'auto' })
|
|
}
|
|
className={cn(
|
|
'flex-1 px-4 py-2 rounded text-sm font-medium transition-colors',
|
|
settings.ui.theme === theme
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted hover:bg-muted/80'
|
|
)}
|
|
>
|
|
{theme.charAt(0).toUpperCase() + theme.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Use the theme toggle in header for quick switching.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Font Size */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Font Size</label>
|
|
<div className="flex gap-2">
|
|
{['small', 'medium', 'large'].map((size) => (
|
|
<button
|
|
key={size}
|
|
onClick={() =>
|
|
onUISettingsChange({ fontSize: size as 'small' | 'medium' | 'large' })
|
|
}
|
|
className={cn(
|
|
'flex-1 px-4 py-2 rounded text-sm font-medium transition-colors',
|
|
settings.ui.fontSize === size
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted hover:bg-muted/80'
|
|
)}
|
|
>
|
|
{size.charAt(0).toUpperCase() + size.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Adjust the UI font size. Requires reload.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Default Track Height */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium">Default Track Height</label>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{settings.ui.defaultTrackHeight}px
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[settings.ui.defaultTrackHeight]}
|
|
onValueChange={([value]) =>
|
|
onUISettingsChange({ defaultTrackHeight: value })
|
|
}
|
|
min={120}
|
|
max={600}
|
|
step={20}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Initial height for new tracks. Default: 400px.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Performance Tab */}
|
|
{activeTab === 'performance' && (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium">Performance Settings</h3>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onResetCategory('performance')}
|
|
className="h-7 text-xs"
|
|
>
|
|
<RotateCcw className="h-3 w-3 mr-1" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Peak Calculation Quality */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Peak Calculation Quality</label>
|
|
<div className="flex gap-2">
|
|
{['low', 'medium', 'high'].map((quality) => (
|
|
<button
|
|
key={quality}
|
|
onClick={() =>
|
|
onPerformanceSettingsChange({
|
|
peakCalculationQuality: quality as 'low' | 'medium' | 'high',
|
|
})
|
|
}
|
|
className={cn(
|
|
'flex-1 px-4 py-2 rounded text-sm font-medium transition-colors',
|
|
settings.performance.peakCalculationQuality === quality
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted hover:bg-muted/80'
|
|
)}
|
|
>
|
|
{quality.charAt(0).toUpperCase() + quality.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Higher quality = more accurate waveforms, slower processing.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Waveform Rendering Quality */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Waveform Rendering Quality</label>
|
|
<div className="flex gap-2">
|
|
{['low', 'medium', 'high'].map((quality) => (
|
|
<button
|
|
key={quality}
|
|
onClick={() =>
|
|
onPerformanceSettingsChange({
|
|
waveformRenderingQuality: quality as 'low' | 'medium' | 'high',
|
|
})
|
|
}
|
|
className={cn(
|
|
'flex-1 px-4 py-2 rounded text-sm font-medium transition-colors',
|
|
settings.performance.waveformRenderingQuality === quality
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted hover:bg-muted/80'
|
|
)}
|
|
>
|
|
{quality.charAt(0).toUpperCase() + quality.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Lower quality = better performance on slower devices.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Enable Spectrogram */}
|
|
<div className="flex items-center justify-between p-3 bg-muted/50 rounded">
|
|
<div>
|
|
<div className="text-sm font-medium">Enable Spectrogram</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Show spectrogram in analysis tools (requires more CPU)
|
|
</p>
|
|
</div>
|
|
<input
|
|
type="checkbox"
|
|
checked={settings.performance.enableSpectrogram}
|
|
onChange={(e) =>
|
|
onPerformanceSettingsChange({ enableSpectrogram: e.target.checked })
|
|
}
|
|
className="h-4 w-4"
|
|
/>
|
|
</div>
|
|
|
|
{/* Max File Size */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium">Maximum File Size</label>
|
|
<span className="text-xs font-mono text-muted-foreground">
|
|
{settings.performance.maxFileSizeMB} MB
|
|
</span>
|
|
</div>
|
|
<Slider
|
|
value={[settings.performance.maxFileSizeMB]}
|
|
onValueChange={([value]) =>
|
|
onPerformanceSettingsChange({ maxFileSizeMB: value })
|
|
}
|
|
min={100}
|
|
max={1000}
|
|
step={50}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Warn when importing files larger than this. Default: 500 MB.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-border bg-muted/30">
|
|
<Button variant="default" onClick={onClose}>
|
|
Done
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|