162 lines
5.3 KiB
TypeScript
162 lines
5.3 KiB
TypeScript
class Playlist {
|
|
private readonly queueContainer: HTMLElement;
|
|
private readonly queueTab: HTMLElement;
|
|
private readonly overlay: HTMLElement;
|
|
private readonly queueList: HTMLOListElement;
|
|
private readonly clearButton: HTMLAnchorElement;
|
|
|
|
private readonly queueInitialHeight: string;
|
|
private readonly queueExpandedHeight = 'calc(100% - 6ex)';
|
|
|
|
// event handlers
|
|
private changedHandlers: Array<() => void> = [];
|
|
private playEpisodeHandler: ((episode: Episode) => void) | null = null;
|
|
|
|
// the actual episode queue
|
|
private readonly episodes: Array<[Episode, HTMLLIElement]> = [];
|
|
private readonly episodeHash: Set<string> = new Set<string>();
|
|
|
|
constructor() {
|
|
this.queueContainer = getOrThrow(
|
|
document.getElementById('queue-container')
|
|
);
|
|
this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
|
|
this.queueTab = getOrThrow(
|
|
this.queueContainer.getElementsByTagName('h2').item(0)
|
|
);
|
|
this.queueList = getOrThrow(
|
|
this.queueContainer.getElementsByTagName('ol').item(0)
|
|
);
|
|
this.overlay = getOrThrow(document.getElementById('overlay'));
|
|
this.clearButton = getOrThrow(
|
|
<HTMLAnchorElement>document.getElementById('clear-playlist')
|
|
);
|
|
|
|
// wire up global playlist controls
|
|
this.queueTab.addEventListener('click', () => this.toggleQueueUI());
|
|
this.overlay.addEventListener('click', () => this.toggleQueueUI());
|
|
this.clearButton.addEventListener('click', () => this.clearPlaylist());
|
|
}
|
|
|
|
public addPlaylistChangedHandler(handler: () => void): void {
|
|
this.changedHandlers.push(handler);
|
|
}
|
|
|
|
public setPlayEpisodeHandler(handler: (episode: Episode) => void): void {
|
|
this.playEpisodeHandler = handler;
|
|
}
|
|
|
|
public hasNextEpisode(): boolean {
|
|
return this.episodes.length > 0;
|
|
}
|
|
|
|
public nextEpisode(): Episode | null {
|
|
if (this.episodes.length > 0) {
|
|
return this.episodes[0][0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public pushEpisode(episode: Episode): void {
|
|
this.pushEpisodes([episode]);
|
|
}
|
|
|
|
public pushEpisodes(episodes: Array<Episode>): void {
|
|
for (const episode of episodes) {
|
|
if (this.isQueued(episode)) this.removeEpisode(episode, true);
|
|
const litem = this.createQueueListItem(episode);
|
|
this.episodes.push([episode, litem]);
|
|
this.episodeHash.add(episode.id);
|
|
this.queueList.appendChild(litem);
|
|
}
|
|
this.playlistChanged();
|
|
}
|
|
|
|
public unshiftEpisodes(episodes: Array<Episode>): void {
|
|
for (let i = episodes.length - 1; i >= 0; i--) {
|
|
const episode = episodes[i];
|
|
if (this.isQueued(episode)) this.removeEpisode(episode, true);
|
|
const litem = this.createQueueListItem(episode);
|
|
this.episodes.unshift([episode, litem]);
|
|
this.episodeHash.add(episode.id);
|
|
this.queueList.prepend(litem);
|
|
}
|
|
this.playlistChanged();
|
|
}
|
|
|
|
public removeEpisode(episode: Episode, ignoreChanged = false): void {
|
|
if (this.isQueued(episode)) {
|
|
const idx = this.episodes.findIndex((e) => e[0].id == episode.id);
|
|
const deleted = this.episodes.splice(idx, 1);
|
|
this.episodeHash.delete(episode.id);
|
|
deleted[0][1].remove();
|
|
}
|
|
if (!ignoreChanged) this.playlistChanged();
|
|
}
|
|
|
|
public isQueued(episode: Episode): boolean {
|
|
return this.episodeHash.has(episode.id);
|
|
}
|
|
|
|
public triggerPlaylistChanged(): void {
|
|
this.playlistChanged();
|
|
}
|
|
|
|
public clearPlaylist(): void {
|
|
this.episodes.length = 0;
|
|
this.episodeHash.clear();
|
|
this.queueList.innerHTML = '';
|
|
this.playlistChanged();
|
|
}
|
|
|
|
private playlistChanged(): void {
|
|
for (const handler of this.changedHandlers) {
|
|
handler();
|
|
}
|
|
}
|
|
|
|
private toggleQueueUI(): void {
|
|
if (this.queueContainer.style.height !== this.queueExpandedHeight) {
|
|
this.queueContainer.style.height = this.queueExpandedHeight;
|
|
this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.75)';
|
|
this.overlay.style.backdropFilter = 'blur(5px)';
|
|
this.overlay.style.pointerEvents = 'auto';
|
|
} else {
|
|
this.queueContainer.style.height = this.queueInitialHeight;
|
|
this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0)';
|
|
this.overlay.style.backdropFilter = 'none';
|
|
this.overlay.style.pointerEvents = 'none';
|
|
}
|
|
}
|
|
|
|
private createQueueListItem(episode: Episode): HTMLLIElement {
|
|
const item = document.createElement('li');
|
|
item.classList.add('episode');
|
|
item.title = episode.title;
|
|
const label = document.createElement('label');
|
|
label.innerHTML = episode.title;
|
|
const sspan = document.createElement('span');
|
|
sspan.innerHTML = episode.series.title;
|
|
const aside = document.createElement('aside');
|
|
aside.appendChild(sspan);
|
|
const controls = document.createElement('div');
|
|
controls.classList.add('controls');
|
|
const playBtn = document.createElement('a');
|
|
playBtn.href = '#';
|
|
playBtn.innerHTML = 'Play Now';
|
|
playBtn.addEventListener('click', () => {
|
|
if (this.playEpisodeHandler) this.playEpisodeHandler(episode);
|
|
});
|
|
const remBtn = document.createElement('a');
|
|
remBtn.href = '#';
|
|
remBtn.innerHTML = 'Remove';
|
|
remBtn.addEventListener('click', () => this.removeEpisode(episode));
|
|
controls.appendChild(playBtn);
|
|
controls.appendChild(remBtn);
|
|
item.appendChild(label);
|
|
item.appendChild(aside);
|
|
item.appendChild(controls);
|
|
return item;
|
|
}
|
|
}
|