Source: src/ui/Player.js

import { p5 } from "../../index.js";
import soundManager from "../controllers/SoundManager.js";
import visualManager from "../controllers/VisualisationManager.js";
import PlayButton from "./PlayButton.js";
import PlayNextButton from "./PlayNextButton.js";
import PlayPrevButton from "./PlayPrevButton.js";
import Playlist from "./Playlist.js";
import PlaylistButton from "./PlaylistButton.js";
import ProgressBar from "./ProgressBar.js";
import Vislist from "./Vislist.js";
import VislistButton from "./VislistButton.js";

/**
 * @class Sound player and controller
 * @memberof UIs
 */
class Player {
  static instance;
  x = 20;
  y = 20;
  width = 200;
  height = 170;
  draggable = false;

  /**
   * Private constructor to enforce singleton pattern
   * @private
   * @returns {Player}
   */
  constructor() {
    if (!Player.instance) {
      Player.instance = this;
    }

    // initialize ui elements
    this.playBackwardButton = new PlayPrevButton(
      this.x + 10,
      this.y + 100,
      18,
      18
    );
    this.playbackButton = new PlayButton(
      this.x + this.width / 2 - 9,
      this.y + 100,
      18,
      18
    );
    this.playForwardButton = new PlayNextButton(
      this.x + this.width - 28,
      this.y + 100,
      18,
      18
    );
    this.playlistButton = new PlaylistButton(this.x + 10, this.y + 135);
    this.vislistButton = new VislistButton(
      this.x + this.width - 95,
      this.y + 135
    );
    this.playBar = new ProgressBar(
      this.x + 10,
      this.y + 60,
      this.width - 20,
      13
    );
    this.playlist = new Playlist(this.x, this.y + this.height, this.width, 200);
    this.vislist = new Vislist(this.x, this.y + this.height, this.width, 200);

    return Player.instance;
  }

  draw() {
    p5.push();
    p5.textFont("Inconsolata");

    // player background -----------------------
    p5.fill("white");
    p5.stroke("black");
    p5.strokeWeight(1);
    p5.rect(this.x, this.y, this.width, this.height);

    // song and artist name -----------------------
    // draw a artist name
    p5.fill("gray");
    p5.stroke("gray");
    p5.strokeWeight(1);
    p5.textSize(16);
    p5.text(soundManager.sound.artist || "-", this.x + 10, this.y + 20);

    // draw a song name
    p5.fill("black");
    p5.textSize(30);
    p5.text(soundManager.sound.name || "-", this.x + 10, this.y + 45);

    if (soundManager.sound?.isReady) {
      // progress bar -----------------------
      // draw a song progress bar
      this.playBar.draw();

      // draw a song duration text
      p5.textSize(13);
      p5.noStroke();
      const durText = this.#formatTime(soundManager.sound.duration || 0);
      const durTextWidth = p5.textWidth(durText);
      p5.text(
        this.#formatTime(soundManager.sound.duration || 0),
        this.width + this.x - durTextWidth - 10,
        this.y + 85
      );

      // draw a song current time text
      p5.textSize(13);
      p5.noStroke();
      p5.text(
        this.#formatTime(soundManager.sound?.loaded.currentTime() || 0),
        this.x + 10,
        this.y + 85
      );

      // fill the current progress
      if (soundManager.sound?.loaded) {
        p5.fill("black");
        const progress =
          soundManager.sound.loaded.currentTime() /
            soundManager.sound.duration || 0;
        const barWidth = progress * this.playBar.width;
        p5.rect(this.x + 10, this.y + 60, barWidth, 13, 4, 0, 0, 4);
      }

      // playback button -----------------------
      // draw play buttons
      this.playbackButton.draw();
      this.playForwardButton.draw();
      this.playBackwardButton.draw();
    } else {
      p5.textSize(20);
      p5.text("loading...", this.x + 10, this.y + 85);
    }

    // playlist & vislist --------------------
    // draw a playlist button
    this.playlistButton.draw();
    this.vislistButton.draw();

    // draw playlist or visual list
    if (this.playlistButton.openList) {
      this.playlist.draw();
    }
    if (this.vislistButton.openList) {
      this.vislist.draw();
    }

    p5.pop();
  }

  // event handlers ------------------------
  // move the player by mouse
  mouseDragged() {
    if (this.draggable) {
      this.x = p5.mouseX - this.width / 1.2;
      this.y = p5.mouseY - this.height / 7;

      // update ui location
      this.playBackwardButton.updateLocation(this.x + 10, this.y + 100);
      this.playbackButton.updateLocation(
        this.x + this.width / 2 - 9,
        this.y + 100
      );
      this.playForwardButton.updateLocation(
        this.x + this.width - 28,
        this.y + 100
      );
      this.playlistButton.updateLocation(this.x + 10, this.y + 135);
      this.vislistButton.updateLocation(this.x + this.width - 95, this.y + 135);
      this.playBar.updateLocation(this.x + 10, this.y + 60);
      this.playlist.updateLocation(this.x, this.y + this.height);
      this.vislist.updateLocation(this.x, this.y + this.height);
    }
  }

  mouseReleased() {}

  mousePressed() {
    // click playlist -------------
    if (this.playlistButton.openList && this.playlist.isMouseIn()) {
      // select a song from the playlist
      const playlistItem = this.playlist.mousePressed();
      if (playlistItem?.name) {
        this.playbackButton.playing = false;
      }
    }

    // click vis list -------------
    if (this.vislistButton.openList && this.vislist.isMouseIn()) {
      // select a vis from the vislist
      const vislistItem = this.vislist.getClickedItem();
      if (vislistItem?.name) {
        visualManager.selectVisual(vislistItem.name);
      }
    }

    // toggle draggable state -------------
    if (
      this.isMouseIn() &&
      !this.playbackButton.isMouseIn() &&
      !this.playlistButton.isMouseIn() &&
      !this.vislistButton.isMouseIn() &&
      !this.playBar.isMouseIn()
    ) {
      this.draggable = true;
    } else {
      this.draggable = false;
    }

    // move to new playing point in playbar -------------
    if (this.playBar.isMouseIn()) {
      // fill the current progress
      if (soundManager.sound?.loaded) {
        const targetProgress = (p5.mouseX - this.x - 10) / this.playBar.width;
        soundManager.sound.loaded.jump(
          soundManager.sound.duration * targetProgress
        );
      }
    }

    // click play, forward, backward buttons ------------------------------------
    this.playbackButton.mousePressed();
    this.playForwardButton.mousePressed();
    this.playBackwardButton.mousePressed();

    // toggle between playlist and visualist buttons -------------
    if (!this.playlistButton.openList && !this.vislistButton.openList) {
      if (this.playlistButton.isMouseIn()) {
        this.playlistButton.toggle();
      }
      if (this.vislistButton.isMouseIn()) {
        this.vislistButton.toggle();
      }
    } else if (this.playlistButton.openList && !this.vislistButton.openList) {
      if (this.vislistButton.isMouseIn()) {
        this.vislistButton.toggle();
        this.playlistButton.toggle();
      }

      if (this.playlistButton.isMouseIn()) {
        this.playlistButton.toggle();
      }
    } else if (!this.playlistButton.openList && this.vislistButton.openList) {
      if (this.playlistButton.isMouseIn()) {
        this.playlistButton.toggle();
        this.vislistButton.toggle();
      }

      if (this.vislistButton.isMouseIn()) {
        this.vislistButton.toggle();
      }
    }
  }

  /**
   * responds to key press events
   * @param {number} key - the ascii code of the keypressed
   */
  keyPressed(key) {
    const KEYCODE_SPACEBAR = 32;

    if (soundManager.sound?.isReady && key.keyCode === KEYCODE_SPACEBAR) {
      this.playbackButton.playOrPause();
    }
  }

  // helper methods ------------------------
  // format time text to min:sec
  #formatTime(time) {
    let min = Math.floor(time / 60);
    let sec = Math.floor(time % 60);

    // if min or sec is 1 digit, add 0 before the number
    if (min < 10) {
      min = "0" + min;
    }
    if (sec < 10) {
      sec = "0" + sec;
    }

    return `${min}:${sec}`;
  }

  // check if the mouse is in the player area
  isMouseIn() {
    return (
      p5.mouseX > this.x &&
      p5.mouseX < this.x + this.width &&
      p5.mouseY > this.y &&
      p5.mouseY < this.y + this.height
    );
  }
}

/**
 * Create a single instance of the player
 * @type {Player}
 */
const player = new Player();
export default player;