Persoenliche Mediazentrale
ulrich
2021-04-10 5f7e0b47ee8d31a87bef7d6fa7268fc8ce839126
www/ui/js/app.js
@@ -1,271 +1,630 @@
function Ablageort(n, o, u) {
  this.name = n;
  this.ort = o;
  this.url = u;
}
function Mediazentrale() {
  var self = this;
  var appMenu;
  // var vorlagen;
  var cache; // mustache templates
  var ortPfad;
  var mediaPfad;
  var katUrl;
  var selTitel;
  this.init = function () {
    self.mediaPfad = '/';
    self.ortPfad = '/';
    self.cache = new Array();
    self.appMenu = new AppMenu();
    self.appMenu.init(
            "data/menu/",
            "hauptmenue.json",
            "data/tpl/app-menu.tpl",
            ".west",
            "8em");
    document.querySelector('.hamburger').addEventListener('click', function (e) {
      self.menue_umschalten();
    });
    self.addEvtListener('#mi-katalog', 'click', self.media_liste);
    self.addEvtListener('#mi-orte', 'click', self.ablageort_liste);
    self.addEvtListener('#mi-prefs', 'click', self.prefs_liste);
    self.addEvtListener('#mi-player', 'click', self.abspieler_liste);
    self.addEvtListener('#mi-listen', 'click', self.abspielliste_liste);
    self.addEvtListener('#mi-list', 'click', self.titel_liste);
    self.fusszeile_umschalten();
    self.seitenleiste_umschalten();
    self.dialog_unten_zeigen();
  };
  /* ---------------- Entitaets-Listen ----------------- */
  this.form_ablageort_neu = function() {
    self.vorlage_laden_und_fuellen("data/tpl/form_ablageort.tpl", "", function(html) {
      document.querySelector(".zentraler-inhalt").innerHTML = html;
      self.addEvtListener('#ok-btn', 'click', function() {
        // hier neuen Ablageort speichern
        var a = new Ablageort(
          document.querySelector('#ablageort-name').value,
          document.querySelector('#ablageort-ort').value,
          document.querySelector('#ablageort-url').value
        );
        // {"name":"Katalog","ort":"/home/ulrich/Videos","url":"/media/test"}
        //var daten = self.serialisieren(a);
        var daten = JSON.stringify(a);
        self.http_post('../api/store/Ablageort', daten, function(){
          // hier die Antwort verarbeiten
  // auf der obersten Ebene werden die Kataloge angezeigt,
  // darunter der Inhalt des aktuellen Pfades
  this.media_liste = function() {
    self.reset_top_buttons();
    if(self.ortPfad === '/') {
      // Kataloge listen
      self.http_get('../api/store/Ablageort/liste/', function (responseText) {
        //document.querySelector('#top-up-btn').removeEventListener('click', self.media_liste_herauf);
        self.vorlage_laden_und_fuellen("data/tpl/katalog_root_liste.tpl", JSON.parse(responseText), function (html) {
          document.querySelector(".zentraler-inhalt").innerHTML = html;
          self.addEvtListener('.entity-eintrag', 'click', function (event) {
            var t = event.target;
            self.http_get('../api/store/Ablageort/' + t.textContent, function(responseText) {
              var ablageort = JSON.parse(responseText);
              self.ortPfad = ablageort.url;
              self.media_liste();
            });
          });
        });
      });
      self.addEvtListener('#cancel-btn', 'click', function() {
        // hier die Aktion abbrechen
    } else {
      var url = '..' + self.ortPfad + self.mediaPfad;
      if(!url.endsWith('/')) {
        url = url + '/';
      }
      self.http_get(url, function(responseText) {
        self.vorlage_laden_und_fuellen("data/tpl/katalog_inhalt_liste.tpl", JSON.parse(responseText), function (html) {
          document.querySelector(".zentraler-inhalt").innerHTML = html;
          self.addEvtListener('.entity-eintrag', 'click', function (event) {
            var t = event.target;
            var tx = t.textContent;
            if(t.classList.contains("entity-typ-folder")) {
              if(self.mediaPfad.endsWith('/')) {
                self.mediaPfad = self.mediaPfad + tx;
              } else {
                self.mediaPfad = self.mediaPfad + '/' + tx;
              }
              self.media_liste();
            } else {
              if(t.classList.contains('selected')) {
                self.addSelectedTitel();
              } else {
                self.removeClassMulti('selected');
                t.classList.add('selected');
              }
              //self.selTitel = new Titel(t.textContent, self.ortPfad);
            }
          });
          self.addEvtListener('#top-up-btn', 'click', function(event) {
            if(self.mediaPfad === '/') {
              self.ortPfad = '/';
            } else {
              var pos = self.mediaPfad.lastIndexOf('/');
              var parent;
              if(pos > 1) {
                parent = self.mediaPfad.substring(0, pos);
              } else {
                parent = '/';
              }
              self.mediaPfad = parent;
            }
            self.media_liste();
          });
        });
      });
    });
  };
  this.addEvtListener = function(selector, eventName, func) {
    var elems = document.querySelectorAll(selector);
    var index;
    for (index = 0; index < elems.length; index++) {
      elems[index].addEventListener(eventName, func);
    }
  };
  
  this.http_get = function(u, cb)  {
    self.http_call('GET', u, null, cb);
  this.ablageort_liste = function() {
    self.entitaet_liste('../api/store/Ablageort/liste/',
      "data/tpl/ablageort_liste.tpl", '../api/store/Ablageort/',
      "self.ablageort_form", function(responseText) {
        var ablageort = JSON.parse(responseText);
        self.ablageort_form(ablageort);
      });
  };
  this.prefs_liste = function() {
    self.entitaet_liste('../api/store/Einstellung/liste/',
      "data/tpl/einstellung_liste.tpl", '../api/store/Einstellung/',
      "self.prefs_form", function(responseText) {
        var einstellung = JSON.parse(responseText);
        self.prefs_form(einstellung);
      });
  };
  this.abspieler_liste = function() {
    self.entitaet_liste('../api/store/Abspieler/liste/',
      "data/tpl/abspieler_liste.tpl", '../api/store/Abspieler/',
      "self.abspieler_form", function(responseText) {
        var abspieler = JSON.parse(responseText);
        self.abspieler_form(abspieler);
      });
  };
  
  this.http_post = function(u, data, cb) {
  this.abspielliste_liste = function() {
    self.entitaet_liste('../api/store/Abspielliste/liste/',
      "data/tpl/abspielliste_liste.tpl", '../api/store/Abspielliste/',
      "self.abspielliste_form", function(responseText) {
        var abspielliste = JSON.parse(responseText);
        self.abspielliste_form(abspielliste);
      });
  };
  /* -------------------- Entitaets-Formulare ------------------ */
  this.abspielliste_form = function(al) {
    self.entitaet_form(al, al.name,
      "data/tpl/form_abspielliste.tpl", '../api/store/Abspielliste/',
      '#abspielliste-name', function() {
          self.abspielliste_auswahl_fuellen();
          self.abspielliste_liste();
    });
  };
  this.abspieler_form = function(pl) {
    self.entitaet_form(pl, pl.key,
      "data/tpl/form_abspieler.tpl", '../api/store/Abspieler/',
      '#abspieler-name', function() {
          self.abspieler_auswahl_fuellen();
          self.abspieler_liste();
    });
  };
  this.prefs_form = function(k) {
    self.entitaet_form(k, k.key,
      "data/tpl/form_einstellung.tpl", '../api/store/Einstellung/',
      '#einstellung-key', function() {
          self.prefs_liste();
    });
  };
  /*
   * Ablageort-Formular anzeigen
   *
   * {"name":"Katalog 2","ort":"/home/ulrich/Videos","url":"/media/kat2"}:
   *
   * @param {type} ablageort  der Ablageort, der bearbeitet werden soll, leer fuer neuen Ort
   * @returns {undefined} kein Rueckgabewert
   */
  this.ablageort_form = function(ort) {
    self.entitaet_form(ort, ort.name,
      "data/tpl/form_ablageort.tpl", '../api/store/Ablageort/',
      '#ablageort-name', function() {
        self.ablageort_liste();
    });
  };
  /* ------------------------------- UI-Dynamik ----------------------- */
  self.reset_top_buttons = function() {
    self.vorlage_laden_und_fuellen("data/tpl/top_btns.tpl", '', function (html) {
      document.querySelector(".top-btns").innerHTML = html;
    });
  };
  this.abspieler_auswahl_fuellen = function() {
    self.http_get('../api/store/Abspieler/liste/', function (responseText) {
      self.vorlage_laden_und_fuellen("data/tpl/abs_sel.tpl", JSON.parse(responseText), function (html) {
        document.querySelector(".abs-sel").innerHTML = html;
      });
    });
  };
  this.abspielliste_auswahl_fuellen = function() {
    self.http_get('../api/store/Abspielliste/', function (responseText) {
      self.vorlage_laden_und_fuellen("data/tpl/pl_sel.tpl", JSON.parse(responseText), function (html) {
        document.querySelector(".pl-sel").innerHTML = html;
        self.addEvtListener('#playlist', 'change', function() {
          self.titel_liste();
        });
      });
    });
  };
  this.addSelectedTitel = function() {
    var elem = document.querySelector(".selected");
    //var titelName = elem.textContent;
    var titelName = elem.attributes.dateiName.nodeValue;
    var album = elem.attributes.album.nodeValue;
    var interpret = elem.attributes.interpret.nodeValue;
    var anzName = elem.attributes.titelAnzName.nodeValue;
    var titel;
    if(self.mediaPfad.endsWith('/')) {
      titel = new Titel(titelName, self.mediaPfad, self.ortPfad, interpret, anzName, album);
    } else {
      titel = new Titel(titelName, self.mediaPfad + '/', self.ortPfad, interpret, anzName, album);
    }
    var plname = document.querySelector('#playlist').value;
    self.http_put('../api/alist/' + plname, JSON.stringify(titel), function(responseText) {
      self.meldung_mit_timeout(responseText, 1500);
    });
  };
  this.removeSelectedTitel = function() {
    var elem = document.querySelector(".selected");
    var parentElem = elem.parentNode;
    //console.log("elem: " + elem.nodeName + ", parent: " + parentElem.nodeName + ", len: " + parentElem.childNodes.length);
    var liElems = parentElem.getElementsByTagName(elem.nodeName); // nur die LI Elemente
    //console.log("liElems.anz: " + liElems.length);
    var gefunden = false;
    for(var i = 0; i < liElems.length && !gefunden; i++) {
      //console.log(liElems.item(i).textContent);
      if(liElems.item(i).classList.contains("selected")) {
        gefunden = true;
        var index = i;
        //console.log(elem.textContent + ' hat Index ' + i);
      }
    }
    // /mz/api/alist/[pl-name]/[nr]
    var plname = document.querySelector('#playlist').value;
    self.http_delete('../api/alist/' + plname + '/' + index,'', function(responseText) {
      // DELETE   http://localhost:9090/mz/api/alist/liste1/0
      self.meldung_mit_timeout(responseText, 1500);
      self.titel_liste();
    });
  };
  /* Unterer Einblendbereich */
  this.dialog_unten_zeigen = function() {
    self.vorlage_laden_und_fuellen("data/tpl/ctrl.tpl", "", function (html) {
      var dlg = document.querySelector(".dialog-unten");
      dlg.style.height = '4.5em';
      dlg.innerHTML = html;
      self.abspieler_auswahl_fuellen();
      self.abspielliste_auswahl_fuellen();
      self.addEvtListener('#dazu-btn', 'click', self.addSelectedTitel);
      self.addEvtListener('#play-btn', 'click', self.play);
      self.addEvtListener('#weg-btn', 'click', self.removeSelectedTitel);
      self.media_liste();
    });
  };
  /* Titel einer Abspielliste */
  this.titel_liste = function() {
    self.reset_top_buttons();
    var plname = document.querySelector('#playlist').value;
    self.http_get('../api/alist/' + plname, function (responseText) {
      self.vorlage_laden_und_fuellen("data/tpl/titel_liste.tpl", JSON.parse(responseText), function (html) {
        document.querySelector(".zentraler-inhalt").innerHTML = html;
        self.addEvtListener('.entity-eintrag', 'click', function (event) {
          var t = event.target;
          self.removeClassMulti('selected');
          t.classList.add('selected');
        });
      });
    });
  };
  /* ------------- Media-Steuerung ------------------------- */
  this.play = function() {
    var abs = document.querySelector('#abspieler').value;
    var lst = document.querySelector('#playlist').value;
    console.log(
      "play playlist.value: " + document.querySelector('#playlist').value +
      ", abspieler.value: " + document.querySelector('#abspieler').value);
    self.http_get('../api/strg/' + abs + '/play/liste/' + lst, function(responseText) {
      self.meldung_mit_timeout(responseText, 1500);
    });
  };
  /* ------------- Helfer fuer Entitaets-Formulare ----------------------- */
  /*
   * url: '../api/store/Ablageort/liste/'
   * tpl: "data/tpl/ablageort_liste.tpl"
   * storeUrl: '../api/store/Ablageort/'
   * formFunc: "self.ablageort_form"
   * cb: etwas wie
   *   function(responseText){
   *     var ablageort = JSON.parse(responseText);
   *     self.ablageort_form(ablageort);
   *   });
   */
  this.entitaet_liste = function(listUrl, tpl, storeUrl, formFunc, cb) {
    self.reset_top_buttons();
    self.http_get(listUrl, function (responseText) {
      self.vorlage_laden_und_fuellen(tpl, JSON.parse(responseText), function (html) {
        document.querySelector(".zentraler-inhalt").innerHTML = html;
        self.addEvtListener('.entity-eintrag', 'click', function (event) {
          var t = event.target;
          self.http_get(storeUrl + t.textContent, cb);
        });
        //self.addEvtListener('#neu-btn', 'click', function (event) {
        self.addEvtListener('#top-neu-btn', 'click', function(event) {
          eval(formFunc + "(this)");
        });
      });
    });
  };
  /*
   * dat: gefuelltes Datenobjekt bei Aenderung
   * key: der alte schluesselbegriff bei Aenderung (z.B. al.name)
   * tpl: "data/tpl/form_abspielliste.tpl"
   * url: '../api/store/Abspielliste/'
   * selector: '#abspielliste-name'
   * cbOk: etwas wie
   *    function() {
   *       self.abspielliste_auswahl_fuellen();
   *       self.abspielliste_liste();
   *     });
   * delSelector: '#abspielliste-name'
   * cbDel: etwas wie
   *    function() {
   *       self.abspielliste_auswahl_fuellen();
   *       self.abspielliste_liste();
   *     });
   */
  this.entitaet_form = function(dat, key, tpl, url, selector, cb) {
    self.vorlage_laden_und_fuellen(tpl, dat, function (html) {
      document.querySelector(".zentraler-inhalt").innerHTML = html;
      const form = document.querySelector('form');
      form.addEventListener('submit', function(event) {
        self.handle_submit(event, key, url, selector, cb);
      });
      self.addEvtListener('#cancel-btn', 'click', cb);
      self.addEvtListener('#loeschen-btn', 'click', function(event) {
        event.preventDefault();
        self.handle_del_btn(selector, url, cb);
      });
    });
  };
  /*
   * existingKey: wenn die Entitaet existiert und geandert werden soll
   *                 leer, wenn neue Entitaet
   */
  this.handle_submit = function(event, existingKey, putUrl, keySelector, cb) {
    event.preventDefault();
    const data = new FormData(event.target);
    const value = Object.fromEntries(data.entries());
    console.log({ value });
    console.log(JSON.stringify(value));
    var daten = JSON.stringify(value);
    var formkey = document.querySelector(keySelector).value;
    formkey = formkey.replace(' ', '').replace(/[\W]+/g, '');
    if(typeof existingKey === "undefined" ||  existingKey.length < 1) {
      // neu
      self.http_put(putUrl + formkey, daten, function (responseText) {
        if(typeof(cb) !== 'function') {
          // ..
        } else {
          cb();
        }
      });
    } else {
      // aendern
      self.http_put(putUrl + existingKey, daten, function (responseText) {
        if(typeof(cb) !== 'function') {
          // ..
        } else {
          cb();
        }
      });
    }
  };
  this.handle_del_btn = function(selectorKey, delUrl, cb) {
    var pkey = document.querySelector(selectorKey).value;
    var dlgdata = {"del-elem": pkey};
    self.dialog_laden_und_zeigen('data/tpl/dlg-loeschen.tpl', dlgdata, function() {
      self.addEvtListener('#nein-btn', 'click', self.dialog_schliessen);
      self.addEvtListener('#ja-btn', 'click', function(event) {
        console.log("loeschen geklickt.");
        self.http_delete(delUrl + pkey, '', function (responseText) {
          self.dialog_schliessen();
          if(typeof(cb) !== 'function') {
            // ..
          } else {
            cb();
          }
        });
      });
    });
  };
  /* ------------------ sonstige Helfer ----------------------- */
  this.addEvtListener = function(selector, eventName, func) {
    document.querySelectorAll(selector).forEach(elem => { elem.addEventListener(eventName, func); });
  };
  this.removeClassMulti = function(selector) {
    document.querySelectorAll('.' + selector).forEach(elem => { elem.classList.remove(selector); });
  };
  /* --------------------- asynchroner HTTP Client ----------------- */
  this.http_get = function (u, cb) {
    self.http_call('GET', u, null, cb);
  };
  this.http_post = function (u, data, cb) {
    self.http_call('POST', u, data, cb);
  };
  this.http_call = function (method, u, data, scallback) {
  this.http_put = function (u, data, cb) {
    self.http_call('PUT', u, data, cb);
  };
  this.http_delete = function (u, data, cb) {
    // console.log("delete " + u);
    self.http_call('DELETE', u, data, cb);
  };
  this.http_call = function (method, u, data, scallback) {
    var xhr = new XMLHttpRequest();
    var url = u;
    xhr.onreadystatechange = function() {
    xhr.onreadystatechange = function () {
      if (this.readyState === 4 && this.status === 200) {
        scallback(this.responseText);
      }
    };
    xhr.open(method, url);
    if(method === 'GET')  {
    if (method === 'GET') {
      xhr.send();
    } else if(method === 'POST' || method === 'PUT') {
    } else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
      xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
      xhr.send(data);
    }
  };
  
  this.serialisieren = function(obj) {
    return '{"' + obj.constructor.name + '":' + JSON.stringify(obj) + '}';
  };
  /* ------------------------ aus App-Vorlage -------------------  */
  /* ab hier aus App-Vorlage */
  this.init = function() {
    //self.vorlagen = new Vorlagen();
    self.cache = new Array();
    self.appMenu = new AppMenu();
    self.appMenu.init(
      "data/menu/",
      "hauptmenue.json",
      "data/tpl/app-menu.tpl",
      ".west",
      "8em");
   document.querySelector('.hamburger').addEventListener('click', function(e) {
      self.menue_umschalten();
    });
  };
  this.menue_umschalten = function() {
  this.menue_umschalten = function () {
    var ham = document.querySelector(".hamburger");
    ham.classList.toggle("is-active"); // hamburger-icon umschalten
    self.appMenu.toggle(); // menue oeffnen/schliessen
  };
  this.info_dialog_zeigen = function() {
  this.info_dialog_zeigen = function () {
    self.dialog_laden_und_zeigen('data/tpl/dlg-info.tpl', '');
    self.menue_umschalten();
  };
  this.seitenleiste_umschalten = function() {
  this.seitenleiste_umschalten = function () {
    var ostDiv = document.querySelector('.ost');
    if(ostDiv.classList.contains('ost-open')) {
    if (ostDiv.classList.contains('ost-open')) {
      ostDiv.classList.remove('ost-open');
       ostDiv.style.flexBasis = '0em';
      ostDiv.style.flexBasis = '0em';
    } else {
       ostDiv.classList.add('ost-open');
       ostDiv.style.flexBasis = '6em';
      ostDiv.classList.add('ost-open');
      ostDiv.style.flexBasis = '6em';
    }
    self.menue_umschalten();
  };
  this.fusszeile_umschalten = function() {
  this.fusszeile_umschalten = function () {
    var suedDiv = document.querySelector('.sued');
    if(suedDiv.classList.contains('sued-open')) {
    if (suedDiv.classList.contains('sued-open')) {
      suedDiv.classList.remove('sued-open');
       suedDiv.style.height = '0';
      suedDiv.style.height = '0';
    } else {
      suedDiv.classList.add('sued-open');
       suedDiv.style.height = '1.5em';
      suedDiv.style.height = '1.5em';
    }
    self.menue_umschalten();
  };
  this.menu_message = function(msg) {
  this.menu_message = function (msg) {
    self.meldung_mit_timeout(msg, 1500);
    var suedDiv = document.querySelector('.sued');
    if(suedDiv.classList.contains('sued-open')) {
    if (suedDiv.classList.contains('sued-open')) {
    } else {
      suedDiv.classList.add('sued-open');
       suedDiv.style.height = '1.5em';
      suedDiv.style.height = '1.5em';
    }
    self.menue_umschalten();
  };
  this.message_1 = function() {
  this.message_1 = function () {
    self.menu_message('Eine Mitteilung.');
  };
  this.message_2 = function() {
  this.message_2 = function () {
    self.menu_message('Was wir schon immer sagen wollten.');
  };
  this.message_3 = function(text) {
  this.message_3 = function (text) {
    self.menu_message(text);
  };
  this.meldung_mit_timeout = function(meldung, timeout) {
  this.meldung_mit_timeout = function (meldung, timeout) {
    var s = document.querySelector('.sued');
    s.classList.add('sued-open');
    s.style.height = '1.5em';
    s.textContent = meldung;
    setTimeout(function() {
    setTimeout(function () {
      s.textContent = 'Bereit.';
      setTimeout(function() {
      setTimeout(function () {
        var suedDiv = document.querySelector('.sued');
        if(suedDiv.classList.contains('sued-open')) {
            suedDiv.classList.remove('sued-open');
            suedDiv.style.height = '0';
        if (suedDiv.classList.contains('sued-open')) {
          suedDiv.classList.remove('sued-open');
          suedDiv.style.height = '0';
        }
      }, 500);
    }, timeout);
  };
  /* Dialog-Funktionen */
  /* --------------------- Dialog-Funktionen ------------------------ */
  /*
    Einen Dialog aus Vorlagen erzeugen
    vurl - URL zur Dialogvorlage
    msgTpl - URL mit einer Vorlage eines Mitteilungstextes (optional)
  */
  this.dialog_laden_und_zeigen = function(vurl, msgTpl) {
    if(msgTpl !== '') {
      fetch(msgTpl)
        .then(data => {
          // Handle data
          self.dialog_zeigen(vurl, data);
        }).catch(error => {
          // Handle error
        });
   Einen Dialog aus Vorlagen erzeugen
   vurl - URL zur Dialogvorlage
   msgTpl - URL mit einer Vorlage eines Mitteilungstextes (optional)
   */
  this.dialog_laden_und_zeigen = function (vurl, msgTpl, cb) {
    var vorlage = self.cache[vurl];
    if(vorlage === undefined) {
      self.http_get(vurl, function(antwort) {
        self.cache[vurl] = antwort;
        self.dialog_zeigen(vurl, msgTpl, cb);
      });
    } else {
      self.dialog_zeigen(vurl, '');
      self.dialog_zeigen(vurl, msgTpl, cb);
    }
  };
  this.dialog_zeigen = function(vurl, inhalt) {
  this.dialog_zeigen = function (vurl, inhalt, cb) {
    var dlg = document.querySelector(".dialog");
    self.html_erzeugen(
      vurl,
      inhalt,
      function(html) {
        //dlg.html(html);
        dlg.style.height = '5em';
        dlg.innerHTML = html;
        document.querySelector('.close-btn').addEventListener('click', self.dialog_schliessen);
        //dlg.slideDown(300);
    self.html_erzeugen(vurl, inhalt, function (html) {
      dlg.style.height = '7em';
      dlg.innerHTML = html;
      document.querySelector('.close-btn').addEventListener('click', self.dialog_schliessen);
      if(typeof(cb) !== 'function') {
        // ..
      } else {
        cb();
      }
    });
  };
  self.dialog_schliessen = function() {
  this.dialog_schliessen = function () {
    document.querySelector('.close-btn').removeEventListener('click', self.dialog_schliessen);
    //$('.dialog').slideUp(300);
    var dlg = document.querySelector('.dialog');
    //dlg.style.display = "none";
    dlg.style.height = '0';
    dlg.innerHTML = '';
  };
  /* Vorlagen */
  /* ---------------------   Vorlagen   ---------------------- */
  /*
    Das HTML erzeugen, das entsteht, wenn eine Vorlage mit Inhalt
    gefüllt wird
    Das Füllen erfolgt asynchron, d.h. der Programmlauf geht nach dem
    Aufruf weiter ohne auf das Laden und Füllen der Vorlage zu warten.
    Das fertige HTML wird der Callback-Funktion übergeben
    sobald die Vorlage geladen und gefüllt ist, unabhängig davon, wo der
    Programmlauf zu diesem Zeitpunkt mittlerweile ist.
    vurl - URL zur Vorlagendatei
    inhalt - die JSON-Struktur, deren Inhalt in die
              Vorlage gefüllt werden soll
    cb - Callback-Funktion, die gerufen wird, wenn die Vorlage gefüllt ist.
          Dieser Callback-Funktion wird das fertige HTML übergeben
  */
  this.html_erzeugen = function(vurl, inhalt, cb) {
   Das HTML erzeugen, das entsteht, wenn eine Vorlage mit Inhalt
   gefüllt wird
   Das Füllen erfolgt asynchron, d.h. der Programmlauf geht nach dem
   Aufruf weiter ohne auf das Laden und Füllen der Vorlage zu warten.
   Das fertige HTML wird der Callback-Funktion übergeben
   sobald die Vorlage geladen und gefüllt ist, unabhängig davon, wo der
   Programmlauf zu diesem Zeitpunkt mittlerweile ist.
   vurl - URL zur Vorlagendatei
   inhalt - die JSON-Struktur, deren Inhalt in die
   Vorlage gefüllt werden soll
   cb - Callback-Funktion, die gerufen wird, wenn die Vorlage gefüllt ist.
   Dieser Callback-Funktion wird das fertige HTML übergeben
   */
  this.html_erzeugen = function (vurl, inhalt, cb) {
    var vorlage = self.cache[vurl];
    if(vorlage === undefined) {
    if (vorlage === undefined) {
      self.vorlage_laden_und_fuellen(vurl, inhalt, cb);
    } else {
      self.vorlage_fuellen(vurl, inhalt, cb);
    }
  };
  this.vorlage_fuellen = function(vurl, inhalt, cb) {
  this.vorlage_fuellen = function (vurl, inhalt, cb) {
    cb(Mustache.render(self.cache[vurl], inhalt));
  };
  /*
    Eine Vorlage vom Server in den lokalen Speicher laden
    vurl - der URL unter dem die Vorlage zu finden ist
    inhalt - die JSON-Struktur, deren Inhalt in die
              Vorlage gefüllt werden soll
    cb - callback: Diese Funktion wird gerufen, wenn die Vorlage mit dem
            Inhalt gefüllt ist
  */
  this.vorlage_laden_und_fuellen = function(vurl, inhalt, cb) {
    /*
    $.ajax({
      url: vurl,
      type: "GET",
      dataType : "text"
    }).done(function( vorlage ) {
      self.cache[vurl] = vorlage;
      self.vorlage_fuellen(vurl, inhalt, cb);
    });
    */
   Eine Vorlage vom Server in den lokalen Speicher laden
   vurl - der URL unter dem die Vorlage zu finden ist
   inhalt - die JSON-Struktur, deren Inhalt in die
   Vorlage gefüllt werden soll
   cb - callback: Diese Funktion wird gerufen, wenn die Vorlage mit dem
   Inhalt gefüllt ist
   */
  this.vorlage_laden_und_fuellen = function (vurl, inhalt, cb) {
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function() {
    xmlhttp.onreadystatechange = function () {
      if (this.readyState == 4 && this.status == 200) {
        self.cache[vurl] = this.responseText;
        self.vorlage_fuellen(vurl, inhalt, cb);
@@ -278,3 +637,33 @@
}
/* ----------- Objekte ---------------- */
function Ablageort(n, o, u) {
  this.name = n;
  this.ort = o;
  this.url = u;
}
function Einstellung(k, v) {
  this.key = k;
  this.value = v;
}
function Abspieler(n, u) {
  this.name = n;
  this.url = u;
}
function Abspielliste(n) {
  this.name = n;
}
function Titel(n, p, u, i, t, a) {
  this.katalogUrl = u;
  this.pfad = p;
  this.name = n;
  this.interpret = i;
  this.titelAnzName = t;
  this.album  = a;
}