Media

Playlist

Numbered tracks with the one playing marked, each one's length and the total; with sources it plays them in turn.

#media#audio#list#interactive

Demo

Preview

Late Night Tapes

5 tracks · 20:02

Click a track.

With sounds

2 tracks · 0:04

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/playlist.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/playlist.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/playlist.qwe
let playlist_demo_tracks = [    { title: "Opening", artist: "Lyra", duration: 214 },    { title: "The Quiet Part", artist: "Lyra", duration: 187 },    { title: "Northbound", artist: "Lyra & Odd Hours", duration: 262 },    { title: "Salt", artist: "Lyra", duration: 198 },    { title: "Closing Time (Live)", artist: "Lyra", duration: 341 },]component PlaylistDemoLive() {    state picked = null    <div.demo-stack>        <Playlist title="Late Night Tapes" tracks={playlist_demo_tracks} playing={1} on_pick={(i) => { picked = i }}/>        <p.demo-note id="playlist-demo-note">{picked == null ? "Click a track." : `Picked track ${picked + 1}: ${playlist_demo_tracks[picked].title}.`}</p>    </div>}view PlaylistDemo() =    <div.demo-stack.demo-narrow style="max-width: 480px">        <PlaylistDemoLive/>        <Playlist title="With sounds" tracks={[            { title: "Roar", artist: "MDN sample sounds", duration: 2, src: "https://mdn.github.io/shared-assets/audio/t-rex-roar.mp3" },            { title: "Roar again", artist: "MDN sample sounds", duration: 2, src: "https://mdn.github.io/shared-assets/audio/t-rex-roar.mp3" },        ]}/>    </div>

Source

ui/playlist.qwe
// Playlist: the tracks of an album or a queue, numbered, with the one playing// marked, each one's length and the total. A click on a track picks it; with// sources, it plays it, moving on to the next as each ends, and a play button// in the header starts and stops.////   <Playlist title="Late Night Tapes" tracks={[//       { title: "Opening", artist: "Lyra", duration: 214, src: "/audio/01.mp3" },//       { title: "The Quiet Part", artist: "Lyra", duration: 187, src: "/audio/02.mp3" },//   ]}/>//   <Playlist tracks={queue} playing={2} on_pick={(i) => { current = i }}/>//// tracks: a list of { title, artist?, duration (seconds), src? }. title: the// list's name (also what screen readers call it; "Playlist"). playing: the// index of the track marked at first (null for none). on_pick(index): inside// one of your components, called with the index of the track picked. With no// src on a track, picking only marks it, for a player of your own to play.import ui/theme.qwe use ui_clslet playlist_css = css`.ui-playlist { display: grid; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.4 var(--ui-font); overflow: hidden; }.ui-playlist-head { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-bottom: 1px solid var(--ui-border); background: var(--ui-subtle); }.ui-playlist-title { flex: 1; min-width: 0; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 600; }.ui-playlist-total { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-playlist-play { display: grid; place-items: center; flex: none; width: 34px; height: 34px; padding: 0; border: 0; border-radius: 50%; background: var(--ui-accent); color: var(--ui-accent-fg); cursor: pointer; }.ui-playlist-play:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-playlist-play svg { width: 16px; height: 16px; }.ui-playlist-list { margin: 0; padding: 4px; list-style: none; }.ui-playlist-track {  display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: 10px; width: 100%; padding: 8px 10px;  border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer;}.ui-playlist-track:hover { background: var(--ui-subtle); }.ui-playlist-track:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-playlist-track[aria-current="true"] { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-playlist-n { color: var(--ui-muted); font-variant-numeric: tabular-nums; text-align: center; }.ui-playlist-track[aria-current="true"] .ui-playlist-n { color: inherit; }.ui-playlist-bars { display: inline-flex; align-items: flex-end; gap: 2px; height: 14px; }.ui-playlist-bars i { width: 3px; height: 40%; background: currentColor; border-radius: 1px; }.ui-playlist-on .ui-playlist-bars i { animation: ui-playlist-bar .8s ease-in-out infinite alternate; }.ui-playlist-on .ui-playlist-bars i:nth-child(2) { animation-delay: .2s; }.ui-playlist-on .ui-playlist-bars i:nth-child(3) { animation-delay: .4s; }@keyframes ui-playlist-bar { from { height: 25%; } to { height: 100%; } }.ui-playlist-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-playlist-artist { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ui-muted); font-size: 12px; }.ui-playlist-track[aria-current="true"] .ui-playlist-artist { color: inherit; opacity: .8; }.ui-playlist-len { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-playlist-track[aria-current="true"] .ui-playlist-len { color: inherit; }@media (prefers-reduced-motion: reduce) { .ui-playlist-on .ui-playlist-bars i { animation: none; height: 70%; } }`// Seconds as "m:ss", or "h:mm:ss" from an hour.fn playlist_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)}`}component Playlist(tracks: list = [], title: str = "Playlist", playing: int? = null, on_pick = null, class: str? = null) {    style global playlist_css    state at = playing    state on = false    state audio = null    let n = len(tracks)    let total = sum(tracks | map((t) => get(t, "duration") ?? 0))    let cur = at != null ? tracks[at] : null    let src = cur != null ? get(cur, "src") : null    let pick = (i: int) => {        at = i        on_pick && on_pick(i)        if audio != null && get(tracks[i], "src") != null {            audio.src = get(tracks[i], "src")            // The browser may refuse to play (no click on the page yet): the track stays marked.            audio.play().catch((_ = null) => { on = false })        }    }    <div class={ui_cls(["ui-playlist", on && "ui-playlist-on", class])} role="region" aria-label={title}>        <audio preload="none" @mount={(a) => { audio = a }}               @play={() => { on = true }} @pause={() => { on = false }}               @ended={() => { if at != null && at < n - 1 { pick(at + 1) } else { on = false } }}></audio>        <div.ui-playlist-head>            {n > 0 && any(tracks, (t) => get(t, "src") != null) && <button.ui-playlist-play type="button" aria-label={on ? "Pause" : "Play"}                    @click={() => {                        if on { audio.pause() } else if src != null { audio.play().catch((_ = null) => { on = false }) } else { pick(0) }                    }}>                {on                    ? <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>}            <h3.ui-playlist-title>{title}</h3>            <span.ui-playlist-total>{n} {n == 1 ? "track" : "tracks"} · {playlist_time(total)}</span>        </div>        <ol.ui-playlist-list>            {for [i, t] in enumerate(tracks) => <li key={i}>                <button.ui-playlist-track type="button" aria-current={i == at ? "true" : null}                        aria-label={`${i + 1}. ${t.title}${get(t, "artist") != null ? `, ${get(t, "artist")}` : ""}, ${playlist_time(get(t, "duration") ?? 0)}${i == at ? " (playing)" : ""}`}                        @click={() => { pick(i) }}>                    <span.ui-playlist-n aria-hidden="true">{i == at                        ? <span.ui-playlist-bars><i></i><i></i><i></i></span>                        : str(i + 1)}</span>                    <span.ui-playlist-name aria-hidden="true">                        {t.title}                        {get(t, "artist") != null && <span.ui-playlist-artist>{get(t, "artist")}</span>}                    </span>                    <span.ui-playlist-len aria-hidden="true">{playlist_time(get(t, "duration") ?? 0)}</span>                </button>            </li>}        </ol>    </div>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

On Playlist: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.