bags_MediaBag.js

/**
 *  File    : bags/MediaBag.js
 *  Created : 07/04/2015
 *  By      : Francesc Busquets <francesc@gmail.com>
 *
 *  JClic.js
 *  An HTML5 player of JClic activities
 *  https://projectestac.github.io/jclic.js
 *
 *  @source https://github.com/projectestac/jclic.js
 *
 *  @license EUPL-1.2
 *  @licstart
 *  (c) 2000-2020 Educational Telematic Network of Catalonia (XTEC)
 *
 *  Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
 *  the European Commission- subsequent versions of the EUPL (the "Licence");
 *  You may not use this work except in compliance with the Licence.
 *
 *  You may obtain a copy of the Licence at:
 *  https://joinup.ec.europa.eu/software/page/eupl
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *  Licence for the specific language governing permissions and limitations
 *  under the Licence.
 *  @licend
 *  @module
 */

import $ from 'jquery';
import MediaBagElement from './MediaBagElement.js';
import Skin from '../skins/Skin.js';
import { log, nSlash } from '../Utils.js';

/**
 * This class stores and manages all the media components (images, sounds, animations, video,
 * MIDI files, etc.) needed to run the activities of a {@link module:project/JClicProject.JClicProject JClicProject}. The main member of
 * the class is `elements`. This is where {@link module:bads/MediaBagElement.MediaBagElement} objects are stored.
 */
export class MediaBag {
  /**
   * MediaBag constructor
   * @param {module:project/JClicProject.JClicProject} project - The JClic project to which this media bag belongs
   */
  constructor(project) {
    this.project = project;
    this.elements = {};
  }

  /**
   * Loads this object settings from a specific JQuery XML element
   * @param {external:jQuery} $xml - The XML element to parse
   */
  setProperties($xml) {
    $xml.children('media').each((_n, child) => {
      const mbe = new MediaBagElement(this.project.basePath, null, this.project.zip);
      mbe.setProperties($(child));
      this.elements[mbe.name] = mbe;
    });
    return this;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    return Object.keys(this.elements).map(k => this.elements[k].getAttributes());
  }

  /**
   * Loads the MediaBag content from a data object
   * @param {object} data - The data object to parse
   */
  setAttributes(data) {
    if (data && data.length)
      data.forEach(el => {
        const mbe = new MediaBagElement(this.project.basePath, null, this.project.zip);
        mbe.setAttributes(el);
        this.elements[mbe.name] = mbe;
      });
    return this;
  }

  /**
   * Finds a {@link module:bads/MediaBagElement.MediaBagElement} by its name, creating a new one if not found and requested.
   * @param {string} name - The name of the element
   * @param {boolean} [create] - When `true`, a new MediaBagElement will be created if not found,
   * using 'name' as its file name.
   * @returns {module:bags/MediaBagElement.MediaBagElement}
   */
  getElement(name, create) {
    name = nSlash(name);
    let result = this.elements[name];
    if (create && !result)
      result = this.getElementByFileName(name, create);
    return result;
  }

  /**
   * Gets a {@link module:bads/MediaBagElement.MediaBagElement} by its file name.
   * @param {string} file - The requested file name
   * @param {boolean} [create] - When `true`, a new {@link module:bads/MediaBagElement.MediaBagElement} will be created if not
   * found.
   * @returns {module:bags/MediaBagElement.MediaBagElement}
   */
  getElementByFileName(file, create) {
    let result = null;
    if (file) {
      file = nSlash(file);
      for (let name in this.elements) {
        if (this.elements[name].file === file) {
          result = this.elements[name];
          break;
        }
      }
      if (!result && create) {
        result = new MediaBagElement(this.project.basePath, null, this.project.zip);
        result.name = file;
        result.file = file;
        result.ext = file.toLowerCase().split('#')[0].split('.').pop();
        result.type = result.getFileType(result.ext);
        this.elements[result.name] = result;
      }
    }
    return result;
  }

  /**
   * Get the names of the media elements that are of the given type.
   * When the search type is `font`, the `fontName` property is used instead of `name`
   * @param {string} type - The type of elements to search
   * @returns {string[]}
   */
  getElementsOfType(type) {
    const result = [];
    $.each(this.elements, (name, element) => {
      if (element.type === type)
        result.push(type === 'font' ? element.fontName : name);
    });
    return result;
  }

  /**
   * Preloads all resources.
   *
   * __Use with care!__ Calling this method will start loading all the resources defined in the
   * MediaBag, whether used or not in the current activity.
   * @param {string} type - The type of media to be build. When `null` or `undefined`, all
   * resources will be build.
   * @param {function} [callback] - Function to be called when each element is ready.
   * @param {module:JClicPlayer.JClicPlayer} [ps] - An optional `PlayStation` (currently a {@link module:JClicPlayer.JClicPlayer JClicPlayer}) used to dynamically load fonts
   * @returns {number} - The total number of elements that will be built
   */
  buildAll(type, callback, ps) {
    let count = 0;
    $.each(this.elements, (name, element) => {
      if (!type || element.type === type) {
        element.build(callback, ps, false);
        count++;
      }
    });
    return count;
  }

  /**
   * Checks if there are media waiting to be loaded
   * @returns {number} - The amount of media elements already loaded, or -1 if all elements are ready
   */
  countWaitingElements() {
    let
      ready = 0,
      allReady = true;

    // Only for debug purposes: return always 'false'
    // TODO: Check loading process!
    $.each(this.elements, (name, element) => {
      if (element.data && !element.ready && !element.checkReady() && !element.checkTimeout()) {
        log('debug', '... waiting for: %s', name);
        allReady = false;
      } else
        ready++;
    });
    return allReady ? -1 : ready;
  }

  /**
   * Loads a {@link module:skins/Skin.Skin Skin} object
   * @param {string} name - The skin name to be loaded
   * @param {string} ps - The {@link module:JClicPlayer.JClicPlayer JClicPlayer} linked to the skin
   * @returns {module:skins/Skin.Skin}
   */
  getSkinElement(name, ps) {
    return Skin.getSkin(name, ps);
  }
}

Object.assign(MediaBag.prototype, {
  /**
   * The collection of {@link module:bads/MediaBagElement.MediaBagElement} objects
   * @name module:bags/MediaBag.MediaBag#elements
   * @type {object} */
  elements: null,
  /**
   * The JClic project to which this MediaBag belongs
   * @name module:bags/MediaBag.MediaBag#project
   * @type {module:project/JClicProject.JClicProject} */
  project: null,
});

export default MediaBag;