195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
class Radiostasis {
|
|
// audio player
|
|
private readonly player: Player;
|
|
private readonly playlist: Playlist;
|
|
|
|
// ui element
|
|
private readonly main: HTMLElement;
|
|
|
|
private debouncer: number | null = null;
|
|
|
|
constructor() {
|
|
this.playlist = new Playlist(() => this.player);
|
|
this.player = new Player(this.playlist);
|
|
this.playlist.initialize();
|
|
|
|
this.main = getOrThrow(document.getElementsByTagName('main').item(0));
|
|
|
|
// set up htmx event handlers
|
|
document.addEventListener('htmx:historyRestore', () =>
|
|
this.wireLoadedFragment()
|
|
);
|
|
document
|
|
.getElementsByTagName('main')
|
|
.item(0)
|
|
?.addEventListener('htmx:afterSwap', () => this.wireLoadedFragment());
|
|
|
|
// set up playlist changed handler
|
|
this.playlist.addPlaylistChangedHandler(() => this.episodeStateChanged());
|
|
|
|
// set up prompt to prevent accidentally leaving the page while playing
|
|
window.addEventListener('beforeunload', (e) => {
|
|
if (!this.player.isPlaying()) return undefined;
|
|
e.preventDefault();
|
|
e.returnValue = '';
|
|
});
|
|
}
|
|
|
|
public initialize(): void {
|
|
const path =
|
|
location.pathname == '/'
|
|
? '/partial/home.html'
|
|
: `/partial/${location.pathname}.html`;
|
|
htmx.ajax('GET', path, 'main');
|
|
}
|
|
|
|
private wireLoadedFragment(): void {
|
|
// save a list of all episodes for the series play/queue buttons
|
|
const seriesEpisodes: Array<Episode> = [];
|
|
|
|
// episode play and queue buttons
|
|
const episodes = this.main.getElementsByClassName('episode');
|
|
for (let i = 0; i < episodes.length; i++) {
|
|
const el = <HTMLLIElement>episodes.item(i);
|
|
if (!el || !el.dataset.episode) continue;
|
|
|
|
const episode = <Episode>JSON.parse(el.dataset.episode);
|
|
|
|
// save and store this model for later use
|
|
seriesEpisodes.push(episode);
|
|
|
|
// play button
|
|
el.getElementsByTagName('a')
|
|
.item(0)
|
|
?.addEventListener('click', (e) => {
|
|
if (this.player.currentEpisode()?.id !== episode.id) {
|
|
this.playlist.reQueueNowPlaying();
|
|
this.player.playEpisode(episode);
|
|
}
|
|
e.preventDefault();
|
|
});
|
|
|
|
// queue button
|
|
el.getElementsByTagName('a')
|
|
.item(1)
|
|
?.addEventListener('click', (e) => {
|
|
if (!this.player.currentEpisode()) {
|
|
// if nothing is playing, first queued item gets loaded
|
|
// into the player but does not autoplay
|
|
this.player.playEpisode(episode, true);
|
|
} else if (this.player.currentEpisode()?.id !== episode.id) {
|
|
if (this.playlist.isQueued(episode)) {
|
|
this.playlist.removeEpisode(episode);
|
|
} else {
|
|
this.playlist.pushEpisode(episode);
|
|
}
|
|
}
|
|
e.preventDefault();
|
|
});
|
|
|
|
// set playing/queued state
|
|
this.updateEpisodeElementFromState(el, episode);
|
|
}
|
|
|
|
// series play and queue buttons
|
|
const series = document
|
|
.getElementsByClassName('seriesDetails')
|
|
.item(0)
|
|
?.getElementsByTagName('section')
|
|
.item(0);
|
|
|
|
if (series) {
|
|
// play series button
|
|
series
|
|
.getElementsByTagName('a')
|
|
.item(0)
|
|
?.addEventListener('click', () => {
|
|
this.playlist.unshiftEpisodes(seriesEpisodes);
|
|
if (this.player.currentEpisode()?.id !== seriesEpisodes[0].id) {
|
|
this.player.playEpisode(seriesEpisodes[0]);
|
|
} else {
|
|
this.playlist.removeEpisode(seriesEpisodes[0]);
|
|
}
|
|
});
|
|
|
|
// queue series button
|
|
series
|
|
.getElementsByTagName('a')
|
|
.item(1)
|
|
?.addEventListener('click', () => {
|
|
const toQueue = seriesEpisodes.filter(
|
|
(e) => this.player.currentEpisode()?.id !== e.id
|
|
);
|
|
this.playlist.pushEpisodes(toQueue);
|
|
// if nothing is playing, peel off the first episode and load it
|
|
// into the player without autoplay
|
|
if (!this.player.currentEpisode()) {
|
|
this.player.playEpisode(seriesEpisodes[0], true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// series filter input
|
|
const filter = <HTMLInputElement>(
|
|
this.main.getElementsByClassName('filter').item(0)
|
|
);
|
|
|
|
if (filter) {
|
|
filter.value = filter.dataset.search ?? '';
|
|
const allSeries = this.main.getElementsByTagName('section');
|
|
filter.addEventListener('input', () => {
|
|
filter.dataset.search = filter.value;
|
|
if (this.debouncer) clearTimeout(this.debouncer);
|
|
this.debouncer = setTimeout(() => {
|
|
const terms = filter.value.toLowerCase().split(' ');
|
|
for (let i = 0; i < allSeries.length; i++) {
|
|
const series = allSeries.item(i);
|
|
if (!series || !series.dataset.filter) continue;
|
|
let match = true;
|
|
for (const term of terms) {
|
|
if (term.length > 0 && series.dataset.filter.indexOf(term) < 0) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) series.classList.remove('no-match');
|
|
else series.classList.add('no-match');
|
|
}
|
|
}, 499);
|
|
});
|
|
}
|
|
}
|
|
|
|
private updateEpisodeElementFromState(
|
|
litem: HTMLLIElement,
|
|
episode: Episode
|
|
): void {
|
|
if (this.player.currentEpisode()?.id === episode.id) {
|
|
litem.classList.add('playing');
|
|
getOrThrow(litem.getElementsByTagName('a').item(0)).innerHTML = 'Playing';
|
|
} else {
|
|
litem.classList.remove('playing');
|
|
getOrThrow(litem.getElementsByTagName('a').item(0)).innerHTML =
|
|
'Play Episode';
|
|
}
|
|
const queueBtn = getOrThrow(litem.getElementsByTagName('a').item(1));
|
|
if (this.playlist.isQueued(episode)) {
|
|
queueBtn.classList.add('queued');
|
|
queueBtn.innerHTML = 'Remove from Queue';
|
|
} else {
|
|
queueBtn.classList.remove('queued');
|
|
queueBtn.innerHTML = 'Queue Episode';
|
|
}
|
|
}
|
|
|
|
private episodeStateChanged(): void {
|
|
const episodes = this.main.getElementsByClassName('episode');
|
|
for (let i = 0; i < episodes.length; i++) {
|
|
const el = <HTMLLIElement>episodes.item(i);
|
|
if (!el || !el.dataset.episode) continue;
|
|
const episode = <Episode>JSON.parse(el.dataset.episode);
|
|
this.updateEpisodeElementFromState(el, episode);
|
|
}
|
|
}
|
|
}
|