mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 04:51:17 +00:00
278 lines
6.7 KiB
TypeScript
278 lines
6.7 KiB
TypeScript
import { css, html, LitElement, TemplateResult } from "lit";
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
|
|
@customElement("play-episode-button")
|
|
export class PlayEpisodeButton extends LitElement {
|
|
@property()
|
|
id = "0";
|
|
|
|
@property()
|
|
src = "";
|
|
|
|
@property()
|
|
mediaType = "";
|
|
|
|
@property()
|
|
title!: string;
|
|
|
|
@property()
|
|
podcast!: string;
|
|
|
|
@property()
|
|
imageSrc!: string;
|
|
|
|
@property()
|
|
playLabel!: string;
|
|
|
|
@property()
|
|
playingLabel!: string;
|
|
|
|
@property({ attribute: false })
|
|
_castopodAudioPlayer!: HTMLDivElement;
|
|
|
|
@property({ attribute: false })
|
|
_audio!: HTMLAudioElement;
|
|
|
|
@state()
|
|
isPlaying!: boolean;
|
|
|
|
@state()
|
|
_playbackSpeed = 1;
|
|
|
|
@state()
|
|
_events = [
|
|
{
|
|
name: "canplay",
|
|
onEvent: (event: Event): void => {
|
|
(event.target as HTMLAudioElement)?.play();
|
|
},
|
|
},
|
|
{
|
|
name: "play",
|
|
onEvent: (): void => {
|
|
this.isPlaying = true;
|
|
},
|
|
},
|
|
{
|
|
name: "pause",
|
|
onEvent: (): void => {
|
|
this.isPlaying = false;
|
|
},
|
|
},
|
|
{
|
|
name: "ratechange",
|
|
onEvent: (event: Event): void => {
|
|
this._playbackSpeed = (event.target as HTMLAudioElement)?.playbackRate;
|
|
},
|
|
},
|
|
];
|
|
|
|
async connectedCallback(): Promise<void> {
|
|
super.connectedCallback();
|
|
|
|
await this._elementReady("div[id=castopod-audio-player]");
|
|
await this._elementReady("div[id=castopod-audio-player] audio");
|
|
|
|
this._castopodAudioPlayer = document.body.querySelector(
|
|
"div[id=castopod-audio-player]"
|
|
) as HTMLDivElement;
|
|
|
|
this._audio = this._castopodAudioPlayer.querySelector(
|
|
"audio"
|
|
) as HTMLAudioElement;
|
|
}
|
|
|
|
private _elementReady(selector: string) {
|
|
return new Promise((resolve) => {
|
|
const element = document.querySelector(selector);
|
|
if (element) {
|
|
resolve(element);
|
|
}
|
|
new MutationObserver((_, observer) => {
|
|
// Query for elements matching the specified selector
|
|
Array.from(document.querySelectorAll(selector)).forEach((element) => {
|
|
resolve(element);
|
|
//Once we have resolved we don't need the observer anymore.
|
|
observer.disconnect();
|
|
});
|
|
}).observe(document.documentElement, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
});
|
|
}
|
|
|
|
play(): void {
|
|
const currentlyPlayingEpisode = this._castopodAudioPlayer.dataset.episode;
|
|
|
|
const isCurrentEpisode = currentlyPlayingEpisode === this.id;
|
|
|
|
if (currentlyPlayingEpisode === "-1") {
|
|
this._showPlayer();
|
|
}
|
|
|
|
if (isCurrentEpisode) {
|
|
this._audio.play();
|
|
} else {
|
|
const playingEpisodeButton = document.querySelector(
|
|
`play-episode-button[id="${currentlyPlayingEpisode}"]`
|
|
) as PlayEpisodeButton;
|
|
if (playingEpisodeButton) {
|
|
this._flushLastPlayButton(playingEpisodeButton);
|
|
}
|
|
|
|
this._loadEpisode();
|
|
}
|
|
}
|
|
|
|
pause(): void {
|
|
this._audio.pause();
|
|
}
|
|
|
|
private _showPlayer(): void {
|
|
this._castopodAudioPlayer.style.display = "";
|
|
document.body.classList.add("pb-[105px]", "sm:pb-[52px]");
|
|
}
|
|
|
|
private _flushLastPlayButton(playingEpisodeButton: PlayEpisodeButton): void {
|
|
playingEpisodeButton.isPlaying = false;
|
|
|
|
for (const event of playingEpisodeButton._events) {
|
|
playingEpisodeButton._audio.removeEventListener(
|
|
event.name,
|
|
event.onEvent,
|
|
false
|
|
);
|
|
}
|
|
|
|
this._playbackSpeed = playingEpisodeButton._playbackSpeed;
|
|
}
|
|
|
|
private _loadEpisode(): void {
|
|
this._castopodAudioPlayer.dataset.episode = this.id;
|
|
|
|
this._audio.src = this.src;
|
|
this._audio.load();
|
|
this._audio.playbackRate = this._playbackSpeed;
|
|
for (const event of this._events) {
|
|
this._audio.addEventListener(event.name, event.onEvent, false);
|
|
}
|
|
|
|
const img: HTMLImageElement | null =
|
|
this._castopodAudioPlayer.querySelector("img");
|
|
|
|
if (img) {
|
|
img.src = this.imageSrc;
|
|
img.alt = this.title;
|
|
}
|
|
|
|
const episodeTitle: HTMLParagraphElement | null =
|
|
this._castopodAudioPlayer.querySelector('p[id="castopod-player-title"]');
|
|
|
|
if (episodeTitle) {
|
|
episodeTitle.title = this.title;
|
|
episodeTitle.innerHTML = this.title;
|
|
}
|
|
|
|
const podcastTitle: HTMLParagraphElement | null =
|
|
this._castopodAudioPlayer.querySelector(
|
|
'p[id="castopod-player-podcast"]'
|
|
);
|
|
|
|
if (podcastTitle) {
|
|
podcastTitle.title = this.podcast;
|
|
podcastTitle.innerHTML = this.podcast;
|
|
}
|
|
}
|
|
|
|
static styles = css`
|
|
button {
|
|
background-color: hsl(var(--color-accent-base));
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.5rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
line-height: 1.25rem;
|
|
border: 2px solid transparent;
|
|
border-radius: 9999px;
|
|
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
button:hover {
|
|
background-color: hsl(var(--color-accent-hover));
|
|
}
|
|
|
|
button:focus {
|
|
outline: none;
|
|
box-shadow:
|
|
0 0 0 2px hsl(var(--color-background-base)),
|
|
0 0 0 4px hsl(var(--color-accent-base));
|
|
}
|
|
|
|
button.playing {
|
|
background-color: hsl(var(--color-background-base));
|
|
border: 2px solid hsl(var(--color-accent-base));
|
|
}
|
|
|
|
button.playing:hover {
|
|
background-color: hsl(var(--color-background-elevated));
|
|
}
|
|
|
|
button.playing svg {
|
|
color: hsl(var(--color-accent-base));
|
|
}
|
|
|
|
svg {
|
|
font-size: 1.5rem;
|
|
color: hsl(var(--color-accent-contrast));
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.animate-spin {
|
|
animation: spin 3s linear infinite;
|
|
}
|
|
`;
|
|
|
|
render(): TemplateResult<1> {
|
|
return html`<button
|
|
class="${this.isPlaying ? "playing" : ""}"
|
|
@click="${this.isPlaying ? this.pause : this.play}"
|
|
title="${this.isPlaying ? this.playingLabel : this.playLabel}"
|
|
>
|
|
${this.isPlaying
|
|
? html`<svg
|
|
class="animate-spin"
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
width="1em"
|
|
height="1em"
|
|
>
|
|
<g>
|
|
<path fill="none" d="M0 0h24v24H0z" />
|
|
<path
|
|
d="M13 9.17A3 3 0 1 0 15 12V2.458c4.057 1.274 7 5.064 7 9.542 0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2c.337 0 .671.017 1 .05v7.12z"
|
|
/>
|
|
</g>
|
|
</svg>`
|
|
: html`<svg
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
width="1em"
|
|
height="1em"
|
|
>
|
|
<path fill="none" d="M0 0h24v24H0z" />
|
|
<path
|
|
d="M7.752 5.439l10.508 6.13a.5.5 0 0 1 0 .863l-10.508 6.13A.5.5 0 0 1 7 18.128V5.871a.5.5 0 0 1 .752-.432z"
|
|
/>
|
|
</svg>`}
|
|
</button>`;
|
|
}
|
|
}
|