I went to the trouble of creating an HTML version of the “Radio Switcher”.
The appearance:
These lines must be adapted to the specific circumstances.
Here’s the code. Simply save it as Radio Switcher.html
Here is a link to the code…
Tonie Konfigurator
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
body {
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
touch-action: manipulation;
}
.device-container {
width: 320px;
height: 480px;
background-color: #222;
border-radius: 20px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
.screen {
width: 100%;
height: 100%;
background-color: white;
display: flex;
flex-direction: column;
position: relative;
}
.screen-header {
background-color: #f5f5f5;
padding: 12px;
border-bottom: 1px solid #ddd;
font-weight: bold;
font-size: 16px;
text-align: center;
}
.screen-content {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.tonie-image-container {
width: 135px;
height: 180px;
margin: 20px auto;
border: 1px solid #eee;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: 10px;
}
.tonie-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.instructions {
padding: 15px;
font-size: 14px;
color: #666;
line-height: 1.5;
text-align: center;
width: 100%;
}
.list-container {
height: 100%;
overflow-y: auto;
padding: 5px 0;
width: 100%;
}
.list-item {
padding: 15px 20px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
font-size: 16px;
}
.list-item.selected {
background-color: #e6f7ff;
color: #1890ff;
font-weight: bold;
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { transform: translateX(0); }
50% { transform: translateX(3px); }
100% { transform: translateX(0); }
}
.buttons {
display: flex;
justify-content: space-between;
padding: 15px;
background-color: #f5f5f5;
border-top: 1px solid #ddd;
gap: 10px;
}
.button {
padding: 20px 16px;
background-color: #4CAF50;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
color: white;
transition: all 0.2s;
flex: 1;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.button:hover {
background-color: #45a049;
}
.button:active {
background-color: #3d8b40;
transform: scale(0.98);
}
.button-select {
background-color: #2196F3;
}
.button-select:hover {
background-color: #1976D2;
}
.button-select:active {
background-color: #1565C0;
}
.button-back {
background-color: #ff9800;
}
.button-back:hover {
background-color: #f57c00;
}
.button-back:active {
background-color: #ef6c00;
}
.hidden {
display: none;
}
.notification {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px 25px;
border-radius: 12px;
z-index: 100;
text-align: center;
animation: fadeInOut 2s forwards;
font-size: 16px;
}
@keyframes fadeInOut {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
.button-info {
display: block;
font-size: 12px;
margin-top: 5px;
opacity: 0.9;
font-weight: normal;
}
.manual-mode {
background: #e3f2fd;
border: 1px solid #90caf9;
color: #1565c0;
padding: 15px;
border-radius: 10px;
margin: 10px;
text-align: center;
}
.current-selection {
background: #f8f9fa;
padding: 10px;
margin: 10px;
border-radius: 8px;
text-align: center;
font-size: 14px;
}
</style>
Tonies
Tonie 1 von 2
Wähle einen Tonie und wechsle die Quelle
Manueller Modus aktiv
Ă–ffne TeddyCloud und trage die URL manuell ein.
<!-- Select Screen -->
<div id="select-screen" class="screen hidden">
<div class="screen-header">Quelle auswählen</div>
<div class="screen-content">
<div id="sources-list" class="list-container">
<!-- Sources will be populated by JavaScript -->
</div>
</div>
</div>
<div class="buttons">
<button id="btn-select" class="button button-select">
Auswählen
<span class="button-info">Klick: Wechseln | Halten: Bestätigen</span>
</button>
<button id="btn-back" class="button button-back">
ZurĂĽck
<span class="button-info">ZurĂĽck | Ausschalten</span>
</button>
</div>
<div id="notification" class="notification hidden"></div>
</div>
<script>
// Konfiguration
const config = {
TEDDYCLOUD_URL: "http://192.168.178.120:80",
TONIES: [
{img: "BerryCreativeTonie.jpg", tag_id: "f58ced0e500304e0"},
{img: "FairyCreativeTonie.jpg", tag_id: "f58ced0e500304e0"},
],
SOURCES: [
{title: "TOGGO Radio", url: "http://radio.toggo.de/live/mp3-192/airable/"},
{title: "Energy Zurich", url: "https://energyzuerich.ice.infomaniak.ch/energyzuerich-high.mp3"},
{title: "Radio SantaClaus", url: "https://streaming.radiostreamlive.com/radiosantaclaus_devices"},
{title: "Technobase.fm", url: "https://listener2.mp3.tb-group.fm/tb.mp3"},
]
};
// Touch-Handling Variablen
let longPressTimeout = null;
const LONG_PRESS_DURATION = 500;
// Tonie-Klasse
class Tonie {
constructor(img, tag_id) {
this.img = img;
this.tag_id = tag_id;
}
async update_url(url) {
console.log(`Versuche Tonie ${this.tag_id} zu aktualisieren mit: ${url}`);
// Methode 1: Fetch ohne CORS (no-cors mode)
try {
const response = await fetch(`${config.TEDDYCLOUD_URL}/content/json/set/${this.tag_id}`, {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `source=${encodeURIComponent(url)}`
});
this.showNotification("âś… Anfrage gesendet");
return true;
} catch (error) {
console.log('Methode 1 fehlgeschlagen:', error);
}
// Methode 2: Bild-Tag als Fallback
try {
return new Promise((resolve) => {
const img = new Image();
const apiUrl = `${config.TEDDYCLOUD_URL}/content/json/set/${this.tag_id}?source=${encodeURIComponent(url)}&_=${Date.now()}`;
img.onload = () => {
this.showNotification("âś… Tonie aktualisiert");
resolve(true);
};
img.onerror = () => {
this.showNotification("❌ Aktualisierung fehlgeschlagen");
resolve(false);
};
img.src = apiUrl;
setTimeout(() => {
if (!img.complete) {
this.showNotification("⚠️ Timeout - prüfe manuell");
resolve(false);
}
}, 3000);
});
} catch (error) {
console.log('Methode 2 fehlgeschlagen:', error);
}
this.showNotification("❌ Automatische Methoden fehlgeschlagen");
return false;
}
openManualSetup(url) {
const manualMode = document.getElementById('manual-mode');
manualMode.style.display = 'block';
manualMode.innerHTML = `
<strong>Manuelle Konfiguration nötig:</strong><br>
<small>1. Ă–ffne: <a href="${config.TEDDYCLOUD_URL}" target="_blank">TeddyCloud</a></small><br>
<small>2. Gehe zu Content Management</small><br>
<small>3. Setze fĂĽr Tonie ${this.tag_id}:</small><br>
<code style="background: #f5f5f5; padding: 5px; display: block; margin: 5px 0; font-size: 12px;">${url}</code>
<button onclick="this.parentElement.style.display='none'" style="margin-top: 5px; padding: 5px 10px;">OK</button>
`;
this.showNotification("📋 Manuelle Konfiguration nötig");
}
showNotification(message) {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.classList.remove('hidden');
setTimeout(() => {
notification.classList.add('hidden');
}, 3000);
}
}
// SelectOption-Klasse
class SelectOption {
constructor(title, url) {
this.title = title;
this.url = url;
}
}
// Screen-Management
class ScreenManager {
constructor() {
this.currentScreen = null;
this.screens = {
tonies: document.getElementById('tonies-screen'),
select: document.getElementById('select-screen')
};
}
showScreen(screenName) {
Object.values(this.screens).forEach(screen => {
screen.classList.add('hidden');
});
this.screens[screenName].classList.remove('hidden');
this.currentScreen = screenName;
}
}
// App-Klasse
class App {
constructor(config) {
this.config = config;
this.screenManager = new ScreenManager();
this.tonies = config.TONIES.map(t => new Tonie(t.img, t.tag_id));
this.sources = config.SOURCES.map(s => new SelectOption(s.title, s.url));
this.currentTonieIndex = 0;
this.selectedSourceIndex = 0;
this.init();
}
init() {
console.log("App wird initialisiert...");
// Event-Listener fĂĽr Buttons - VERBESSERTE VERSION
const btnSelect = document.getElementById('btn-select');
const btnBack = document.getElementById('btn-back');
// Einfache Click-Events fĂĽr Desktop
btnSelect.addEventListener('click', (e) => {
console.log("Select Button geklickt");
this.handleSelectClick();
});
btnBack.addEventListener('click', () => {
console.log("Back Button geklickt");
this.handleBackClick();
});
// Contextmenu fĂĽr Rechtsklick/Long Press auf Desktop
btnSelect.addEventListener('contextmenu', (e) => {
e.preventDefault();
console.log("Select Button long press");
this.handleSelectHold();
});
btnBack.addEventListener('contextmenu', (e) => {
e.preventDefault();
console.log("Back Button long press");
this.handleBackHold();
});
// Touch Events fĂĽr Mobile
btnSelect.addEventListener('touchstart', (e) => {
e.preventDefault();
console.log("Select Button touch start");
longPressTimeout = setTimeout(() => {
console.log("Select Button long touch");
this.handleSelectHold();
}, LONG_PRESS_DURATION);
});
btnSelect.addEventListener('touchend', (e) => {
e.preventDefault();
if (longPressTimeout) {
clearTimeout(longPressTimeout);
longPressTimeout = null;
console.log("Select Button short touch");
this.handleSelectClick();
}
});
btnBack.addEventListener('touchstart', (e) => {
e.preventDefault();
console.log("Back Button touch start");
longPressTimeout = setTimeout(() => {
console.log("Back Button long touch");
this.handleBackHold();
}, LONG_PRESS_DURATION);
});
btnBack.addEventListener('touchend', (e) => {
e.preventDefault();
if (longPressTimeout) {
clearTimeout(longPressTimeout);
longPressTimeout = null;
console.log("Back Button short touch");
this.handleBackClick();
}
});
// Touch bewegen = Long Press abbrechen
btnSelect.addEventListener('touchmove', (e) => {
if (longPressTimeout) {
clearTimeout(longPressTimeout);
longPressTimeout = null;
}
});
btnBack.addEventListener('touchmove', (e) => {
if (longPressTimeout) {
clearTimeout(longPressTimeout);
longPressTimeout = null;
}
});
// Tonies Screen initialisieren
this.updateTonieDisplay();
this.populateSourcesList();
// Standard-Screen anzeigen
this.screenManager.showScreen('tonies');
console.log("App initialisiert");
}
updateTonieDisplay() {
const currentTonie = this.tonies[this.currentTonieIndex];
const imgElement = document.getElementById('tonie-img');
imgElement.src = currentTonie.img;
imgElement.alt = `Tonie ${this.currentTonieIndex + 1}`;
// Aktuelle Auswahl anzeigen
document.getElementById('current-selection').textContent =
`Tonie ${this.currentTonieIndex + 1} von ${this.tonies.length}`;
console.log(`Tonie aktualisiert: Index ${this.currentTonieIndex}`);
}
populateSourcesList() {
const sourcesList = document.getElementById('sources-list');
sourcesList.innerHTML = '';
this.sources.forEach((source, index) => {
const item = document.createElement('div');
item.className = 'list-item';
if (index === this.selectedSourceIndex) {
item.classList.add('selected');
}
item.textContent = source.title;
item.addEventListener('click', () => {
console.log(`Quelle ausgewählt: ${source.title}`);
this.selectedSourceIndex = index;
this.populateSourcesList();
});
sourcesList.appendChild(item);
});
console.log(`Quellenliste aktualisiert, ausgewählt: ${this.selectedSourceIndex}`);
}
handleSelectClick() {
console.log(`handleSelectClick aufgerufen, aktueller Screen: ${this.screenManager.currentScreen}`);
if (this.screenManager.currentScreen === 'tonies') {
// Tonie wechseln
this.currentTonieIndex = (this.currentTonieIndex + 1) % this.tonies.length;
this.updateTonieDisplay();
console.log(`Tonie gewechselt zu: ${this.currentTonieIndex}`);
} else if (this.screenManager.currentScreen === 'select') {
// Quelle wechseln
this.selectedSourceIndex = (this.selectedSourceIndex + 1) % this.sources.length;
this.populateSourcesList();
console.log(`Quelle gewechselt zu: ${this.selectedSourceIndex}`);
}
}
async handleSelectHold() {
console.log(`handleSelectHold aufgerufen, aktueller Screen: ${this.screenManager.currentScreen}`);
if (this.screenManager.currentScreen === 'tonies') {
// Zum Auswahlbildschirm wechseln
this.screenManager.showScreen('select');
console.log("Wechsle zu Auswahlbildschirm");
} else if (this.screenManager.currentScreen === 'select') {
// Quelle bestätigen und Tonie aktualisieren
const selectedSource = this.sources[this.selectedSourceIndex];
const currentTonie = this.tonies[this.currentTonieIndex];
console.log(`Bestätige Auswahl: ${selectedSource.title} -> ${selectedSource.url}`);
const success = await currentTonie.update_url(selectedSource.url);
if (success) {
this.playTone();
}
// ZurĂĽck zum Tonies Screen
setTimeout(() => {
this.screenManager.showScreen('tonies');
console.log("ZurĂĽck zu Tonie-Bildschirm");
}, 1000);
}
}
handleBackClick() {
console.log(`handleBackClick aufgerufen, aktueller Screen: ${this.screenManager.currentScreen}`);
if (this.screenManager.currentScreen === 'select') {
this.screenManager.showScreen('tonies');
// Verstecke manuellen Modus
document.getElementById('manual-mode').style.display = 'none';
console.log("ZurĂĽck zu Tonie-Bildschirm");
}
}
handleBackHold() {
console.log("handleBackHold aufgerufen - Ausschalten");
if (confirm("Möchten Sie das Gerät wirklich ausschalten?")) {
document.body.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100vh;background:#000;color:#fff;font-size:24px;">Gerät wird ausgeschaltet...</div>';
setTimeout(() => {
document.body.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100vh;background:#000;color:#666;font-size:18px;">Gerät ausgeschaltet</div>';
}, 2000);
}
}
playTone() {
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 2000;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.05);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.05);
} catch (error) {
console.log("Audio nicht verfĂĽgbar");
}
}
}
// App starten
document.addEventListener('DOMContentLoaded', () => {
console.log("DOM geladen, starte App...");
new App(config);
});
</script>
It’s not working perfectly everywhere yet…
I can swap radio stations on every PC, but not on my smartphone (even though it’s on the same Wi-Fi network as the TC).
But I’m still working on it…