Skip to content

Commit e316b0b

Browse files
authored
Merge pull request #274 from jamiepine/feat/landing-page-redesign
Landing page v0.2.0 redesign
2 parents 732270b + 0c6aa15 commit e316b0b

27 files changed

Lines changed: 3449 additions & 463 deletions

app/src/components/AudioPlayer/AudioPlayer.tsx

Lines changed: 31 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ export function AudioPlayer() {
139139
barRadius: 2,
140140
height: 80,
141141
normalize: true,
142-
backend: 'WebAudio',
142+
// Use MediaElement backend (default). Unlike the WebAudio backend,
143+
// MediaElement uses a standard <audio> element for playback which
144+
// benefits from the browser/webview's built-in audio session recovery.
145+
// This prevents audio loss when another app steals audio output or
146+
// the system audio session is interrupted.
143147
interact: true, // Enable interaction (click to seek)
144148
mediaControls: false, // Don't show native controls
145149
});
@@ -189,15 +193,6 @@ export function AudioPlayer() {
189193
const currentVolume = usePlayerStore.getState().volume;
190194
wavesurfer.setVolume(currentVolume);
191195

192-
// Get the underlying audio element and ensure it's not muted
193-
// (unless we're using native playback, which will be set later)
194-
const mediaElement = wavesurfer.getMediaElement();
195-
if (mediaElement && !isUsingNativePlaybackRef.current) {
196-
mediaElement.volume = currentVolume;
197-
mediaElement.muted = false;
198-
debug.log('Audio element volume:', mediaElement.volume, 'muted:', mediaElement.muted);
199-
}
200-
201196
// Auto-play when ready - check if we should use native playback
202197
// Get current values from the store and queries at runtime (not captured closure values)
203198
const currentAudioUrl = usePlayerStore.getState().audioUrl;
@@ -264,21 +259,8 @@ export function AudioPlayer() {
264259
debug.log('Should use native playback:', shouldUseNative);
265260

266261
if (!shouldUseNative) {
267-
debug.log('No custom devices assigned, falling back to WaveSurfer');
268-
// Reset native playback flag and unmute WaveSurfer
262+
debug.log('No custom devices assigned, using standard playback');
269263
isUsingNativePlaybackRef.current = false;
270-
const mediaElement = wavesurfer.getMediaElement();
271-
if (mediaElement) {
272-
const currentVolume = usePlayerStore.getState().volume;
273-
mediaElement.volume = currentVolume;
274-
mediaElement.muted = false;
275-
debug.log(
276-
'WaveSurfer unmuted for normal playback - volume:',
277-
mediaElement.volume,
278-
'muted:',
279-
mediaElement.muted,
280-
);
281-
}
282264
} else {
283265
const deviceIds = assignedChannels.flatMap((ch: any) => ch.device_ids);
284266
debug.log('Device IDs to play to:', deviceIds);
@@ -299,19 +281,10 @@ export function AudioPlayer() {
299281
// Mark that we're using native playback
300282
isUsingNativePlaybackRef.current = true;
301283

302-
// Mute WaveSurfer's audio element to prevent UI audio output
303-
// Keep WaveSurfer running for visualization
304-
const mediaElement = wavesurfer.getMediaElement();
305-
if (mediaElement) {
306-
mediaElement.volume = 0;
307-
mediaElement.muted = true;
308-
debug.log(
309-
'WaveSurfer muted for native playback - volume:',
310-
mediaElement.volume,
311-
'muted:',
312-
mediaElement.muted,
313-
);
314-
}
284+
// Mute WaveSurfer's audio output — native handles the actual sound
285+
// Keep WaveSurfer running for waveform visualization
286+
wavesurfer.setVolume(0);
287+
wavesurfer.setMuted(true);
315288

316289
// Start WaveSurfer playback for visualization (muted)
317290
wavesurfer.play().catch((error) => {
@@ -334,38 +307,15 @@ export function AudioPlayer() {
334307
'Native playback failed during auto-play, falling back to WaveSurfer:',
335308
error,
336309
);
337-
// Reset native playback flag and unmute WaveSurfer
338310
isUsingNativePlaybackRef.current = false;
339-
const mediaElement = wavesurfer.getMediaElement();
340-
if (mediaElement) {
341-
const currentVolume = usePlayerStore.getState().volume;
342-
mediaElement.volume = currentVolume;
343-
mediaElement.muted = false;
344-
debug.log(
345-
'WaveSurfer unmuted after native playback failure - volume:',
346-
mediaElement.volume,
347-
'muted:',
348-
mediaElement.muted,
349-
);
350-
}
351311
// Fall through to WaveSurfer playback
352312
}
353-
} else {
354-
debug.log('Not using native playback, using WaveSurfer');
355-
// Reset native playback flag and unmute WaveSurfer
356-
isUsingNativePlaybackRef.current = false;
357-
const mediaElement = wavesurfer.getMediaElement();
358-
if (mediaElement) {
359-
const currentVolume = usePlayerStore.getState().volume;
360-
mediaElement.volume = currentVolume;
361-
mediaElement.muted = false;
362-
debug.log(
363-
'WaveSurfer unmuted for normal playback - volume:',
364-
mediaElement.volume,
365-
'muted:',
366-
mediaElement.muted,
367-
);
368-
}
313+
}
314+
315+
// Standard playback path — ensure WaveSurfer is unmuted
316+
if (!isUsingNativePlaybackRef.current) {
317+
wavesurfer.setMuted(false);
318+
wavesurfer.setVolume(usePlayerStore.getState().volume);
369319
}
370320

371321
// Only auto-play if shouldAutoPlay flag is set (user explicitly clicked to play)
@@ -389,28 +339,6 @@ export function AudioPlayer() {
389339
// Handle play/pause
390340
wavesurfer.on('play', () => {
391341
setIsPlaying(true);
392-
// Ensure audio element volume is set correctly
393-
const mediaElement = wavesurfer.getMediaElement();
394-
if (mediaElement) {
395-
// Double-check: if using native playback, keep WaveSurfer muted
396-
// Otherwise, ensure it's unmuted
397-
if (isUsingNativePlaybackRef.current) {
398-
mediaElement.volume = 0;
399-
mediaElement.muted = true;
400-
debug.log('Playing (native mode) - WaveSurfer muted for visualization only');
401-
} else {
402-
// Ensure WaveSurfer is unmuted for normal playback
403-
const currentVolume = usePlayerStore.getState().volume;
404-
mediaElement.volume = currentVolume;
405-
mediaElement.muted = false;
406-
debug.log(
407-
'Playing (normal mode) - volume:',
408-
mediaElement.volume,
409-
'muted:',
410-
mediaElement.muted,
411-
);
412-
}
413-
}
414342
});
415343
wavesurfer.on('pause', () => setIsPlaying(false));
416344
wavesurfer.on('finish', () => {
@@ -492,11 +420,6 @@ export function AudioPlayer() {
492420
if (wavesurferRef.current) {
493421
debug.log('Destroying WaveSurfer instance');
494422
try {
495-
const mediaElement = wavesurferRef.current.getMediaElement();
496-
if (mediaElement) {
497-
mediaElement.pause();
498-
mediaElement.src = '';
499-
}
500423
wavesurferRef.current.destroy();
501424
} catch (error) {
502425
debug.error('Error destroying WaveSurfer:', error);
@@ -537,13 +460,10 @@ export function AudioPlayer() {
537460
}
538461

539462
// Reset native playback flag when loading new audio
540-
// Also unmute WaveSurfer if it was muted
463+
// Unmute WaveSurfer if it was muted for native playback
541464
if (isUsingNativePlaybackRef.current) {
542-
const mediaElement = wavesurfer.getMediaElement();
543-
if (mediaElement) {
544-
mediaElement.muted = false;
545-
mediaElement.volume = usePlayerStore.getState().volume;
546-
}
465+
wavesurfer.setMuted(false);
466+
wavesurfer.setVolume(usePlayerStore.getState().volume);
547467
}
548468
isUsingNativePlaybackRef.current = false;
549469

@@ -559,16 +479,7 @@ export function AudioPlayer() {
559479
wavesurfer.pause();
560480
}
561481

562-
// Stop the media element explicitly
563-
const mediaElement = wavesurfer.getMediaElement();
564-
if (mediaElement) {
565-
debug.log('Stopping media element');
566-
mediaElement.pause();
567-
mediaElement.currentTime = 0;
568-
mediaElement.src = '';
569-
}
570-
571-
// Use empty() to completely destroy the waveform and media element
482+
// Use empty() to completely destroy the waveform and reset media
572483
debug.log('Calling wavesurfer.empty() to destroy audio');
573484
wavesurfer.empty();
574485
} catch (error) {
@@ -623,20 +534,13 @@ export function AudioPlayer() {
623534
// Sync volume
624535
useEffect(() => {
625536
if (wavesurferRef.current) {
626-
wavesurferRef.current.setVolume(volume);
627-
// Also ensure the underlying audio element volume is set
628-
const mediaElement = wavesurferRef.current.getMediaElement();
629-
if (mediaElement) {
630-
// If using native playback, keep WaveSurfer muted regardless of volume setting
631-
if (isUsingNativePlaybackRef.current) {
632-
mediaElement.volume = 0;
633-
mediaElement.muted = true;
634-
debug.log('Volume sync: Using native playback, keeping WaveSurfer muted');
635-
} else {
636-
mediaElement.volume = volume;
637-
mediaElement.muted = volume === 0;
638-
debug.log('Volume synced:', volume, 'muted:', mediaElement.muted);
639-
}
537+
// If using native playback, keep WaveSurfer muted regardless of volume setting
538+
if (isUsingNativePlaybackRef.current) {
539+
wavesurferRef.current.setVolume(0);
540+
debug.log('Volume sync: Using native playback, keeping WaveSurfer muted');
541+
} else {
542+
wavesurferRef.current.setVolume(volume);
543+
debug.log('Volume synced:', volume);
640544
}
641545
}
642546
}, [volume]);
@@ -757,11 +661,8 @@ export function AudioPlayer() {
757661
isUsingNativePlaybackRef.current = true;
758662

759663
// Mute WaveSurfer and start it for visualization
760-
const mediaElement = wavesurferRef.current.getMediaElement();
761-
if (mediaElement) {
762-
mediaElement.volume = 0;
763-
mediaElement.muted = true;
764-
}
664+
wavesurferRef.current.setVolume(0);
665+
wavesurferRef.current.setMuted(true);
765666

766667
// Start WaveSurfer for visualization (muted)
767668
wavesurferRef.current.play().catch((error) => {
@@ -785,11 +686,8 @@ export function AudioPlayer() {
785686
} else {
786687
// Ensure WaveSurfer is not muted if not using native playback
787688
if (!isUsingNativePlaybackRef.current) {
788-
const mediaElement = wavesurferRef.current.getMediaElement();
789-
if (mediaElement) {
790-
mediaElement.muted = false;
791-
mediaElement.volume = volume;
792-
}
689+
wavesurferRef.current.setMuted(false);
690+
wavesurferRef.current.setVolume(volume);
793691
}
794692

795693
wavesurferRef.current.play().catch((error) => {

app/src/components/EffectsTab/EffectsDetail.tsx

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import { useEffect, useRef, useState } from 'react';
55
import { EffectsChainEditor } from '@/components/Effects/EffectsChainEditor';
66
import { GenerationPicker } from '@/components/Effects/GenerationPicker';
77
import { Button } from '@/components/ui/button';
8+
import {
9+
Dialog,
10+
DialogContent,
11+
DialogDescription,
12+
DialogFooter,
13+
DialogHeader,
14+
DialogTitle,
15+
} from '@/components/ui/dialog';
816
import { Input } from '@/components/ui/input';
917
import { Label } from '@/components/ui/label';
1018
import { Separator } from '@/components/ui/separator';
@@ -29,6 +37,11 @@ export function EffectsDetail() {
2937
const [saving, setSaving] = useState(false);
3038
const [deleting, setDeleting] = useState(false);
3139

40+
// "Save as Custom" dialog state
41+
const [saveAsDialogOpen, setSaveAsDialogOpen] = useState(false);
42+
const [saveAsName, setSaveAsName] = useState('');
43+
const [saveAsDescription, setSaveAsDescription] = useState('');
44+
3245
// Preview state
3346
const [previewGenId, setPreviewGenId] = useState<string | null>(null);
3447
const [previewLoading, setPreviewLoading] = useState(false);
@@ -165,8 +178,38 @@ export function EffectsDetail() {
165178
}
166179
}
167180

168-
async function handleSaveAsNew() {
169-
await handleSaveNew();
181+
function handleSaveAsNew() {
182+
// Open the dialog with a suggested name based on the current preset
183+
setSaveAsName(`${name} (Copy)`);
184+
setSaveAsDescription(description);
185+
setSaveAsDialogOpen(true);
186+
}
187+
188+
async function handleSaveAsConfirm() {
189+
if (!saveAsName.trim()) {
190+
toast({ title: 'Name required', variant: 'destructive' });
191+
return;
192+
}
193+
setSaving(true);
194+
try {
195+
const created = await apiClient.createEffectPreset({
196+
name: saveAsName.trim(),
197+
description: saveAsDescription.trim() || undefined,
198+
effects_chain: workingChain,
199+
});
200+
queryClient.invalidateQueries({ queryKey: ['effect-presets'] });
201+
setSaveAsDialogOpen(false);
202+
setSelectedPresetId(created.id);
203+
toast({ title: 'Preset saved', description: `"${created.name}" has been created.` });
204+
} catch (error) {
205+
toast({
206+
title: 'Failed to save',
207+
description: error instanceof Error ? error.message : 'Unknown error',
208+
variant: 'destructive',
209+
});
210+
} finally {
211+
setSaving(false);
212+
}
170213
}
171214

172215
async function handleDelete() {
@@ -327,6 +370,53 @@ export function EffectsDetail() {
327370
</p>
328371
</div>
329372
</div>
373+
374+
{/* Save as Custom dialog */}
375+
<Dialog open={saveAsDialogOpen} onOpenChange={setSaveAsDialogOpen}>
376+
<DialogContent className="sm:max-w-md">
377+
<DialogHeader>
378+
<DialogTitle>Save as Custom Preset</DialogTitle>
379+
<DialogDescription>
380+
Create a new custom preset based on the current effects chain.
381+
</DialogDescription>
382+
</DialogHeader>
383+
<div className="space-y-3 py-2">
384+
<div className="space-y-1.5">
385+
<Label className="text-xs">Name</Label>
386+
<Input
387+
value={saveAsName}
388+
onChange={(e) => setSaveAsName(e.target.value)}
389+
placeholder="My preset..."
390+
className="h-9"
391+
autoFocus
392+
onKeyDown={(e) => {
393+
if (e.key === 'Enter' && saveAsName.trim()) {
394+
handleSaveAsConfirm();
395+
}
396+
}}
397+
/>
398+
</div>
399+
<div className="space-y-1.5">
400+
<Label className="text-xs">Description</Label>
401+
<Textarea
402+
value={saveAsDescription}
403+
onChange={(e) => setSaveAsDescription(e.target.value)}
404+
placeholder="Describe what this preset does..."
405+
className="min-h-[60px] resize-none"
406+
/>
407+
</div>
408+
</div>
409+
<DialogFooter>
410+
<Button variant="outline" onClick={() => setSaveAsDialogOpen(false)} disabled={saving}>
411+
Cancel
412+
</Button>
413+
<Button onClick={handleSaveAsConfirm} disabled={saving || !saveAsName.trim()}>
414+
<Save className="h-3.5 w-3.5 mr-1.5" />
415+
{saving ? 'Saving...' : 'Save'}
416+
</Button>
417+
</DialogFooter>
418+
</DialogContent>
419+
</Dialog>
330420
</div>
331421
);
332422
}

0 commit comments

Comments
 (0)