diff --git a/ui/src/lib/components/ListeningMode.svelte b/ui/src/lib/components/ListeningMode.svelte index 8863cff..9155d0d 100644 --- a/ui/src/lib/components/ListeningMode.svelte +++ b/ui/src/lib/components/ListeningMode.svelte @@ -26,6 +26,56 @@ let samplePlayingVoice = $state(null); let sampleAudio: HTMLAudioElement | null = null; + // ── Pull-down-to-dismiss gesture ───────────────────────────────────────── + let dragY = $state(0); + let isDragging = $state(false); + let dragStartY = 0; + let dragStartTime = 0; + + function onTouchStart(e: TouchEvent) { + // Don't hijack touches that start inside a scrollable element + const target = e.target as Element; + if (target.closest('.overflow-y-auto')) return; + // Don't activate if a modal is open (they handle their own scroll) + if (showVoiceModal || showChapterModal) return; + + isDragging = true; + dragStartY = e.touches[0].clientY; + dragStartTime = Date.now(); + dragY = 0; + } + + function onTouchMove(e: TouchEvent) { + if (!isDragging) return; + const delta = e.touches[0].clientY - dragStartY; + // Only track downward movement + if (delta > 0) { + dragY = delta; + // Prevent page scroll while dragging the overlay down + e.preventDefault(); + } else { + dragY = 0; + } + } + + function onTouchEnd() { + if (!isDragging) return; + isDragging = false; + + const elapsed = Date.now() - dragStartTime; + const velocity = dragY / Math.max(elapsed, 1); // px/ms + + // Dismiss if dragged far enough (>130px) or flicked fast enough (>0.4px/ms) + if (dragY > 130 || velocity > 0.4) { + // Animate out: snap to bottom then close + dragY = window.innerHeight; + setTimeout(onclose, 220); + } else { + // Spring back to 0 + dragY = 0; + } + } + // ── Voice search filtering ──────────────────────────────────────────────── const voiceSearchLower = $derived(voiceSearch.toLowerCase()); const filteredKokoro = $derived(kokoroVoices.filter((v) => voiceLabel(v).toLowerCase().includes(voiceSearchLower))); @@ -187,7 +237,20 @@ -
+
diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index c0057ab..dd37474 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -12,6 +12,7 @@ import * as m from '$lib/paraglide/messages.js'; import { locales, getLocale } from '$lib/paraglide/runtime.js'; import ListeningMode from '$lib/components/ListeningMode.svelte'; + import { fly } from 'svelte/transition'; let { children, data }: { children: Snippet; data: LayoutData } = $props(); @@ -953,8 +954,10 @@ {#if listeningModeOpen} - { listeningModeOpen = false; listeningModeChapters = false; }} - openChapters={listeningModeChapters} - /> +
+ { listeningModeOpen = false; listeningModeChapters = false; }} + openChapters={listeningModeChapters} + /> +
{/if}