1import { 2 LitElement, 3 html, 4 css, 5} from "https://esm.sh/[email protected]/lit-element.js"; 6 7class PodcastPlayer extends LitElement { 8 static get properties() { 9 return { 10 currentTime: { type: String }, 11 currentSpeedIdx: { type: Number }, 12 duration: { type: String }, 13 }; 14 } 15 16 static get styles() { 17 return css` 18 .podcast-player { 19 --orange: #cb6633; 20 display: grid; 21 grid-template-columns: 2fr 1fr 2fr; 22 grid-template-rows: 3rem 1.5rem 1.5rem; 23 gap: 0.25rem 2rem; 24 width: 100%; 25 padding: 1rem 0 0.5rem 0; 26 } 27 28 .podcast-player .sr-only { 29 position: absolute; 30 width: 1px; 31 height: 1px; 32 padding: 0; 33 margin: -1px; 34 overflow: hidden; 35 clip: rect(0, 0, 0, 0); 36 border: 0; 37 } 38 39 .podcast-player > button, 40 .podcast-player > span { 41 padding: 0.5rem; 42 display: block; 43 line-height: 1; 44 } 45 46 .postcast-player > span { 47 font-variant: tabular-nums; 48 } 49 50 :focus { 51 outline: 2px solid var(--orange); 52 } 53 54 .podcast-player button { 55 -webkit-appearance: none; 56 font-family: inherit; 57 min-width: 26px; 58 border: 1px solid transparent; 59 background-color: transparent; 60 border-radius: 0; 61 cursor: pointer; 62 color: currentColor; 63 border-radius: 3px; 64 display: inline-grid; 65 place-items: center; 66 } 67 68 .podcast-player button svg { 69 width: min(100%, 2rem); 70 aspect-ratio: 1; 71 } 72 73 .podcast-player .button-play { 74 background: var(--orange); 75 } 76 77 .podcast-player .progress-meter { 78 grid-row: 2; 79 grid-column: 1 / -1; 80 } 81 82 .podcast-player input[type="range"] { 83 -webkit-appearance: none; 84 width: 100%; 85 background: transparent; 86 } 87 88 .podcast-player input[type="range"]::-webkit-slider-runnable-track { 89 width: 100%; 90 height: 0.5rem; 91 cursor: pointer; 92 animate: 0.2s; 93 border: 1px solid #333; 94 border-radius: 3px; 95 } 96 97 .podcast-player input[type="range"]::-webkit-slider-thumb { 98 border: none; 99 height: 1rem; 100 width: 1rem; 101 border-radius: 3px; 102 background: var(--orange); 103 cursor: pointer; 104 -webkit-appearance: none; 105 margin-top: -0.25rem; 106 } 107 108 .podcast-player input[type="range"]::-ms-fill-lower { 109 background: var(--orange); 110 } 111 112 .podcast-player input[type="range"]::-ms-fill-upper { 113 background: white; 114 } 115 116 .podcast-player time { 117 grid-row: 3; 118 } 119 120 .podcast-player .duration { 121 grid-column: 3; 122 justify-self: end; 123 } 124 125 /* Speed */ 126 .podcast-player .button-speed { 127 font-size: 12px; 128 min-width: 3em; 129 grid-column: 2; 130 grid-row: 3; 131 white-space: nowrap; 132 } 133 134 .podcast-player .button-mute { 135 display: none; 136 } 137 138 /* Play/Pause */ 139 .button-play .pause { 140 display: none; 141 } 142 143 :host(.is-playing) .button-play .pause { 144 display: inline; 145 } 146 :host(.is-playing) .button-play .play { 147 display: none; 148 } 149 150 /* Mute/Unmute */ 151 .button-mute .muted { 152 display: none; 153 } 154 155 :host(.is-muted) .button-mute .muted { 156 display: inline; 157 } 158 159 :host(.is-muted) .button-mute .unmuted { 160 display: none; 161 } 162 `; 163 } 164 165 constructor() { 166 super(); 167 168 // HTMLAudioElement 169 this.audio = this.querySelector("audio"); 170 this.audio.controls = false; // remove controls if it has 'em 171 172 this.speeds = [1, 1.25, 1.5, 1.75, 2]; 173 this.currentSpeedIdx = 0; 174 this.currentTime = 0; 175 this.duration = 0; 176 177 this.audio.addEventListener("timeupdate", this.handleTimeUpdate.bind(this)); 178 this.audio.addEventListener( 179 "loadedmetadata", 180 this.handleLoadedMetadata.bind(this) 181 ); 182 183 this.audio.addEventListener("play", () => { 184 this.classList.add("is-playing"); 185 }); 186 187 this.audio.addEventListener("pause", () => { 188 this.classList.remove("is-playing"); 189 }); 190 191 window.addEventListener( 192 "DOMContentLoaded", 193 this.timeJump.bind(this), 194 false 195 ); 196 window.addEventListener("hashchange", this.timeJump.bind(this), false); 197 } 198 199 handleLoadedMetadata() { 200 this.duration = this.audio.duration; 201 } 202 203 handleTimeUpdate(e) { 204 this.currentTime = this.audio.currentTime; 205 } 206 207 timeJump(event) { 208 let params = new URLSearchParams(window.location.hash.substring(1)); 209 let t = params.get("t") || 0; 210 211 var timestamp = this.parseTime(t); 212 213 if (t) { 214 // Preload the media 215 this.audio.setAttribute("preload", "true"); 216 // Set the current time. Will update if playing. Will fail if paused. 217 this.audio.currentTime = timestamp; 218 // If the media is able to play, play. 219 this.audio.addEventListener( 220 "canplay", 221 () => { 222 /* only start the player if it is not already playing */ 223 if (!this.audio.paused) { 224 return false;
225 } 226 227 this.audio.currentTime = timestamp; 228 this.audio.play(); 229 this.classList.add("is-playing"); 230 }, 231 false 232 ); 233 } 234 } 235 236 parseTime(str) { 237 var plain = /^\d+(\.\d+)?$/g, 238 npt = /^(?:npt:)?(?:(?:(\d+):)?(\d\d?):)?(\d\d?)(\.\d+)?$/, 239 quirks = /^(?:(\d\d?)[hH])?(?:(\d\d?)[mM])?(\d\d?)[sS]$/, 240 match; 241 if (plain.test(str)) { 242 return parseFloat(str); 243 } 244 match = npt.exec(str) || quirks.exec(str); 245 if (match) { 246 return ( 247 3600 * (parseInt(match[1], 10) || 0) + 248 60 * (parseInt(match[2], 10) || 0) + 249 parseInt(match[3], 10) + 250 (parseFloat(match[4]) || 0) 251 ); 252 } 253 return 0; 254 } 255 256 toHHMMSS(totalsecs) { 257 var sec_num = parseInt(totalsecs, 10); // don't forget the second param 258 var hours = Math.floor(sec_num / 3600); 259 var minutes = Math.floor((sec_num - hours * 3600) / 60); 260 var seconds = sec_num - hours * 3600 - minutes * 60; 261 262 if (hours < 10) { 263 hours = "0" + hours; 264 } 265 if (minutes < 10) { 266 minutes = "0" + minutes; 267 } 268 if (seconds < 10) { 269 seconds = "0" + seconds; 270 } 271 272 hours = hours > 0 ? hours + ":" : ""; 273 minutes = minutes + ":"; 274 275 var time = hours + minutes + seconds; 276 return time; 277 } 278 279 changeSpeed() { 280 this.currentSpeedIdx = 281 this.currentSpeedIdx + 1 < this.speeds.length 282 ? this.currentSpeedIdx + 1 283 : 0; 284 this.audio.playbackRate = this.speeds[this.currentSpeedIdx]; 285 } 286 287 mute() { 288 this.audio.muted = !this.audio.muted; 289 this.classList.toggle("is-muted", this.audio.muted); 290 } 291 292 play() { 293 if (this.audio.paused) { 294 this.audio.play(); 295 } else { 296 this.audio.pause(); 297 } 298 this.classList.toggle("is-playing", !this.audio.paused); 299 } 300 301 rewind() { 302 this.audio.currentTime -= 30; 303 } 304 305 ff() { 306 this.audio.currentTime += 30; 307 } 308 309 seek(e) { 310 this.audio.currentTime = e.target.value; 311 } 312 313 render() { 314 return html` 315 <slot></slot> 316 <svg style="display: none;"> 317 <symbol id="icon-play" viewBox="0 0 30.406 46.843"> 318 <path 319 d="M977.571,603.068l-2.828-2.4,2.828-2.4,2.828,2.4Zm-3.535,3-2.829-2.4,2.829-2.4,2.828,2.4Zm-3.536,3-2.828-2.4,2.828-2.4,2.828,2.4Zm10.607-9.01-2.829-2.4,2.829-2.4,2.828,2.4Zm3.535-3-2.828-2.4,2.828-2.4,2.829,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm3.535-3-2.828-2.4,2.828-2.4,2.829,2.4Zm-17.677,9.01-2.829-2.4,2.829-2.4,2.828,2.4Zm-3.536,3-2.828-2.4,2.828-2.4,2.828,2.4Zm7.071-6.006-2.828-2.4,2.828-2.4,2.828,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm3.535-3-2.828-2.4,2.828-2.4,2.829,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm-17.678,9.01-2.828-2.4,2.828-2.4,2.828,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm3.535-3-2.828-2.4,2.828-2.4,2.828,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm3.535-3-2.828-2.4,2.828-2.4,2.829,2.4ZM970.5,591.055l-2.828-2.4,2.828-2.4,2.828,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm3.535-3-2.828-2.4,2.828-2.4,2.828,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm-10.607,3-2.828-2.4,2.828-2.4,2.828,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm3.535-3-2.828-2.4,2.828-2.4,2.828,2.4Zm-7.071,0-2.828-2.4,2.828-2.4,2.828,2.4Zm3.536-3-2.829-2.4,2.829-2.4,2.828,2.4Zm21.213,12.013-2.829-2.4,2.829-2.4,2.828,2.4Zm-3.536-3-2.828-2.4,2.828-2.4,2.829,2.4Zm-3.535-3-2.829-2.4,2.829-2.4,2.828,2.4Zm-3.536-3-2.828-2.4,2.828-2.4,2.829,2.4Zm-3.535-3-2.829-2.4,2.829-2.4,2.828,2.4Zm-3.536-3-2.828-2.4,2.828-2.4,2.828,2.4Zm-3.535-3-2.829-2.4,2.829-2.4,2.828,2.4Zm-3.536-3-2.828-2.4,2.828-2.4,2.828,2.4Zm0,6.007-2.828-2.4,2.828-2.4,2.828,2.4Z" 320 transform="translate(-967.656 -562.219)" 321 /> 322 </symbol> 323 <symbol id="icon-pause" viewBox="0 0 32 32"> 324 <path 325 fill="currentColor" 326 d="M4 4 H12 V28 H4 z M20 4 H28 V28 H20 z " 327 ></path> 328 </symbol> 329 <symbol id="icon-rewind" viewBox="0 0 32 32"> 330 <path 331 fill="currentColor" 332 d="M4 4 H8 V14 L28 4 V28 L8 18 V28 H4 z " 333 ></path> 334 </symbol>
335 <symbol id="icon-speaker-unmuted" viewBox="0 0 32 32"> 336 <path 337 fill="currentColor" 338 d="M2 12 L8 12 L16 6 L16 26 L8 20 L2 20 z M32 16 A16 16 0 0 1 27.25 27.375 L25.25 25.25 A13 13 0 0 0 29 16 A13 13 0 0 0 25.25 6.75 L27.25 4.625 A16 16 0 0 1 32 16 M25 16 A9 9 0 0 1 22.375 22.375 L20.25 20.25 A6 6 0 0 0 22 16 A6 6 0 0 0 20.25 11.75 L22.375 9.625 A9 9 0 0 1 25 16 " 339 ></path> 340 </symbol> 341 <symbol id="icon-speaker-muted" viewBox="0 0 32 32"> 342 <path 343 fill="currentColor" 344 d="M2 12 L8 12 L16 6 L16 26 L8 20 L2 20 z " 345 ></path> 346 </symbol> 347 </svg> 348 349 <div class="podcast-player"> 350 <button 351 class="button-rewind" 352 aria-label="Rewind 30 seconds" 353 @click="${this.rewind}" 354 > 355 â 30 356 </button> 357 358 <button class="button-play" aria-label="Play" @click="${this.play}"> 359 <svg class="play"><use xlink:href="#icon-play"></use></svg> 360 <svg class="pause"><use xlink:href="#icon-pause"></use></svg> 361 </button> 362 363 <button 364 class="button-ff" 365 aria-label="Fast Forward 30 seconds" 366 @click="${this.ff}" 367 > 368 30 â 369 </button> 370 371 <span class="sr-only">Current Time</span> 372 <span class="currenttime time">${this.toHHMMSS(this.currentTime)}</span> 373 <input 374 type="range" 375 class="progress-meter" 376 value="${this.currentTime}" 377 max="${this.duration}" 378 @click="${this.seek}" 379 /> 380 <span class="sr-only">Duration</span> 381 <span class="duration time">${this.toHHMMSS(this.duration)}</span> 382 383 <button class="button-speed" @click="${this.changeSpeed}"> 384 ${this.speeds[this.currentSpeedIdx]}Ã 385 </button> 386 387 <button class="button-mute" aria-label="Mute" @click="${this.mute}"> 388 <svg class="unmuted"> 389 <use xlink:href="#icon-speaker-unmuted"></use> 390 </svg> 391 <svg class="muted"><use xlink:href="#icon-speaker-muted"></use></svg> 392 </button> 393 </div> 394 `; 395 } 396} 397 398customElements.define("podcast-player", PodcastPlayer);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.