287 lines
8.8 KiB
TypeScript
287 lines
8.8 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 playerDelegate: () => Player;
|
|
|
|
// the actual episode queue
|
|
private readonly episodes: Array<[Episode, HTMLLIElement]> = [];
|
|
private readonly episodeHash: Set<string> = new Set<string>();
|
|
|
|
constructor(playerDelegate: () => Player) {
|
|
this.playerDelegate = playerDelegate;
|
|
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 initialize(): void {
|
|
this.playerDelegate().addStateChangedHandler(() => this.stateChanged());
|
|
this.unshiftCurrent();
|
|
}
|
|
|
|
public addPlaylistChangedHandler(handler: () => void): void {
|
|
this.changedHandlers.push(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, false);
|
|
const litem = this.createQueueListItem(episode);
|
|
this.episodes.push([episode, litem]);
|
|
this.episodeHash.add(episode.id);
|
|
this.queueList.appendChild(litem);
|
|
}
|
|
this.notify(
|
|
episodes.length == 1
|
|
? 'Episode added to queue.'
|
|
: `${episodes.length} episodes added to queue.`
|
|
);
|
|
this.playlistChanged();
|
|
}
|
|
|
|
public unshiftEpisodes(episodes: Array<Episode>): void {
|
|
this.shiftCurrent();
|
|
const nowPlaying = this.playerDelegate().currentEpisode();
|
|
if (nowPlaying) {
|
|
const litem = this.createQueueListItem(nowPlaying);
|
|
this.episodes.unshift([nowPlaying, litem]);
|
|
this.episodeHash.add(nowPlaying.id);
|
|
this.queueList.prepend(litem);
|
|
}
|
|
for (let i = episodes.length - 1; i >= 0; i--) {
|
|
const episode = episodes[i];
|
|
if (this.isQueued(episode)) this.removeEpisode(episode, true, false);
|
|
const litem = this.createQueueListItem(episode);
|
|
this.episodes.unshift([episode, litem]);
|
|
this.episodeHash.add(episode.id);
|
|
this.queueList.prepend(litem);
|
|
}
|
|
this.notify(
|
|
episodes.length == 1
|
|
? 'Episode added to queue.'
|
|
: `${episodes.length} episodes added to queue.`
|
|
);
|
|
this.unshiftCurrent();
|
|
this.playlistChanged();
|
|
}
|
|
|
|
public removeEpisode(
|
|
episode: Episode,
|
|
ignoreChanged = false,
|
|
notify = true
|
|
): 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();
|
|
if (notify) this.notify('Episode removed from queue.');
|
|
}
|
|
|
|
public isQueued(episode: Episode): boolean {
|
|
return this.episodeHash.has(episode.id);
|
|
}
|
|
|
|
public triggerPlaylistChanged(): void {
|
|
this.playlistChanged();
|
|
}
|
|
|
|
public clearPlaylist(): void {
|
|
let removed = this.episodes.length;
|
|
if (this.playerDelegate().currentEpisode() != null) {
|
|
this.playerDelegate().stopPlaybackAndResetUi();
|
|
removed++;
|
|
}
|
|
|
|
this.episodes.length = 0;
|
|
this.episodeHash.clear();
|
|
this.queueList.innerHTML = '';
|
|
this.unshiftCurrent();
|
|
this.playlistChanged();
|
|
if (removed > 0) {
|
|
this.notify('Playlist cleared.');
|
|
}
|
|
}
|
|
|
|
public reQueueNowPlaying(): void {
|
|
const currentEpisode = this.playerDelegate().currentEpisode();
|
|
if (currentEpisode) {
|
|
this.unshiftEpisodes([currentEpisode]);
|
|
}
|
|
}
|
|
|
|
private playlistChanged(): void {
|
|
this.shiftCurrent();
|
|
this.unshiftCurrent();
|
|
for (const handler of this.changedHandlers) {
|
|
handler();
|
|
}
|
|
}
|
|
|
|
private shiftCurrent(): void {
|
|
this.queueList.firstChild?.remove();
|
|
}
|
|
|
|
private stateChanged(): void {
|
|
this.shiftCurrent();
|
|
this.unshiftCurrent();
|
|
}
|
|
|
|
private unshiftCurrent(): void {
|
|
const currentEpisode = this.playerDelegate().currentEpisode();
|
|
if (currentEpisode) {
|
|
this.queueList.prepend(this.createQueueListItem(currentEpisode, false));
|
|
} else {
|
|
const litem = document.createElement('li');
|
|
litem.classList.add('episode');
|
|
litem.title = 'No episode playing';
|
|
const label = document.createElement('label');
|
|
label.innerHTML = 'No episode playing';
|
|
litem.appendChild(label);
|
|
this.queueList.prepend(litem);
|
|
}
|
|
}
|
|
|
|
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';
|
|
this.queueTab.innerHTML = 'Close';
|
|
this.queueTab.classList.add('expanded');
|
|
} 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';
|
|
this.queueTab.innerHTML = 'Playlist';
|
|
this.queueTab.classList.remove('expanded');
|
|
}
|
|
}
|
|
|
|
private createQueueListItem(
|
|
episode: Episode,
|
|
queueControls = true
|
|
): 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);
|
|
item.appendChild(label);
|
|
item.appendChild(aside);
|
|
if (queueControls) {
|
|
// play now/remove buttons
|
|
const controls = document.createElement('div');
|
|
controls.classList.add('controls');
|
|
const playBtn = document.createElement('a');
|
|
playBtn.href = '#';
|
|
playBtn.innerHTML = 'Play Now';
|
|
playBtn.addEventListener('click', (e) => {
|
|
this.reQueueNowPlaying();
|
|
this.playerDelegate().playEpisode(episode);
|
|
e.preventDefault();
|
|
});
|
|
const remBtn = document.createElement('a');
|
|
remBtn.href = '#';
|
|
remBtn.innerHTML = 'Remove';
|
|
remBtn.addEventListener('click', (e) => {
|
|
this.removeEpisode(episode);
|
|
e.preventDefault();
|
|
});
|
|
controls.appendChild(playBtn);
|
|
controls.appendChild(remBtn);
|
|
item.appendChild(controls);
|
|
} else {
|
|
// pause/skip buttons
|
|
item.classList.add('playing');
|
|
const controls = document.createElement('div');
|
|
controls.classList.add('controls');
|
|
const pauseBtn = document.createElement('a');
|
|
pauseBtn.href = '#';
|
|
pauseBtn.innerHTML = this.playerDelegate().isPlaying() ? 'Pause' : 'Play';
|
|
if (this.playerDelegate().isPlaying()) {
|
|
pauseBtn.classList.add('pause');
|
|
}
|
|
pauseBtn.addEventListener('click', () => {
|
|
this.playerDelegate().playPause();
|
|
pauseBtn.innerHTML = this.playerDelegate().isPlaying()
|
|
? 'Pause'
|
|
: 'Play';
|
|
});
|
|
const skipBtn = document.createElement('a');
|
|
skipBtn.href = '#';
|
|
skipBtn.innerHTML = 'Skip';
|
|
skipBtn.addEventListener('click', (e) => {
|
|
this.playerDelegate().nextEpisode();
|
|
e.preventDefault();
|
|
});
|
|
controls.appendChild(pauseBtn);
|
|
controls.appendChild(skipBtn);
|
|
item.appendChild(controls);
|
|
}
|
|
return item;
|
|
}
|
|
|
|
private notify(message: string): void {
|
|
Toastify({
|
|
text: message,
|
|
gravity: 'bottom',
|
|
position: 'right',
|
|
stopOnFocus: false,
|
|
style: {
|
|
marginBottom: '10ex',
|
|
background: '#090',
|
|
color: '#fff',
|
|
},
|
|
}).showToast();
|
|
}
|
|
}
|