From 9c2c3c7fa694464e8eb16affe9fdca9363ee0b05 Mon Sep 17 00:00:00 2001
From: ulrich <undisclosed>
Date: Sat, 24 Feb 2018 19:18:24 +0000
Subject: [PATCH] radio-ui-Vorlage hinzugefuegt

---
 web/data/menu/untermenue-1.json                        |   27 
 web/data/menu/hauptmenue.json                          |   32 +
 web/data/menu/sendermenue.json                         |   27 
 web/hamburger.css                                      |   99 +++
 web/data/tpl/dlg-msg.tpl                               |    6 
 web/pirc.md                                            |   45 +
 web/data/tpl/dlg-sender-neu.tpl                        |   17 
 web/data/menu/abspielermenue.json                      |   27 
 web/data/tpl/abspieler-eintrag.tpl                     |    1 
 web/app.css                                            |  376 ++++++++++++
 web/data/tpl/sender.tpl                                |   12 
 web/data/tpl/dlg-abspieler-neu.tpl                     |   27 
 web/data/tpl/dlg-sender-edit-form.tpl                  |   17 
 web/README.md                                          |  206 ++++++
 web/data/tpl/kachel.tpl                                |    6 
 web/data/tpl/dlg-abspieler-loeschen.tpl                |   13 
 web/data/tpl/abspieler.tpl                             |    9 
 web/data/tpl/dlg-sender-edit.tpl                       |    8 
 web/data/tpl/dlg-abspieler-edit-form.tpl               |   32 +
 web/radiostreams.md                                    |   43 +
 web/data/tpl/dlg-info.tpl                              |    8 
 web/data/menu/untermenue-2.json                        |   27 
 web/app.js                                             |  475 +++++++++++++++
 web/data/msg-test.json                                 |    4 
 web/data/sender.json                                   |   31 +
 web/data/abspieler.json                                |   31 +
 src/java/de/uhilger/radiozentrale/web/Initialiser.java |   13 
 web/beispiel.md                                        |   45 +
 web/data/tpl/dlg-sender-loeschen.tpl                   |   13 
 web/index.html                                         |   64 ++
 30 files changed, 1,729 insertions(+), 12 deletions(-)

diff --git a/src/java/de/uhilger/radiozentrale/web/Initialiser.java b/src/java/de/uhilger/radiozentrale/web/Initialiser.java
index 7b257db..ca7f4aa 100644
--- a/src/java/de/uhilger/radiozentrale/web/Initialiser.java
+++ b/src/java/de/uhilger/radiozentrale/web/Initialiser.java
@@ -38,7 +38,7 @@
 import javax.servlet.ServletContextListener;
 
 /**
- * Initialisierungsklasse der Anwendung Nutzerverwaltung
+ * Initialisierungsklasse der Anwendung radiozentrale
  * 
  * Hier wird u.a. geprüft, ob die Datenbank vorhanden ist und diese 
  * angelegt, falls nicht. Das SQL-Skript zur Anlage der Datenbank findet 
@@ -138,17 +138,6 @@
       if(!dbVorhanden(db, sql.getProperty(SQL_DB_VORHANDEN))) {
         logger.info("Datenbank ist nicht vorhanden");
         int[] ergebnis = db.executeScript(getSqlSkript());
-        /*
-        User admin = new User();
-        admin.setId("admin");
-        admin.setPw("admin");
-        admin.setFirstName("admin");
-        admin.setLastName("admin");
-        admin.setEmail("none");
-        UserMgr um = new UserMgr();
-        um.setServletContext(servletContext);
-        um.createUser(admin);
-        */
       }
     } catch(Exception ex) {
       logger.log(Level.INFO, ex.getMessage(), ex);
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..ff8bc8d
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,206 @@
+# radio-ui
+
+Eine Bedienoberfläche für Webradio
+
+## Funktionen
+
+Das radio-ui zeigt eine Übersicht von Radiosendern. Antippen eines Senders in der Übersicht spielt den [Livestream](/gitblit/doc/web!radio-ui.git/master/radiostreams.md) des Senders ab.
+
+Radiosender können hinzugefügt, geändert und gelöscht werden. Ein Suchfeld ermöglicht das Einschränken der Übersicht auf bestimmte Sender.
+
+Das Abspiel-Ziel ist konfigurierbar: Ein Radionsender kann entweder auf dem Gerät abgespielt werden, auf dem das radio-ui im Browser läuft oder auf einem anderen Gerät wie z.B. einem [Raspberry Pi](/gitblit/doc/web!radio-ui.git/master/pirc.md).
+
+## Protoyp, Demo
+
+Das radio-ui als einzelne Webanwendung hat prototypischen Charakter, es kann für allerlei Webradio-Anwendungen die wiederverwendbare Grundlage bilden.
+
+Als reinem Client-Teil solcher Anwendungen fehlt dem radio-ui ein zentraler Anwendungskern mit Funktionen zum Speichern individueller Einstellungen an einem zentralen Ort. Die Trennung von Client und Server ist bewußt so gewählt, um einen gleichartigen Client-Teil für verschiedene Implementierungen auf der Serverseite zu ermöglichen.
+
+Der aktuelle Stand des Codes in diesem Repository kann hier ausprobiert werden: [Link zur Demo](/data/ulrich/test/radio-ui).
+
+Code und Demo des radio-ui [veranschaulichen](/gitblit/doc/web!radio-ui.git/master/beispiel.md) zusätzlich zum praktischen Nutzen als Webradiofernbedienung verschiedene Aspekte des Baus von Webanwendungen.
+
+## Abhängigkeiten
+
+Neben dem Code in diesem Repository sind zur Nutzung des radio-ui die folgenden Komponenten erforderlich
+
++ [jQuery](http://jquery.com/)
++ [Mustache](https://github.com/janl/mustache.js)
++ [app-menu](/gitblit/docs/web!app-menu.git)
+
+Diese sind in der Datei `index.html` über die folgenden Einträge eingebunden.
+
+````
+<link rel="stylesheet" type="text/css" href="../jslib/app-menu/app-menu.css">
+<script src="../jslib/jquery-1.11.1/jquery-1.11.1.min.js"></script>
+<script src="../jslib/mustache/mustache.min.js"></script>
+<script src="../jslib/app-menu/app-menu.js"></script>
+````
+
+Sollen einzelne Komponenten von einem anderen Ort, beispielsweise aus einem Content Delivery Network (CDN) eingebunden werden, müssen diese Einträge angepasst werden.
+
+### Zeichensätze
+
+Neben den obigen Komponenten werden zudem die Zeichensätze `Amethysta`, `Oswald` und `Roboto Condensed` verwendet. Sie können von Google Fonts heruntergeladen werden und sind über das Stylesheet `app.css` von folgenden Orten auf dem Server eingebunden:
+
+````
+/lib/fonts/Amethysta/Amethysta-Regular.ttf
+/lib/fonts/Oswald/Oswald-Regular.ttf
+/lib/fonts/Oswald/Oswald-Bold.ttf
+/lib/fonts/Roboto_Condensed/RobotoCondensed-Regular.ttf
+/lib/fonts/Roboto_Condensed/RobotoCondensed-Bold.ttf
+````
+
+### Bilder
+
+Zu jedem Sender wird an der Bedienoberfläche des radio-ui ein Piktogramm angezeigt. In der Demo des radio-ui ist hierfür der Ordner `bilder` vorgesehen. Prinzipiell kann aber jeder Ablageort für Bilder verwendet werden. Jedem Sender ist ein URL zu einem Bild über die Datei `sender.json` zugeordnet. Der URL für ein Bild kann zur Laufzeit über die Funktion `Sender ändern` geändert werden.
+
+## Installation
+
+Zur Verwendung des radio-ui muss dessen Code mit den Komponenen kombiniert werden, die vom radio-ui benötigt werden.
+
+1. Code aus diesem Repository herunterladen
+1. Die unter Abhängigkeiten oben angegebenen Komponenten herunterladen
+1. Aus den heruntergeladenen Teilen eine Ordnerstruktur bilden
+
+````
+radio-ui/
+  hier der Inhalt aus diesem Repository
+jslib/
+  jquery-1.11.1/
+    jquery-1.11.1.min.js
+  mustache/
+    mustache.min.js
+  app-menu/
+    hier der Inhalt aus dem app-menu-Repository
+lib/
+  fonts/
+    Amethysta/
+      Amethysta-Regular.ttf
+    Oswald/
+      Oswald-Regular.ttf
+      Oswald-Bold.ttf
+    Roboto_Condensed/
+      RobotoCondensed-Regular.ttf
+      RobotoCondensed-Bold-ttf
+bilder/
+  hier Bilder nach Wahl
+````
+
+Die obige Ordnerstruktur wird auf einem HTTP-Server wie z.B. [Tomcat](https://de.wikipedia.org/wiki/Apache_Tomcat) oder [lighttpd](https://de.wikipedia.org/wiki/Lighttpd) hinterlegt und kann dann mit einem Browser aufgerufen werden.
+
+## Noch zu erledigen
+
+### Templates für Mustache dynamisch laden
+
+Spart Ladezeit zu Beginn. Lösungsansatz: Eine Funktion, die als Parameter das Element erhält, dessen html mit dem gerenderten Template ersetzt werden soll und den zu rendernden Inhalt. Die dynamische Ladefunktion
+
+1. prüft, ob das Template schon im cache ist
+1. Lädt das Template, wenn nicht
+1. wartet, bis das Template geladen ist
+1. rendert das Template mit dem übergebenen Inhalt
+1. schreibt das Ergebnis ins html des übergebenen Elements
+
+Der letzte Teil des Ablaufs muss asynchron gefasst sein, damit auf das Laden gewartet werden kann.
+
+## Schnittstellenbeschreibung
+
+Für den Prototyp des radio-ui können verschiedene Arten des Umgangs mit Sendern und Abspielern hergestellt werden. Die folgenden Angaben zeigen die Punkte auf, an denen eine konkrete Implementierung ansetzen kann.
+
+### Sender
+
+#### Senderliste lesen
+
+Aufruf
+
+````
+data/sender.json
+````
+
+Antwort
+
+````
+{
+  "senderliste": {
+    "titel": "Senderliste",
+    "inhalt":  [
+      {
+        "senderid": 1,
+        "sendername": "hr info",
+        "senderurl": "http://hr-hrinfo-live.cast.addradio.de/hr/hrinfo/live/mp3/128/stream.mp3",
+        "senderlogo": "../bilder/hr-info.png"
+      },
+      {
+        "senderid": 2,
+        "sendername": "radiobob",
+        "senderurl": "http://bob.hoerradar.de/radiobob-live-mp3-hq",
+        "senderlogo": "../bilder/radio-bob-logo-80.png"
+      },
+      usw.
+    ]
+  }
+}
+````
+
+#### Neuen Sender speichern
+
+Noch nicht realisiert.
+
+#### Änderungen an einem Sender speichern
+
+Noch nicht realisiert.
+
+#### Sender löschen
+
+Noch nicht realisiert.
+
+### Abspieler
+
+#### Abspielerliste lesen
+
+Aufruf
+
+````
+data/abspieler.json
+````
+
+Antwort
+
+````
+{
+  "abspielerliste": {
+    "titel": "Abspielerliste",
+    "inhalt":  [
+      {
+        "abspielerid": 1,
+        "abspielername": "dieses Gerät",
+        "abspielerurl": "",
+        "abspielertyp":"lokal",
+        "abspielerbild": "",
+        "abspielerzustand":""
+      },
+      {
+        "abspielerid": 2,
+        "abspielername": "Raspberry Pi Wohnzimmer",
+        "abspielerurl": "",
+        "abspielertyp":"pirc",
+        "abspielerbild": "",
+        "abspielerzustand":"selected"
+      },
+      usw.
+    ]
+  }
+}
+````
+
+#### Neuen Abspieler speichern
+
+Noch nicht realisiert.
+
+#### Änderungen an einem Abspieler speichern
+
+Noch nicht realisiert.
+
+#### Abspieler löschen
+
+Noch nicht realisiert.
diff --git a/web/app.css b/web/app.css
new file mode 100644
index 0000000..3841a47
--- /dev/null
+++ b/web/app.css
@@ -0,0 +1,376 @@
+
+/*
+  app-layout anfang
+
+  Aus dem app-layout-Projekt übernommen und
+  teilweise angepasst
+*/
+
+html, body {
+  margin: 0;
+  padding: 0;
+  height: 100%; /* Anmerkung 2 */
+  font-size: large;
+  /* font-size: initial; */
+  /* font-size: 1.5em; */   /* currently ems cause chrome bug misinterpreting rems on body element */
+  line-height: 1.6;
+  font-weight: 400;
+  font-family: RobotoCondensed;
+}
+body {
+  min-height: 0; /* Anmerkung 1 */
+  display: flex;
+  flex-flow: column;
+}
+.inhalt {
+  display: flex;
+  flex-flow: row;
+  height: 100%; /* Anmerkung 2 */
+  min-height: 0; /* Anmerkung 1 */
+}
+.nord {
+  background-color: lightgray;
+  display: flex;
+  flex-flow: row;
+  height: 2.5em;
+  align-items: center;
+}
+.sued {
+  background-color: lightgray;
+}
+.west {
+  flex-grow: 0;
+  flex-shrink: 0;
+  flex-basis: 4em;
+  background-color: antiquewhite;
+  transition: all 0.3s ease-in;
+  overflow: hidden;
+  white-space: nowrap;
+  background-color: #f7f7f7;
+}
+.ost {
+  flex-grow: 0;
+  flex-shrink: 0;
+  flex-basis: 10em;
+  background-color: antiquewhite;
+}
+.zentrum {
+  display: flex;
+  flex-flow: row;
+  overflow-x: hidden;
+  margin: 0;
+}
+
+/*
+  Anmerkungen:
+  1.) min.height: 0 fuer body und inhalt ist gegen einen Bug, vgl.
+      http://stackoverflow.com/questions/33859811/understanding-flexbox-and-overflowauto
+  2.) height 100% fuer html, body und inhalt sorgt dafuer, dass sich alles
+      immer ueber das gesamte Browserfenster ausdehnt.
+*/
+
+/* app-layout ende */
+
+#nav-menu {
+  /* padding-top: 0.5em;*/
+  width: 1.8em;
+}
+
+#app-titel {
+  padding-left: 1em;
+}
+
+.pointer-cursor {
+  cursor: pointer;
+}
+
+.sendersuche {
+  padding-left: 0.5em;
+}
+
+.zentrum-behaelter {
+  display: flex;
+  flex-flow: column;
+  background-color: #eaeaea;
+  width: 100%;
+}
+
+.inline-label {
+  display: inline-block;
+  width: 4em;
+  text-align: right;
+}
+
+/* Dialogfeld */
+
+.dialog {
+  position: relative;
+}
+
+.dlg-behaelter {
+  padding: 0.4em;
+}
+
+.dlg-info {
+  background-color: #dcf2fb; // blau
+  padding: 0.4em;
+}
+
+.nachricht {
+  background-color: #fffbde; // gelb
+}
+
+.form-button-footer {
+  text-align: right;
+}
+
+.dlg-sender-edit, .dlg-sender-neu, .dlg-abspieler-neu {
+  background-color: #f6f6f6; // grau
+}
+
+.sender-eingabe, .abspieler-eingabe {
+  width: 80%;
+  margin-left: 0.4em;
+}
+
+/*
+  Close Button
+
+  <div>
+    <span class="close-btn">&#10006;</span>
+  </div>
+*/
+
+.close-btn {
+  position: absolute;
+  top: 0px;
+  right: 0.4em;
+  margin: 0;
+  padding: 0;
+  font-size: 1.3em;
+  color: #b8b8b8;
+}
+
+/* Abspieler */
+
+.abspieler-behaelter {
+  display: flex;
+  flex-flow: row;
+  padding: 0.6em;
+}
+
+.abspieler-lb {
+  width: 4.5em;
+  padding-right: 0.2em;
+}
+
+.abspieler-auswahl, .abspieler-form-auswahl {
+  width: 70%;
+  margin-left: 0.4em;
+}
+
+#abspielen-stoppen {
+  margin-left: 0.4em;
+  width: 2em;
+  font-size: 0.8em;
+  padding: 0 0 0 0.3em;
+}
+
+/* Sender */
+
+.sender-behaelter {
+  display: flex;
+  flex-flow: column;
+}
+
+.sender {
+  display: flex;
+  flex-flow: row;
+  padding: 0.4em;
+  flex-wrap: wrap;
+  background-color: #eaeaea;
+}
+
+.sender-kachel {
+  display: flex;
+  flex-flow: column;
+  align-items: center;
+  margin: 0.4em;
+  padding: 0.4em;
+  background-color: white;
+  cursor: pointer;
+}
+
+.sender-name {
+  font-family: RobotoCondensedBold;
+}
+
+.sender-bild {
+  width: 5em;
+  background-color: #ebe6e6;
+}
+
+/* Menü-Erscheinung */
+
+.app-menu-kopf {
+  font-family: Oswald;
+}
+
+
+/* aus Skeleton (leicht angepasst) */
+
+/* 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;
+  font-family: RobotoCondensed;
+  font-size: 0.8em;
+}
+/* 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; }
+
+
+/* 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; }
+
+/* Ende Skeleton */
+
+/* fonts */
+
+@font-face {
+    font-family: 'Amethysta';
+    font-style: normal;
+    font-weight: 700;
+    src: url(/lib/fonts/Amethysta/Amethysta-Regular.ttf) format('truetype');
+}
+@font-face {
+    font-family: 'Oswald';
+    font-style: normal;
+    font-weight: 700;
+    src: url(/lib/fonts/Oswald/Oswald-Regular.ttf) format('truetype');
+}
+@font-face {
+    font-family: 'OswaldBold';
+    font-weight: bold;
+    src: url(/lib/fonts/Oswald/Oswald-Bold.ttf) format('truetype');
+}
+@font-face {
+    font-family: 'RobotoCondensed';
+    font-weight: normal;
+    src: url(/lib/fonts/Roboto_Condensed/RobotoCondensed-Regular.ttf) format('truetype');
+}
+@font-face {
+    font-family: 'RobotoCondensedBold';
+    font-weight: bold;
+    src: url(/lib/fonts/Roboto_Condensed/RobotoCondensed-Bold.ttf) format('truetype');
+}
diff --git a/web/app.js b/web/app.js
new file mode 100644
index 0000000..7647533
--- /dev/null
+++ b/web/app.js
@@ -0,0 +1,475 @@
+var TPL_HOME = 'home';
+var TPL_PREFS = 'prefs';
+var TPL_SENDER = 'sender';
+var TPL_DLG_MSG = 'dlg-msg';
+var TPL_DLG_INFO = 'dlg-info';
+var TPL_DLG_SENDER_NEU = 'dlg-sender-neu';
+var TPL_DLG_SENDER_EDIT = 'dlg-sender-edit';
+var TPL_DLG_SENDER_DEL = 'dlg-sender-del';
+var TPL_SENDER_EDIT_FORM = 'sender-edit-form';
+var TPL_SENDER_KACHEL = 'sender-kachel';
+var TPL_ABSPIELER = 'abspieler';
+var TPL_DLG_ABSPIELER_NEU = 'dlg-abspieler-neu';
+var TPL_DLG_ABSPIELER_EDIT_FORM = 'dlg-abspieler-edit-form';
+var TPL_DLG_ABSPIELER_DEL = 'dlg-abspieler-del';
+var TPL_ABSPIELER_EINTRAG = 'abspieler-eintrag';
+var templateCache = {}; // mustache templates
+var prefsRendered = false;
+var senderKlickModus = "1"; // 1=abspielen, 2=bearbeiten, 3=loeschen
+var ID_UNDEFINED = -1;
+var A_ID_UNDEFINED = -1;
+var suchTimeout;
+
+function app_init() {
+  $('.sued').text('Initialisieren..');
+  $('.dialog').hide();
+  $('.ost').hide();
+
+  // Menü initialisieren
+  app_menu_init(
+    "data/menu/",
+    "hauptmenue.json",
+    "../jslib/app-menu/app-menu.tpl",
+    ".west",
+    "8em");
+
+  // Templates laden
+  app_get_template('data/tpl/dlg-msg.tpl', TPL_DLG_MSG);
+  app_get_template('data/tpl/dlg-info.tpl', TPL_DLG_INFO);
+  app_get_template('data/tpl/sender.tpl', TPL_SENDER);
+  app_get_template('data/tpl/dlg-sender-neu.tpl', TPL_DLG_SENDER_NEU);
+  app_get_template('data/tpl/dlg-sender-edit.tpl', TPL_DLG_SENDER_EDIT);
+  app_get_template('data/tpl/dlg-sender-loeschen.tpl', TPL_DLG_SENDER_DEL);
+  app_get_template('data/tpl/dlg-sender-edit-form.tpl', TPL_SENDER_EDIT_FORM);
+  app_get_template('data/tpl/kachel.tpl', TPL_SENDER_KACHEL);
+  app_get_template('data/tpl/abspieler.tpl', TPL_ABSPIELER);
+  app_get_template('data/tpl/dlg-abspieler-neu.tpl', TPL_DLG_ABSPIELER_NEU);
+  app_get_template('data/tpl/abspieler-eintrag.tpl', TPL_ABSPIELER_EINTRAG);
+  app_get_template('data/tpl/dlg-abspieler-loeschen.tpl', TPL_DLG_ABSPIELER_DEL);
+  app_get_template('data/tpl/dlg-abspieler-edit-form.tpl', TPL_DLG_ABSPIELER_EDIT_FORM);
+
+  // Klicks auf das Hamburger-Menü-Icon an eine Funktion leiten
+  $(".hamburger").on("click", function(e) {
+    app_menue_umschalten();
+  });
+
+  // Eingaben ins Suchfeld an eine Funktion leiten
+  $(".sendersuche-eingabe").on('keyup', function() {
+    clearTimeout(suchTimeout);
+    suchTimeout = setTimeout('app_sendersuche()', 500);
+  });
+
+  // Abspieler und Sender laden
+  setTimeout(function() {
+    app_get_abspieler();
+    app_get_sender();
+    $('.sued').text('Bereit.');
+  }, 200);
+}
+
+function app_sendersuche() {
+  var suchbegriff = $(".sendersuche-eingabe").val();
+
+  app_meldung_mit_timeout(
+    'Suchbegriff: ' + suchbegriff,
+    1500
+  );
+
+  /*
+    nachfolgend wird ueber alle divs der Klasse sender-kachel
+    iteriert und diejenigen mit .hide() ausgeblendet, die nicht
+    den Suchbegiff im Namen tragen
+
+    evtl. findet sich spaeter noch eine elegantere Suchfunktion..
+  */
+
+  var divs = $(".sender-kachel");
+  if(suchbegriff.length > 0) {
+    divs.each(function(index, element) {
+      if ($(element).html().indexOf(suchbegriff) < 0) {
+        $(element).hide();
+      } else {
+        $(element).show();
+      }
+    });
+  } else {
+    $(divs).show();
+  }
+}
+
+function app_menue_umschalten() {
+  $(".hamburger").toggleClass("is-active"); // hamburger-icon umschalten
+  app_menu_toggle(); // menue oeffnen/schliessen
+}
+
+/* Abspieler und abspielen */
+
+function app_get_abspiel_ziel() {
+  var opt = $(".abspieler-auswahl :selected");
+  var atyp = opt.attr("atyp");
+  var aname = opt.text();
+  var aurl = opt.attr("aurl");
+  var zustand = opt.attr("standardzustand");
+  return new Abspieler(-1, aname, aurl, atyp, "", zustand);
+}
+
+/*
+  Abspiel-URL für Raspi:
+  http://raspi:8080/pirc/sys/rpc?c=de.uhilger.pirc.App&m=abspielen&p=http://bob.hoerradar.de/radiobob-live-mp3-hq&p=
+
+  der folgende Aufruf übergibt zusätzlich einen Parameter zum Abspielen über
+  das Audiokabel (nicht das HDMI-Kabel) des Raspi (sofern angeschlossen)
+  http://raspi-wz:8080/pirc/sys/rpc?c=de.uhilger.pirc.App
+  &m=abspielenMitParametern&p=http://bob.hoerradar.de/radiobob-live-mp3-hq
+  &p=-o%20local&p=
+*/
+function app_abspielen(sender, abspieler) {
+  var url;
+  if(abspieler.typ === 'pirc') {
+    url =
+      abspieler.url +
+      '/sys/rpc?c=de.uhilger.pirc.App&m=abspielenMitParametern&p=' +
+      sender.url +
+      '&p=-o%20local&p=';
+    app_radio_kommando_senden(url);
+  } else {
+    url = sender.url;
+    var win = window.open(url, '_blank');
+    win.focus();
+  }
+}
+
+function app_abspielen_stoppen() {
+  var abspieler = app_get_abspiel_ziel();
+  if(abspieler.typ === 'pirc') {
+    var url =
+      abspieler.url +
+      '/sys/rpc?c=de.uhilger.pirc.App&m=kommando&p=q';
+    app_radio_kommando_senden(url);
+  } else {
+    // 'hier abspielen' stoppen muss noch gebaut werden
+  }
+}
+
+function app_abspieler_neu(abspieler) {
+  var html = Mustache.render(templateCache[TPL_ABSPIELER_EINTRAG], abspieler);
+  if(abspieler.zustand === 'selected') {
+    $.each($(".abspieler-auswahl option:selected"), function() {
+       $(this).prop('selected', false);
+    });
+  }
+  $(".abspieler-auswahl").append(html);
+}
+
+function app_abspieler_entfernen(abspieler) {
+  $(".abspieler-auswahl :selected").remove();
+}
+
+/* Senderliste */
+
+function app_kachel_anpassen(sender) {
+  var kachel = $( "div[sid='" + sender.id + "']" );
+  kachel.find(".sender-name").text(sender.name);
+  kachel.attr("data-verweis", sender.url);
+  kachel.find("img").attr("src", sender.logo);
+}
+
+function app_kachel_neu(sender) {
+  var html = Mustache.render(templateCache[TPL_SENDER_KACHEL], sender);
+  $("#sender").append(html);
+  var kachel = $( "div[sid='" + sender.id + "']" );
+  kachel.on('click', app_kachel_klick);
+}
+
+function app_kachel_entfernen(sender) {
+  $( "div[sid='" + sender.id + "']" ).remove();
+}
+
+function app_kachel_klick() {
+  var kachel = $( this );
+  var senderId = kachel.attr("sid");
+  var senderName = kachel.find(".sender-name").text();
+  var senderUrl = kachel.attr("data-verweis");
+  var senderLogo = kachel.find("img").attr("src");
+  var s = new Sender(senderId, senderName, senderUrl, senderLogo);
+  switch (senderKlickModus) {
+      case "1":
+        app_abspielen(s, app_get_abspiel_ziel());
+        break;
+      case "2":
+        $(".dlg-behaelter").html(
+          Mustache.render(templateCache[TPL_SENDER_EDIT_FORM], s)
+        );
+        $('#sender-speichern').on('click', function() {
+          $('#sender-speichern').off('click');
+          app_kachel_anpassen(app_sender_dialog_lesen(senderId));
+          app_dialog_schliessen();
+          app_meldung_mit_timeout('Speichern gewaehlt', 1500);
+        });
+        break;
+      case "3":
+        app_dialog_zeigen(templateCache[TPL_DLG_SENDER_DEL], s);
+        /*
+        $(".dlg-behaelter").html(
+          Mustache.render(templateCache[TPL_DLG_SENDER_DEL], s)
+        );
+        */
+        $('#sender-loeschen').on('click', function() {
+          $('#sender-loeschen').off('click');
+          app_kachel_entfernen(s);
+          app_dialog_schliessen();
+          app_meldung_mit_timeout('Loeschen gewaehlt ' + s.name, 1500);
+        });
+        //app_menue_umschalten();
+        break;
+      default:
+        app_meldung_mit_timeout(
+          'Sender ' + s.name + ', ID ' + s.id,
+          1500
+        );
+        break;
+  }
+}
+
+/* --- Menüfunktionen --- */
+
+function app_nachricht_test() {
+  app_dialog_laden_und_zeigen(
+    templateCache[TPL_DLG_MSG], 'data/msg-test.json');
+  app_menue_umschalten();
+}
+
+function app_info_dialog_zeigen() {
+  app_dialog_laden_und_zeigen(templateCache[TPL_DLG_INFO], '');
+  app_menue_umschalten();
+}
+
+function app_neuer_sender() {
+  app_dialog_laden_und_zeigen(templateCache[TPL_DLG_SENDER_NEU], '');
+  $('#sender-speichern').on('click', function() {
+    $('#sender-speichern').off('click');
+    app_kachel_neu(app_sender_dialog_lesen(--ID_UNDEFINED));
+    app_dialog_schliessen();
+    app_meldung_mit_timeout('Speichern gewaehlt', 1500);
+  });
+  app_menue_umschalten();
+}
+
+function app_sender_bearbeiten() {
+  senderKlickModus = "2"; // bearbeiten
+  app_dialog_laden_und_zeigen(templateCache[TPL_DLG_SENDER_EDIT], '');
+  app_menue_umschalten();
+}
+
+function app_sender_loeschen() {
+  senderKlickModus = "3"; // loeschen
+  app_dialog_laden_und_zeigen(templateCache[TPL_DLG_SENDER_EDIT], '');
+  app_menue_umschalten();
+}
+
+function app_abspieler_loeschen() {
+  var abspieler = app_get_abspiel_ziel();
+  app_dialog_zeigen(templateCache[TPL_DLG_ABSPIELER_DEL], abspieler);
+  $('#abspieler-loeschen').on('click', function() {
+    $('#abspieler-loeschen').off('click');
+    app_abspieler_entfernen(abspieler);
+    app_dialog_schliessen();
+    app_meldung_mit_timeout('Loeschen gewaehlt ' + abspieler.name, 1500);
+  });
+  app_menue_umschalten();
+}
+function app_neuer_abspieler() {
+  app_dialog_laden_und_zeigen(templateCache[TPL_DLG_ABSPIELER_NEU], '');
+  $('#abspieler-speichern').on('click', function() {
+    $('#abspieler-speichern').off('click');
+    app_abspieler_neu(app_abspieler_dialog_lesen(--A_ID_UNDEFINED));
+    app_dialog_schliessen();
+    app_meldung_mit_timeout('Speichern gewaehlt', 1500);
+  });
+  app_menue_umschalten();
+}
+function app_abspieler_bearbeiten() {
+  var abspieler = app_get_abspiel_ziel();
+  app_dialog_zeigen(templateCache[TPL_DLG_ABSPIELER_EDIT_FORM], abspieler);
+  $("#abspieler-typ").val(abspieler.typ);
+  $("#abspieler-zustand").val(abspieler.zustand);
+  $('#abspieler-speichern').on('click', function() {
+    $('#abspieler-speichern').off('click');
+
+    // Aenderungen auf die Bedienoberflaeche uebernehmen
+    var abspieler = app_abspieler_dialog_lesen();
+    if(abspieler.zustand === 'selected') {
+      $(".abspieler-auswahl").children().attr("standardzustand", "");
+    }
+    var opt = $(".abspieler-auswahl :selected");
+    opt.attr("atyp", abspieler.typ);
+    opt.attr("aurl", abspieler.url);
+    opt.attr("standardzustand", abspieler.zustand);
+    opt.text(abspieler.name);
+
+    /*
+      hier fehlt noch eine Funktion zum Speichern
+      der Aenderungen
+    */
+
+    app_dialog_schliessen();
+    app_meldung_mit_timeout('Speichern gewaehlt', 1500);
+  });
+  app_menue_umschalten();
+}
+
+/* --- Dialogfunktionen --- */
+
+function app_dialog_zeigen(vorlage, inhalt) {
+  var dlg = $(".dialog");
+  dlg.html(Mustache.render(vorlage, inhalt));
+  $(".close-btn").on('click', function() {
+    app_dialog_schliessen();
+  });
+  dlg.slideDown(300);
+}
+
+function app_sender_dialog_lesen(senderId) {
+  var nameNeu = $("#sender-name").val();
+  var logoNeu = $("#sender-logo").val();
+  var urlNeu = $("#sender-url").val();
+  return new Sender(senderId, nameNeu, urlNeu, logoNeu);
+}
+
+function app_abspieler_dialog_lesen(abspielerId) {
+  var aName = $("#abspieler-name").val();
+  var aUrl = $("#abspieler-url").val();
+  var aTyp = $("#abspieler-typ").val();
+  var aBild = $("#abspieler-bild").val();
+  var aZustand = $("#abspieler-zustand").val();
+  return new Abspieler(abspielerId, aName, aUrl, aTyp, aBild, aZustand);
+}
+
+function app_dialog_schliessen() {
+  senderKlickModus = "1";
+  $('.close-btn').off('click');
+  $('.dialog').slideUp(300);
+}
+
+/* --- Meldungen in der Fusszeile --- */
+
+/*
+  Eine Meldung eine Zeitlang in der Fusszeile anzeigen
+
+  meldung - Text der Meldung
+  timeout - die Anzahl Millisekunden, die eine Meldung zu sehen sein soll
+*/
+function app_meldung_mit_timeout(meldung, timeout) {
+  var s = $('.sued');
+  s.text(meldung);
+  setTimeout(function() {
+    s.text('Bereit.');
+  }, timeout);
+}
+
+/* --- Ajax-Aufrufe --- */
+
+function app_get_sender() {
+  $.ajax({
+    url: "data/sender.json",
+    type: "GET",
+    dataType : "json"
+  }).done(function( senderliste ) {
+    $(".sender-kachel").off("click");
+    $(".sender-behaelter").html(
+      Mustache.render(templateCache[TPL_SENDER], senderliste));
+    $('.sender-kachel').on('click', app_kachel_klick);
+  });
+}
+
+function app_get_abspieler() {
+  $.ajax({
+    url: "data/abspieler.json",
+    type: "GET",
+    dataType : "json"
+  }).done(function( abspielerliste ) {
+    $(".abspieler-behaelter").html(
+      Mustache.render(templateCache[TPL_ABSPIELER], abspielerliste));
+    $("#abspielen-stoppen").on('click', app_abspielen_stoppen);
+  });
+}
+
+/*
+  vorlage - die Dialogvorlage
+  msgTpl - eine Vorlage mit einem Mitteilungstext (optional)
+*/
+function app_dialog_laden_und_zeigen(vorlage, msgTpl) {
+  if(msgTpl !== '') {
+    $.ajax({
+      url: msgTpl,
+      type: "GET",
+      dataType : "json"
+    }).done(function( msg ) {
+      app_dialog_zeigen(vorlage, msg);
+    });
+  } else {
+    app_dialog_zeigen(vorlage, '');
+  }
+}
+
+function app_radio_kommando_senden(kommando) {
+  $.ajax({
+    url: kommando,
+    type: "GET",
+    dataType : "text",
+    crossDomain: true
+  }).done(function( antwort ) {
+    /*
+      der folgende Schritt wird nicht erreicht, weil beim Aufruf
+      aus dem Browser heraus keine Adresse aufgerufen werden darf,
+      die nicht von der Domain kommt, von der aus das radio-ui
+      ausgeliefert wird.
+
+      Kommt das radio-ui z.B. von der Domain uhilger.de, wird ein Aufruf
+      zur Domain raspi-az blockiert.
+
+      Das laesst sich vermeiden, wenn der Server den Aufruf zum Raspi
+      uebernimmt.
+
+      Interessanterweise kommt der Aufruf zum Raspi durch und löst auch
+      das Abspielen des Radiosenders aus. Nur die Antwort vom Raspi wird
+      blockiert und hier nicht ausgegeben.
+    */
+    app_meldung_mit_timeout(antwort, 2000);
+  }).fail(function(xhr, status, errorThrown ) {
+    app_meldung_mit_timeout(errorThrown + " " + status, 2000);
+  });
+}
+
+/*
+  Ein Template vom Server in den Cache laden
+  template_url - home.tpl, prefs.tpl, sender.tpl
+  tname - 'home', 'prefs', 'sender'
+*/
+function app_get_template(template_url, tname) {
+  $.ajax({
+    url: template_url,
+    type: "GET",
+    dataType : "text"
+  }).done(function( template ) {
+    templateCache[tname] = template;
+  });
+}
+
+/* Objekte */
+
+function Sender(i, n, u, l) {
+  this.id = i;
+  this.name = n;
+  this.url = u;
+  this.logo = l;
+}
+
+function Abspieler(i, n, u, t, b, z) {
+  this.id = i;
+  this.name = n;
+  this.url = u;
+  this.typ = t;
+  this.bild = b;
+  this.zustand = z;
+}
diff --git a/web/beispiel.md b/web/beispiel.md
new file mode 100644
index 0000000..80080c6
--- /dev/null
+++ b/web/beispiel.md
@@ -0,0 +1,45 @@
+# App-Vorlage
+
+Das [radio-ui](README.md) ist in verschiedener Hinsicht eine Vorlage für Webanwendungen.
+
++ Es veranschaulicht die Nutzung des app-layout
++ Es zeigt, wie Vorlagen in einer Webapp zur Vereinfachung eingesetzt werden können
++ Es nutzt das app-menu und zeigt damit, wie es für eine Steuerung von WEbapps eingesetzt wird
++ Es implementiert den Client-Teil eines CRUD-Musters (Create, Read, Update, Delete)
+
+Die oben aufgezählten Teile sind nachfolgend erläutert.
+
+## app-layout
+
+Das radio-ui verwendet das [app-layout](/gitblit/docs/web!app-layout.git), das mit Hilfe von HTML und CSS die Grundlage einer für Webanwendungen geeigneten Bedienoberfläche schafft.
+
+## Nutzung von Vorlagen
+
+Die Datei `index.html` definiert Bereiche einer HTML-Seite als `div`-Elemente, die während der Ausführung des Programmes dynamisch mit wechselnden Inhalten gefüllt werden.
+
+Inhalte werden dabei mit Hilfe von [Mustache](https://github.com/janl/mustache.js) aus einer Vorlage und einer Inhaltsbeschreibung in [JSON](https://de.wikipedia.org/wiki/JavaScript_Object_Notation) zu [HTML](https://de.wikipedia.org/wiki/Hypertext_Markup_Language) zusammengesetzt.
+
+Durch die Nutzung von Vorlagen lassen sich dynamische und statische Inhalte sowie deren Gestaltung und Struktur optimal und frei von Redundanzen zusammenführen.
+
+## app-menu
+
+Das [app-menu](/gitblit/docs/web!app-menu.git) setzt auf die Nutzung von Vorlagen auf und fügt dem radio-ui ein Menü zur Bedienung der Anwendung hinzu. Es ist zudem eingebettet in die Funktion des app-layout. Das radio-ui zeigt so den Einsatz des app-menu und mithin das Zusammenspiel von Vorlagen, app-layout und Menü am praktischen Beispiel.
+
+## CRUD-Muster
+
+CRUD (Create, Read, Update, Delete) ist ein häufig vorkommendes Nutzungsmuster von Anwendungen, Daten werden erzeugt, gelesen, bearbeitet und gelöscht. Bei Webanwendungen zerfällt es in einen Teil, der auf dem Server ausgeführt wird und einen, der im Browser abläuft (Bedienoberfläche).
+
+Allein bezogen auf den Umgang mit den Daten auf der Serverseite ist das CRUD-Muster schnell umgesetzt. Eine Datenstruktur wird erzeugt, gelesen, gespeichert und gelöscht. Eine Bedienoberfläche zur Ausführung dieser Operationen erfordert allerdings mehr als nur den Anstoß der jeweiligen Operation.
+
+Beim radio-ui wird beispielsweise mit den Daten der Radiosender auf diese Weise umgegangen. Die folgende Tabelle veranschaulicht die benötigten Funktionen.
+
+| Operation        | Bedienoberfläche (radio-ui)           | Server  |
+| ------------- |-------------| -----|
+| Create      | Funktion zum Auslösen der Erstellung eines neuen Senders, Dialog zur Eingabe der Daten, Funktion zum Senden der neuen Daten  | Speichern eines neuen Senders |
+| Read      | Funktion zum Abruf der Senderliste, Lesen der Senderliste, Darstellen der Liste als Kacheln      |   Ausgeben einer Liste von Sendern |
+| Update | Funktion zum Auslösen der Änderung an einem Sender, Funktion zur Auswahl eines Senders aus einer Liste, Darstellen der Daten des gewählten Senders in einem Dialog zum Bearbeiten, Funktion zum Senden der Änderungen    |    Speichern von Änderungen an einem bestehenden Sender |
+| Delete | Funktion zum Auslösen der Löschfunktion, Funktion zur Auswahl eines Senders oder mehrerer Sender aus einer Liste, Funktion zum Senden einer Liste zu löschender Sender, Funktion zur Absicherung gegenüber versehentlichem Löschen    |    Löschen eines Senders oder mehrerer Sender |
+
+Die Tabelle läßt ahnen, dass der größere Aufwand zur Herstellung eines CRUD-Musters auf die Bedienoberfläche entfällt.
+
+Mit seinen Menüfunktionen und der Art, wie mit Daten als Liste und als einzelnem Datensatz umgegangen wird sowie der Kombination dieser Funktionen mit der Verwendung von Vorlagen zeigt das radio-ui einen generischen Lösungsansatz, der sich für die Wiederverwendung in anderen Anwendungen eignet und dabei wenige Abhängigkeiten erfordert.
diff --git a/web/data/abspieler.json b/web/data/abspieler.json
new file mode 100644
index 0000000..5d8c3e7
--- /dev/null
+++ b/web/data/abspieler.json
@@ -0,0 +1,31 @@
+{
+  "abspielerliste": {
+    "titel": "Abspielerliste",
+    "inhalt":  [
+      {
+        "abspielerid": 1,
+        "abspielername": "dieses Gerät",
+        "abspielerurl": "",
+        "abspielertyp":"lokal",
+        "abspielerbild": "",
+        "abspielerzustand":""
+      },
+      {
+        "abspielerid": 2,
+        "abspielername": "Raspberry Pi Wohnzimmer",
+        "abspielerurl": "http://raspi-wz:8080/pirc",
+        "abspielertyp":"pirc",
+        "abspielerbild": "",
+        "abspielerzustand":""
+      },
+      {
+        "abspielerid": 3,
+        "abspielername": "Raspberry Pi Arbeitszimmer",
+        "abspielerurl": "http://raspi-az:8080/pirc",
+        "abspielertyp":"pirc",
+        "abspielerbild": "",
+        "abspielerzustand":"selected"
+      }
+    ]
+  }
+}
diff --git a/web/data/menu/abspielermenue.json b/web/data/menu/abspielermenue.json
new file mode 100644
index 0000000..923ec99
--- /dev/null
+++ b/web/data/menu/abspielermenue.json
@@ -0,0 +1,27 @@
+{
+  "menue": {
+    "menuetitel": "Abspieler",
+    "wurzel": false,
+    "vorgaenger": {
+      "vtitel": "Hauptmenü",
+      "vverweis": "hauptmenue.json"
+    },
+    "inhalt": [
+      {
+        "titel": "Neu",
+        "umenue": false,
+        "funktion": "app_neuer_abspieler"
+      },
+      {
+        "titel": "Bearbeiten",
+        "umenue": false,
+        "funktion": "app_abspieler_bearbeiten"
+      },
+      {
+        "titel": "Löschen",
+        "umenue": false,
+        "funktion": "app_abspieler_loeschen"
+      }
+    ]
+  }
+}
diff --git a/web/data/menu/hauptmenue.json b/web/data/menu/hauptmenue.json
new file mode 100644
index 0000000..f73ecbe
--- /dev/null
+++ b/web/data/menu/hauptmenue.json
@@ -0,0 +1,32 @@
+{
+  "menue": {
+    "menuetitel": "Hauptmenü",
+    "wurzel": true,
+    "vorgaenger": {
+      "vtitel": "",
+      "vverweis": ""
+    },
+    "inhalt":  [
+      {
+        "titel": "Sender",
+        "umenue": true,
+        "verweis": "sendermenue.json"
+      },
+      {
+        "titel": "Abspieler",
+        "umenue": true,
+        "verweis": "abspielermenue.json"
+      },
+      {
+        "titel": "Nachricht-Test",
+        "umenue": false,
+        "funktion": "app_nachricht_test"
+      },
+      {
+        "titel": "Über radio-ui",
+        "umenue": false,
+        "funktion": "app_info_dialog_zeigen"
+      }
+    ]
+  }
+}
diff --git a/web/data/menu/sendermenue.json b/web/data/menu/sendermenue.json
new file mode 100644
index 0000000..f0e4ca6
--- /dev/null
+++ b/web/data/menu/sendermenue.json
@@ -0,0 +1,27 @@
+{
+  "menue": {
+    "menuetitel": "Sender",
+    "wurzel": false,
+    "vorgaenger": {
+      "vtitel": "Hauptmenü",
+      "vverweis": "hauptmenue.json"
+    },
+    "inhalt": [
+      {
+        "titel": "Neu",
+        "umenue": false,
+        "funktion": "app_neuer_sender"
+      },
+      {
+        "titel": "Bearbeiten",
+        "umenue": false,
+        "funktion": "app_sender_bearbeiten"
+      },
+      {
+        "titel": "Löschen",
+        "umenue": false,
+        "funktion": "app_sender_loeschen"
+      }
+    ]
+  }
+}
diff --git a/web/data/menu/untermenue-1.json b/web/data/menu/untermenue-1.json
new file mode 100644
index 0000000..dac9873
--- /dev/null
+++ b/web/data/menu/untermenue-1.json
@@ -0,0 +1,27 @@
+{
+  "menue": {
+    "menuetitel": "Untermenü 1",
+    "wurzel": false,
+    "vorgaenger": {
+      "vtitel": "Hauptmenü",
+      "vverweis": "hauptmenue.json"
+    },
+    "inhalt": [
+      {
+        "titel": "U1 Menüeintrag 1",
+        "umenue": false,
+        "funktion": "app_menu_message"
+      },
+      {
+        "titel": "U1 Menüeintrag 2",
+        "umenue": true,
+        "verweis": "untermenue-2.json"
+      },
+      {
+        "titel": "U1 Menüeintrag 3",
+        "umenue": false,
+        "funktion": "app_menu_message"
+      }
+    ]
+  }
+}
diff --git a/web/data/menu/untermenue-2.json b/web/data/menu/untermenue-2.json
new file mode 100644
index 0000000..bf54a84
--- /dev/null
+++ b/web/data/menu/untermenue-2.json
@@ -0,0 +1,27 @@
+{
+  "menue": {
+    "menuetitel": "Untermenü 2",
+    "wurzel": false,
+    "vorgaenger": {
+      "vtitel": "Untermenü 1",
+      "vverweis": "untermenue-1.json"
+    },
+    "inhalt":  [
+      {
+        "titel": "U2 Menüeintrag 1",
+        "umenue": false,
+        "funktion": "app_menu_message"
+      },
+      {
+        "titel": "U2 Menüeintrag 2",
+        "umenue": false,
+        "funktion": "app_menu_message"
+      },
+      {
+        "titel": "U2 Menüeintrag 3",
+        "umenue": false,
+        "funktion": "app_menu_message"
+      }
+    ]
+  }
+}
diff --git a/web/data/msg-test.json b/web/data/msg-test.json
new file mode 100644
index 0000000..23cb64e
--- /dev/null
+++ b/web/data/msg-test.json
@@ -0,0 +1,4 @@
+{
+  "typ": "nachricht",
+  "text": "Hier kann eine Mitteilung stehen."
+}
diff --git a/web/data/sender.json b/web/data/sender.json
new file mode 100644
index 0000000..260208e
--- /dev/null
+++ b/web/data/sender.json
@@ -0,0 +1,31 @@
+{
+  "senderliste": {
+    "titel": "Senderliste",
+    "inhalt":  [
+      {
+        "senderid": 1,
+        "sendername": "hr info",
+        "senderurl": "http://hr-hrinfo-live.cast.addradio.de/hr/hrinfo/live/mp3/128/stream.mp3",
+        "senderlogo": "../bilder/hr-info.png"
+      },
+      {
+        "senderid": 2,
+        "sendername": "radiobob",
+        "senderurl": "http://bob.hoerradar.de/radiobob-live-mp3-hq",
+        "senderlogo": "../bilder/radio-bob-logo-80.png"
+      },
+      {
+        "senderid": 3,
+        "sendername": "hr3",
+        "senderurl": "http://hr-hrinfo-live.cast.addradio.de/hr/hrinfo/live/mp3/128/stream.mp3",
+        "senderlogo": "../bilder/hr3.png"
+      },
+      {
+        "senderid": 4,
+        "sendername": "FFH",
+        "senderurl": "http://mp3.ffh.de/radioffh/hqlivestream.mp3",
+        "senderlogo": "../bilder/ffh.svg"
+      }
+    ]
+  }
+}
diff --git a/web/data/tpl/abspieler-eintrag.tpl b/web/data/tpl/abspieler-eintrag.tpl
new file mode 100644
index 0000000..f857525
--- /dev/null
+++ b/web/data/tpl/abspieler-eintrag.tpl
@@ -0,0 +1 @@
+<option {{zustand}} atyp="{{typ}}" aurl="{{url}}">{{name}}</option>
diff --git a/web/data/tpl/abspieler.tpl b/web/data/tpl/abspieler.tpl
new file mode 100644
index 0000000..45b7ffe
--- /dev/null
+++ b/web/data/tpl/abspieler.tpl
@@ -0,0 +1,9 @@
+<label class="inline-label abspieler-lb">Abspieler:</label>
+<select name="abspieler" size="1" class="abspieler-auswahl">
+  {{#abspielerliste}}
+    {{#inhalt}}
+    <option {{abspielerzustand}} standardzustand="{{abspielerzustand}}" atyp="{{abspielertyp}}" aurl="{{abspielerurl}}">{{abspielername}}</option>
+    {{/inhalt}}
+  {{/abspielerliste}}
+</select>
+<button type="button" id="abspielen-stoppen">&#x25a0;</button>
diff --git a/web/data/tpl/dlg-abspieler-edit-form.tpl b/web/data/tpl/dlg-abspieler-edit-form.tpl
new file mode 100644
index 0000000..3ff7039
--- /dev/null
+++ b/web/data/tpl/dlg-abspieler-edit-form.tpl
@@ -0,0 +1,32 @@
+<div class="dlg-sender-edit">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-abspieler-titel">Abspieler bearbeiten</div>
+    <form>
+      <input value="{{id}}" id="abspieler-id" name="abspieler-id" type="hidden"
+          class="abspieler-eingabe"></br>
+      <label class="inline-label">Name:</label>
+      <input value="{{name}}" id="abspieler-name" name="abspieler-name" placeholder="Name"
+          type="text" class="abspieler-eingabe"></br>
+      <!-- <label class="inline-label">Bild:</label> -->
+      <input value="{{logo}}" id="abspieler-bild" name="abspieler-bild" placeholder="Bild"
+          type="text" class="abspieler-eingabe" type="hidden"></br>
+      <label class="inline-label">URL:</label>
+      <input value="{{url}}" id="abspieler-url"  name="abspieler-url" placeholder="URL"
+          type="text" class="abspieler-eingabe"></br>
+      <label class="inline-label">Typ:</label>
+      <select id="abspieler-typ" size="1" class="abspieler-form-auswahl">
+        <option value="lokal">lokal</option>
+        <option value="pirc">pirc</option>
+      </select></br>
+      <label class="inline-label">Zustand:</label>
+      <select id="abspieler-zustand" size="1" class="abspieler-form-auswahl">
+        <option value="">nicht ausgew&auml;hlt</option>
+        <option value="selected">ausgew&auml;hlt</option>
+      </select></br>
+      <div class="form-button-footer">
+        <button type="button" id="abspieler-speichern">Speichern</button>
+      </div>
+    </form>
+  </div>
+</div>
diff --git a/web/data/tpl/dlg-abspieler-loeschen.tpl b/web/data/tpl/dlg-abspieler-loeschen.tpl
new file mode 100644
index 0000000..6626443
--- /dev/null
+++ b/web/data/tpl/dlg-abspieler-loeschen.tpl
@@ -0,0 +1,13 @@
+<div class="nachricht">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-info-app-info">
+      Wollen Sie den Abspieler <br/>
+      <b><i>{{name}}</i></b><br/>
+      wirklich l&ouml;schen?
+    </div>
+    <div class="form-button-footer">
+      <button type="button" id="abspieler-loeschen">L&ouml;schen</button>
+    </div>
+  </div>
+</div>
diff --git a/web/data/tpl/dlg-abspieler-neu.tpl b/web/data/tpl/dlg-abspieler-neu.tpl
new file mode 100644
index 0000000..3a9cbce
--- /dev/null
+++ b/web/data/tpl/dlg-abspieler-neu.tpl
@@ -0,0 +1,27 @@
+<div class="dlg-abspieler-neu">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-abspieler-titel">Neuer Abspieler</div>
+    <form>
+      <label class="inline-label">Name:</label>
+      <input id="abspieler-name" placeholder="Name" type="text" class="abspieler-eingabe"></br>
+      <label class="inline-label">URL:</label>
+      <input id="abspieler-url" placeholder="URL" type="text" class="abspieler-eingabe"></br>
+      <label class="inline-label">Typ:</label>
+      <select id="abspieler-typ" size="1" class="abspieler-auswahl">
+        <option value="lokal">lokal</option>
+        <option value="pirc">pirc</option>
+      </select></br>
+      <label class="inline-label">Bild:</label>
+      <input id="abspieler-bild" placeholder="Bild-URL" type="text" class="abspieler-eingabe"></br>
+      <label class="inline-label">Zustand:</label>
+      <select id="abspieler-zustand" size="1" class="abspieler-auswahl">
+        <option value="">nicht ausgew&auml;hlt</option>
+        <option value="selected">ausgew&auml;hlt</option>
+      </select></br>
+      <div class="form-button-footer">
+        <button type="button" id="abspieler-speichern">Speichern</button>
+      </div>
+    </form>
+  </div>
+</div>
diff --git a/web/data/tpl/dlg-info.tpl b/web/data/tpl/dlg-info.tpl
new file mode 100644
index 0000000..b4071f7
--- /dev/null
+++ b/web/data/tpl/dlg-info.tpl
@@ -0,0 +1,8 @@
+<div class="dlg-info">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-info-app-titel">radio-ui</div>
+    <div class="dlg-info-app-info">Eine Bedienoberfläche für Webradio von <a href='https://uhilger.de'>Ulrich Hilger</a>.</div>
+    <div class="dlg-info-app-info">Weitere Infos im <a href='https://uhilger.de/gitblit/docs/web!radio-ui.git'>Code-Repository</a>.</div>
+  </div>
+</div>
diff --git a/web/data/tpl/dlg-msg.tpl b/web/data/tpl/dlg-msg.tpl
new file mode 100644
index 0000000..2122dbe
--- /dev/null
+++ b/web/data/tpl/dlg-msg.tpl
@@ -0,0 +1,6 @@
+<div class="{{typ}}">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    {{text}}
+  </div>
+</div>
diff --git a/web/data/tpl/dlg-sender-edit-form.tpl b/web/data/tpl/dlg-sender-edit-form.tpl
new file mode 100644
index 0000000..1522751
--- /dev/null
+++ b/web/data/tpl/dlg-sender-edit-form.tpl
@@ -0,0 +1,17 @@
+<div class="dlg-sender-titel">Sender bearbeiten</div>
+<form>
+  <input value="{{id}}" id="sender-id" name="sender-id" type="hidden"
+      class="sender-eingabe"></br>
+  <label class="inline-label">Name:</label>
+  <input value="{{name}}" id="sender-name" name="sender-name" placeholder="Name"
+      type="text" class="sender-eingabe"></br>
+  <label class="inline-label">Logo:</label>
+  <input value="{{logo}}" id="sender-logo" name="sender-logo" placeholder="Logo"
+      type="text" class="sender-eingabe"></br>
+  <label class="inline-label">URL:</label>
+  <input value="{{url}}" id="sender-url"  name="sender-url" placeholder="URL"
+      type="text" class="sender-eingabe"></br>
+  <div class="form-button-footer">
+    <button type="button" id="sender-speichern">Speichern</button>
+  </div>
+</form>
diff --git a/web/data/tpl/dlg-sender-edit.tpl b/web/data/tpl/dlg-sender-edit.tpl
new file mode 100644
index 0000000..15ee980
--- /dev/null
+++ b/web/data/tpl/dlg-sender-edit.tpl
@@ -0,0 +1,8 @@
+<div class="dlg-sender-edit">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-sender-text">
+      Sender zum Bearbeiten oder L&ouml;schen antippen...
+    </div>
+  </div>
+</div>
diff --git a/web/data/tpl/dlg-sender-loeschen.tpl b/web/data/tpl/dlg-sender-loeschen.tpl
new file mode 100644
index 0000000..0b5d3e0
--- /dev/null
+++ b/web/data/tpl/dlg-sender-loeschen.tpl
@@ -0,0 +1,13 @@
+<div class="nachricht">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-info-app-info">
+      Wollen Sie den Sender<br/>
+      <b><i>{{name}}</i></b><br/>
+      wirklich l&ouml;schen?
+    </div>
+    <div class="form-button-footer">
+      <button type="button" id="sender-loeschen">L&ouml;schen</button>
+    </div>
+  </div>
+</div>
diff --git a/web/data/tpl/dlg-sender-neu.tpl b/web/data/tpl/dlg-sender-neu.tpl
new file mode 100644
index 0000000..65862ff
--- /dev/null
+++ b/web/data/tpl/dlg-sender-neu.tpl
@@ -0,0 +1,17 @@
+<div class="dlg-sender-neu">
+  <span class="close-btn pointer-cursor">&#10006;</span>
+  <div class="dlg-behaelter">
+    <div class="dlg-sender-titel">Neuer Sender</div>
+    <form>
+      <label class="inline-label">Name:</label>
+      <input id="sender-name" placeholder="Name" type="text" class="sender-eingabe"></br>
+      <label class="inline-label">Logo:</label>
+      <input id="sender-logo" placeholder="Logo" type="text" class="sender-eingabe"></br>
+      <label class="inline-label">URL:</label>
+      <input id="sender-url" placeholder="URL" type="text" class="sender-eingabe"></br>
+      <div class="form-button-footer">
+        <button type="button" id="sender-speichern">Speichern</button>
+      </div>
+    </form>
+  </div>
+</div>
diff --git a/web/data/tpl/kachel.tpl b/web/data/tpl/kachel.tpl
new file mode 100644
index 0000000..13ed6f4
--- /dev/null
+++ b/web/data/tpl/kachel.tpl
@@ -0,0 +1,6 @@
+<div class="sender-kachel" sid="{{id}}" data-verweis="{{url}}">
+  <div class="sender-name">{{name}}</div>
+  <div class="sender-bild-behaelter">
+    <img class="sender-bild" src="{{logo}}">
+  </div>
+</div>
diff --git a/web/data/tpl/sender.tpl b/web/data/tpl/sender.tpl
new file mode 100644
index 0000000..f08ca94
--- /dev/null
+++ b/web/data/tpl/sender.tpl
@@ -0,0 +1,12 @@
+<div id="sender" class="sender">
+{{#senderliste}}
+  {{#inhalt}}
+  <div class="sender-kachel" sid="{{senderid}}" data-verweis="{{senderurl}}">
+    <div class="sender-name">{{sendername}}</div>
+    <div class="sender-bild-behaelter">
+      <img class="sender-bild" src="{{senderlogo}}">
+    </div>
+  </div>
+  {{/inhalt}}
+{{/senderliste}}
+</div>
diff --git a/web/hamburger.css b/web/hamburger.css
new file mode 100644
index 0000000..bca57b2
--- /dev/null
+++ b/web/hamburger.css
@@ -0,0 +1,99 @@
+/*!
+ * entnommen aus
+ *
+ * Hamburgers
+ * @description Tasty CSS-animated hamburgers
+ * @author Jonathan Suh @jonsuh
+ * @site https://jonsuh.com/hamburgers
+ * @link https://github.com/jonsuh/hamburgers
+ */
+
+.hamburger {
+  display: inline-block;
+  cursor: pointer;
+  transition-property: opacity, filter;
+  transition-duration: 0.15s;
+  transition-timing-function: linear;
+  font: inherit;
+  color: inherit;
+  text-transform: none;
+  background-color: transparent;
+  border: 0;
+  margin: 0;
+  overflow: visible;
+}
+
+.hamburger:hover {
+  opacity: 0.7;
+}
+
+.hamburger-box {
+  width: 40px;
+  height: 24px;
+  display: inline-block;
+  position: relative;
+}
+
+.hamburger-inner {
+  display: block;
+  top: 50%;
+  margin: 0;
+}
+
+.hamburger-inner, .hamburger-inner::before, .hamburger-inner::after {
+  width: 30px;
+  height: 4px;
+  background-color: #000;
+  border-radius: 4px;
+  position: absolute;
+  transition-property: transform;
+  transition-duration: 0.15s;
+  transition-timing-function: ease;
+}
+
+.hamburger-inner::before, .hamburger-inner::after {
+  content: "";
+  display: block;
+}
+
+.hamburger-inner::before {
+  top: -10px;
+}
+
+.hamburger-inner::after {
+  bottom: -10px;
+}
+
+/*
+ * Elastic
+ */
+.hamburger--elastic .hamburger-inner {
+  top: 2px;
+  transition-duration: 0.275s;
+  transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
+
+.hamburger--elastic .hamburger-inner::before {
+  top: 10px;
+  transition: opacity 0.125s 0.275s ease;
+}
+
+.hamburger--elastic .hamburger-inner::after {
+  top: 20px;
+  transition: transform 0.275s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+}
+
+.hamburger--elastic.is-active .hamburger-inner {
+  transform: translate3d(0, 10px, 0) rotate(135deg);
+  transition-delay: 0.075s;
+}
+
+.hamburger--elastic.is-active .hamburger-inner::before {
+  transition-delay: 0s;
+  opacity: 0;
+}
+
+.hamburger--elastic.is-active .hamburger-inner::after {
+  transform: translate3d(0, -20px, 0) rotate(-270deg);
+  transition-delay: 0.075s;
+}
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..8db16d9
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Radio-UI</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
+    <link rel="stylesheet" type="text/css" href="../jslib/app-menu/app-menu.css">
+    <link rel="stylesheet" type="text/css" href="hamburger.css">
+    <link rel="stylesheet" type="text/css" href="app.css">
+  </head>
+  <body>
+    <!-- Kopfzeile -->
+    <div class="nord">
+      <div id="nav-menu">
+        <div id="nav-toggle" class="hamburger hamburger--elastic">
+          <div class="hamburger-box">
+            <div class="hamburger-inner"></div>
+          </div>
+        </div>
+      </div>
+      <div class="app-titel">
+        <span id="app-titel">Radio-UI</span>
+      </div>
+      <div class="sendersuche">
+        <input type="search" class="sendersuche-eingabe" placeholder="&#x1F50E;">
+      </div>
+    </div>
+    <!-- Inhalt -->
+    <div class="inhalt">
+      <!-- westliche Seitenleiste -->
+      <div class="west"></div>
+      <!-- zentraler Bereich -->
+      <div class="zentrum-behaelter">
+        <!-- Einblendbereich -->
+        <div class="dialog"></div>
+        <!-- Abspieler-Auswahl -->
+        <div class="abspieler-behaelter"></div>
+        <!-- zentraler Inhaltsbereich -->
+        <div class="zentrum">
+          <div id="prefs"></div>
+          <div id="home"></div>
+          <div class="sender-behaelter"></div>
+        </div>
+      </div>
+      <!-- oestliche Seitenleiste -->
+      <div class="ost">
+        östliche Seitenleiste
+      </div>
+    </div>
+    <!-- Fusszeile -->
+    <div class="sued">
+      Fußzeile
+    </div>
+    <script src="../jslib/jquery-1.11.1/jquery-1.11.1.min.js"></script>
+    <script src="../jslib/mustache/mustache.min.js"></script>
+    <script src="../jslib/app-menu/app-menu.js"></script>
+    <script src="app.js"></script>
+    <script type="text/javascript" charset="utf-8">
+      $(document).ready(function() {
+        app_init();
+      });
+    </script>
+  </body>
+</html>
diff --git a/web/pirc.md b/web/pirc.md
new file mode 100644
index 0000000..2d8a873
--- /dev/null
+++ b/web/pirc.md
@@ -0,0 +1,45 @@
+# Radio abspielen mit dem Raspberry Pi
+
+Das [radio-ui](README.md) kann verwendet werden, um den Livestream eines Radiosenders von einem Raspberry Pi wiedergeben zu lassen. Hierzu wird auf dem betreffenden Raspberry Pi die Anwendung [pirc](/gitblit/docs/pirc.git) genutzt. Pirc kombiniert zum Spielen von Audiostreams den [omxplayer](https://elinux.org/Omxplayer) mit einer auf [Tomcat](https://de.wikipedia.org/wiki/Apache_Tomcat) laufenden Fernbedienung, die per HTTP gesteuert werden kann.
+
+Auf dieser Seite ist beschrieben, wie pirc aus dem radio-ui heraus genutzt und mithin das Abspielen von Webradio auf dem Raspberry Pi bewerkstelligt werden kann.
+
+## Kombination von pirc und radio-ui
+
+Zur Verwendung des radio-ui mit [pirc](/gitblit/docs/pirc.git) kann die Anwendung radio-ui direkt auf einen Raspberry Pi installiert werden, beispielsweise ähnlich wie pirc als Webanwendung für Tomcat. Stattdessen kann radio-ui auch von einer anderen Maschine aus die pirc-Instanz auf einen Raspberry Pi aufrufen.
+
+In beiden Fällen sendet das radio-ui Kommandos via HTTP an pirc wie nachfolgend beschrieben.
+
+## Kommandos an den Raspberry Pi
+
+Wird [pirc](/gitblit/docs/pirc.git) auf dem Raspberry Pi ausgeführt, können Kommandos via HTTP an die betreffende Maschine gesendet werden. Sie bestehen aus einem Klassennamen `c`, einem Methodennamen `m` und Parametern `p`. Ein Kommando setzt sich wie folgt zusammen
+
+1. `http://raspi:8080/` - URL der Maschine
+1. `pirc/` - Name des Kontext, unter dem die Anwendung pirc läuft
+1. `sys/rpc` - Service-Endpunkt der pirc-API
+1. `c=de.uhilger.pirc.App` - Name der Klasse von pirc, die die Abspielmethode enthält
+1. `m=abspielen` - Name der Methode zum Abspielen
+1. `p=Sender-URL` - der Parameter mit dem URL zum Audiostream des Webradiosenders
+1. `p=` - optionaler Token zur Authentisierung (kann hier leer bleiben)
+
+Das obige Beispiel geht davon aus, dass der betreffende Raspberry Pi unter dem Namen `raspi` auf Port `8080` über `HTTP` erreichbar ist und dass auf dem Pi die Anwendung pirc unter dem Kontextnamen `pirc` installiert wurde. Zum Aufruf mit dem radio-ui kann das obige Kommando weitgehend unverändert bleiben. Nur der Teil 6. mit dem URL des Senders muss veränderlich gestaltet sein.
+
+### Abspielen
+
+Das Kommando zum Abspielen des Livestreams des Radiosenders radiobob (vgl. [Senderliste](radiostreams.md)) lautet z.B. wie folgt
+
+````
+http://raspi:8080/pirc/sys/rpc?c=de.uhilger.pirc.App&m=abspielen&p=http://bob.hoerradar.de/radiobob-live-mp3-hq&p=
+````
+
+Es bewirkt, dass auf dem betreffenden Raspberry Pi eine Instanz des [omxplayer](https://elinux.org/Omxplayer) gestartet wird und diese fortlaufend den Webradio-Stream unter der angegebenen Adresse abspielt.
+
+### Stoppen
+
+Zum Stoppen des zur Zeit abgespielten Webradios dient das folgende Kommando
+
+````
+http://raspi:8080/sys/rpc?c=de.uhilger.pirc.App&m=kommando&p=q
+````
+
+Mit dem obigen Aufruf wird das Kommando `q` an den auf dem Raspberry Pi laufenden Abspielprozess gesendet. Das bewirkt, dass der im Abspielprozess laufende [omxplayer](https://elinux.org/Omxplayer) das Abspielen stoppt und der Prozess endet.
diff --git a/web/radiostreams.md b/web/radiostreams.md
new file mode 100644
index 0000000..c55eadd
--- /dev/null
+++ b/web/radiostreams.md
@@ -0,0 +1,43 @@
+# Radiostreams
+
+Nachfolgend die Adressen einiger Radiostreams, die vom [radio-ui](README.md) verwendet werden können.
+
+## hr3
+
+[Livestream-Übersicht](http://www.hr3.de/service/hr3-online-hoeren,webradio-100.html)
+
+128 kbit http://hr-hrinfo-live.cast.addradio.de/hr/hrinfo/live/mp3/128/stream.mp3
+
+48 kbit http://hr-hr3-live.cast.addradio.de/hr/hr3/live/mp3/48/stream.mp3
+
+## hr info
+
+[Livestream-Übersicht](http://www.hr-inforadio.de/livestream/index.html)
+
+128 kBit/s http://hr-hrinfo-live.cast.addradio.de/hr/hrinfo/live/mp3/128/stream.mp3
+
+48 kBit/s http://hr-hrinfo-live.cast.addradio.de/hr/hrinfo/live/mp3/48/stream.mp3
+
+## radiobob
+
+[Livestream-Übersicht](http://www.radiobob.de/radiobob/empfang/livestream)
+
+MP3: http://bob.hoerradar.de/radiobob-live-mp3-hq
+
+Mobile Nutzung: http://bob.hoerradar.de/aac-radiobob
+
+### Test der Bandbreite
+
+Spielt man den URL 'hq' von Radiobob im Browser zeigt der Netzwerkmonitor von Linux eine Netznutzung von 24-32 kbit/s. Das ergibt die folgende Betrachtung:
+
++ 32	kBit/s
++ 115.20.000	Bit/Std (kBit x 3600 x 1000)
++ 13,74	MB pro Stunde (Bit : 8 : 1.024 : 1.024)
+
+## FFH
+
+[Livestream-Übersicht](https://www.ffh.de/musik/webradios/stream-adressen.html)
+
+128kbps http://mp3.ffh.de/radioffh/hqlivestream.mp3
+
+48kbps http://mp3.ffh.de/radioffh/livestream.aac

--
Gitblit v1.9.3