PageSourceSearch

https://shoptalkshow.com/wp-content/themes/shoptalk6/javascripts/podcast-player.js?v1.5

js shoptalkshow.com collected 2026-10-02 02:17:27 UTC 11,449 bytes, 398 lines download raw bytes

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.