Persoenliche Mediazentrale
ulrich
2021-04-04 b379f5d6fa95c9c938c38ce592739ea732847821
Durchstich Neuer Ablageort
6 files added
10 files modified
702 ■■■■■ changed files
src/de/uhilger/mediaz/App.java 2 ●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/Server.java 48 ●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/api/AblageTestHandler.java 11 ●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/api/StoreHandler.java 125 ●●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/api/StoreTestHandler.java 1 ●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/conf/Store.java 2 ●●●●● patch | view | raw | blame | history
src/mediaz_de_DE.properties 1 ●●●● patch | view | raw | blame | history
www/ui/app.css 145 ●●●●● patch | view | raw | blame | history
www/ui/data/ablageort.css 20 ●●●●● patch | view | raw | blame | history
www/ui/data/ablageort.html 43 ●●●●● patch | view | raw | blame | history
www/ui/data/formulare.css 145 ●●●●● patch | view | raw | blame | history
www/ui/data/menu/hauptmenue.json 5 ●●●●● patch | view | raw | blame | history
www/ui/data/test.html 28 ●●●●● patch | view | raw | blame | history
www/ui/data/tpl/form_ablageort.tpl 13 ●●●●● patch | view | raw | blame | history
www/ui/index.html 37 ●●●●● patch | view | raw | blame | history
www/ui/js/app.js 76 ●●●●● patch | view | raw | blame | history
src/de/uhilger/mediaz/App.java
@@ -53,7 +53,7 @@
   *
   * @param args Kommandozeilenparameter
   */
  public static void main(String[] args) {
  public static void main(String[] args) throws ClassNotFoundException {
    rb = ResourceBundle.getBundle(RB_NAME);
    logger.info(new File(".").getAbsolutePath());
    
src/de/uhilger/mediaz/Server.java
@@ -21,7 +21,11 @@
import de.uhilger.mediaz.api.AblageTestHandler;
import de.uhilger.mediaz.api.FileHandler;
import de.uhilger.mediaz.api.StopServerHandler;
import de.uhilger.mediaz.api.StoreHandler;
import de.uhilger.mediaz.api.StoreTestHandler;
import de.uhilger.mediaz.conf.Store;
import de.uhilger.mediaz.entity.Ablageort;
import de.uhilger.mediaz.entity.ConfigurationElement;
import java.io.File;
import java.io.IOException;
import java.util.logging.Logger;
@@ -30,8 +34,8 @@
import java.util.logging.Level;
/**
 * Die Klasse Server stellt Methoden zur Ausführung eines
 * HTTP-Servers bereit
 * Die Klasse Server stellt Methoden zur Ausführung eines HTTP-Servers
 * bereit
 * 
 * @author Ulrich Hilger
 * @version 0.1, 25.03.2021
@@ -42,6 +46,7 @@
  
  public static final String RB_SERVER_START_MSG = "msgServerStart";
  public static final String RB_WEBROOT = "webroot";
  public static final String RB_STORE = "store";
  //public static final String RB_UI_ROOT = "uiroot";
  public static final String RB_STOP_SERVER = "stopServer";
  public static final String RB_ABLAGE_TEST = "testAblage";
@@ -54,6 +59,7 @@
  
  /**
   * Ein neues Objekt der Kalsse Server erzeugen
   *
   * @param port  der Port, über den dieser Server erreichbar sein soll
   */
  public Server(int port) {
@@ -70,8 +76,9 @@
  }
  
  /**
   * Den Namen des Kontexts angeben, über den dieser Server
   * erreichbar sein soll
   * Den Namen des Kontexts angeben, über den dieser Server erreichbar sein
   * soll
   *
   * @param ctxName Name des Kontexts, unter dem der Server aufrufbar sein soll
   */
  public void setContextName(String ctxName) {
@@ -84,13 +91,14 @@
  }
  
  /**
   * Die Endpunkte ('Context'e) einrichten, unter denen die Dienste
   * dieses Servers erreichbar sein sollen und den Server starten
   * Die Endpunkte ('Context'e) einrichten, unter denen die Dienste dieses
   * Servers erreichbar sein sollen und den Server starten
   * 
   * @throws IOException wenn etwas schief geht, finden sich Angaben
   * in diesem Objekt
   * @throws IOException wenn etwas schief geht, finden sich Angaben in diesem
   * Objekt
   * @throws java.lang.ClassNotFoundException
   */
  public void start() throws IOException {
  public void start() throws IOException, ClassNotFoundException {
    logger.log(Level.INFO, App.getRs(RB_SERVER_START_MSG), Integer.toString(port));
    
    String wwwData = App.getInitParameter(App.getRs(App.RB_AP_WWW_DATA));
@@ -99,7 +107,10 @@
    //File uiDir = new File(ui);
    HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
    server.createContext(ctx + App.getRs(RB_WEBROOT), new FileHandler(wwwDir.getAbsolutePath()));
    server.createContext(ctx + App.getRs(RB_WEBROOT),
            new FileHandler(wwwDir.getAbsolutePath()));
    ablageorteEinklinken(server);
    server.createContext(ctx + App.getRs(RB_STORE), new StoreHandler());
    //server.createContext(ctx + App.getRs(RB_UI_ROOT), new FileHandler(uiDir.getAbsolutePath()));    
    server.createContext(ctx + App.getRs(RB_STOP_SERVER), new StopServerHandler());
    server.createContext(ctx + App.getRs(RB_ABLAGE_TEST), new AblageTestHandler());
@@ -108,4 +119,21 @@
    server.start();
  }
  private void ablageorteEinklinken(HttpServer server) throws ClassNotFoundException, IOException {
    Store store = new Store();
    String conf = App.getInitParameter(App.getRs(App.RB_AP_CONF));
    File ablageortDir = new File(conf, Ablageort.class.getSimpleName());
    File[] orte = ablageortDir.listFiles();
    if (orte != null) {
      for (File ort : orte) {
        ConfigurationElement elem = store.readFromFile(ort);
        if (elem instanceof Ablageort) {
          Ablageort ablageort = (Ablageort) elem;
          server.createContext(ctx + ablageort.getUrl(),
                  new FileHandler(new File(ablageort.getOrt()).getAbsolutePath()));
        }
      }
    }
  }
}
src/de/uhilger/mediaz/api/AblageTestHandler.java
@@ -21,19 +21,20 @@
  @Override
  public void handle(HttpExchange e) throws IOException {
    Ablageort ablage = new Ablageort();
    ablage.setName("Katalog");
    ablage.setOrt("/home/ulrich/Videos");
    Ablageort ablageort = new Ablageort();
    ablageort.setName("Katalog");
    ablageort.setOrt("/home/ulrich/Videos");
    ablageort.setUrl("/media/test");
    
    Gson gson = new Gson();
    File mediaOrdner = new File(ablage.getOrt());
    File mediaOrdner = new File(ablageort.getOrt());
    File[] files = mediaOrdner.listFiles();
    String json = gson.toJson(files);
    
    StringBuilder sb = new StringBuilder();
    sb.append(json);
    
    json = gson.toJson(ablage);
    json = gson.toJson(ablageort);
    sb.append(json);
    
    byte[] b = sb.toString().getBytes();
src/de/uhilger/mediaz/api/StoreHandler.java
New file
@@ -0,0 +1,125 @@
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package de.uhilger.mediaz.api;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import de.uhilger.mediaz.App;
import de.uhilger.mediaz.Server;
import de.uhilger.mediaz.conf.Store;
import de.uhilger.mediaz.entity.Ablageort;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.logging.Logger;
/**
 *
 * @author ulrich
 */
public class StoreHandler extends Store implements HttpHandler  {
  private static final Logger logger = Logger.getLogger(StoreHandler.class.getName());
  /*
    HTTP GET: lies einen Ablageort und schreibe JSON
    HTTP PUT: schreibe einen neuen Ablageort auf die Platte
    HTTP POST: schreibe Aenderungen auf die Platte
    HTTP DELETE: loesche den Ablageort
    Beispiele:
    HTTP GET an /mz/api/store/Ablageort/Katalog
    liest den Ablageort namens "Katalog"
    HTTP POST an /mz/api/store/Ablageort
    schreibt den neuen Ablageort im Body der Anfrage
    HTTP PUT an /mz/api/store/Ablageort
    sucht den Ablageort mit dem Namen laut Body der Anfrage
    und schreibt den Inhalt aus der Anfrage in die Datei
    HTTP DELETE an /mz/api/store/Ablageort/Katalog
    löscht den Ablageort namens "Katalog"
  */
  public static final String HTTP_GET = "GET";
  public static final String HTTP_PUT = "PUT";
  public static final String HTTP_POST = "POST";
  public static final String HTTP_DELETE = "DELETE";
  @Override
  public void handle(HttpExchange e) throws IOException {
    String method = e.getRequestMethod();
    String path = e.getRequestURI().toString();
    String[] elems = path.split(App.getRs(Server.RB_SLASH));
    String type = "";
    String elemName = "";
    switch(method) {
      case HTTP_GET:
        type = elems[elems.length - 2];
        elemName = elems[elems.length - 1];
        //this.readFromFile(file);
        break;
      case HTTP_PUT:
        type = elems[elems.length - 1];
        elemName = "noch bauen: lesen aus Body";
        break;
      case HTTP_POST:
        type = elems[elems.length - 1];
        elemName = bodyLesen(e);
        if(type.equalsIgnoreCase("Ablageort")) {
          Gson gson = new Gson();
          Ablageort ort = gson.fromJson(elemName, Ablageort.class);
          elemName = ort.getName();
        }
        break;
      case HTTP_DELETE:
        type = elems[elems.length - 2];
        elemName = elems[elems.length - 1];
        break;
    }
    String response = "Method: " + method + ", Path: " + path +
            ", Type: " + type + ", elemName: " + elemName;
    logger.info(response);
    e.sendResponseHeaders(200, response.length());
    OutputStream os = e.getResponseBody();
    os.write(response.getBytes());
    os.close();
  }
  private String bodyLesen(HttpExchange e) throws IOException {
    InputStream is = e.getRequestBody();
    BufferedReader r = new BufferedReader(new InputStreamReader(is));
    StringBuilder sb = new StringBuilder();
    String line = r.readLine();
    while(line != null) {
      sb.append(line);
      line = r.readLine();
    }
    r.close();
    // {"Ablageort":{"name":"test1","ort":"test2","url":"test3"}}
    //String data = sb.toString();
    //data = data.substring(1, data.length() - 1);
    //String json = data.substring(data.indexOf("{"));
    // {"name":"test1","ort":"test2","url":"test3"}
    String json = sb.toString();
    logger.info("json: " + json);
    return json;
  }
}
src/de/uhilger/mediaz/api/StoreTestHandler.java
@@ -30,6 +30,7 @@
    Ablageort ort = new Ablageort();
    ort.setName("Katalog");
    ort.setOrt("/home/ulrich/Videos");
    ort.setUrl("/media/test");
    Store store = new Store();
    File file = store.writeToFile(ort);
    try {
src/de/uhilger/mediaz/conf/Store.java
@@ -94,4 +94,6 @@
    logger.info(parts[parts.length-2]);
    return parts[parts.length-2];
  }
}
src/mediaz_de_DE.properties
@@ -9,6 +9,7 @@
# HTTP-Endpunkte
webroot=/
# uiroot=/ui
store=/api/store
stopServer=/api/server/stop
testAblage=/api/test/ablage
testStore=/api/test/store
www/ui/app.css
@@ -117,3 +117,148 @@
  font-size: 1.3em;
  color: #b8b8b8;
}
/* ab hier Mediazentrale */
.entity-formular {
  display: flex;
  flex-flow: column;
}
.entity-element {
  margin: 0.4rem;
}
/*
@media (min-width: 800px) {
  .zentrum-behaelter {
    padding: 0 1em 0 1em;
  }
  .zentrum-behaelter, .nord {
    margin: 0 10% 0 10%;
  }
}
/* von Skeleton */
/* Buttons
–––––––––––––––––––––––––––––––––––––––––––––––––– */
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
  display: inline-block;
  height: 38px;
  padding: 0 30px;
  color: #555;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  line-height: 38px;
  letter-spacing: .1rem;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  background-color: transparent;
  border-radius: 4px;
  border: 1px solid #bbb;
  cursor: pointer;
  box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
  color: #333;
  border-color: #888;
  outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
  color: #FFF;
  background-color: #33C3F0;
  border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
  color: #FFF;
  background-color: #1EAEDB;
  border-color: #1EAEDB; }
/* Forms
–––––––––––––––––––––––––––––––––––––––––––––––––– */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
  height: 38px;
  padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
  background-color: #fff;
  border: 1px solid #D1D1D1;
  border-radius: 4px;
  box-shadow: none;
  box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none; }
textarea {
  min-height: 65px;
  padding-top: 6px;
  padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
  border: 1px solid #33C3F0;
  outline: 0; }
label,
legend {
  display: block;
  margin-bottom: .5rem;
  font-weight: 600; }
fieldset {
  padding: 0;
  border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
  display: inline; }
label > .label-body {
  display: inline-block;
  margin-left: .5rem;
  font-weight: normal; }
www/ui/data/ablageort.css
New file
@@ -0,0 +1,20 @@
/*
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
*/
/*
    Created on : 04.04.2021, 17:02:13
    Author     : ulrich
*/
.entity-formular {
  display: flex;
  flex-flow: column;
}
.entity-element {
  margin: 0.4rem;
}
www/ui/data/ablageort.html
New file
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<!--
  Mediazentrale - Personal Media Center
  Copyright (C) 2021  Ulrich Hilger
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as
  published by the Free Software Foundation, either version 3 of the
  License, or (at your option) any later version.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.
  You should have received a copy of the GNU Affero General Public License
  along with this program.  If not, see <https://www.gnu.org/licenses/>.
-->
<html>
  <head>
    <title>Ablageort</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="../app.css">
    <link rel="stylesheet" type="text/css" href="formulare.css">
    <link rel="stylesheet" type="text/css" href="ablageort.css">
  </head>
  <body>
    <div class="entity-formular">
      Ablageort
      <input class="entity-element" type="text" id="ablageort-name" placeholder="Name" >
      <input class="entity-element" type="text" id="ablageort-ort" placeholder="Pfad" >
      <input class="entity-element" type="text" id="ablageort-url" placeholder="URL" >
      <div class="entity-buttons">
        <button class="button-primary" id="ok-btn">Speichern</button>
        <button class="button" id="cancel-btn">Abbrechen</button>
      </div>
    </div>
  </body>
</html>
www/ui/data/formulare.css
New file
@@ -0,0 +1,145 @@
/*
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
*/
/*
    Created on : 04.04.2021, 17:05:29
    Author     : ulrich
*/
@media (min-width: 800px) {
  .zentrum-behaelter {
    padding: 0 1em 0 1em;
  }
  .zentrum-behaelter, .nord {
    margin: 0 10% 0 10%;
  }
}
/* von Skeleton */
/* Buttons
–––––––––––––––––––––––––––––––––––––––––––––––––– */
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
  display: inline-block;
  height: 38px;
  padding: 0 30px;
  color: #555;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  line-height: 38px;
  letter-spacing: .1rem;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  background-color: transparent;
  border-radius: 4px;
  border: 1px solid #bbb;
  cursor: pointer;
  box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
  color: #333;
  border-color: #888;
  outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
  color: #FFF;
  background-color: #33C3F0;
  border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
  color: #FFF;
  background-color: #1EAEDB;
  border-color: #1EAEDB; }
/* Forms
–––––––––––––––––––––––––––––––––––––––––––––––––– */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
  height: 38px;
  padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
  background-color: #fff;
  border: 1px solid #D1D1D1;
  border-radius: 4px;
  box-shadow: none;
  box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none; }
textarea {
  min-height: 65px;
  padding-top: 6px;
  padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
  border: 1px solid #33C3F0;
  outline: 0; }
label,
legend {
  display: block;
  margin-bottom: .5rem;
  font-weight: 600; }
fieldset {
  padding: 0;
  border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
  display: inline; }
label > .label-body {
  display: inline-block;
  margin-left: .5rem;
  font-weight: normal; }
www/ui/data/menu/hauptmenue.json
@@ -8,6 +8,11 @@
    },
    "inhalt":  [
      {
        "titel": "Neuer Ablageort",
        "umenue": false,
        "funktion": "app.form_ablageort_neu"
      },
      {
        "titel": "Seite umschalten",
        "umenue": false,
        "funktion": "app.seitenleiste_umschalten"
www/ui/data/test.html
New file
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<!--
  Mediazentrale - Personal Media Center
  Copyright (C) 2021  Ulrich Hilger
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as
  published by the Free Software Foundation, either version 3 of the
  License, or (at your option) any later version.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.
  You should have received a copy of the GNU Affero General Public License
  along with this program.  If not, see <https://www.gnu.org/licenses/>.
-->
<html>
  <head>
    <title>TODO supply a title</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body>
    <div>TODO write content</div>
  </body>
</html>
www/ui/data/tpl/form_ablageort.tpl
New file
@@ -0,0 +1,13 @@
    <div class="entity-formular">
      Ablageort
      <input class="entity-element" type="text" id="ablageort-name" placeholder="Name" />
      <input class="entity-element" type="text" id="ablageort-ort" placeholder="Pfad" />
      <input class="entity-element" type="text" id="ablageort-url" placeholder="URL" />
      <div class="entity-buttons">
        <button class="button-primary" id="ok-btn">Speichern</button>
        <button class="button" id="cancel-btn">Abbrechen</button>
      </div>
    </div>
www/ui/index.html
@@ -1,7 +1,24 @@
<!DOCTYPE html>
<!--
  Mediazentrale - Personal Media Center
  Copyright (C) 2021  Ulrich Hilger
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as
  published by the Free Software Foundation, either version 3 of the
  License, or (at your option) any later version.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.
  You should have received a copy of the GNU Affero General Public License
  along with this program.  If not, see <https://www.gnu.org/licenses/>.
-->
<html>
  <head>
    <title>App-Vorlage</title>
    <title>Mediazentrale</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="apple-mobile-web-app-capable" content="yes" />
@@ -21,7 +38,7 @@
        </div>
      </div>
      <div class="app-titel">
        <span id="app-titel">App-Vorlage</span>
        <span id="app-titel">Mediazentrale</span>
      </div>
    </div>
    <div class="inhalt">
@@ -36,19 +53,7 @@
        <div class="zentrum">
          <div class="zentraler-inhalt">
            <p>
              Hier kann beliebiger Inhalt erscheinen.
            </p>
            <p>
              Wenn dessen Darstellung mehr
              Platz benötigt als das Anzeigegerät bietet wird ein
              Rollbalken eingeblendet. Beim Rollen zu anfangs nicht sichtbaren
              Teilen des Inhalts bleiben die den Inhaltsbereich
              umschließenden Elemente sichtbar.
            </p>
            <p>
              Ein Klick auf das Hamburger-Piktogramm oben links bzw. dessen
              Antippen blendet ein Menü ein von dem aus weitere Funktionen
              ausgelöst werden können.
              Hier erschient der Media-Inhalt.
            </p>
          </div>
        </div>
@@ -70,7 +75,7 @@
    <script>
        var app;
        document.addEventListener('DOMContentLoaded', function () {
            app = new AppVorlage();
            app = new Mediazentrale();
            app.init();
        });
    </script>
www/ui/js/app.js
@@ -1,10 +1,84 @@
function AppVorlage() {
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
  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
        });
      });
      self.addEvtListener('#cancel-btn', 'click', function() {
        // hier die Aktion abbrechen
      });
    });
  };
  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.http_post = function(u, data, cb) {
    self.http_call('POST', 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') {
      xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
      xhr.send(data);
    }
  };
  this.serialisieren = function(obj) {
    return '{"' + obj.constructor.name + '":' + JSON.stringify(obj) + '}';
  };
  /* ab hier aus App-Vorlage */
  this.init = function() {
    //self.vorlagen = new Vorlagen();
    self.cache = new Array();