Persoenliche Mediazentrale
ulrich
2021-04-24 4bbb9f05ba12894477e611eb9b75113e3e3cce29
www/ui/js/app.js
@@ -1,197 +1,881 @@
function AppVorlage() {
function Mediazentrale() {
  var self = this;
  var appMenu;
  // var vorlagen;
  var cache; // mustache templates
  var ortPfad;
  var mediaPfad;
  var katUrl;
  var selTitel;
  var katName;
  this.init = function() {
    //self.vorlagen = new Vorlagen();
  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");
            "data/menu/",
            "hauptmenue.json",
            "data/tpl/app-menu.txt",
            ".west",
            "6em");
   document.querySelector('.hamburger').addEventListener('click', function(e) {
    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.addEvtListener('#mi-live', 'click', self.livestream_liste);
    self.addEvtListener('#mi-devices', 'click', self.geraet_liste);
    self.addEvtListener('#mi-switch', 'click', self.geraet_schalt_liste);
    self.fusszeile_umschalten();
    self.seitenleiste_umschalten();
    self.dialog_unten_zeigen();
  };
  /* ---------------- Entitaets-Listen ----------------- */
  this.livestream_selection = function() {
    document.querySelector('.breadcrumb-behaelter').textContent = '';
    document.querySelector('.bereich-name').textContent = 'Livestream-Auswahl';
    self.http_get('../api/store/Livestream/liste/', function(responseText) {
      self.html_erzeugen("data/tpl/livestream_liste.txt", 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');
        });
      });
    });
  };
  this.menue_umschalten = function() {
  // auf der obersten Ebene werden die Kataloge angezeigt,
  // darunter der Inhalt des aktuellen Pfades
  this.media_liste = function() {
    self.reset_top_buttons();
    //console.log("ortPfad: " + self.ortPfad + ", mediaPfad: " + self.mediaPfad);
    document.querySelector('.bereich-name').textContent = '';
    if(self.ortPfad === '/') {
      var bb = document.querySelector('.breadcrumb-behaelter');
      bb.textContent = "Kataloge";
      // Kataloge listen
      self.http_get('../api/store/Ablageort/liste/', function (responseText) {
        //document.querySelector('#top-up-btn').removeEventListener('click', self.media_liste_herauf);
        self.html_erzeugen("data/tpl/katalog_root_liste.txt", JSON.parse(responseText), function (html) {
          document.querySelector(".zentraler-inhalt").innerHTML = html;
          self.addEvtListener('.entity-eintrag', 'click', function (event) {
            var t = event.target;
            self.katName = t.textContent;
            if(self.katName !== "Livestreams") {
              self.http_get('../api/store/Ablageort/' + t.textContent, function(responseText) {
                var ablageort = JSON.parse(responseText);
                self.ortPfad = ablageort.url;
                self.media_liste();
              });
            } else {
              self.livestream_selection();
            }
          });
        });
      });
    } else {
      var bb = document.querySelector('.breadcrumb-behaelter');
      var brPfad = self.katName + self.mediaPfad;
      var breadcrumbs = brPfad.split('/');
      var brLinks = "";
      var brLinkPfad = "";
      for(var index = 0; index < breadcrumbs.length; index++) {
        // <a class="breadcrumb-link" href="#">breadcrumbs[index]</a>
        if(index === 0) {
          brLinkPfad = '';
        } else {
          brLinkPfad = brLinkPfad + '/' + breadcrumbs[index];
        }
        brLinks = brLinks + "<a brlink='" + brLinkPfad + "' class='breadcrumb-link' href='#'>" + breadcrumbs[index] + "</a>";
        //console.log('   breadcrumbs[' + index + ']: ' + breadcrumbs[index]);
      }
      bb.innerHTML = brLinks;
      self.addEvtListener('.breadcrumb-link', 'click', function(event) {
        //console.log(event.target.attributes.brlink.nodeValue);
        var neuerPfad = event.target.attributes.brlink.nodeValue;
        self.mediaPfad = neuerPfad;
        self.media_liste();
      });
      var url = '..' + self.ortPfad + self.mediaPfad;
      if(!url.endsWith('/')) {
        url = url + '/';
      }
      self.http_get(url, function(responseText) {
        self.html_erzeugen("data/tpl/katalog_inhalt_liste.txt", 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')) {
                t.classList.add('added-to-playlist');
                self.titelDazu();
              } 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.ein_aus_btn = function() {
    self.addEvtListener('#ein-aus-btn', 'click', function (event) {
      var geraetName = event.target.attributes.gname.nodeValue;
      var nameElem = event.target.parentNode.querySelector('.schalt-geraet-name');
      if(nameElem.classList.contains('schalt-geraet-true')) {
        // ausschalten
        self.http_get('../api/gstrg/geraet/' + geraetName + "/aus", function(responseText) {
          // console.log(responseText);
          self.geraet_schalt_liste();
        });
      } else {
        // einschalten
        self.http_get('../api/gstrg/geraet/' + geraetName + "/ein", function(responseText) {
          // console.log(responseText);
          self.geraet_schalt_liste();
        });
      }
    });
  };
  this.geraet_schalt_liste = function() {
    /*self.entitaet_liste('Geräte schalten','../api/store/Geraet/listealles/',
      "data/tpl/geraet_schalt_liste.txt", '../api/store/Geraet/',
      "self.form_geraet_status", function(responseText) {*/
    self.entitaet_liste('Geräte schalten','../api/store/Geraet/listealles/',
      "data/tpl/geraet_schalt_liste.txt", '../api/store/Geraet/',
      "", function(responseText) {
        //var geraet = JSON.parse(responseText);
        //self.geraet_status_form(geraet);
      }, self.ein_aus_btn);
  };
  this.geraet_liste = function() {
    self.entitaet_liste('Geräte','../api/store/Geraet/liste/',
      "data/tpl/geraet_liste.txt", '../api/store/Geraet/',
      "self.geraet_form", function(responseText) {
        var geraet = JSON.parse(responseText);
        self.geraet_form(geraet);
      });
  };
  this.ablageort_liste = function() {
    self.entitaet_liste('Kataloge','../api/store/Ablageort/liste/',
      "data/tpl/ablageort_liste.txt", '../api/store/Ablageort/',
      "self.ablageort_form", function(responseText) {
        var ablageort = JSON.parse(responseText);
        self.ablageort_form(ablageort);
      });
  };
  this.prefs_liste = function() {
    self.entitaet_liste('Einstellungen','../api/store/Einstellung/liste/',
      "data/tpl/einstellung_liste.txt", '../api/store/Einstellung/',
      "self.prefs_form", function(responseText) {
        var einstellung = JSON.parse(responseText);
        self.prefs_form(einstellung);
      });
  };
  this.abspieler_liste = function() {
    self.entitaet_liste('Abspieler','../api/store/Abspieler/liste/',
      "data/tpl/abspieler_liste.txt", '../api/store/Abspieler/',
      "self.abspieler_form", function(responseText) {
        var abspieler = JSON.parse(responseText);
        self.abspieler_form(abspieler);
      });
  };
  this.livestream_liste = function() {
    self.entitaet_liste('Livestreams','../api/store/Livestream/liste/',
      "data/tpl/livestream_liste.txt", '../api/store/Livestream/',
      "self.livestream_form", function(responseText) {
        var livestream = JSON.parse(responseText);
        self.livestream_form(livestream);
      });
  };
  this.abspielliste_liste = function() {
    self.entitaet_liste('Abspielliste','../api/store/Abspielliste/liste/',
      "data/tpl/abspielliste_liste.txt", '../api/store/Abspielliste/',
      "self.abspielliste_form", function(responseText) {
        //console.log("responseTest: '" + responseText + "'");
        var abspielliste = JSON.parse(responseText);
        self.abspielliste_form(abspielliste);
      });
  };
  /* -------------------- Entitaets-Formulare ------------------ */
  this.abspielliste_form = function(al) {
    self.entitaet_form('Abspielliste', al, al.name,
      "data/tpl/form_abspielliste.txt", '../api/store/Abspielliste/',
      '#abspielliste-name', function(event) {
          if(event !== undefined) {
            event.preventDefault();
          }
          self.abspielliste_auswahl_fuellen();
          self.abspielliste_liste();
    });
  };
  this.abspieler_form = function(pl) {
    self.entitaet_form('Abspieler', pl, pl.key,
      "data/tpl/form_abspieler.txt", '../api/store/Abspieler/',
      '#abspieler-name', function() {
          self.abspieler_auswahl_fuellen();
          self.abspieler_liste();
    });
  };
  this.livestream_form = function(ls) {
    self.entitaet_form('Livestream', ls, ls.name,
      "data/tpl/form_livestream.txt", '../api/store/Livestream/',
      '#livestream-name', function() {
          self.livestream_liste();
    });
  };
  this.geraet_form = function(ge) {
    self.entitaet_form('Gerät', ge, ge.name,
      "data/tpl/form_geraet.txt", '../api/store/Geraet/',
      '#geraet-name', function() {
          self.geraet_liste();
    });
  };
  this.geraet_status_form = function(ge) {
    self.entitaet_form('Gerät', ge, ge.name,
      "data/tpl/form_geraet_status.txt", '../api/store/Geraet/',
      '#geraet-name', function() {
          self.geraet_schalt_liste();
    });
  };
  this.prefs_form = function(k) {
    self.entitaet_form('Einstellung', k, k.key,
      "data/tpl/form_einstellung.txt", '../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('Katalog', ort, ort.name,
      "data/tpl/form_ablageort.txt", '../api/store/Ablageort/',
      '#ablageort-name', function() {
        self.ablageort_liste();
    });
  };
  /* ------------------------------- UI-Dynamik ----------------------- */
  self.reset_top_buttons = function() {
    self.html_erzeugen("data/tpl/top_btns.txt", '', function (html) {
      document.querySelector(".top-btns").innerHTML = html;
    });
  };
  this.abspieler_auswahl_fuellen = function() {
    self.http_get('../api/store/Abspieler/liste/', function (responseText) {
      self.html_erzeugen("data/tpl/abs_sel.txt", 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.html_erzeugen("data/tpl/pl_sel.txt", JSON.parse(responseText), function (html) {
        document.querySelector(".pl-sel").innerHTML = html;
        self.addEvtListener('#playlist', 'change', function() {
          self.titel_liste();
        });
      });
    });
  };
  /* Unterer Einblendbereich */
  this.dialog_unten_zeigen = function() {
    self.html_erzeugen("data/tpl/ctrl.txt", "", function (html) {
      var dlg = document.querySelector(".dialog-unten");
      //dlg.style.height = '10em';
      dlg.innerHTML = html;
      self.abspieler_auswahl_fuellen();
      self.abspielliste_auswahl_fuellen();
      self.addEvtListener('#dazu-btn', 'click', self.titelDazu);
      self.addEvtListener('#play-btn', 'click', self.play);
      self.addEvtListener('#stop-btn', 'click', function() {
        self.kommando('stop');
      });
      self.addEvtListener('#pause-btn', 'click', function() {
        self.kommando('pause');
      });
      self.addEvtListener('#weiter-btn', 'click', function() {
        self.kommando('weiter');
      });
      self.addEvtListener('#hier-btn', 'click', self.hier_spielen);
/*
    <button class="ctrl-btn ctrl-item" id="hier-btn" title="hier spielen"><i class="icon-tablet"></i></button>
 */
      self.addEvtListener('#weg-btn', 'click', self.titelWeg);
      self.addEvtListener('#leeren-btn', 'click', self.alleTitelEntfernen);
      self.addEvtListener('#media-btn', 'click', self.media_liste);
      self.addEvtListener('#plst-btn', 'click', self.titel_liste);
      self.addEvtListener('#live-btn', 'click', self.livestream_selection);
      self.addEvtListener('#switch-btn', 'click', self.geraet_schalt_liste);
      self.media_liste();
    });
  };
  /* Titel einer Abspielliste */
  this.titel_liste = function() {
    self.reset_top_buttons();
    var plname = document.querySelector('#playlist').value;
    document.querySelector('.bereich-name').textContent = 'Abspielliste ' + plname;
    var bb = document.querySelector('.breadcrumb-behaelter');
    bb.textContent = "";
    self.http_get('../api/alist/' + plname, function (responseText) {
      self.html_erzeugen("data/tpl/titel_liste.txt", 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 bereichName = document.querySelector('.bereich-name').textContent;
    if(bereichName === '') {
      var titel = self.titelErmitteln(document.querySelector(".selected"));
      var playername = document.querySelector('#abspieler').value;
      console.log('plname: ' + playername + ' url: ' + titel.katalogUrl + titel.pfad + titel.name);
      self.http_post('../api/strg/' + playername + '/titel', JSON.stringify(titel), function(responseText) {
        self.meldung_mit_timeout(responseText, 1500);
      });
    } else if(bereichName === 'Livestream-Auswahl') {
      var streamName = document.querySelector(".selected").textContent;
      var playername = document.querySelector('#abspieler').value;
      var stream = new Livestream(streamName, '-');
      self.http_post('../api/strg/' + playername + '/stream', JSON.stringify(stream), function(responseText) {
        self.meldung_mit_timeout(responseText, 1500);
      });
    } else {
      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);
      });
    }
  };
  this.kommando = function(kommando) {
    var abs = document.querySelector('#abspieler').value;
    self.http_get('../api/strg/' + abs + '/' + kommando, function(responseText) {
      self.meldung_mit_timeout(responseText, 1500);
    });
  };
  this.hier_spielen = function() {
    var url;
    // den Host noch vom Server abrufen und den nachfolgenden Code ersetzen
    var host = 'http://' + window.location.host + '/mz';
    console.log('host: ' + host);
    var bereichName = document.querySelector('.bereich-name').textContent;
    if(bereichName === '') {
      var titel = self.titelErmitteln(document.querySelector(".selected"));
      //var playername = document.querySelector('#abspieler').value;
      console.log(' url: ' + titel.katalogUrl + titel.pfad + titel.name);
      //self.http_post('../api/strg/' + playername + '/titel', JSON.stringify(titel), function(responseText) {
      //  self.meldung_mit_timeout(responseText, 1500);
      //});
      url = host + titel.katalogUrl + titel.pfad + titel.name;
      window.open(url);
    } else if(bereichName === 'Livestream-Auswahl') {
      var streamName = document.querySelector(".selected").textContent;
      // hier den Stream-URL abrufen
      //GET /mz/api/store/[typname]/[name]
      self.http_get('../api/store/Livestream/' + streamName, function(responseText) {
        var stream = JSON.parse(responseText);
        url = stream.url;
        window.open(url);
      });
    } else {
      var lst = document.querySelector('#playlist').value;
      console.log(
        "play playlist.value: " + document.querySelector('#playlist').value +
        ", abspieler.value: " + document.querySelector('#abspieler').value);
      // hier noch URL fuer Stream der Abspielliste abrufen
      // Es muss auch noch die Funktion auf dem Server gabut werden, die
      // eine Abspielliste als Stream liefert
      url = 'Stream fuer Abspielliste ' + lst + ' noch nicht gebaut.';
    }
    console.log('url: ' + url);
    //window.open(url);
  };
  /* ------------- Verwaltungsfunktionen Abspielliste -------------------- */
  self.alleTitelEntfernen = function() {
    var plname = document.querySelector('#playlist').value;
    self.http_delete('../api/alist/' + plname + '/alle', '', function(responseText) {
      // DELETE   http://localhost:9090/mz/api/alist/liste1/0
      //self.meldung_mit_timeout(responseText, 1500);
      self.titel_liste();
    });
  };
  this.titelDazu = function() {
    var titel = self.titelErmitteln(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.titelWeg = 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();
    });
  };
  /* ------------- Helfer fuer Entitaets-Formulare ----------------------- */
  /*
   * url: '../api/store/Ablageort/liste/'
   * tpl: "data/tpl/ablageort_liste.txt"
   * 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(bname, listUrl, tpl, storeUrl, formFunc, cb, customListCode) {
    self.reset_top_buttons();
    document.querySelector('.bereich-name').textContent = bname;
    var bb = document.querySelector('.breadcrumb-behaelter');
    bb.textContent = "";
    self.http_get(listUrl, function (responseText) {
      self.html_erzeugen(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)");
        });
        if(typeof(customListCode) !== 'function') {
          // ..
        } else {
          customListCode();
        }
      });
    });
  };
  /*
   * dat: gefuelltes Datenobjekt bei Aenderung
   * key: der alte schluesselbegriff bei Aenderung (z.B. al.name)
   * tpl: "data/tpl/form_abspielliste.txt"
   * 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(bname, dat, key, tpl, url, selector, cb) {
    document.querySelector('.bereich-name').textContent = bname;
    self.html_erzeugen(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());
    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.txt', 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); });
  };
  self.titelErmitteln = function(elem) {
    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);
    }
    return titel;
  };
  /* --------------------- 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_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 () {
      if (this.readyState === 4 && this.status === 200) {
        scallback(this.responseText);
      }
    };
    xhr.open(method, url);
    if (method === 'GET') {
      xhr.send();
    } else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
      xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
      xhr.send(data);
    }
  };
  /* ------------------------ aus App-Vorlage -------------------  */
  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() {
    self.dialog_laden_und_zeigen('data/tpl/dlg-info.tpl', '');
  this.info_dialog_zeigen = function () {
    self.dialog_laden_und_zeigen('data/tpl/dlg-info.txt', '');
    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);
@@ -204,3 +888,46 @@
}
/* ----------- 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 Livestream(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;
}
function Geraet(n, e, a, s, st) {
  this.name = n;
  this.einUrl = e;
  this.ausUrl = a;
  this.statusUrl = s;
  this.status = st;
}