mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 18:06:42 +02:00
Applied formatting to 1487+ files using pnpm format:write - TypeScript/JavaScript files - Svelte components - Astro pages - JSON configs - Markdown docs 13 files still need manual review (Astro JSX comments)
303 lines
7.6 KiB
Svelte
303 lines
7.6 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import type { Snippet } from 'svelte';
|
|
import { Text } from '../../atoms';
|
|
|
|
interface Props {
|
|
/** Audio source URL */
|
|
src: string;
|
|
/** Optional known duration (seconds) */
|
|
duration?: number;
|
|
/** Custom play icon snippet */
|
|
playIcon?: Snippet;
|
|
/** Custom pause icon snippet */
|
|
pauseIcon?: Snippet;
|
|
/** Custom skip back icon snippet */
|
|
skipBackIcon?: Snippet;
|
|
/** Custom skip forward icon snippet */
|
|
skipForwardIcon?: Snippet;
|
|
}
|
|
|
|
let { src, duration, playIcon, pauseIcon, skipBackIcon, skipForwardIcon }: Props = $props();
|
|
|
|
let audio: HTMLAudioElement;
|
|
let currentTime = $state(0);
|
|
let audioDuration = $state(duration || 0);
|
|
let isPlaying = $state(false);
|
|
let isLoading = $state(true);
|
|
let playbackRate = $state(1.0);
|
|
|
|
onMount(() => {
|
|
if (audio) {
|
|
audio.addEventListener('loadedmetadata', () => {
|
|
audioDuration = audio.duration;
|
|
isLoading = false;
|
|
});
|
|
|
|
audio.addEventListener('timeupdate', () => {
|
|
currentTime = audio.currentTime;
|
|
});
|
|
|
|
audio.addEventListener('ended', () => {
|
|
isPlaying = false;
|
|
currentTime = 0;
|
|
});
|
|
|
|
audio.addEventListener('error', () => {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
});
|
|
|
|
function togglePlay() {
|
|
if (!audio) return;
|
|
|
|
if (isPlaying) {
|
|
audio.pause();
|
|
} else {
|
|
audio.play();
|
|
}
|
|
isPlaying = !isPlaying;
|
|
}
|
|
|
|
function seek(e: Event) {
|
|
if (!audio) return;
|
|
const input = e.target as HTMLInputElement;
|
|
audio.currentTime = parseFloat(input.value);
|
|
}
|
|
|
|
function changePlaybackRate() {
|
|
if (!audio) return;
|
|
const rates = [1.0, 1.25, 1.5, 1.75, 2.0];
|
|
const currentIndex = rates.indexOf(playbackRate);
|
|
const nextIndex = (currentIndex + 1) % rates.length;
|
|
playbackRate = rates[nextIndex];
|
|
audio.playbackRate = playbackRate;
|
|
}
|
|
|
|
function formatTime(seconds: number) {
|
|
if (isNaN(seconds)) return '0:00';
|
|
const mins = Math.floor(seconds / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function skipForward() {
|
|
if (!audio) return;
|
|
audio.currentTime = Math.min(audio.currentTime + 10, audioDuration);
|
|
}
|
|
|
|
function skipBackward() {
|
|
if (!audio) return;
|
|
audio.currentTime = Math.max(audio.currentTime - 10, 0);
|
|
}
|
|
|
|
// Calculate progress percentage for styling
|
|
const progressPercent = $derived(audioDuration > 0 ? (currentTime / audioDuration) * 100 : 0);
|
|
</script>
|
|
|
|
<div class="rounded-2xl border border-theme bg-content p-4">
|
|
<audio bind:this={audio} {src} preload="metadata"></audio>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="mb-4">
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max={audioDuration || 100}
|
|
value={currentTime}
|
|
oninput={seek}
|
|
class="audio-slider w-full"
|
|
style="--progress: {progressPercent}%"
|
|
disabled={isLoading}
|
|
/>
|
|
<div class="mt-1 flex justify-between">
|
|
<Text variant="muted">{formatTime(currentTime)}</Text>
|
|
<Text variant="muted">{formatTime(audioDuration)}</Text>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls -->
|
|
<div class="flex items-center justify-center gap-4">
|
|
<!-- Skip Backward -->
|
|
<button
|
|
onclick={skipBackward}
|
|
disabled={isLoading}
|
|
class="rounded-full bg-secondary-button p-2 transition-colors hover:bg-content-hover disabled:opacity-50"
|
|
type="button"
|
|
aria-label="Skip backward 10 seconds"
|
|
>
|
|
{#if skipBackIcon}
|
|
{@render skipBackIcon()}
|
|
{:else}
|
|
<svg class="h-6 w-6 text-theme" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M12.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0019 16V8a1 1 0 00-1.6-.8l-5.333 4zM4.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0011 16V8a1 1 0 00-1.6-.8l-5.334 4z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- Play/Pause -->
|
|
<button
|
|
onclick={togglePlay}
|
|
disabled={isLoading}
|
|
class="rounded-full bg-primary-button p-4 text-primary-button-text transition-colors hover:opacity-90 disabled:opacity-50"
|
|
type="button"
|
|
aria-label={isPlaying ? 'Pause' : 'Play'}
|
|
>
|
|
{#if isLoading}
|
|
<svg class="h-6 w-6 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
|
|
></circle>
|
|
<path
|
|
class="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
></path>
|
|
</svg>
|
|
{:else if isPlaying}
|
|
{#if pauseIcon}
|
|
{@render pauseIcon()}
|
|
{:else}
|
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
|
|
</svg>
|
|
{/if}
|
|
{:else if playIcon}
|
|
{@render playIcon()}
|
|
{:else}
|
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8 5v14l11-7z" />
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- Skip Forward -->
|
|
<button
|
|
onclick={skipForward}
|
|
disabled={isLoading}
|
|
class="rounded-full bg-secondary-button p-2 transition-colors hover:bg-content-hover disabled:opacity-50"
|
|
type="button"
|
|
aria-label="Skip forward 10 seconds"
|
|
>
|
|
{#if skipForwardIcon}
|
|
{@render skipForwardIcon()}
|
|
{:else}
|
|
<svg class="h-6 w-6 text-theme" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M11.933 12.8a1 1 0 000-1.6L6.6 7.2A1 1 0 005 8v8a1 1 0 001.6.8l5.333-4zM19.933 12.8a1 1 0 000-1.6l-5.333-4A1 1 0 0013 8v8a1 1 0 001.6.8l5.333-4z"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</button>
|
|
|
|
<!-- Playback Speed -->
|
|
<button
|
|
onclick={changePlaybackRate}
|
|
disabled={isLoading}
|
|
class="rounded-lg bg-secondary-button px-3 py-1 text-sm font-medium text-theme transition-colors hover:bg-content-hover disabled:opacity-50"
|
|
type="button"
|
|
aria-label="Change playback speed"
|
|
>
|
|
{playbackRate}x
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Custom Range Slider Styling */
|
|
.audio-slider {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
outline: none;
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
|
|
.audio-slider:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Webkit (Chrome, Safari, Edge) - Track */
|
|
.audio-slider::-webkit-slider-runnable-track {
|
|
width: 100%;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: linear-gradient(
|
|
to right,
|
|
var(--color-primary-button) 0%,
|
|
var(--color-primary-button) var(--progress, 0%),
|
|
var(--color-border) var(--progress, 0%),
|
|
var(--color-border) 100%
|
|
);
|
|
}
|
|
|
|
/* Webkit - Thumb */
|
|
.audio-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background: var(--color-primary-button);
|
|
cursor: pointer;
|
|
border: 3px solid var(--color-content-bg);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
margin-top: -6px;
|
|
transition: transform 0.1s ease;
|
|
}
|
|
|
|
.audio-slider::-webkit-slider-thumb:hover {
|
|
transform: scale(1.15);
|
|
}
|
|
|
|
.audio-slider::-webkit-slider-thumb:active {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* Firefox - Track */
|
|
.audio-slider::-moz-range-track {
|
|
width: 100%;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: var(--color-border);
|
|
}
|
|
|
|
/* Firefox - Progress */
|
|
.audio-slider::-moz-range-progress {
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
background: var(--color-primary-button);
|
|
}
|
|
|
|
/* Firefox - Thumb */
|
|
.audio-slider::-moz-range-thumb {
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: var(--color-primary-button);
|
|
cursor: pointer;
|
|
border: 3px solid var(--color-content-bg);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
transition: transform 0.1s ease;
|
|
}
|
|
|
|
.audio-slider::-moz-range-thumb:hover {
|
|
transform: scale(1.15);
|
|
}
|
|
|
|
.audio-slider::-moz-range-thumb:active {
|
|
transform: scale(1.1);
|
|
}
|
|
</style>
|