ulrich
2018-02-24 9c2c3c7fa694464e8eb16affe9fdca9363ee0b05
radio-ui-Vorlage hinzugefuegt
1 files modified
29 files added
1741 ■■■■■ changed files
src/java/de/uhilger/radiozentrale/web/Initialiser.java 13 ●●●●● patch | view | raw | blame | history
web/README.md 206 ●●●●● patch | view | raw | blame | history
web/app.css 376 ●●●●● patch | view | raw | blame | history
web/app.js 475 ●●●●● patch | view | raw | blame | history
web/beispiel.md 45 ●●●●● patch | view | raw | blame | history
web/data/abspieler.json 31 ●●●●● patch | view | raw | blame | history
web/data/menu/abspielermenue.json 27 ●●●●● patch | view | raw | blame | history
web/data/menu/hauptmenue.json 32 ●●●●● patch | view | raw | blame | history
web/data/menu/sendermenue.json 27 ●●●●● patch | view | raw | blame | history
web/data/menu/untermenue-1.json 27 ●●●●● patch | view | raw | blame | history
web/data/menu/untermenue-2.json 27 ●●●●● patch | view | raw | blame | history
web/data/msg-test.json 4 ●●●● patch | view | raw | blame | history
web/data/sender.json 31 ●●●●● patch | view | raw | blame | history
web/data/tpl/abspieler-eintrag.tpl 1 ●●●● patch | view | raw | blame | history
web/data/tpl/abspieler.tpl 9 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-abspieler-edit-form.tpl 32 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-abspieler-loeschen.tpl 13 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-abspieler-neu.tpl 27 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-info.tpl 8 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-msg.tpl 6 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-sender-edit-form.tpl 17 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-sender-edit.tpl 8 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-sender-loeschen.tpl 13 ●●●●● patch | view | raw | blame | history
web/data/tpl/dlg-sender-neu.tpl 17 ●●●●● patch | view | raw | blame | history
web/data/tpl/kachel.tpl 6 ●●●●● patch | view | raw | blame | history
web/data/tpl/sender.tpl 12 ●●●●● patch | view | raw | blame | history
web/hamburger.css 99 ●●●●● patch | view | raw | blame | history
web/index.html 64 ●●●●● patch | view | raw | blame | history
web/pirc.md 45 ●●●●● patch | view | raw | blame | history
web/radiostreams.md 43 ●●●●● patch | view | raw | blame | history
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);
web/README.md
New file
@@ -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.
web/app.css
New file
@@ -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');
}
web/app.js
New file
@@ -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;
}
web/beispiel.md
New file
@@ -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.
web/data/abspieler.json
New file
@@ -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"
      }
    ]
  }
}
web/data/menu/abspielermenue.json
New file
@@ -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"
      }
    ]
  }
}
web/data/menu/hauptmenue.json
New file
@@ -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"
      }
    ]
  }
}
web/data/menu/sendermenue.json
New file
@@ -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"
      }
    ]
  }
}
web/data/menu/untermenue-1.json
New file
@@ -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"
      }
    ]
  }
}
web/data/menu/untermenue-2.json
New file
@@ -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"
      }
    ]
  }
}
web/data/msg-test.json
New file
@@ -0,0 +1,4 @@
{
  "typ": "nachricht",
  "text": "Hier kann eine Mitteilung stehen."
}
web/data/sender.json
New file
@@ -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"
      }
    ]
  }
}
web/data/tpl/abspieler-eintrag.tpl
New file
@@ -0,0 +1 @@
<option {{zustand}} atyp="{{typ}}" aurl="{{url}}">{{name}}</option>
web/data/tpl/abspieler.tpl
New file
@@ -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>
web/data/tpl/dlg-abspieler-edit-form.tpl
New file
@@ -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>
web/data/tpl/dlg-abspieler-loeschen.tpl
New file
@@ -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>
web/data/tpl/dlg-abspieler-neu.tpl
New file
@@ -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>
web/data/tpl/dlg-info.tpl
New file
@@ -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>
web/data/tpl/dlg-msg.tpl
New file
@@ -0,0 +1,6 @@
<div class="{{typ}}">
  <span class="close-btn pointer-cursor">&#10006;</span>
  <div class="dlg-behaelter">
    {{text}}
  </div>
</div>
web/data/tpl/dlg-sender-edit-form.tpl
New file
@@ -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>
web/data/tpl/dlg-sender-edit.tpl
New file
@@ -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>
web/data/tpl/dlg-sender-loeschen.tpl
New file
@@ -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>
web/data/tpl/dlg-sender-neu.tpl
New file
@@ -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>
web/data/tpl/kachel.tpl
New file
@@ -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>
web/data/tpl/sender.tpl
New file
@@ -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>
web/hamburger.css
New file
@@ -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;
}
web/index.html
New file
@@ -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>
web/pirc.md
New file
@@ -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.
web/radiostreams.md
New file
@@ -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