Playlist
Numbered tracks with the one playing marked, each one's length and the total; with sources it plays them in turn.
Demo
Late Night Tapes
5 tracks · 20:02Click a track.
With sounds
2 tracks · 0:04Get 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.qwemain.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>}Related
AudioPlayerAn audio clip with play, a seek bar that says the time, and a button cycling through playback speeds.6 views · 7 downloadsThumbnailA small image in five sizes and three shapes, with a line of text over it, a "+N" veil, or a link.7 views · 9 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 downloads
Thread
Thread 0 posts
On Playlist: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.