je commitbericht

This commit is contained in:
filoor 2025-05-27 11:50:28 +02:00
parent bd98a2b2f1
commit df8f4c7d9b
50 changed files with 4625 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

6
.lh/.lhignore Normal file
View File

@ -0,0 +1,6 @@
# list file to not track by the local-history extension. comment line starts with a '#' character
# each line describe a regular expression pattern (search for 'Javascript regex')
# it will relate to the workspace directory root. for example:
# '.*\.txt' ignores any file with 'txt' extension
# '/test/.*' ignores all the files under the 'test' directory
# '.*/test/.*' ignores all the files under any 'test' directory (even under sub-folders)

View File

@ -0,0 +1,30 @@
{
"sourceFile": "adhan-webapp/Dockerfile",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 3,
"patches": [
{
"date": 1748182920273,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1748183154468,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -10,10 +10,7 @@\n \n # Kopieer de rest van de bestanden\n COPY . .\n \n-# Voeg cache-control headers toe aan statische bestanden\n-RUN echo \"location /static/ { add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; }\" > /etc/nginx/conf.d/static.conf\n-\n EXPOSE 80\n \n CMD [\"python\", \"app.py\"]\n"
},
{
"date": 1748183197708,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,1 +1,20 @@\n-\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\n\\ No newline at end of file\n+FROM python:3.11-slim\n+\n+WORKDIR /app\n+\n+# Installeer alleen de benodigde packages\n+RUN pip install --no-cache-dir flask requests\n+\n+# Kopieer de applicatie bestanden\n+COPY . .\n+\n+# Maak de benodigde directories aan\n+RUN mkdir -p /app/static/clips\n+\n+# Stel de juiste permissies in\n+RUN chmod -R 755 /app\n+\n+EXPOSE 80\n+\n+# Start de applicatie\n+CMD [\"python\", \"app.py\"]\n"
},
{
"date": 1748199351044,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,1 +1,20 @@\n-\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\n\\ No newline at end of file\n+FROM python:3.11-slim\n+\n+WORKDIR /app\n+\n+# Installeer alleen de benodigde packages\n+RUN pip install --no-cache-dir flask requests\n+\n+# Kopieer de applicatie bestanden\n+COPY . .\n+\n+# Maak de benodigde directories aan\n+RUN mkdir -p /app/static/clips\n+\n+# Stel de juiste permissies in\n+RUN chmod -R 755 /app\n+\n+EXPOSE 80\n+\n+# Start de applicatie\n+CMD [\"python\", \"start_all.py\"]\n"
}
],
"date": 1748182920273,
"name": "Commit-0",
"content": "FROM python:3.11-slim\n\nWORKDIR /app\n\nRUN pip install flask requests\n\n# Kopieer eerst alleen de requirements om caching te optimaliseren\nCOPY requirements.txt .\nRUN pip install -r requirements.txt\n\n# Kopieer de rest van de bestanden\nCOPY . .\n\n# Voeg cache-control headers toe aan statische bestanden\nRUN echo \"location /static/ { add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; }\" > /etc/nginx/conf.d/static.conf\n\nEXPOSE 80\n\nCMD [\"python\", \"app.py\"]\n"
}
]
}

View File

@ -0,0 +1,18 @@
{
"sourceFile": "adhan-webapp/README.md",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 0,
"patches": [
{
"date": 1748203692581,
"content": "Index: \n===================================================================\n--- \n+++ \n"
}
],
"date": 1748203692581,
"name": "Commit-0",
"content": "# Adhaan Gebedstijden Display\n\nEen moderne, islamitische gebedstijden display applicatie met automatische adhaan, weersinformatie en Sonos integratie.\n\n## ✨ Nieuwe Functies\n\n### 🌤️ Weersinformatie\n- Actuele temperatuur en weersomstandigheden\n- Automatische updates elke 10 minuten\n- Nederlandse weerbeschrijvingen\n- Windsnelheid en luchtvochtigheid\n\n### 🎵 Uitgebreide Audio Opties\n- Meerdere adhaan-stijlen beschikbaar\n- Audio preview functie in instellingen\n- Verbeterde instellingen interface\n- Test functionaliteit voor Sonos\n\n## 🚀 Installatie\n\n### 1. OpenWeatherMap API Key verkrijgen\n\n1. Ga naar [OpenWeatherMap](https://openweathermap.org/api)\n2. Maak een gratis account aan\n3. Verkrijg je API key (1000 gratis calls per dag)\n\n### 2. Configuratie\n\nBewerk `config.py` en vervang `YOUR_API_KEY_HERE` met je OpenWeatherMap API key:\n\n```python\nOPENWEATHER_API_KEY = 'jouw_api_key_hier'\n```\n\nOptioneel kun je ook de locatie aanpassen:\n\n```python\nWEATHER_LOCATION = 'Amsterdam,NL' # Vervang met jouw stad\n```\n\n### 3. Docker opstarten\n\n```bash\ndocker-compose up -d\n```\n\n## 📱 Gebruik\n\n### Hoofdscherm\n- **Links**: Huidige tijd, datum en countdown naar volgende gebed\n- **Rechts**: Hadith, gebedstijden en weersinformatie\n- **Knoppen**: Dark/light mode, instellingen, mute\n\n### Instellingen\n- **Volume**: Sonos volume instelling (0-100)\n- **Zones**: Selecteer welke Sonos speakers gebruikt worden\n- **Audio**: Kies adhaan-stijl met preview functie\n- **Test**: Test audio op Sonos of lokaal\n\n## 🎵 Audio Bestanden Toevoegen\n\n1. Plaats MP3 bestanden in `static/clips/`\n2. Bestanden worden automatisch beschikbaar in instellingen\n3. Aanbevolen: max 5MB per bestand\n\n## 🔧 API Endpoints\n\n- `GET /api/weather` - Actuele weerdata\n- `GET /api/hadith` - Willekeurige hadith\n- `POST /api/mute` - Toggle mute status\n- `POST /api/test-audio` - Test audio bestand\n\n## 🌍 Functies\n\n- ✅ Automatische adhaan op gebedstijden\n- ✅ Sonos integratie met meerdere zones\n- ✅ Nederlandse hadiths die automatisch wisselen\n- ✅ Dark/light theme toggle\n- ✅ Mute functionaliteit\n- ✅ Responsive design\n- ✅ Weersinformatie met automatische updates\n- ✅ Meerdere adhaan-stijlen met preview\n- ✅ Moderne, islamitische UI\n\n## 🛠️ Technische Details\n\n- **Backend**: Flask (Python)\n- **Frontend**: HTML5, CSS3, JavaScript\n- **Audio**: MP3 ondersteuning\n- **API's**: VUMG (gebedstijden), OpenWeatherMap (weer), Sonos\n- **Deployment**: Docker + TrueNAS\n\n## 📝 Changelog\n\n### v2.0 - Nieuwe Functies\n- Weersinformatie toegevoegd\n- Meerdere adhaan-stijlen\n- Audio preview functie\n- Verbeterde instellingen interface\n- API endpoints voor dynamische updates\n- Configuratie bestand voor eenvoudige setup\n\n### v1.0 - Basis Functionaliteit\n- ✅ Gebedstijden display\n- ✅ Automatische adhaan\n- ✅ Sonos integratie\n- ✅ Hadith rotatie\n- ✅ Dark/light theme "
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
{
"sourceFile": "adhan-webapp/config.py",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 1,
"patches": [
{
"date": 1748203602606,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1748203978113,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,9 +1,9 @@\n # Configuratie bestand voor de Adhaan applicatie\n \n # OpenWeatherMap API configuratie\n # Verkrijg een gratis API key op: https://openweathermap.org/api\n-OPENWEATHER_API_KEY = 'YOUR_API_KEY_HERE'\n+OPENWEATHER_API_KEY = '99caf7628fd7197acd71df3fc2cafbdb'\n \n # Locatie voor weersinformatie\n WEATHER_LOCATION = 'Amsterdam,NL'\n \n"
}
],
"date": 1748203602606,
"name": "Commit-0",
"content": "# Configuratie bestand voor de Adhaan applicatie\n\n# OpenWeatherMap API configuratie\n# Verkrijg een gratis API key op: https://openweathermap.org/api\nOPENWEATHER_API_KEY = 'YOUR_API_KEY_HERE'\n\n# Locatie voor weersinformatie\nWEATHER_LOCATION = 'Amsterdam,NL'\n\n# Sonos API configuratie\nSONOS_API_IP = '192.168.0.114'\n\n# VUMG API voor gebedstijden\nVUMG_API_URL = 'https://www.vumg.nl/?rest_route=/dpt/v1/prayertime&filter=today'\n\n# Bestandspaden\nimport os\nBASE_DIR = os.path.dirname(os.path.abspath(__file__))\nSETTINGS_PATH = os.path.join(BASE_DIR, 'settings.json')\nHADITHS_PATH = os.path.join(BASE_DIR, 'static', 'hadiths.json')\nCLIPS_PATH = os.path.join(BASE_DIR, 'static', 'clips') "
}
]
}

View File

@ -0,0 +1,18 @@
{
"sourceFile": "adhan-webapp/requirements.txt",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 0,
"patches": [
{
"date": 1748204335831,
"content": "Index: \n===================================================================\n--- \n+++ \n"
}
],
"date": 1748204335831,
"name": "Commit-0",
"content": "Flask==3.0.0\nrequests==2.31.0\nhijridate==2.5.0 "
}
]
}

View File

@ -0,0 +1,26 @@
{
"sourceFile": "adhan-webapp/settings.json",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 2,
"patches": [
{
"date": 1748201503324,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1748275757237,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,8 +1,9 @@\n {\n- \"volume\": 20,\n+ \"volume\": 15,\n \"zones\": [\n- \"Keuken\"\n+ \"Keuken\",\n+ \"Slaapkamer\"\n ],\n \"audio_clip\": \"adhan1.mp3\",\n \"mute\": false\n }\n\\ No newline at end of file\n"
},
{
"date": 1748276087388,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,1 +1,9 @@\n-\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\n\\ No newline at end of file\n+{\n+ \"volume\": 15,\n+ \"zones\": [\n+ \"Keuken\",\n+ \"Slaapkamer\"\n+ ],\n+ \"audio_clip\": \"adhan1.mp3\",\n+ \"mute\": false\n+}\n\\ No newline at end of file\n"
}
],
"date": 1748201503324,
"name": "Commit-0",
"content": "{\n \"volume\": 20,\n \"zones\": [\n \"Keuken\"\n ],\n \"audio_clip\": \"adhan1.mp3\",\n \"mute\": false\n}"
}
]
}

View File

@ -0,0 +1,18 @@
{
"sourceFile": "adhan-webapp/start_all.py",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 0,
"patches": [
{
"date": 1748199344007,
"content": "Index: \n===================================================================\n--- \n+++ \n"
}
],
"date": 1748199344007,
"name": "Commit-0",
"content": "import subprocess\nimport sys\nimport time\n\n# Start Flask-app\nflask_proc = subprocess.Popen([sys.executable, 'app.py'])\n\n# Start adhan_cron.py elke minuut in een loop\ntry:\n while True:\n cron_proc = subprocess.Popen([sys.executable, 'adhan_cron.py'])\n cron_proc.wait()\n time.sleep(60)\nexcept KeyboardInterrupt:\n flask_proc.terminate()\n sys.exit(0) "
}
]
}

View File

@ -0,0 +1,18 @@
{
"sourceFile": "adhan-webapp/static/clips/README.md",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 0,
"patches": [
{
"date": 1748203518364,
"content": "Index: \n===================================================================\n--- \n+++ \n"
}
],
"date": 1748203518364,
"name": "Commit-0",
"content": "# Adhaan Audio Bestanden\n\nDeze directory bevat verschillende adhaan audio bestanden voor de gebedstijden applicatie.\n\n## Beschikbare Adhaan Stijlen:\n\n1. **adhan1.mp3** - Klassieke adhaan (origineel)\n2. **adhan_mekka.mp3** - Mekka stijl adhaan\n3. **adhan_medina.mp3** - Medina stijl adhaan\n4. **adhan_kort.mp3** - Korte versie adhaan\n5. **adhan_traditioneel.mp3** - Traditionele Nederlandse moskee stijl\n\n## Gebruik:\n\nDe gebruiker kan in de instellingen kiezen welke adhaan-stijl gebruikt wordt.\nAlle bestanden moeten in MP3 formaat zijn voor compatibiliteit met de browser en Sonos systemen.\n\n## Toevoegen van nieuwe bestanden:\n\n1. Plaats het MP3 bestand in deze directory\n2. Het bestand wordt automatisch beschikbaar in de instellingen\n3. Zorg ervoor dat het bestand niet te groot is (max 5MB aanbevolen) "
}
]
}

View File

@ -0,0 +1,26 @@
{
"sourceFile": "adhan-webapp/static/countdown.js",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 2,
"patches": [
{
"date": 1748182675995,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1748202535810,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -3,20 +3,57 @@\n const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n document.getElementById(\"current-time\").textContent = timeStr;\n }\n \n-function startCountdown(targetTimeStr) {\n+// Gebedstijden ophalen uit een globale variabele (wordt in de template gezet)\n+let prayerTimes = window.prayerTimes || [];\n+let prayerNames = window.prayerNames || [];\n+let currentPrayerIndex = 0;\n+\n+function startCountdowns(times, names) {\n+ prayerTimes = times;\n+ prayerNames = names;\n+ currentPrayerIndex = getNextPrayerIndex();\n+ startCountdown(prayerTimes[currentPrayerIndex], prayerNames[currentPrayerIndex]);\n+}\n+\n+function getNextPrayerIndex() {\n+ const now = new Date();\n+ for (let i = 0; i < prayerTimes.length; i++) {\n+ const [h, m] = prayerTimes[i].split(\":\");\n+ const t = new Date();\n+ t.setHours(parseInt(h));\n+ t.setMinutes(parseInt(m));\n+ t.setSeconds(0);\n+ if (t > now) return i;\n+ }\n+ return 0; // fallback: eerste gebed\n+}\n+\n+function startCountdown(targetTimeStr, prayerName) {\n const parts = targetTimeStr.split(\":\");\n- const target = new Date();\n+ let target = new Date();\n target.setHours(parseInt(parts[0]));\n target.setMinutes(parseInt(parts[1]));\n- target.setSeconds(parseInt(parts[2]) || 0);\n+ target.setSeconds(0);\n \n+ document.querySelector('.huidig-gebed .naam').textContent = prayerName;\n+\n function update() {\n const now = new Date();\n let diff = Math.floor((target - now) / 1000);\n \n- if (diff < 0) diff = 0;\n+ if (diff <= 0) {\n+ // Speel adhaan af via browser\n+ const audio = document.getElementById('adhanAudio');\n+ if (audio) audio.play();\n+ // Ga naar volgende gebed\n+ currentPrayerIndex = (currentPrayerIndex + 1) % prayerTimes.length;\n+ const nextTime = prayerTimes[currentPrayerIndex];\n+ const nextName = prayerNames[currentPrayerIndex];\n+ startCountdown(nextTime, nextName);\n+ return;\n+ }\n \n const h = String(Math.floor(diff / 3600)).padStart(2, '0');\n const m = String(Math.floor((diff % 3600) / 60)).padStart(2, '0');\n const s = String(diff % 60).padStart(2, '0');\n@@ -24,9 +61,10 @@\n document.getElementById(\"countdown\").textContent = `${h}:${m}:${s}`;\n }\n \n update();\n- setInterval(update, 1000);\n+ window._countdownInterval && clearInterval(window._countdownInterval);\n+ window._countdownInterval = setInterval(update, 1000);\n }\n \n function setupThemeToggle() {\n const toggleBtn = document.getElementById(\"themeToggle\");\n"
},
{
"date": 1748204399019,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1,10 +1,30 @@\n function updateCurrentTime() {\n const now = new Date();\n const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n document.getElementById(\"current-time\").textContent = timeStr;\n+ \n+ // Update datum informatie elke minuut\n+ updateDateInfo();\n }\n \n+function updateDateInfo() {\n+ fetch('/api/date-info')\n+ .then(response => response.json())\n+ .then(data => {\n+ const gregorianElement = document.querySelector('.gregorian-datum');\n+ const hijriArabicElement = document.querySelector('.hijri-datum-arabic');\n+ const hijriDutchElement = document.querySelector('.hijri-datum-dutch');\n+ \n+ if (gregorianElement) gregorianElement.textContent = data.gregorian_full;\n+ if (hijriArabicElement) hijriArabicElement.textContent = data.hijri_arabic;\n+ if (hijriDutchElement) hijriDutchElement.textContent = data.hijri_dutch;\n+ })\n+ .catch(error => {\n+ console.log('Fout bij bijwerken datum:', error);\n+ });\n+}\n+\n // Gebedstijden ophalen uit een globale variabele (wordt in de template gezet)\n let prayerTimes = window.prayerTimes || [];\n let prayerNames = window.prayerNames || [];\n let currentPrayerIndex = 0;\n"
}
],
"date": 1748182675995,
"name": "Commit-0",
"content": "function updateCurrentTime() {\n const now = new Date();\n const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n document.getElementById(\"current-time\").textContent = timeStr;\n}\n\nfunction startCountdown(targetTimeStr) {\n const parts = targetTimeStr.split(\":\");\n const target = new Date();\n target.setHours(parseInt(parts[0]));\n target.setMinutes(parseInt(parts[1]));\n target.setSeconds(parseInt(parts[2]) || 0);\n\n function update() {\n const now = new Date();\n let diff = Math.floor((target - now) / 1000);\n\n if (diff < 0) diff = 0;\n\n const h = String(Math.floor(diff / 3600)).padStart(2, '0');\n const m = String(Math.floor((diff % 3600) / 60)).padStart(2, '0');\n const s = String(diff % 60).padStart(2, '0');\n\n document.getElementById(\"countdown\").textContent = `${h}:${m}:${s}`;\n }\n\n update();\n setInterval(update, 1000);\n}\n\nfunction setupThemeToggle() {\n const toggleBtn = document.getElementById(\"themeToggle\");\n const html = document.documentElement;\n\n function applyTheme(theme) {\n html.className = theme;\n localStorage.setItem('theme', theme);\n }\n\n const saved = localStorage.getItem('theme') || 'light';\n applyTheme(saved);\n\n toggleBtn.addEventListener(\"click\", () => {\n const current = html.className === 'light' ? 'dark' : 'light';\n applyTheme(current);\n });\n}\n"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
adhan-webapp/.DS_Store vendored Normal file

Binary file not shown.

23
adhan-webapp/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM python:3.11-slim
WORKDIR /app
# Kopieer requirements eerst voor betere caching
COPY requirements.txt .
# Installeer dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Kopieer de applicatie bestanden
COPY . .
# Maak de benodigde directories aan
RUN mkdir -p /app/static/clips
# Stel de juiste permissies in
RUN chmod -R 755 /app
EXPOSE 80
# Start de applicatie
CMD ["python", "start_all.py"]

108
adhan-webapp/README.md Normal file
View File

@ -0,0 +1,108 @@
# Adhaan Gebedstijden Display
Een moderne, islamitische gebedstijden display applicatie met automatische adhaan, weersinformatie en Sonos integratie.
## ✨ Nieuwe Functies
### 🌤️ Weersinformatie
- Actuele temperatuur en weersomstandigheden
- Automatische updates elke 10 minuten
- Nederlandse weerbeschrijvingen
- Windsnelheid en luchtvochtigheid
### 🎵 Uitgebreide Audio Opties
- Meerdere adhaan-stijlen beschikbaar
- Audio preview functie in instellingen
- Verbeterde instellingen interface
- Test functionaliteit voor Sonos
## 🚀 Installatie
### 1. OpenWeatherMap API Key verkrijgen
1. Ga naar [OpenWeatherMap](https://openweathermap.org/api)
2. Maak een gratis account aan
3. Verkrijg je API key (1000 gratis calls per dag)
### 2. Configuratie
Bewerk `config.py` en vervang `YOUR_API_KEY_HERE` met je OpenWeatherMap API key:
```python
OPENWEATHER_API_KEY = 'jouw_api_key_hier'
```
Optioneel kun je ook de locatie aanpassen:
```python
WEATHER_LOCATION = 'Amsterdam,NL' # Vervang met jouw stad
```
### 3. Docker opstarten
```bash
docker-compose up -d
```
## 📱 Gebruik
### Hoofdscherm
- **Links**: Huidige tijd, datum en countdown naar volgende gebed
- **Rechts**: Hadith, gebedstijden en weersinformatie
- **Knoppen**: Dark/light mode, instellingen, mute
### Instellingen
- **Volume**: Sonos volume instelling (0-100)
- **Zones**: Selecteer welke Sonos speakers gebruikt worden
- **Audio**: Kies adhaan-stijl met preview functie
- **Test**: Test audio op Sonos of lokaal
## 🎵 Audio Bestanden Toevoegen
1. Plaats MP3 bestanden in `static/clips/`
2. Bestanden worden automatisch beschikbaar in instellingen
3. Aanbevolen: max 5MB per bestand
## 🔧 API Endpoints
- `GET /api/weather` - Actuele weerdata
- `GET /api/hadith` - Willekeurige hadith
- `POST /api/mute` - Toggle mute status
- `POST /api/test-audio` - Test audio bestand
## 🌍 Functies
- ✅ Automatische adhaan op gebedstijden
- ✅ Sonos integratie met meerdere zones
- ✅ Nederlandse hadiths die automatisch wisselen
- ✅ Dark/light theme toggle
- ✅ Mute functionaliteit
- ✅ Responsive design
- ✅ Weersinformatie met automatische updates
- ✅ Meerdere adhaan-stijlen met preview
- ✅ Moderne, islamitische UI
## 🛠️ Technische Details
- **Backend**: Flask (Python)
- **Frontend**: HTML5, CSS3, JavaScript
- **Audio**: MP3 ondersteuning
- **API's**: VUMG (gebedstijden), OpenWeatherMap (weer), Sonos
- **Deployment**: Docker + TrueNAS
## 📝 Changelog
### v2.0 - Nieuwe Functies
- Weersinformatie toegevoegd
- Meerdere adhaan-stijlen
- Audio preview functie
- Verbeterde instellingen interface
- API endpoints voor dynamische updates
- Configuratie bestand voor eenvoudige setup
### v1.0 - Basis Functionaliteit
- ✅ Gebedstijden display
- ✅ Automatische adhaan
- ✅ Sonos integratie
- ✅ Hadith rotatie
- ✅ Dark/light theme

Binary file not shown.

172
adhan-webapp/adhan_cron.py Normal file
View File

@ -0,0 +1,172 @@
import requests, json, os
from datetime import datetime, date
import time
SETTINGS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'settings.json')
VUMG_API = 'https://www.vumg.nl/?rest_route=/dpt/v1/prayertime&filter=today'
SONOS_API_IP = '192.168.0.114' # Pas aan indien nodig
LOG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'adhan_cron.log')
LAST_PLAYED_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'last_played.json')
def logregel(message):
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_message = f"[{timestamp}] {message}"
print(log_message) # Voor Docker logs
with open(LOG_PATH, 'a') as log:
log.write(log_message + '\n')
def get_current_volume(settings):
"""Bepaal het juiste volume op basis van de huidige tijd"""
# Fallback naar oude volume als nieuwe instellingen niet bestaan
if 'volume_day' not in settings or 'volume_night' not in settings:
return settings.get('volume', 30)
current_time = datetime.now().strftime('%H:%M')
night_start = settings.get('night_start', '20:00')
day_start = settings.get('day_start', '07:00')
# Converteer tijden naar vergelijkbare format
current_minutes = int(current_time[:2]) * 60 + int(current_time[3:5])
night_minutes = int(night_start[:2]) * 60 + int(night_start[3:5])
day_minutes = int(day_start[:2]) * 60 + int(day_start[3:5])
# Bepaal of het avond is
if night_minutes > day_minutes: # Normale dag/nacht cyclus (bijv. dag 07:00-20:00)
is_night = current_minutes >= night_minutes or current_minutes < day_minutes
else: # Avond gaat over middernacht (bijv. avond 20:00-07:00)
is_night = current_minutes >= night_minutes and current_minutes < day_minutes
if is_night:
volume = settings.get('volume_night', settings.get('volume', 30) // 2)
logregel(f"🌙 Avond volume gebruikt: {volume}")
return volume
else:
volume = settings.get('volume_day', settings.get('volume', 30))
logregel(f"☀️ Dag volume gebruikt: {volume}")
return volume
def check_recent_play(prayer_name):
"""Controleer of deze adhaan recent is afgespeeld (binnen 2 minuten)"""
try:
if os.path.exists(LAST_PLAYED_PATH):
with open(LAST_PLAYED_PATH, 'r') as f:
last_played = json.load(f)
last_time = last_played.get(prayer_name)
if last_time:
last_datetime = datetime.fromisoformat(last_time)
time_diff = (datetime.now() - last_datetime).total_seconds()
if time_diff < 120: # 2 minuten
logregel(f"⏭️ {prayer_name} recent afgespeeld ({int(time_diff)}s geleden), overslaan")
return True
except Exception as e:
logregel(f"Fout bij controleren recent play: {e}")
return False
def mark_as_played(prayer_name):
"""Markeer deze adhaan als afgespeeld"""
try:
last_played = {}
if os.path.exists(LAST_PLAYED_PATH):
with open(LAST_PLAYED_PATH, 'r') as f:
last_played = json.load(f)
last_played[prayer_name] = datetime.now().isoformat()
with open(LAST_PLAYED_PATH, 'w') as f:
json.dump(last_played, f)
except Exception as e:
logregel(f"Fout bij markeren als afgespeeld: {e}")
def play_sonos_clip(zone, audio_clip, volume, prayer_name):
"""Speel een clip af op een specifieke Sonos zone met error handling"""
try:
# Stop eerst alle audio op deze zone
stop_url = f"http://{SONOS_API_IP}:5005/{zone}/pause"
requests.get(stop_url, timeout=2)
time.sleep(0.5) # Korte pauze
# Speel de clip af
clip_url = f"http://{SONOS_API_IP}:5005/{zone}/clip/{audio_clip}/{volume}"
response = requests.get(clip_url, timeout=10)
if response.status_code == 200:
logregel(f"✅ AFGESPEELD {prayer_name} op {zone} ({audio_clip}, volume {volume})")
return True
else:
logregel(f"❌ HTTP {response.status_code} voor {zone}: {response.text[:100]}")
return False
except requests.exceptions.Timeout:
logregel(f"⏱️ TIMEOUT bij {zone} voor {prayer_name}")
return False
except Exception as e:
logregel(f"❌ FOUT Sonos {zone} {prayer_name}: {e}")
return False
now = datetime.now().strftime('%H:%M')
logregel(f"CRON gestart, huidige tijd: {now}")
# Laad settings
try:
with open(SETTINGS_PATH) as f:
settings = json.load(f)
except Exception as e:
logregel(f"Fout bij laden settings: {e}")
exit(1)
# Controleer mute-status
if settings.get('mute', False):
logregel("Adhaan niet afgespeeld: mute staat aan.")
exit(0)
# Haal gebedstijden op
try:
res = requests.get(VUMG_API, timeout=10)
res.raise_for_status()
data = res.json()[0]
gebedstijden = {
"Fajr": data.get("fajr_jamah", "00:00"),
"Zuhr": data.get("zuhr_jamah", "00:00"),
"Asr": data.get("asr_jamah", "00:00"),
"Maghrib": data.get("maghrib_jamah", "00:00"),
"Isha": data.get("isha_jamah", "00:00")
}
except Exception as e:
logregel(f"Fout bij ophalen gebedstijden: {e}")
exit(1)
# Check voor gebedstijd
for naam, tijd in gebedstijden.items():
if tijd[:5] == now: # Alleen uren:minuten vergelijken
# Controleer of recent afgespeeld
if check_recent_play(naam):
continue
logregel(f"🕌 GEBEDSTIJD GEDETECTEERD: {naam} om {tijd}")
# Markeer als afgespeeld VOOR het afspelen
mark_as_played(naam)
# Speel af op alle zones
success_count = 0
zones = settings.get('zones', ['Woonkamer'])
audio_clip = settings.get('audio_clip', 'adhan1.mp3')
volume = get_current_volume(settings)
for zone in zones:
if play_sonos_clip(zone, audio_clip, volume, naam):
success_count += 1
time.sleep(1) # Pauze tussen zones
if success_count > 0:
logregel(f"🎵 Adhaan succesvol afgespeeld op {success_count}/{len(zones)} zones")
else:
logregel(f"❌ Adhaan kon niet worden afgespeeld op geen enkele zone")
break # Stop na eerste match
logregel(f"CRON voltooid om {datetime.now().strftime('%H:%M:%S')}")

508
adhan-webapp/app.py Normal file
View File

@ -0,0 +1,508 @@
from flask import Flask, render_template, request, redirect, send_from_directory, jsonify, request as flask_request
import requests, json, os, random
from datetime import datetime
from config import *
from hijridate import Gregorian
app = Flask(__name__)
# Voeg cache-control headers toe voor statische bestanden
@app.after_request
def add_header(response):
if 'Cache-Control' not in response.headers:
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
return response
# Specifieke route voor statische bestanden met cache-control
@app.route('/static/<path:filename>')
def serve_static(filename):
return send_from_directory('static', filename, cache_timeout=0)
SETTINGS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'settings.json')
HADITHS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'hadiths.json')
VUMG_API = 'https://www.vumg.nl/?rest_route=/dpt/v1/prayertime&filter=today'
CLIPS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'clips')
SONOS_API_IP = '192.168.0.114' # IP van host waar Sonos API draait
def fetch_weather_data():
"""Haalt weersinformatie op voor de geconfigureerde locatie"""
try:
params = {
'q': WEATHER_LOCATION,
'appid': OPENWEATHER_API_KEY,
'units': 'metric',
'lang': 'nl'
}
response = requests.get('https://api.openweathermap.org/data/2.5/weather', params=params, timeout=5)
response.raise_for_status()
data = response.json()
weather_info = {
'temperature': round(data['main']['temp']),
'feels_like': round(data['main']['feels_like']),
'description': data['weather'][0]['description'].capitalize(),
'humidity': data['main']['humidity'],
'wind_speed': round(data['wind']['speed'] * 3.6), # m/s naar km/h
'icon': data['weather'][0]['icon']
}
return weather_info
except Exception as e:
print(f"⚠️ Fout bij ophalen weerdata: {e}")
return {
'temperature': '--',
'feels_like': '--',
'description': 'Weer niet beschikbaar',
'humidity': '--',
'wind_speed': '--',
'icon': '01d'
}
def load_hadith():
try:
with open(HADITHS_PATH) as f:
hadiths = json.load(f)['hadiths']
return random.choice(hadiths)
except Exception as e:
print(f"⚠️ Fout bij laden van hadith: {e}")
return {"text": "De daden die Allah het meest liefheeft zijn degenen die regelmatig worden verricht, zelfs als ze klein zijn.", "bron": "Sahih al-Bukhari"}
def load_settings():
try:
if os.path.exists(SETTINGS_PATH) and os.path.getsize(SETTINGS_PATH) > 0:
with open(SETTINGS_PATH) as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Fout bij laden van settings: {e}")
return {"volume": 30, "zones": ["Woonkamer"], "audio_clip": "adhan1.mp3"}
def get_current_volume(settings):
"""Bepaal het juiste volume op basis van de huidige tijd"""
from datetime import datetime
# Fallback naar oude volume als nieuwe instellingen niet bestaan
if 'volume_day' not in settings or 'volume_night' not in settings:
return settings.get('volume', 30)
current_time = datetime.now().strftime('%H:%M')
night_start = settings.get('night_start', '20:00')
day_start = settings.get('day_start', '07:00')
# Converteer tijden naar vergelijkbare format
current_minutes = int(current_time[:2]) * 60 + int(current_time[3:5])
night_minutes = int(night_start[:2]) * 60 + int(night_start[3:5])
day_minutes = int(day_start[:2]) * 60 + int(day_start[3:5])
# Bepaal of het avond is
if night_minutes > day_minutes: # Normale dag/nacht cyclus (bijv. dag 07:00-20:00)
is_night = current_minutes >= night_minutes or current_minutes < day_minutes
else: # Avond gaat over middernacht (bijv. avond 20:00-07:00)
is_night = current_minutes >= night_minutes and current_minutes < day_minutes
if is_night:
return settings.get('volume_night', settings.get('volume', 30) // 2)
else:
return settings.get('volume_day', settings.get('volume', 30))
def fetch_sonos_zones():
try:
res = requests.get(f'http://{SONOS_API_IP}:5005/zones', timeout=5)
res.raise_for_status()
data = res.json()
zones = []
for group in data:
for player in group['members']:
zones.append(player['roomName'])
return sorted(set(zones))
except Exception as e:
print(f'Fout bij ophalen Sonos-zones: {e}')
return ['Woonkamer', 'Slaapkamer', 'Keuken']
def get_date_info():
"""Genereert zowel Gregoriaanse als Hijri datum informatie"""
now = datetime.now()
# Gregoriaanse datum
gregorian_date = now.strftime('%A %d %B %Y')
gregorian_short = now.strftime('%d-%m-%Y')
# Nederlandse maandnamen
dutch_months = {
'January': 'januari', 'February': 'februari', 'March': 'maart',
'April': 'april', 'May': 'mei', 'June': 'juni',
'July': 'juli', 'August': 'augustus', 'September': 'september',
'October': 'oktober', 'November': 'november', 'December': 'december'
}
# Nederlandse dagnamen
dutch_days = {
'Monday': 'maandag', 'Tuesday': 'dinsdag', 'Wednesday': 'woensdag',
'Thursday': 'donderdag', 'Friday': 'vrijdag', 'Saturday': 'zaterdag', 'Sunday': 'zondag'
}
# Vervang Engelse namen door Nederlandse
for eng, nl in dutch_months.items():
gregorian_date = gregorian_date.replace(eng, nl)
for eng, nl in dutch_days.items():
gregorian_date = gregorian_date.replace(eng, nl)
try:
# Converteer naar Hijri datum
hijri_date = Gregorian(now.year, now.month, now.day).to_hijri()
# Arabische maandnamen
hijri_months = [
'محرم', 'صفر', 'ربيع الأول', 'ربيع الثاني', 'جمادى الأولى', 'جمادى الثانية',
'رجب', 'شعبان', 'رمضان', 'شوال', 'ذو القعدة', 'ذو الحجة'
]
# Nederlandse maandnamen voor Hijri
hijri_months_nl = [
'Muharram', 'Safar', 'Rabi al-Awwal', 'Rabi al-Thani', 'Jumada al-Awwal', 'Jumada al-Thani',
'Rajab', 'Sha\'ban', 'Ramadan', 'Shawwal', 'Dhu al-Qi\'dah', 'Dhu al-Hijjah'
]
hijri_arabic = f"{hijri_date.day} {hijri_months[hijri_date.month - 1]} {hijri_date.year} هـ"
hijri_dutch = f"{hijri_date.day} {hijri_months_nl[hijri_date.month - 1]} {hijri_date.year} AH"
except Exception as e:
print(f"⚠️ Fout bij Hijri conversie: {e}")
hijri_arabic = "التاريخ الهجري غير متاح"
hijri_dutch = "Hijri datum niet beschikbaar"
return {
'gregorian_full': gregorian_date,
'gregorian_short': gregorian_short,
'hijri_arabic': hijri_arabic,
'hijri_dutch': hijri_dutch,
'current_time': now.strftime('%H:%M')
}
@app.route('/')
def index():
settings = load_settings()
next_time = "Onbekend"
next_name = "Onbekend"
debug_data = {}
gebedstijden = {}
hadith = load_hadith()
weather = fetch_weather_data()
date_info = get_date_info()
try:
res = requests.get(VUMG_API)
res.raise_for_status()
data = res.json()[0]
gebedstijden = {
"Fajr": data.get("fajr_jamah", "00:00"),
"Zuhr": data.get("zuhr_jamah", "00:00"),
"Asr": data.get("asr_jamah", "00:00"),
"Maghrib": data.get("maghrib_jamah", "00:00"),
"Isha": data.get("isha_jamah", "00:00")
}
now = datetime.now().strftime('%H:%M')
for naam, tijd in gebedstijden.items():
if tijd > now:
next_time = tijd
next_name = naam
break
else:
next_time = list(gebedstijden.values())[0]
next_name = list(gebedstijden.keys())[0]
debug_data = {
"api_response": data,
"gebedstijden": gebedstijden,
"now": now,
"next_time": next_time,
"next_name": next_name
}
except Exception as e:
print("❌ Fout bij ophalen/verwerken van gebedstijden:", e)
dua = "اللّهُمَّ اجْعَلْ صَلاتِي نُورًا"
return render_template('index.html',
next_time=next_time,
next_name=next_name,
dua=dua,
hadith=hadith,
gebedstijden=gebedstijden,
debug=debug_data,
settings=settings,
weather=weather,
date_info=date_info)
@app.route('/instellingen', methods=['GET', 'POST'])
def instellingen():
settings = load_settings()
clips = [f for f in os.listdir(CLIPS_PATH) if f.endswith('.mp3') or f.endswith('.wav')]
alle_zones = fetch_sonos_zones()
if request.method == 'POST':
# Nieuwe volume instellingen
if 'volume_day' in request.form and 'volume_night' in request.form:
settings['volume_day'] = int(request.form['volume_day'])
settings['volume_night'] = int(request.form['volume_night'])
settings['night_start'] = request.form['night_start']
settings['day_start'] = request.form['day_start']
# Update oude volume voor backward compatibility
settings['volume'] = settings['volume_day']
else:
# Fallback: gebruik default waarden als velden ontbreken
settings['volume_day'] = settings.get('volume_day', 15)
settings['volume_night'] = settings.get('volume_night', 8)
settings['night_start'] = settings.get('night_start', '20:00')
settings['day_start'] = settings.get('day_start', '07:00')
settings['volume'] = settings['volume_day']
settings['zones'] = request.form.getlist('zones')
settings['audio_clip'] = request.form['audio_clip']
with open(SETTINGS_PATH, 'w') as f:
json.dump(settings, f, indent=2)
if 'test_clip' in request.form:
# Gebruik het juiste volume voor de test
test_volume = get_current_volume(settings)
for zone in settings['zones']:
url = f"http://{SONOS_API_IP}:5005/{zone}/clip/{settings['audio_clip']}/{test_volume}"
try:
r = requests.get(url, timeout=3)
print(f"🎵 Test clip verzonden naar {zone}, status: {r.status_code}")
except Exception as e:
print(f"❌ Fout bij afspelen op zone {zone}: {e}")
return redirect('/instellingen')
return render_template('settings.html',
settings=settings,
alle_zones=alle_zones,
clips=clips)
@app.route('/api/hadith')
def api_hadith():
hadith = load_hadith()
return jsonify(hadith)
@app.route('/api/mute', methods=['POST'])
def toggle_mute():
try:
data = flask_request.get_json()
mute = data.get('mute', False)
with open(SETTINGS_PATH, 'r+') as f:
settings = json.load(f)
settings['mute'] = mute
f.seek(0)
json.dump(settings, f, indent=2)
f.truncate()
return jsonify({'success': True, 'mute': mute})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/weather')
def api_weather():
"""API endpoint voor het ophalen van actuele weerdata"""
weather = fetch_weather_data()
return jsonify(weather)
@app.route('/api/date-info')
def api_date_info():
"""API endpoint voor het ophalen van actuele datum informatie"""
date_info = get_date_info()
return jsonify(date_info)
@app.route('/api/test-audio', methods=['POST'])
def test_audio():
"""Test een audio bestand lokaal in de browser"""
try:
data = flask_request.get_json()
audio_file = data.get('audio_file', 'adhan1.mp3')
# Controleer of het bestand bestaat
audio_path = os.path.join(CLIPS_PATH, audio_file)
if not os.path.exists(audio_path):
return jsonify({'success': False, 'error': 'Audio bestand niet gevonden'}), 404
return jsonify({
'success': True,
'message': f'Audio bestand {audio_file} is beschikbaar',
'url': f'/static/clips/{audio_file}'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/debug')
def debug():
"""Debug pagina voor het testen van adhaan functionaliteit"""
settings = load_settings()
gebedstijden = {}
try:
res = requests.get(VUMG_API)
res.raise_for_status()
data = res.json()[0]
gebedstijden = {
"Fajr": data.get("fajr_jamah", "00:00"),
"Zuhr": data.get("zuhr_jamah", "00:00"),
"Asr": data.get("asr_jamah", "00:00"),
"Maghrib": data.get("maghrib_jamah", "00:00"),
"Isha": data.get("isha_jamah", "00:00")
}
except Exception as e:
print("❌ Fout bij ophalen gebedstijden voor debug:", e)
# Fallback tijden voor testing
gebedstijden = {
"Fajr": "06:00",
"Zuhr": "12:30",
"Asr": "15:00",
"Maghrib": "17:30",
"Isha": "19:00"
}
return render_template('debug.html',
gebedstijden=gebedstijden,
settings=settings)
@app.route('/api/debug-adhaan', methods=['POST'])
def debug_adhaan():
"""API endpoint voor debug adhaan testing"""
import time
try:
data = flask_request.get_json()
prayer_name = data.get('prayer', 'Test')
test_time = data.get('time', 'Onbekend')
settings = load_settings()
# Check mute status
if settings.get('mute', False):
return jsonify({
'success': False,
'error': 'Adhaan niet afgespeeld: mute staat aan',
'muted': True
})
# Verbeterde Sonos afspeling
success_count = 0
error_messages = []
zones = settings.get('zones', ['Woonkamer'])
audio_clip = settings.get('audio_clip', 'adhan1.mp3')
volume = get_current_volume(settings)
for zone in zones:
try:
# Stop eerst alle audio op deze zone
stop_url = f"http://{SONOS_API_IP}:5005/{zone}/pause"
requests.get(stop_url, timeout=2)
time.sleep(0.5) # Korte pauze
# Speel de clip af
clip_url = f"http://{SONOS_API_IP}:5005/{zone}/clip/{audio_clip}/{volume}"
response = requests.get(clip_url, timeout=10)
if response.status_code == 200:
success_count += 1
print(f"🔧 DEBUG: Adhaan afgespeeld op {zone} voor {prayer_name} om {test_time}")
else:
error_messages.append(f"{zone}: HTTP {response.status_code}")
except requests.exceptions.Timeout:
error_messages.append(f"{zone}: Timeout")
except Exception as e:
error_messages.append(f"{zone}: {str(e)}")
# Pauze tussen zones
if len(zones) > 1:
time.sleep(1)
if success_count > 0:
return jsonify({
'success': True,
'message': f'Adhaan afgespeeld op {success_count}/{len(zones)} zone(s)',
'errors': error_messages if error_messages else None
})
else:
return jsonify({
'success': False,
'error': f'Geen zones bereikbaar: {", ".join(error_messages)}'
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/sonos-status')
def sonos_status():
"""API endpoint voor Sonos status controle"""
try:
settings = load_settings()
zones = settings.get('zones', ['Woonkamer'])
status_info = {}
for zone in zones:
try:
# Haal zone status op
status_url = f"http://{SONOS_API_IP}:5005/{zone}/state"
response = requests.get(status_url, timeout=3)
if response.status_code == 200:
zone_data = response.json()
status_info[zone] = {
'status': 'online',
'playbackState': zone_data.get('playbackState', 'unknown'),
'currentTrack': zone_data.get('currentTrack', {}).get('title', 'Geen track'),
'volume': zone_data.get('volume', 0)
}
else:
status_info[zone] = {'status': 'error', 'error': f'HTTP {response.status_code}'}
except Exception as e:
status_info[zone] = {'status': 'offline', 'error': str(e)}
return jsonify({
'success': True,
'zones': status_info,
'api_ip': SONOS_API_IP
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/sonos-stop', methods=['POST'])
def sonos_stop():
"""API endpoint om alle Sonos zones te stoppen"""
try:
settings = load_settings()
zones = settings.get('zones', ['Woonkamer'])
stopped_zones = []
for zone in zones:
try:
stop_url = f"http://{SONOS_API_IP}:5005/{zone}/pause"
response = requests.get(stop_url, timeout=3)
if response.status_code == 200:
stopped_zones.append(zone)
except Exception as e:
print(f"Fout bij stoppen {zone}: {e}")
return jsonify({
'success': True,
'message': f'Audio gestopt op {len(stopped_zones)} zone(s)',
'stopped_zones': stopped_zones
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/quran')
def quran():
"""Quran speler pagina"""
return render_template('quran.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

21
adhan-webapp/config.py Normal file
View File

@ -0,0 +1,21 @@
# Configuratie bestand voor de Adhaan applicatie
# OpenWeatherMap API configuratie
# Verkrijg een gratis API key op: https://openweathermap.org/api
OPENWEATHER_API_KEY = '99caf7628fd7197acd71df3fc2cafbdb'
# Locatie voor weersinformatie
WEATHER_LOCATION = 'Amsterdam,NL'
# Sonos API configuratie
SONOS_API_IP = '192.168.0.114'
# VUMG API voor gebedstijden
VUMG_API_URL = 'https://www.vumg.nl/?rest_route=/dpt/v1/prayertime&filter=today'
# Bestandspaden
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SETTINGS_PATH = os.path.join(BASE_DIR, 'settings.json')
HADITHS_PATH = os.path.join(BASE_DIR, 'static', 'hadiths.json')
CLIPS_PATH = os.path.join(BASE_DIR, 'static', 'clips')

3
adhan-webapp/done Normal file
View File

@ -0,0 +1,3 @@
Mon May 26 16:08:33 CEST 2025: Hadith auto-update functionaliteit toegevoegd - JavaScript code toegevoegd om elke 30 seconden een nieuwe hadith op te halen via /api/hadith endpoint
Mon May 26 16:19:32 CEST 2025: Instellingen pagina design aangepast - Hetzelfde design als rest van applicatie met theme ondersteuning, Material Icons en consistente styling
Mon May 26 18:17:57 CEST 2025: Tijdzone probleem opgelost - Container gebruikt nu Nederlandse tijd (18:17), volgende test bij Maghrib om 21:45

View File

@ -0,0 +1,3 @@
Flask==3.0.0
requests==2.31.0
hijridate==2.5.0

View File

@ -0,0 +1,13 @@
{
"volume": 15,
"volume_day": 15,
"volume_night": 8,
"night_start": "00:00",
"day_start": "07:00",
"zones": [
"Keuken",
"Slaapkamer"
],
"audio_clip": "adhan1.mp3",
"mute": false
}

16
adhan-webapp/start_all.py Normal file
View File

@ -0,0 +1,16 @@
import subprocess
import sys
import time
# Start Flask-app
flask_proc = subprocess.Popen([sys.executable, 'app.py'])
# Start adhan_cron.py elke minuut in een loop
try:
while True:
cron_proc = subprocess.Popen([sys.executable, 'adhan_cron.py'])
cron_proc.wait()
time.sleep(60)
except KeyboardInterrupt:
flask_proc.terminate()
sys.exit(0)

BIN
adhan-webapp/static/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,22 @@
# Adhaan Audio Bestanden
Deze directory bevat verschillende adhaan audio bestanden voor de gebedstijden applicatie.
## Beschikbare Adhaan Stijlen:
1. **adhan1.mp3** - Klassieke adhaan (origineel)
2. **adhan_mekka.mp3** - Mekka stijl adhaan
3. **adhan_medina.mp3** - Medina stijl adhaan
4. **adhan_kort.mp3** - Korte versie adhaan
5. **adhan_traditioneel.mp3** - Traditionele Nederlandse moskee stijl
## Gebruik:
De gebruiker kan in de instellingen kiezen welke adhaan-stijl gebruikt wordt.
Alle bestanden moeten in MP3 formaat zijn voor compatibiliteit met de browser en Sonos systemen.
## Toevoegen van nieuwe bestanden:
1. Plaats het MP3 bestand in deze directory
2. Het bestand wordt automatisch beschikbaar in de instellingen
3. Zorg ervoor dat het bestand niet te groot is (max 5MB aanbevolen)

Binary file not shown.

View File

@ -0,0 +1 @@
Sun May 25 22:08:19 CEST 2025: Weersinformatie toegevoegd met OpenWeatherMap API integratie

View File

@ -0,0 +1,105 @@
function updateCurrentTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
document.getElementById("current-time").textContent = timeStr;
// Update datum informatie elke minuut
updateDateInfo();
}
function updateDateInfo() {
fetch('/api/date-info')
.then(response => response.json())
.then(data => {
const gregorianElement = document.querySelector('.gregorian-datum');
const hijriArabicElement = document.querySelector('.hijri-datum-arabic');
const hijriDutchElement = document.querySelector('.hijri-datum-dutch');
if (gregorianElement) gregorianElement.textContent = data.gregorian_full;
if (hijriArabicElement) hijriArabicElement.textContent = data.hijri_arabic;
if (hijriDutchElement) hijriDutchElement.textContent = data.hijri_dutch;
})
.catch(error => {
console.log('Fout bij bijwerken datum:', error);
});
}
// Gebedstijden ophalen uit een globale variabele (wordt in de template gezet)
let prayerTimes = window.prayerTimes || [];
let prayerNames = window.prayerNames || [];
let currentPrayerIndex = 0;
function startCountdowns(times, names) {
prayerTimes = times;
prayerNames = names;
currentPrayerIndex = getNextPrayerIndex();
startCountdown(prayerTimes[currentPrayerIndex], prayerNames[currentPrayerIndex]);
}
function getNextPrayerIndex() {
const now = new Date();
for (let i = 0; i < prayerTimes.length; i++) {
const [h, m] = prayerTimes[i].split(":");
const t = new Date();
t.setHours(parseInt(h));
t.setMinutes(parseInt(m));
t.setSeconds(0);
if (t > now) return i;
}
return 0; // fallback: eerste gebed
}
function startCountdown(targetTimeStr, prayerName) {
const parts = targetTimeStr.split(":");
let target = new Date();
target.setHours(parseInt(parts[0]));
target.setMinutes(parseInt(parts[1]));
target.setSeconds(0);
document.querySelector('.huidig-gebed .naam').textContent = prayerName;
function update() {
const now = new Date();
let diff = Math.floor((target - now) / 1000);
if (diff <= 0) {
// Speel adhaan af via browser
const audio = document.getElementById('adhanAudio');
if (audio) audio.play();
// Ga naar volgende gebed
currentPrayerIndex = (currentPrayerIndex + 1) % prayerTimes.length;
const nextTime = prayerTimes[currentPrayerIndex];
const nextName = prayerNames[currentPrayerIndex];
startCountdown(nextTime, nextName);
return;
}
const h = String(Math.floor(diff / 3600)).padStart(2, '0');
const m = String(Math.floor((diff % 3600) / 60)).padStart(2, '0');
const s = String(diff % 60).padStart(2, '0');
document.getElementById("countdown").textContent = `${h}:${m}:${s}`;
}
update();
window._countdownInterval && clearInterval(window._countdownInterval);
window._countdownInterval = setInterval(update, 1000);
}
function setupThemeToggle() {
const toggleBtn = document.getElementById("themeToggle");
const html = document.documentElement;
function applyTheme(theme) {
html.className = theme;
localStorage.setItem('theme', theme);
}
const saved = localStorage.getItem('theme') || 'light';
applyTheme(saved);
toggleBtn.addEventListener("click", () => {
const current = html.className === 'light' ? 'dark' : 'light';
applyTheme(current);
});
}

View File

@ -0,0 +1,104 @@
{
"hadiths": [
{
"text": "De daden die Allah het meest liefheeft zijn degenen die regelmatig worden verricht, zelfs als ze klein zijn.",
"bron": "Sahih al-Bukhari"
},
{
"text": "Wie gelooft in Allah en de Laatste Dag, laat hem zijn buur goed behandelen.",
"bron": "Sahih Muslim"
},
{
"text": "De sterkste onder jullie is degene die zijn woede kan beheersen.",
"bron": "Sahih al-Bukhari"
},
{
"text": "Een glimlach naar je broeder is een daad van liefdadigheid.",
"bron": "At-Tirmidhi"
},
{
"text": "Allah is Barmhartig en houdt van barmhartigheid. Hij geeft barmhartigheid aan degenen die barmhartig zijn.",
"bron": "Sahih al-Bukhari"
},
{
"text": "Wie niet dankbaar is tegenover de mensen, is niet dankbaar tegenover Allah.",
"bron": "At-Tirmidhi"
},
{
"text": "De beste onder jullie zijn degenen die het beste karakter hebben.",
"bron": "Sahih al-Bukhari"
},
{
"text": "Allah kijkt niet naar jullie uiterlijk of jullie bezittingen, maar Hij kijkt naar jullie harten en jullie daden.",
"bron": "Sahih Muslim"
},
{
"text": "Wie zijn gebed verwaarloost, heeft de band met zijn religie verbroken.",
"bron": "Musnad Ahmad"
},
{
"text": "De gelovige die met mensen omgaat en hun lasten verdraagt, is beter dan de gelovige die niet met mensen omgaat en hun lasten niet verdraagt.",
"bron": "At-Tirmidhi"
},
{
"text": "Weet dat met elke moeilijkheid verlichting komt.",
"bron": "Koran 94:5-6"
},
{
"text": "De sterkste mens is niet degene die anderen overwint, maar degene die zichzelf overwint wanneer hij boos is.",
"bron": "Sahih al-Bukhari"
},
{
"text": "Allah belast een ziel niet boven haar vermogen.",
"bron": "Koran 2:286"
},
{
"text": "Allah heeft de slechte gedachten van mijn ummah vergeven, zolang ze er niet naar handelen of erover spreken.",
"bron": "Sahih al-Bukhari"
},
{
"text": "Shaytaan komt tot een van jullie en zegt: 'Wie heeft dit geschapen? Wie heeft dat geschapen?' totdat hij zegt: 'Wie heeft Allah geschapen?' Wanneer dit punt wordt bereikt, zoek dan toevlucht bij Allah en stop ermee.",
"bron": "Sahih al-Bukhari"
},
{
"text": "De gelovigen klaagden bij de Profeet over dwanggedachten. Hij zei: 'Dat is duidelijk geloof.' - Het feit dat je je zorgen maakt over deze gedachten toont je geloof aan.",
"bron": "Sahih Muslim"
},
{
"text": "Wanneer een van jullie getroffen wordt door dwanggedachten in zijn geloof, laat hem zeggen: 'Ik geloof in Allah en Zijn boodschappers.'",
"bron": "Sahih Muslim"
},
{
"text": "Allah heeft mijn ummah vergeven voor wat hun harten ingeven, zolang ze er niet naar handelen of erover spreken.",
"bron": "Sahih al-Bukhari"
},
{
"text": "Zoek toevlucht bij Allah tegen de ingeefdheid van shaytaan. Zeg: 'A'udhu billahi min ash-shaytaan ir-rajeem.'",
"bron": "Sahih Muslim"
},
{
"text": "Wanneer shaytaan jullie probeert af te leiden tijdens het gebed, spuug dan drie keer naar links en zoek toevlucht bij Allah.",
"bron": "Sahih al-Bukhari"
},
{
"text": "De harten van de kinderen van Adam zijn allemaal tussen twee vingers van de Barmhartige. Hij keert ze om zoals Hij wil.",
"bron": "Sahih Muslim"
},
{
"text": "O Allah, Gij die de harten doet keren, keer mijn hart naar Uw gehoorzaamheid.",
"bron": "At-Tirmidhi"
},
{
"text": "Degene die zegt: 'La hawla wa la quwwata illa billah' (Er is geen macht behalve bij Allah), wordt geholpen tegen zijn zorgen en verdriet.",
"bron": "Abu Dawud"
},
{
"text": "Wanneer je bezorgd of angstig bent, zeg dan veel: 'La hawla wa la quwwata illa billahi al-'ali al-'azeem.'",
"bron": "Al-Hakim"
},
{
"text": "Allah zal voor degene die Hem vreest een uitweg maken en hem voorzien van waar hij het niet verwacht.",
"bron": "Koran 65:2-3"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

View File

@ -0,0 +1,402 @@
/* Quran pagina - volledige breedte met smal zijmenu en correcte theme ondersteuning */
/* Default (dark) theme variabelen voor Quran pagina */
:root {
--quran-bg: #111;
--quran-text: #fff;
--quran-panel-bg: rgba(24, 24, 24, 0.95);
--quran-panel-text: #fff;
--quran-accent: #fff;
--quran-border: rgba(255, 255, 255, 0.1);
--quran-card-bg: rgba(40, 40, 40, 0.95);
--quran-text-secondary: #bbb;
--quran-hover-bg: rgba(255, 255, 255, 0.1);
}
/* Light theme variabelen voor Quran pagina */
html.light {
--quran-bg: #f7fafc;
--quran-text: #222;
--quran-panel-bg: #fff;
--quran-panel-text: #222;
--quran-accent: #d4af37;
--quran-border: #e0e6ed;
--quran-card-bg: #ffffff;
--quran-text-secondary: #666;
--quran-hover-bg: rgba(212, 175, 55, 0.1);
}
/* Zorg ervoor dat het zijmenu de Quran theme variabelen gebruikt */
.app .right.new-layout {
min-width: 120px !important;
max-width: 120px !important;
flex: 0 0 120px !important;
padding: 0 !important;
background: var(--quran-panel-bg) !important;
color: var(--quran-text) !important;
border: 1px solid var(--quran-border) !important;
}
/* Zorg dat alleen vertical-tijden wordt getoond */
.app .hadith-tijden-row {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
background: var(--quran-panel-bg);
}
.app .hadith-center {
display: none !important; /* Verberg de hadith sectie */
}
.app .vertical-tijden {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 120px;
width: 100%;
background: none;
height: 100%;
padding: 2rem 0;
}
/* Zorg dat zijmenu items de Quran theme variabelen gebruiken */
.app .tijden-en-icoontjes {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.7rem;
}
.app .tijden-en-icoontjes li {
font-size: 1.25rem;
color: var(--quran-text) !important;
opacity: 0.85;
font-family: 'Lato', Arial, sans-serif;
letter-spacing: 0.04em;
transition: color 0.2s, opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.app .tijden-rij {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: auto;
gap: 0.1rem;
}
.app .tijden-rij .naam {
font-size: 1.05rem;
color: var(--quran-text) !important;
font-family: 'Lato', Arial, sans-serif;
opacity: 0.85;
letter-spacing: 0.04em;
text-align: center;
margin-bottom: 0.1rem;
}
.app .tijden-rij .tijd {
font-size: 1.15rem;
color: var(--quran-text) !important;
font-family: 'Lato', Arial, sans-serif;
opacity: 0.85;
text-align: center;
}
.app .tijden-en-icoontjes .icoon-link {
font-size: 1.35rem;
color: var(--quran-text) !important;
text-decoration: none;
background: none;
border: none;
opacity: 0.85;
transition: color 0.2s, opacity 0.2s;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.app .tijden-en-icoontjes .icoon-link:hover {
color: var(--quran-accent) !important;
opacity: 1;
}
.app .icoon-link .material-icons {
font-size: 1.7rem;
color: var(--quran-text) !important;
vertical-align: middle;
display: inline-block;
line-height: 1;
transition: color 0.2s, opacity 0.2s;
}
.app .icoon-link:hover .material-icons {
color: var(--quran-accent) !important;
}
/* Main content area - twee kolommen layout */
.quran-full-content {
flex: 1;
display: grid;
grid-template-columns: 500px 1fr;
gap: 0;
height: 100vh;
background: var(--quran-bg);
color: var(--quran-text);
overflow: hidden;
}
/* Linker kolom - Player */
.quran-left-column {
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
border-right: 1px solid var(--quran-border);
background: var(--quran-panel-bg);
}
/* Rechter kolom - Zoeken en Sura lijst */
.quran-right-column {
display: flex;
flex-direction: column;
background: var(--quran-bg);
overflow: hidden;
}
/* Header styling */
.quran-header {
text-align: left;
padding: 0;
margin-bottom: 20px;
}
.quran-header h1 {
font-family: 'Cairo', sans-serif;
font-size: 2.2em;
color: var(--quran-accent);
margin: 0;
padding-bottom: 15px;
border-bottom: 1px solid var(--quran-border);
}
/* Reciter Selection */
.reciter-selection {
margin-bottom: 30px;
}
.reciter-selection label {
display: block;
margin-bottom: 10px;
color: var(--quran-text);
font-size: 0.9em;
opacity: 0.8;
}
.custom-select {
width: 100%;
padding: 12px;
border: 1px solid var(--quran-border);
border-radius: 8px;
background: var(--quran-card-bg);
color: var(--quran-text);
font-size: 1em;
transition: all 0.2s ease;
}
.custom-select:hover {
background: var(--quran-hover-bg);
}
/* Player Section */
.player-section {
background: var(--quran-card-bg);
border-radius: 12px;
padding: 25px;
margin-top: auto;
margin-bottom: 20px;
}
.sura-info h2 {
font-size: 1.8em;
margin-bottom: 10px;
}
.sura-info p {
font-size: 0.9em;
margin-bottom: 20px;
}
/* Player Controls */
.player-controls {
display: flex;
justify-content: center;
gap: 15px;
margin: 20px 0;
}
.control-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: transparent;
border: 2px solid var(--quran-accent);
color: var(--quran-accent);
transition: all 0.2s ease;
}
.play-btn {
width: 60px;
height: 60px;
background: var(--quran-accent);
color: var(--quran-bg);
}
.control-btn:hover {
transform: scale(1.1);
background: var(--quran-accent);
color: var(--quran-bg);
}
/* Progress Bar */
.progress-section {
display: flex;
align-items: center;
gap: 15px;
}
.progress-bar {
height: 4px;
border-radius: 2px;
background: var(--quran-border);
}
/* Search Section */
.search-section {
padding: 20px;
background: var(--quran-panel-bg);
border-bottom: 1px solid var(--quran-border);
}
.search-section label {
display: block;
margin-bottom: 10px;
color: var(--quran-text);
font-size: 0.9em;
opacity: 0.8;
}
.search-input {
width: 97%;
padding: 12px;
border: 1px solid var(--quran-border);
border-radius: 8px;
background: var(--quran-card-bg);
color: var(--quran-text);
font-size: 1em;
}
/* Sura List */
.sura-list-section {
flex: 1;
padding: 20px;
overflow-y: auto;
background: var(--quran-bg);
}
.sura-list-section h3 {
font-size: 1.4em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--quran-border);
}
.sura-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
padding: 0;
}
.sura-item {
background: var(--quran-card-bg);
border: 1px solid var(--quran-border);
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: all 0.2s ease;
}
.sura-item:hover {
transform: translateY(-2px);
background: var(--quran-hover-bg);
border-color: var(--quran-accent);
}
.sura-number {
font-size: 0.8em;
opacity: 0.7;
margin-bottom: 5px;
}
.sura-name-arabic {
font-size: 1.4em;
margin-bottom: 5px;
}
.sura-name-dutch {
font-size: 0.9em;
opacity: 0.8;
}
/* Responsive Design */
@media (max-width: 1200px) {
.quran-full-content {
grid-template-columns: 1fr;
height: auto;
}
.quran-left-column {
border-right: none;
border-bottom: 1px solid var(--quran-border);
}
.quran-right-column {
height: 100vh;
}
}
@media (max-width: 600px) {
.quran-header h1 {
font-size: 1.8em;
}
.sura-grid {
grid-template-columns: 1fr;
}
.player-section {
padding: 15px;
}
.control-btn {
width: 40px;
height: 40px;
}
.play-btn {
width: 50px;
height: 50px;
}
}

View File

@ -0,0 +1,524 @@
// Quran Speler JavaScript
class QuranPlayer {
constructor() {
console.log('QuranPlayer constructor called');
this.currentIndex = 0;
this.isPlaying = false;
this.currentReciter = '';
this.audio = document.getElementById('quranAudio');
this.suras = this.getSuraList();
this.reciters = this.getReciterList();
this.loadingTimeout = null;
console.log('Suras loaded:', this.suras.length);
console.log('Reciters loaded:', this.reciters.length);
this.init();
}
init() {
console.log('Initializing QuranPlayer...');
this.setupEventListeners();
this.populateReciters();
this.renderSuraGrid();
this.setupAudioEvents();
console.log('QuranPlayer initialized successfully');
}
getSuraList() {
return [
{ number: 1, arabic: "الفاتحة", dutch: "Al-Faatihah", file: "001" },
{ number: 2, arabic: "البقرة", dutch: "Al-Baqarah", file: "002" },
{ number: 3, arabic: "آل عمران", dutch: "Aali-Imraan", file: "003" },
{ number: 4, arabic: "النساء", dutch: "An-Nisaa", file: "004" },
{ number: 5, arabic: "المائدة", dutch: "Al-Ma'idah", file: "005" },
{ number: 6, arabic: "الأنعام", dutch: "Al-An'aam", file: "006" },
{ number: 7, arabic: "الأعراف", dutch: "Al-A'raaf", file: "007" },
{ number: 8, arabic: "الأنفال", dutch: "Al-Anfaal", file: "008" },
{ number: 9, arabic: "التوبة", dutch: "At-Tawbah", file: "009" },
{ number: 10, arabic: "يونس", dutch: "Yoonus", file: "010" },
{ number: 11, arabic: "هود", dutch: "Hood", file: "011" },
{ number: 12, arabic: "يوسف", dutch: "Yoosuf", file: "012" },
{ number: 13, arabic: "الرعد", dutch: "Ar-Ra'd", file: "013" },
{ number: 14, arabic: "إبراهيم", dutch: "Ibraheem", file: "014" },
{ number: 15, arabic: "الحجر", dutch: "Al-Hijr", file: "015" },
{ number: 16, arabic: "النحل", dutch: "An-Nahl", file: "016" },
{ number: 17, arabic: "الإسراء", dutch: "Al-Israa", file: "017" },
{ number: 18, arabic: "الكهف", dutch: "Al-Kahf", file: "018" },
{ number: 19, arabic: "مريم", dutch: "Maryam", file: "019" },
{ number: 20, arabic: "طه", dutch: "Taa Haa", file: "020" },
{ number: 21, arabic: "الأنبياء", dutch: "Al-Anbiyaa", file: "021" },
{ number: 22, arabic: "الحج", dutch: "Al-Hajj", file: "022" },
{ number: 23, arabic: "المؤمنون", dutch: "Al-Mu'minoon", file: "023" },
{ number: 24, arabic: "النور", dutch: "An-Noor", file: "024" },
{ number: 25, arabic: "الفرقان", dutch: "Al-Furqaan", file: "025" },
{ number: 26, arabic: "الشعراء", dutch: "Ash-Shu'araa", file: "026" },
{ number: 27, arabic: "النمل", dutch: "An-Naml", file: "027" },
{ number: 28, arabic: "القصص", dutch: "Al-Qasas", file: "028" },
{ number: 29, arabic: "العنكبوت", dutch: "Al-Ankaboot", file: "029" },
{ number: 30, arabic: "الروم", dutch: "Ar-Room", file: "030" },
{ number: 31, arabic: "لقمان", dutch: "Luqmaan", file: "031" },
{ number: 32, arabic: "السجدة", dutch: "As-Sajdah", file: "032" },
{ number: 33, arabic: "الأحزاب", dutch: "Al-Ahzaab", file: "033" },
{ number: 34, arabic: "سبأ", dutch: "Saba", file: "034" },
{ number: 35, arabic: "فاطر", dutch: "Faatir", file: "035" },
{ number: 36, arabic: "يس", dutch: "Yaa Seen", file: "036" },
{ number: 37, arabic: "الصافات", dutch: "As-Saaffaat", file: "037" },
{ number: 38, arabic: "ص", dutch: "Saad", file: "038" },
{ number: 39, arabic: "الزمر", dutch: "Az-Zumar", file: "039" },
{ number: 40, arabic: "غافر", dutch: "Ghaafir", file: "040" },
{ number: 41, arabic: "فصلت", dutch: "Fussilat", file: "041" },
{ number: 42, arabic: "الشورى", dutch: "Ash-Shooraa", file: "042" },
{ number: 43, arabic: "الزخرف", dutch: "Az-Zukhruf", file: "043" },
{ number: 44, arabic: "الدخان", dutch: "Ad-Dukhaan", file: "044" },
{ number: 45, arabic: "الجاثية", dutch: "Al-Jaathiyah", file: "045" },
{ number: 46, arabic: "الأحقاف", dutch: "Al-Ahqaaf", file: "046" },
{ number: 47, arabic: "محمد", dutch: "Muhammad", file: "047" },
{ number: 48, arabic: "الفتح", dutch: "Al-Fath", file: "048" },
{ number: 49, arabic: "الحجرات", dutch: "Al-Hujuraat", file: "049" },
{ number: 50, arabic: "ق", dutch: "Qaaf", file: "050" },
{ number: 51, arabic: "الذاريات", dutch: "Adh-Dhaariyaat", file: "051" },
{ number: 52, arabic: "الطور", dutch: "At-Toor", file: "052" },
{ number: 53, arabic: "النجم", dutch: "An-Najm", file: "053" },
{ number: 54, arabic: "القمر", dutch: "Al-Qamar", file: "054" },
{ number: 55, arabic: "الرحمن", dutch: "Ar-Rahmaan", file: "055" },
{ number: 56, arabic: "الواقعة", dutch: "Al-Waaqi'ah", file: "056" },
{ number: 57, arabic: "الحديد", dutch: "Al-Hadeed", file: "057" },
{ number: 58, arabic: "المجادلة", dutch: "Al-Mujaadilah", file: "058" },
{ number: 59, arabic: "الحشر", dutch: "Al-Hashr", file: "059" },
{ number: 60, arabic: "الممتحنة", dutch: "Al-Mumtahanah", file: "060" },
{ number: 61, arabic: "الصف", dutch: "As-Saff", file: "061" },
{ number: 62, arabic: "الجمعة", dutch: "Al-Jumu'ah", file: "062" },
{ number: 63, arabic: "المنافقون", dutch: "Al-Munaafiqoon", file: "063" },
{ number: 64, arabic: "التغابن", dutch: "At-Taghaabun", file: "064" },
{ number: 65, arabic: "الطلاق", dutch: "At-Talaaq", file: "065" },
{ number: 66, arabic: "التحريم", dutch: "At-Tahreem", file: "066" },
{ number: 67, arabic: "الملك", dutch: "Al-Mulk", file: "067" },
{ number: 68, arabic: "القلم", dutch: "Al-Qalam", file: "068" },
{ number: 69, arabic: "الحاقة", dutch: "Al-Haaqqah", file: "069" },
{ number: 70, arabic: "المعارج", dutch: "Al-Ma'aarij", file: "070" },
{ number: 71, arabic: "نوح", dutch: "Nooh", file: "071" },
{ number: 72, arabic: "الجن", dutch: "Al-Jinn", file: "072" },
{ number: 73, arabic: "المزمل", dutch: "Al-Muzzammil", file: "073" },
{ number: 74, arabic: "المدثر", dutch: "Al-Muddaththir", file: "074" },
{ number: 75, arabic: "القيامة", dutch: "Al-Qiyaamah", file: "075" },
{ number: 76, arabic: "الإنسان", dutch: "Al-Insaan", file: "076" },
{ number: 77, arabic: "المرسلات", dutch: "Al-Mursalaat", file: "077" },
{ number: 78, arabic: "النبأ", dutch: "An-Naba", file: "078" },
{ number: 79, arabic: "النازعات", dutch: "An-Naazi'aat", file: "079" },
{ number: 80, arabic: "عبس", dutch: "Abasa", file: "080" },
{ number: 81, arabic: "التكوير", dutch: "At-Takweer", file: "081" },
{ number: 82, arabic: "الانفطار", dutch: "Al-Infitaar", file: "082" },
{ number: 83, arabic: "المطففين", dutch: "Al-Mutaffifeen", file: "083" },
{ number: 84, arabic: "الانشقاق", dutch: "Al-Inshiqaaq", file: "084" },
{ number: 85, arabic: "البروج", dutch: "Al-Burooj", file: "085" },
{ number: 86, arabic: "الطارق", dutch: "At-Taariq", file: "086" },
{ number: 87, arabic: "الأعلى", dutch: "Al-A'laa", file: "087" },
{ number: 88, arabic: "الغاشية", dutch: "Al-Ghaashiyah", file: "088" },
{ number: 89, arabic: "الفجر", dutch: "Al-Fajr", file: "089" },
{ number: 90, arabic: "البلد", dutch: "Al-Balad", file: "090" },
{ number: 91, arabic: "الشمس", dutch: "Ash-Shams", file: "091" },
{ number: 92, arabic: "الليل", dutch: "Al-Layl", file: "092" },
{ number: 93, arabic: "الضحى", dutch: "Ad-Duhaa", file: "093" },
{ number: 94, arabic: "الشرح", dutch: "Ash-Sharh", file: "094" },
{ number: 95, arabic: "التين", dutch: "At-Teen", file: "095" },
{ number: 96, arabic: "العلق", dutch: "Al-Alaq", file: "096" },
{ number: 97, arabic: "القدر", dutch: "Al-Qadr", file: "097" },
{ number: 98, arabic: "البينة", dutch: "Al-Bayyinah", file: "098" },
{ number: 99, arabic: "الزلزلة", dutch: "Az-Zalzalah", file: "099" },
{ number: 100, arabic: "العاديات", dutch: "Al-Aadiyaat", file: "100" },
{ number: 101, arabic: "القارعة", dutch: "Al-Qaari'ah", file: "101" },
{ number: 102, arabic: "التكاثر", dutch: "At-Takaathur", file: "102" },
{ number: 103, arabic: "العصر", dutch: "Al-Asr", file: "103" },
{ number: 104, arabic: "الهمزة", dutch: "Al-Humazah", file: "104" },
{ number: 105, arabic: "الفيل", dutch: "Al-Feel", file: "105" },
{ number: 106, arabic: "قريش", dutch: "Quraysh", file: "106" },
{ number: 107, arabic: "الماعون", dutch: "Al-Maa'oon", file: "107" },
{ number: 108, arabic: "الكوثر", dutch: "Al-Kawthar", file: "108" },
{ number: 109, arabic: "الكافرون", dutch: "Al-Kaafiroon", file: "109" },
{ number: 110, arabic: "النصر", dutch: "An-Nasr", file: "110" },
{ number: 111, arabic: "المسد", dutch: "Al-Masad", file: "111" },
{ number: 112, arabic: "الإخلاص", dutch: "Al-Ikhlaas", file: "112" },
{ number: 113, arabic: "الفلق", dutch: "Al-Falaq", file: "113" },
{ number: 114, arabic: "الناس", dutch: "An-Naas", file: "114" }
];
}
getReciterList() {
return [
{ id: "abdulbaset_mujawwad", name: "AbdulBaset AbdulSamad", arabic: "عبد الباسط عبد الصمد" },
{ id: "muhammad_ayyoob_hq", name: "Muhammad Ayyoob", arabic: "محمد أيوب" },
{ id: "abdurrahmaan_as-sudays", name: "Abdur-Rahman as-Sudais", arabic: "عبد الرحمن السديس" },
{ id: "maher_almu3aiqly/year1440", name: "Maher Al-Muaiqly", arabic: "ماهر المعيقلي" },
{ id: "fares", name: "Fares Abbad", arabic: "فارس عباد" },
{ id: "ahmed_ibn_3ali_al-3ajamy", name: "Ahmed ibn Ali al-Ajmy", arabic: "أحمد بن علي العجمي" },
{ id: "sa3d_al-ghaamidi/complete", name: "Saad Al-Ghamdi", arabic: "سعد الغامدي" },
{ id: "sa3ood_al-shuraym", name: "Sa'ud ash-Shuraym", arabic: "سعود الشريم" },
{ id: "khaalid_al-qahtaanee", name: "Khalid al-Qahtani", arabic: "خالد القحطاني" },
{ id: "mahmood_khaleel_al-husaree_iza3a", name: "Mahmoud Khaleel Al-Husary", arabic: "محمود خليل الحصري" },
{ id: "minshawi_mujawwad", name: "Muhammad Siddiq al-Minshawi", arabic: "محمد صديق المنشاوي" },
{ id: "abu_bakr_ash-shatri_tarawee7", name: "Abu Bakr al-Shatri", arabic: "أبو بكر الشاطري" }
];
}
setupEventListeners() {
// Reciteur selectie
document.getElementById('reciterSelect').addEventListener('change', (e) => {
this.currentReciter = e.target.value;
this.updateReciterDisplay();
console.log('Reciter selected:', this.currentReciter);
});
// Zoekfunctionaliteit
document.getElementById('suraSearch').addEventListener('input', (e) => {
this.filterSuras(e.target.value);
});
// Player controls
document.getElementById('playBtn').addEventListener('click', () => this.togglePlay());
document.getElementById('prevBtn').addEventListener('click', () => this.previousSura());
document.getElementById('nextBtn').addEventListener('click', () => this.nextSura());
// Direct play button
document.getElementById('directPlayBtn').addEventListener('click', () => this.directPlay());
// Progress bar
document.getElementById('progressBar').addEventListener('input', (e) => {
this.seekTo(e.target.value);
});
}
populateReciters() {
const select = document.getElementById('reciterSelect');
console.log('Populating reciters...', this.reciters);
this.reciters.forEach(reciter => {
const option = document.createElement('option');
option.value = reciter.id;
option.textContent = `${reciter.name} - ${reciter.arabic}`;
select.appendChild(option);
});
console.log('Reciters populated, total options:', select.children.length);
}
renderSuraGrid() {
const grid = document.getElementById('suraGrid');
grid.innerHTML = '';
this.suras.forEach((sura, index) => {
const suraItem = document.createElement('div');
suraItem.className = 'sura-item';
suraItem.dataset.index = index;
suraItem.innerHTML = `
<div class="sura-number">Sura ${sura.number}</div>
<div class="sura-name-arabic">${sura.arabic}</div>
<div class="sura-name-dutch">${sura.dutch}</div>
`;
suraItem.addEventListener('click', () => this.selectSura(index));
grid.appendChild(suraItem);
});
}
filterSuras(searchTerm) {
const items = document.querySelectorAll('.sura-item');
const term = searchTerm.toLowerCase();
items.forEach((item, index) => {
const sura = this.suras[index];
const matches = sura.dutch.toLowerCase().includes(term) ||
sura.arabic.includes(term) ||
sura.number.toString().includes(term);
item.style.display = matches ? 'block' : 'none';
});
}
selectSura(index) {
if (!this.currentReciter) {
alert('Selecteer eerst een reciteur');
return;
}
this.currentIndex = index;
this.updateActiveSura();
this.loadCurrentSura();
}
updateActiveSura() {
document.querySelectorAll('.sura-item').forEach((item, index) => {
item.classList.toggle('active', index === this.currentIndex);
});
}
clearLoadingTimeout() {
if (this.loadingTimeout) {
clearTimeout(this.loadingTimeout);
this.loadingTimeout = null;
}
}
hideLoading() {
this.clearLoadingTimeout();
document.querySelector('.player-section').classList.remove('loading');
}
showLoading() {
document.querySelector('.player-section').classList.add('loading');
// Timeout na 10 seconden
this.loadingTimeout = setTimeout(() => {
console.log('Loading timeout - hiding loading state');
this.hideLoading();
}, 10000);
}
loadCurrentSura() {
const sura = this.suras[this.currentIndex];
const url = `https://download.quranicaudio.com/quran/${this.currentReciter}/${sura.file}.mp3`;
console.log('Loading sura:', url);
// Reset audio element
this.audio.pause();
this.audio.currentTime = 0;
// Show loading state
this.showLoading();
// Set crossorigin attribute voor CORS
this.audio.crossOrigin = "anonymous";
this.audio.preload = "metadata";
this.audio.src = url;
this.updateSuraInfo();
// Force load
this.audio.load();
// Auto-play als er al iets speelde
if (this.isPlaying) {
// Wacht even voordat we proberen af te spelen
setTimeout(() => {
this.audio.play().catch(e => {
console.log('Auto-play geblokkeerd:', e);
this.isPlaying = false;
this.updatePlayButton();
this.hideLoading();
});
}, 500);
}
}
updateSuraInfo() {
const sura = this.suras[this.currentIndex];
const reciter = this.reciters.find(r => r.id === this.currentReciter);
document.getElementById('currentSuraName').textContent = `${sura.arabic} - ${sura.dutch}`;
document.getElementById('currentReciter').textContent = reciter ? reciter.name : 'Onbekende reciteur';
}
updateReciterDisplay() {
const reciter = this.reciters.find(r => r.id === this.currentReciter);
if (reciter && this.suras[this.currentIndex]) {
this.updateSuraInfo();
}
}
togglePlay() {
if (!this.currentReciter || !this.audio.src) {
alert('Selecteer eerst een reciteur en sura');
return;
}
if (this.isPlaying) {
this.audio.pause();
} else {
// Toon loading als audio nog niet klaar is
if (this.audio.readyState < 3) {
this.showLoading();
}
this.audio.play().catch(e => {
console.error('Fout bij afspelen:', e);
this.hideLoading();
alert('Kon audio niet afspelen. Controleer je internetverbinding.');
});
}
}
updatePlayButton() {
const playBtn = document.getElementById('playBtn');
const icon = playBtn.querySelector('.material-icons');
icon.textContent = this.isPlaying ? 'pause' : 'play_arrow';
}
previousSura() {
if (this.currentIndex > 0) {
this.selectSura(this.currentIndex - 1);
}
}
nextSura() {
if (this.currentIndex < this.suras.length - 1) {
this.selectSura(this.currentIndex + 1);
}
}
seekTo(percentage) {
if (this.audio.duration) {
this.audio.currentTime = (this.audio.duration * percentage) / 100;
}
}
directPlay() {
if (!this.currentReciter || this.currentIndex === undefined) {
alert('Selecteer eerst een reciteur en sura');
return;
}
const sura = this.suras[this.currentIndex];
const url = `https://download.quranicaudio.com/quran/${this.currentReciter}/${sura.file}.mp3`;
console.log('Opening direct play:', url);
window.open(url, '_blank');
}
showDirectPlayOption() {
document.getElementById('directPlayBtn').style.display = 'flex';
}
setupAudioEvents() {
this.audio.addEventListener('loadstart', () => {
console.log('Audio loading started');
this.showLoading();
});
this.audio.addEventListener('loadedmetadata', () => {
console.log('Audio metadata loaded');
this.hideLoading();
});
this.audio.addEventListener('loadeddata', () => {
console.log('Audio data loaded');
this.hideLoading();
});
this.audio.addEventListener('canplay', () => {
console.log('Audio can play');
this.hideLoading();
});
this.audio.addEventListener('canplaythrough', () => {
console.log('Audio can play through');
this.hideLoading();
});
this.audio.addEventListener('play', () => {
console.log('Audio started playing');
this.isPlaying = true;
this.updatePlayButton();
this.hideLoading();
document.querySelectorAll('.sura-item')[this.currentIndex]?.classList.add('playing');
});
this.audio.addEventListener('pause', () => {
console.log('Audio paused');
this.isPlaying = false;
this.updatePlayButton();
document.querySelectorAll('.sura-item')[this.currentIndex]?.classList.remove('playing');
});
this.audio.addEventListener('timeupdate', () => {
if (this.audio.duration) {
const progress = (this.audio.currentTime / this.audio.duration) * 100;
document.getElementById('progressBar').value = progress;
document.getElementById('currentTime').textContent = this.formatTime(this.audio.currentTime);
document.getElementById('totalTime').textContent = this.formatTime(this.audio.duration);
}
});
this.audio.addEventListener('ended', () => {
console.log('Audio ended');
this.nextSura();
});
this.audio.addEventListener('error', (e) => {
console.error('Audio error:', e, this.audio.error);
this.hideLoading();
this.showDirectPlayOption(); // Toon direct play optie bij fout
let errorMsg = 'Fout bij laden van audio.';
if (this.audio.error) {
switch(this.audio.error.code) {
case 1:
errorMsg = 'Audio laden afgebroken.';
break;
case 2:
errorMsg = 'Netwerk fout bij laden audio.';
break;
case 3:
errorMsg = 'Audio formaat niet ondersteund.';
break;
case 4:
errorMsg = 'Audio bestand niet gevonden.';
break;
}
}
alert(errorMsg + ' Probeer de "Direct afspelen" knop of een andere reciteur.');
});
this.audio.addEventListener('waiting', () => {
console.log('Audio waiting/buffering');
this.showLoading();
});
this.audio.addEventListener('playing', () => {
console.log('Audio playing (after buffering)');
this.hideLoading();
});
this.audio.addEventListener('stalled', () => {
console.log('Audio stalled');
this.hideLoading();
});
this.audio.addEventListener('suspend', () => {
console.log('Audio suspended');
this.hideLoading();
});
this.audio.addEventListener('abort', () => {
console.log('Audio aborted');
this.hideLoading();
});
// Timeout voor loading state
this.audio.addEventListener('loadstart', () => {
setTimeout(() => {
if (document.querySelector('.player-section').classList.contains('loading')) {
console.log('Loading took too long, showing direct play option');
this.hideLoading();
this.showDirectPlayOption();
}
}, 8000); // 8 seconden timeout
});
}
formatTime(seconds) {
if (isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
}
// Initialiseer de Quran speler wanneer de pagina geladen is
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, initializing QuranPlayer...');
new QuranPlayer();
});

View File

@ -0,0 +1,956 @@
:root {
--bg-light: #f7fafc;
--bg-dark: #162447;
--text-light: #222;
--text-dark: #fff;
--accent: #fff; /* goud */
--accent-blue: #222; /* zwart voor knoppen */
--panel-radius: 0rem;
--panel-shadow: 0 8px 32px 0 rgba(0,0,0,0.25);
--tijdkleur-light: #222;
--tijdkleur-dark: #fff;
--icoon-light: #222;
--icoon-dark: #fff;
}
html.light {
--bg: #f7fafc;
--weather-temp: #222;
--panel-bg: #fff;
--panel-text: #222;
--accent: #fff;
--accent-blue: #f0f0f0;
--panel-border: #e0e6ed;
--list-bg: #f7fafc;
--list-border: #e0e6ed;
--active-bg: #fffbe6;
--tijdkleur: var(--tijdkleur-light);
--icoon-kleur: var(--icoon-light);
--card-bg: #ffffff;
--border-color: #e0e6ed;
--primary-color: #fff;
--text-color: #222;
--text-secondary: #666;
}
html.dark, body.dark {
--bg: #111;
--text: #fff;
--panel-bg: #181818cc;
--panel-text: #fff;
--accent: #fff;
--accent-blue: #222;
--panel-border: #222;
--list-bg: rgba(255,255,255,0.04);
--list-border: #222;
--active-bg: rgba(234, 179, 8, 0.08);
--tijdkleur: var(--tijdkleur-dark);
--icoon-kleur: var(--icoon-dark);
--card-bg: rgba(255, 255, 255, 0.1);
--border-color: rgba(255, 255, 255, 0.2);
--primary-color: #fff;
--text-color: #fff;
--text-secondary: #bbb;
}
body, html {
font-family: 'Lato', Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
}
body {
margin: 0;
padding: 0;
display: flex;
height: 100vh;
overflow: hidden;
}
/* Specifieke styling voor settings pagina */
body.settings-page {
overflow-y: auto;
height: auto;
min-height: 100vh;
}
.app {
display: flex;
width: 100%;
height: 100%;
}
.left {
flex: 1;
background: url('/static/moskee.png') center/cover no-repeat;
position: relative;
}
.overlay {
background: rgba(0, 0, 0, 0.3);
color: var(--text);
height: 100%;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 2rem;
position: relative;
}
.tijdstip {
margin-bottom: 2rem;
}
.tijdstip #current-time {
font-family: 'Cairo', Arial, sans-serif;
font-size: 6rem;
font-weight: bold;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
color: var(--accent);
text-shadow: 2px 2px 8px #000;
}
.tijdstip .datum {
font-size: 1.5rem;
color: #e0e6ed;
}
.datum-sectie {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.gregorian-datum {
font-size: 1.4rem;
color: #e0e6ed;
font-family: 'Lato', Arial, sans-serif;
font-weight: 400;
}
.hijri-datum-arabic {
font-size: 1.3rem;
color: var(--accent);
font-family: 'Cairo', Arial, sans-serif;
font-weight: 600;
direction: rtl;
text-align: left;
text-shadow: 1px 1px 3px rgba(0,0,0,0.5);
}
.hijri-datum-dutch {
font-size: 1.1rem;
color: #c0c6d0;
font-family: 'Lato', Arial, sans-serif;
font-weight: 300;
font-style: italic;
}
.huidig-gebed {
margin-top: auto;
width: 100%;
max-width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
z-index: 2;
padding-bottom: 2rem;
}
.huidig-gebed .naam {
font-family: 'Cairo', Arial, sans-serif;
font-size: 4rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--accent);
}
#countdown {
font-size: 6rem;
font-weight: bold;
margin-top: 0.5rem;
color: #fff;
text-shadow: 2px 2px 8px #000;
}
.right {
flex: 1;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
background: var(--panel-bg);
color: var(--panel-text);
border-radius: var(--panel-radius) 0 0 var(--panel-radius);
box-shadow: var(--panel-shadow);
min-width: 350px;
/* max-width: 600px; */
border: 1px solid var(--panel-border);
position: relative;
}
#hadith-dua-pane {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
blockquote {
font-style: italic;
font-size: 2.4rem;
background: none;
border: none;
margin: 2rem 0 0.5rem 0;
padding: 0;
color: var(--text);
text-align: center;
font-family: 'Cairo', Arial, sans-serif;
line-height: 1.3;
}
blockquote footer {
font-size: 1.3rem;
color: var(--accent);
margin-top: 1rem;
text-align: right;
font-family: 'Lato', Arial, sans-serif;
}
.gebedstijden {
list-style: none;
padding: 0;
margin: 2rem 0;
border-radius: 1rem;
background: var(--list-bg);
box-shadow: 0 2px 8px 0 rgba(31, 64, 104, 0.10);
}
.gebedstijden li {
display: flex;
justify-content: space-between;
padding: 0.7rem 1.2rem;
border-bottom: 1px solid var(--list-border);
font-size: 1.3rem;
font-family: 'Lato', Arial, sans-serif;
}
.gebedstijden li.active {
font-weight: bold;
color: var(--accent);
background: var(--active-bg);
border-left: 4px solid var(--accent);
}
.icoontjes {
display: flex;
gap: 1.5rem;
font-size: 1.7rem;
margin-top: auto;
opacity: 0.8;
}
#themeToggle {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
font-size: 1.5rem;
border: none;
cursor: pointer;
z-index: 99;
color: var(--accent);
text-shadow: 1px 1px 4px #000;
}
.switch-buttons {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.switch-buttons button {
background: var(--accent-blue);
color: var(--text);
border: none;
padding: 0.7rem 2rem;
border-radius: 1.2rem 1.2rem 0 0;
font-size: 1.1rem;
font-family: 'Cairo', Arial, sans-serif;
cursor: pointer;
opacity: 0.8;
transition: background 0.2s, opacity 0.2s, color 0.2s;
box-shadow: 0 2px 8px 0 rgba(31, 64, 104, 0.10);
}
.switch-buttons button.active {
background: var(--accent);
color: var(--bg);
opacity: 1;
}
@media (max-width: 900px) {
.app {
flex-direction: column;
}
.right {
border-radius: var(--panel-radius) var(--panel-radius) 0 0;
min-width: unset;
max-width: unset;
width: 100%;
padding: 1.2rem;
}
.left {
min-height: 40vh;
}
}
@media (max-width: 600px) {
.tijdstip #current-time, #countdown {
font-size: 2.2rem;
}
.huidig-gebed .naam {
font-size: 1.2rem;
}
.right {
padding: 0.7rem;
}
blockquote {
font-size: 1.2rem;
}
.gregorian-datum {
font-size: 1.1rem;
}
.hijri-datum-arabic {
font-size: 1rem;
}
.hijri-datum-dutch {
font-size: 0.9rem;
}
}
.right.new-layout {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
background: var(--panel-bg);
color: var(--panel-text);
border-radius: var(--panel-radius) 0 0 var(--panel-radius);
box-shadow: var(--panel-shadow);
min-width: 350px;
/* max-width: 600px; */
border: 1px solid var(--panel-border);
position: relative;
padding: 0;
}
.hadith-tijden-row {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
}
.hadith-center {
flex: 1 1 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2.5rem 2rem 2.5rem 2.5rem;
}
.hadith-center blockquote {
font-size: 2.2rem;
text-align: left;
color: var(--text);
margin: 0;
padding: 0;
background: none;
border: none;
font-family: 'Cairo', Arial, sans-serif;
line-height: 1.3;
}
.hadith-center blockquote footer {
font-size: 1.1rem;
color: var(--accent);
margin-top: 1.2rem;
text-align: left;
font-family: 'Lato', Arial, sans-serif;
}
.vertical-tijden {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 90px;
background: none;
/* padding: 2.5rem 1.2rem 2.5rem 0; */
height: 100%;
}
.gebedstijden-verticaal {
list-style: none;
padding: 0;
margin: 0 0 2.5rem 0;
width: 100%;
}
.gebedstijden-verticaal li {
display: flex;
flex-direction: column;
align-items: flex-end;
padding: 0.3rem 0;
font-size: 1.1rem;
color: #bbb;
font-family: 'Lato', Arial, sans-serif;
transition: color 0.2s, font-weight 0.2s;
}
.gebedstijden-verticaal li.active {
color: var(--accent);
font-weight: bold;
font-size: 1.2rem;
}
.gebedstijden-verticaal .naam {
font-size: 0.95em;
letter-spacing: 0.05em;
}
.gebedstijden-verticaal .tijd {
font-size: 0.95em;
opacity: 0.8;
}
.vertical-icoontjes {
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: center;
margin-top: 1.5rem;
font-size: 1.6rem;
opacity: 0.8;
}
@media (max-width: 900px) {
.hadith-tijden-row {
flex-direction: column;
}
.vertical-tijden {
flex-direction: row;
min-width: unset;
padding: 1.2rem 0 0 0;
width: 100%;
justify-content: center;
}
.gebedstijden-verticaal {
flex-direction: row;
display: flex;
width: auto;
margin: 0 1.5rem 0 0;
}
.gebedstijden-verticaal li {
align-items: center;
padding: 0 0.7rem;
}
.vertical-icoontjes {
flex-direction: row;
gap: 1.2rem;
margin-top: 0;
margin-left: 1rem;
}
}
.tijden-en-icoontjes {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.7rem;
}
.tijden-en-icoontjes li {
font-size: 1.25rem;
color: #fff;
opacity: 0.85;
font-family: 'Lato', Arial, sans-serif;
letter-spacing: 0.04em;
transition: color 0.2s, opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.tijden-en-icoontjes .tijd {
font-size: 1.25rem;
font-family: 'Lato', Arial, sans-serif;
color: #fff;
background: none;
border: none;
padding: 0;
margin: 0;
opacity: 0.85;
}
.tijden-en-icoontjes .icoon-link {
font-size: 1.35rem;
color: #fff;
text-decoration: none;
background: none;
border: none;
opacity: 0.85;
transition: color 0.2s, opacity 0.2s;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.tijden-en-icoontjes .icoon-link:hover {
color: var(--accent);
opacity: 1;
}
.tijden-rij {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: auto;
gap: 0.1rem;
}
.tijden-rij .naam {
font-size: 1.05rem;
color: var(--tijdkleur) !important;
font-family: 'Lato', Arial, sans-serif;
opacity: 0.85;
letter-spacing: 0.04em;
text-align: center;
margin-bottom: 0.1rem;
}
.tijden-rij .tijd {
font-size: 1.15rem;
color: var(--tijdkleur) !important;
font-family: 'Lato', Arial, sans-serif;
opacity: 0.85;
text-align: center;
}
.icoon-link .material-icons {
font-size: 1.7rem;
color: var(--icoon-kleur) !important;
vertical-align: middle;
display: inline-block;
line-height: 1;
transition: color 0.2s, opacity 0.2s;
}
.icoon-link:hover .material-icons {
color: #1e7d4d;
}
#themeToggle .material-icons {
color: var(--icoon-kleur) !important;
transition: color 0.2s;
}
/* Weer sectie styling */
.weather-section {
margin-top: auto;
padding: 1.2rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
border-radius: 0;
font-family: 'Cairo', Arial, sans-serif;
}
.weather-info {
text-align: left;
}
.weather-main {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 0.8rem;
}
.weather-temp {
font-size: 2rem;
font-weight: 700;
color: var(--tijdkleur);
font-family: 'Cairo', sans-serif;
opacity: 0.9;
}
.weather-desc {
font-size: 1rem;
color: var(--tijdkleur);
opacity: 0.7;
font-family: 'Lato', sans-serif;
text-transform: capitalize;
}
.weather-details {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 1rem;
margin-top: 0.8rem;
border-top: 1px solid var(--border-color);
padding-top: 0.8rem;
}
.weather-detail {
font-size: 0.9rem;
color: var(--tijdkleur);
opacity: 0.7;
flex: 1;
text-align: center;
font-family: 'Lato', sans-serif;
}
/* Responsive aanpassingen voor weer */
@media (max-width: 768px) {
.weather-section {
margin-top: 1rem;
padding: 1rem;
}
.weather-temp {
font-size: 1.8rem;
}
.weather-details {
flex-direction: row;
gap: 0.5rem;
}
}
/* Instellingen pagina styling */
.settings-container {
max-width: 800px;
margin: 0 auto;
padding: 1.5rem;
background: var(--panel-bg);
border-radius: var(--panel-radius);
border: 1px solid var(--panel-border);
box-shadow: var(--panel-shadow);
min-height: auto;
max-height: 100vh;
overflow-y: auto;
}
.settings-header {
margin-bottom: 1.5rem;
}
.settings-header h1 {
font-family: 'Cairo', sans-serif;
font-size: 2rem;
color: var(--primary-color);
text-align: center;
margin: 0.5rem 0;
font-weight: 700;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
margin-bottom: 1rem;
}
.back-link:hover {
color: var(--accent);
transform: translateX(-5px);
}
.back-link .material-icons {
font-size: 1.2rem;
}
.settings-form {
width: 100%;
}
.settings-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.form-row .form-group {
margin-bottom: 0;
}
.form-group label {
display: flex;
align-items: center;
gap: 0.6rem;
font-weight: 600;
color: var(--primary-color);
font-size: 1rem;
margin-bottom: 0.3rem;
}
.form-group label .material-icons {
font-size: 1.3rem;
color: var(--accent);
}
.form-group input[type="number"],
.form-group input[type="time"],
.form-group select {
width: 100%;
padding: 0.7rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--card-bg);
color: var(--text-color);
font-size: 0.95rem;
transition: all 0.3s ease;
}
.form-group input[type="number"]:focus,
.form-group input[type="time"]:focus,
.form-group select:focus {
outline: none;
border-color: #fff;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.audio-section {
background: var(--list-bg);
padding: 1.2rem;
border-radius: 8px;
border: 1px solid var(--border-color);
height: fit-content;
}
.audio-preview-section {
background: var(--list-bg);
padding: 1.2rem;
border-radius: 8px;
border: 1px solid var(--border-color);
height: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
}
.audio-row {
align-items: stretch;
margin-bottom: 1.5rem;
}
.audio-preview {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 0.6rem;
}
.preview-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
background: var(--accent);
color: var(--accent-blue);
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.preview-btn:hover {
background: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.preview-btn .material-icons {
font-size: 1.1rem;
}
.checkboxes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.8rem;
margin-top: 1rem;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.8rem;
background: var(--card-bg);
border-radius: 6px;
border: 1px solid var(--border-color);
transition: all 0.3s ease;
cursor: pointer;
min-height: 40px;
}
.checkbox-item:hover {
background: var(--active-bg);
border-color: var(--accent);
}
.checkbox-item input[type="checkbox"] {
width: auto;
margin: 0;
cursor: pointer;
accent-color: var(--accent);
}
.checkbox-item label {
margin: 0;
cursor: pointer;
font-weight: 500;
color: var(--text-color);
}
.button-group {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.btn-primary,
.btn-secondary {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.8rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: none;
letter-spacing: 0.5px;
}
.btn-primary {
background: var(--accent);
color: var(--accent-blue);
}
.btn-primary:hover {
background: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 255, 255, 0.3);
}
.btn-secondary {
background: var(--card-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--active-bg);
border-color: var(--accent);
transform: translateY(-2px);
}
.btn-primary .material-icons,
.btn-secondary .material-icons {
font-size: 1.1rem;
}
/* Responsive design voor instellingen */
@media (max-width: 768px) {
.settings-container {
margin: 0.5rem;
padding: 1rem;
min-height: auto;
max-height: 100vh;
overflow-y: auto;
}
.settings-header h1 {
font-size: 1.8rem;
}
.settings-section {
gap: 1rem;
}
.form-row {
grid-template-columns: 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.audio-row {
margin-bottom: 1rem;
}
.audio-section,
.audio-preview-section {
padding: 1rem;
}
.checkboxes {
grid-template-columns: 1fr;
gap: 0.6rem;
}
.button-group {
flex-direction: column;
align-items: center;
gap: 0.8rem;
margin-top: 1rem;
}
.btn-primary,
.btn-secondary {
width: 100%;
max-width: 300px;
justify-content: center;
}
}

View File

@ -0,0 +1,473 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Debug - Gebedstijden Tester</title>
<link rel="stylesheet" href="/static/style.css">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@700&family=Lato:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
.debug-container {
max-width: 1200px;
margin: 2rem auto;
padding: 2rem;
background: var(--panel-bg);
border-radius: 15px;
border: 1px solid var(--panel-border);
}
.debug-header {
text-align: center;
margin-bottom: 2rem;
}
.debug-header h1 {
color: var(--accent);
font-family: 'Cairo', sans-serif;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.debug-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.debug-panel {
background: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.debug-panel h3 {
color: var(--accent);
margin-bottom: 1rem;
font-family: 'Cairo', sans-serif;
}
.time-controls {
display: flex;
flex-direction: column;
gap: 1rem;
}
.time-input {
display: flex;
align-items: center;
gap: 1rem;
}
.time-input input {
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 5px;
background: var(--card-bg);
color: var(--text-color);
font-size: 1.1rem;
width: 120px;
}
.btn {
padding: 0.7rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
margin: 0.2rem;
}
.btn-primary {
background: var(--accent);
color: #1a1a2e;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.2);
color: var(--text-color);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.prayer-times-list {
list-style: none;
padding: 0;
margin: 0;
}
.prayer-times-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.7rem;
margin-bottom: 0.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.prayer-times-list li.active {
background: rgba(234, 179, 8, 0.2);
border-color: var(--accent);
}
.status-display {
background: rgba(0, 0, 0, 0.3);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
color: #00ff00;
}
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.back-link {
display: inline-block;
margin-bottom: 1rem;
color: var(--accent);
text-decoration: none;
font-weight: 600;
}
.back-link:hover {
color: #b8941f;
}
@media (max-width: 768px) {
.debug-grid {
grid-template-columns: 1fr;
}
.debug-container {
margin: 1rem;
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="debug-container">
<a href="/" class="back-link">← Terug naar hoofdpagina</a>
<div class="debug-header">
<h1>🔧 Debug Mode</h1>
<p>Test de adhaan functionaliteit door de tijd aan te passen</p>
</div>
<div class="debug-grid">
<div class="debug-panel">
<h3>⏰ Tijd Controles</h3>
<div class="status-display" id="statusDisplay">
Huidige tijd: <span id="currentDebugTime">--:--</span><br>
Volgende gebed: <span id="nextPrayerDebug">--</span><br>
Status: <span id="debugStatus">Normaal</span>
</div>
<div class="time-controls">
<div class="time-input">
<label>Stel tijd in:</label>
<input type="time" id="manualTime" />
<button class="btn btn-primary" onclick="setManualTime()">Zet Tijd</button>
</div>
<div class="quick-actions">
<button class="btn btn-secondary" onclick="adjustTime(-60)">-1 min</button>
<button class="btn btn-secondary" onclick="adjustTime(-10)">-10 sec</button>
<button class="btn btn-secondary" onclick="adjustTime(10)">+10 sec</button>
<button class="btn btn-secondary" onclick="adjustTime(60)">+1 min</button>
</div>
<div class="quick-actions">
<button class="btn btn-primary" onclick="setTimeToNextPrayer(-1)">1 min voor gebed</button>
<button class="btn btn-primary" onclick="setTimeToNextPrayer(0)">Exact gebedstijd</button>
<button class="btn btn-danger" onclick="resetTime()">Reset naar echte tijd</button>
</div>
</div>
</div>
<div class="debug-panel">
<h3>🕌 Gebedstijden Vandaag</h3>
<ul class="prayer-times-list" id="prayerTimesList">
{% for naam, tijd in gebedstijden.items() %}
<li data-prayer="{{ naam }}" data-time="{{ tijd[:5] }}">
<span>{{ naam }}</span>
<span>{{ tijd[:5] }}</span>
<button class="btn btn-secondary" onclick="setTimeToPrayer('{{ tijd[:5] }}', -1)">-1 min</button>
<button class="btn btn-primary" onclick="setTimeToPrayer('{{ tijd[:5] }}', 0)">Nu</button>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="debug-panel">
<h3>📊 Debug Log</h3>
<div class="status-display" id="debugLog" style="height: 200px; overflow-y: auto;">
Debug log wordt hier weergegeven...<br>
</div>
<button class="btn btn-secondary" onclick="clearLog()">Wis Log</button>
</div>
<div class="debug-panel">
<h3>🔊 Sonos Status</h3>
<div class="status-display" id="sonosStatus" style="height: 150px; overflow-y: auto;">
Sonos status wordt geladen...<br>
</div>
<div class="quick-actions">
<button class="btn btn-primary" onclick="checkSonosStatus()">🔄 Ververs Status</button>
<button class="btn btn-danger" onclick="stopAllSonos()">⏹️ Stop Alle Audio</button>
</div>
</div>
</div>
<script>
let debugTimeOffset = 0; // Offset in seconden
let isDebugMode = false;
function log(message) {
const logElement = document.getElementById('debugLog');
const timestamp = new Date().toLocaleTimeString();
logElement.innerHTML += `[${timestamp}] ${message}<br>`;
logElement.scrollTop = logElement.scrollHeight;
}
function clearLog() {
document.getElementById('debugLog').innerHTML = 'Debug log gewist...<br>';
}
function getDebugTime() {
const now = new Date();
if (isDebugMode) {
now.setTime(now.getTime() + (debugTimeOffset * 1000));
}
return now;
}
function updateDebugDisplay() {
const debugTime = getDebugTime();
const timeStr = debugTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
document.getElementById('currentDebugTime').textContent = timeStr;
// Update status
const statusElement = document.getElementById('debugStatus');
statusElement.textContent = isDebugMode ? 'Debug Mode Actief' : 'Normaal';
statusElement.style.color = isDebugMode ? '#ff6b6b' : '#00ff00';
// Check voor gebedstijd
checkPrayerTime(timeStr);
}
function checkPrayerTime(currentTime) {
const prayerTimes = {{ gebedstijden | tojson }};
let nextPrayer = null;
let isExactTime = false;
for (const [name, time] of Object.entries(prayerTimes)) {
const prayerTime = time.substring(0, 5);
if (prayerTime === currentTime) {
isExactTime = true;
log(`🕌 ADHAAN TIJD! ${name} (${prayerTime})`);
// Trigger adhaan
triggerAdhaan(name);
}
if (prayerTime > currentTime && !nextPrayer) {
nextPrayer = `${name} (${prayerTime})`;
}
}
if (!nextPrayer) {
// Volgende dag
const firstPrayer = Object.entries(prayerTimes)[0];
nextPrayer = `${firstPrayer[0]} (${firstPrayer[1].substring(0, 5)}) - morgen`;
}
document.getElementById('nextPrayerDebug').textContent = nextPrayer;
// Update active prayer in list
document.querySelectorAll('.prayer-times-list li').forEach(li => {
li.classList.remove('active');
if (li.dataset.time === currentTime) {
li.classList.add('active');
}
});
}
function triggerAdhaan(prayerName) {
// Speel adhaan af
const audio = document.getElementById('adhanAudio');
if (audio) {
audio.play().catch(e => log(`Fout bij afspelen audio: ${e.message}`));
}
// Verstuur naar Sonos (als niet gemute)
fetch('/api/debug-adhaan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prayer: prayerName, time: getDebugTime().toLocaleTimeString() })
})
.then(response => response.json())
.then(data => {
if (data.success) {
log(`✅ Adhaan verzonden naar Sonos voor ${prayerName}`);
} else {
log(`❌ Fout bij Sonos: ${data.error}`);
}
})
.catch(e => log(`❌ Netwerk fout: ${e.message}`));
}
function setManualTime() {
const timeInput = document.getElementById('manualTime');
if (!timeInput.value) return;
const [hours, minutes] = timeInput.value.split(':');
const now = new Date();
const targetTime = new Date();
targetTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
debugTimeOffset = Math.floor((targetTime - now) / 1000);
isDebugMode = true;
log(`⏰ Tijd handmatig ingesteld op ${timeInput.value}`);
updateDebugDisplay();
}
function adjustTime(seconds) {
debugTimeOffset += seconds;
isDebugMode = true;
const sign = seconds > 0 ? '+' : '';
log(`⏰ Tijd aangepast: ${sign}${seconds} seconden`);
updateDebugDisplay();
}
function setTimeToPrayer(prayerTime, offsetMinutes) {
const [hours, minutes] = prayerTime.split(':');
const now = new Date();
const targetTime = new Date();
targetTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
targetTime.setTime(targetTime.getTime() + (offsetMinutes * 60 * 1000));
debugTimeOffset = Math.floor((targetTime - now) / 1000);
isDebugMode = true;
const offsetText = offsetMinutes === 0 ? 'exact' : `${offsetMinutes} min ervoor`;
log(`🕌 Tijd ingesteld op ${prayerTime} (${offsetText})`);
updateDebugDisplay();
}
function setTimeToNextPrayer(offsetMinutes) {
const prayerTimes = {{ gebedstijden | tojson }};
const currentTime = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
for (const [name, time] of Object.entries(prayerTimes)) {
const prayerTime = time.substring(0, 5);
if (prayerTime > currentTime) {
setTimeToPrayer(prayerTime, offsetMinutes);
return;
}
}
// Als geen gebed meer vandaag, neem eerste van morgen
const firstPrayer = Object.entries(prayerTimes)[0];
setTimeToPrayer(firstPrayer[1].substring(0, 5), offsetMinutes);
}
function resetTime() {
debugTimeOffset = 0;
isDebugMode = false;
log(`🔄 Tijd gereset naar echte tijd`);
updateDebugDisplay();
}
function checkSonosStatus() {
log('🔄 Sonos status controleren...');
fetch('/api/sonos-status')
.then(response => response.json())
.then(data => {
const statusElement = document.getElementById('sonosStatus');
if (data.success) {
let statusHtml = `Sonos API: ${data.api_ip}<br><br>`;
for (const [zone, info] of Object.entries(data.zones)) {
const statusColor = info.status === 'online' ? '#00ff00' : '#ff6b6b';
statusHtml += `<span style="color: ${statusColor}">● ${zone}</span><br>`;
statusHtml += ` Status: ${info.status}<br>`;
if (info.status === 'online') {
statusHtml += ` Afspelen: ${info.playbackState}<br>`;
statusHtml += ` Track: ${info.currentTrack}<br>`;
statusHtml += ` Volume: ${info.volume}%<br>`;
} else if (info.error) {
statusHtml += ` Fout: ${info.error}<br>`;
}
statusHtml += '<br>';
}
statusElement.innerHTML = statusHtml;
log('✅ Sonos status bijgewerkt');
} else {
statusElement.innerHTML = `❌ Fout: ${data.error}`;
log(`❌ Sonos status fout: ${data.error}`);
}
})
.catch(error => {
log(`❌ Netwerk fout bij Sonos status: ${error.message}`);
document.getElementById('sonosStatus').innerHTML = `❌ Netwerk fout: ${error.message}`;
});
}
function stopAllSonos() {
log('⏹️ Alle Sonos audio stoppen...');
fetch('/api/sonos-stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(data => {
if (data.success) {
log(`✅ ${data.message}`);
// Ververs status na stoppen
setTimeout(checkSonosStatus, 1000);
} else {
log(`❌ Fout bij stoppen: ${data.error}`);
}
})
.catch(error => {
log(`❌ Netwerk fout bij stoppen: ${error.message}`);
});
}
// Update elke seconde
setInterval(updateDebugDisplay, 1000);
// Initiële updates
updateDebugDisplay();
checkSonosStatus();
log('🔧 Debug mode gestart');
</script>
<!-- Hidden audio element for testing -->
<audio id="adhanAudio" src="/static/clips/{{ settings['audio_clip'] }}" preload="auto"></audio>
</body>
</html>

View File

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Gebedstijden Display</title>
<link rel="stylesheet" href="/static/style.css">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@700&family=Lato:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div class="app">
<!-- Dark/Light toggle -->
<button id="themeToggle" title="Schakel modus"><span id="themeIcon" class="material-icons">brightness_6</span></button>
<div class="left">
<div class="overlay">
<div class="tijdstip">
<div id="current-time">--:--</div>
<div class="datum-sectie">
<div class="gregorian-datum">{{ date_info.gregorian_full }}</div>
<div class="hijri-datum-arabic">{{ date_info.hijri_arabic }}</div>
<div class="hijri-datum-dutch">{{ date_info.hijri_dutch }}</div>
</div>
</div>
<div class="huidig-gebed">
<div class="naam">{{ next_name }}</div>
<div id="countdown">--:--:--</div>
</div>
</div>
</div>
<div class="right new-layout">
<div class="hadith-tijden-row">
<div class="hadith-center">
<blockquote>
"{{ hadith.text }}"
<footer>— {{ hadith.bron }}</footer>
</blockquote>
</div>
<div class="vertical-tijden">
<ul class="tijden-en-icoontjes">
{% for naam, tijd in gebedstijden.items() %}
<li class="tijden-rij">
<span class="naam">{{ naam }}</span>
<span class="tijd">{{ tijd[:5] }}</span>
</li>
{% endfor %}
<li><a href="/instellingen" title="Instellingen" class="icoon-link"><span class="material-icons">settings</span></a></li>
<li><a href="#" id="muteBtn" title="Mute" class="icoon-link"><span id="muteIcon" class="material-icons">volume_off</span></a></li>
<li><a href="/debug" title="Debug Mode" class="icoon-link"><span class="material-icons">bug_report</span></a></li>
<li><a href="/quran" title="Quran Speler" class="icoon-link"><span class="material-icons">menu_book</span></a></li>
</ul>
</div>
</div>
<!-- Weer sectie -->
<div class="weather-section">
<div class="weather-info">
<div class="weather-main">
<span class="weather-temp">{{ weather.temperature }}°C</span>
<span class="weather-desc">{{ weather.description }}</span>
</div>
<div class="weather-details">
<span class="weather-detail">Voelt als {{ weather.feels_like }}°C</span>
<span class="weather-detail">Vochtigheid {{ weather.humidity }}%</span>
<span class="weather-detail">Wind {{ weather.wind_speed }} km/h</span>
</div>
</div>
</div>
</div>
</div>
<audio id="adhanAudio" src="/static/clips/{{ settings['audio_clip'] }}" preload="auto"></audio>
<script>
// Prayer times data from server
window.prayerTimes = [
{% for naam, tijd in gebedstijden.items() %}
"{{ tijd[:5] }}"{% if not loop.last %},{% endif %}
{% endfor %}
];
window.prayerNames = [
{% for naam, tijd in gebedstijden.items() %}
"{{ naam }}"{% if not loop.last %},{% endif %}
{% endfor %}
];
const nextPrayerTime = "{{ next_time }}";
</script>
<script src="/static/countdown.js"></script>
<script>
window.onload = function () {
updateCurrentTime();
setInterval(updateCurrentTime, 60000);
setupThemeToggle();
startCountdowns(window.prayerTimes, window.prayerNames);
// Update weerdata elke 10 minuten
updateWeather();
setInterval(updateWeather, 600000); // 10 minuten
// Update hadith elke 30 seconden
updateHadith();
setInterval(updateHadith, 30000); // 30 seconden
};
// Functie om weerdata bij te werken
function updateWeather() {
fetch('/api/weather')
.then(response => response.json())
.then(data => {
document.querySelector('.weather-temp').textContent = data.temperature + '°C';
document.querySelector('.weather-desc').textContent = data.description;
document.querySelector('.weather-detail:nth-child(1)').textContent = `Voelt als ${data.feels_like}°C`;
document.querySelector('.weather-detail:nth-child(2)').textContent = `Vochtigheid ${data.humidity}%`;
document.querySelector('.weather-detail:nth-child(3)').textContent = `Wind ${data.wind_speed} km/h`;
})
.catch(error => {
console.log('Fout bij bijwerken weerdata:', error);
});
}
// Functie om hadith bij te werken
function updateHadith() {
fetch('/api/hadith')
.then(response => response.json())
.then(data => {
const hadithTextElement = document.querySelector('.hadith-center blockquote');
const hadithFooterElement = document.querySelector('.hadith-center blockquote footer');
if (hadithTextElement && hadithFooterElement) {
// Update de tekst en bron
hadithTextElement.innerHTML = `"${data.text}"<footer>— ${data.bron}</footer>`;
}
})
.catch(error => {
console.log('Fout bij bijwerken hadith:', error);
});
}
// Theme toggle icon dynamisch aanpassen
function updateThemeIcon() {
const html = document.documentElement;
const icon = document.getElementById('themeIcon');
if (html.className === 'light') {
icon.textContent = 'dark_mode';
} else {
icon.textContent = 'light_mode';
}
}
updateThemeIcon();
document.getElementById('themeToggle').addEventListener('click', updateThemeIcon);
// Mute functionaliteit
let muteStatus = false;
function setMuteIcon() {
const icon = document.getElementById('muteIcon');
icon.textContent = muteStatus ? 'volume_off' : 'volume_up';
}
function toggleMute(e) {
e.preventDefault();
muteStatus = !muteStatus;
setMuteIcon();
fetch('/api/mute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mute: muteStatus })
});
}
document.getElementById('muteBtn').addEventListener('click', toggleMute);
setMuteIcon();
</script>
</body>
</html>

View File

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Quran Speler - Gebedstijden Display</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/quran.css">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&family=Lato:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div class="app">
<!-- Dark/Light toggle -->
<button id="themeToggle" title="Schakel modus"><span id="themeIcon" class="material-icons">brightness_6</span></button>
<!-- Main Quran Content (twee kolommen) -->
<div class="quran-full-content">
<!-- Linker kolom - Player -->
<div class="quran-left-column">
<!-- Header -->
<div class="quran-header">
<h1>القرآن الكريم - Quran Speler</h1>
</div>
<!-- Reciter Selection -->
<div class="reciter-selection">
<label for="reciterSelect">Reciteur / القارئ:</label>
<select id="reciterSelect" class="custom-select">
<option value="">Selecteer een reciteur...</option>
</select>
</div>
<!-- Player Section -->
<div class="player-section">
<div class="current-playing">
<div class="sura-info">
<h2 id="currentSuraName">Selecteer een sura om af te spelen</h2>
<p id="currentReciter">Geen reciteur geselecteerd</p>
</div>
<div class="player-controls">
<button id="prevBtn" class="control-btn" title="Vorige">
<span class="material-icons">skip_previous</span>
</button>
<button id="playBtn" class="control-btn play-btn" title="Afspelen/Pauzeren">
<span class="material-icons">play_arrow</span>
</button>
<button id="nextBtn" class="control-btn" title="Volgende">
<span class="material-icons">skip_next</span>
</button>
<button id="directPlayBtn" class="control-btn" title="Direct afspelen in nieuw tabblad" style="display: none;">
<span class="material-icons">open_in_new</span>
</button>
</div>
<div class="progress-section">
<span id="currentTime">0:00</span>
<input type="range" id="progressBar" min="0" max="100" value="0" class="progress-bar">
<span id="totalTime">0:00</span>
</div>
</div>
</div>
</div>
<!-- Rechter kolom - Zoeken en Sura lijst -->
<div class="quran-right-column">
<!-- Search Section -->
<div class="search-section">
<label for="suraSearch">Zoek Sura / البحث عن السورة:</label>
<input type="text" id="suraSearch" placeholder="Zoek naar sura naam..." class="search-input">
</div>
<!-- Sura List -->
<div class="sura-list-section">
<h3>Sura's / السور</h3>
<div class="sura-grid" id="suraGrid">
<!-- Sura's worden hier dynamisch geladen -->
</div>
</div>
</div>
</div>
<!-- Zijmenu (exact hetzelfde als hoofdpagina) -->
<div class="right new-layout">
<div class="hadith-tijden-row">
<div class="vertical-tijden">
<ul class="tijden-en-icoontjes">
{% for naam, tijd in gebedstijden.items() %}
<li class="tijden-rij">
<span class="naam">{{ naam }}</span>
<span class="tijd">{{ tijd[:5] }}</span>
</li>
{% endfor %}
<li><a href="/instellingen" title="Instellingen" class="icoon-link"><span class="material-icons">settings</span></a></li>
<li><a href="#" id="muteBtn" title="Mute" class="icoon-link"><span id="muteIcon" class="material-icons">volume_off</span></a></li>
<li><a href="/debug" title="Debug Mode" class="icoon-link"><span class="material-icons">bug_report</span></a></li>
<li><a href="/" title="Terug naar hoofdpagina" class="icoon-link"><span class="material-icons">home</span></a></li>
</ul>
</div>
</div>
</div>
<!-- Audio Element -->
<audio id="quranAudio" preload="none"></audio>
</div>
<script src="/static/quran.js"></script>
<script>
// Theme toggle icon dynamisch aanpassen
function updateThemeIcon() {
const html = document.documentElement;
const icon = document.getElementById('themeIcon');
if (html.className === 'light') {
icon.textContent = 'dark_mode';
} else {
icon.textContent = 'light_mode';
}
}
document.getElementById('themeToggle').addEventListener('click', function() {
const html = document.documentElement;
html.className = html.className === 'light' ? '' : 'light';
localStorage.setItem('theme', html.className);
updateThemeIcon();
});
// Laad opgeslagen theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.className = savedTheme;
}
updateThemeIcon();
// Mute functionaliteit
let muteStatus = false;
function setMuteIcon() {
const icon = document.getElementById('muteIcon');
icon.textContent = muteStatus ? 'volume_off' : 'volume_up';
}
function toggleMute(e) {
e.preventDefault();
muteStatus = !muteStatus;
setMuteIcon();
fetch('/api/mute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mute: muteStatus })
});
}
document.getElementById('muteBtn').addEventListener('click', toggleMute);
setMuteIcon();
</script>
</body>
</html>

View File

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<title>Instellingen - Gebedstijden Display</title>
<link rel="stylesheet" href="/static/style.css">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@700&family=Lato:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="settings-page">
<div class="app">
<!-- Dark/Light toggle -->
<button id="themeToggle" title="Schakel modus"><span id="themeIcon" class="material-icons">brightness_6</span></button>
<div class="settings-container">
<div class="settings-header">
<a href="/" class="back-link" title="Terug naar hoofdpagina">
<span class="material-icons">arrow_back</span>
Terug naar hoofdpagina
</a>
<h1>Instellingen</h1>
</div>
<form method="POST" class="settings-form">
<div class="settings-section">
<div class="form-group">
<label>
<span class="material-icons">info</span>
Volume Instellingen:
</label>
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0.5rem 0;">
Stel verschillende volumes in voor overdag en 's avonds. Het systeem wisselt automatisch tussen deze volumes op basis van de ingestelde tijden.
</p>
</div>
<div class="form-row">
<div class="form-group">
<label for="volume_day">
<span class="material-icons">volume_up</span>
Volume Overdag (0100):
</label>
<input type="number" name="volume_day" value="{{ settings.volume_day or settings.volume }}" min="0" max="100" />
</div>
<div class="form-group">
<label for="volume_night">
<span class="material-icons">volume_down</span>
Volume Avond (0100):
</label>
<input type="number" name="volume_night" value="{{ settings.volume_night or (settings.volume // 2) }}" min="0" max="100" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="day_start">
<span class="material-icons">wb_sunny</span>
Dag begint om:
</label>
<input type="time" name="day_start" value="{{ settings.day_start or '07:00' }}" />
</div>
<div class="form-group">
<label for="night_start">
<span class="material-icons">nightlight</span>
Avond begint om:
</label>
<input type="time" name="night_start" value="{{ settings.night_start or '20:00' }}" />
</div>
</div>
<div class="form-group">
<label>
<span class="material-icons">speaker_group</span>
Sonos Zones:
</label>
<div class="checkboxes">
{% for zone in alle_zones %}
<div class="checkbox-item">
<input type="checkbox" name="zones" value="{{ zone }}" id="zone_{{ loop.index }}"
{% if zone in settings.zones %}checked{% endif %}>
<label for="zone_{{ loop.index }}">{{ zone }}</label>
</div>
{% endfor %}
</div>
</div>
<div class="form-row audio-row">
<div class="form-group audio-section">
<label for="audio_clip">
<span class="material-icons">music_note</span>
Adhaan Audio Bestand:
</label>
<select name="audio_clip" id="audioSelect">
{% for clip in clips %}
<option value="{{ clip }}" {% if clip == settings.audio_clip %}selected{% endif %}>
{% if clip == 'adhan1.mp3' %}Klassieke Adhaan
{% elif 'mekka' in clip %}Mekka Stijl
{% elif 'medina' in clip %}Medina Stijl
{% elif 'kort' in clip %}Korte Versie
{% elif 'traditioneel' in clip %}Traditioneel
{% else %}{{ clip }}
{% endif %}
</option>
{% endfor %}
</select>
</div>
<div class="form-group audio-preview-section">
<label>
<span class="material-icons">play_circle</span>
Audio Voorbeeld:
</label>
<div class="audio-preview">
<button type="button" class="preview-btn" onclick="previewAudio()">
<span class="material-icons">play_arrow</span>
Voorbeeld afspelen
</button>
<audio id="previewAudio" preload="none"></audio>
</div>
</div>
</div>
<div class="button-group">
<button type="submit" name="test_clip" class="btn-secondary">
<span class="material-icons">speaker</span>
Test op Sonos
</button>
<button type="submit" class="btn-primary">
<span class="material-icons">save</span>
Instellingen Opslaan
</button>
</div>
</div>
</form>
</div>
</div>
<script>
function previewAudio() {
const select = document.getElementById('audioSelect');
const audio = document.getElementById('previewAudio');
const selectedClip = select.value;
audio.src = `/static/clips/${selectedClip}`;
audio.play().catch(e => {
console.log('Audio afspelen mislukt:', e);
alert('Kon audio niet afspelen. Controleer of het bestand bestaat.');
});
}
// Auto-update preview audio source when selection changes
document.getElementById('audioSelect').addEventListener('change', function() {
const audio = document.getElementById('previewAudio');
audio.src = `/static/clips/${this.value}`;
});
// Set initial audio source
window.onload = function() {
const select = document.getElementById('audioSelect');
const audio = document.getElementById('previewAudio');
audio.src = `/static/clips/${select.value}`;
// Setup theme toggle
setupThemeToggle();
updateThemeIcon();
};
// Theme toggle functionaliteit (gekopieerd van index.html)
function setupThemeToggle() {
const toggleBtn = document.getElementById("themeToggle");
if (!toggleBtn) return;
const currentTheme = localStorage.getItem("theme") || "dark";
document.documentElement.className = currentTheme;
toggleBtn.addEventListener("click", function () {
const html = document.documentElement;
const newTheme = html.className === "light" ? "dark" : "light";
html.className = newTheme;
localStorage.setItem("theme", newTheme);
updateThemeIcon();
});
}
function updateThemeIcon() {
const html = document.documentElement;
const icon = document.getElementById('themeIcon');
if (icon) {
if (html.className === 'light') {
icon.textContent = 'dark_mode';
} else {
icon.textContent = 'light_mode';
}
}
}
</script>
</body>
</html>

30
docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
services:
adhan-webapp:
build:
context: /mnt/Main/config/adhan/adhan-webapp
container_name: adhan-webapp
ports:
- '5090:80'
restart: unless-stopped
volumes:
- /mnt/Main/config/adhan/adhan-webapp/settings.json:/app/settings.json
- /mnt/Main/config/adhan/adhan-webapp/static:/app/static:ro
- /mnt/Main/config/adhan/adhan-webapp/templates:/app/templates:ro
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment:
- FLASK_ENV=development
- FLASK_DEBUG=1
- PYTHONUNBUFFERED=1
- TZ=Europe/Amsterdam
sonos-api:
container_name: sonos-api
image: chrisns/docker-node-sonos-http-api
network_mode: host
restart: unless-stopped
volumes:
- /mnt/Main/config/adhan/sonos/settings.json:/app/settings.json:ro
- /mnt/Main/config/adhan/sonos/presets:/app/presets:ro
- /mnt/Main/config/adhan/sonos/cache:/app/cache:ro
- /mnt/Main/config/adhan/sonos/clips:/app/static/clips:ro
version: '3.8'

48
done Normal file
View File

@ -0,0 +1,48 @@
2025-05-25 16:18:07 - Afteller probleem opgelost: JavaScript initialisatie aangepast om gebedstijd correct door te geven
2025-05-25 16:22:29 - Docker configuratie aangepast voor betere caching en live updates: Dockerfile en docker-compose.yml bijgewerkt
2025-05-25 16:26:20 - Docker en Flask configuratie aangepast: nginx configuratie verwijderd en cache-control headers toegevoegd via Flask
2025-05-25 16:26:51 - Configuratie vereenvoudigd voor TrueNAS: Dockerfile, docker-compose.yml en app.py aangepast voor betere bestandspaden en permissies
2025-05-25 16:28:49 - Nederlandse hadiths toegevoegd: hadiths.json bestand gemaakt, app.py aangepast voor willekeurige hadith selectie, template en styling bijgewerkt
2025-05-25 16:31:42 - Tijd en afteller links vergroot en countdown hoger geplaatst via CSS-update
2025-05-25 16:32:44 - Afteltijd en gebedsnaam helemaal onderaan links geplaatst via CSS-update
2025-05-25 16:33:25 - CSS aangepast zodat afteller altijd volledig zichtbaar is onderaan, ook op kleinere schermen
2025-05-25 16:35:05 - CSS aangepast: .huidig-gebed nu onderaan via flexbox, altijd volledig zichtbaar binnen overlay
2025-05-25 16:39:20 - Switch functionaliteit toegevoegd voor hadith/dua en tijden aan de rechterkant, inclusief styling en JS
2025-05-25 16:40:13 - Hadith-tekst en bron vergroot en blockquote opmaak vereenvoudigd (geen achtergrond/rand meer)
2025-05-25 16:43:08 - Endpoint toegevoegd voor willekeurige hadith en JavaScript toegevoegd om elke 30 seconden een nieuwe hadith te tonen
2025-05-25 16:46:19 - Design vernieuwd: islamitische sfeer met warme kleuren, Google Fonts, afgeronde panelen, goudaccenten en moderne typografie
2025-05-25 16:47:43 - CSS volledig themavriendelijk gemaakt: dark/light mode werkt nu met alle kleuren en accenten
2025-05-25 20:46:58 - Thema aangepast: alle blauw-tinten vervangen door zwart/grijs, nu echt zwart-goud-wit design
2025-05-25 20:54:07 - adhan_cron.py script aangemaakt voor automatische adhaan via Sonos met settings.json
2025-05-25 20:55:59 - start_all.py toegevoegd en Dockerfile aangepast zodat Flask en adhan_cron samen in één container draaien
2025-05-25 21:00:34 - adhan_cron.py uitgebreid: logt nu altijd wanneer de cronjob draait en met welk tijdstip
2025-05-25 21:06:27 - adhan_cron.py aangepast: logt nu naar stdout én bestand, zichtbaar in Docker/TrueNAS logs
2025-05-25 21:15:35 - Hadith verticaal gecentreerd in het rechterpaneel via CSS (#hadith-dua-pane)
2025-05-25 21:21:26 - Layout aangepast: gebedstijden nu verticaal rechts van de hadith, mute en instellingen als verticale icoontjes eronder
2025-05-25 21:22:56 - Zijmenu aangepast: alleen tijden zonder seconden, alles in één uniforme verticale lijst met dezelfde stijl
2025-05-25 21:23:53 - Gebedsnamen toegevoegd boven de tijden in het zijmenu, alles netjes uitgelijnd in één verticale lijst
2025-05-25 21:24:55 - Breedte van tijden-rij vergroot en white-space toegevoegd zodat naam en tijd altijd naast elkaar staan
2025-05-25 21:25:39 - Tijden-rij aangepast: gebedsnaam nu boven de tijd, alles gecentreerd en gestapeld
2025-05-25 21:28:35 - Material Icons toegevoegd en icoon-stijl aangepast naar modern Android/Google stijl
2025-05-25 21:29:10 - Theme-toggle icoon vervangen door Material Icon, wisselt nu automatisch tussen dark_mode en light_mode
2025-05-25 21:30:31 - Kleur van theme-icoon, tijden en namen nu afhankelijk van dark/light theme via CSS-variabelen
2025-05-25 21:32:38 - Mute functionaliteit toegevoegd: icoon wisselt, status wordt opgeslagen, adhaan wordt niet afgespeeld als mute aan staat
2025-05-25 22:00:35 - Sonos-zones nu dynamisch opgehaald via de Sonos-API in instellingenpagina
Sun May 25 22:10:59 CEST 2025: Uitgebreide audio opties toegevoegd met preview functie
Sun May 25 22:13:01 CEST 2025: Instellingen pagina volledig vernieuwd met moderne UI
Sun May 25 22:13:01 CEST 2025: API endpoints toegevoegd voor dynamische updates
Sun May 25 22:13:01 CEST 2025: Configuratie bestand (config.py) aangemaakt voor eenvoudige setup
Sun May 25 22:13:01 CEST 2025: README.md bijgewerkt met installatie instructies
Sun May 25 22:18:12 CEST 2025: Weer-sectie verbeterd - details nu horizontaal naast elkaar met mooie styling
Sun May 25 22:20:27 CEST 2025: Islamitische kalender toegevoegd - toont nu zowel Gregoriaanse als Hijri datum in Nederlands en Arabisch
Sun May 25 22:25:25 CEST 2025: Debug scherm toegevoegd - tijd manipulatie voor adhaan testing met +/- knoppen en directe gebedstijd triggers
Sun May 25 22:34:29 CEST 2025: Sonos herstart probleem opgelost - debounce mechanisme toegevoegd, pause voor clip, betere error handling en Sonos status monitoring
2025-05-25 23:59:21 - Quran pagina layout aangepast naar twee-kolommen structuur met speler links en sura lijst rechts
2025-05-26 00:01:43 - Quran pagina layout en styling verbeterd om meer op het ontwerp te lijken
2025-05-26 00:03:05 - Rechtermenu styling aangepast om Quran theme variabelen te gebruiken voor correcte dark/light mode
2025-05-26 15:14:18 - Hadiths toegevoegd over dwanggedachten, obsessieve gedachten en geestelijke rust
2025-05-26 20:55:37 - Volume instellingen uitgebreid met dag/avond volumes en tijdsinstellingen
2025-05-26 20:58:10 - Settings layout verbeterd: elementen naast elkaar geplaatst voor beter gebruik van schermbreedte
2025-05-26 20:59:32 - Settings layout verder verfijnd: betere spacing, uitlijning en responsive design
2025-05-26 21:04:05 - Layout compacter gemaakt en scrolling toegevoegd aan settings pagina
2025-05-26 21:06:51 - Volume error opgelost in backend - oude volume veld vervangen door volume_day/volume_night

BIN
sonos/clips/adhan1.mp3 Normal file

Binary file not shown.

1
sonos/settings.json Normal file
View File

@ -0,0 +1 @@
{}