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:
149
lib/utils/audio-cleanup.ts
Normal file
149
lib/utils/audio-cleanup.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Audio cleanup utilities to prevent memory leaks
|
||||
*/
|
||||
|
||||
/**
|
||||
* Safely disconnect and cleanup an AudioNode
|
||||
*/
|
||||
export function cleanupAudioNode(node: AudioNode | null | undefined): void {
|
||||
if (!node) return;
|
||||
|
||||
try {
|
||||
node.disconnect();
|
||||
} catch (error) {
|
||||
// Node may already be disconnected, ignore error
|
||||
console.debug('AudioNode cleanup error (expected):', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup multiple audio nodes
|
||||
*/
|
||||
export function cleanupAudioNodes(nodes: Array<AudioNode | null | undefined>): void {
|
||||
nodes.forEach(cleanupAudioNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely stop and cleanup an AudioBufferSourceNode
|
||||
*/
|
||||
export function cleanupAudioSource(source: AudioBufferSourceNode | null | undefined): void {
|
||||
if (!source) return;
|
||||
|
||||
try {
|
||||
source.stop();
|
||||
} catch (error) {
|
||||
// Source may already be stopped, ignore error
|
||||
console.debug('AudioSource stop error (expected):', error);
|
||||
}
|
||||
|
||||
cleanupAudioNode(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup canvas and release resources
|
||||
*/
|
||||
export function cleanupCanvas(canvas: HTMLCanvasElement | null | undefined): void {
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
// Clear the canvas
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
// Reset transform
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
}
|
||||
|
||||
// Release context (helps with memory)
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel animation frame safely
|
||||
*/
|
||||
export function cleanupAnimationFrame(frameId: number | null | undefined): void {
|
||||
if (frameId !== null && frameId !== undefined) {
|
||||
cancelAnimationFrame(frameId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup media stream tracks
|
||||
*/
|
||||
export function cleanupMediaStream(stream: MediaStream | null | undefined): void {
|
||||
if (!stream) return;
|
||||
|
||||
stream.getTracks().forEach(track => {
|
||||
track.stop();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cleanup registry for managing multiple cleanup tasks
|
||||
*/
|
||||
export class CleanupRegistry {
|
||||
private cleanupTasks: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* Register a cleanup task
|
||||
*/
|
||||
register(cleanup: () => void): void {
|
||||
this.cleanupTasks.push(cleanup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an audio node for cleanup
|
||||
*/
|
||||
registerAudioNode(node: AudioNode): void {
|
||||
this.register(() => cleanupAudioNode(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an audio source for cleanup
|
||||
*/
|
||||
registerAudioSource(source: AudioBufferSourceNode): void {
|
||||
this.register(() => cleanupAudioSource(source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a canvas for cleanup
|
||||
*/
|
||||
registerCanvas(canvas: HTMLCanvasElement): void {
|
||||
this.register(() => cleanupCanvas(canvas));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an animation frame for cleanup
|
||||
*/
|
||||
registerAnimationFrame(frameId: number): void {
|
||||
this.register(() => cleanupAnimationFrame(frameId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a media stream for cleanup
|
||||
*/
|
||||
registerMediaStream(stream: MediaStream): void {
|
||||
this.register(() => cleanupMediaStream(stream));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all cleanup tasks and clear the registry
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.cleanupTasks.forEach(task => {
|
||||
try {
|
||||
task();
|
||||
} catch (error) {
|
||||
console.error('Cleanup task failed:', error);
|
||||
}
|
||||
});
|
||||
this.cleanupTasks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of registered cleanup tasks
|
||||
*/
|
||||
get size(): number {
|
||||
return this.cleanupTasks.length;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user