audioshowkit/src/player/MusicPlaylist.js

251 lines
7.7 KiB
JavaScript

import "../styles/music-playlist.css";
import Music from "./Music.js";
/**
* A playlist that holds a multitude of musics in the form of {@link Music}.
*/
export default class MusicPlaylist {
#list = [];
#name;
#current = 0;
#positionChangeListeners = [];
#lengthChangeListeners = [];
/**
* @callback MusicPlaylist~positionChangeListener
* @param {number} old the previous position.
* @param {number} current the the current (new) position.
*/
/**
* @callback MusicPlaylist~lengthChangeListener
* @param {number} changed The index of the music that has changed.
* @param {boolean} added True if and only if the change was caused by adding music.
* @param {Music} music The music that was added or removed.
*/
/**
* Instantiates a playlist for musics.
*
* @param {string} name The name of the playlist.
*/
constructor(name) {
this.#name = name;
}
/**
* @returns {string} The name of the playlist.
*/
get name() {
return this.#name;
}
/**
*
* @param {number} index the index of the music to retrieve.
* @returns {Music} the music at the given index.
*/
musicAtIndex(index) {
if (index >= this.total) {
return null;
}
return this.#list[index];
}
/**
* Automatically creates and adds a {@link Music} to this playlist.
*
* @param {string} url where the audio data can be found.
* @param {string} name the name of the music.
* @param {string} author the author(s) of the music.
*/
add(url, name, author) {
const music = new Music(url, name, author, this);
this.#list.push(music);
this.#onAdd(this.total - 1, music);
}
/**
* Adds an existing {@link Music} object to this playlist. Alternatively, use {@link MusicPlaylist#add} to automatically instantiate a new one.
*
* @param {Music} music The existing music to add.
*/
addExisting(music) {
this.#list.push(music);
this.#onAdd(this.total - 1, music);
}
/**
* Calls all playlist length change listeners.
*
* @param {number} index The index at which the new music was added.
* @param {Music} music The music that was added.
*/
#onAdd(index, music) {
this.#lengthChangeListeners.forEach(listener => {
listener(index, true, music);
});
}
/**
* Removes a {@link Music} from this playlist.
*
* @param {number} index the index of the music to be removed.
* @returns {Music} the music that was removed, or null if the index of was invalid.
*/
remove(index) {
if (index >= this.#list.length) {
return null;
}
let removed = this.#list.splice(index, 1)[0];
this.#lengthChangeListeners.forEach(listener => {
listener(index, false, removed);
});
if (removed.length > 0) {
return removed[0];
}
}
/**
* Attempts to find a {@link Music} given a name. Returns the first one found.
*
* @param {string} name the name of the music to be found.
* @returns {number} the index of the music found, or -1 if it was not found.
*/
findMusicIndex(name) {
return this.#list.findIndex((item) => item.getDisplayName() == name);
}
/**
* The number of {@link Music} in this playlist.
*
* @returns {number} total number of musics in this playlist.
*/
get total() {
return this.#list.length;
}
/**
* @returns {number} The current position in the playlist.
*/
get currentPosition() {
return this.#current;
}
/**
* The current position in the playlist.
*/
set currentPosition(position) {
if (typeof position !== "number") throw new Error("Given position is invalid.");
if (position >= this.#list.length || position < 0) return;
const old = this.#current;
this.#current = position;
this.#positionChangeListeners.forEach(positionChangeListener => {
positionChangeListener(old, position);
});
}
/**
* @returns {Music} The current music in the playlist being used.
*/
get currentMusic() {
return this.musicAtIndex(this.currentPosition);
}
/**
*
* @param {MusicPlaylist~positionChangeListener} listener the listener to receive the updates.
* @returns {boolean} true if and only if successfully added the listener.
*/
addPositionChangeListener(listener) {
if (this.#positionChangeListeners.includes(listener)) return false;
this.#positionChangeListeners.push(listener);
return true;
}
/**
*
* @param {MusicPlaylist~positionChangeListener} listener the music change listener to remove.
* @returns {boolean} true if and only if the music change listener given was successfully removed.
*/
removePositionChangeListener(listener) {
const removeIndex = this.#positionChangeListeners.indexOf(listener);
if (removeIndex < 0) return false;
this.#positionChangeListeners.splice(removeIndex, 1);
return true;
}
/**
* Adds a listener for whether music was added or removed to the playlist.
*
* @param {MusicPlaylist~lengthChangeListener} listener The listener to be added.
* @returns {boolean} True if and only if the listener was successfully added.
*/
addLengthChangeListener(listener) {
if (this.#lengthChangeListeners.includes(listener)) return false;
this.#lengthChangeListeners.push(listener);
return true;
}
/**
* Removes a listener for whether music is added or removed to the playlist.
*
* @param {MusicPlaylist~lengthChangeListener} listener The listener to be removed.
* @returns {boolean} True if and only if the provided listener was successfully removed.
*/
removeLengthChangeListener(listener) {
const removeIndex = this.#lengthChangeListeners.indexOf(listener);
if (removeIndex < 0) return false;
this.#lengthChangeListeners.splice(removeIndex, 1);
return true;
}
generatePlaylistElement() {
const element = document.createElement("table");
const headers = element.insertRow();
const musicHeader = document.createElement("th");
musicHeader.innerText = "Music";
headers.appendChild(musicHeader);
const authorHeader = document.createElement("th");
authorHeader.innerText = "Author";
headers.appendChild(authorHeader);
const insertMusic = (music, musicPos = undefined) => {
if (musicPos) musicPos++;
const row = element.insertRow(musicPos);
row.insertCell().innerText = music.displayName;
row.insertCell().innerText = music.author;
};
for (const music of this) {
insertMusic(music);
}
this.addLengthChangeListener((changed, added, music) => {
if (added) {
insertMusic(music, changed);
} else {
element.deleteRow(changed);
}
});
element.rows[this.currentPosition + 1].classList.add("active");
this.addPositionChangeListener((old, current) => {
element.rows[old + 1].classList.remove("active");
element.rows[current + 1].classList.add("active");
});
element.classList.add("music-playlist");
return element;
}
[Symbol.iterator]() {
let index = -1;
const data = this.#list;
return {
next: () => ({ value: data[++index], done: index >= data.length }) // Brackets so not read as a body of anonymous function.
};
}
}