AudioPlayer
An audio clip with play, a seek bar that says the time, and a button cycling through playback speeds.
Demo
T. rex roar · MDN sample sounds
Ended 0 times.
No speed button
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/audio_player.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/audio_player.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/audio_player.qwe
// The clip is one of MDN's shared sample sounds.let audio_player_demo_src = "https://mdn.github.io/shared-assets/audio/t-rex-roar.mp3"component AudioPlayerDemoLive() { state ended = 0 <div.demo-stack> <AudioPlayer src={audio_player_demo_src} title="T. rex roar" artist="MDN sample sounds" on_end={() => { ended += 1 }}/> <p.demo-note id="audio-player-demo-note">Ended {ended} times.</p> </div>}view AudioPlayerDemo() = <div.demo-stack.demo-narrow style="max-width: 520px"> <AudioPlayerDemoLive/> <AudioPlayer src={audio_player_demo_src} title="Compact, two speeds" compact={true} speeds={[1, 2]}/> <AudioPlayer src={audio_player_demo_src} title="No speed button" speeds={[]}/> </div>Source
ui/audio_player.qwe
// AudioPlayer: an <audio> with the kit's own controls, in one row: play and// pause, the title, a seek bar with the time, and a speed button that cycles// through the speeds. The seek bar is a slider for the keyboard and screen// readers, and says the time.//// <AudioPlayer src="/audio/ep12.mp3" title="Episode 12: Views first" artist="The Quillo Podcast"/>// <AudioPlayer src={clip} title="Voice note" speeds={[1, 1.5, 2]} on_end={() => { heard = true }}/>//// src: the audio. title, artist: what is playing (title is also the player's// name for screen readers). speeds: the playback speeds the button cycles// through ([1, 1.25, 1.5, 2]); none hides the button. loop: as on <audio>.// preload: "metadata" (default), "auto" or "none". compact: no title line.// on_play(), on_pause(), on_end(): inside one of your components.import ui/theme.qwe use ui_clslet audio_player_css = css`.ui-audio-player { display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 13px/1.3 var(--ui-font);}.ui-audio-player-play { display: grid; place-items: center; flex: none; width: 40px; height: 40px; padding: 0; border: 0; border-radius: 50%; background: var(--ui-accent); color: var(--ui-accent-fg); cursor: pointer; transition: transform .12s;}.ui-audio-player-play:hover { transform: scale(1.05); }.ui-audio-player-play:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-audio-player-play svg { width: 18px; height: 18px; }.ui-audio-player-main { flex: 1; min-width: 0; display: grid; gap: 4px; }.ui-audio-player-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }.ui-audio-player-artist { color: var(--ui-muted); font-weight: 400; }.ui-audio-player-row { display: flex; align-items: center; gap: 8px; }.ui-audio-player-time { flex: none; color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-audio-player-seek { flex: 1; min-width: 60px; appearance: none; height: 18px; margin: 0; background: transparent; cursor: pointer; }.ui-audio-player-seek::-webkit-slider-runnable-track { height: 5px; border-radius: 999px; background: linear-gradient(to right, var(--ui-accent) var(--f, 0%), var(--ui-subtle) var(--f, 0%)); box-shadow: inset 0 0 0 1px var(--ui-border); }.ui-audio-player-seek::-moz-range-track { height: 5px; border-radius: 999px; background: var(--ui-subtle); }.ui-audio-player-seek::-moz-range-progress { height: 5px; border-radius: 999px; background: var(--ui-accent); }.ui-audio-player-seek::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; margin-top: -4.5px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); }.ui-audio-player-seek::-moz-range-thumb { width: 10px; height: 10px; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--ui-accent); }.ui-audio-player-seek:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 999px; }.ui-audio-player-speed { flex: none; height: 28px; min-width: 44px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); font: 600 12px/1 var(--ui-font); font-variant-numeric: tabular-nums; cursor: pointer;}.ui-audio-player-speed:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-audio-player-speed:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-audio-player-play { transition: none; } }`// Seconds as "m:ss", or "h:mm:ss" from an hour.fn audio_player_time(s: num) -> str { let t = floor(s) let h = floor(t / 3600) let m = floor(t % 3600 / 60) let sec = t % 60 let two = (n: int) => n < 10 ? `0${n}` : str(n) h > 0 ? `${h}:${two(m)}:${two(sec)}` : `${m}:${two(sec)}`}// A speed as its button says it: 1 is "1×", 1.25 is "1.25×".fn audio_player_speed(x: num) -> str = `${x}×`component AudioPlayer(src: str, title: str = "Audio", artist: str? = null, speeds: list = [1, 1.25, 1.5, 2], loop: bool = false, preload: str = "metadata", compact: bool = false, on_play = null, on_pause = null, on_end = null, class: str? = null) { style global audio_player_css state el = null state playing = false state time = 0 state dur = 0 state at = 0 let total = dur > 0 ? dur : 0 let fill = total > 0 ? round(time * 100 / total, 2) : 0 let toggle = () => { if el != null { // The browser may refuse to play (no click on the page yet): the button stays Play. if playing { el.pause() } else { el.play().catch((_ = null) => { playing = false }) } } } <div class={ui_cls(["ui-audio-player", class])} role="region" aria-label={title}> <audio src={src} loop={loop} preload={preload} @mount={(a) => { el = a; if math.isfinite(a.duration) && a.duration > 0 { dur = a.duration } }} @loadedmetadata={(e) => { dur = math.isfinite(e.duration) ? e.duration : 0 }} @durationchange={(e) => { dur = math.isfinite(e.duration) ? e.duration : 0 }} @timeupdate={(e) => { time = e.currentTime }} @play={() => { playing = true; on_play && on_play() }} @pause={() => { playing = false; on_pause && on_pause() }} @ended={() => { playing = false; on_end && on_end() }}></audio> <button.ui-audio-player-play type="button" aria-label={playing ? "Pause" : "Play"} @click={toggle}> {playing ? <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg> : <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 4v16l14-8z"/></svg>} </button> <div.ui-audio-player-main> {!compact && <div.ui-audio-player-title>{title}{artist != null && <span.ui-audio-player-artist> · {artist}</span>}</div>} <div.ui-audio-player-row> <span.ui-audio-player-time aria-hidden="true">{audio_player_time(time)}</span> <input.ui-audio-player-seek type="range" min="0" max={total} step="0.1" value={time} aria-label="Seek" disabled={total == 0} aria-valuetext={`${audio_player_time(time)} of ${audio_player_time(total)}`} style={`--f: ${fill}%`} @input={(e) => { if el != null { el.currentTime = float(e.value) } }}/> <span.ui-audio-player-time aria-hidden="true">{audio_player_time(total)}</span> </div> </div> {len(speeds) > 0 && <button.ui-audio-player-speed type="button" aria-label={`Speed: ${audio_player_speed(speeds[at])}`} title="Playback speed" @click={() => { at = (at + 1) % len(speeds) if el != null { el.playbackRate = speeds[at] } }}>{audio_player_speed(speeds[at])}</button>} </div>}Related
PlaylistNumbered tracks with the one playing marked, each one's length and the total; with sources it plays them in turn.6 views · 5 downloadsVolumeControlA mute button and a level slider with an icon that shows how loud it is; a real range, so forms and keyboards work.6 views · 7 downloadsCameraCaptureTakes a photo with the camera into a hidden input as a data URL, with retake, and says so when there is no camera.6 views · 6 downloadsCoverImageA wide picture with a title, subtitle and actions over a gradient, in three heights.7 views · 7 downloadsEmbedFrameAnother page in a frame that keeps its shape and fills its width, with a title, permissions and a sandbox.5 views · 4 downloadsFigureAn image (or anything) with a caption and a credit under it, as a figure with its figcaption.8 views · 8 downloads
Thread
Thread 0 posts
On AudioPlayer: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.